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

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.

could we delay strdup after line 121

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.

basename() may modify its argument; libelf_insert() takes the module name as a const string, so dlinsert() now derives the basename with strrchr(), the same way binfmt/builtin.c does

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