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

Adhemerval Zanella Netto adhemerval.zanella@linaro.org
Tue Jul 7 17:17:35 GMT 2026



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



More information about the Libc-alpha mailing list