libs/libc/elf: Load the libraries a module names in DT_NEEDED.
Some checks are pending
Build Documentation / build-html (push) Waiting to run
MemBrowse Memory Report / changes-filter (push) Waiting to run
MemBrowse Memory Report / load-targets (push) Waiting to run
MemBrowse Memory Report / identical (push) Blocked by required conditions
MemBrowse Memory Report / analyze (push) Blocked by required conditions

A module that names a shared library in DT_NEEDED now gets it loaded and
its imports bound against it, rather than being refused.

libelf_insert() does the loading, which is what dlopen() calls anyway: the
library lands in the module registry like anything else, its exports come
back through libelf_getsymbol() -- the same call dlsym() uses -- and a
library named by two modules is loaded once.  A bare name is looked for
along LD_LIBRARY_PATH, where dlopen() looks for it.  Undefined symbols
resolve against the globally registered symbols first, then the modules
this one depends on, then the table exec() supplied.  Nothing here calls
into dlfcn, because this loader is also the kernel's module loader, which
has none.

Each library becomes one of the module's dependencies[], and the dependency
holds it in place of the reference libelf_insert() took.  So a library
loaded only for DT_NEEDED is kept by the modules that depend on it, and
libelf_undepend() unloads it with the last of them; one that dlopen() or
insmod also opened stays until that reference goes too.
CONFIG_LIBC_ELF_MAXDEPEND bounds how many libraries a module may name,
which is what it already meant.

Six things had to be fixed to make it work, none of which a build shows.

reldata was a file-scope global.  Loading a library from inside
libelf_relocatedyn() makes that function reentrant, so the nested load
overwrote the outer one's relocation offsets and the module resumed binding
with the library's DT_REL.  It is now per call.

A cross-object call needs the callee's data base, not the caller's.  A
symbol resolved from an FDPIC library comes back as a descriptor, and
R_ARM_FUNCDESC_VALUE was treating it as a code address and pairing it with
the importing module's GOT.  It now copies both words, so the library runs
with its own.

An object with no imports has no PLT and so no DT_PLTGOT, but it still has
a GOT and still has to be entered with it.  Without the fallback its
descriptors carried a data base of zero and the library read its globals
through a null pointer.

R_ARM_FUNCDESC, a pointer to a descriptor, wrapped a library's descriptor
in a second one.  It now stores the library's descriptor as it is.

The flag that says a resolved value is a descriptor was set only for an
import and never cleared, so the next relocation against a symbol of the
module itself took that symbol for a descriptor too.  It is cleared there.

libelf_symname() was static, and reading a DT_NEEDED name needs it.

A module with DT_NEEDED is refused where CONFIG_LIBC_ELF_MAXDEPEND is zero,
since that is where the dependency logic is compiled out.

A DT_NEEDED library is one shared instance, its data included, because the
loader returns the object already in the registry.  A module started with
exec() is different: that path loads the module afresh each time, so two
running instances have separate data while sharing one copy of the text.

Built for mps3-an547:picostest with CONFIG_FDPIC both ways.  Run on
mps2-an500:xipfs under QEMU: fdpicxip solib loads libcounter.so by name out
of DT_NEEDED, two instances share one pinned copy of its text, and the
library is unloaded, and its pin given back, when the second one exits.  A
library also opened with dlopen() stays loaded after its DT_NEEDED user
exits, and dlclose() unloads it.

With CONFIG_ARCH_ADDRENV the program runs in its own address space, which
a library libelf_insert() loads cannot reach, so DT_NEEDED is refused
there as before.

Assisted-by: Claude Code:claude-opus-5-5
Signed-off-by: Marco Casaroli <marco.casaroli@gmail.com>
This commit is contained in:
Marco Casaroli 2026-08-25 11:18:55 +02:00 • committed by Alan C. Assis
parent 1eba351585
commit dcd93b0f67
9 changed files with 364 additions and 88 deletions

View file

@ -296,6 +296,12 @@
#define ARCH_ELFDATA_SET_PLTREL(d, v) (d)->pltrel = (v)
/* Whether the value a relocation resolved to is itself a descriptor, which
* it is when the symbol came from another object rather than from this one.
*/
#define ARCH_ELFDATA_SET_SYMISDESC(d, v) (d)->symisdesc = (v)
/****************************************************************************
* Public Types
****************************************************************************/
@ -315,6 +321,7 @@ struct arch_elfdata_s
uintptr_t gotbase; /* DT_PLTGOT: this object's data base */
uint16_t ndesc; /* Capacity, in descriptors */
uint16_t usedesc; /* Next free slot */
uint8_t symisdesc; /* Symbol value is a descriptor, not code */
/* The pool the descriptors are taken from */

View file

@ -92,12 +92,12 @@
* portion of the build
*/
/* dlopen() needs a name too: it is the only way to tell that a library is
* already loaded.
/* A name is also the only way to tell that a library is already loaded, so
* dlopen() and DT_NEEDED need one.
*/
#if defined(CONFIG_BUILD_FLAT) || defined(__KERNEL__) || \
defined(CONFIG_LIBC_DLFCN)
defined(CONFIG_LIBC_DLFCN) || CONFIG_LIBC_ELF_MAXDEPEND > 0
# define HAVE_LIBC_ELF_NAMES
# define LIBC_ELF_NAMEMAX NAME_MAX
#endif
@ -798,7 +798,9 @@ FAR const void *libelf_getsymbol(FAR void *handle, FAR const char *name);
* Name: libelf_uninit
*
* Description:
* Uninitialize module resources.
* Uninitialize module resources. Gives up everything the module holds,
* the DT_NEEDED libraries included, so the caller must hold the last
* reference.
*
****************************************************************************/

View file

@ -238,6 +238,17 @@ int libelf_reallocbuffer(FAR struct mod_loadinfo_s *loadinfo,
int libelf_freebuffers(FAR struct mod_loadinfo_s *loadinfo);
/****************************************************************************
* Name: libelf_symname
*
* Description:
* Read a name out of a string table into the I/O buffer.
*
****************************************************************************/
int libelf_symname(FAR struct mod_loadinfo_s *loadinfo,
FAR const Elf_Sym *sym, Elf_Off sh_offset);
/****************************************************************************
* Name: libelf_addr
*

View file

@ -32,6 +32,7 @@
#include <assert.h>
#include <inttypes.h>
#include <nuttx/debug.h>
#include <nuttx/envpath.h>
#include <nuttx/arch.h>
#include <nuttx/cache.h>
@ -46,6 +47,14 @@
* Pre-processor Definitions
****************************************************************************/
/* With an address environment the program lives in its own address space,
* which a library loaded by libelf_insert() cannot reach.
*/
#if CONFIG_LIBC_ELF_MAXDEPEND > 0 && !defined(CONFIG_ARCH_ADDRENV)
# define LIBELF_NEEDED
#endif
#define I_REL 0 /* Index into relxxx[] arrays for relocations */
#define I_PLT 1 /* ... for PLTs */
#define N_RELS 2 /* Number of relxxx[] indexes */
@ -67,7 +76,13 @@
#if defined(ARCH_ELFDATA) && defined(ARCH_ELFDATA_SET_PLTREL)
# define ARCH_ELFDATA_PLTREL(v) ARCH_ELFDATA_SET_PLTREL(&arch_data, v)
#else
# define ARCH_ELFDATA_PLTREL(v)
# define ARCH_ELFDATA_PLTREL(v) ((void)(v))
#endif
#if defined(ARCH_ELFDATA) && defined(ARCH_ELFDATA_SET_SYMISDESC)
# define ARCH_ELFDATA_SYMISDESC(v) ARCH_ELFDATA_SET_SYMISDESC(&arch_data, v)
#else
# define ARCH_ELFDATA_SYMISDESC(v) ((void)(v))
#endif
#if defined(ARCH_ELFDATA) && defined(ARCH_ELFDATA_INIT)
@ -93,7 +108,12 @@ typedef struct
int idx;
} Elf_SymCache;
struct
/* Where a dynamic object's relocation tables live. Per load, not per file:
* loading a DT_NEEDED library re-enters this function, and a shared
* instance would be overwritten by the nested load.
*/
struct reldata_s
{
int stroff; /* offset to string table */
int symoff; /* offset to symbol table */
@ -102,12 +122,66 @@ struct
int reloff[2]; /* offset to the relocation section */
int relsz[2]; /* size of relocation table */
int relrela[2]; /* type of relocation type - 0: DT_REL / 1: DT_RELA */
} reldata;
};
/****************************************************************************
* Private Functions
****************************************************************************/
#ifdef LIBELF_NEEDED
/****************************************************************************
* Name: libelf_insertneeded
*
* Description:
* Load a library a module names in DT_NEEDED. A bare name is looked for
* along LD_LIBRARY_PATH, as dlopen() looks for it, and the module is
* registered under its base name, so a library two modules name is loaded
* once and reference counted.
*
* Returned Value:
* The handle of the loaded library, or NULL.
*
****************************************************************************/
static FAR void *libelf_insertneeded(FAR const char *name)
{
FAR const char *modname;
modname = strrchr(name, '/');
modname = modname != NULL ? modname + 1 : name;
#ifdef CONFIG_LIBC_ENVPATH
if (name[0] != '/')
{
FAR void *handle = NULL;
FAR char *fullpath;
ENVPATH_HANDLE env;
env = envpath_init("LD_LIBRARY_PATH");
if (env != NULL)
{
while ((fullpath = envpath_next(env, name)) != NULL)
{
handle = libelf_insert(fullpath, modname);
lib_free(fullpath);
if (handle != NULL)
{
break;
}
}
envpath_release(env);
}
return handle;
}
#endif
return libelf_insert(name, modname);
}
#endif
/****************************************************************************
* Name: libelf_readrels
*
@ -702,6 +776,13 @@ static int libelf_relocatedyn(FAR struct module_s *modp,
int i;
int idx_rel;
int idx_sym;
#ifdef LIBELF_NEEDED
int j;
uintptr_t libs[CONFIG_LIBC_ELF_MAXDEPEND];
int nlibs = 0;
#endif
struct reldata_s reldata;
bool symfromlib;
/* Define potential architecture specific elf data container */
@ -764,6 +845,33 @@ static int libelf_relocatedyn(FAR struct module_s *modp,
case DT_PLTRELSZ:
reldata.relsz[I_PLT] = dyn[i].d_un.d_val;
break;
case DT_NEEDED:
#ifdef LIBELF_NEEDED
/* Remember it; the name lives in the string table, which is
* not located until the loop has seen DT_STRTAB.
*/
if (nlibs >= CONFIG_LIBC_ELF_MAXDEPEND)
{
berr("ERROR: More than %d DT_NEEDED entries\n",
CONFIG_LIBC_ELF_MAXDEPEND);
lib_free(sym);
lib_free(rels);
lib_free(dyn);
return -ENOEXEC;
}
libs[nlibs++] = dyn[i].d_un.d_val;
break;
#else
berr("ERROR: Cannot load a DT_NEEDED library\n");
lib_free(sym);
lib_free(rels);
lib_free(dyn);
return -ENOSYS;
#endif
case DT_PLTGOT:
/* The object's data base. Every function descriptor built
@ -842,9 +950,81 @@ static int libelf_relocatedyn(FAR struct module_s *modp,
loadinfo->gotbase);
}
/* After the loop, because DT_PLTGOT is read there. Both relocation
* tables are walked under this one arch_data, so the pool cursor
* survives from one to the next.
/* Load whatever the object names in DT_NEEDED. This is what dlopen()
* does, through the same libelf_insert(), but the loader is also the
* kernel's module loader, which has no dlfcn.
*/
#ifdef LIBELF_NEEDED
symhdr = &loadinfo->shdr[loadinfo->dsymtabidx];
for (i = 0; i < nlibs; i++)
{
Elf_Sym namesym;
FAR void *handle;
/* The name is a string table offset, which is what st_name is, so
* the existing reader can fetch it.
*/
memset(&namesym, 0, sizeof(namesym));
namesym.st_name = libs[i];
ret = libelf_symname(loadinfo, &namesym,
loadinfo->shdr[symhdr->sh_link].sh_offset);
if (ret < 0)
{
berr("ERROR: DT_NEEDED %d has no name\n", i);
lib_free(sym);
lib_free(rels);
lib_free(dyn);
return ret;
}
handle = libelf_insertneeded((FAR const char *)loadinfo->iobuffer);
if (handle == NULL)
{
berr("ERROR: Cannot open needed library %s\n",
(FAR char *)loadinfo->iobuffer);
lib_free(sym);
lib_free(rels);
lib_free(dyn);
return -ELIBACC;
}
binfo("Opened needed library %s\n", (FAR char *)loadinfo->iobuffer);
/* The dependency holds the library from now on, in place of the
* reference libelf_insert() took, so libelf_undepend() lets it go.
*/
libelf_registry_lock();
ret = libelf_depend(modp, handle);
if (ret >= 0)
{
((FAR struct module_s *)handle)->nopen--;
}
libelf_registry_unlock();
if (ret < 0)
{
berr("ERROR: Cannot depend on %s: %d\n",
(FAR char *)loadinfo->iobuffer, ret);
libelf_remove(handle);
lib_free(sym);
lib_free(rels);
lib_free(dyn);
return ret;
}
}
#endif
/* Must follow the tag loop, which is where DT_PLTGOT is read. Both
* relocation tables are walked under this one arch_data, so a cursor in
* it spans the object.
*/
ARCH_ELFDATA_SETUP(loadinfo);
@ -943,14 +1123,36 @@ static int libelf_relocatedyn(FAR struct module_s *modp,
{
FAR void *ep;
symfromlib = false;
ep = libelf_findglobal(modp, loadinfo, symhdr,
&sym[idx_sym]);
/* libelf_findglobal() searches only the registered
* symbols. A module from exec() has its own export
* table, and an FDPIC module imports its libc there.
/* libelf_findglobal() searches only the globally
* registered symbols, and has left the name in the
* I/O buffer. Try the modules this one depends on,
* its DT_NEEDED libraries among them, then the table
* exec() supplied.
*/
#ifdef LIBELF_NEEDED
for (j = 0; ep == NULL && j < CONFIG_LIBC_ELF_MAXDEPEND;
j++)
{
FAR struct module_s *dep = modp->dependencies[j];
if (dep != NULL)
{
ep = (FAR void *)
libelf_getsymbol(dep,
(FAR char *)loadinfo->iobuffer);
/* An FDPIC object exports descriptors */
symfromlib = ep != NULL && dep->gotbase != 0;
}
}
#endif
if (ep == NULL && exports != NULL)
{
FAR const struct symtab_s *sm;
@ -998,6 +1200,12 @@ static int libelf_relocatedyn(FAR struct module_s *modp,
extsym.st_value = (uintptr_t)ep;
/* Whether the resolved value is itself a descriptor,
* which it is when the symbol came from a library.
*/
ARCH_ELFDATA_SYMISDESC(symfromlib);
ret = up_relocate(rel, &extsym, addr, ARCH_ELFDATA_PARM);
if (ret < 0)
{
@ -1011,10 +1219,9 @@ static int libelf_relocatedyn(FAR struct module_s *modp,
}
else if (loadinfo->fdpic)
{
/* A relocation naming a symbol inside this object. A
* pointer to a static function is emitted against the
* section symbol, so the offset, Thumb bit included, is
* the addend and must not come from the patched word.
/* A symbol defined inside this object. Its value is
* the symbol's own, translated; the addend stays where
* the relocation type expects it.
*/
Elf_Sym defsym = sym[idx_sym];
@ -1022,6 +1229,8 @@ static int libelf_relocatedyn(FAR struct module_s *modp,
defsym.st_value = libelf_addr(loadinfo,
sym[idx_sym].st_value);
ARCH_ELFDATA_SYMISDESC(false);
addr = libelf_addr(loadinfo, rel->r_offset);
if (reldata.relrela[idx_rel] == 1)
@ -1058,6 +1267,8 @@ static int libelf_relocatedyn(FAR struct module_s *modp,
dynsym.st_value = libelf_addr(loadinfo,
*(FAR uint32_t *)addr);
ARCH_ELFDATA_SYMISDESC(false);
ret = up_relocate(rel, &dynsym, addr, ARCH_ELFDATA_PARM);
}
@ -1073,9 +1284,7 @@ static int libelf_relocatedyn(FAR struct module_s *modp,
}
}
/* Hand back what the relocations consumed. The error paths above do
* not bother: the load is being abandoned, so the cursor has no reader.
*/
/* Hand back what the relocations consumed. */
ARCH_ELFDATA_TEARDOWN(loadinfo);

View file

@ -174,6 +174,15 @@ int libelf_undepend(FAR struct module_s *importer)
}
importer->dependencies[i] = NULL;
/* A library loaded for a DT_NEEDED entry is held only by the
* modules that depend on it, so the last of them unloads it.
*/
if (exporter->dependents == 0 && exporter->nopen == 0)
{
libelf_remove(exporter);
}
}
}

View file

@ -42,7 +42,9 @@
* Name: libelf_uninit
*
* Description:
* Uninitialize module resources.
* Uninitialize module resources. Gives up everything the module holds,
* the DT_NEEDED libraries included, so the caller must hold the last
* reference.
*
****************************************************************************/
@ -244,8 +246,8 @@ int libelf_remove(FAR void *handle)
}
/* Give back a reference. The module goes only when the last one does,
* so an rmmod() cannot pull a module out from under a dlopen() that is
* still holding it.
* so an rmmod() cannot pull a module out from under a dlopen(), or from
* under a module that names it in DT_NEEDED.
*/
if (modp->nopen > 1)

View file

@ -77,6 +77,57 @@ extern int nglobals;
* Private Functions
****************************************************************************/
/****************************************************************************
* Name: libelf_symcallback
*
* Description:
* libelf_registry_foreach() callback function. Test if the provided
* module, modp, exports the symbol of interest. If so, return that symbol
* value and setup the module dependency relationship.
*
* Returned Value:
* 0 (OK) is returned on success and a negated errno is returned on
* failure.
*
****************************************************************************/
static int libelf_symcallback(FAR struct module_s *modp, FAR void *arg)
{
FAR struct mod_exportinfo_s *exportinfo = (FAR struct mod_exportinfo_s *)
arg;
/* Check if this module exports a symbol of that name */
exportinfo->symbol = symtab_findbyname(modp->modinfo.exports,
exportinfo->name,
modp->modinfo.nexports);
if (exportinfo->symbol != NULL)
{
/* Yes.. save the dependency relationship and return SYM_FOUND to
* stop the traversal.
*/
#if CONFIG_LIBC_ELF_MAXDEPEND > 0
int ret = libelf_depend(exportinfo->modp, modp);
if (ret < 0)
{
berr("ERROR: libelf_depend failed: %d\n", ret);
return ret;
}
#endif
return SYM_FOUND;
}
return SYM_NOT_FOUND;
}
/****************************************************************************
* Public Functions
****************************************************************************/
/****************************************************************************
* Name: libelf_symname
*
@ -93,8 +144,8 @@ extern int nglobals;
*
****************************************************************************/
static int libelf_symname(FAR struct mod_loadinfo_s *loadinfo,
FAR const Elf_Sym *sym, Elf_Off sh_offset)
int libelf_symname(FAR struct mod_loadinfo_s *loadinfo,
FAR const Elf_Sym *sym, Elf_Off sh_offset)
{
FAR uint8_t *buffer;
off_t offset;
@ -187,57 +238,6 @@ static int libelf_symname(FAR struct mod_loadinfo_s *loadinfo,
return OK;
}
/****************************************************************************
* Name: libelf_symcallback
*
* Description:
* libelf_registry_foreach() callback function. Test if the provided
* module, modp, exports the symbol of interest. If so, return that symbol
* value and setup the module dependency relationship.
*
* Returned Value:
* 0 (OK) is returned on success and a negated errno is returned on
* failure.
*
****************************************************************************/
static int libelf_symcallback(FAR struct module_s *modp, FAR void *arg)
{
FAR struct mod_exportinfo_s *exportinfo = (FAR struct mod_exportinfo_s *)
arg;
/* Check if this module exports a symbol of that name */
exportinfo->symbol = symtab_findbyname(modp->modinfo.exports,
exportinfo->name,
modp->modinfo.nexports);
if (exportinfo->symbol != NULL)
{
/* Yes.. save the dependency relationship and return SYM_FOUND to
* stop the traversal.
*/
#if CONFIG_LIBC_ELF_MAXDEPEND > 0
int ret = libelf_depend(exportinfo->modp, modp);
if (ret < 0)
{
berr("ERROR: libelf_depend failed: %d\n", ret);
return ret;
}
#endif
return SYM_FOUND;
}
return SYM_NOT_FOUND;
}
/****************************************************************************
* Public Functions
****************************************************************************/
/****************************************************************************
* Name: libelf_findsymtab
*

View file

@ -210,28 +210,36 @@ int up_relocate(const Elf32_Rel *rel, const Elf32_Sym *sym, uintptr_t addr,
"at addr=%08" PRIxPTR " to sym=%p st_value=%08" PRIx32 "\n",
addr, sym, sym->st_value);
if (data->pltrel)
if (data->symisdesc)
{
/* Resolved to a function in another object, which published a
* descriptor of its own. Take both words: the callee has to
* run with its own data base, not ours.
*/
*desc = *(struct fdpic_desc_s *)sym->st_value;
}
else if (data->pltrel)
{
/* A lazy descriptor holds its PLT stub address, not an
* addend. Overwrite it, do not add to it.
*/
desc->entry = sym->st_value;
desc->got = data->gotbase;
}
else
{
desc->entry = sym->st_value + desc->entry;
desc->got = data->gotbase;
}
desc->got = data->gotbase;
}
break;
case R_ARM_FUNCDESC:
{
/* A pointer to a descriptor, which the loader has to supply.
* Carve one out of the pool reserved behind the writable segment
* and store its address.
/* A pointer to a descriptor the loader must supply. Carve one
* from the pool and store its address.
*/
struct fdpic_desc_s *desc;
@ -249,6 +257,16 @@ int up_relocate(const Elf32_Rel *rel, const Elf32_Sym *sym, uintptr_t addr,
return -ENOEXEC;
}
/* Another object's function comes with a descriptor of its own,
* which is already what this word must point to.
*/
if (data->symisdesc)
{
*(uint32_t *)addr = sym->st_value;
break;
}
if (data->usedesc >= data->ndesc)
{
berr("ERROR: Out of function descriptors\n");

View file

@ -210,28 +210,36 @@ int up_relocate(const Elf32_Rel *rel, const Elf32_Sym *sym, uintptr_t addr,
"at addr=%08" PRIxPTR " to sym=%p st_value=%08" PRIx32 "\n",
addr, sym, sym->st_value);
if (data->pltrel)
if (data->symisdesc)
{
/* Resolved to a function in another object, which published a
* descriptor of its own. Take both words: the callee has to
* run with its own data base, not ours.
*/
*desc = *(struct fdpic_desc_s *)sym->st_value;
}
else if (data->pltrel)
{
/* A lazy descriptor holds its PLT stub address, not an
* addend. Overwrite it, do not add to it.
*/
desc->entry = sym->st_value;
desc->got = data->gotbase;
}
else
{
desc->entry = sym->st_value + desc->entry;
desc->got = data->gotbase;
}
desc->got = data->gotbase;
}
break;
case R_ARM_FUNCDESC:
{
/* A pointer to a descriptor, which the loader has to supply.
* Carve one out of the pool reserved behind the writable segment
* and store its address.
/* A pointer to a descriptor the loader must supply. Carve one
* from the pool and store its address.
*/
struct fdpic_desc_s *desc;
@ -249,6 +257,16 @@ int up_relocate(const Elf32_Rel *rel, const Elf32_Sym *sym, uintptr_t addr,
return -ENOEXEC;
}
/* Another object's function comes with a descriptor of its own,
* which is already what this word must point to.
*/
if (data->symisdesc)
{
*(uint32_t *)addr = sym->st_value;
break;
}
if (data->usedesc >= data->ndesc)
{
berr("ERROR: Out of function descriptors\n");