[PATCH v4 1/1] ldconfig: add --install option
Adhemerval Zanella Netto
adhemerval.zanella@linaro.org
Mon Jun 8 20:11:05 GMT 2026
On 05/06/26 21:24, 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.
>
> --- >8 ---
>
> 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.
>
> diff --git a/NEWS b/NEWS
> index e2173fa1aa..484d09e6b3 100644
> --- a/NEWS
> +++ b/NEWS
> @@ -21,6 +21,8 @@ Major new features:
> * Static PIE is now supported for arm-*-linux-gnueabi. It requires toolchain
> support to correctly set the expected linker options.
>
> +* 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/ldconfig.c b/elf/ldconfig.c
> index 070e933df6..9835bfa65d 100644
> --- a/elf/ldconfig.c
> +++ b/elf/ldconfig.c
> @@ -104,6 +104,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;
>
> @@ -132,6 +135,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 }
> };
>
> @@ -197,6 +201,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;
> }
> @@ -1045,8 +1052,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)
> @@ -1139,6 +1146,125 @@ main (int argc, char **argv)
> exit (0);
> }
>
> + if (opt_install)
> + {
> + if (argv[remaining] == NULL)
> + error (EXIT_FAILURE, 0, _("Missing source file name"));
> +
> + char *source = (opt_chroot
> + ? chroot_canon (opt_chroot, argv[remaining])
> + : argv[remaining]);
> + if (source == NULL)
> + error (EXIT_FAILURE, errno, _("Can't find %s"), argv[remaining]);
> +
> + 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);
I tend to avoid sprintf, and we do have xasprintf:
char dest = xasprintf ("%s~", cache_file);
It requires adding the xasprintf to ldconfig-modules and the header inclusion
on sysdeps/generic/ldconfig.h.
> + int dest_fd;
> +
> + struct stat st;
> + if (fstat (src_fd, &st) < 0)
> + error (EXIT_FAILURE, errno, _("Can't stat %s"), source);
> +
> + char buf[512];
> + int 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_WRONLY, 0644);
I think it requires O_CREAT, and the elf/cache.c:682 uses:
int fd = open (temp_name, O_CREAT|O_WRONLY|O_TRUNC|O_NOFOLLOW, S_IRUSR|S_IWUSR);
Both O_TRUNC (stale temp file) and O_NOFOLLOW (symlink attack on the privileged write
target) should be used as well.
> + 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)
> + {
> + unlink (dest);
> + close (dest_fd);
> + error (EXIT_FAILURE, errno, _("Error writing file %s"), dest);
> + }
I think there is no need to a loop here, we can use the same idea as from save_cache:
a short write now aborts immediately with the proper errno and message, instead of being
silently retried.
/* Write out the part already read for verification. As in cache.c,
a short write is treated as a fatal error: the real cache file is
untouched until the rename below, so aborting here is safe. */
if (write (dest_fd, buf, sz) != (ssize_t) sz)
{
unlink (dest);
close (dest_fd);
error (EXIT_FAILURE, errno, _("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 += r;
> + r -= w;
> + sz += w;
> + }
> + if (w <= 0)
> + break;
> + }
Same as before here:
/* Copy the rest of the file. */
while ((r = read (src_fd, buf, sizeof (buf))) > 0)
{
if (write (dest_fd, buf, r) != (ssize_t) r)
{
unlink (dest);
close (dest_fd);
error (EXIT_FAILURE, errno, _("Error writing file %s"), dest);
}
sz += r;
}
if (r < 0)
{
unlink (dest);
close (dest_fd);
error (EXIT_FAILURE, errno, _("Error reading file %s"), source);
}
> +
> + close (src_fd);
> +
> + /* Make sure we copied it all. */
> + if (sz < st.st_size)
> + {
> + unlink (dest);
> + close (dest_fd);
> + error (EXIT_FAILURE, errno, _("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))
> + {
> + unlink (dest);
> + close (dest_fd);
> + error (EXIT_FAILURE, errno,
> + _("Changing access rights of %s to %#o failed"), dest,
> + S_IROTH|S_IRGRP|S_IRUSR|S_IWUSR);
> + }
> +
> + fsync (dest_fd);
> + if (rename (dest, cache_file) < 0)
> + {
> + unlink (dest);
> + close (dest_fd);
> + error (EXIT_FAILURE, errno, _("Can't rename %s to %s"), dest, cache_file);
> + }
> +
> + close (dest_fd);
> + exit (0);
> + }
>
> if (opt_build_cache)
> init_cache ();
>
I really think we should add a testcase for this, something like:
diff --git a/elf/Makefile b/elf/Makefile
index 5ede78c5902..5498509f702 100644
--- a/elf/Makefile
+++ b/elf/Makefile
@@ -708,6 +709,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 \
@@ -2816,6 +2818,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/tst-ldconfig-install.sh b/elf/tst-ldconfig-install.sh
new file mode 100644
index 00000000000..d0a77e188e5
--- /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
It should cover most of the usage.
More information about the Libc-alpha
mailing list