From c838048c2024fc108478398318c0d427b5b7a807 Mon Sep 17 00:00:00 2001 From: Luc Varoqui Date: Mon, 12 Oct 2026 15:57:46 +0200 Subject: [PATCH] fix(syswrap): return the client's auxv from prctl(PR_GET_AUXV) The kernel answers PR_GET_AUXV (Linux 6.4+) with the auxv it saved when it exec'd the Valgrind tool, so a program reading its auxv this way got the tool's entries, including AT_EXECFN (e.g. .../callgrind-amd64-linux), while getauxval() and /proc/self/auxv already report the client's. Multicall binaries that pick their behavior from AT_EXECFN, such as rust-coreutils 0.8.0 shipped with Ubuntu 26.04, failed with "unknown program 'callgrind-amd64-linux'". Handle PR_GET_AUXV in the prctl wrapper: copy the client auxv, zero-padded to the size the kernel reports, into the caller's buffer and return that size. Keep the kernel's EINVAL for nonzero arg4/arg5 and on kernels without the option, and EFAULT for an unaddressable buffer. Closes COD-3810 Co-Authored-By: Claude --- .gitignore | 1 + callgrind/tests/Makefile.am | 5 + callgrind/tests/prctl_get_auxv.c | 133 ++++++++++++++++++++++ callgrind/tests/prctl_get_auxv.stderr.exp | 0 callgrind/tests/prctl_get_auxv.stdout.exp | 10 ++ callgrind/tests/prctl_get_auxv.vgtest | 5 + configure.ac | 15 +++ coregrind/m_clientstate.c | 4 + coregrind/m_main.c | 5 + coregrind/m_syswrap/syswrap-linux.c | 43 +++++++ coregrind/pub_core_clientstate.h | 6 + include/vki/vki-linux.h | 2 + 12 files changed, 229 insertions(+) create mode 100644 callgrind/tests/prctl_get_auxv.c create mode 100644 callgrind/tests/prctl_get_auxv.stderr.exp create mode 100644 callgrind/tests/prctl_get_auxv.stdout.exp create mode 100644 callgrind/tests/prctl_get_auxv.vgtest diff --git a/.gitignore b/.gitignore index 4ddcb08a0d..89e22ed946 100644 --- a/.gitignore +++ b/.gitignore @@ -167,6 +167,7 @@ /callgrind/tests/find_debuginfo /callgrind/tests/runtime_obj_skip_c /callgrind/tests/runtime_obj_skip_underflow +/callgrind/tests/prctl_get_auxv /callgrind/tests/*.so /callgrind/tests/out_dbg /callgrind/tests/test-suite-overall.log diff --git a/callgrind/tests/Makefile.am b/callgrind/tests/Makefile.am index 48d1bcf590..f41e584371 100644 --- a/callgrind/tests/Makefile.am +++ b/callgrind/tests/Makefile.am @@ -17,6 +17,8 @@ EXTRA_DIST = \ allow_file_caps.stdout.exp \ allow_file_caps_refused.vgtest allow_file_caps_refused.stderr.exp \ allow_file_caps_refused.stdout.exp \ + prctl_get_auxv.vgtest prctl_get_auxv.stderr.exp \ + prctl_get_auxv.stdout.exp \ find_debuginfo.vgtest find_debuginfo.stderr.exp find_debuginfo.post.exp \ runtime_obj_skip_py.vgtest runtime_obj_skip_py.stderr.exp runtime_obj_skip_py.post.exp \ runtime_obj_skip_py.py runtime_obj_skip_py_shim.c \ @@ -43,6 +45,9 @@ EXTRA_DIST = \ inline-crossfile-helper1.h inline-crossfile-helper2.h filter_inline check_PROGRAMS = clreq register_desc register_desc_threads find_debuginfo simwork threads inline-samefile inline-crossfile runtime_obj_skip_c runtime_obj_skip_underflow +if HAVE_PR_GET_AUXV +check_PROGRAMS += prctl_get_auxv +endif AM_CFLAGS += $(AM_FLAG_M3264_PRI) AM_CXXFLAGS += $(AM_FLAG_M3264_PRI) diff --git a/callgrind/tests/prctl_get_auxv.c b/callgrind/tests/prctl_get_auxv.c new file mode 100644 index 0000000000..b8353c5fdf --- /dev/null +++ b/callgrind/tests/prctl_get_auxv.c @@ -0,0 +1,133 @@ +/* prctl(PR_GET_AUXV) must return the program's own auxv, the one on its + stack that getauxval() reads, not the auxv of the Valgrind tool binary + the kernel actually exec'd, and otherwise behave like the kernel. + + Run natively, --kernel-size prints the size the kernel returns. The + test itself takes that kernel size as its only argument. */ + +#include +#include +#include +#include +#include +#include +#include + +static const char *base_name(const char *path) +{ + const char *slash = strrchr(path, '/'); + return slash ? slash + 1 : path; +} + +static int has_only_byte(const void *start, size_t len, unsigned char byte) +{ + const unsigned char *p = start; + for (size_t i = 0; i < len; i++) + if (p[i] != byte) + return 0; + return 1; +} + +static void print_error(const char *what, long res, int err, + int expected_err, const char *expected_name) +{ + printf("%s: %s\n", what, + res == -1 && err == expected_err ? expected_name : "not rejected"); +} + +int main(int argc, char **argv, char **envp) +{ + if (argc > 1 && strcmp(argv[1], "--kernel-size") == 0) { + printf("%ld\n", (long)prctl(PR_GET_AUXV, 0, 0, 0, 0)); + return 0; + } + if (argc != 2) { + fprintf(stderr, "usage: %s KERNEL_SIZE\n", argv[0]); + return 2; + } + long kernel_size = strtol(argv[1], NULL, 10); + + /* The auxv starts right after the NULL that ends envp. */ + char **env_end = envp; + while (*env_end != NULL) + env_end++; + unsigned long *stack_auxv = (unsigned long *)(env_end + 1); + size_t stack_auxv_size = 2 * sizeof(unsigned long); + for (const unsigned long *p = stack_auxv; p[0] != AT_NULL; p += 2) + stack_auxv_size += 2 * sizeof(unsigned long); + + unsigned long buf[512]; + memset(buf, 0xaa, sizeof(buf)); + long size = prctl(PR_GET_AUXV, buf, sizeof(buf), 0, 0); + if (size < 0) { + perror("prctl(PR_GET_AUXV)"); + return 1; + } + + for (const unsigned long *p = buf; p[0] != AT_NULL; p += 2) + if (p[0] == AT_EXECFN) + printf("AT_EXECFN: %s\n", base_name((const char *)p[1])); + + printf("size: %s\n", size == kernel_size ? "same as the kernel's" + : "differs from the kernel's"); + + int is_size_plausible = (size_t)size >= stack_auxv_size + && (size_t)size <= sizeof(buf); + int matches_stack = is_size_plausible + && memcmp(buf, stack_auxv, stack_auxv_size) == 0; + printf("matches the auxv on the stack: %s\n", matches_stack ? "yes" : "no"); + + const char *bytes = (const char *)buf; + int is_zero_padded = is_size_plausible + && has_only_byte(bytes + stack_auxv_size, size - stack_auxv_size, 0) + && has_only_byte(bytes + size, sizeof(buf) - size, 0xaa); + printf("zero padding up to the returned size: %s\n", + is_zero_padded ? "yes" : "no"); + + /* The kernel answers from the auxv it saved at exec, so later writes + to the copy on the stack do not show. */ + unsigned long first_value = stack_auxv[1]; + stack_auxv[1] = ~first_value; + unsigned long later[512]; + long later_size = prctl(PR_GET_AUXV, later, sizeof(later), 0, 0); + stack_auxv[1] = first_value; + int is_saved_copy = later_size == size + && memcmp(later, buf, stack_auxv_size) == 0; + printf("stack auxv modified: %s\n", + is_saved_copy ? "saved vector returned" : "modified vector returned"); + + unsigned long small[4], fill; + memset(small, 0xaa, sizeof(small)); + memset(&fill, 0xaa, sizeof(fill)); + long small_size = prctl(PR_GET_AUXV, small, 2 * sizeof(unsigned long), 0, 0); + int is_truncated = small_size == size + && memcmp(small, buf, 2 * sizeof(unsigned long)) == 0 + && small[2] == fill && small[3] == fill; + printf("short buffer: %s\n", + is_truncated ? "full size returned, rest untouched" : "wrong"); + + long res; + errno = 0; + res = prctl(PR_GET_AUXV, buf, sizeof(buf), 1, 0); + print_error("nonzero arg4", res, errno, EINVAL, "EINVAL"); + + errno = 0; + res = prctl(PR_GET_AUXV, buf, sizeof(buf), 0, 1); + print_error("nonzero arg5", res, errno, EINVAL, "EINVAL"); + + errno = 0; + res = prctl(PR_GET_AUXV, (void *)8, sizeof(buf), 0, 0); + print_error("unmapped buffer", res, errno, EFAULT, "EFAULT"); + + void *read_only = mmap(NULL, sizeof(buf), PROT_READ, + MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (read_only == MAP_FAILED) { + perror("mmap"); + return 1; + } + errno = 0; + res = prctl(PR_GET_AUXV, read_only, sizeof(buf), 0, 0); + print_error("read-only buffer", res, errno, EFAULT, "EFAULT"); + + return 0; +} diff --git a/callgrind/tests/prctl_get_auxv.stderr.exp b/callgrind/tests/prctl_get_auxv.stderr.exp new file mode 100644 index 0000000000..e69de29bb2 diff --git a/callgrind/tests/prctl_get_auxv.stdout.exp b/callgrind/tests/prctl_get_auxv.stdout.exp new file mode 100644 index 0000000000..85b0846dbc --- /dev/null +++ b/callgrind/tests/prctl_get_auxv.stdout.exp @@ -0,0 +1,10 @@ +AT_EXECFN: prctl_get_auxv +size: same as the kernel's +matches the auxv on the stack: yes +zero padding up to the returned size: yes +stack auxv modified: saved vector returned +short buffer: full size returned, rest untouched +nonzero arg4: EINVAL +nonzero arg5: EINVAL +unmapped buffer: EFAULT +read-only buffer: EFAULT diff --git a/callgrind/tests/prctl_get_auxv.vgtest b/callgrind/tests/prctl_get_auxv.vgtest new file mode 100644 index 0000000000..f3dabdc443 --- /dev/null +++ b/callgrind/tests/prctl_get_auxv.vgtest @@ -0,0 +1,5 @@ +prereq: test -x prctl_get_auxv && ../../tests/os_test linux 6.4.0 +prog: prctl_get_auxv +args: `./prctl_get_auxv --kernel-size` +vgopts: -q --callgrind-out-file=callgrind.out.prctl_get_auxv +cleanup: rm -f callgrind.out.prctl_get_auxv diff --git a/configure.ac b/configure.ac index d67b72134d..5471d14425 100644 --- a/configure.ac +++ b/configure.ac @@ -2187,6 +2187,21 @@ AC_MSG_RESULT([no]) AM_CONDITIONAL(HAVE_NR_IO_PGETEVENTS, [test x$ac_have_nr_io_pgetevents = xyes]) +AC_MSG_CHECKING([for PR_GET_AUXV]) +AC_COMPILE_IFELSE([AC_LANG_PROGRAM([[ +#include +]], [[ +return PR_GET_AUXV +]])], [ +ac_have_pr_get_auxv=yes +AC_MSG_RESULT([yes]) +], [ +ac_have_pr_get_auxv=no +AC_MSG_RESULT([no]) +]) + +AM_CONDITIONAL(HAVE_PR_GET_AUXV, [test x$ac_have_pr_get_auxv = xyes]) + #---------------------------------------------------------------------------- # Checking for supported compiler flags. #---------------------------------------------------------------------------- diff --git a/coregrind/m_clientstate.c b/coregrind/m_clientstate.c index 61cf0502b4..ea37e3fc32 100644 --- a/coregrind/m_clientstate.c +++ b/coregrind/m_clientstate.c @@ -59,6 +59,10 @@ SizeT VG_(clstk_max_size) = 0; initimg-{linux,solaris}.c. */ UWord* VG_(client_auxv) = NULL; +/* Solaris and Linux only, a startup copy of the client auxv. */ +const UWord* VG_(client_saved_auxv) = NULL; +SizeT VG_(client_saved_auxv_size) = 0; + Addr VG_(brk_base) = 0; /* start of brk */ Addr VG_(brk_limit) = 0; /* current brk */ diff --git a/coregrind/m_main.c b/coregrind/m_main.c index c7df35e5ca..a536e56502 100644 --- a/coregrind/m_main.c +++ b/coregrind/m_main.c @@ -1745,6 +1745,11 @@ Int valgrind_main ( Int argc, HChar **argv, HChar **envp ) } client_auxv_len += 2 * sizeof(UWord); + UWord *saved_auxv = VG_(malloc)("main.saved_auxv", client_auxv_len); + VG_(memcpy)(saved_auxv, VG_(client_auxv), client_auxv_len); + VG_(client_saved_auxv) = saved_auxv; + VG_(client_saved_auxv_size) = client_auxv_len; + VG_(write)(fd, VG_(client_auxv), client_auxv_len); /* Don't bother to seek the file back to the start; instead do diff --git a/coregrind/m_syswrap/syswrap-linux.c b/coregrind/m_syswrap/syswrap-linux.c index 3606bb9ad6..5157f582cb 100644 --- a/coregrind/m_syswrap/syswrap-linux.c +++ b/coregrind/m_syswrap/syswrap-linux.c @@ -1584,6 +1584,46 @@ PRE(sys_prctl) case VKI_PR_CAPBSET_DROP: PRE_REG_READ2(int, "prctl", int, option, int, capability); break; + case VKI_PR_GET_AUXV: { + PRE_REG_READ5(long, "prctl", int, option, void *, addr, + unsigned long, size, unsigned long, arg4, + unsigned long, arg5); + /* The kernel would return the auxv it saved when it exec'd the + Valgrind tool. Return the client's auxv instead, from the copy + saved at startup: the vector getauxval() and /proc/self/auxv + report, read without touching the client stack. + A call completed in the pre-handler must not set SfMayBlock. */ + *flags &= ~SfMayBlock; + if (ARG4 != 0 || ARG5 != 0) { + SET_STATUS_Failure(VKI_EINVAL); + break; + } + /* With a zero size the kernel copies nothing and returns the size + of its saved auxv, or fails if it does not know PR_GET_AUXV. */ + SysRes kres = VG_(do_syscall5)(__NR_prctl, VKI_PR_GET_AUXV, 0, 0, 0, 0); + if (sr_isError(kres)) { + SET_STATUS_from_SysRes(kres); + break; + } + vg_assert(VG_(client_saved_auxv) != NULL); + SizeT auxv_size = VG_(client_saved_auxv_size); + /* Like the kernel, report the size of a zero-padded buffer at least + as large as the auxv, and copy as much of it as fits. */ + SizeT total = VG_MAX(sr_Res(kres), auxv_size); + SizeT len = VG_MIN(total, ARG3); + if (len > 0) { + PRE_MEM_WRITE("prctl(get-auxv)", ARG2, len); + if (!VG_(am_is_valid_for_client)((Addr)ARG2, len, VKI_PROT_WRITE)) { + SET_STATUS_Failure(VKI_EFAULT); + break; + } + SizeT copied = VG_MIN(len, auxv_size); + VG_(memcpy)((void *)(Addr)ARG2, VG_(client_saved_auxv), copied); + VG_(memset)((HChar *)(Addr)ARG2 + copied, 0, len - copied); + } + SET_STATUS_Success(total); + break; + } default: PRE_REG_READ5(long, "prctl", int, option, unsigned long, arg2, unsigned long, arg3, @@ -1612,6 +1652,9 @@ POST(sys_prctl) case VKI_PR_GET_ENDIAN: POST_MEM_WRITE(ARG2, sizeof(Int)); break; + case VKI_PR_GET_AUXV: + POST_MEM_WRITE(ARG2, VG_MIN(RES, ARG3)); + break; case VKI_PR_SET_NAME: { const HChar* new_name = (const HChar*) (Addr)ARG2; diff --git a/coregrind/pub_core_clientstate.h b/coregrind/pub_core_clientstate.h index 573de81cee..381a37f60c 100644 --- a/coregrind/pub_core_clientstate.h +++ b/coregrind/pub_core_clientstate.h @@ -51,6 +51,12 @@ extern SizeT VG_(clstk_max_size); // max size of the main threads's client stack or initimg-solaris.c, respectively. */ extern UWord* VG_(client_auxv); +/* Linux and Solaris only: a copy of the client auxv taken at startup, + which later changes to the client stack cannot affect. The size is in + bytes and includes the terminating AT_NULL entry. */ +extern const UWord* VG_(client_saved_auxv); +extern SizeT VG_(client_saved_auxv_size); + extern Addr VG_(brk_base); // start of brk extern Addr VG_(brk_limit); // current brk diff --git a/include/vki/vki-linux.h b/include/vki/vki-linux.h index 3f9272f4d1..5c937d9e4c 100644 --- a/include/vki/vki-linux.h +++ b/include/vki/vki-linux.h @@ -2797,6 +2797,8 @@ struct vki_vt_consize { #define VKI_PR_SET_TAGGED_ADDR_CTRL 55 #define VKI_PR_GET_TAGGED_ADDR_CTRL 56 +#define VKI_PR_GET_AUXV 0x41555856 + //---------------------------------------------------------------------- // From linux-2.6.19/include/linux/usbdevice_fs.h //----------------------------------------------------------------------