Skip to content

[9/10] libs/libc/elf: Load the libraries a module names in DT_NEEDED - #20368

Open
casaroli wants to merge 1 commit into
apache:masterfrom
casaroli:fdpic-dtneeded
Open

casaroli wants to merge 1 commit into
apache:masterfrom
casaroli:fdpic-dtneeded

Conversation

@casaroli

@casaroli casaroli commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Summary

[8/10] #20131 is merged, so an FDPIC module loads and runs. A module that names a shared library in DT_NEEDED still has nothing to load it with, and this loads it and binds the module's imports against it.

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(), which is the 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.

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

reldata was a file-scope global. Opening 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 per call now.

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 copies both words now, 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.

Impact

Nothing happens without CONFIG_LIBC_DLFCN: a module with DT_NEEDED is refused there, as before, since there is no way to load what it asks for.

With CONFIG_ARCH_ADDRENV a module with DT_NEEDED is refused too, as before: the program runs in its own address space, which a library libelf_insert() loads cannot reach. dlopen() is still a stub in kernel builds, so neither path exists there yet.

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.

Each library becomes one of the module's dependencies[], and the dependency holds it in place of the reference libelf_insert() took. 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 one module may name, which is what it already meant.

Testing

mps3-an547:picostest builds three ways on master: with CONFIG_FDPIC off, with it on, and with it on plus CONFIG_LIBC_DLFCN, which is the path this patch adds. With CONFIG_FDPIC on, the applications the configuration carries are FDPIC objects (OS/ABI: ARM FDPIC).

mps2-an500:xipfs with CONFIG_FDPIC under QEMU, xipfs_test fdpic, with the module fixtures rebuilt by the tree's module flags (apache/nuttx-apps#3762) and #20388: 34 passed, 0 failed. Without the last two fixes the C++ library tests crash.

qemu-armv8a:knsh (CONFIG_ARCH_ADDRENV) links with -Werror; in a flat build elf_bind.o is unchanged by the ADDRENV guard.

tools/checkpatch.sh -c -u -m -g passes.

Review

Everything below this in the series is merged, so it is one commit on master.

acassis
acassis previously approved these changes Sep 25, 2026
@github-actions github-actions Bot added Arch: arm Issues related to ARM (32-bit) architecture Size: M The size of the change in this PR is medium Area: BINFMT labels Sep 25, 2026
@github-actions

github-actions Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

MemBrowse Memory Report

s698pm-dkit

@casaroli
casaroli force-pushed the fdpic-dtneeded branch 3 times, most recently from 6123697 to f0e23e1 Compare September 26, 2026 17:37
@casaroli
casaroli marked this pull request as ready for review September 26, 2026 17:39
acassis
acassis previously approved these changes Sep 27, 2026
Comment thread binfmt/Kconfig Outdated
Comment thread libs/libc/elf/elf_symbols.c
Comment thread libs/libc/elf/elf.h Outdated
Comment thread include/nuttx/lib/elf.h
Comment thread libs/libc/elf/elf_remove.c Outdated
Comment thread libs/libc/elf/elf_bind.c Outdated
@casaroli casaroli changed the title [9/10] libs/libc/elf: Load DT_NEEDED libraries with dlopen() [9/10] libs/libc/elf: Load the libraries a module names in DT_NEEDED Sep 28, 2026
@casaroli
casaroli force-pushed the fdpic-dtneeded branch 3 times, most recently from b67a0d3 to c99788d Compare September 28, 2026 11:14
@github-actions github-actions Bot added Area: OS Components OS Components issues and removed Area: BINFMT labels Sep 28, 2026
Comment thread include/nuttx/lib/elf.h
Comment thread include/nuttx/lib/elf.h
@casaroli
casaroli force-pushed the fdpic-dtneeded branch 2 times, most recently from 9aac60f to 276b6a8 Compare September 28, 2026 13:52
casaroli added a commit to casaroli/nuttx-apps that referenced this pull request Sep 28, 2026
The fixtures were built by makefiles and scripts in nuttx/tools/fdpic.
Review of apache/nuttx#19940 asked that NuttX not carry a module build
of its own, so the build moves here, beside the sources it builds.

They keep a build of their own because they are loader edge cases: a
library with a SONAME, a module with more DT_NEEDED entries than the
loader follows, one whose imports stay in the lazy binding table, and
one naming a symbol the firmware does not export.  Application.mk cannot
say any of that.

The build uses the configured tree's module flags (CMODULEFLAGS,
CXXMODULEFLAGS, LDMODULEFLAGS), linker script and crt0 source.  Three
things are pinned instead, so that the committed headers do not depend
on how the tree is configured: the CPU (cortex-m3, so one set of headers
runs on both v7-M and v8-M), no debug information, and -Os.  A library
is compiled with default visibility, because the module flags hide every
symbol and a library must export its functions.

fdpic-embed.py becomes xxd and a template.  fdpic-verify.sh and
nuttx-exports.sh go with no replacement: they checked at build time what
the xipfs suite already asserts at run time.

The headers are regenerated, and the .fdpic suffix is gone: a module is
named like any other module.  A fixture now starts in the tree's crt0,
which needs apache/nuttx#20388 to run its constructors, and the library
fixtures need apache/nuttx#20368.

Assisted-by: Claude Code:claude-opus-5-5
Signed-off-by: Marco Casaroli <marco.casaroli@gmail.com>
Comment thread libs/libc/machine/arm/armv8-m/arch_elf.c Outdated
Comment thread libs/libc/machine/arm/armv7-m/arch_elf.c Outdated
Comment thread libs/libc/elf/elf_bind.c
****************************************************************************/

/* With an address environment the program lives in its own address space,
* which a library loaded by libelf_insert() cannot reach.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but still can reach as per task list, so let's remove LIBELF_NEEDED and use CONFIG_LIBC_ELF_MAXDEPEND directly

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without this guard qemu-armv8a:knsh does not link. We could fix ld-kernel.script for qemu-armv8a so that it links, but DT_NEEDED would still not work: libelf_insert() loads the library into kernel memory, which the task's address environment does not map.

Ideally the library is loaded only once and mapped into each task that needs it. That needs reference counting across tasks and a much larger design change, so I would like to do it in follow-up PRs. The other option is to load the library into the task's own address space, but then a library used by several tasks is copied into every one of them.

Comment thread libs/libc/elf/elf_bind.c Outdated
Comment thread libs/libc/elf/elf_bind.c Outdated
Comment thread libs/libc/elf/elf_bind.c
casaroli added a commit to casaroli/nuttx-apps that referenced this pull request Sep 29, 2026
The fixtures were built by makefiles and scripts in nuttx/tools/fdpic.
Review of apache/nuttx#19940 asked that NuttX not carry a module build
of its own, so the build moves here, beside the sources it builds.

They keep a build of their own because they are loader edge cases: a
library with a SONAME, a module with more DT_NEEDED entries than the
loader follows, one whose imports stay in the lazy binding table, and
one naming a symbol the firmware does not export.  Application.mk cannot
say any of that.

The build uses the configured tree's module flags (CMODULEFLAGS,
CXXMODULEFLAGS, LDMODULEFLAGS), linker script and crt0 source.  Three
things are pinned instead, so that the committed headers do not depend
on how the tree is configured: the CPU (cortex-m3, so one set of headers
runs on both v7-M and v8-M), no debug information, and -Os.  A library
is compiled with default visibility, because the module flags hide every
symbol and a library must export its functions.

fdpic-embed.py becomes xxd and a template.  fdpic-verify.sh and
nuttx-exports.sh go with no replacement: they checked at build time what
the xipfs suite already asserts at run time.

The headers are regenerated, and the .fdpic suffix is gone: a module is
named like any other module.  A fixture now starts in the tree's crt0,
which needs apache/nuttx#20388 to run its constructors, and the library
fixtures need apache/nuttx#20368.

Assisted-by: Claude Code:claude-opus-5-5
Signed-off-by: Marco Casaroli <marco.casaroli@gmail.com>
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Arch: arm Issues related to ARM (32-bit) architecture Area: OS Components OS Components issues Size: M The size of the change in this PR is medium

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants