Skip to content

libc/dlfcn: Count opens so a library can be shared - #19639

Open
casaroli wants to merge 2 commits into
apache:masterfrom
casaroli:dlopen-refcount
Open

libc/dlfcn: Count opens so a library can be shared#19639
casaroli wants to merge 2 commits into
apache:masterfrom
casaroli:dlopen-refcount

Conversation

@casaroli

@casaroli casaroli commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Summary

dlopen() of a library that is already loaded fails. libelf_insert() rejects a name already in the module registry with EEXIST and dlinsert() passes that straight out, so the second caller gets NULL. POSIX says dlopen() shall return a handle to the object, and today there is no way for two modules to hold the same library at once — which is what a shared library is for.

dlopen() now takes another reference on a library that is already loaded, and dlclose() tears it down only when the last handle goes.

The count lives in the dlfcn layer rather than in libelf_insert(), so insmod keeps its own behaviour: a second insmod of the same name still fails with EEXIST, which is right for a kernel module.

Module names in a PROTECTED build

The module name is what makes any of this possible, and a PROTECTED build did not have one.

Names were defined for CONFIG_BUILD_FLAT or the kernel side of a split build, on the reasoning that only the kernel needed them. That predates dlopen() being usable from user space. Without a name the user-space copy of libelf cannot recognise a second open, cannot count opens, and cannot make dlclose() mean anything — two dlopen()s there produce two independent copies of the library and lose track of the first.

Names are therefore defined wherever CONFIG_LIBC_DLFCN is, which costs NAME_MAX per loaded module in that configuration and nothing otherwise.

Impact

No change for a configuration without CONFIG_LIBC_DLFCN.

insmod/rmmod are unaffected.

BUILD_KERNEL is deliberately untouched: dlopen() returns NULL there unconditionally, because dlinsert() is a stub. Sharing a library between processes with separate address spaces needs the text in a shared region and each process's data at a matching virtual address, which is a different problem from this one.

Testing

Tested by apache/nuttx-apps#3691, which opens a library twice, closes one handle and checks the library is still usable through the other. That test fails on master and passes with this change.

Built mps3-an547:picostest with and without CONFIG_LIBC_DLFCN.

Built stm32f4discovery:kostest, a CONFIG_BUILD_PROTECTED=y configuration, with CONFIG_LIBC_DLFCN enabled — this is the case the name change is for.

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

@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown

MemBrowse Memory Report

No memory changes detected for:

Comment thread libs/libc/dlfcn/lib_dlopen.c Outdated
xiaoxiang781216
xiaoxiang781216 previously approved these changes Aug 3, 2026
@cederom cederom added the breaking change This change requires a mitigation entry in the release notes. label Aug 3, 2026
@github-actions github-actions Bot added Area: Documentation Improvements or additions to documentation Size: M The size of the change in this PR is medium and removed breaking change This change requires a mitigation entry in the release notes. Area: OS Components OS Components issues Size: S The size of the change in this PR is small labels Aug 3, 2026
dlopen() of a library that is already loaded fails.  libelf_insert()
rejects a name that is already in the module registry with EEXIST, and
dlinsert() passes that straight out, so the second caller gets NULL.
POSIX says dlopen() shall return a handle to the object, and there is no
way today for two modules to hold the same library at once -- which is
what a shared library is for.

So dlopen() now takes another reference on a library that is already
there, and dlclose() only tears it down when the last handle goes.  The
count lives in the dlfcn layer rather than in libelf_insert() so that
insmod keeps its own behaviour: a second insmod of the same name still
fails with EEXIST, which is right for a kernel module.

The module name is what makes any of this possible, and a PROTECTED build
did not have one.  Names were defined for CONFIG_BUILD_FLAT or the kernel
side of a split build, on the reasoning that only the kernel needed them,
which predates dlopen() being usable from user space.  Without a name the
user-space copy of libelf cannot recognise a second open of a library,
cannot count opens, and cannot make dlclose() mean anything -- two
dlopen()s there produce two independent copies of the library and lose
track of the first.  Names are therefore defined wherever CONFIG_LIBC_DLFCN
is, which costs NAME_MAX per loaded module in that configuration.

The path no longer has to be copied either.  The module name is the
basename of the file and libelf_insert() takes it as a const string, so
dlinsert() finds it with strrchr() instead of handing a writable
duplicate of the whole path to basename().

BUILD_KERNEL is deliberately untouched.  dlopen() returns NULL there
unconditionally: dlinsert() is a stub, because sharing a library between
processes with separate address spaces needs the text in a shared region
and the data per process at a matching virtual address, which is a
different problem from this one.

Built for mps3-an547:picostest with and without CONFIG_LIBC_DLFCN, and
for stm32f4discovery:kostest, a PROTECTED configuration, with it enabled.

Assisted-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Marco Casaroli <marco.casaroli@gmail.com>
Describe the shared library open semantics in the FLAT and PROTECTED
builds: dlopen() of a library that is already loaded returns a handle
to it and takes an additional reference, and the library is unloaded
only when the last handle is closed.  Note the consequences that follow
from having a single instance: libraries are matched by basename, data
is shared by all users, and constructors and destructors run once.

Contrast this with insmod(), which still rejects a duplicate module
name, and note that dlopen() is not implemented in the KERNEL build.

Assisted-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Marco Casaroli <marco.casaroli@gmail.com>

libelf_registry_lock();

if (modp->nopen > 1)

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.

it's better to move the dup check into libelf_insert, so inmod/rmmod could reuse the same code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Area: Documentation Improvements or additions to documentation 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