Skip to content

[9/10] libs/libc/elf: Load DT_NEEDED libraries with dlopen() - #20368

Draft
casaroli wants to merge 2 commits into
apache:masterfrom
casaroli:fdpic-dtneeded
Draft

casaroli wants to merge 2 commits into
apache:masterfrom
casaroli:fdpic-dtneeded

Conversation

@casaroli

@casaroli casaroli commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Summary

[8/10] #20131 loads an FDPIC module. A module that names a shared library in DT_NEEDED is still refused, and this loads it and binds the module's imports against it.

dlopen() does the loading. It is already the loader for a shared library, so the work goes there rather than into a dependency walker of the loader's own: 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 opened once and reference counted. Undefined symbols are resolved against the globally registered symbols first, then the opened libraries, then the table exec() supplied. The handles are closed when the module is removed.

Four 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.

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

CONFIG_FDPIC now depends on the flat build. A module's read-only segment is held by a filesystem pin that is given back when the module is unloaded, on a task other than the one that loaded it, so it is held through a reference to the file rather than a descriptor -- and the file interface is not reachable from the loader in the protected and kernel builds. Selecting it there would leak the pin and leave the filesystem unable to compact.

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.

A DT_NEEDED library is one shared instance, its data included, because dlopen() 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.

CONFIG_LIBC_ELF_MAXNEEDED bounds how many libraries one module may name, and defaults to four.

Testing

mps3-an547:picostest builds three ways on top of [8/10]: 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).

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

Review

Draft until [8/10] #20131 merges.

The branch carries that commit as well as this one, so the first commit here is [8/10] and only the second is new. The two cannot be separated: [8/10] adds the case DT_NEEDED that refuses such a module, and this patch replaces that same block with the loading path, so neither applies to the other's absence. The first commit drops out of this PR when #20131 merges.

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

exec() of an FDPIC module now works.  The loader already places such an
object and binds it; what was missing is everything binfmt has to carry
across from the load to the running task.

The task needs the module's data base in its PIC base register.  binfmt
builds a D-Space for any object with a GOT, taking the base from the .got
section address; an FDPIC object names it in DT_PLTGOT instead, which the
loader has already translated, so the two are the same idea reached by
different routes and both are what up_initial_state() installs.

Constructors are not binfmt's business.  A module carries its own crt0,
which walks .init_array on the task that runs the module and then calls
main, so they run in the module's own context and with its own data base.
For a module that arrives through dlopen(), libelf_insert() walks the array
instead, and it enters each entry through fdpic_invoke() because a
descriptor resolved on the calling task carries the wrong base.

The read-only segment of a module that executes in place is held by a
filesystem pin.  The load takes it, and the module owns it from the point
where nothing can fail any more; it is given back when the task that runs
the module exits.  The pin is held through a reference to the file rather
than a descriptor, because the descriptor belongs to the task that called
the loader and the release happens on another one.

libelf_remove() and libelf_uninit() give back what an FDPIC module holds:
the pin, and the writable segment, while the read-only one is media rather
than an allocation and must not be freed.

Built for mps3-an547:picostest with CONFIG_FDPIC both ways.

Assisted-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

dlopen() does the loading.  It is already the loader for a shared library,
so the work goes there rather than into a dependency walker of the loader's
own: 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 opened once and reference counted.
Undefined symbols are resolved against the globally registered symbols
first, then the opened libraries, then the table exec() supplied.  The
handles are closed when the module is removed.

Four 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 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.

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

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

CONFIG_FDPIC now depends on the flat build.  A module's read-only segment
is held by a filesystem pin that has to be given back when the module is
unloaded, which happens on a task other than the one that loaded it, so it
is held through a reference to the file rather than a descriptor -- and the
file interface is not reachable from the loader in the protected and kernel
builds.  Selecting it there would leak the pin and leave the filesystem
unable to compact.

A DT_NEEDED library is one shared instance, its data included, because
dlopen() 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.

Assisted-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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: BINFMT 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.

2 participants