Date: Thu, 11 Jun 2026 09:13:23 +0000 From: bugzilla-noreply@freebsd.org To: bugs@FreeBSD.org Subject: [Bug 295991] lib/libc posix_spawnp(): PATH-search child runs on an undersized rfork_thread stack and underflows into the caller's heap (heavily-linked processes; PHP proc_open) Message-ID: <bug-295991-227@https.bugs.freebsd.org/bugzilla/>
index | next in thread | raw e-mail
https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=295991 Bug ID: 295991 Summary: lib/libc posix_spawnp(): PATH-search child runs on an undersized rfork_thread stack and underflows into the caller's heap (heavily-linked processes; PHP proc_open) Product: Base System Version: 15.0-RELEASE Hardware: amd64 OS: Any Status: New Severity: Affects Only Me Priority: --- Component: bin Assignee: bugs@FreeBSD.org Reporter: kernel-error@kernel-error.com ## Description On amd64, `do_posix_spawn()` runs the spawn child via `rfork_thread(RFSPAWN, stack + stacksz, _posix_spawn_thr, &psa)` on a small `malloc`'d buffer (`lib/libc/gen/posix_spawn.c`): - `_RFORK_THREAD_STACK_SIZE` = 4096 (`:240`) - for a PATH-searched (relative) command, `stacksz = 4096 + MAX(3, argc+2)*sizeof(char*)`, 16-byte aligned (`:285`, `:295-298`) — e.g. 4128 bytes for `argv = {"true", NULL}` - `stack = malloc(stacksz)` (`:308`); the child is run on it (`:341`) - with `use_env_path` set, the child calls `__libc_execvpe()` (`:264`), i.e. the PATH search `RFSPAWN`/`rfork_thread` gives the child a **shared address space** (vfork semantics) until it execs. In a process that has **many shared objects loaded**, the spawn child's stack usage while running `__libc_execvpe()` exceeds this ~4 KB buffer and **underflows it**, scribbling over whatever heap sits below the buffer in the shared parent address space. The caller then crashes or misbehaves later, far from `posix_spawn`. This is **distinct from CVE-2020-7458 / SA-20:18** (that was a *long $PATH* overflow, fixed in 2020). The problem here is **independent of $PATH length** — it reproduces with a single PATH entry — and is driven by how much stack `__libc_execvpe` uses in a heavily-linked process, against a fixed ~4 KB child stack. ## Real-world impact (PHP) — likely the cause behind PR 277888 PHP's `proc_open()` with an array (argv) command calls `posix_spawnp()` with a relative program name. On FreeBSD with a normal set of PHP extensions loaded: ``` pkg install php84 <plus a typical set of extensions: dom intl pdo pdo_pgsql pgsql session simplexml sodium xml xmlwriter zip imagick memcached redis ...> php -r 'proc_open(["true"], [], $p);' # → Segmentation fault (~100%) ``` The crash itself surfaces much later, at PHP module shutdown (in `_efree`/ `zend_interned_strings_dtor`), because the underflow had overwritten the header of a permanent interned string back during the `proc_open` call. Tellingly, it depends on the spawn path, not on PHP logic: | call | spawn path | crash | |---|---|---| | `proc_open(["true"], …)` (relative) | `posix_spawnp` → `__libc_execvpe` (PATH search) | yes | | `proc_open(["/usr/bin/true"], …)` (absolute) | `posix_spawnp` → `execvPe` direct branch | no | | `proc_open("true", …)` (string) | `posix_spawn` (`/bin/sh -c`) → `_execve` | no | This very likely explains the `proc_open(["date"], $p)` reproducer discussed in **PR 277888** ("lang/php83 and lang/php84: segmentation fault on unloading modules"). That PR was closed FIXED via an `ext/xsl` change, yet its own comments report that the `proc_open(["date"], $p)` repro still segfaults on 14.2-RELEASE-p4 / php 8.4.10 / php 8.3.21 — i.e. that part appears mis-attributed; the cause looks to be here in libc. (Upstream PHP tracking: php/php-src#21995.) ## Evidence that it is the spawn child stack Interpose `rfork_thread()` via `LD_PRELOAD` and give the spawn child a different stack (everything else in libc unchanged; PHP heap layout unchanged because libc still does its own `malloc(stacksz)`): - child stack = **1 MB** → the PHP crash disappears: **0/30** vs **30/30** baseline. - child stack = **~4 KB with a PROT_NONE guard page just below it** → the spawn child dies with **SIGSEGV** (it grows down into the guard page). Because the guard sits immediately below the child stack, this is strong, **location-independent** evidence that the child exhausts its stack downward on this path (it rules out the "maybe it only mattered what was adjacent below the stack" confound). It does not by itself pin down which frame overflows (`execvPe` / `_execve` / run-time-linker work / the `rfork_thread` trampoline). Minimal interposer (essence): ```c pid_t rfork_thread(int flags, void *stack, int (*fn)(void*), void *arg) { long pg = sysconf(_SC_PAGESIZE); size_t sz = GUARDSPAWN_KB*1024; /* 1024 -> fixes it; 4 -> child SIGSEGV */ char *b = mmap(0, pg+sz, PROT_READ|PROT_WRITE, MAP_ANON|MAP_PRIVATE, -1, 0); mprotect(b, pg, PROT_NONE); /* guard page below the child stack */ return real_rfork_thread(flags, b+pg+sz, fn, arg); } ``` Note: a *minimal* standalone program calling `posix_spawnp("true", …)` does **not** underflow the 4 KB stack — the excess usage only appears once enough shared objects are loaded (so it bites real applications like PHP, not toy reproducers). I could not pin down *why*; a plausible (unproven) hypothesis is run-time-linker work on the child's small stack in the shared address space. The recent commit 4daf2d3e7db5 ("posix_spawn: use rfork_thread on all arches", main, 2026-01) does not change `_RFORK_THREAD_STACK_SIZE`; it uses `rfork_thread()` on all arches but the ~4 KB custom child stack remains the x86 path (other arches set `stack = NULL; stacksz = 0`). So current `main` still has this on x86/amd64. ## Suggested fix (maintainers' call) Size the spawn child stack adequately for `__libc_execvpe` under realistic load (the `stacksz` reservation currently only accounts for the ENOEXEC `alloca`), and/or place a guard page below it so an underflow faults deterministically instead of silently corrupting the caller's heap. --- *Disclosure: this analysis was produced with LLM assistance. All stacks, traces, repro counts, source references and the interposer experiment are from real runs on FreeBSD 15.0-RELEASE amd64; the interpretation is the part worth double-checking.* -- You are receiving this mail because: You are the assignee for the bug.home | help
Want to link to this message? Use this
URL: <https://mail-archive.FreeBSD.org/cgi/mid.cgi?bug-295991-227>
