diff --git a/utils/ld-cache.c b/utils/ld-cache.c new file mode 100644 index 0000000000000000000000000000000000000000..8c9c54809a67d8907b99a8806870680d3fe46e76 --- /dev/null +++ b/utils/ld-cache.c @@ -0,0 +1,317 @@ +#include <stdio.h> +#include <sys/mman.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <unistd.h> +#include <fcntl.h> +#include <string.h> + +#include "ld-cache.h" +#include "utils.h" + +// ========================================================================== +// this is stolen from the ld.so config (dl-cache.h) since we need +// to use a slightly re-brained version of the linker to do our +// filthy, filthy business (and these details are not in a shipped header) + +#define FLAG_ANY -1 +#define FLAG_TYPE_MASK 0x00ff +#define FLAG_LIBC4 0x0000 +#define FLAG_ELF 0x0001 +#define FLAG_ELF_LIBC5 0x0002 +#define FLAG_ELF_LIBC6 0x0003 +#define FLAG_REQUIRED_MASK 0xff00 +#define FLAG_SPARC_LIB64 0x0100 +#define FLAG_IA64_LIB64 0x0200 +#define FLAG_X8664_LIB64 0x0300 +#define FLAG_S390_LIB64 0x0400 +#define FLAG_POWERPC_LIB64 0x0500 +#define FLAG_MIPS64_LIBN32 0x0600 +#define FLAG_MIPS64_LIBN64 0x0700 +#define FLAG_X8664_LIBX32 0x0800 +#define FLAG_ARM_LIBHF 0x0900 +#define FLAG_AARCH64_LIB64 0x0a00 +#define FLAG_ARM_LIBSF 0x0b00 +#define FLAG_MIPS_LIB32_NAN2008 0x0c00 +#define FLAG_MIPS64_LIBN32_NAN2008 0x0d00 +#define FLAG_MIPS64_LIBN64_NAN2008 0x0e00 + +static const char *flag_descr[] = { "libc4", "ELF", "libc5", "libc6"}; + +static const char *const abi_tag_os[] = +{ + [0] = "Linux", + [1] = "Hurd", + [2] = "Solaris", + [3] = "FreeBSD", + [4] = "kNetBSD", + [5] = "Syllable", + [6] = "Unknown OS" +}; +#define MAXTAG (sizeof abi_tag_os / sizeof abi_tag_os[0] - 1) + +/* Used to align cache_file_new. */ +#define ALIGN_CACHE(addr) \ + (((addr) + __alignof__ (struct cache_file_new) -1) \ + & (~(__alignof__ (struct cache_file_new) - 1))) + +// end of stolen header structures +//////////////////////////////////////////////////////////////////////////// +static void +ld_cache_reset (ldcache_t *cache) +{ + cache->fd = -1; + cache->map_size = 0; + cache->data = NULL; + cache->file.old = NULL; + cache->type = CACHE_NONE; + cache->mmap = MAP_FAILED; + cache->is_open = 0; +} + +void +ld_cache_close (ldcache_t *cache) +{ + // 0 is a valid fd, but is also the default value of the unset + // struct member, so we have to check if it's _really_ open: + if( cache->is_open ) + { + if( cache->fd >= 0 ) + close( cache->fd ); + } + + if( cache->mmap && cache->map_size ) + munmap( cache->mmap, cache->map_size ); + + ld_cache_reset( cache ); +} + +int +ld_cache_open (ldcache_t *cache, const char *path) +{ + struct stat ldcache = {}; + + ld_cache_close( cache ); + + cache->fd = open( path, O_RDONLY ); + + if( cache->fd < 0 ) + goto cleanup; + + // now we have a real file descriptor tag the + // cache as successfully opened: + cache->is_open = 1; + + fstat( cache->fd, &ldcache ); + + // cache file must be at least this big or it's invalid: + if( ldcache.st_size < sizeof( struct cache_file ) ) + goto cleanup; + + cache->mmap = + mmap( NULL, ldcache.st_size, PROT_READ, MAP_PRIVATE, cache->fd, 0 ); + + if( cache->mmap == MAP_FAILED ) + goto cleanup; + + cache->map_size = ldcache.st_size; + + // plain modern (circa 2016) cache map: + if( memcmp( cache->mmap->magic, CACHEMAGIC, sizeof(CACHEMAGIC) -1 ) ) + { + DEBUG( DEBUG_LDCACHE, "New format ld cache" ); + cache->file.new = (struct cache_file_new *)cache->mmap; + + // if the magic strings don't reside at the expected offsets, bail out: + if( memcmp( cache->file.new->magic, + CACHEMAGIC_NEW, sizeof(CACHEMAGIC_NEW) - 1 ) || + memcmp( cache->file.new->version, + CACHE_VERSION, sizeof(CACHE_VERSION) - 1 ) ) + { + fprintf( stderr, "invalid cache, expected %s: %s\n", + CACHEMAGIC_NEW, CACHE_VERSION ); + goto cleanup; + } + + cache->data = (char *)cache->file.new; + cache->type = CACHE_NEW; + } + else + { + size_t header = sizeof( struct cache_file ); + size_t entry = sizeof( struct file_entry ); + size_t block = header + (cache->mmap->nlibs * entry); + size_t offset = ALIGN_CACHE( block ); + int nlibs = cache->mmap->nlibs; + + DEBUG( DEBUG_LDCACHE, "Old format ld cache" ); + + // it's an old-style cache, unless we successfully probe for a + // nested new cache inside it: + cache->type = CACHE_OLD; + + /* This is where the strings start in an old style cache */ + cache->data = (const char *) &cache->mmap->libs[ nlibs ]; + + if( cache->map_size > (offset + sizeof( struct cache_file_new )) ) + { + cache->file.new = (void *)cache->mmap + offset; + + // this is the probe: as in the pervious if block, except + // that if we don't find a new cache it's not an error, + // it just means we're in an old style cache: + if( memcmp( cache->file.new->magic, CACHEMAGIC_NEW, + sizeof(CACHEMAGIC_NEW) - 1 ) || + memcmp( cache->file.new->version, CACHE_VERSION, + sizeof(CACHE_VERSION) - 1) ) + { + // nope, no encapsulated new cache: + cache->file.old = cache->mmap; + } + else + { + DEBUG( DEBUG_LDCACHE, "... with a new style cache inside" ); + cache->type = CACHE_NEW; + cache->data = (char *)cache->file.new; + } + } + } + + DEBUG( DEBUG_LDCACHE, "Opened ld.cache at %s", path ); + return 1; + +cleanup: + DEBUG( DEBUG_LDCACHE, "Failed to open ld.cache at %s", path ); + ld_cache_close( cache ); + return 0; +} + +// iterate over the entries in the ld cache, invoking callback cb on each one +// until eithe the callback returns true or we run out of entries. +// if the callback terminates iteration by returning true, return that value, +// otherwise return false: +intptr_t +ld_cache_foreach (ldcache_t *cache, ld_cache_entry_cb cb, void *data) +{ + int rval = 0; + const char *base = cache->data; + + switch( cache->type ) + { + case CACHE_OLD: + for (int i = 0; !rval && (i < cache->file.old->nlibs); i++) + { + struct file_entry *f = &cache->file.old->libs[i]; + rval = cb( base + f->key, f->flags, 0, 0, base + f->value, data ); + } + break; + + case CACHE_NEW: + for (int i = 0; !rval && (i < cache->file.new->nlibs); i++) + { + struct file_entry_new *f = &cache->file.new->libs[i]; + rval = cb( base + f->key, f->flags, + f->osversion, f->hwcap, base + f->value, data ); + } + break; + + default: + fprintf( stderr, "Invalid ld cache type %d", cache->type ); + break; + } + + return rval; +} + +intptr_t +ld_entry_dump (const char *name, + int flag, + unsigned int osv, + uint64_t hwcap, + const char *path, + void *data) +{ + FILE *stream = (FILE *)data; + fprintf( stream, "%s → %s\n", name, path ); + + fputs( " type: ", stream ); + switch (flag & FLAG_TYPE_MASK) + { + case FLAG_LIBC4: + case FLAG_ELF: + case FLAG_ELF_LIBC5: + case FLAG_ELF_LIBC6: + fputs( flag_descr[flag & FLAG_TYPE_MASK], stream ); + break; + default: + fputs( "???", stdout ); + break; + } + fputs( "\n", stream ); + + fputs( " requires: ", stream ); + switch (flag & FLAG_REQUIRED_MASK) + { + case FLAG_SPARC_LIB64: + fputs( "Sparc 64bit", stream ); + break; + case FLAG_IA64_LIB64: + fputs( "IA-64", stream ); + break; + case FLAG_X8664_LIB64: + fputs( "x86-64", stream ); + break; + case FLAG_S390_LIB64: + fputs( "s390 64bit", stream ); + break; + case FLAG_POWERPC_LIB64: + fputs( "PPC 64bit", stream ); + break; + case FLAG_MIPS64_LIBN32: + fputs( "MIPS N32", stream ); + break; + case FLAG_MIPS64_LIBN64: + fputs( "MIPS 64bit", stream ); + break; + case FLAG_X8664_LIBX32: + fputs( "x32", stream ); + break; + case FLAG_ARM_LIBHF: + fputs( "Arm hard-float", stream ); + break; + case FLAG_AARCH64_LIB64: + fputs( "AArch64", stream ); + break; + /* Uses the ARM soft-float ABI. */ + case FLAG_ARM_LIBSF: + fputs( "Arm soft-float", stream ); + break; + case FLAG_MIPS_LIB32_NAN2008: + fputs( "MIPS nan2008", stream ); + break; + case FLAG_MIPS64_LIBN32_NAN2008: + fputs( "MIPS N32 nan2008", stream ); + break; + case FLAG_MIPS64_LIBN64_NAN2008: + fputs( "IPS 64bit nan2008", stream ); + break; + case 0: + break; + default: + fprintf (stream, "%0x", flag & FLAG_REQUIRED_MASK); + break; + } + fputs( "\n", stream ); + + unsigned int os = osv >> 24; + + fprintf( stream, " OS ABI: %s %d.%d.%d (%0x)\n", + abi_tag_os[ os > MAXTAG ? MAXTAG : os ], + (osv >> 16) & 0xff, + (osv >> 8 ) & 0xff, + osv & 0xff, + osv ); + + fprintf( stream, " hwcaps: %0"FMT_XU64"\n", hwcap ); + return 0; +} diff --git a/utils/ld-cache.h b/utils/ld-cache.h new file mode 100644 index 0000000000000000000000000000000000000000..2ce9496892e953d8ea5077fe0e77567e203d76bc --- /dev/null +++ b/utils/ld-cache.h @@ -0,0 +1,82 @@ +#pragma once + +#include <sys/types.h> +#include <stdint.h> + +typedef enum +{ + CACHE_NONE, + CACHE_NEW , + CACHE_OLD , +} cache_type; + + +// ========================================================================== +// this is stolen from the ld.so config (dl-cache.h) since we need +// to use a slightly re-brained version of the linker to do our +// filthy, filthy business (and these details are not in a shipped header) +#define CACHEMAGIC "ld.so-1.7.0" + +#define CACHEMAGIC_NEW "glibc-ld.so.cache" +#define CACHE_VERSION "1.1" +#define CACHEMAGIC_VERSION_NEW CACHEMAGIC_NEW CACHE_VERSION + +struct file_entry_new +{ + int32_t flags; /* This is 1 for an ELF library. */ + uint32_t key, value; /* String table indices. */ + uint32_t osversion; /* Required OS version. */ + uint64_t hwcap; /* Hwcap entry. */ +}; + +struct cache_file_new +{ + char magic[sizeof CACHEMAGIC_NEW - 1]; + char version[sizeof CACHE_VERSION - 1]; + uint32_t nlibs; /* Number of entries. */ + uint32_t len_strings; /* Size of string table. */ + uint32_t unused[5]; /* Leave space for future extensions + and align to 8 byte boundary. */ + struct file_entry_new libs[0]; /* Entries describing libraries. */ + /* After this the string table of size len_strings is found. */ +}; + +struct file_entry +{ + int flags; /* This is 1 for an ELF library. */ + unsigned int key, value; /* String table indices. */ +}; + +struct cache_file +{ + char magic[sizeof CACHEMAGIC - 1]; + unsigned int nlibs; + struct file_entry libs[0]; +}; + +typedef struct +{ + int fd; + size_t map_size; + struct cache_file *mmap; + const char *data; + union { struct cache_file *old; struct cache_file_new *new; } file; + cache_type type; + int is_open; +} ldcache_t; + +// ========================================================================== + +typedef intptr_t (*ld_cache_entry_cb) (const char *name, + int flag, + unsigned int osv, + uint64_t hwcap, + const char *path, + void *data); + +int ld_cache_open (ldcache_t *cache, const char *path); +void ld_cache_close (ldcache_t *cache); +intptr_t ld_cache_foreach (ldcache_t *cache, ld_cache_entry_cb cb, void *data); + +intptr_t ld_entry_dump (const char *name, int flag, unsigned int osv, + uint64_t hwcap, const char *path, void *data); diff --git a/utils/ld-libs.c b/utils/ld-libs.c new file mode 100644 index 0000000000000000000000000000000000000000..d3e5cb7c59f6ee11af2b673dd558b4f3b847a9fe --- /dev/null +++ b/utils/ld-libs.c @@ -0,0 +1,903 @@ +#include <string.h> +#include <dlfcn.h> +#include <link.h> +#include <errno.h> +#include <stdio.h> +#include <sys/param.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <fcntl.h> +#include <unistd.h> +#include <stdlib.h> + +#include "utils.h" +#include "ld-libs.h" + + +struct dso_cache_search +{ + int idx; + const char *name; + ld_libs_t *ldlibs; +}; + +// make sure the prefix buffer is properly NUL terminated +static inline void sanitise_ldlibs(ld_libs_t *ldlibs) +{ + ldlibs->prefix.path[ ldlibs->prefix.len ] = '\0'; +} + +static const char * +_rtldstr(int flag) +{ + char flags[160] = { 0 }; + char *f = &flags[0]; + + if( !flag) + return "LOCAL"; + +#define RTLDFLAGSTR(x) \ + if( x & flag ) f += snprintf(f, &flags[80] - f, " %s", & #x [5]) + + RTLDFLAGSTR(RTLD_LAZY); + RTLDFLAGSTR(RTLD_NOW); + RTLDFLAGSTR(RTLD_NOLOAD); + RTLDFLAGSTR(RTLD_DEEPBIND); + RTLDFLAGSTR(RTLD_GLOBAL); + RTLDFLAGSTR(RTLD_NODELETE); + + return ( flags[0] == ' ' ) ? &flags[1] : &flags[0]; +} + +// as we are pulling in files from a non '/' prefix ('/host' by default) +// we need to compensate for this when resolving symlinks. +// this will keep following the path at entry i in ldlibs until +// it finds something that is not a symlink. +static void resolve_symlink_prefixed (ld_libs_t *ldlibs, int i) +{ + int count = 0; + char resolved[PATH_MAX]; + char link_dir[PATH_MAX]; + + sanitise_ldlibs(ldlibs); + // prefix is unset or is /, nothing to do here (we can rely on + // libc's built-in symlink following if there's no prefix): + if( ldlibs->prefix.len == 0 || + (ldlibs->prefix.path[0] == '/' && ldlibs->prefix.path[1] == '\0') ) + return; + + LDLIB_DEBUG( ldlibs, DEBUG_PATH, + "resolving (un)prefixed link in %s", ldlibs->needed[i].path ); + + // set the resolved path to the current needed path as a starting point: + safe_strncpy( resolved, ldlibs->needed[i].path, PATH_MAX ); + + // now keep poking resolve_link (resolved will be updated each time) + // until it returns false: + while( resolve_link(ldlibs->prefix.path, resolved, link_dir) ) + { + LDLIB_DEBUG( ldlibs, DEBUG_PATH, " resolved to: %s", resolved ); + + if( ++count > MAXSYMLINKS ) + { + fprintf( stderr, "%s: MAXSYMLINKS (%d) exceeded resolving %s\n", + __PRETTY_FUNCTION__, MAXSYMLINKS, + ldlibs->needed[i].path ); + break; + } + } + + // if the path changed, copy `resolved' back into needed[].path: + if( count ) + safe_strncpy( ldlibs->needed[i].path, resolved, PATH_MAX ); +} + + +// set the ldlibs elf class and machine based on the link map entry +// passed to us if possible: if we found values for these, return 1, +// otherwise return 0: +static int +find_elf_constraints(ld_libs_t *ldlibs, struct link_map *m) +{ + int fd = -1; + Elf *dso = NULL; + GElf_Ehdr ehdr = { }; + + // absolute path or it's a "fake" link map entry which we can't use + // as there's no actual file to open and inspect: + if( !m || !m->l_name || (m->l_name[0] != '/')) + return 0; + + // if we can't open the DSO pointed to by the link map, bail: + fd = open( m->l_name, O_RDONLY ); + + if( fd < 0 ) + return 0; + + dso = elf_begin( fd, ELF_C_READ_MMAP, NULL ); + + if( dso && gelf_getehdr( dso, &ehdr ) ) + { + ldlibs->elf_class = gelf_getclass( dso ); + ldlibs->elf_machine = ehdr.e_machine; + DEBUG( DEBUG_SEARCH|DEBUG_CAPSULE, + "elf class: %d; elf machine: %d; set from: %s", + ldlibs->elf_class, ldlibs->elf_machine, m->l_name ); + } + + if( dso != NULL ) + elf_end( dso ); + + if( fd >= 0 ) + close( fd ); + + return ( ldlibs->elf_class != ELFCLASSNONE ); +} + +// record the class & machine of the start of the link chain +// so that we can only consider matching libraries later +// this matters on multi-arch systems so we don't pick an +// i386 or x32 DSO to statisfy a DT_NEEDED from an x86-64 one. +// return true if we found a valid DSO, false (can't happen?) otherwise +static int +set_elf_constraints (ld_libs_t *ldlibs) +{ + void *handle; + struct link_map *map; + struct link_map *m; + + if( (handle = dlopen( NULL, RTLD_LAZY|RTLD_NOLOAD )) && + (dlinfo( handle, RTLD_DI_LINKMAP, &map ) == 0) ) + { + // we're not guaranteed to be at the start of the link map chain: + while( map->l_prev ) + map = map->l_prev; + + // check link maps until we find one we can fill in + // our constraints from: + for( m = map; m; m = m->l_next ) + if( find_elf_constraints( ldlibs, m ) ) + break; + } + else + { + // this would be frankly beyond bizarre: + fprintf(stderr, "dlopen/dlinfo on self failed: %s\n", dlerror() ); + } + + return ( ( ldlibs->elf_class != ELFCLASSNONE ) && + ( ldlibs->elf_machine |= EM_NONE ) ); +} + +// check that the currently opened DSO at offset idx in the needed array +// matches the class & architecture of the DSO we started with: +// return true on a match, false otherwise +static int +check_elf_constraints (ld_libs_t *ldlibs, int idx) +{ + GElf_Ehdr ehdr = {}; + int eclass; + + // bogus ELF DSO - no ehdr available? + if( !gelf_getehdr( ldlibs->needed[ idx ].dso, &ehdr ) ) + return 0; + + eclass = gelf_getclass( ldlibs->needed[ idx ].dso ); + + // check class (32 vs 64 bit) + if( ldlibs->elf_class != eclass ) + return 0; + + // check target architecture (i386, x86-64) + // x32 ABI is class 32 but machine x86-64 + if( ldlibs->elf_machine != ehdr.e_machine ) + return 0; + + DEBUG( DEBUG_ELF, "constraints: class %d; machine: %d;", + ldlibs->elf_class, ldlibs->elf_machine ); + DEBUG( DEBUG_ELF, "results : class %d; machine: %d;", + eclass, ehdr.e_machine ); + + // both the class (word size) and machine (architecture) match + return 1; +} + +static void clear_needed (dso_needed_t *needed) +{ + elf_end( needed->dso ); + needed->dso = NULL; + + if( needed->fd >= 0 ) + close( needed->fd ); + needed->fd = -1; + + free( needed->name ); + needed->name = NULL; + + needed->depcount = 0; + + memset( needed->path, 0, PATH_MAX ); + memset( needed->requestors, 0, sizeof(int) * DSO_LIMIT ); +} + +// open the dso at offset i in the needed array, but only accept it +// if it matches the class & architecture of the starting DSO: +// return a true value only if we finish with a valid fd for the DSO +// +// will set up the needed entry at offset i correctly if we are +// successful, and clear it if we are not: +// +// designed to be called on a populated ldlib needed entry, 'name' +// is the original requested name (typically an absolute path to +// a DSO or a standard DT_NEEDED style specifier following 'libfoo.so.X') +// +// note that this expects the prefix to have already been prepended +// to the DSO path in the ldlibs->needed[i].path buffer if necessary +// this is to allow both prefixed and unprefixed DSOs to be handled +// here: +static int +ld_lib_open (ld_libs_t *ldlibs, const char *name, int i) +{ + + LDLIB_DEBUG( ldlibs, DEBUG_SEARCH, + "ldlib_open: target -: %s", ldlibs->needed[i].path ); + + // resolve the symlink manually if there's a prefix: + resolve_symlink_prefixed( ldlibs, i ); + + LDLIB_DEBUG( ldlibs, DEBUG_SEARCH, + "ldlib_open: target +: %s", ldlibs->needed[i].path ); + + ldlibs->needed[i].fd = open( ldlibs->needed[i].path, O_RDONLY ); + + if( ldlibs->needed[i].fd >= 0 ) + { + int acceptable = 0; + + ldlibs->needed[i].name = NULL; + ldlibs->needed[i].dso = + elf_begin( ldlibs->needed[i].fd, ELF_C_READ_MMAP, NULL ); + + acceptable = check_elf_constraints( ldlibs, i ); + + LDLIB_DEBUG( ldlibs, DEBUG_SEARCH, + "[%03d] %s on fd #%d; elf: %p; acceptable: %d", + i, + ldlibs->needed[i].path, + ldlibs->needed[i].fd , + ldlibs->needed[i].dso , + acceptable ); + + // either clean up the current entry so we can find a better DSO + // or (for a valid candidate) copy the original requested name in: + if( !acceptable ) + clear_needed( &ldlibs->needed[i] ); + else + ldlibs->needed[i].name = strdup( name ); + } + + // the fd will only be valid if everything worked out: + return ldlibs->needed[i].fd >= 0; +} + +// search callback for search_ldcache. see search_ldcache and ld_cache_foreach: +// returning a true value means we found (and set up) the DSO we wanted: +static int +search_ldcache_cb (const char *name, // name of the DSO in the ldcache + int flag, // 1 for an ELF DSO + unsigned int osv, // OS version. we don't use this + uint64_t hwcap, // HW caps. Ibid. + const char *path, // absolute path to DSO (may be a symlink) + struct dso_cache_search *target) +{ + // passed an empty query, just abort the whole search + if( !target->name || !(*target->name) ) + return 1; + + // what would this even mean? malformed cache entry? + // skip it and move on + if( !name || !*name ) + return 0; + + if( strcmp( name, target->name ) == 0 ) + { + int idx = target->idx; + char *prefix = target->ldlibs->prefix.path; + size_t plen = target->ldlibs->prefix.len; + char *lpath = target->ldlibs->needed[ idx ].path; + + LDLIB_DEBUG( target->ldlibs, DEBUG_SEARCH|DEBUG_LDCACHE, + "checking %s vs %s [%s]", + target->name, name, path ); + // copy in the prefix and append the DSO path to it + safe_strncpy( lpath, prefix, PATH_MAX ); + safe_strncpy( lpath + plen, path, PATH_MAX - plen ); + + // try to open the DSO. This will finish setting up the + // needed[idx] slot if successful, and reset it ready for + // another attempt if it fails: + return ld_lib_open( target->ldlibs, name, idx ); + } + + return 0; +} + +// search the ld.so.cache loaded into ldlibs for one matching `name' +// name should be unadorned: eg just libfoo.so.X - no path elements +// attached (as the cache lookup is for unadorned library names): +// +// if a match is found, the needed array entry at i will be populated +// and will contain a valid fd for the DSO. (and search_ldcache will +// return true). Otherwise the entry will be empty and we will return false: +// +// this function will respect any path prefix specified in ldlibs +static int +search_ldcache (const char *name, ld_libs_t *ldlibs, int i) +{ + struct dso_cache_search target; + char cachepath[PATH_MAX]; + + target.idx = i; + target.name = name; + target.ldlibs = ldlibs; + + if( !ldlibs->ldcache.is_open ) + { + if( ldlibs->prefix.len == 0 || + strcmp(ldlibs->prefix.path, "/") == 0) + { + safe_strncpy( &cachepath[0], "/etc/ld.so.cache", PATH_MAX ); + } + else + { + safe_strncpy( &cachepath[0], ldlibs->prefix.path, PATH_MAX ); + safe_strncpy( &cachepath[ldlibs->prefix.len], "/etc/ld.so.cache", PATH_MAX ); + cachepath[ PATH_MAX - 1 ] = '\0'; + } + + ld_cache_open( &ldlibs->ldcache, &cachepath[0] ); + } + + ld_cache_foreach( &ldlibs->ldcache, + (ld_cache_entry_cb)search_ldcache_cb, &target ); + + return ldlibs->needed[i].fd >= 0; +} + +// search a : separated path (such as LD_LIBRARY_PATH from the environment) +// for a DSO matching the bare `name' (eg libfoo.so.X) +// +// if a match is found, the needed array entry at i will be populated +// and will contain a valid fd for the DSO. (and search_ldcache will +// return true). Otherwise the entry will be empty and we will return false: +// +// this function will respect any path prefix specified in ldlibs +static int +search_ldpath (const char *name, const char *ldpath, ld_libs_t *ldlibs, int i) +{ + char *sp = (char *)ldpath; + char *prefix = ldlibs->prefix.path; + size_t plen = ldlibs->prefix.len; + + prefix[plen] = '\0'; + + sanitise_ldlibs(ldlibs); + + LDLIB_DEBUG( ldlibs, DEBUG_SEARCH, + "searching for %s in %s (prefix: %s)", + name, ldpath, plen ? prefix : "-none-" ); + + while( sp && *sp ) + { + size_t len; + char *end; + + end = strchr( sp, ':' ); + if( end ) + len = MIN((end - sp), PATH_MAX - plen - 1); + else + len = MIN(strlen( sp ), PATH_MAX - plen - 1); + + safe_strncpy( prefix + plen, sp, len + 1); + prefix[plen + len + 1] = '\0'; + + LDLIB_DEBUG( ldlibs, DEBUG_SEARCH, " searchpath element: %s", prefix ); + // append the target name, without overflowing, then resolve + if( (plen + len + strlen( name ) + 1 < PATH_MAX) ) + { + prefix[plen + len] = '/'; + safe_strncpy( prefix + plen + len + 1, name, + PATH_MAX - plen - len - 1 ); + + LDLIB_DEBUG( ldlibs, DEBUG_SEARCH, "examining %s", prefix ); + // if path resolution succeeds _and_ we can open an acceptable + // DSO at that location, we're good to go (ldlib_open will + // finish setting up or clearing the needed[] entry for us): + // FIXME: this is broken if the target is an absolute symlink + if( realpath( prefix, ldlibs->needed[i].path ) && + ld_lib_open( ldlibs, name, i ) ) + return 1; + } + + // search the next path element if there is one + if( !end ) + break; + + sp = end + 1; + } + + return 0; +} + +// find a DSO using an algorithm that matches the one used by the +// normal dynamic linker and set up the needed array entry at offset i. +// +// Exceptions: +// we don't support DT_RPATH/DT_RUNPATH +// we don't handle ${ORIGIN} and similar +// we will respect any path prefix specified in ldlibs +// +// if a match is found, the needed array entry at i will be populated +// and will contain a valid fd for the DSO. (and search_ldcache will +// return true). Otherwise the entry will be empty and we will return false: +static int +dso_find (const char *name, ld_libs_t *ldlibs, int i) +{ + int found = 0; + const char *ldpath = NULL; + int absolute = (name && (name[0] == '/')); + + // 'name' is an absolute path, or relative to CWD: + // we may to need to do some path manipulation + if( strchr( name, '/' ) ) + { + size_t plen = ldlibs->prefix.len; + char prefixed[PATH_MAX]; + const char *target; + + // we have a path prefix, so yes, we need to do some path manipulation: + if( ldlibs->prefix.len ) + { + sanitise_ldlibs(ldlibs); + safe_strncpy( prefixed, ldlibs->prefix.path, PATH_MAX ); + + if( absolute ) + { + safe_strncpy( prefixed + plen, name, PATH_MAX - plen ); + LDLIB_DEBUG( ldlibs, DEBUG_SEARCH|DEBUG_PATH, + "absolute path to DSO %s", prefixed ); + } + else + { // name is relative... this is probably wrong? + // I don't think this can ever really happen but + // worst case is we'll simply not open a DSO whose + // path we couldn't resolve, and then move on: + safe_strncpy( prefixed + plen, "/", PATH_MAX - plen ); + safe_strncpy( prefixed + plen + 1, name, PATH_MAX - plen - 1); + LDLIB_DEBUG( ldlibs, DEBUG_SEARCH|DEBUG_PATH, + "relative path to DSO %s", prefixed ); + } + + target = prefixed; + } + else + { // name is a standard bare 'libfoo.so.X' spec: + target = name; + } + + LDLIB_DEBUG( ldlibs, DEBUG_SEARCH|DEBUG_PATH, + "resolving path %s", target ); + // this will fail for a non-absolute path, but that's OK + // if realpath lookup succeeds needed[i].path will be set correctly: + if( realpath( target, ldlibs->needed[i].path ) ) + return ld_lib_open( ldlibs, name, i ); + } + + // path was absolute and we couldn't resolve it. give up: + if( absolute ) + return 0; + + LDLIB_DEBUG( ldlibs, DEBUG_SEARCH, "target DSO is %s", name ); + // now search LD_LIBRARY_PATH, the ld.so.cache, and the default locations + // in that order (similar algorithm to the linker, but with the RPATH and + // ${ORIGIN} support dropped) + if( (ldpath = getenv( "LD_LIBRARY_PATH" )) ) + if( (found = search_ldpath( name, ldpath, ldlibs, i )) ) + return found; + + if( (found = search_ldcache( name, ldlibs, i )) ) + return found; + + if( (found = search_ldpath( name, "/lib:/usr/lib", ldlibs, i )) ) + return found; + + return 0; +} + +// if a DSO has already been requested and found as a result of a DT_NEEDED +// entry we've seen before then it's already in the needed array - check +// for such pre-required entries and simply record the dependency instead +// of reopening the DSO (and return true to indicate that we already have the +// DSO) +// +// we have assumed that the root of the DSO chain can never be already-needed +// as this would indicate a circular dependency. +static int +already_needed (dso_needed_t *needed, int requesting_idx, const char *name) +{ + for( int i = DSO_LIMIT - 1; i > 0; i-- ) + { + if( needed[i].name && strcmp( needed[i].name, name ) == 0) + { + needed[i].requestors[requesting_idx] = 1; + return i; + } + } + + return 0; +} + +// we're getting to the meat of it: process a DSO at offset idx in the +// needed array, extract each SHT_DYNAMIC section, then make sure we +// can find a DSO to satisfy every DT_NEEDED sub-entry in the section. +// this function recurses into itself each time it finds a previously +// unseen DT_NEEDED value (but not if the DT_NEEDED value is for a DSO +// it has already found and recorded in the needed array) +// +// NOTE: you must use dso_find to seed the 0th entry in the needed array +// or the elf handle in needed[0].dso will not be set up and hilarity* +// will ensue. +static void +_dso_iterate_sections (ld_libs_t *ldlibs, int idx) +{ + Elf_Scn *scn = NULL; + + //debug(" ldlibs: %p; idx: %d (%s)", ldlibs, idx, ldlibs->needed[idx].name); + + ldlibs->last_idx = idx; + + LDLIB_DEBUG( ldlibs, DEBUG_ELF, + "%03d: fd:%d dso:%p ↠%s", + idx, + ldlibs->needed[idx].fd, + ldlibs->needed[idx].dso, + ldlibs->needed[idx].path ); + + while((scn = elf_nextscn( ldlibs->needed[idx].dso, scn )) != NULL) + { + GElf_Shdr shdr = {}; + gelf_getshdr( scn, &shdr ); + + // SHT_DYNAMIC is the only section type we care about here: + if( shdr.sh_type == SHT_DYNAMIC ) + { + int i = 0; + GElf_Dyn dyn = {}; + Elf_Data *edata = NULL; + + edata = elf_getdata( scn, edata ); + + // process eaach DT_* entry in the SHT_DYNAMIC section: + while( !ldlibs->error && + gelf_getdyn( edata, i++, &dyn ) && + (dyn.d_tag != DT_NULL) ) + { + int skip = 0; + int next = ldlibs->last_idx; + dso_needed_t *needed = ldlibs->needed; + char *next_dso; // name of the dependency we're going to need + + // we're only gathering DT_NEEDED (dependency) entries here: + if( dyn.d_tag != DT_NEEDED ) + continue; + + next_dso = + elf_strptr( needed[idx].dso, shdr.sh_link, dyn.d_un.d_val ); + + ////////////////////////////////////////////////// + // ignore the linker itself + if( strstr( next_dso, "ld-" ) == next_dso ) + continue; + + // ignore any DSOs we've been specifically told to leave out: + for( char **x = (char **)ldlibs->exclude; x && *x; x++ ) + { + if( strcmp( *x, next_dso ) == 0 ) + { + LDLIB_DEBUG( ldlibs, DEBUG_SEARCH|DEBUG_ELF, + "skipping %s / %s", next_dso, *x ); + skip = 1; + break; + } + } + + if( skip ) + continue; + + ////////////////////////////////////////////////// + // if we got this far, we have another dependency: + needed[idx].depcount++; + + // already on our list, no need to do anything else here: + if( already_needed( needed, idx, next_dso ) ) + continue; + + next++; + if( next >= DSO_LIMIT ) + { + ldlibs->error = strdup( "Too many dependencies: abort" ); + break; + } + + if( !dso_find( next_dso, ldlibs, next ) ) + { + ldlibs->not_found[ ldlibs->last_not_found++ ] = + strdup( next_dso ); + ldlibs->error = strdup( "Missing dependencies:" ); + } + else + { + // record which DSO requested the new library we found: + needed[next].requestors[idx] = 1; + // now find the dependencies of our newest dependency: + _dso_iterate_sections( ldlibs, next ); + } + } + } + } +} + +static void +_dso_iterator_format_error (ld_libs_t * ldlibs) +{ + size_t extra_space = 0; + + if( ! ldlibs->error ) + return; + + if( ! ldlibs->not_found[0] ) + return; + + for( int i = 0; (i < DSO_LIMIT) && ldlibs->not_found[i]; i++ ) + extra_space += strlen( ldlibs->not_found[i] ) + 1; + + if( extra_space ) + { + char *append_here; + char *end; + size_t prev_space = strlen( ldlibs->error ); + + ldlibs->error = + realloc( ldlibs->error, prev_space + extra_space + 2 ); + append_here = ldlibs->error + prev_space; + end = ldlibs->error + prev_space + extra_space + 1; + memset( append_here, 0, extra_space + 2 ); + + for( int i = 0; (i < DSO_LIMIT) && ldlibs->not_found[i]; i++ ) + { + append_here += + snprintf( append_here, end - append_here, + " %s", ldlibs->not_found[i] ); + free( ldlibs->not_found[i] ); + ldlibs->not_found[i] = NULL; + } + } +} + +// wrapper to format any accumulated errors and similar after +// invoking the actual dso iterator: returns true if we gathered +// all the needed info witout error, false otherwise: +static int +dso_iterate_sections (ld_libs_t *ldlibs, int idx) +{ + _dso_iterate_sections( ldlibs, idx ); + _dso_iterator_format_error( ldlibs ); + + return ldlibs->error == NULL; +} + + +//////////////////////////////////////////////////////////////////////////// +// public API: +int +ld_libs_init (ld_libs_t *ldlibs, + const char **exclude, + const char *prefix, + unsigned long dbg, + int *errcode) +{ + memset( ldlibs, 0, sizeof(ld_libs_t) ); + + ld_cache_close( &ldlibs->ldcache ); + + ldlibs->elf_class = ELFCLASSNONE; + ldlibs->elf_machine = EM_NONE; + ldlibs->exclude = exclude; + ldlibs->debug = dbg; + + if( errcode ) + *errcode = 0; + + // super important, 0 is valid but is usually stdin, + // don't want to go stomping all over that by accident: + for( int x = 0; x < DSO_LIMIT; x++ ) + ldlibs->needed[x].fd = -1; + + if( elf_version(EV_CURRENT) == EV_NONE ) + { + if( errcode ) + *errcode = elf_errno(); + + return 0; + } + + set_elf_constraints( ldlibs ); + // ================================================================== + // set up the path prefix at which we expect to find the encapsulated + // library and its ld.so.cache and dependencies and so forth: + if( prefix ) + { + size_t prefix_len = strlen( prefix ); + ssize_t space = PATH_MAX - prefix_len; + + // if we don't have at least this much space it's not + // going to work out: + if( (space - strlen( "/usr/lib/libx.so.x" )) <= 0 ) + { + ldlibs->error = + strdup( "capsule_dlmopen: prefix is too large" ); + + if( errcode ) + *errcode = ENAMETOOLONG; + + return 0; + } + + safe_strncpy( ldlibs->prefix.path, prefix, PATH_MAX ); + ldlibs->prefix.len = prefix_len; + } + else + { + ldlibs->prefix.path[0] = '\0'; + ldlibs->prefix.len = 0; + } + + return 1; +} + +// set up the starting point for our capsule load +// (ie the DSO we actually want): +// returns false on error: +int +ld_libs_set_target (ld_libs_t *ldlibs, const char *target) +{ + int rv = dso_find( target, ldlibs, 0 ); + + // failed but we don't have an error message: + if( !rv && !ldlibs->error ) + { + int elf_rv; + + // try for a libelf error message, fall back otherwise: + if( (elf_rv = elf_errno()) ) + ldlibs->error = strdup( elf_errmsg(elf_rv) ); + else + ldlibs->error = strdup( "ld_libs_set_target: could not open dso"); + } + + return rv; +} + +int +ld_libs_find_dependencies (ld_libs_t *ldlibs) +{ + return dso_iterate_sections( ldlibs, 0 ); +} + +// And now we actually open everything we have found, in reverse +// dependency order (which prevents dlmopen from going and finding +// DT_NEEDED values from outside the capsule), which it will do +// if we don't work backwards: +void * +ld_libs_load (ld_libs_t *ldlibs, Lmid_t *namespace, int flag, int *error) +{ + int go; + Lmid_t lm = (*namespace > 0) ? *namespace : LM_ID_NEWLM; + void *ret = NULL; + + if( !flag ) + flag = RTLD_LAZY; + + do + { + go = 0; + + for( int j = 0; j < DSO_LIMIT; j++ ) + { + // reached the end of the list + if( !ldlibs->needed[j].name ) + continue; + + // library has no further dependencies which have not already + // been satisfied (except for the libc and linker DSOs), + // this means we can safely open it without dlmopen accidentally + // pulling in DSOs from outside the encapsulated tree: + if( ldlibs->needed[j].depcount == 0 ) + { + const char *path = ldlibs->needed[j].path; + go++; + + LDLIB_DEBUG( ldlibs, DEBUG_CAPSULE, + "DLMOPEN %p %s %s", + (void *)lm, _rtldstr(flag), path ); + + // The actual dlmopen. If this was the first one, it may + // have created a new link map id, wich we record later on: + + // note that since we do the opens in reverse dependency order, + // the _last_ one we open will be the DSO we actually asked for + // so if we succeed, ret has to contain the right handle. + ret = dlmopen( lm, path, flag ); + + if( !ret ) + { + ldlibs->error = dlerror(); + + if( error ) + *error = EINVAL; + + return NULL; + } + + // If this was the first dlmopen, record the new LM Id + // for return to our caller: + if( lm == LM_ID_NEWLM ) + { + dlinfo( ret, RTLD_DI_LMID, namespace ); + lm = *namespace; + LDLIB_DEBUG( ldlibs, DEBUG_CAPSULE, + "new Lmid_t handle %p\n", (void *)lm ); + } + + // go through the map of DSOs and reduce the dependency + // count for any DSOs which had the current DSO as a dep: + for( int k = 0; k < DSO_LIMIT; k++ ) + if( ldlibs->needed[j].requestors[k] ) + ldlibs->needed[k].depcount--; + + clear_needed( &ldlibs->needed[j] ); + } + } + } while (go); + + return ret; +} + +void +ld_libs_finish (ld_libs_t *ldlibs) +{ + for( int i = 0; i < DSO_LIMIT; i++ ) + clear_needed( &ldlibs->needed[i] ); + + for( int i = ldlibs->last_not_found; i >= 0; i-- ) + { + free( ldlibs->not_found[i] ); + ldlibs->not_found[i] = NULL; + } + + ldlibs->last_not_found = 0; + + ld_cache_close( &ldlibs->ldcache ); + + ldlibs->last_idx = 0; + ldlibs->elf_class = ELFCLASSNONE; + ldlibs->elf_machine = EM_NONE; + + ldlibs->prefix.len = 0; + ldlibs->prefix.path[0] = '\0'; + + if( ldlibs->error ) + free( ldlibs->error ); + + ldlibs->error = NULL; +} diff --git a/utils/ld-libs.h b/utils/ld-libs.h new file mode 100644 index 0000000000000000000000000000000000000000..2a8f347ca90c95e548e5fb7b53412e0b3403fb9f --- /dev/null +++ b/utils/ld-libs.h @@ -0,0 +1,50 @@ +#pragma once + +#include <limits.h> +#include <libelf.h> +#include <gelf.h> + +#include "ld-cache.h" + +// we only handle up to this many library dependencies - +// yes, hardwired limits are bad but there's already enough complexity +// here - can revisit this decision if it ever becomes close to being +// an issue (shouldn't affect the api or abi): +#define DSO_LIMIT 256 + +typedef struct +{ + int fd; + char *name; + char path[PATH_MAX]; + int requestors[DSO_LIMIT]; + int depcount; + Elf *dso; +} dso_needed_t; + +typedef struct +{ + ldcache_t ldcache; + int last_idx; + int elf_class; + Elf64_Half elf_machine; + struct { char path[PATH_MAX]; size_t len; } prefix; + const char **exclude; + dso_needed_t needed[DSO_LIMIT]; + char *not_found[DSO_LIMIT]; + char *error; + int last_not_found; + unsigned long debug; +} ld_libs_t; + +int ld_libs_init (ld_libs_t *ldlibs, + const char **exclude, + const char *prefix, + unsigned long dbg, + int *error); + +int ld_libs_set_target (ld_libs_t *ldlibs, const char *target); +int ld_libs_find_dependencies (ld_libs_t *ldlibs); +void ld_libs_finish (ld_libs_t *ldlibs); + +void *ld_libs_load (ld_libs_t *ldlibs, Lmid_t *namespace, int flag, int *error); diff --git a/utils/utils.h b/utils/utils.h index e2fc79d41c6ee1b0963f8855240cbe0cf4a2c95e..035a2260117934df9a3049b70bd8c4c1c7b97b23 100644 --- a/utils/utils.h +++ b/utils/utils.h @@ -17,6 +17,7 @@ #pragma once +#include <link.h> // for __ELF_NATIVE_CLASS #include "debug.h" #if __ELF_NATIVE_CLASS == 64