[v3] libdwfl: resolve all paths relative to sysroot
Commit Message
Whenever possible, resolve all symlinks as if the sysroot path were a
chroot environment. This prevents potential interactions with files from
the host filesystem.
Signed-off-by: Michal Sekletar <msekleta@redhat.com>
---
configure.ac | 26 ++++++++++++++++
libdwfl/dwfl_segment_report_module.c | 44 +++++++++++++++++++++++-----
libdwfl/link_map.c | 37 ++++++++++++++++++++++-
tests/run-sysroot.sh | 34 +++++++++++++++++++++
4 files changed, 132 insertions(+), 9 deletions(-)
Comments
On Mon, Jun 2, 2025 at 8:47 AM Michal Sekletar <msekleta@redhat.com> wrote:
>
> Whenever possible, resolve all symlinks as if the sysroot path were a
> chroot environment. This prevents potential interactions with files from
> the host filesystem.
>
> Signed-off-by: Michal Sekletar <msekleta@redhat.com>
> ---
> configure.ac | 26 ++++++++++++++++
> libdwfl/dwfl_segment_report_module.c | 44 +++++++++++++++++++++++-----
> libdwfl/link_map.c | 37 ++++++++++++++++++++++-
> tests/run-sysroot.sh | 34 +++++++++++++++++++++
> 4 files changed, 132 insertions(+), 9 deletions(-)
>
> diff --git a/configure.ac b/configure.ac
> index 0670e01a..b8531aa7 100644
> --- a/configure.ac
> +++ b/configure.ac
> @@ -279,6 +279,32 @@ case "$CFLAGS" in
> ;;
> esac
>
> +AC_CACHE_CHECK(
> + [for openat2 with RESOLVE_IN_ROOT support],
> + [eu_cv_openat2_RESOLVE_IN_ROOT],
> + [AC_LINK_IFELSE(
> + [AC_LANG_PROGRAM(
> + [[#define _GNU_SOURCE
> + #include <fcntl.h>
> + #include <stdlib.h>
> + #include <unistd.h>
> + #include <sys/syscall.h>
> + #include <linux/openat2.h>
> + #include <stdio.h>
> + ]], [[
> + struct open_how how = { .flags = O_RDONLY|O_DIRECTORY, .resolve = RESOLVE_IN_ROOT };
> + int dfd = open (".", O_PATH);
> + return syscall (SYS_openat2, dfd, ".", &how, sizeof(how)) < 0;
> + ]]
> + )],
> + [eu_cv_openat2_RESOLVE_IN_ROOT=yes],
> + [eu_cv_openat2_RESOLVE_IN_ROOT=no]
> + )]
> +)
> +AS_IF([test "x$eu_cv_openat2_RESOLVE_IN_ROOT" = xyes],
> + [AC_DEFINE([HAVE_OPENAT2_RESOLVE_IN_ROOT], [1], [Define if openat2 is available])]
> +)
> +
> dnl enable debugging of branch prediction.
> AC_ARG_ENABLE([debugpred],
> AS_HELP_STRING([--enable-debugpred],[build binaries with support to debug branch prediction]),
> diff --git a/libdwfl/dwfl_segment_report_module.c b/libdwfl/dwfl_segment_report_module.c
> index 32f44af8..f2f866c2 100644
> --- a/libdwfl/dwfl_segment_report_module.c
> +++ b/libdwfl/dwfl_segment_report_module.c
> @@ -37,6 +37,12 @@
> #include <inttypes.h>
> #include <fcntl.h>
>
> +#ifdef HAVE_OPENAT2_RESOLVE_IN_ROOT
> +#include <linux/openat2.h>
> +#include <sys/syscall.h>
> +#include <unistd.h>
> +#endif
> +
> #include <system.h>
>
>
> @@ -783,17 +789,39 @@ dwfl_segment_report_module (Dwfl *dwfl, int ndx, const char *name,
> /* We were not handed specific executable hence try to look for it in
> sysroot if it is set. */
> if (dwfl->sysroot && !executable)
> - {
> - int r;
> - char *n;
> + {
> +#ifdef HAVE_OPENAT2_RESOLVE_IN_ROOT
> + int sysrootfd, err;
> +
> + struct open_how how = {
> + .flags = O_RDONLY,
> + .resolve = RESOLVE_IN_ROOT,
> + };
> +
> + sysrootfd = open (dwfl->sysroot, O_DIRECTORY|O_PATH);
> + if (sysrootfd < 0)
> + return -1;
> +
> + fd = syscall (SYS_openat2, sysrootfd, name, &how, sizeof(how));
> + err = fd < 0 ? -errno : 0;
>
> - r = asprintf (&n, "%s%s", dwfl->sysroot, name);
> - if (r > 0)
> + close (sysrootfd);
> +
> + /* Fallback to regular open() if openat2 is not available. */
> + if (fd < 0 && err == -ENOSYS)
> +#endif
> {
> - fd = open (n, O_RDONLY);
> - free (n);
> + int r;
> + char *n;
> +
> + r = asprintf (&n, "%s%s", dwfl->sysroot, name);
> + if (r > 0)
> + {
> + fd = open (n, O_RDONLY);
> + free (n);
> + }
> }
> - }
> + }
> else
> fd = open (name, O_RDONLY);
>
> diff --git a/libdwfl/link_map.c b/libdwfl/link_map.c
> index 8ab14862..013a415d 100644
> --- a/libdwfl/link_map.c
> +++ b/libdwfl/link_map.c
> @@ -34,6 +34,12 @@
>
> #include <fcntl.h>
>
> +#ifdef HAVE_OPENAT2_RESOLVE_IN_ROOT
> +#include <linux/openat2.h>
> +#include <sys/syscall.h>
> +#include <unistd.h>
> +#endif
> +
> /* This element is always provided and always has a constant value.
> This makes it an easy thing to scan for to discern the format. */
> #define PROBE_TYPE AT_PHENT
> @@ -418,19 +424,48 @@ report_r_debug (uint_fast8_t elfclass, uint_fast8_t elfdata,
> /* This code is mostly inlined dwfl_report_elf. */
> char *sysroot_name = NULL;
> const char *sysroot = dwfl->sysroot;
> + int fd;
>
> /* Don't use the sysroot if the path is already inside it. */
> bool name_in_sysroot = sysroot && startswith (name, sysroot);
>
> if (sysroot && !name_in_sysroot)
> {
> + const char *n = NULL;
> +
> if (asprintf (&sysroot_name, "%s%s", sysroot, name) < 0)
> return release_buffer (&memory_closure, &buffer, &buffer_available, -1);
>
> + n = name;
> name = sysroot_name;
> +
> +#ifdef HAVE_OPENAT2_RESOLVE_IN_ROOT
> + int sysrootfd, err;
> +
> + struct open_how how = {
> + .flags = O_RDONLY,
> + .resolve = RESOLVE_IN_ROOT,
> + };
> +
> + sysrootfd = open (sysroot, O_DIRECTORY|O_PATH);
> + if (sysrootfd < 0)
> + return -1;
> +
> + fd = syscall (SYS_openat2, sysrootfd, n, &how, sizeof(how));
> + err = fd < 0 ? -errno : 0;
> +
> + close (sysrootfd);
> +
> + /* Fallback to regular open() if openat2 is not available. */
> + if (fd < 0 && err == -ENOSYS)
> +#endif
> + {
> + fd = open (name, O_RDONLY);
> + }
> }
> + else
> + fd = open (name, O_RDONLY);
>
> - int fd = open (name, O_RDONLY);
> if (fd >= 0)
> {
> Elf *elf;
> diff --git a/tests/run-sysroot.sh b/tests/run-sysroot.sh
> index 1dc079cd..fe302446 100755
> --- a/tests/run-sysroot.sh
> +++ b/tests/run-sysroot.sh
> @@ -46,4 +46,38 @@ TID 431185:
> #8 0x0000aaaae56127f0 _start
> EOF
>
> +HAVE_OPENAT2=$(grep '^#define HAVE_OPENAT2_RESOLVE_IN_ROOT' \
> + ${abs_builddir}/../config.h | awk '{print $3}')
> +
> +if [[ "$HAVE_OPENAT2" = 1 ]]; then
> + # Change the layout of files in sysroot to test symlink escape scenario
> + rm -f "${tmpdir}/sysroot/bin"
> + mkdir "${tmpdir}/sysroot/bin"
> + mv "${tmpdir}/sysroot/usr/bin/bash" "${tmpdir}/sysroot/bin/bash"
> + ln -s /bin/bash "${tmpdir}/sysroot/usr/bin/bash"
> +
> + # Check that stack with --sysroot generates correct backtrace even if target
> + # binary is actually absolute symlink pointing outside of sysroot directory
> + testrun "${abs_top_builddir}"/src/stack --core "${tmpdir}/core.bash" \
> + --sysroot "${tmpdir}/sysroot" >"${tmpdir}/stack.out"
> +
> + # Remove 2 stack frames with symbol names contained in .gnu_debugdata.
> + # Whether or not these names appear in the output depends on if elfutils
> + # was built with LZMA support.
> + sed -i '4,5d' "${tmpdir}/stack.out"
> +
> + # Check that we are able to get fully symbolized backtrace
> + testrun_compare cat "${tmpdir}/stack.out" <<\EOF
> +PID 431185 - core
> +TID 431185:
> +#0 0x0000ffff8ebe5a8c kill
> +#3 0x0000aaaae562b2fc execute_command
> +#4 0x0000aaaae561cbb4 reader_loop
> +#5 0x0000aaaae5611bf0 main
> +#6 0x0000ffff8ebd09dc __libc_start_call_main
> +#7 0x0000ffff8ebd0ab0 __libc_start_main@@GLIBC_2.34
> +#8 0x0000aaaae56127f0 _start
> +EOF
> +fi
> +
> exit_cleanup
> --
> 2.39.5 (Apple Git-154)
>
Thanks Michal, I've pushed this patch.
Aaron
@@ -279,6 +279,32 @@ case "$CFLAGS" in
;;
esac
+AC_CACHE_CHECK(
+ [for openat2 with RESOLVE_IN_ROOT support],
+ [eu_cv_openat2_RESOLVE_IN_ROOT],
+ [AC_LINK_IFELSE(
+ [AC_LANG_PROGRAM(
+ [[#define _GNU_SOURCE
+ #include <fcntl.h>
+ #include <stdlib.h>
+ #include <unistd.h>
+ #include <sys/syscall.h>
+ #include <linux/openat2.h>
+ #include <stdio.h>
+ ]], [[
+ struct open_how how = { .flags = O_RDONLY|O_DIRECTORY, .resolve = RESOLVE_IN_ROOT };
+ int dfd = open (".", O_PATH);
+ return syscall (SYS_openat2, dfd, ".", &how, sizeof(how)) < 0;
+ ]]
+ )],
+ [eu_cv_openat2_RESOLVE_IN_ROOT=yes],
+ [eu_cv_openat2_RESOLVE_IN_ROOT=no]
+ )]
+)
+AS_IF([test "x$eu_cv_openat2_RESOLVE_IN_ROOT" = xyes],
+ [AC_DEFINE([HAVE_OPENAT2_RESOLVE_IN_ROOT], [1], [Define if openat2 is available])]
+)
+
dnl enable debugging of branch prediction.
AC_ARG_ENABLE([debugpred],
AS_HELP_STRING([--enable-debugpred],[build binaries with support to debug branch prediction]),
@@ -37,6 +37,12 @@
#include <inttypes.h>
#include <fcntl.h>
+#ifdef HAVE_OPENAT2_RESOLVE_IN_ROOT
+#include <linux/openat2.h>
+#include <sys/syscall.h>
+#include <unistd.h>
+#endif
+
#include <system.h>
@@ -783,17 +789,39 @@ dwfl_segment_report_module (Dwfl *dwfl, int ndx, const char *name,
/* We were not handed specific executable hence try to look for it in
sysroot if it is set. */
if (dwfl->sysroot && !executable)
- {
- int r;
- char *n;
+ {
+#ifdef HAVE_OPENAT2_RESOLVE_IN_ROOT
+ int sysrootfd, err;
+
+ struct open_how how = {
+ .flags = O_RDONLY,
+ .resolve = RESOLVE_IN_ROOT,
+ };
+
+ sysrootfd = open (dwfl->sysroot, O_DIRECTORY|O_PATH);
+ if (sysrootfd < 0)
+ return -1;
+
+ fd = syscall (SYS_openat2, sysrootfd, name, &how, sizeof(how));
+ err = fd < 0 ? -errno : 0;
- r = asprintf (&n, "%s%s", dwfl->sysroot, name);
- if (r > 0)
+ close (sysrootfd);
+
+ /* Fallback to regular open() if openat2 is not available. */
+ if (fd < 0 && err == -ENOSYS)
+#endif
{
- fd = open (n, O_RDONLY);
- free (n);
+ int r;
+ char *n;
+
+ r = asprintf (&n, "%s%s", dwfl->sysroot, name);
+ if (r > 0)
+ {
+ fd = open (n, O_RDONLY);
+ free (n);
+ }
}
- }
+ }
else
fd = open (name, O_RDONLY);
@@ -34,6 +34,12 @@
#include <fcntl.h>
+#ifdef HAVE_OPENAT2_RESOLVE_IN_ROOT
+#include <linux/openat2.h>
+#include <sys/syscall.h>
+#include <unistd.h>
+#endif
+
/* This element is always provided and always has a constant value.
This makes it an easy thing to scan for to discern the format. */
#define PROBE_TYPE AT_PHENT
@@ -418,19 +424,48 @@ report_r_debug (uint_fast8_t elfclass, uint_fast8_t elfdata,
/* This code is mostly inlined dwfl_report_elf. */
char *sysroot_name = NULL;
const char *sysroot = dwfl->sysroot;
+ int fd;
/* Don't use the sysroot if the path is already inside it. */
bool name_in_sysroot = sysroot && startswith (name, sysroot);
if (sysroot && !name_in_sysroot)
{
+ const char *n = NULL;
+
if (asprintf (&sysroot_name, "%s%s", sysroot, name) < 0)
return release_buffer (&memory_closure, &buffer, &buffer_available, -1);
+ n = name;
name = sysroot_name;
+
+#ifdef HAVE_OPENAT2_RESOLVE_IN_ROOT
+ int sysrootfd, err;
+
+ struct open_how how = {
+ .flags = O_RDONLY,
+ .resolve = RESOLVE_IN_ROOT,
+ };
+
+ sysrootfd = open (sysroot, O_DIRECTORY|O_PATH);
+ if (sysrootfd < 0)
+ return -1;
+
+ fd = syscall (SYS_openat2, sysrootfd, n, &how, sizeof(how));
+ err = fd < 0 ? -errno : 0;
+
+ close (sysrootfd);
+
+ /* Fallback to regular open() if openat2 is not available. */
+ if (fd < 0 && err == -ENOSYS)
+#endif
+ {
+ fd = open (name, O_RDONLY);
+ }
}
+ else
+ fd = open (name, O_RDONLY);
- int fd = open (name, O_RDONLY);
if (fd >= 0)
{
Elf *elf;
@@ -46,4 +46,38 @@ TID 431185:
#8 0x0000aaaae56127f0 _start
EOF
+HAVE_OPENAT2=$(grep '^#define HAVE_OPENAT2_RESOLVE_IN_ROOT' \
+ ${abs_builddir}/../config.h | awk '{print $3}')
+
+if [[ "$HAVE_OPENAT2" = 1 ]]; then
+ # Change the layout of files in sysroot to test symlink escape scenario
+ rm -f "${tmpdir}/sysroot/bin"
+ mkdir "${tmpdir}/sysroot/bin"
+ mv "${tmpdir}/sysroot/usr/bin/bash" "${tmpdir}/sysroot/bin/bash"
+ ln -s /bin/bash "${tmpdir}/sysroot/usr/bin/bash"
+
+ # Check that stack with --sysroot generates correct backtrace even if target
+ # binary is actually absolute symlink pointing outside of sysroot directory
+ testrun "${abs_top_builddir}"/src/stack --core "${tmpdir}/core.bash" \
+ --sysroot "${tmpdir}/sysroot" >"${tmpdir}/stack.out"
+
+ # Remove 2 stack frames with symbol names contained in .gnu_debugdata.
+ # Whether or not these names appear in the output depends on if elfutils
+ # was built with LZMA support.
+ sed -i '4,5d' "${tmpdir}/stack.out"
+
+ # Check that we are able to get fully symbolized backtrace
+ testrun_compare cat "${tmpdir}/stack.out" <<\EOF
+PID 431185 - core
+TID 431185:
+#0 0x0000ffff8ebe5a8c kill
+#3 0x0000aaaae562b2fc execute_command
+#4 0x0000aaaae561cbb4 reader_loop
+#5 0x0000aaaae5611bf0 main
+#6 0x0000ffff8ebd09dc __libc_start_call_main
+#7 0x0000ffff8ebd0ab0 __libc_start_main@@GLIBC_2.34
+#8 0x0000aaaae56127f0 _start
+EOF
+fi
+
exit_cleanup