[v7,1/1] ldconfig: add --install option

Message ID xnzf0b4t0y.fsf@greed.delorie.com (mailing list archive)
State Committed
Commit 2324aa7aa81a324ea9936a29ed41d6b0fa3ae6e4
Headers
Series [v7,1/1] ldconfig: add --install option |

Checks

Context Check Description
redhat-pt-bot/TryBot-apply_patch success Patch applied to master at the time it was sent
redhat-pt-bot/TryBot-32bit success Build for i686
linaro-tcwg-bot/tcwg_glibc_build--master-arm success Build passed
linaro-tcwg-bot/tcwg_glibc_build--master-aarch64 success Build passed
linaro-tcwg-bot/tcwg_glibc_check--master-aarch64 success Test passed

Commit Message

DJ Delorie June 30, 2026, 9:28 p.m. UTC
  Add --install option, which copies a pre-built ld.so.cache into place,
honoring the cache and root options and defaults.  This gives the user
a canonical "correct" way to install a pre-built cache without risk
of a program trying to load a partially-written file.

Co-authored-by: Adhemerval Zanella Netto <adhemerval.zanella@linaro.org>

--- >8 ---

Changes since v6:
  - split install_cache_file into separate function
  - preserve errno across unlink/close
  - don't use errno from 

Changes since v5:
  - Fixed style issues
  - Make counters ssize_t
  - Fix write count accumulation logic
  - additional error checks
  
Changes since v4:
  - Added testcase
  - Fix open mode

Changes since v3:
  - initialize w because some compilers can't tell initialization
    isn't needed.

Changes since v2:
  - removed "rename if possible" option to avoid removing source file.
    We always copy now.
---
 NEWS                        |   2 +
 elf/Makefile                |   6 ++
 elf/ldconfig.c              | 162 +++++++++++++++++++++++++++++++++++-
 elf/tst-ldconfig-install.sh | 125 ++++++++++++++++++++++++++++
 4 files changed, 293 insertions(+), 2 deletions(-)
 create mode 100644 elf/tst-ldconfig-install.sh
  

Comments

Adhemerval Zanella Netto July 7, 2026, 5:17 p.m. UTC | #1
On 30/06/26 18:28, DJ Delorie wrote:
> 
> Add --install option, which copies a pre-built ld.so.cache into place,
> honoring the cache and root options and defaults.  This gives the user
> a canonical "correct" way to install a pre-built cache without risk
> of a program trying to load a partially-written file.
> 
> Co-authored-by: Adhemerval Zanella Netto <adhemerval.zanella@linaro.org>

This version looks good to me, thanks.

Reviewed-by: Adhemerval Zanella  <adhemerval.zanella@linaro.org>

> 
> --- >8 ---
> 
> Changes since v6:
>   - split install_cache_file into separate function
>   - preserve errno across unlink/close
>   - don't use errno from 
> 
> Changes since v5:
>   - Fixed style issues
>   - Make counters ssize_t
>   - Fix write count accumulation logic
>   - additional error checks
>   
> Changes since v4:
>   - Added testcase
>   - Fix open mode
> 
> Changes since v3:
>   - initialize w because some compilers can't tell initialization
>     isn't needed.
> 
> Changes since v2:
>   - removed "rename if possible" option to avoid removing source file.
>     We always copy now.
> ---
>  NEWS                        |   2 +
>  elf/Makefile                |   6 ++
>  elf/ldconfig.c              | 162 +++++++++++++++++++++++++++++++++++-
>  elf/tst-ldconfig-install.sh | 125 ++++++++++++++++++++++++++++
>  4 files changed, 293 insertions(+), 2 deletions(-)
>  create mode 100644 elf/tst-ldconfig-install.sh
> 
> diff --git a/NEWS b/NEWS
> index f9d90c5194..e30b7a5f76 100644
> --- a/NEWS
> +++ b/NEWS
> @@ -37,6 +37,8 @@ Major new features:
>    status via prctl syscall.  This prevents disabling or corrupting GCS
>    shadow stack during runtime.
>  
> +* Pre-built ld.so.cache files can be installed with ldconfig.
> +
>  Deprecated and removed features, and other changes affecting compatibility:
>  
>  * Although malloc and related functions currently return pointers
> diff --git a/elf/Makefile b/elf/Makefile
> index 789c504da9..3479450191 100644
> --- a/elf/Makefile
> +++ b/elf/Makefile
> @@ -718,6 +718,7 @@ tests-special += $(tests-execstack-special-$(have-z-execstack))
>  ifeq ($(run-built-tests),yes)
>  tests-special += \
>    $(objpfx)tst-ldconfig-X.out \
> +  $(objpfx)tst-ldconfig-install.out \
>    $(objpfx)tst-ldconfig-p.out \
>    $(objpfx)tst-ldconfig-soname.out \
>    $(objpfx)tst-rtld-help.out \
> @@ -2833,6 +2834,11 @@ $(objpfx)tst-ldconfig-X.out : tst-ldconfig-X.sh $(objpfx)ldconfig
>  		 '$(run-program-env)' > $@; \
>  	$(evaluate-test)
>  
> +$(objpfx)tst-ldconfig-install.out : tst-ldconfig-install.sh $(objpfx)ldconfig
> +	$(SHELL) $< '$(common-objpfx)' '$(test-wrapper-env)' \
> +		'$(run-program-env)' > $@; \
> +	$(evaluate-test)
> +
>  $(objpfx)tst-ldconfig-p.out : tst-ldconfig-p.sh $(objpfx)ldconfig
>  	$(SHELL) $< '$(common-objpfx)' '$(sysconfdir)' '$(test-wrapper-env)' \
>  		 '$(run-program-env)' > $@; \
> diff --git a/elf/ldconfig.c b/elf/ldconfig.c
> index 1ea55400f3..a39f3ecfcd 100644
> --- a/elf/ldconfig.c
> +++ b/elf/ldconfig.c
> @@ -109,6 +109,9 @@ static int opt_manual_link;
>  /* Should we ignore an old auxiliary cache file?  */
>  static int opt_ignore_aux_cache;
>  
> +/* Install a pre-existing cache file instead of generating a new one.  */
> +static int opt_install;
> +
>  /* Cache file to use.  */
>  static char *cache_file;
>  
> @@ -141,6 +144,7 @@ static const struct argp_option options[] =
>    { NULL, 'l', NULL, 0, N_("Manually link individual libraries."), 0},
>    { "format", 'c', N_("FORMAT"), 0, N_("Format to use: new (default), old, or compat"), 0},
>    { "ignore-aux-cache", 'i', NULL, 0, N_("Ignore auxiliary cache file"), 0},
> +  { "install", 'I', NULL, 0, N_("Install pre-existing cache file"), 0},
>    { NULL, 0, NULL, 0, NULL, 0 }
>  };
>  
> @@ -209,6 +213,9 @@ parse_opt (int key, char *arg, struct argp_state *state)
>        else if (strcmp (arg, "new") == 0)
>  	opt_format = opt_format_new;
>        break;
> +    case 'I':
> +      opt_install = 1;
> +      break;
>      default:
>        return ARGP_ERR_UNKNOWN;
>      }
> @@ -1041,6 +1048,155 @@ search_dirs (void)
>      }
>  }
>  
> +static void
> +install_cache_file (const char *source_arg)
> +{
> +  int e;
> +
> +  if (source_arg == NULL)
> +    error (EXIT_FAILURE, 0, _("Missing source file name"));
> +
> +  const char *source = (opt_chroot
> +			? chroot_canon (opt_chroot, source_arg)
> +			: source_arg);
> +  if (source == NULL)
> +    error (EXIT_FAILURE, errno, _("Can't find %s"), source_arg);
> +
> +  int src_fd = open (source, O_RDONLY);
> +  if (src_fd < 0)
> +    error (EXIT_FAILURE, errno, _("Can't open %s"), source);
> +
> +  char *dest = xmalloc (strlen (cache_file) + 1 + 1);
> +
> +  /* This matches the temp file created by cache.c, and should be
> +     on the same filesystem as the cache file.  */
> +  sprintf (dest, "%s~", cache_file);
> +  int dest_fd;
> +
> +  struct stat st;
> +  if (fstat (src_fd, &st) < 0)
> +    error (EXIT_FAILURE, errno, _("Can't stat %s"), source);
> +
> +  char buf[512];
> +  ssize_t r, w = 0, sz = 0;
> +  char *bp = buf;
> +
> +  /* Read the first part of the file and verify it looks
> +     reasonable.  */
> +  while (sz < sizeof (buf)
> +	 && (r = read (src_fd, bp, sizeof (buf) - sz)) > 0)
> +    {
> +      sz += r;
> +      bp += r;
> +    }
> +  if (r < 0)
> +    error (EXIT_FAILURE, errno, _("Error reading file %s"), source);
> +
> +  if (! ((sz >= sizeof (CACHEMAGIC)
> +	  && memcmp (buf, CACHEMAGIC,
> +		     sizeof (CACHEMAGIC) - 1) == 0)
> +	 || (sz >= sizeof (CACHEMAGIC_NEW)
> +	     && memcmp (buf, CACHEMAGIC_NEW,
> +			sizeof (CACHEMAGIC_NEW) - 1) == 0)))
> +    {
> +      error (EXIT_FAILURE, 0,
> +	     _("File %s does not look like an ld.so.cache file"),
> +	     source);
> +    }
> +
> +  /* Now write that first part out.  */
> +  dest_fd = open (dest, O_CREAT|O_WRONLY|O_TRUNC|O_NOFOLLOW,
> +		  S_IRUSR|S_IWUSR);
> +  if (dest_fd < 0)
> +    error (EXIT_FAILURE, errno, _("Can't create %s"), dest);
> +
> +  r = sz;
> +  bp = buf;
> +  while (r > 0 && (w = write (dest_fd, bp, r)) > 0)
> +    {
> +      r -= w;
> +      bp += w;
> +    }
> +  if (w < 0)
> +    {
> +      e = errno;
> +      unlink (dest);
> +      close (dest_fd);
> +      error (EXIT_FAILURE, e, _("Error writing file %s"), dest);
> +    }
> +
> +  /* At this point, sz contains the number of bytes copied so far.
> +     Copy the rest of the file.  */
> +  while ((r = read (src_fd, buf, sizeof(buf))) > 0)
> +    {
> +      bp = buf;
> +      while (r > 0 && (w = write (dest_fd, bp, r)) > 0)
> +	{
> +	  bp += w;
> +	  r -= w;
> +	  sz += w;
> +	}
> +      if (w <= 0)
> +	break;
> +    }
> +  if (r < 0)
> +    {
> +      e = errno;
> +      unlink (dest);
> +      close (dest_fd);
> +      error (EXIT_FAILURE, e, _("Error reading file %s"), source);
> +    }
> +  if (w < 0)
> +    {
> +      e = errno;
> +      unlink (dest);
> +      close (dest_fd);
> +      error (EXIT_FAILURE, e, _("Error writing file %s"), dest);
> +    }
> +
> +  close (src_fd);
> +
> +  /* Make sure we copied it all.  */
> +  if (sz < st.st_size)
> +    {
> +      unlink (dest);
> +      close (dest_fd);
> +      error (EXIT_FAILURE, 0, _("Unable to copy file %s to %s"),
> +	     source, dest);
> +    }
> +
> +  /* Make sure user can always read the cache file */
> +  if (fchmod (dest_fd, S_IROTH|S_IRGRP|S_IRUSR|S_IWUSR))
> +    {
> +      e = errno;
> +      unlink (dest);
> +      close (dest_fd);
> +      error (EXIT_FAILURE, e,
> +	     _("Changing access rights of %s to %#o failed"), dest,
> +	     S_IROTH|S_IRGRP|S_IRUSR|S_IWUSR);
> +    }
> +
> +  if (fsync (dest_fd) != 0)
> +    {
> +      e = errno;
> +      unlink (dest);
> +      close (dest_fd);
> +      error (EXIT_FAILURE, e, _("Writing to %s failed"), dest);
> +    }
> +
> +  if (rename (dest, cache_file) < 0)
> +    {
> +      e = errno;
> +      unlink (dest);
> +      close (dest_fd);
> +      error (EXIT_FAILURE, e, _("Can't rename %s to %s"),
> +	     dest, cache_file);
> +    }
> +
> +  if (close (dest_fd) != 0)
> +    error (EXIT_FAILURE, errno, _("Writing to %s failed"), dest);
> +  exit (0);
> +}
>  
>  int
>  main (int argc, char **argv)
> @@ -1060,8 +1216,8 @@ main (int argc, char **argv)
>    argp_parse (&argp, argc, argv, 0, &remaining, NULL);
>  
>    /* Remaining arguments are additional directories if opt_manual_link
> -     is not set.  */
> -  if (remaining != argc && !opt_manual_link)
> +     and opt_install are not set.  */
> +  if (remaining != argc && !opt_manual_link && !opt_install)
>      {
>        int i;
>        for (i = remaining; i < argc; ++i)
> @@ -1157,6 +1313,8 @@ main (int argc, char **argv)
>        exit (0);
>      }
>  
> +  if (opt_install)
> +    install_cache_file (argv[remaining]);
>  
>    if (opt_build_cache)
>      init_cache ();
> diff --git a/elf/tst-ldconfig-install.sh b/elf/tst-ldconfig-install.sh
> new file mode 100644
> index 0000000000..550281c7ab
> --- /dev/null
> +++ b/elf/tst-ldconfig-install.sh
> @@ -0,0 +1,125 @@
> +#!/bin/sh
> +# Test that ldconfig --install installs a pre-built cache file.
> +# 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; if not, see
> +# <https://www.gnu.org/licenses/>.
> +
> +set -e
> +
> +common_objpfx=$1
> +test_wrapper_env=$2
> +run_program_env=$3
> +
> +testroot="${common_objpfx}elf/tst-ldconfig-install-directory"
> +cleanup () {
> +    rm -rf "$testroot"
> +}
> +trap cleanup 0
> +
> +rm -rf "$testroot"
> +mkdir -p "$testroot/etc"
> +
> +ldconfig="${common_objpfx}elf/ldconfig"
> +run_ldconfig () {
> +    ${test_wrapper_env} ${run_program_env} "$ldconfig" "$@"
> +}
> +
> +errors=0
> +fail () {
> +    echo "error: $1"
> +    errors=1
> +}
> +
> +# Build a pre-built cache to install.
> +source="$testroot/prebuilt-ld.so.cache"
> +mkdir -p "$testroot/lib"
> +run_ldconfig -X -f /dev/null -C "$source" "$testroot/lib"
> +test -r "$source" || fail "ldconfig did not create the pre-built cache"
> +
> +# Pad the source past the 512-byte internal copy buffer so that the multi-block
> +# copy path is exercised, while leaving the cache magic at the start intact.
> +dd if=/dev/zero bs=1024 count=4 >> "$source" 2>/dev/null
> +
> +dest="$testroot/etc/ld.so.cache"
> +temp="$dest~"
> +
> +run_ldconfig --install -f /dev/null -C "$dest" "$source"
> +
> +# The destination must exist and be byte-identical to the source.
> +if test -r "$dest"; then
> +  if cmp -s "$source" "$dest"; then
> +    echo "info: installed cache matches the source"
> +  else
> +    fail "installed cache differs from the source"
> +  fi
> +else
> +  fail "destination cache file was not created"
> +fi
> +
> +# The temporary file used during the atomic rename must not be left behind.
> +if test -e "$temp"; then
> +  fail "temporary file $temp was left behind"
> +fi
> +
> +# The installed cache must be world-readable (0644).
> +if test -r "$dest"; then
> +  mode=$(ls -l "$dest" | cut -c1-10)
> +  case "$mode" in
> +    (-rw-r--r--) echo "info: installed cache has expected permissions" ;;
> +    (*) fail "installed cache has unexpected permissions: $mode" ;;
> +  esac
> +fi
> +
> +# A second install over an existing cache must also succeed.
> +run_ldconfig --install -f /dev/null -C "$dest" "$source"
> +if cmp -s "$source" "$dest"; then
> +  echo "info: re-install over an existing cache works"
> +else
> +  fail "re-install produced a different cache"
> +fi
> +
> +# Error case: a source file that is not a cache must be rejected, and no
> +# destination must be produced.
> +rm -f "$dest"
> +notcache="$testroot/not-a-cache"
> +echo "this is not an ld.so.cache file" > "$notcache"
> +if run_ldconfig --install -f /dev/null -C "$dest" "$notcache" 2>"$testroot/err"; then
> +  fail "ldconfig accepted a file that is not a cache"
> +else
> +  if grep -q "does not look like an ld.so.cache file" "$testroot/err"; then
> +    echo "info: non-cache source correctly rejected"
> +  else
> +    fail "unexpected error message for non-cache source"
> +    cat "$testroot/err"
> +  fi
> +fi
> +test -e "$dest" && fail "destination created from an invalid source"
> +
> +# Error case: a missing source argument must be diagnosed.
> +if run_ldconfig --install -f /dev/null -C "$dest" 2>"$testroot/err"; then
> +  fail "ldconfig accepted --install without a source file"
> +else
> +  grep -q "Missing source file name" "$testroot/err" \
> +    || fail "unexpected error message for missing source"
> +fi
> +
> +# Error case: a nonexistent source must be diagnosed.
> +if run_ldconfig --install -f /dev/null -C "$dest" \
> +       "$testroot/does-not-exist" 2>"$testroot/err"; then
> +  fail "ldconfig accepted a nonexistent source file"
> +fi
> +
> +exit $errors
  
DJ Delorie July 7, 2026, 7:05 p.m. UTC | #2
Adhemerval Zanella Netto <adhemerval.zanella@linaro.org> writes:
> On 30/06/26 18:28, DJ Delorie wrote:
>> Add --install option, which copies a pre-built ld.so.cache into place,
>> honoring the cache and root options and defaults.  This gives the user
>> a canonical "correct" way to install a pre-built cache without risk
>> of a program trying to load a partially-written file.
>> 
>> Co-authored-by: Adhemerval Zanella Netto <adhemerval.zanella@linaro.org>
>
> This version looks good to me, thanks.
>
> Reviewed-by: Adhemerval Zanella  <adhemerval.zanella@linaro.org>

Thanks!

I'm assuming this is a post-2.44 patch, yes?
  
Adhemerval Zanella Netto July 7, 2026, 7:31 p.m. UTC | #3
On 07/07/26 16:05, DJ Delorie wrote:
> Adhemerval Zanella Netto <adhemerval.zanella@linaro.org> writes:
>> On 30/06/26 18:28, DJ Delorie wrote:
>>> Add --install option, which copies a pre-built ld.so.cache into place,
>>> honoring the cache and root options and defaults.  This gives the user
>>> a canonical "correct" way to install a pre-built cache without risk
>>> of a program trying to load a partially-written file.
>>>
>>> Co-authored-by: Adhemerval Zanella Netto <adhemerval.zanella@linaro.org>
>>
>> This version looks good to me, thanks.
>>
>> Reviewed-by: Adhemerval Zanella  <adhemerval.zanella@linaro.org>
> 
> Thanks!
> 
> I'm assuming this is a post-2.44 patch, yes?
> 

It should be doable for 2.44 if Andreas is OK with this.
  
Andreas K. Huettel July 8, 2026, 12:50 p.m. UTC | #4
Am Mittwoch, 8. Juli 2026, 04:31:23 Japanische Normalzeit schrieb Adhemerval Zanella Netto:
> 
> On 07/07/26 16:05, DJ Delorie wrote:
> > Adhemerval Zanella Netto <adhemerval.zanella@linaro.org> writes:
> >> On 30/06/26 18:28, DJ Delorie wrote:
> >>> Add --install option, which copies a pre-built ld.so.cache into place,
> >>> honoring the cache and root options and defaults.  This gives the user
> >>> a canonical "correct" way to install a pre-built cache without risk
> >>> of a program trying to load a partially-written file.
> >>>
> >>> Co-authored-by: Adhemerval Zanella Netto <adhemerval.zanella@linaro.org>
> >>
> >> This version looks good to me, thanks.
> >>
> >> Reviewed-by: Adhemerval Zanella  <adhemerval.zanella@linaro.org>
> > 
> > Thanks!
> > 
> > I'm assuming this is a post-2.44 patch, yes?
> > 
> 
> It should be doable for 2.44 if Andreas is OK with this.
> 

It's small, well-separated, and doesnt affect existing operation. So yeah, I
dont see any problems.

Mostly I don't fully understand the use case yet, but that may be a problem 
behind *my* keyboard and is irrelevant for the release... 
Is the source file supposed to come from the live system (since it needs to 
know all installed libraries)? or from some other magic source?
  
Florian Weimer July 8, 2026, 3:36 p.m. UTC | #5
* Andreas K. Huettel:

> Am Mittwoch, 8. Juli 2026, 04:31:23 Japanische Normalzeit schrieb Adhemerval Zanella Netto:
>> 
>> On 07/07/26 16:05, DJ Delorie wrote:
>> > Adhemerval Zanella Netto <adhemerval.zanella@linaro.org> writes:
>> >> On 30/06/26 18:28, DJ Delorie wrote:
>> >>> Add --install option, which copies a pre-built ld.so.cache into place,
>> >>> honoring the cache and root options and defaults.  This gives the user
>> >>> a canonical "correct" way to install a pre-built cache without risk
>> >>> of a program trying to load a partially-written file.
>> >>>
>> >>> Co-authored-by: Adhemerval Zanella Netto <adhemerval.zanella@linaro.org>
>> >>
>> >> This version looks good to me, thanks.
>> >>
>> >> Reviewed-by: Adhemerval Zanella  <adhemerval.zanella@linaro.org>
>> > 
>> > Thanks!
>> > 
>> > I'm assuming this is a post-2.44 patch, yes?
>> > 
>> 
>> It should be doable for 2.44 if Andreas is OK with this.
>> 
>
> It's small, well-separated, and doesnt affect existing operation. So yeah, I
> dont see any problems.
>
> Mostly I don't fully understand the use case yet, but that may be a problem 
> behind *my* keyboard and is irrelevant for the release... 
> Is the source file supposed to come from the live system (since it needs to 
> know all installed libraries)? or from some other magic source?

I think this needs a NEWS entry.  The dynamic linker keeps
/etc/ld.so.cache mapped continuously, so if you use cp to copy the
cache, it's more likely that it will lead to crashes because the size of
the mapping does not match the file contents.

Using ldconfig --install avoids that because doesn't overwrite the file
in place.  Using rsync (without --inplace) or GNU install would be fine,
too.

Thanks,
Florian
  
DJ Delorie July 13, 2026, 6:01 p.m. UTC | #6
Florian Weimer <fweimer@redhat.com> writes:
>> It's small, well-separated, and doesnt affect existing operation. So yeah, I
>> dont see any problems.
>>
>> Mostly I don't fully understand the use case yet, but that may be a problem 
>> behind *my* keyboard and is irrelevant for the release... 
>> Is the source file supposed to come from the live system (since it needs to 
>> know all installed libraries)? or from some other magic source?
>
> I think this needs a NEWS entry.  The dynamic linker keeps
> /etc/ld.so.cache mapped continuously, so if you use cp to copy the
> cache, it's more likely that it will lead to crashes because the size of
> the mapping does not match the file contents.
>
> Using ldconfig --install avoids that because doesn't overwrite the file
> in place.  Using rsync (without --inplace) or GNU install would be fine,
> too.

Andreas,

Does this answer your concerns?  Is it still OK to push, or do you now
request changes or a NEWS entry?
  
Andreas K. Huettel July 14, 2026, 9:47 a.m. UTC | #7
Am Dienstag, 14. Juli 2026, 03:01:42 Japanische Normalzeit schrieb DJ Delorie:
> Florian Weimer <fweimer@redhat.com> writes:
> >> It's small, well-separated, and doesnt affect existing operation. So yeah, I
> >> dont see any problems.
> >>
> >> Mostly I don't fully understand the use case yet, but that may be a problem 
> >> behind *my* keyboard and is irrelevant for the release... 
> >> Is the source file supposed to come from the live system (since it needs to 
> >> know all installed libraries)? or from some other magic source?
> >
> > I think this needs a NEWS entry.  The dynamic linker keeps
> > /etc/ld.so.cache mapped continuously, so if you use cp to copy the
> > cache, it's more likely that it will lead to crashes because the size of
> > the mapping does not match the file contents.
> >
> > Using ldconfig --install avoids that because doesn't overwrite the file
> > in place.  Using rsync (without --inplace) or GNU install would be fine,
> > too.
> 
> Andreas,
> 
> Does this answer your concerns?  Is it still OK to push, or do you now
> request changes or a NEWS entry?

I'm fine with it as it is, please push it.

NEWS is nice but optional, maybe subsequently per extra commit.

> 
>
  
DJ Delorie July 14, 2026, 6:25 p.m. UTC | #8
"Andreas K. Huettel" <dilfridge@gentoo.org> writes:
> I'm fine with it as it is, please push it.

Pushed!
  

Patch

diff --git a/NEWS b/NEWS
index f9d90c5194..e30b7a5f76 100644
--- a/NEWS
+++ b/NEWS
@@ -37,6 +37,8 @@  Major new features:
   status via prctl syscall.  This prevents disabling or corrupting GCS
   shadow stack during runtime.
 
+* Pre-built ld.so.cache files can be installed with ldconfig.
+
 Deprecated and removed features, and other changes affecting compatibility:
 
 * Although malloc and related functions currently return pointers
diff --git a/elf/Makefile b/elf/Makefile
index 789c504da9..3479450191 100644
--- a/elf/Makefile
+++ b/elf/Makefile
@@ -718,6 +718,7 @@  tests-special += $(tests-execstack-special-$(have-z-execstack))
 ifeq ($(run-built-tests),yes)
 tests-special += \
   $(objpfx)tst-ldconfig-X.out \
+  $(objpfx)tst-ldconfig-install.out \
   $(objpfx)tst-ldconfig-p.out \
   $(objpfx)tst-ldconfig-soname.out \
   $(objpfx)tst-rtld-help.out \
@@ -2833,6 +2834,11 @@  $(objpfx)tst-ldconfig-X.out : tst-ldconfig-X.sh $(objpfx)ldconfig
 		 '$(run-program-env)' > $@; \
 	$(evaluate-test)
 
+$(objpfx)tst-ldconfig-install.out : tst-ldconfig-install.sh $(objpfx)ldconfig
+	$(SHELL) $< '$(common-objpfx)' '$(test-wrapper-env)' \
+		'$(run-program-env)' > $@; \
+	$(evaluate-test)
+
 $(objpfx)tst-ldconfig-p.out : tst-ldconfig-p.sh $(objpfx)ldconfig
 	$(SHELL) $< '$(common-objpfx)' '$(sysconfdir)' '$(test-wrapper-env)' \
 		 '$(run-program-env)' > $@; \
diff --git a/elf/ldconfig.c b/elf/ldconfig.c
index 1ea55400f3..a39f3ecfcd 100644
--- a/elf/ldconfig.c
+++ b/elf/ldconfig.c
@@ -109,6 +109,9 @@  static int opt_manual_link;
 /* Should we ignore an old auxiliary cache file?  */
 static int opt_ignore_aux_cache;
 
+/* Install a pre-existing cache file instead of generating a new one.  */
+static int opt_install;
+
 /* Cache file to use.  */
 static char *cache_file;
 
@@ -141,6 +144,7 @@  static const struct argp_option options[] =
   { NULL, 'l', NULL, 0, N_("Manually link individual libraries."), 0},
   { "format", 'c', N_("FORMAT"), 0, N_("Format to use: new (default), old, or compat"), 0},
   { "ignore-aux-cache", 'i', NULL, 0, N_("Ignore auxiliary cache file"), 0},
+  { "install", 'I', NULL, 0, N_("Install pre-existing cache file"), 0},
   { NULL, 0, NULL, 0, NULL, 0 }
 };
 
@@ -209,6 +213,9 @@  parse_opt (int key, char *arg, struct argp_state *state)
       else if (strcmp (arg, "new") == 0)
 	opt_format = opt_format_new;
       break;
+    case 'I':
+      opt_install = 1;
+      break;
     default:
       return ARGP_ERR_UNKNOWN;
     }
@@ -1041,6 +1048,155 @@  search_dirs (void)
     }
 }
 
+static void
+install_cache_file (const char *source_arg)
+{
+  int e;
+
+  if (source_arg == NULL)
+    error (EXIT_FAILURE, 0, _("Missing source file name"));
+
+  const char *source = (opt_chroot
+			? chroot_canon (opt_chroot, source_arg)
+			: source_arg);
+  if (source == NULL)
+    error (EXIT_FAILURE, errno, _("Can't find %s"), source_arg);
+
+  int src_fd = open (source, O_RDONLY);
+  if (src_fd < 0)
+    error (EXIT_FAILURE, errno, _("Can't open %s"), source);
+
+  char *dest = xmalloc (strlen (cache_file) + 1 + 1);
+
+  /* This matches the temp file created by cache.c, and should be
+     on the same filesystem as the cache file.  */
+  sprintf (dest, "%s~", cache_file);
+  int dest_fd;
+
+  struct stat st;
+  if (fstat (src_fd, &st) < 0)
+    error (EXIT_FAILURE, errno, _("Can't stat %s"), source);
+
+  char buf[512];
+  ssize_t r, w = 0, sz = 0;
+  char *bp = buf;
+
+  /* Read the first part of the file and verify it looks
+     reasonable.  */
+  while (sz < sizeof (buf)
+	 && (r = read (src_fd, bp, sizeof (buf) - sz)) > 0)
+    {
+      sz += r;
+      bp += r;
+    }
+  if (r < 0)
+    error (EXIT_FAILURE, errno, _("Error reading file %s"), source);
+
+  if (! ((sz >= sizeof (CACHEMAGIC)
+	  && memcmp (buf, CACHEMAGIC,
+		     sizeof (CACHEMAGIC) - 1) == 0)
+	 || (sz >= sizeof (CACHEMAGIC_NEW)
+	     && memcmp (buf, CACHEMAGIC_NEW,
+			sizeof (CACHEMAGIC_NEW) - 1) == 0)))
+    {
+      error (EXIT_FAILURE, 0,
+	     _("File %s does not look like an ld.so.cache file"),
+	     source);
+    }
+
+  /* Now write that first part out.  */
+  dest_fd = open (dest, O_CREAT|O_WRONLY|O_TRUNC|O_NOFOLLOW,
+		  S_IRUSR|S_IWUSR);
+  if (dest_fd < 0)
+    error (EXIT_FAILURE, errno, _("Can't create %s"), dest);
+
+  r = sz;
+  bp = buf;
+  while (r > 0 && (w = write (dest_fd, bp, r)) > 0)
+    {
+      r -= w;
+      bp += w;
+    }
+  if (w < 0)
+    {
+      e = errno;
+      unlink (dest);
+      close (dest_fd);
+      error (EXIT_FAILURE, e, _("Error writing file %s"), dest);
+    }
+
+  /* At this point, sz contains the number of bytes copied so far.
+     Copy the rest of the file.  */
+  while ((r = read (src_fd, buf, sizeof(buf))) > 0)
+    {
+      bp = buf;
+      while (r > 0 && (w = write (dest_fd, bp, r)) > 0)
+	{
+	  bp += w;
+	  r -= w;
+	  sz += w;
+	}
+      if (w <= 0)
+	break;
+    }
+  if (r < 0)
+    {
+      e = errno;
+      unlink (dest);
+      close (dest_fd);
+      error (EXIT_FAILURE, e, _("Error reading file %s"), source);
+    }
+  if (w < 0)
+    {
+      e = errno;
+      unlink (dest);
+      close (dest_fd);
+      error (EXIT_FAILURE, e, _("Error writing file %s"), dest);
+    }
+
+  close (src_fd);
+
+  /* Make sure we copied it all.  */
+  if (sz < st.st_size)
+    {
+      unlink (dest);
+      close (dest_fd);
+      error (EXIT_FAILURE, 0, _("Unable to copy file %s to %s"),
+	     source, dest);
+    }
+
+  /* Make sure user can always read the cache file */
+  if (fchmod (dest_fd, S_IROTH|S_IRGRP|S_IRUSR|S_IWUSR))
+    {
+      e = errno;
+      unlink (dest);
+      close (dest_fd);
+      error (EXIT_FAILURE, e,
+	     _("Changing access rights of %s to %#o failed"), dest,
+	     S_IROTH|S_IRGRP|S_IRUSR|S_IWUSR);
+    }
+
+  if (fsync (dest_fd) != 0)
+    {
+      e = errno;
+      unlink (dest);
+      close (dest_fd);
+      error (EXIT_FAILURE, e, _("Writing to %s failed"), dest);
+    }
+
+  if (rename (dest, cache_file) < 0)
+    {
+      e = errno;
+      unlink (dest);
+      close (dest_fd);
+      error (EXIT_FAILURE, e, _("Can't rename %s to %s"),
+	     dest, cache_file);
+    }
+
+  if (close (dest_fd) != 0)
+    error (EXIT_FAILURE, errno, _("Writing to %s failed"), dest);
+  exit (0);
+}
 
 int
 main (int argc, char **argv)
@@ -1060,8 +1216,8 @@  main (int argc, char **argv)
   argp_parse (&argp, argc, argv, 0, &remaining, NULL);
 
   /* Remaining arguments are additional directories if opt_manual_link
-     is not set.  */
-  if (remaining != argc && !opt_manual_link)
+     and opt_install are not set.  */
+  if (remaining != argc && !opt_manual_link && !opt_install)
     {
       int i;
       for (i = remaining; i < argc; ++i)
@@ -1157,6 +1313,8 @@  main (int argc, char **argv)
       exit (0);
     }
 
+  if (opt_install)
+    install_cache_file (argv[remaining]);
 
   if (opt_build_cache)
     init_cache ();
diff --git a/elf/tst-ldconfig-install.sh b/elf/tst-ldconfig-install.sh
new file mode 100644
index 0000000000..550281c7ab
--- /dev/null
+++ b/elf/tst-ldconfig-install.sh
@@ -0,0 +1,125 @@ 
+#!/bin/sh
+# Test that ldconfig --install installs a pre-built cache file.
+# 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; if not, see
+# <https://www.gnu.org/licenses/>.
+
+set -e
+
+common_objpfx=$1
+test_wrapper_env=$2
+run_program_env=$3
+
+testroot="${common_objpfx}elf/tst-ldconfig-install-directory"
+cleanup () {
+    rm -rf "$testroot"
+}
+trap cleanup 0
+
+rm -rf "$testroot"
+mkdir -p "$testroot/etc"
+
+ldconfig="${common_objpfx}elf/ldconfig"
+run_ldconfig () {
+    ${test_wrapper_env} ${run_program_env} "$ldconfig" "$@"
+}
+
+errors=0
+fail () {
+    echo "error: $1"
+    errors=1
+}
+
+# Build a pre-built cache to install.
+source="$testroot/prebuilt-ld.so.cache"
+mkdir -p "$testroot/lib"
+run_ldconfig -X -f /dev/null -C "$source" "$testroot/lib"
+test -r "$source" || fail "ldconfig did not create the pre-built cache"
+
+# Pad the source past the 512-byte internal copy buffer so that the multi-block
+# copy path is exercised, while leaving the cache magic at the start intact.
+dd if=/dev/zero bs=1024 count=4 >> "$source" 2>/dev/null
+
+dest="$testroot/etc/ld.so.cache"
+temp="$dest~"
+
+run_ldconfig --install -f /dev/null -C "$dest" "$source"
+
+# The destination must exist and be byte-identical to the source.
+if test -r "$dest"; then
+  if cmp -s "$source" "$dest"; then
+    echo "info: installed cache matches the source"
+  else
+    fail "installed cache differs from the source"
+  fi
+else
+  fail "destination cache file was not created"
+fi
+
+# The temporary file used during the atomic rename must not be left behind.
+if test -e "$temp"; then
+  fail "temporary file $temp was left behind"
+fi
+
+# The installed cache must be world-readable (0644).
+if test -r "$dest"; then
+  mode=$(ls -l "$dest" | cut -c1-10)
+  case "$mode" in
+    (-rw-r--r--) echo "info: installed cache has expected permissions" ;;
+    (*) fail "installed cache has unexpected permissions: $mode" ;;
+  esac
+fi
+
+# A second install over an existing cache must also succeed.
+run_ldconfig --install -f /dev/null -C "$dest" "$source"
+if cmp -s "$source" "$dest"; then
+  echo "info: re-install over an existing cache works"
+else
+  fail "re-install produced a different cache"
+fi
+
+# Error case: a source file that is not a cache must be rejected, and no
+# destination must be produced.
+rm -f "$dest"
+notcache="$testroot/not-a-cache"
+echo "this is not an ld.so.cache file" > "$notcache"
+if run_ldconfig --install -f /dev/null -C "$dest" "$notcache" 2>"$testroot/err"; then
+  fail "ldconfig accepted a file that is not a cache"
+else
+  if grep -q "does not look like an ld.so.cache file" "$testroot/err"; then
+    echo "info: non-cache source correctly rejected"
+  else
+    fail "unexpected error message for non-cache source"
+    cat "$testroot/err"
+  fi
+fi
+test -e "$dest" && fail "destination created from an invalid source"
+
+# Error case: a missing source argument must be diagnosed.
+if run_ldconfig --install -f /dev/null -C "$dest" 2>"$testroot/err"; then
+  fail "ldconfig accepted --install without a source file"
+else
+  grep -q "Missing source file name" "$testroot/err" \
+    || fail "unexpected error message for missing source"
+fi
+
+# Error case: a nonexistent source must be diagnosed.
+if run_ldconfig --install -f /dev/null -C "$dest" \
+       "$testroot/does-not-exist" 2>"$testroot/err"; then
+  fail "ldconfig accepted a nonexistent source file"
+fi
+
+exit $errors