diff mbox series

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

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

Commit Message

Adhemerval Zanella March 11, 2025, 1:36 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.)

Co-authored-by: Geoffrey Thomas <geofft@ldpreload.com>
---
 elf/Makefile                        | 22 +++++++++++
 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 +++++++++++++++++++++++++++++
 sysdeps/generic/ldsodefs.h          |  4 ++
 sysdeps/mach/hurd/Makefile          |  2 +
 sysdeps/unix/sysv/linux/dl-origin.c | 23 +++++++++++
 9 files changed, 150 insertions(+)
 create mode 100644 elf/liborigin-mod.c
 create mode 100644 elf/tst-origin.c
 create mode 100755 elf/tst-origin.sh
diff mbox series

Patch

diff --git a/elf/Makefile b/elf/Makefile
index 67052b5694..53cf154894 100644
--- a/elf/Makefile
+++ b/elf/Makefile
@@ -455,6 +455,7 @@  tests += \
   tst-noload \
   tst-non-directory-path \
   tst-null-argv \
+  tst-origin \
   tst-p_align1 \
   tst-p_align2 \
   tst-p_align3 \
@@ -762,6 +763,7 @@  modules-names += \
   libmarkermod5-3 \
   libmarkermod5-4 \
   libmarkermod5-5 \
+  liborigin-mod \
   libtracemod1-1 \
   libtracemod2-1 \
   libtracemod3-1 \
@@ -3433,3 +3435,23 @@  LDFLAGS-tst-version-hash-zero-linkmod.so = \
 $(objpfx)tst-version-hash-zero-refmod.so: \
   $(objpfx)tst-version-hash-zero-linkmod.so
 tst-version-hash-zero-refmod.so-no-z-defs = yes
+
+$(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/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;
+}