[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