/* * revdep - reverse dependency checker for rawnix * elf.c - ELF parsing, hash-table cache, library resolution * * Copyright (c) 2025 zorz@gmx.com * Licensed under the BSD 2-Clause License. See revdep.h. * * x86_64-only. musl-only. No ld.so.conf (musl uses DT_RUNPATH). */ #define _POSIX_C_SOURCE 200809L #define _DEFAULT_SOURCE #include "revdep.h" #include #include #include #include #include #include #include #include /* -------------------------------------------------------------------- */ /* ELF inspection */ /* -------------------------------------------------------------------- */ static bool elf_is_valid(Elf *e) { GElf_Ehdr ehdr; if (!e || elf_kind(e) != ELF_K_ELF) return false; if (!gelf_getehdr(e, &ehdr)) return false; /* x86_64 only */ if (ehdr.e_machine != EM_X86_64) return false; /* executables and shared objects */ if (ehdr.e_type != ET_EXEC && ehdr.e_type != ET_DYN) return false; /* Linux or unspecified */ if (ehdr.e_ident[EI_OSABI] != ELFOSABI_NONE && ehdr.e_ident[EI_OSABI] != ELFOSABI_LINUX) return false; return true; } static bool elf_read_dynamic(Elf *e, elfinfo_t *info) { size_t phnum, i, j, count; GElf_Phdr phdr; Elf_Scn *scn; GElf_Shdr shdr; Elf_Data *data; GElf_Dyn dyn; const char *str; if (elf_getphdrnum(e, &phnum) == -1) return false; for (i = 0; i < phnum; i++) { if (!gelf_getphdr(e, (int)i, &phdr)) continue; if (phdr.p_type != PT_DYNAMIC) continue; scn = gelf_offscn(e, phdr.p_offset); if (!scn) continue; if (!gelf_getshdr(scn, &shdr)) continue; if (shdr.sh_type != SHT_DYNAMIC) continue; data = elf_getdata(scn, NULL); if (!data) return false; count = shdr.sh_size / gelf_fsize(e, ELF_T_DYN, 1, EV_CURRENT); for (j = 0; j < count; j++) { if (!gelf_getdyn(data, (int)j, &dyn)) break; str = elf_strptr(e, shdr.sh_link, dyn.d_un.d_val); if (!str) continue; switch (dyn.d_tag) { case DT_NEEDED: strvec_push(&info->needed, str); break; case DT_RPATH: strsplit(str, ':', &info->rpath); break; case DT_RUNPATH: strsplit(str, ':', &info->runpath); break; default: break; } } return true; } return false; } elfinfo_t * elf_inspect(const char *path) { elfinfo_t *info; int fd; Elf *e; if (!path) return NULL; info = calloc(1, sizeof(*info)); if (!info) return NULL; info->path = xstrdup(path); strvec_init(&info->needed); strvec_init(&info->rpath); strvec_init(&info->runpath); info->valid = false; fd = open(path, O_RDONLY); if (fd == -1) { elf_free(info); return NULL; } e = elf_begin(fd, ELF_C_READ_MMAP, NULL); if (!e) { close(fd); elf_free(info); return NULL; } if (!elf_is_valid(e) || !elf_read_dynamic(e, info)) { elf_end(e); close(fd); elf_free(info); return NULL; } info->valid = true; elf_end(e); close(fd); return info; } void elf_free(elfinfo_t *e) { if (!e) return; free(e->path); strvec_free(&e->needed); strvec_free(&e->rpath); strvec_free(&e->runpath); free(e); } /* -------------------------------------------------------------------- */ /* Hash-table ELF cache */ /* */ /* Many packages share the same libraries (libz, libc++, etc). */ /* Without caching, we'd re-open and re-parse the same .so hundreds */ /* of times. The hash table gives O(1) lookup by path. */ /* -------------------------------------------------------------------- */ #define CACHE_BUCKETS 2048 typedef struct cache_node { char *key; elfinfo_t *val; struct cache_node *next; } cache_node_t; static cache_node_t *cache_table[CACHE_BUCKETS]; static unsigned long hash_djb2(const char *s) { unsigned long h = 5381; int c; while ((c = *s++)) h = ((h << 5) + h) + (unsigned long)c; return h % CACHE_BUCKETS; } elfinfo_t * cache_lookup(const char *path) { unsigned long h; cache_node_t *n; elfinfo_t *info; if (!path) return NULL; h = hash_djb2(path); for (n = cache_table[h]; n; n = n->next) { if (strcmp(n->key, path) == 0) return n->val; } /* not cached — inspect and store */ info = elf_inspect(path); if (!info || !info->valid) { elf_free(info); return NULL; } n = malloc(sizeof(*n)); if (!n) { elf_free(info); return NULL; } n->key = xstrdup(path); n->val = info; n->next = cache_table[h]; cache_table[h] = n; return info; } void cache_clear(void) { size_t i; cache_node_t *n, *next; for (i = 0; i < CACHE_BUCKETS; i++) { for (n = cache_table[i]; n; n = next) { next = n->next; free(n->key); elf_free(n->val); free(n); } cache_table[i] = NULL; } } /* -------------------------------------------------------------------- */ /* $ORIGIN expansion */ /* */ /* musl only needs $ORIGIN (and $LIB = "lib"). */ /* No $PLATFORM, no multi-lib. */ /* -------------------------------------------------------------------- */ static char * expand_origin(const elfinfo_t *elf, const char *dir) { char origin[PATH_MAX]; char result[PATH_MAX]; const char *src; char *dst, *end, *d; size_t len, dlen; if (!elf || !dir) return NULL; /* get directory of the ELF file */ len = strlen(elf->path); if (len >= PATH_MAX) return NULL; memcpy(origin, elf->path, len + 1); d = dirname(origin); dlen = strlen(d); src = dir; dst = result; end = result + PATH_MAX - 1; while (*src && dst < end) { if (*src == '$') { if (strncmp(src, "$ORIGIN", 7) == 0 || strncmp(src, "${ORIGIN}", 9) == 0) { size_t skip = (src[1] == '{') ? 9 : 7; if (dst + dlen >= end) return NULL; memcpy(dst, d, dlen); dst += dlen; src += skip; continue; } if (strncmp(src, "$LIB", 4) == 0 || strncmp(src, "${LIB}", 6) == 0) { size_t skip = (src[1] == '{') ? 6 : 4; if (dst + 3 >= end) return NULL; memcpy(dst, "lib", 3); dst += 3; src += skip; continue; } } *dst++ = *src++; } *dst = '\0'; return xstrdup(result); } /* -------------------------------------------------------------------- */ /* Library resolution — follows ld.so search order */ /* -------------------------------------------------------------------- */ static bool try_lib_in_dirs(const elfinfo_t *elf, const char *lib, const strvec_t *dirs, bool do_expand) { size_t i; char *dir, *full, *real; elfinfo_t *libelf; if (!elf || !lib || !dirs) return false; for (i = 0; i < dirs->count; i++) { if (do_expand) { dir = expand_origin(elf, dirs->items[i]); if (!dir) continue; } else { dir = dirs->items[i]; } full = xpathjoin(dir, lib); if (do_expand) free(dir); if (!full) continue; real = realpath_safe(full); free(full); if (!real) continue; libelf = cache_lookup(real); free(real); if (libelf) return true; } return false; } /* * Search order (simplified for musl — no ld.so.conf, no ldconfig cache): * * 1. DT_RPATH (only if DT_RUNPATH absent) * 2. DT_RUNPATH * 3. System dirs (/lib, /usr/lib) * 4. Package-specific dirs (/etc/revdep.d/) */ bool lib_find(const elfinfo_t *elf, const pkg_t *pkg, const char *lib, const strvec_t *sys_dirs) { if (!elf || !lib || !sys_dirs) return false; /* absolute or relative path in DT_NEEDED — resolve directly */ if (strchr(lib, '/')) { char *real = realpath_safe(lib); if (!real) return false; elfinfo_t *e = cache_lookup(real); free(real); return e != NULL; } /* 1. DT_RPATH (deprecated, but still used by some software) */ if (elf->rpath.count > 0 && elf->runpath.count == 0) { if (try_lib_in_dirs(elf, lib, &elf->rpath, true)) return true; } /* 2. DT_RUNPATH */ if (elf->runpath.count > 0) { if (try_lib_in_dirs(elf, lib, &elf->runpath, true)) return true; } /* 3. system directories */ if (try_lib_in_dirs(elf, lib, sys_dirs, false)) return true; /* 4. per-package directories from /etc/revdep.d */ if (pkg && pkg->dirs.count > 0) { if (try_lib_in_dirs(elf, lib, &pkg->dirs, false)) return true; } return false; }