support LD_PRELOAD, and write version.h

6 files changed +206 −29

  • + tests/11_ld_preload/Makefile
  • + version.h.in
  • • CHANGELOG.md
  • • CMakeLists.txt
  • • doc/libtree.1
  • • libtree.c
1diff --git a/CHANGELOG.md b/CHANGELOG.md
2index dace919..6efed49 100644
3--- a/CHANGELOG.md
4+++ b/CHANGELOG.md
5@@ -1,3 +1,9 @@
6+# v3.2.0
7+- Add support for `LD_PRELOAD`: libraries listed in this environment variable
8+ are shown as dependencies of each top-level file, marked `[LD_PRELOAD]`.
9+- CMake build system
10+- Static executable
11+
12 # v3.1.1
13 - Build system portability fixes
14 - Fix make check exit code
15diff --git a/CMakeLists.txt b/CMakeLists.txt
16index acf8bdc..4964c4f 100644
17--- a/CMakeLists.txt
18+++ b/CMakeLists.txt
19@@ -1,14 +1,18 @@
20 cmake_minimum_required(VERSION 3.10)
21
22-project(libtree C)
23+project(libtree VERSION 3.2.0 LANGUAGES C)
24
25 set(CMAKE_C_STANDARD 99)
26 set(CMAKE_C_STANDARD_REQUIRED ON)
27
28 include(GNUInstallDirs)
29
30+configure_file(version.h.in version.h @ONLY)
31+
32 add_executable(libtree libtree.c)
33
34+target_include_directories(libtree PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
35+
36 target_compile_definitions(libtree PRIVATE _FILE_OFFSET_BITS=64)
37
38 target_compile_options(libtree PRIVATE
39diff --git a/doc/libtree.1 b/doc/libtree.1
40index ff87b85..ae9ca07 100644
41--- a/doc/libtree.1
42+++ b/doc/libtree.1
43@@ -43,6 +43,14 @@ All arguments after '--' are interpreted as paths, not flags.
44 .SH ENVIRONMENT
45 .B LD_LIBRARY_PATH
46 can be used to provide additional search paths.
47+.PP
48+.B LD_PRELOAD
49+can be used to specify additional libraries (separated by spaces and/or
50+colons) to be loaded before all others. These are shown as direct
51+dependencies of each top-level file, marked
52+.BR [LD_PRELOAD] ,
53+and are located using the same search rules as that file's regular
54+dependencies.
55 .SH AUTHOR
56 Harmen Stoppels <me@harmenstoppels.nl>
57 .SH "SEE ALSO"
58diff --git a/libtree.c b/libtree.c
59index aee4187..3753c6a 100644
60--- a/libtree.c
61+++ b/libtree.c
62@@ -10,7 +10,7 @@
63 #include <sys/utsname.h>
64 #include <unistd.h>
65
66-#define VERSION "3.2.0-dev"
67+#include "version.h"
68
69 #define ET_EXEC 2
70 #define ET_DYN 3
71@@ -176,9 +176,20 @@ typedef enum {
72 LD_LIBRARY_PATH,
73 RUNPATH,
74 LD_SO_CONF,
75- DEFAULT
76+ DEFAULT,
77+ LD_PRELOAD
78 } how_t;
79
80+/**
81+ * small_vec_u64 is an array that lives on the stack until it grows to the heap
82+ */
83+struct small_vec_u64_t {
84+ uint64_t buf[SMALL_VEC_SIZE];
85+ uint64_t *p;
86+ size_t n;
87+ size_t capacity;
88+};
89+
90 struct found_t {
91 how_t how;
92 // only set when found by in the rpath NOT of the direct parent. so, when
93@@ -233,6 +244,10 @@ struct libtree_state_t {
94 size_t default_paths_offset;
95 size_t ld_so_conf_offset;
96
97+ // Offsets into the string table of each LD_PRELOAD entry, stored as
98+ // separate null-terminated strings.
99+ struct small_vec_u64_t preload_offsets;
100+
101 // This is so we know we have to print a | or white space
102 // in the tree
103 char found_all_needed[MAX_RECURSION_DEPTH];
104@@ -240,16 +255,6 @@ struct libtree_state_t {
105
106 // Keep track of the files we've see
107
108-/**
109- * small_vec_u64 is an array that lives on the stack until it grows to the heap
110- */
111-struct small_vec_u64_t {
112- uint64_t buf[SMALL_VEC_SIZE];
113- uint64_t *p;
114- size_t n;
115- size_t capacity;
116-};
117-
118 static inline void utoa(char *str, size_t v) {
119 char *p = str;
120 do {
121@@ -418,7 +423,8 @@ static void apply_exclude_list(size_t *needed_not_found,
122 static int check_absolute_paths(size_t *needed_not_found,
123 struct small_vec_u64_t *needed_buf_offsets,
124 size_t depth, struct libtree_state_t *s,
125- struct compat_t compat) {
126+ struct compat_t compat, struct found_t reason,
127+ size_t extra_after) {
128 int exit_code = 0;
129 // First go over absolute paths in needed libs.
130 for (size_t i = 0; i < *needed_not_found;) {
131@@ -441,7 +447,7 @@ static int check_absolute_paths(size_t *needed_not_found,
132 // Include \0
133 memcpy(path, st->arr + needed_buf_offsets->p[i], len + 1);
134
135- s->found_all_needed[depth] = *needed_not_found <= 1;
136+ s->found_all_needed[depth] = *needed_not_found <= 1 && extra_after == 0;
137 char *err = NULL;
138
139 // If it is not an absolute path, we bail, cause it then starts to
140@@ -451,8 +457,7 @@ static int check_absolute_paths(size_t *needed_not_found,
141 err = " is not absolute";
142 exit_code = ERR_DEPENDENCY_NOT_FOUND;
143 } else {
144- int code = recurse(path, depth + 1, s, compat,
145- (struct found_t){.how = DIRECT});
146+ int code = recurse(path, depth + 1, s, compat, reason);
147 if (code == ERR_DEPENDENCY_NOT_FOUND)
148 exit_code = ERR_DEPENDENCY_NOT_FOUND;
149
150@@ -485,7 +490,7 @@ static int check_search_paths(struct found_t reason, size_t offset,
151 size_t *needed_not_found,
152 struct small_vec_u64_t *needed_buf_offsets,
153 size_t depth, struct libtree_state_t *s,
154- struct compat_t compat) {
155+ struct compat_t compat, size_t extra_after) {
156 int exit_code = 0;
157 char path[MAX_PATH_LENGTH];
158 char *path_end = path + MAX_PATH_LENGTH;
159@@ -529,7 +534,8 @@ static int check_search_paths(struct found_t reason, size_t offset,
160 // Otherwise append.
161 memcpy(search_path_end, st->arr + needed_buf_offsets->p[i],
162 soname_len + 1);
163- s->found_all_needed[depth] = *needed_not_found <= 1;
164+ s->found_all_needed[depth] =
165+ *needed_not_found <= 1 && extra_after == 0;
166
167 // And try to locate the lib.
168 int code = recurse(path, depth + 1, s, compat, reason);
169@@ -712,6 +718,9 @@ static void print_line(size_t depth, char *name, char *color_bold,
170 case LD_LIBRARY_PATH:
171 fputs("[LD_LIBRARY_PATH]", stdout);
172 break;
173+ case LD_PRELOAD:
174+ fputs("[LD_PRELOAD]", stdout);
175+ break;
176 case RUNPATH:
177 fputs("[runpath]", stdout);
178 break;
179@@ -740,9 +749,10 @@ static void print_line(size_t depth, char *name, char *color_bold,
180 static void print_error(size_t depth, size_t needed_not_found,
181 struct small_vec_u64_t *needed_buf_offsets,
182 char *runpath, struct libtree_state_t *s,
183- int no_def_lib) {
184+ int no_def_lib, size_t extra_after) {
185 for (size_t i = 0; i < needed_not_found; ++i) {
186- s->found_all_needed[depth] = i + 1 >= needed_not_found;
187+ s->found_all_needed[depth] =
188+ i + 1 >= needed_not_found && extra_after == 0;
189 tree_preamble(s, depth + 1);
190 if (s->color)
191 fputs(BOLD_RED, stdout);
192@@ -1318,9 +1328,79 @@ static int recurse(char *current_file, size_t depth, struct libtree_state_t *s,
193 if (needed_not_found && s->verbosity == 0)
194 apply_exclude_list(&needed_not_found, &needed_buf_offsets, s);
195
196+ // LD_PRELOAD only applies to the top-level object: those libraries are
197+ // loaded by the dynamic linker before anything else, using the same
198+ // search rules as the regular dependencies of the top-level object.
199+ struct small_vec_u64_t preload_buf_offsets;
200+ small_vec_u64_init(&preload_buf_offsets);
201+ size_t preload_not_found = 0;
202+
203+ if (depth == 0 && s->preload_offsets.n > 0) {
204+ for (size_t i = 0; i < s->preload_offsets.n; ++i)
205+ small_vec_u64_append(&preload_buf_offsets, s->preload_offsets.p[i]);
206+ preload_not_found = preload_buf_offsets.n;
207+
208+ if (preload_not_found)
209+ exit_code |= check_absolute_paths(
210+ &preload_not_found, &preload_buf_offsets, depth, s, curr_type,
211+ (struct found_t){.how = LD_PRELOAD}, needed_not_found);
212+
213+ if (preload_not_found && runpath == MAX_OFFSET_T) {
214+ for (int j = depth; j >= 0 && preload_not_found; --j) {
215+ if (s->rpath_offsets[j] == SIZE_MAX)
216+ continue;
217+
218+ exit_code |= check_search_paths(
219+ (struct found_t){.how = LD_PRELOAD}, s->rpath_offsets[j],
220+ &preload_not_found, &preload_buf_offsets, depth, s,
221+ curr_type, needed_not_found);
222+ }
223+ }
224+
225+ if (preload_not_found && s->ld_library_path_offset != SIZE_MAX) {
226+ exit_code |= check_search_paths(
227+ (struct found_t){.how = LD_PRELOAD}, s->ld_library_path_offset,
228+ &preload_not_found, &preload_buf_offsets, depth, s, curr_type,
229+ needed_not_found);
230+ }
231+
232+ if (preload_not_found && runpath != MAX_OFFSET_T) {
233+ exit_code |= check_search_paths(
234+ (struct found_t){.how = LD_PRELOAD}, runpath_buf_offset,
235+ &preload_not_found, &preload_buf_offsets, depth, s, curr_type,
236+ needed_not_found);
237+ }
238+
239+ if (preload_not_found && !no_def_lib) {
240+ exit_code |= check_search_paths(
241+ (struct found_t){.how = LD_PRELOAD}, s->ld_so_conf_offset,
242+ &preload_not_found, &preload_buf_offsets, depth, s, curr_type,
243+ needed_not_found);
244+ }
245+
246+ if (preload_not_found && !no_def_lib) {
247+ exit_code |= check_search_paths(
248+ (struct found_t){.how = LD_PRELOAD}, s->default_paths_offset,
249+ &preload_not_found, &preload_buf_offsets, depth, s, curr_type,
250+ needed_not_found);
251+ }
252+
253+ if (preload_not_found) {
254+ print_error(depth, preload_not_found, &preload_buf_offsets,
255+ runpath == MAX_OFFSET_T
256+ ? NULL
257+ : s->string_table.arr + runpath_buf_offset,
258+ s, no_def_lib, needed_not_found);
259+ exit_code |= ERR_DEPENDENCY_NOT_FOUND;
260+ }
261+ }
262+
263+ small_vec_u64_free(&preload_buf_offsets);
264+
265 if (needed_not_found)
266- exit_code |= check_absolute_paths(
267- &needed_not_found, &needed_buf_offsets, depth, s, curr_type);
268+ exit_code |= check_absolute_paths(&needed_not_found, &needed_buf_offsets,
269+ depth, s, curr_type,
270+ (struct found_t){.how = DIRECT}, 0);
271
272 // Consider rpaths only when runpath is empty
273 if (runpath == MAX_OFFSET_T) {
274@@ -1332,7 +1412,7 @@ static int recurse(char *current_file, size_t depth, struct libtree_state_t *s,
275
276 exit_code |= check_search_paths(
277 (struct found_t){.how = RPATH, .depth = j}, s->rpath_offsets[j],
278- &needed_not_found, &needed_buf_offsets, depth, s, curr_type);
279+ &needed_not_found, &needed_buf_offsets, depth, s, curr_type, 0);
280 }
281 }
282
283@@ -1340,28 +1420,28 @@ static int recurse(char *current_file, size_t depth, struct libtree_state_t *s,
284 if (needed_not_found && s->ld_library_path_offset != SIZE_MAX) {
285 exit_code |= check_search_paths(
286 (struct found_t){.how = LD_LIBRARY_PATH}, s->ld_library_path_offset,
287- &needed_not_found, &needed_buf_offsets, depth, s, curr_type);
288+ &needed_not_found, &needed_buf_offsets, depth, s, curr_type, 0);
289 }
290
291 // Then consider runpaths
292 if (needed_not_found && runpath != MAX_OFFSET_T) {
293 exit_code |= check_search_paths(
294 (struct found_t){.how = RUNPATH}, runpath_buf_offset,
295- &needed_not_found, &needed_buf_offsets, depth, s, curr_type);
296+ &needed_not_found, &needed_buf_offsets, depth, s, curr_type, 0);
297 }
298
299 // Check ld.so.conf paths
300 if (needed_not_found && !no_def_lib) {
301 exit_code |= check_search_paths(
302 (struct found_t){.how = LD_SO_CONF}, s->ld_so_conf_offset,
303- &needed_not_found, &needed_buf_offsets, depth, s, curr_type);
304+ &needed_not_found, &needed_buf_offsets, depth, s, curr_type, 0);
305 }
306
307 // Then consider standard paths
308 if (needed_not_found && !no_def_lib) {
309 exit_code |= check_search_paths(
310 (struct found_t){.how = DEFAULT}, s->default_paths_offset,
311- &needed_not_found, &needed_buf_offsets, depth, s, curr_type);
312+ &needed_not_found, &needed_buf_offsets, depth, s, curr_type, 0);
313 }
314
315 // Finally summarize those that could not be found.
316@@ -1370,7 +1450,7 @@ static int recurse(char *current_file, size_t depth, struct libtree_state_t *s,
317 runpath == MAX_OFFSET_T
318 ? NULL
319 : s->string_table.arr + runpath_buf_offset,
320- s, no_def_lib);
321+ s, no_def_lib, 0);
322 s->string_table.n = old_buf_size;
323 small_vec_u64_free(&needed_buf_offsets);
324 small_vec_u64_free(&needed);
325@@ -1529,6 +1609,38 @@ static void parse_ld_library_path(struct libtree_state_t *s) {
326 *search++ = ':';
327 }
328
329+static void parse_ld_preload(struct libtree_state_t *s) {
330+ small_vec_u64_init(&s->preload_offsets);
331+
332+ char *val = getenv("LD_PRELOAD");
333+ if (val == NULL)
334+ return;
335+
336+ // LD_PRELOAD entries are separated by spaces, tabs and/or colons. Store
337+ // each entry as its own null-terminated string in the string table.
338+ char *p = val;
339+ while (1) {
340+ while (*p == ':' || isspace((unsigned char)*p))
341+ ++p;
342+
343+ if (*p == '\0')
344+ break;
345+
346+ char *start = p;
347+ while (*p != '\0' && *p != ':' && !isspace((unsigned char)*p))
348+ ++p;
349+
350+ size_t len = p - start;
351+ size_t offset = s->string_table.n;
352+ string_table_maybe_grow(&s->string_table, len + 1);
353+ memcpy(s->string_table.arr + offset, start, len);
354+ s->string_table.arr[offset + len] = '\0';
355+ s->string_table.n += len + 1;
356+
357+ small_vec_u64_append(&s->preload_offsets, offset);
358+ }
359+}
360+
361 static void set_default_paths(struct libtree_state_t *s) {
362 s->default_paths_offset = s->string_table.n;
363 // TODO: how to retrieve this list properly at runtime?
364@@ -1548,6 +1660,7 @@ static void libtree_state_init(struct libtree_state_t *s) {
365 static void libtree_state_free(struct libtree_state_t *s) {
366 free(s->string_table.arr);
367 free(s->visited.arr);
368+ small_vec_u64_free(&s->preload_offsets);
369 }
370
371 static int print_tree(int pathc, char **pathv, struct libtree_state_t *s) {
372@@ -1556,6 +1669,7 @@ static int print_tree(int pathc, char **pathv, struct libtree_state_t *s) {
373
374 parse_ld_so_conf(s);
375 parse_ld_library_path(s);
376+ parse_ld_preload(s);
377 set_default_paths(s);
378
379 int exit_code = 0;
380@@ -1790,6 +1904,10 @@ int main(int argc, char **argv) {
381 fputs("]\n"
382 " --max-depth <n> Limit library traversal to at most n levels of depth\n"
383 "\n"
384+ "Environment variables:\n"
385+ " LD_LIBRARY_PATH Additional search paths\n"
386+ " LD_PRELOAD Additional libraries loaded before all others\n"
387+ "\n"
388 "* For brevity, the following libraries are not shown by default:\n"
389 " ",
390 stdout);
391diff --git a/tests/11_ld_preload/Makefile b/tests/11_ld_preload/Makefile
392new file mode 100644
393index 0000000..884ba7f
394--- /dev/null
395+++ b/tests/11_ld_preload/Makefile
396@@ -0,0 +1,40 @@
397+LIBTREE ?= ../../libtree
398+
399+# Basic test for LD_PRELOAD support: preloaded libraries should show up as
400+# direct dependencies of the top-level file, marked [LD_PRELOAD], and should
401+# be located using the same search rules (absolute path, LD_LIBRARY_PATH,
402+# rpath, ...) as the top-level file's regular dependencies.
403+
404+.PHONY: clean
405+
406+LD_LIBRARY_PATH=
407+
408+all: check
409+
410+libb.so:
411+ echo 'int b(void){return 1;}' | $(CC) -shared -Wl,-soname,$@ -Wl,--no-as-needed -o $@ -nostdlib -Wno-implicit-function-declaration -x c -
412+
413+libpre1.so:
414+ echo 'int p1(void){return 2;}' | $(CC) -shared -Wl,-soname,$@ -Wl,--no-as-needed -o $@ -nostdlib -Wno-implicit-function-declaration -x c -
415+
416+libpre2.so:
417+ echo 'int p2(void){return 3;}' | $(CC) -shared -Wl,-soname,$@ -Wl,--no-as-needed -o $@ -nostdlib -Wno-implicit-function-declaration -x c -
418+
419+exe: libb.so
420+ echo 'int b(void); int _start(void){return b();}' | $(CC) -Wl,--no-as-needed -Wl,--disable-new-dtags "-Wl,-rpath,$(CURDIR)" libb.so -o $@ -Wno-implicit-function-declaration -nostdlib -x c -
421+
422+check: exe libpre1.so libpre2.so
423+ # Found via absolute path
424+ LD_PRELOAD="$(CURDIR)/libpre1.so" $(LIBTREE) exe | grep -q 'libpre1.so \[LD_PRELOAD\]'
425+ # Found via LD_LIBRARY_PATH, multiple entries (space and colon separated)
426+ LD_PRELOAD="libpre1.so libpre2.so" LD_LIBRARY_PATH="$(CURDIR)" $(LIBTREE) exe | grep -q 'libpre1.so \[LD_PRELOAD\]'
427+ LD_PRELOAD="libpre1.so:libpre2.so" LD_LIBRARY_PATH="$(CURDIR)" $(LIBTREE) exe | grep -q 'libpre2.so \[LD_PRELOAD\]'
428+ # Without LD_PRELOAD, no [LD_PRELOAD] entries are shown
429+ ! $(LIBTREE) exe | grep -q '\[LD_PRELOAD\]'
430+ # A library that cannot be found is reported as such
431+ LD_PRELOAD="libdoesnotexist.so" $(LIBTREE) exe | grep -q 'libdoesnotexist.so not found'
432+
433+clean:
434+ rm -f *.so exe*
435+
436+CURDIR ?= $(.CURDIR)
437diff --git a/version.h.in b/version.h.in
438new file mode 100644
439index 0000000..32f49cf
440--- /dev/null
441+++ b/version.h.in
442@@ -0,0 +1 @@
443+#define VERSION "@PROJECT_VERSION@"