[PATCH v3] elf: Canonicalize $ORIGIN in an explicit ld.so invocation [BZ 25263]
Geoffrey Thomas
geofft@ldpreload.com
Wed Mar 12 20:21:26 GMT 2025
On Tue, Mar 11, 2025, at 4:17 PM, Adhemerval Zanella wrote:
> When an executable is invoked directly, we calculate $ORIGIN by calling
> readlink on /proc/self/exe, which the Linux kernel resolves to the
> target of any symlinks. However, if an executable is run through ld.so,
> we cannot use /proc/self/exe and instead use the path given as an
> argument. This leads to a different calculation of $ORIGIN, which is
> most notable in that it causes ldd to behave differently (e.g., by not
> finding a library) from directly running the program.
>
> To make the behavior consistent, take advantage of the fact that the
> kernel also resolves /proc/self/fd/ symlinks to the target of any
> symlinks in the same manner, so once we have opened the main executable
> in order to load it, replace the user-provided path with the result of
> calling readlink("/proc/self/fd/N").
>
> (On non-Linux platforms this resolution does not happen and so no
> behavior change is needed.)
>
> The __fd_to_filename usage on loader (through dl-origin.c) pulls
> _itoa.c, which in turn defines a lot of
>
> The __fd_to_filename requires _fitoa_word and _itoa_word, which for
duplicate paragraph
> 32-bits pulls a lot of definitions from _itoa.c (due _ITOA_NEEDED
> debing defined). To simplify the build move the required function
"due to _ITOA_NEEDED being defined"
> to a new file, _fitoa_word.c.
>
> Checked on x86_64-linux-gnu and i686-linux-gnu.
I have also just tested this version on aarch64-linux-gnu and it works.
Reviewed-by: Geoffrey Thomas <geofft@ldpreload.com>
(and Tested-by and Signed-off-by if you need them)
Thanks again for your help in getting this through!
>
> Co-authored-by: Geoffrey Thomas <geofft@ldpreload.com>
> ---
> elf/Makefile | 23 +++++++++++
> elf/dl-load.c | 6 +++
> elf/dl-origin.c | 6 +++
> elf/liborigin-mod.c | 1 +
> elf/tst-origin.c | 26 +++++++++++++
> elf/tst-origin.sh | 60 +++++++++++++++++++++++++++++
> stdio-common/Makefile | 3 ++
> stdio-common/_fitoa_word.c | 60 +++++++++++++++++++++++++++++
> stdio-common/_itoa.c | 43 ---------------------
> sysdeps/generic/_itoa.h | 31 ---------------
> sysdeps/generic/ldsodefs.h | 4 ++
> sysdeps/mach/hurd/Makefile | 2 +
> sysdeps/unix/sysv/linux/dl-origin.c | 23 +++++++++++
> 13 files changed, 214 insertions(+), 74 deletions(-)
> create mode 100644 elf/liborigin-mod.c
> create mode 100644 elf/tst-origin.c
> create mode 100755 elf/tst-origin.sh
> create mode 100644 stdio-common/_fitoa_word.c
>
> diff --git a/elf/Makefile b/elf/Makefile
> index 77a76f2142..8055d9ffbb 100644
> --- a/elf/Makefile
> +++ b/elf/Makefile
> @@ -456,6 +456,7 @@ tests += \
> tst-noload \
> tst-non-directory-path \
> tst-null-argv \
> + tst-origin \
> tst-p_align1 \
> tst-p_align2 \
> tst-p_align3 \
> @@ -763,6 +764,7 @@ modules-names += \
> libmarkermod5-3 \
> libmarkermod5-4 \
> libmarkermod5-5 \
> + liborigin-mod \
> libtracemod1-1 \
> libtracemod2-1 \
> libtracemod3-1 \
> @@ -3442,3 +3444,24 @@ $(objpfx)tst-dlopen-constructor-null: \
> $(objpfx)tst-dlopen-constructor-null-mod2.so
> $(objpfx)tst-dlopen-constructor-null-mod2.so: \
> $(objpfx)tst-dlopen-constructor-null-mod1.so
> +
> +CFLAGS-tst-origin.c += $(no-stack-protector)
> +$(objpfx)tst-origin: $(objpfx)tst-origin.o $(objpfx)liborigin-mod.so
> + $(LINK.o) -o $@ -B$(csu-objpfx) $(LDFLAGS.so) $< \
> + -Wl,-rpath,\$$ORIGIN \
> + -L$(subst :, -L,$(rpath-link)) -Wl,--no-as-needed -lorigin-mod
> +$(objpfx)liborigin-mod.so: $(objpfx)liborigin-mod.os
> + $(LINK.o) -shared -o $@ -B$(csu-objpfx) $(LDFLAGS.so) \
> + $(LDFLAGS-soname-fname) \
> + $<
> +$(objpfx)tst-origin.out: tst-origin.sh $(objpfx)tst-origin
> + $(SHELL) \
> + $< \
> + '$(common-objpfx)' \
> + '$(test-wrapper-env)' \
> + '$(run-program-env)' \
> + '$(rpath-link)' \
> + tst-origin \
> + liborigin-mod.so \
> + > $@; \
> + $(evaluate-test)
> diff --git a/elf/dl-load.c b/elf/dl-load.c
> index 4998652adf..6b7e9799f3 100644
> --- a/elf/dl-load.c
> +++ b/elf/dl-load.c
> @@ -965,6 +965,12 @@ _dl_map_object_from_fd (const char *name, const
> char *origname, int fd,
> {
> assert (nsid == LM_ID_BASE);
> memset (&id, 0, sizeof (id));
> + char *realname_can = _dl_canonicalize (fd);
> + if (realname_can != NULL)
> + {
> + free (realname);
> + realname = realname_can;
> + }
> }
> else
> {
> diff --git a/elf/dl-origin.c b/elf/dl-origin.c
> index 9f6b921b01..812f5dbb28 100644
> --- a/elf/dl-origin.c
> +++ b/elf/dl-origin.c
> @@ -47,3 +47,9 @@ _dl_get_origin (void)
>
> return result;
> }
> +
> +char *
> +_dl_canonicalize (int fd)
> +{
> + return NULL;
> +}
> diff --git a/elf/liborigin-mod.c b/elf/liborigin-mod.c
> new file mode 100644
> index 0000000000..aa6d4c27df
> --- /dev/null
> +++ b/elf/liborigin-mod.c
> @@ -0,0 +1 @@
> +void foo (void) {}
> diff --git a/elf/tst-origin.c b/elf/tst-origin.c
> new file mode 100644
> index 0000000000..734b2e81f6
> --- /dev/null
> +++ b/elf/tst-origin.c
> @@ -0,0 +1,26 @@
> +/* Test if $ORIGIN works correctly with symlinks (BZ 25263)
> + Copyright (C) 2025 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/>. */
> +
> +extern void foo (void);
> +
> +int
> +main (int argc, char *argv[])
> +{
> + foo ();
> + return 0;
> +}
> diff --git a/elf/tst-origin.sh b/elf/tst-origin.sh
> new file mode 100755
> index 0000000000..2555d3ed9e
> --- /dev/null
> +++ b/elf/tst-origin.sh
> @@ -0,0 +1,60 @@
> +#!/bin/sh
> +# Test if $ORIGIN works correctly with symlinks (BZ 25263)
> +# Copyright (C) 2025 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
> +
> +objpfx=$1
> +test_wrapper_env=$2
> +run_program_env=$3
> +library_path=$4
> +test_program=$5
> +test_library=$6
> +
> +cleanup()
> +{
> + # Move the binary and library back to build directory
> + mv $tmpdir/sub/$test_program ${objpfx}elf
> + mv $tmpdir/sub/$test_library ${objpfx}elf
> +
> + rm -rf $tmpdir
> +}
> +
> +tmpdir=$(mktemp -d "${objpfx}elf/tst-origin.XXXXXXXXXX")
> +#trap cleanup 0
> +
> +mkdir ${tmpdir}/sub
> +
> +# Remove the dependency from $library_path
> +mv ${objpfx}elf/$test_program $tmpdir/sub
> +mv ${objpfx}elf/$test_library $tmpdir/sub
> +
> +cd ${tmpdir}
> +ln -s sub/$test_program $test_program
> +
> +${test_wrapper_env} \
> +${run_program_env} \
> +${objpfx}elf/ld.so --library-path "$library_path" \
> + ./$test_program 2>&1 && rc=0 || rc=$?
> +
> +# Also check if ldd resolves the dependency
> +LD_TRACE_LOADED_OBJECTS=1 \
> +${objpfx}elf/ld.so --library-path "$library_path" \
> + ./$test_program 2>&1 | grep 'not found' && rc=1 || rc=0
> +
> +exit $rc
> diff --git a/stdio-common/Makefile b/stdio-common/Makefile
> index b68e9223c6..d3733d0c3d 100644
> --- a/stdio-common/Makefile
> +++ b/stdio-common/Makefile
> @@ -59,6 +59,7 @@ headers := \
> # headers
>
> routines := \
> + _fitoa_word \
> _itoa \
> _itowa \
> asprintf \
> @@ -663,6 +664,8 @@ CFLAGS-dprintf.c += $(config-cflags-wno-ignored-attributes)
> # off for non-shared builds.
> CFLAGS-_itoa.o = $(no-stack-protector)
> CFLAGS-_itoa.op = $(no-stack-protector)
> +CFLAGS-_fitoa_word.o = $(no-stack-protector)
> +CFLAGS-_fitoa_word.op = $(no-stack-protector)
>
> CFLAGS-scanf13.c += $(test-config-cflags-wno-fortify-source)
>
> diff --git a/stdio-common/_fitoa_word.c b/stdio-common/_fitoa_word.c
> new file mode 100644
> index 0000000000..f0b2707173
> --- /dev/null
> +++ b/stdio-common/_fitoa_word.c
> @@ -0,0 +1,60 @@
> +/* Internal function for converting integers to ASCII.
> + Copyright (C) 1994-2025 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/>. */
> +
> +#include <_itoa.h>
> +
> +char *
> +_itoa_word (_ITOA_WORD_TYPE value, char *buflim,
> + unsigned int base, int upper_case)
> +{
> + const char *digits = (upper_case
> + ? _itoa_upper_digits
> + : _itoa_lower_digits);
> +
> + switch (base)
> + {
> +#define SPECIAL(Base) \
> + case Base: \
> + do \
> + *--buflim = digits[value % Base]; \
> + while ((value /= Base) != 0); \
> + break
> +
> + SPECIAL (10);
> + SPECIAL (16);
> + SPECIAL (8);
> + default:
> + do
> + *--buflim = digits[value % base];
> + while ((value /= base) != 0);
> + }
> + return buflim;
> +}
> +#undef SPECIAL
> +
> +char *
> +_fitoa_word (_ITOA_WORD_TYPE value, char *buf, unsigned int base,
> + int upper_case)
> +{
> + char tmpbuf[sizeof (value) * 4]; /* Worst case length: base 2.
> */
> + char *cp = _itoa_word (value, tmpbuf + sizeof (value) * 4, base,
> upper_case);
> + while (cp < tmpbuf + sizeof (value) * 4)
> + *buf++ = *cp++;
> + return buf;
> +}
> +
> diff --git a/stdio-common/_itoa.c b/stdio-common/_itoa.c
> index 51c3ab9c14..08859f0dd0 100644
> --- a/stdio-common/_itoa.c
> +++ b/stdio-common/_itoa.c
> @@ -162,38 +162,6 @@ const struct base_table_t _itoa_base_table[]
> attribute_hidden =
> };
> #endif
>
> -#if IS_IN (libc)
> -char *
> -_itoa_word (_ITOA_WORD_TYPE value, char *buflim,
> - unsigned int base, int upper_case)
> -{
> - const char *digits = (upper_case
> - ? _itoa_upper_digits
> - : _itoa_lower_digits);
> -
> - switch (base)
> - {
> -#define SPECIAL(Base) \
> - case Base: \
> - do \
> - *--buflim = digits[value % Base]; \
> - while ((value /= Base) != 0); \
> - break
> -
> - SPECIAL (10);
> - SPECIAL (16);
> - SPECIAL (8);
> - default:
> - do
> - *--buflim = digits[value % base];
> - while ((value /= base) != 0);
> - }
> - return buflim;
> -}
> -#undef SPECIAL
> -#endif /* IS_IN (libc) */
> -
> -
> #if _ITOA_NEEDED
> char *
> _itoa (unsigned long long int value, char *buflim, unsigned int base,
> @@ -460,17 +428,6 @@ _itoa (unsigned long long int value, char *buflim,
> unsigned int base,
> }
> #endif
>
> -char *
> -_fitoa_word (_ITOA_WORD_TYPE value, char *buf, unsigned int base,
> - int upper_case)
> -{
> - char tmpbuf[sizeof (value) * 4]; /* Worst case length: base 2. */
> - char *cp = _itoa_word (value, tmpbuf + sizeof (value) * 4, base, upper_case);
> - while (cp < tmpbuf + sizeof (value) * 4)
> - *buf++ = *cp++;
> - return buf;
> -}
> -
> #if _ITOA_NEEDED
> char *
> _fitoa (unsigned long long value, char *buf, unsigned int base, int upper_case)
> diff --git a/sysdeps/generic/_itoa.h b/sysdeps/generic/_itoa.h
> index d7e3007389..2f170d3bf2 100644
> --- a/sysdeps/generic/_itoa.h
> +++ b/sysdeps/generic/_itoa.h
> @@ -51,40 +51,9 @@ hidden_proto (_itoa_upper_digits)
> hidden_proto (_itoa_lower_digits)
> #endif
>
> -#if IS_IN (libc)
> extern char *_itoa_word (_ITOA_WORD_TYPE value, char *buflim,
> unsigned int base,
> int upper_case) attribute_hidden;
> -#else
> -static inline char * __attribute__ ((unused, always_inline))
> -_itoa_word (_ITOA_WORD_TYPE value, char *buflim,
> - unsigned int base, int upper_case)
> -{
> - const char *digits = (upper_case
> - ? _itoa_upper_digits
> - : _itoa_lower_digits);
> -
> - switch (base)
> - {
> -# define SPECIAL(Base) \
> - case Base: \
> - do \
> - *--buflim = digits[value % Base]; \
> - while ((value /= Base) != 0); \
> - break
> -
> - SPECIAL (10);
> - SPECIAL (16);
> - SPECIAL (8);
> - default:
> - do
> - *--buflim = digits[value % base];
> - while ((value /= base) != 0);
> - }
> - return buflim;
> -}
> -# undef SPECIAL
> -#endif
>
> /* Similar to the _itoa functions, but output starts at buf and pointer
> after the last written character is returned. */
> diff --git a/sysdeps/generic/ldsodefs.h b/sysdeps/generic/ldsodefs.h
> index 8465cbaa9b..19494b82ee 100644
> --- a/sysdeps/generic/ldsodefs.h
> +++ b/sysdeps/generic/ldsodefs.h
> @@ -1223,6 +1223,10 @@ extern struct link_map * _dl_get_dl_main_map
> (void) attribute_hidden;
> /* Find origin of the executable. */
> extern const char *_dl_get_origin (void) attribute_hidden;
>
> +/* Return the canonalized path name from the opened file descriptor FD,
> + or NULL otherwise. */
> +extern char * _dl_canonicalize (int fd) attribute_hidden;
> +
> /* Count DSTs. */
> extern size_t _dl_dst_count (const char *name) attribute_hidden;
>
> diff --git a/sysdeps/mach/hurd/Makefile b/sysdeps/mach/hurd/Makefile
> index 13e5cea4c2..4b69b40065 100644
> --- a/sysdeps/mach/hurd/Makefile
> +++ b/sysdeps/mach/hurd/Makefile
> @@ -300,6 +300,8 @@ ifeq ($(subdir),elf)
> check-execstack-xfail += ld.so libc.so libpthread.so
> # We always create a thread for signals
> test-xfail-tst-single_threaded-pthread-static = yes
> +# Bug 25263
> +test-xfail-tst-origin = yes
>
> CFLAGS-tst-execstack.c += -DDEFAULT_RWX_STACK=1
> endif
> diff --git a/sysdeps/unix/sysv/linux/dl-origin.c
> b/sysdeps/unix/sysv/linux/dl-origin.c
> index decdd8ae9e..3c52ba51a6 100644
> --- a/sysdeps/unix/sysv/linux/dl-origin.c
> +++ b/sysdeps/unix/sysv/linux/dl-origin.c
> @@ -21,6 +21,7 @@
> #include <fcntl.h>
> #include <ldsodefs.h>
> #include <sysdep.h>
> +#include <fd_to_filename.h>
>
> /* On Linux >= 2.1 systems which have the dcache implementation we can get
> the path of the application from the /proc/self/exe symlink. Try this
> @@ -72,3 +73,25 @@ _dl_get_origin (void)
>
> return result;
> }
> +
> +/* On Linux, readlink on the magic symlinks in /proc/self/fd also has
> + the same behavior of returning the canonical path from the dcache.
> + If it does not work, we do not bother to canonicalize. */
> +
> +char *
> +_dl_canonicalize (int fd)
> +{
> + struct fd_to_filename fdfilename;
> + char canonical[PATH_MAX];
> + char *path = __fd_to_filename (fd, &fdfilename);
> + int size = INTERNAL_SYSCALL_CALL (readlinkat, AT_FDCWD, path,
> + canonical, PATH_MAX - 1);
> +
> + /* Check if the path was truncated. */
> + if (size >= 0 && size < PATH_MAX - 1)
> + {
> + canonical[size] = '\0';
> + return __strdup (canonical);
> + }
> + return NULL;
> +}
> --
> 2.43.0
--
Geoffrey Thomas
geofft@ldpreload.com
More information about the Libc-alpha
mailing list