[RFC 1/3] dlconf: Add support for config file for ld.so

Adhemerval Zanella Netto adhemerval.zanella@linaro.org
Mon May 4 19:32:33 GMT 2026



On 21/04/26 06:00, Łukasz Stelmach wrote:
> From: Mateusz Moscicki <m.moscicki2@samsung.com>
> 
> Configuration allows to specify which ld.so.cache file to use when
> looking for dependencies for a library or binary from a specific path
> (note that setting LD_LIBRARY_PATH or RUNPATH will result in the
> following configuration not being used).

Hi,

This RFC failed to apply [1]. And even after I tried to manually fix it, it
does not build cleanly with --enable-dconf (most likely due some extra
object pulling during ld.so build). Did you create the patchset against an
older glibc version?

I understand this feature make sense in an embedded or multi-stack Linux 
like Tizen, where distinct library subtrees co-exist under different directories;
and these subtrees have their own versions of common libraries.

But I have some design concerns that it would require further discussion:
 
  1. The --enable-dlconf / #ifdef DLCONF compile-time gate: we try to avoid
     configure switch that changes the dynamic linker semantics and this
     ones adds:

     a. A feature that is inaccessible to users if a distro's glibc does
        not enable it.

     b. two separate ld.so code paths to maintain, test, and audit forever.

     The glibc's precedent for optional features that affect ld.so behavior 
     (tunables, audit hooks, hwcaps) is to make them runtime-conditional, not 
     compile-time-conditional.

  2. The two-tier config format adds operational fragility: the extra dat file
     adds extra maintenance and code complexity. Compare to how ldconfig works: 
     it is explicitly integrated into the package manager and init system. This
     feature introduces a second pipeline with no such integration.

  3. No namespace awareness: The feature uses three module-level globals
     (conf_data, cache_list, g_caches/g_dlopen_paths) with no awareness of glibc's 
     link-map namespaces (dlmopen creates separate namespaces). A process using 
     multiple namespaces will have all of them share the same dlconf policy, which
     may not be the intent.

There is also some implementation issues:

  1. The hot-path cost of a failed lookup: when no configuration applies to a 
     binary (the common case for most binaries on a configured system), 
     _dl_map_new_object now does, for every library load:

     a. A lazy mmap of /run/ldconfig.dat
     b. strdup(path) + strtok_r path tokenization
     c. A trie traversal with per-node bounds unchecked pointer arithmetic
     d. Heap allocation of a conf_dat_entry + linked list nodes on a hit
     e. Free of all of the above
 
     It is a lot of heap allocations and we try to avoid it specially during
     the process startup because the initial bump allocator is very limited
     on what it can actually returns to OS.

  2. Thread safety: the lazy load of conf_data is racy:

     static struct conf_dat_entry *dlconf_find_proper_conf_dat_entry (const char *path)
     {
       if (__glibc_unlikely(conf_data == NULL))
       {
         if (file_exists (DLCONF_DAT_PATH))
	   load_conf_dat (DLCONF_DAT_PATH);
       }
       [...]
     }

     The conf_data is a global set by load_conf_dat and if two threads calls
     dlopen concurrently, both can see conf_data == NULL and both call load_conf_dat,
     double-mapping the file and leaking one copy.  The loader uses _dl_load_lock 
     in other places precisely for this reason.

  3. The dlopen_isolation check fires after the library is loaded: In elf/dl-open.c,
     the dlconf_allowed_dlopen check is called after _dl_map_new_object has already 
     mapped and linked the library.

  4. Priority ordering is backwards from Linux convention: Every other .d/ drop-in
     directory convention in Linux (systemd units, udev rules, sysctl.d, modules-load.d) 
     gives higher-numbered files higher priority.

  5. No tests: this patchset adds 3,370 lines of new C code with no new test cases.

Most implementation issues can be fixed, but 5. is really a deal breaker if you intend 
this for inclusion.


However, before sending a new version I think you should consider some alternative 
approaches:

  1. Extend the existing ld.so.cache format: the cache format already has an extension 
     section (cache_extension_tag_glibc_hwcaps is one example). Adding per-library or
     per-directory metadata (alternative cache path, isolation flag) to the existing 
     format would let ldconfig generate it in one step, with no new binary format, 
     no new operational pipeline, and no change to ld.so's startup sequence.

  2. Extend ldconfig to generate per-subtree caches: ldconfig already traverses directory
     trees and it allows it to generate subsidiary cache files (e.g., 
     /etc/ld.so.cache.d/usr-lib-foo.cache) referenced from the main cache. The main cache 
     lookup already happens exactly once per process; adding a redirect there keeps 
     the runtime path clean.

  3. Add a new ld.so option --cache=PATH:DIR,.... Instead of system-wide policy: it lets
     the launcher (a shell script, systemd unit, or container runtime) pass 
     --cache=/etc/ld-foo.so.cache:/usr/lib/foo to ld.so when executing the binary. This 
     is explicit, per-process, requires no new config file format, and composes with 
     existing namespace tools. The isolation use case then becomes the launcher's 
     responsibility, which is arguably where it belongs.

For the three options, I would go for the 1. which seems the cleanest one. And I would
not make it a configure option, instead it will be an optional ldconfg extension as we
did for the hwcap.  It will most likely require a bit of working from the ldconfig and
loader, but it would be cleaner than current proposal.


However, there is also the question whether another design can solve the both the
per-path cache selection, and load-time isolation. One option could be a supervisor
process.

A supervisor process creates a private mount namespace for the target binary and 
bind-mounts the custom cache file over the standard one before execve. The child 
process sees a different /etc/ld.so.cache without any glibc change: 

supervisor:
  1. Read config: /usr/lib/foo/* → cache=/etc/ld-foo.so.cache
  2. fork()
  child:
    3. unshare(CLONE_NEWNS)
    4. mount("--bind", "/etc/ld-foo.so.cache", "/etc/ld.so.cache", ...)
    5. execve("/usr/lib/foo/some_binary", argv, envp)

>From that point on, the entire process — including all libraries transitively 
loaded — sees the custom cache, because the mount namespace is inherited. This 
covers the transitive loading case automatically: when /usr/lib/foo/libfoo.so 
later loads libbar.so, the lookup hits the custom cache.

With Linux user namespaces available (unshare -r), this works without root, which
makes it composable with unprivileged launchers. This is precisely what
bubblewrap/Flatpak does.

To emulate isolation=yes (no fallback to /lib, /usr/lib), the supervisor can do two 
things in the namespace before exec:

  a. overlay default paths with empty dirs:
   mount("tmpfs", "/lib", MS_BIND, ...)
   mount("tmpfs", "/usr/lib", MS_BIND, ...)

  b. use LD_LIBRARY_PATH to take priority:

The dlopen isolation would be more complex, since it would require the supervisor
to injects a small preload library that wraps dlopen() and enforces path policy.
But it is doable.

This can not provide two libraries from different isolated paths use two different 
cache simultaneously, but I am not sure how common and required this scenario is. 
For this case you will indeed need glibc changes.


I do not have a strong opinion which design is better for your usercase, but
I am also a bit worried of the extra ldconfig code and complexity that might
be only used in a very narrow and specific usercases (specially now with runtime
environment leaning more and more of Linux namespaces and kernel-provided isolation).


[1] https://patchwork.sourceware.org/project/glibc/list/?series=60045

> 
> The configuration is stored in:
> 
>     /etc/ldconfig.conf
>     /etc/ldconfig.conf.d/*
> 
> and has a format similar to INI:
> 
>     [additional_cache]
>     cache=/etc/ld-additional.so.cache
> 
>     [somelib_paths]
>     path=/usr/lib/some1/
> 
>     [somelib_paths:ext]
>     path=/usr/lib/some2/
> 
>     [somelib]
>     cache=:additional_cache
>     path=:somelib_paths
>     isolation=yes
>     dlopen_isolation=yes
>     dlopen_path=/usr/lib/some1
>     dlopen_path=/usr/lib/some2
> 
> The names of the sections are not relevant.
> 
>   path= - path from which objects are loaded. The name of other section
>           can be used after ':'.
> 
>   cache= - location of the ld.so.cache file, which will be used for
>            objects loaded from the path=. The name of the other section can be
>            used after ':'.
> 
>   isolation= - if "yes" then the default /etc/ld.so.cache will not be
>   used, as well as the default paths (/lib, /usr/lib) to search for
>   dependencies. If "no" then the default cache and paths will be
>   searched if the specified ld.so.cache does not contain dependencies.
> 
>   dlopen_isolation= - if "yes" then object can only dlopen libraries from
>   paths specified by "dlopen_path"
> 
>   dlopen_path= - the path from which libraries can be loaded by dlopen()
>   when "dlopen_isolation" is set
> 
>   [<section>:ext] - expands the content of the <section>
> 
> Both "path", "cache", "dlopen_path" can occur multiple times.
> 
> Configuration from ldconfig.conf.d/ directory has higher priority than
> ldconfig.conf file.
> Configuration from ldconfig.conf.d/001-ldconfig.conf has higher priority than
> ldconfig.conf.d/002-ldconfig.conf
> 
> Preparing custom ld.so.cache (current version of the script is prepared
> for aarch64):
> 1. Prepare a file with a list of libraries to be included in the
>    resulting ld.so.cache file. For example:
> 
>        # cat list.txt
>        /usr/lib/libc.so.6
>        /usr/lib/some1/libsome1.so
>        /usr/lib/some2/libsome2.so
> 
> 2. Run ld_so_cache_maker.py which is located in packaging/ subdirectory:
> 
>        python3 ld_so_cache_maker.py list.txt ld-additional.so.cache
> 
> Instead of searching for configuration files each time and parsing them, a binary
> file is used in which the configuration is stored as a tree. This makes
> the search time much shorter.
> 
> The ldconfig.dat path is:
> 
>     /run/ldconfig.dat
> 
> The file is generated from the configuration files in:
> 
>     /etc/ldconfig.conf
>     /etc/ldconfig.conf.d/*
> 
> by:
> 
>     ld.so --build-ldconfig-dat
> 
> The content of the ldconfig.dat can be displayed by:
> 
>     ld.so --print-ldconfig-dat
> 
> This is an example output:
> 
>     root:/> ld.so --print-ldconfig-dat
>     /
>     |   isolation: 0
>     |   dlopen_isolation: 0
>     `-- usr/
>         |   isolation: 0
>         |   dlopen_isolation: 0
>         `-- lib/
>             |   isolation: 0
>             |   dlopen_isolation: 0
>             |-- some1/
>             |       isolation: 1
>             |       dlopen_isolation: 1
>             |       caches:
>             |         `-- /etc/ld-additional.so.cache
>             |       dlopen_paths:
>             |         |-- /usr/lib/some1
>             |         `-- /usr/lib/some2
>             `-- some2/
>                     isolation: 1
>                     dlopen_isolation: 1
>                     caches:
>                     `-- /etc/ld-additional.so.cache
>                     dlopen_paths:
>                     |-- /usr/lib/some1
>                     `-- /usr/lib/some2
>     root:~>
> 
> Signed-off-by: Mateusz Mościcki <m.moscicki2@samsung.com>
> Signed-off-by: Łukasz Stelmach <l.stelmach@samsung.com>




More information about the Libc-alpha mailing list