https://sourceware.org/bugzilla/show_bug.cgi?id=34562

            Bug ID: 34562
           Summary: gprofng: libcollector derives auxv address by walking
                    environ and crashes (SIGSEGV) when environ is
                    relocated to the heap (e.g., via setenv in ELF
                    constructor)
           Product: binutils
           Version: 2.46.1
            Status: UNCONFIRMED
          Severity: critical
          Priority: P2
         Component: gprofng
          Assignee: unassigned at sourceware dot org
          Reporter: rogerio at cadence dot com
  Target Milestone: ---

Overview from Cloude

During profiling of applications using `gprofng collect app`, the target
program intermittently crashes with a **SIGSEGV** inside `_dl_init` before the
application's `main` even executes. 

This issue occurs because `libgp-collector.so` attempts to locate the auxiliary
vector (`auxv`) by walking past the `NULL` terminator of `environ` on the
assumption that `auxv` immediately follows it on the initial stack. However, if
any library or ELF constructor calls `setenv()` or otherwise relocates
`environ` to a heap-allocated memory chunk (via `malloc`) prior to
`libgp-collector` initializing, this walk traverses arbitrary heap data
instead.

If the adjacent heap contains bytes matching `0x21` (which is `AT_SYSINFO_EHDR`
but also a common `malloc` chunk header size for a 0x20-byte chunk), the
collector misidentifies it as the vDSO header pointer and attempts to
dereference the subsequent QWORD, resulting in an immediate segmentation fault.

---

Root Cause Analysis & Faulting Code
In `binutils-v2.46.1/.../lib64/gprofng/libgp-collector.so`, inside
`__collector_ext_mmap_install` (where the `process_vsyscall_page` logic is
inlined):

1. Locating the auxv pointer incorrectly:

51f18: mov  0xfefb1(%rip),%rax   # <environ@GLIBC_2.2.5>
51f1f: mov  (%rax),%rax          ; rax = environ
51f22: test %rax,%rax
51f25: je   ...
51f30: add  $0x8,%rax            ; Walk forward
51f34: cmpq $0x0,-0x8(%rax)      ; Until the previous slot is NULL
51f39: jne  51f30
51f3b: mov  (%rax),%rdx          ; First qword past NULL, assumed to be a_type
51f3e: mov  %rax,0x1148ab(%rip)  # <auxv> <-- Cached as the auxv array pointer


2. Dereferencing without validation:

51a29: mov  0x10(%rax),%rdx      ; next a_type
51a2d: add  $0x10,%rax           ; sizeof(Elf64_auxv_t)
51a31: test %rdx,%rdx
51a34: je   ...                  ; a_type == AT_NULL -> exit loop
51a3a: cmp  $0x21,%rdx           ; a_type == AT_SYSINFO_EHDR ?
51a3e: jne  51a29
51a20: mov  0x8(%rax),%r14       ; r14 = a_val (assumed Elf64_Ehdr *)
...
51ab8: movzwl 0x3c(%r14),%esi    ; ehdr->e_shnum  <-- SIGSEGV HERE


There is zero validation of `r14` (no ELF magic validation, no memory mapping
check) before reading fields like `e_shnum` and `e_shoff`.

---

Minimal Reproducer
The following reproducer compiles a constructor (`moveenv.c`) that mimics
glibc's `setenv()` behavior by relocating `environ` to the heap and placing a
fake `{0x21, <invalid-pointer>}` sequence right after the `NULL` terminator.

hello.c

#include <stdio.h>
int main(void)
{
  double s = 0;
  for (long i = 1; i < 40000000; i++) s += 1.0 / i;
  printf("%f\n", s);
  return 0;
}


moveenv.c

#include <stdlib.h>
extern char **environ;

__attribute__((constructor(101))) static void move_environ(void)
{
  int n = 0;
  while (environ[n]) n++;
  unsigned long *blk = (unsigned long *) malloc((n + 8) * sizeof(char *));
  for (int i = 0; i < n; i++)
    ((char **) blk)[i] = environ[i];
  ((char **) blk)[n] = NULL;
  blk[n + 1] = 33;                     /* a_type = AT_SYSINFO_EHDR */
  blk[n + 2] = 0x6d75645f736e6463UL;   /* a_val  = ASCII "cdns_dum" (invalid
ptr) */
  blk[n + 3] = 0;                      /* a_type = AT_NULL */
  environ = (char **) blk;
}


Execution Commands:

gcc -O2 -g -o hello hello.c
gcc -O2 -g -shared -fPIC -o libmoveenv.so moveenv.c

# 1. Baseline runs correctly:
gprofng collect app -F off -o base.er ./hello 

# 2. Preloading the heap-relocated environment trigger crashes:
LD_PRELOAD=$PWD/libmoveenv.so gprofng collect app -F off -o bug.er ./hello
```

Resulting Crash Backtrace:

=> 0x7ffff7eceab8 <__collector_ext_mmap_install+920>: movzwl 0x3c(%r14),%esi
si_addr = 0x0
r14     = 0x6d75645f736e6463  ("cdns_dum")
rdx     = 0x21                (False AT_SYSINFO_EHDR match)

#0  __collector_ext_mmap_install () from libgp-collector.so
#1  __collector_open_experiment () from libgp-collector.so
#2  collector_init () from libgp-collector.so
#3  call_init (l=..., argc=1, argv=0x7fffffffcc48, env=0x7fffffffcc58) at
dl-init.c:72

---

Suggested Fixes by AI:

To resolve this issue robustly, `libcollector` should avoid traversing the
stack from `environ` entirely:

1. Use Upstream APIs**: Utilize `getauxval(AT_SYSINFO_EHDR)` (supported in
glibc 2.16+).
2. Fallback to Procfs**: If `getauxval` is unavailable, open and parse
`/proc/self/auxv`, which is guaranteed to be accurate regardless of
heap-relocated environment arrays.
3. Add Validation**: Before dereferencing any address found as `a_val` for
`AT_SYSINFO_EHDR`, perform basic validation:
   * Verify that `a_val` is non-zero.
   * Verify that the memory address range is readable.
   * Verify that the header starts with the expected ELF magic bytes
(`\x7fELF`).

-- 
You are receiving this mail because:
You are on the CC list for the bug.

Reply via email to