[glibc] ldconfig: add --install option

DJ Delorie via Glibc-cvs <[email protected]> Tue, 14 Jul 2026 18:24:51 +0000 (GMT)
Newsgroups gmane.comp.lib.glibc.cvs
Message-ID <[email protected]>
https://sourceware.org/git/gitweb.cgi?p=glibc.git;h=2324aa7aa81a324ea9936a29ed41d6b0fa3ae6e4

commit 2324aa7aa81a324ea9936a29ed41d6b0fa3ae6e4
Author: DJ Delorie <[email protected]>
Date:   Tue Jun 30 17:26:34 2026 -0400

    ldconfig: add --install option
    
    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 <[email protected]>
    Reviewed-by: Adhemerval Zanella  <[email protected]>

Diff:
---
 NEWS                        |   2 +
 elf/Makefile                |   6 ++
 elf/ldconfig.c              | 162 +++++++++++++++++++++++++++++++++++++++++++-
 elf/tst-ldconfig-install.sh | 125 ++++++++++++++++++++++++++++++++++
 4 files changed, 293 insertions(+), 2 deletions(-)

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 99a743e15c..94c5b7e6ed 100644
--- a/elf/Makefile
+++ b/elf/Makefile
@@ -727,6 +727,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 \
@@ -2847,6 +2848,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