[PATCH v5 2/4] Add system-wide tunables: cache ld.so.cache

Adhemerval Zanella Netto adhemerval.zanella@linaro.org
Fri Feb 27 18:36:26 GMT 2026



On 21/02/24 20:47, DJ Delorie wrote:
> 
> 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.

I agree with Yury here that we need to add some tests, at least some
minimal one that stress the ld.so.cache reload (which is not the usual
scenario).  I would suggest add test-containts tests that:

  1. Construct a ld.so.cache, start the test, trigger ld.so.cache
     reconstruct, and check if:

     1.1. A new entry is used on dlopen.

     1.2. If removing an entry triggers a dlopen failure.

     1.3. If not modifying the ld.so.cache contents keep the same
          contents.

  2. Construct a ld.so.cache, start the test, create a invalid/corrupt
     ld.so.cache and check if the test can load new libraries to
     assure the cached load keeps working.

> ---
>  elf/dl-cache.c | 254 ++++++++++++++++++++++++++++++++-----------------
>  1 file changed, 169 insertions(+), 85 deletions(-)
> 
> diff --git a/elf/dl-cache.c b/elf/dl-cache.c
> index 9458ffae2a..4612dea835 100644
> --- a/elf/dl-cache.c
> +++ b/elf/dl-cache.c
> @@ -16,6 +16,7 @@
>     License along with the GNU C Library; if not, see
>     <https://www.gnu.org/licenses/>.  */
>  
> +#include <startup.h>
>  #include <assert.h>
>  #include <unistd.h>
>  #include <ldsodefs.h>
> @@ -25,11 +26,18 @@
>  #include <_itoa.h>
>  #include <dl-hwcaps.h>
>  #include <dl-isa-level.h>
> +#include <sys/types.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 __stat64_t64 cache_file_time;
> +static struct __stat64_t64 new_cache_file_time;
>  
>  #ifdef SHARED
>  /* This is used to cache the priorities of glibc-hwcaps
> @@ -52,6 +60,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
> @@ -83,10 +92,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)
> @@ -373,6 +378,159 @@ _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 int copy_old_time = 0;

Maybe use bool here?

> +
> +  /* 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_time, 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;
> +
> +  /* At this point, NEW_CACHE_FILE_TIME is valid as well as
> +     CACHE_FILE_TIME, so we compare them.  We list fields in the order
> +     they're most likely to be different in.  */
> +  return ((new_cache_file_time.st_mtime != cache_file_time.st_mtime)
> +	  || (new_cache_file_time.st_ino != cache_file_time.st_ino)
> +	  || (new_cache_file_time.st_size != cache_file_time.st_size)
> +	  || (new_cache_file_time.st_dev != cache_file_time.st_dev)
> +	   );

Maybe only keep the required fields, instead of while stat struct?
If we use a struct the test could optimized in bulk by the compiler.

> +}
> +
> +/* Attemps 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 && 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)
> +			    + cache->nlibs * sizeof (struct file_entry));
> +
> +      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.  */
> +    return;
> +
> +  /* 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
> @@ -388,81 +546,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,
> @@ -488,7 +579,7 @@ _dl_load_cache_lookup (const char *name)
>      return NULL;
>  
>    /* The double copy is *required* since malloc may be interposed
> -     and call dlopen itself whose completion would unmap the data
> +     and call dlopen itself whose completion may unmap the data
>       we are accessing. Therefore we must make the copy of the
>       mapping data without using malloc.  */
>    char *temp;
> @@ -506,14 +597,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



More information about the Libc-alpha mailing list