[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