diff mbox series

[v3] elf: Canonicalize $ORIGIN in an explicit ld.so invocation [BZ 25263]

Message ID 20250311201720.2794859-1-adhemerval.zanella@linaro.org
State New
Headers show
Series [v3] elf: Canonicalize $ORIGIN in an explicit ld.so invocation [BZ 25263] | expand

Commit Message

Adhemerval Zanella March 11, 2025, 8:17 p.m. UTC
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
32-bits pulls a lot of definitions from _itoa.c (due _ITOA_NEEDED
debing defined).  To simplify the build move the required function
to a new file, _fitoa_word.c.

Checked on x86_64-linux-gnu and i686-linux-gnu.

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 mbox series

Patch

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;
+}