[PATCH v8 2/5] Add system-wide tunables: cache ld.so.cache
Arjun Shankar
arjun@redhat.com
Wed Jun 24 01:16:56 GMT 2026
Hi DJ,
> The purpose of this change is twofold:
>
> 1. The ld.so.cache is cached in memory and only re-read if/when
> it changes on disk. This allows us to have much more intensive
> security checks in the future, without impacting performance as
> much. It also allows for cases where the cache is corrupted -
> we continue using the last valid one.
>
> 2. We break out the load/check logic so that the cache can be
> loaded independently of the library lookup, such as for
> code that only needs to look at the extensions.
This looks good to me.
Reviewed-by: Arjun Shankar <arjun@redhat.com>
> ---
> elf/Makefile | 4 +
> elf/dl-cache.c | 260 ++++++++++++------
> elf/tst-ldconfig-cache.c | 134 +++++++++
> elf/tst-ldconfig-cache.root/etc/ld.so.conf | 3 +
> elf/tst-ldconfig-cache.root/ldconfig.req | 0
> .../tst-ldconfig-cache.script | 7 +
> 6 files changed, 324 insertions(+), 84 deletions(-)
> create mode 100644 elf/tst-ldconfig-cache.c
> create mode 100644 elf/tst-ldconfig-cache.root/etc/ld.so.conf
> create mode 100644 elf/tst-ldconfig-cache.root/ldconfig.req
> create mode 100644 elf/tst-ldconfig-cache.root/tst-ldconfig-cache.script
>
> diff --git a/elf/Makefile b/elf/Makefile
> index 8ce764705c..c162b363a8 100644
> --- a/elf/Makefile
> +++ b/elf/Makefile
> @@ -575,6 +575,7 @@ endif
> tests-container += \
> tst-dlopen-self-container \
> tst-dlopen-tlsmodid-container \
> + tst-ldconfig-cache \
> tst-pldd \
> tst-preload-pthread-libc \
> tst-rootdir \
> @@ -731,6 +732,9 @@ one-hundred = $(foreach x,0 1 2 3 4 5 6 7 8 9, \
> 0$x 1$x 2$x 3$x 4$x 5$x 6$x 7$x 8$x 9$x)
> tst-tls-many-dynamic-modules := \
> $(foreach n,$(one-hundred),tst-tls-manydynamic$(n)mod)
> +tst-ldconfig-cache-modules := \
> + $(foreach n,01 02 03 04 05,tst-tls-manydynamic$(n)mod)
> +$(objpfx)tst-ldconfig-cache.out: $(tst-ldconfig-cache-modules:%=$(objpfx)%.so)
> tst-tls-many-dynamic-modules-dep-suffixes = 0 1 2 3 4 5 6 7 8 9 10 11 12 13 \
> 14 15 16 17 18 19
> tst-tls-many-dynamic-modules-dep = \
> diff --git a/elf/dl-cache.c b/elf/dl-cache.c
> index c1de93f204..07c7e639f5 100644
> --- a/elf/dl-cache.c
> +++ b/elf/dl-cache.c
> @@ -26,11 +26,21 @@
> #include <_itoa.h>
> #include <dl-hwcaps.h>
> #include <dl-isa-level.h>
> +#include <fcntl.h>
> +#include <sys/stat.h>
>
> /* This is the starting address and the size of the mmap()ed file. */
> static struct cache_file *cache;
> static struct cache_file_new *cache_new;
> static size_t cachesize;
> +static struct cache_extension_all_loaded ext;
> +
> +static struct {
> + typeof ((*(struct __stat64_t64 *)0).st_mtime) mtime;
> + typeof ((*(struct __stat64_t64 *)0).st_ino) ino;
> + typeof ((*(struct __stat64_t64 *)0).st_size) size;
> + typeof ((*(struct __stat64_t64 *)0).st_dev) dev;
> +} cache_file_time, new_cache_file_time;
>
> #ifdef SHARED
> /* This is used to cache the priorities of glibc-hwcaps
> @@ -53,6 +63,7 @@ glibc_hwcaps_priorities_free (void)
> free (glibc_hwcaps_priorities);
> glibc_hwcaps_priorities = NULL;
> glibc_hwcaps_priorities_allocated = 0;
> + glibc_hwcaps_priorities_length = 0;
> }
>
> /* Ordered comparison of a hwcaps string from the cache on the left
> @@ -84,10 +95,6 @@ glibc_hwcaps_compare (uint32_t left_index, struct dl_hwcaps_priority *right)
> static void
> glibc_hwcaps_priorities_init (void)
> {
> - struct cache_extension_all_loaded ext;
> - if (!cache_extension_load (cache_new, cache, cachesize, &ext))
> - return;
> -
> uint32_t length = (ext.sections[cache_extension_tag_glibc_hwcaps].size
> / sizeof (uint32_t));
> if (length > glibc_hwcaps_priorities_allocated)
> @@ -374,6 +381,165 @@ _dl_cache_libcmp (const char *p1, const char *p2)
> return *p1 - *p2;
> }
>
> +/* Set the cache back to the "no cache" state, which may include
> + cleaning up a loaded cache. */
> +static void
> +_dl_maybe_unload_ldsocache (void)
> +{
> + if (cache != NULL)
> + __munmap (cache, cachesize);
> +
> + cache = NULL;
> + cache_new = NULL;
> + cachesize = 0;
> +
> +#ifdef SHARED
> + glibc_hwcaps_priorities_free ();
> +#endif
> +}
> +
> +/* Returns TRUE if for any reason the cache needs to be reloaded
> + (including, the first time, loaded). */
> +static bool
> +_dl_check_ldsocache_needs_loading (void)
> +{
> + int rv;
> + static bool copy_old_time = 0;
> + struct __stat64_t64 new_cache_file_stat;
> +
> + /* Save the previous stat every time. We only care when this
> + changes, and we only stat it here, so we can get away with doing
> + the copy now instead of at every single return statement in this
> + function. However, we only need to copy it if the previous stat
> + succeeded. The only way this could be subverted is if the admin
> + moves the file aside, then moves it back, but CACHE would be set
> + to NULL in the interim so that would be detected. */
> + if (copy_old_time)
> + cache_file_time = new_cache_file_time;
> + rv = __fstatat64_time64 (AT_FDCWD, LD_SO_CACHE, &new_cache_file_stat, 0);
> + copy_old_time = (rv >= 0);
> +
> + /* No file to load, but there used to be. Assume user intentionally
> + deleted the cache and act accordingly. */
> + if (rv < 0 && cache != NULL)
> + {
> + _dl_maybe_unload_ldsocache ();
> + return false;
> + }
> +
> + /* No file to load and no loaded cache, so nothing to do. */
> + if (rv < 0)
> + return false;
> +
> + /* Any file is better than no file (likely the first time
> + through). */
> + if (cache == NULL)
> + return true;
> +
> + /* Store the fields we check, in order they're likely to differ. */
> + new_cache_file_time.mtime = new_cache_file_stat.st_mtime;
> + new_cache_file_time.ino = new_cache_file_stat.st_ino;
> + new_cache_file_time.size = new_cache_file_stat.st_size;
> + new_cache_file_time.dev = new_cache_file_stat.st_dev;
> +
> + /* At this point, NEW_CACHE_FILE_TIME is valid as well as
> + CACHE_FILE_TIME, so we compare them. */
> + return (memcmp (&new_cache_file_time, &cache_file_time,
> + sizeof(new_cache_file_time)));
> +}
> +
> +/* Attempts to load and validate the cache. On return, CACHE is either
> + unchanged (still loaded or still not loaded) or valid. */
> +static void
> +_dl_maybe_load_ldsocache (void)
> +{
> + struct cache_file *tmp_cache = NULL;
> + struct cache_file_new *tmp_cache_new = NULL;
> + size_t tmp_cachesize = 0;
> +
> + /* Read the contents of the file. */
> + void *file = _dl_sysdep_read_whole_file (LD_SO_CACHE, &tmp_cachesize,
> + PROT_READ);
> +
> + /* We can handle three different cache file formats here:
> + - only the new format
> + - the old libc5/glibc2.0/2.1 format
> + - the old format with the new format in it
> + The following checks if the cache contains any of these formats. */
> + if (file != MAP_FAILED && tmp_cachesize > sizeof *cache_new
> + && memcmp (file, CACHEMAGIC_VERSION_NEW,
> + sizeof CACHEMAGIC_VERSION_NEW - 1) == 0
> + /* Check for corruption, avoiding overflow. */
> + && ((tmp_cachesize - sizeof *cache_new) / sizeof (struct file_entry_new)
> + >= ((struct cache_file_new *) file)->nlibs))
> + {
> + if (! cache_file_new_matches_endian (file))
> + {
> + __munmap (file, tmp_cachesize);
> + return;
> + }
> +
> + tmp_cache_new = file;
> + tmp_cache = file;
> + }
> + else if (file != MAP_FAILED && tmp_cachesize > sizeof *cache
> + && memcmp (file, CACHEMAGIC, sizeof CACHEMAGIC - 1) == 0
> + /* Check for corruption, avoiding overflow. */
> + && ((tmp_cachesize - sizeof *cache) / sizeof (struct file_entry)
> + >= ((struct cache_file *) file)->nlibs))
> + {
> + size_t offset;
> + /* Looks ok. */
> + tmp_cache = file;
> +
> + /* Check for new version. */
> + offset = ALIGN_CACHE (sizeof (struct cache_file)
> + + tmp_cache->nlibs * sizeof (struct file_entry));
OK. Now correctly using tmp_cache instead of cache.
> +
> + tmp_cache_new = (struct cache_file_new *) ((void *) tmp_cache + offset);
> + if (tmp_cachesize < (offset + sizeof (struct cache_file_new))
> + || memcmp (tmp_cache_new->magic, CACHEMAGIC_VERSION_NEW,
> + sizeof CACHEMAGIC_VERSION_NEW - 1) != 0)
> + tmp_cache_new = NULL;
> + else
> + {
> + if (! cache_file_new_matches_endian (tmp_cache_new))
> + /* The old-format part of the cache is bogus as well
> + if the endianness does not match. (But it is
> + unclear how the new header can be located if the
> + endianness does not match.) */
> + {
> + __munmap (file, tmp_cachesize);
> + return;
> + }
> + }
> + }
> + else
> + {
> + if (file != MAP_FAILED)
> + __munmap (file, tmp_cachesize);
> + return;
> + }
> +
> + struct cache_extension_all_loaded tmp_ext;
> + if (!cache_extension_load (tmp_cache_new, tmp_cache, tmp_cachesize, &tmp_ext))
> + {
> + /* The extension is corrupt, so the cache is corrupt. */
> + __munmap (file, tmp_cachesize);
> + return;
> + }
OK. Now munmap'ing before return here.
> +
> + /* If we've gotten here, the loaded cache is good and we need to
> + save it. */
> + _dl_maybe_unload_ldsocache ();
> + cache = tmp_cache;
> + cache_new = tmp_cache_new;
> + cachesize = tmp_cachesize;
> + ext = tmp_ext;
> +
> + assert (cache != NULL);
> +}
> +
>
> /* Look up NAME in ld.so.cache and return the file name stored there, or null
> if none is found. The cache is loaded if it was not already. If loading
> @@ -389,81 +555,14 @@ _dl_load_cache_lookup (const char *name)
> if (__glibc_unlikely (GLRO(dl_debug_mask) & DL_DEBUG_LIBS))
> _dl_debug_printf (" search cache=%s\n", LD_SO_CACHE);
>
> - if (cache == NULL)
> - {
> - /* Read the contents of the file. */
> - void *file = _dl_sysdep_read_whole_file (LD_SO_CACHE, &cachesize,
> - PROT_READ);
> -
> - /* We can handle three different cache file formats here:
> - - only the new format
> - - the old libc5/glibc2.0/2.1 format
> - - the old format with the new format in it
> - The following checks if the cache contains any of these formats. */
> - if (file != MAP_FAILED && cachesize > sizeof *cache_new
> - && memcmp (file, CACHEMAGIC_VERSION_NEW,
> - sizeof CACHEMAGIC_VERSION_NEW - 1) == 0
> - /* Check for corruption, avoiding overflow. */
> - && ((cachesize - sizeof *cache_new) / sizeof (struct file_entry_new)
> - >= ((struct cache_file_new *) file)->nlibs))
> - {
> - if (! cache_file_new_matches_endian (file))
> - {
> - __munmap (file, cachesize);
> - file = (void *) -1;
> - }
> - cache_new = file;
> - cache = file;
> - }
> - else if (file != MAP_FAILED && cachesize > sizeof *cache
> - && memcmp (file, CACHEMAGIC, sizeof CACHEMAGIC - 1) == 0
> - /* Check for corruption, avoiding overflow. */
> - && ((cachesize - sizeof *cache) / sizeof (struct file_entry)
> - >= ((struct cache_file *) file)->nlibs))
> - {
> - size_t offset;
> - /* Looks ok. */
> - cache = file;
> -
> - /* Check for new version. */
> - offset = ALIGN_CACHE (sizeof (struct cache_file)
> - + cache->nlibs * sizeof (struct file_entry));
> -
> - cache_new = (struct cache_file_new *) ((void *) cache + offset);
> - if (cachesize < (offset + sizeof (struct cache_file_new))
> - || memcmp (cache_new->magic, CACHEMAGIC_VERSION_NEW,
> - sizeof CACHEMAGIC_VERSION_NEW - 1) != 0)
> - cache_new = (void *) -1;
> - else
> - {
> - if (! cache_file_new_matches_endian (cache_new))
> - {
> - /* The old-format part of the cache is bogus as well
> - if the endianness does not match. (But it is
> - unclear how the new header can be located if the
> - endianness does not match.) */
> - cache = (void *) -1;
> - cache_new = (void *) -1;
> - __munmap (file, cachesize);
> - }
> - }
> - }
> - else
> - {
> - if (file != MAP_FAILED)
> - __munmap (file, cachesize);
> - cache = (void *) -1;
> - }
> + if (_dl_check_ldsocache_needs_loading ())
> + _dl_maybe_load_ldsocache ();
>
> - assert (cache != NULL);
> - }
> -
> - if (cache == (void *) -1)
> - /* Previously looked for the cache file and didn't find it. */
> + if (cache == NULL)
> return NULL;
>
> const char *best;
> - if (cache_new != (void *) -1)
> + if (cache_new != NULL)
> {
> const char *string_table = (const char *) cache_new;
> best = search_cache (string_table, cachesize,
> @@ -510,14 +609,7 @@ _dl_load_cache_lookup (const char *name)
> void
> _dl_unload_cache (void)
> {
> - if (cache != NULL && cache != (struct cache_file *) -1)
> - {
> - __munmap (cache, cachesize);
> - cache = NULL;
> - }
> -#ifdef SHARED
> - /* This marks the glibc_hwcaps_priorities array as out-of-date. */
> - glibc_hwcaps_priorities_length = 0;
> -#endif
> + /* Functionality is no longer needed, but kept for internal ABI for
> + now. */
> }
> #endif
> diff --git a/elf/tst-ldconfig-cache.c b/elf/tst-ldconfig-cache.c
> new file mode 100644
> index 0000000000..9f71418b3a
> --- /dev/null
> +++ b/elf/tst-ldconfig-cache.c
> @@ -0,0 +1,134 @@
> +/* Test ldconfig cache is correctly used when changed.
> + Copyright (C) 2026 Free Software Foundation, Inc.
> + This file is part of the GNU C Library.
> +
> + The GNU C Library is free software; you can redistribute it and/or
> + modify it under the terms of the GNU Lesser General Public License as
> + published by the Free Software Foundation; either version 2.1 of the
> + License, or (at your option) any later version.
> +
> + The GNU C Library is distributed in the hope that it will be useful,
> + but WITHOUT ANY WARRANTY; without even the implied warranty of
> + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
> + Lesser General Public License for more details.
> +
> + You should have received a copy of the GNU Lesser General Public
> + License along with the GNU C Library; see the file COPYING.LIB. If
> + not, see <https://www.gnu.org/licenses/>. */
> +
> +/* What we're testing for: We initially load ld.so.cache at startup
> + and remember it. If we detect that ld.so.cache has changed, and we
> + can load it successfully, we replace our remember it. If it
> + doesn't change, or if the new version is corrupted, we continue
> + using the old remembered copy. */
> +
> +#include <fcntl.h>
> +
> +#include <support/support.h>
> +#include <support/check.h>
> +
> +#include <support/xstdio.h>
> +#include <support/xstdlib.h>
> +#include <support/xdlfcn.h>
> +#include <support/xunistd.h>
> +
> +
> +/* Verify that we can (or can't) load one of our test objects. */
> +static void
> +try (int i, int invert)
> +{
> + char dlname[100];
> + char symname[100];
> + int (*proc)(int);
> + void *dl;
> +
> + /* These match the objects copied by tst-ldconfig-cache.script,
> + copied from tst-tls-manydynamic*.so. */
> + sprintf (dlname, "libcache%d.so", i);
> + sprintf (symname, "set_value_%02d", i);
> +
> + dl = dlopen (dlname, RTLD_NOW);
> +
> + if (invert)
> + {
> + /* This is a negative test; if the object doesn't load the test
> + passes. */
> + TEST_VERIFY (dl == NULL);
> + return;
> + }
> + else
> + {
> + /* This is a positive test; if the object doesn't load the test
> + fails. */
> + if (dl == NULL)
> + FAIL_EXIT1 ("error: dlopen: %s\n", dlerror ());
> + }
> +
> + proc = xdlsym (dl, symname);
> + /* We don't need to call the symbol, just make sure it exists. */
> + TEST_VERIFY (proc != NULL);
> +
> + xdlclose (dl);
> +}
> +
> +/* Cause corruption in the cache that should prevent loading it. */
> +static void
> +corrupt (void)
> +{
> + int fd = xopen ("/etc/ld.so.cache", O_RDWR, 0);
> + char bytes[] = { 15, 32, 184, 4 };
> + xwrite (fd, bytes, sizeof(bytes));
> + xclose (fd);
> +}
> +
> +/* Regenerate the cache from ld.so.conf. */
> +static void
> +ldconfig (void)
> +{
> + xsystem ("/sbin/ldconfig -X");
> +}
> +
> +/* Change ld.so.conf to refer to the new directory, and generate a new
> + cache. */
> +static void
> +newpath (const char *p)
> +{
> + FILE *f = xfopen ("/etc/ld.so.conf", "w");
> + fprintf (f, "%s\n", p);
> + xfclose (f);
> +
> + ldconfig ();
> +}
> +
> +static int
> +do_test (void)
> +{
> + /* Test that the cache we started with can still load objects in
> + /a. */
> + try (1, 0);
> +
> + /* Create a new cache that doesn't include /a but corrupt it. Test
> + that we still use the cache with /a in it. */
> + newpath ("/c");
> + corrupt ();
> + try (2, 0);
> +
> + /* Regenerate a clean cache with /a in it and verify we can load
> + objects in /a. */
> + newpath ("/a");
> + try (3, 0);
> +
> + /* Generate a new cache with /b but not /a and make sure objects
> + in /a can't be loaded. */
> + newpath ("/b");
> + try (3, 1);
> +
> + /* But objects in /b can be loaded. */
> + try (4, 0);
> + /* Even multiple times. */
> + try (5, 0);
> +
> + return 0;
> +}
> +
> +#include <support/test-driver.c>
> diff --git a/elf/tst-ldconfig-cache.root/etc/ld.so.conf b/elf/tst-ldconfig-cache.root/etc/ld.so.conf
> new file mode 100644
> index 0000000000..2b4c2d7817
> --- /dev/null
> +++ b/elf/tst-ldconfig-cache.root/etc/ld.so.conf
> @@ -0,0 +1,3 @@
> +/lib
> +/lib64
> +/a
> diff --git a/elf/tst-ldconfig-cache.root/ldconfig.req b/elf/tst-ldconfig-cache.root/ldconfig.req
> new file mode 100644
> index 0000000000..e69de29bb2
> diff --git a/elf/tst-ldconfig-cache.root/tst-ldconfig-cache.script b/elf/tst-ldconfig-cache.root/tst-ldconfig-cache.script
> new file mode 100644
> index 0000000000..4f258cac1e
> --- /dev/null
> +++ b/elf/tst-ldconfig-cache.root/tst-ldconfig-cache.script
> @@ -0,0 +1,7 @@
> +mkdirp 0755 /a
> +cp $B/elf/tst-tls-manydynamic01mod.so /a/libcache1.so
> +cp $B/elf/tst-tls-manydynamic02mod.so /a/libcache2.so
> +cp $B/elf/tst-tls-manydynamic03mod.so /a/libcache3.so
> +mkdirp 0755 /b
> +cp $B/elf/tst-tls-manydynamic04mod.so /b/libcache4.so
> +cp $B/elf/tst-tls-manydynamic05mod.so /b/libcache5.so
> --
> 2.47.3
>
--
Arjun Shankar
he/him/his
More information about the Libc-alpha
mailing list