Skip to content
Snippets Groups Projects
  • Vivek Das Mohapatra's avatar
    e96ef3c8
    capsule-dlmopen.c: add some debug output to ELF DSO type checking · e96ef3c8
    Vivek Das Mohapatra authored
    Dump out the class (ELFCLASS32 vs ELFCLASS64) and the machine
    (eg EM_386 vs EM_X86_64) of candidate DSOs.
    
    This matters when the target file tree is multi-arch, anc can contaim
    different classes and machines.
    
    x86 hardware typically has 3:
    
      ELFCLASS32/EM_386    - old school i[3456]86 binary
      ELFCLASS64/EM_X86_64 - straightforward x86-64 binary
      ELFCLASS32/EM_X86_64 - x32 (32 bit word size using x86-64 registers)
    
    But we don't want to open any that don't match the category we
    actually are.
    e96ef3c8
    History
    capsule-dlmopen.c: add some debug output to ELF DSO type checking
    Vivek Das Mohapatra authored
    Dump out the class (ELFCLASS32 vs ELFCLASS64) and the machine
    (eg EM_386 vs EM_X86_64) of candidate DSOs.
    
    This matters when the target file tree is multi-arch, anc can contaim
    different classes and machines.
    
    x86 hardware typically has 3:
    
      ELFCLASS32/EM_386    - old school i[3456]86 binary
      ELFCLASS64/EM_X86_64 - straightforward x86-64 binary
      ELFCLASS32/EM_X86_64 - x32 (32 bit word size using x86-64 registers)
    
    But we don't want to open any that don't match the category we
    actually are.
capsule-dlmopen.c 49.69 KiB
// Copyright © 2017 Collabora Ltd

// This file is part of libcapsule.

// libcapsule is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.

// libcapsule is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Lesser General Public License for more details.

// You should have received a copy of the GNU Lesser General Public
// License along with libcapsule.  If not, see <http://www.gnu.org/licenses/>.

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <limits.h>
#include <errno.h>

#include <sys/types.h>
#include <sys/param.h>
#include <sys/stat.h>
#include <sys/mman.h>

#include <string.h>
#include <fcntl.h>

#include <libelf.h>
#include <gelf.h>
#include <dlfcn.h>

#include "capsule.h"
#include "utils/utils.h"
#include "utils/dump.h"
#include "utils/mmap-info.h"
#include "utils/process-pt-dynamic.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)

#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];
};

/* 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

// ==========================================================================
// And now some definitions related to us handling the ld.so.cache ourselves:

// 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

struct dso_cache_search
{
    int idx;
    const char *name;
};

typedef enum
{
    CACHE_NONE,
    CACHE_NEW ,
    CACHE_OLD ,
} cache_type;

typedef struct
{
    int   fd;
    char *name;
    char  path[PATH_MAX];
    int   requestors[DSO_LIMIT];
    int   depcount;
    Elf  *dso;
} dso_needed_t;

typedef struct
{
    int    cache_fd;
    size_t cache_map_size;
    struct cache_file *cache_mmap;
    const char *cache_data;
    union { struct cache_file *old; struct cache_file_new *new; } cache;
    cache_type ctype;
    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;
} ldlibs_t;

typedef int (*ldcache_entry_cb) (ldlibs_t *ldlibs,
                                 const char *name,
                                 int flag,
                                 unsigned int osv,
                                 uint64_t hwcap,
                                 const char *path,
                                 void *data);

// ==========================================================================
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 );
}

// 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(ldlibs_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 (ldlibs_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 (ldlibs_t *ldlibs, int idx)
{
    GElf_Ehdr ehdr = {};
    int eclass;

    // bogus ELF DSO - no ehdr available?
    if( !gelf_getehdr( ldlibs->needed[ 0 ].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;
}

// make sure all the string buffers are zeroed out
static inline void sanitise_ldlibs(ldlibs_t *ldlibs)
{
    ldlibs->prefix.path[ ldlibs->prefix.len ] = '\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.
void resolve_symlink_prefixed (ldlibs_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 );
}

// 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
ldlib_open (ldlibs_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;
}

// iterate over the ld.so.cache loaded into the ldlibs structure,
// calling cb for each entry, until cb returns true or we run out
// of entries. will return true as soon as cb returns true, or
// false if we get to the end of the cache without cb returning true:
static int
iterate_ldcache (ldlibs_t *ldlibs, ldcache_entry_cb cb, void *data)
{
    int rval = 0;
    const char *base = ldlibs->cache_data;

    switch (ldlibs->ctype)
    {
      case CACHE_OLD:
        for (int i = 0; !rval && (i < ldlibs->cache.old->nlibs); i++)
        {
            struct file_entry *f = &ldlibs->cache.old->libs[i];
            rval = cb( ldlibs, base + f->key, f->flags,
                       0, 0, base + f->value, data );
        }
        break;

      case CACHE_NEW:
        for (int i = 0; !rval && (i < ldlibs->cache.new->nlibs); i++)
        {
            struct file_entry_new *f = &ldlibs->cache.new->libs[i];
            rval = cb( ldlibs, base + f->key, f->flags,
                       f->osversion, f->hwcap, base + f->value, data );
        }
        break;

      default:
        fprintf( stderr, "Invalid ld cache type %d, cannot parse",
                 ldlibs->ctype );
        exit(22);
    }

    return rval;
}

// search callback for search_ldcache. see search_ldcache and iterate_ldcache:
// returning a true value means we found (and set up) the DSO we wanted:
static int
search_ldcache_cb (ldlibs_t *ldlibs,
                   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 = ldlibs->prefix.path;
        size_t plen   = ldlibs->prefix.len;
        char  *lpath  = ldlibs->needed[ idx ].path;

        LDLIB_DEBUG( 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 ldlib_open( 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, ldlibs_t *ldlibs, int i)
{
    struct dso_cache_search target;

    target.idx   = i;
    target.name  = name;

    iterate_ldcache( ldlibs, (ldcache_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, ldlibs_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):
            if( realpath( prefix, ldlibs->needed[i].path ) &&
                ldlib_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, ldlibs_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 ldlib_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 (ldlibs_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 (ldlibs_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 (ldlibs_t *ldlibs, int idx)
{
    _dso_iterate_sections( ldlibs, idx );
    _dso_iterator_format_error( ldlibs );

    return ldlibs->error == NULL;
}

// map the ld.so.cache for the system into memory so that we can search it
// for DSOs in the same way as the dynamic linker.
//
// returns true on success, false otherwise.
//
// this function respects any path prefix specified in ldlibs
static int
load_ld_cache (ldlibs_t *libs, const char *path)
{
    struct stat ldcache = {};
    const char *cachepath;
    int rv;

    if( libs->prefix.len == 0 )
    {
        cachepath = path;
        libs->cache_fd = open( path, O_RDONLY );
    }
    else
    {
        safe_strncpy( libs->prefix.path + libs->prefix.len,
                      path, PATH_MAX - libs->prefix.len );
        cachepath = libs->prefix.path;
        libs->cache_fd = open( libs->prefix.path, O_RDONLY );
    }

    if( libs->cache_fd < 0 )
    {
        fprintf( stderr, "failed to open ld.so cache file %s: %s\n",
                 cachepath, strerror( errno ) );
        goto no_cache;
    }

    fstat( libs->cache_fd, &ldcache );

    // cache file must be at least this big or it's invalid:
    if( ldcache.st_size < sizeof( struct cache_file ) )
        goto no_cache;

    libs->cache_mmap =
      mmap( NULL, ldcache.st_size, PROT_READ, MAP_PRIVATE, libs->cache_fd, 0 );

    if( libs->cache_mmap == MAP_FAILED )
    {
        fprintf( stderr, "failed to mmap ld.so cache file %s: %s\n",
                 path, strerror( errno ) );
        goto no_cache;
    }
    libs->cache_map_size = ldcache.st_size;

    // plain modern (circa 2016) cache map:
    if( memcmp( libs->cache_mmap->magic, CACHEMAGIC, sizeof(CACHEMAGIC) -1 ) )
    {
        DEBUG( DEBUG_LDCACHE, "New format ld cache" );
        libs->cache.new = (struct cache_file_new *)libs->cache_mmap;

        // if the magic strings don't reside at the expected offsets, bail out:
        if( memcmp( libs->cache.new->magic, CACHEMAGIC_NEW,
                    sizeof(CACHEMAGIC_NEW) - 1 ) ||
            memcmp( libs->cache.new->version, CACHE_VERSION,
                    sizeof(CACHE_VERSION) - 1) )
        {
            fprintf( stderr, "invalid cache, expected %s: %s\n",
                     CACHEMAGIC_NEW, CACHE_VERSION );
            goto no_cache;
        }

        libs->cache_data = (char *)libs->cache.new;
        libs->ctype = CACHE_NEW;
    }
    else
    {
        size_t header = sizeof( struct cache_file );
        size_t entry  = sizeof( struct file_entry );
        size_t block  = header + (libs->cache_mmap->nlibs * entry);
        size_t offset = ALIGN_CACHE( block );
        int nlibs = libs->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:
        libs->ctype = CACHE_OLD;

        /* This is where the strings start.  */
        libs->cache_data = (const char *) &libs->cache_mmap->libs[ nlibs ];

        if( libs->cache_map_size > (offset + sizeof( struct cache_file_new )) )
        {
            libs->cache.new = (void *)libs->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( libs->cache.new->magic, CACHEMAGIC_NEW,
                        sizeof(CACHEMAGIC_NEW) - 1 ) ||
                memcmp( libs->cache.new->version, CACHE_VERSION,
                        sizeof(CACHE_VERSION) - 1) )
            {
                // nope, no encapsulated new cache:
                libs->cache.old = libs->cache_mmap;
            }
            else
            {
                DEBUG( DEBUG_LDCACHE, "... with a new style cache inside" );
                libs->ctype = CACHE_NEW;
                libs->cache_data = (char *)libs->cache.new;
            }
        }
    }

    if( libs->cache_fd >= 0 )
    {
        DEBUG( DEBUG_LDCACHE, "Opened ld.cache at %s", cachepath );
        rv = 1;
    }
    else
    {
        DEBUG( DEBUG_LDCACHE, "No ld.cache at %s", cachepath );
        rv = 0;
    }

    sanitise_ldlibs( libs );
    return rv;

no_cache:

    if( libs->cache_fd >= 0 )
        close( libs->cache_fd );

    if( libs->cache_mmap != MAP_FAILED && libs->cache_mmap != NULL )
        munmap( libs->cache_mmap, libs->cache_map_size );

    libs->cache_fd       = -1;
    libs->cache_map_size = 0;
    libs->ctype          = CACHE_NONE;
    libs->cache_mmap     = NULL;
    libs->cache_data     = NULL;
    libs->cache.new      = NULL;

    return 0;
}

static void
cleanup_ldlibs (ldlibs_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;

    if( ldlibs->cache_mmap )
    {
        munmap( ldlibs->cache_mmap, ldlibs->cache_map_size );
        ldlibs->cache_map_size = 0;
    }
    // these are into the region we just munmap()ed
    ldlibs->cache_data = NULL;
    ldlibs->cache.new  = NULL;
    ldlibs->cache.old  = NULL;

    if( ldlibs->cache_fd >= 0 )
    {
        close( ldlibs->cache_fd );
        ldlibs->cache_fd = -1;
    }

    ldlibs->ctype = CACHE_NONE;
    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;
}

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];
}

// 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:
static void *
load_ldlibs (ldlibs_t *ldlibs, Lmid_t *namespace, int flag, int *errcode, char **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 )
                {
                    if( error )
                        *error = dlerror();

                    if( errcode )
                        *errcode = 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;
}

static void
init_ldlibs (ldlibs_t *ldlibs,
             const char **exclude,
             const char *prefix,
             unsigned long dbg,
             int *errcode,
             char **error)
{
    memset( ldlibs, 0, sizeof(ldlibs_t) );
    ldlibs->cache_fd    = -1;
    ldlibs->ctype       = CACHE_NONE;
    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;

    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 )
        {
            if( error )
                *error = strdup( "capsule_dlmopen: prefix is too large" );

            if( errcode )
                *errcode = ENAMETOOLONG;

            return;
        }

        safe_strncpy( ldlibs->prefix.path, prefix, PATH_MAX );
        ldlibs->prefix.len = prefix_len;
    }
    else
    {
        ldlibs->prefix.path[0] = '\0';
        ldlibs->prefix.len     = 0;
    }
}

// ==========================================================================
// some pretty printers for debugging:
static int
dump_ld_entry (ldlibs_t *ldlibs,
               const char *name,
               int flag,
               unsigned int osv,
               uint64_t hwcap,
               const char *path,
               void *cb_data)
{
    fprintf( stderr, "%s → %s\n", name, path );

    fputs( "  type: ", stderr );
    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], stderr );
        break;
      default:
        fputs ("???", stdout);
        break;
    }
    fputs( "\n", stderr );

    fputs( "  requires: ", stderr );
    switch (flag & FLAG_REQUIRED_MASK)
    {
      case FLAG_SPARC_LIB64:
        fputs ("Sparc 64bit", stderr);
        break;
      case FLAG_IA64_LIB64:
        fputs ("IA-64", stderr);
        break;
      case FLAG_X8664_LIB64:
        fputs ("x86-64", stderr);
        break;
      case FLAG_S390_LIB64:
        fputs ("s390 64bit", stderr);
        break;
      case FLAG_POWERPC_LIB64:
        fputs ("PPC 64bit", stderr);
        break;
      case FLAG_MIPS64_LIBN32:
        fputs ("MIPS N32", stderr);
        break;
      case FLAG_MIPS64_LIBN64:
        fputs ("MIPS 64bit", stderr);
        break;
      case FLAG_X8664_LIBX32:
        fputs ("x32", stderr);
        break;
      case FLAG_ARM_LIBHF:
        fputs ("Arm hard-float", stderr);
        break;
      case FLAG_AARCH64_LIB64:
        fputs ("AArch64", stderr);
        break;
        /* Uses the ARM soft-float ABI.  */
      case FLAG_ARM_LIBSF:
        fputs ("Arm soft-float", stderr);
        break;
      case FLAG_MIPS_LIB32_NAN2008:
        fputs ("MIPS nan2008", stderr);
      break;
      case FLAG_MIPS64_LIBN32_NAN2008:
        fputs ("MIPS N32 nan2008", stderr);
        break;
      case FLAG_MIPS64_LIBN64_NAN2008:
        fputs ("IPS 64bit nan2008", stderr);
        break;
      case 0:
        break;
      default:
        fprintf (stderr, "%0x", flag & FLAG_REQUIRED_MASK);
        break;
    }
    fputs( "\n", stderr );

    unsigned int os = osv >> 24;

    fprintf( stderr, "  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( stderr, "  hwcaps: %0"FMT_XU64"\n", hwcap );
    return 0;
}

// dump out the contents of the ld cache to stderr:
static void
dump_ld_cache (ldlibs_t *ldlibs)
{
    iterate_ldcache( ldlibs, dump_ld_entry, NULL );
}

static void
wrap (const char *name,
      ElfW(Addr) base,
      ElfW(Dyn) *dyn,
      capsule_item_t *wrappers)
{
    int mmap_errno = 0;
    char *mmap_error = NULL;
    ElfW(Addr) start = (ElfW(Addr)) dyn - base;
    // we don't know the size so we'll have to rely on the linker putting
    // well formed entries into the mmap()ed DSO region.
    // (tbf if the linker is putting duff entries here we're boned anyway)
    //
    // the utility functions expect an upper bound though so set that to
    // something suitably large:
    size_t size = SIZE_MAX - base - (ElfW(Addr)) dyn;
    relocation_data_t rdata = { 0 };

    rdata.target    = name;
    rdata.debug     = debug_flags;
    rdata.error     = NULL;
    rdata.relocs    = wrappers;

    // if RELRO linking has happened we'll need to tweak the mprotect flags
    // before monkeypatching the symbol tables, for which we will need the
    // sizes, locations and current protections of any mmap()ed regions:
    rdata.mmap_info = load_mmap_info( &mmap_errno, &mmap_error );

    if( mmap_errno || mmap_error )
    {
        DEBUG( DEBUG_MPROTECT,
               "mmap/mprotect flags information load error (errno: %d): %s",
               mmap_errno, mmap_error );
        DEBUG( DEBUG_MPROTECT,
               "relocation will be unable to handle RELRO linked libraries" );
    }

    // make all the mmap()s writable:
    for( int i = 0; rdata.mmap_info[i].start != MAP_FAILED; i++ )
        if( mmap_entry_should_be_writable( &rdata.mmap_info[i] ) )
            add_mmap_protection( &rdata.mmap_info[i], PROT_WRITE );

    // install any required wrappers inside the capsule:
    process_pt_dynamic( (void *)start, // offset from phdr to dyn section
                        size,   //  fake size value (max possible value)
                        base,   //  address of phdr in memory
                        process_dt_rela,
                        process_dt_rel,
                        &rdata );

    // put the mmap()/mprotect() permissions back the way they were:
    for( int i = 0; rdata.mmap_info[i].start != MAP_FAILED; i++ )
        if( mmap_entry_should_be_writable( &rdata.mmap_info[i] ) )
            reset_mmap_protection( &rdata.mmap_info[i] );

    free_mmap_info( rdata.mmap_info );
    rdata.mmap_info = NULL;
}

static inline int
excluded_from_wrap (const char *name, char **exclude)
{
    const char *dso = strrchr(name, '/');

    // we can't ever subvert the runtime linker itself:
    if( strncmp( "/ld-", dso, 4 ) == 0 )
        return 1;

    for( char **x = exclude; x && *x; x++ )
        if( strcmp ( *x, dso + 1 ) == 0 )
            return 1;

    return 0;
}

// replace calls out to dlopen in the encapsulated DSO with a wrapper
// which should take care of preserving the /path-prefix and namespace
// wrapping of the original capsule_dlmopen() call.
//
// strictly speaking we can wrap things other than dlopen(),
// but that's currently all we use this for:
static int install_wrappers ( void *dl_handle,
                              capsule_item_t *wrappers,
                              const char **exclude,
                              int *errcode,
                              char **error)
{
    int replacements = 0;
    struct link_map *map;

    if( dlinfo( dl_handle, RTLD_DI_LINKMAP, &map ) != 0 )
    {
        if( error )
            *error = dlerror();

        if( errcode )
            *errcode = EINVAL;

        DEBUG( DEBUG_WRAPPERS, "mangling capsule symbols: %s", *error );

        return -1;
    }

    DEBUG( DEBUG_WRAPPERS, "link_map: %p <- %p -> %p",
               map ? map->l_next : NULL ,
               map ? map         : NULL ,
               map ? map->l_prev : NULL );

    // no guarantee that we're at either end of the link map:
    while( map->l_prev )
        map = map->l_prev;

    if (map->l_next)
        for( struct link_map *m = map; m; m = m->l_next )
            if( !excluded_from_wrap(m->l_name, (char **)exclude) )
                wrap( m->l_name, m->l_addr, m->l_ld, wrappers );

    return replacements;
}
// ==========================================================================
void *
capsule_dlmopen (const char *dso,
                 const char *prefix,
                 Lmid_t *namespace,
                 capsule_item_t *wrappers,
                 unsigned long dbg,
                 const char **exclude,
                 int *errcode,
                 char **error)
{
    void *ret = NULL;
    ldlibs_t ldlibs = { 0 };

    if( dbg == 0 )
        dbg = debug_flags;

    if( elf_version(EV_CURRENT) == EV_NONE )
    {
        if( error )
            *error = strdup( "capsule_dlmopen: incompatible libelf version" );

        if( errcode )
            *errcode = elf_errno();

        return NULL;
    }

    init_ldlibs( &ldlibs, exclude, prefix, dbg, errcode, error );

    if( errcode && *errcode )
        return NULL;

    // ==================================================================
    // read in the ldo.so.cache - this will contain all architectures
    // currently installed (x86_64, i386, x32) in no particular order
    if( load_ld_cache( &ldlibs, "/etc/ld.so.cache" ) )
    {
        if( debug_flags & DEBUG_LDCACHE )
            dump_ld_cache( &ldlibs );
    }
    else
    {
        int rv = (errno == 0) ? EINVAL : errno;

        if( error )
            *error = strdup( "capsule_dlmopen: failed to read ld.so.cache" );

        if( errcode )
            *errcode = rv;

        return NULL;
    }

    // ==================================================================
    // find the starting point of our capsule
    if( !dso_find( dso, &ldlibs, 0 ) )
    {
        int rv = (errno == 0) ? EINVAL : errno;

        if( error )
        {
            int elf_rv;

            if( ldlibs.error )
            {
                *error = ldlibs.error;
                ldlibs.error = NULL;
            }
            else if( (elf_rv = elf_errno()) )
            {
                *error = strdup( elf_errmsg(elf_rv) );
            }
            else
            {
                *error = strdup( "capsule_dlmopen: could not open dso" );
            }
        }

        if( errcode )
            *errcode = rv;

        goto cleanup;
    }

    // ==================================================================
    // once we have the starting point recursively find all its DT_NEEDED
    // entries, except for the linker itself and libc, which must not
    // be different between the capsule and the "real" DSO environment:
    dso_iterate_sections( &ldlibs, 0 );

    if( ldlibs.error )
    {
        if( error )
        {
            *error = ldlibs.error;
            ldlibs.error = NULL;
        }

        if( errcode )
            *errcode = EINVAL;

        goto cleanup;
    }

    // ==================================================================
    // load the stack of DSOs we need:
    ret = load_ldlibs( &ldlibs, namespace, 0, errcode, error );

    if( !ret )
        goto cleanup;
    // TODO: failure in the dlopen fixup phase should probably be fatal:
    if( ret      != NULL && // no errors so far
        wrappers != NULL )  // have a dlopen fixup function
        install_wrappers( ret, wrappers, exclude, errcode, error );

cleanup:
    cleanup_ldlibs( &ldlibs );
    return ret;
}

void *
capsule_shim_dlopen(Lmid_t ns,
                    const char *prefix,
                    const char **exclude,
                    const char *file,
                    int flag)
{
    void *res = NULL;
    int code = 0;
    char *errors = NULL;
    ldlibs_t ldlibs = { 0 };

    DEBUG( DEBUG_WRAPPERS,
           "dlopen(%s, %x) wrapper: LMID: %ld; prefix: %s;",
           file, flag, ns, prefix );

    if( prefix && strcmp(prefix, "/") )
    {
        init_ldlibs( &ldlibs, exclude, prefix, debug_flags, &code, &errors );

        if( !load_ld_cache( &ldlibs, "/etc/ld.so.cache" ) )
        {
            int rv = (errno == 0) ? EINVAL : errno;

            DEBUG( DEBUG_LDCACHE|DEBUG_WRAPPERS,
                   "Loading ld.so.cache from %s (error: %d)", prefix, rv );
            goto cleanup;
        }

        // find the initial DSO (ie what the caller actually asked for):
        if( !dso_find( file, &ldlibs, 0 ) )
        {
            int rv = (errno == 0) ? EINVAL : errno;

            DEBUG( DEBUG_SEARCH|DEBUG_WRAPPERS,
                           "Not found: %s under %s (error: %d)",
                           file, prefix, rv );
            goto cleanup;
        }

        // harvest all the requested DSO's dependencies:
        dso_iterate_sections( &ldlibs, 0 );

        if( ldlibs.error )
        {
            DEBUG( DEBUG_WRAPPERS, "capsule dlopen error: %s", ldlibs.error );
            goto cleanup;
        }

        // load them up in reverse dependency order:
        res = load_ldlibs( &ldlibs, &ns, flag, &code, &errors );

        if( !res )
            DEBUG( DEBUG_WRAPPERS,
                   "capsule dlopen error %d: %s", code, errors );

        goto cleanup;
    }
    else // no prefix: straightforward dlmopen into our capsule namespace:
    {
        res = dlmopen( ns, file, flag );

        if( !res )
            DEBUG( DEBUG_WRAPPERS,
                   "capsule dlopen error %s: %s", file, dlerror() );
    }

    return res;

cleanup:
    cleanup_ldlibs( &ldlibs );
    return res;
}