[php-src] Issue #24563: Allocation-failure call sites still on zend_error_noreturn(E_ERROR) on master (zend_strtod.c "Balloc() failed",
zend_multiply.h) — the EOL-7.4 minimal mitigation hardened these, master did not
| From: | srayuth089 | Date: | Thu, 22 Oct 2026 17:22:40 +0000 |
| Subject: | [php-src] Issue #24563: Allocation-failure call sites still on zend_error_noreturn(E_ERROR) on master (zend_strtod.c "Balloc() failed", zend_multiply.h) — the EOL-7.4 minimal mitigation hardened these, master did not |
||
| Groups: | php.bugs | ||
| Request: | Send a blank email to php-bugs+get-253107@lists.php.net to get a copy of this message | ||
Issue: https://github.com/php/php-src/issues/24563
Author: srayuth089
## Context
While studying the fixes for GHSA-w8hw-w9fx-j79w / GHSA-6wmf-4rv5-p3rp (thank you for the
delayed-OOM machinery in 3584a98bda — the design is excellent), we noticed a consistency
gap between the two fix branches:
- The EOL-7.4 minimal mitigation **712e60f9ed** converted several allocation-failure call
sites to the fail-fast
zend_mm_safe_error_ex() path.
- The full 8.x fix and current **master** kept several such call sites on
zend_error_noreturn(E_ERROR, ...), i.e. they still run the error machinery and bail
out via longjmp from the failing context — the exact pattern the advisory describes as
dangerous when the allocator is in a failing state.
This is an **informational hardening-consistency observation, not a vulnerability report** —
see the empirical section: we could not reach any of these paths from PHP userland under
the conditions we tested.
## Call sites in question (master, checked 2026-10-22)
| Location | Code | Note |
|---|---|---|
| Zend/zend_strtod.c ~590 / ~603 | zend_error_noreturn(E_ERROR, "Balloc()
failed to allocate memory") when MALLOC() (raw malloc,
#define MALLOC malloc at ~237) returns NULL | dtoa allocates Bigints outside Zend MM
entirely (also unaccounted by memory_limit) |
| Zend/zend_multiply.h ~343 / ~355 | zend_error_noreturn(E_ERROR, "Possible
integer overflow ...") | size-arithmetic checks; 7.4 converted both |
| ext/sodium/libsodium.c ~42 | zend_error_noreturn(E_ERROR, "Memory
allocation too large ...") | untouched in both branches |
## Empirical work (why we rate this informational, not exploitable)
We tried hard to reach the Balloc() path from userland before writing this up.
PHP 8.4.27, arm64 (docker), instrumentation at three levels — all evidence below is
reproducible and we are happy to share the tooling (a ~30-line LD_PRELOAD malloc
interposer, RLIMIT_AS sweep harness, phase-marker snippets):
1. **Phase markers** (STDERR markers around string-build vs conversion): across an
RLIMIT_AS sweep at **1 KB granularity**, every allocation failure occurred during
string *construction* (Zend MM chunk mmap), before zend_strtod was ever entered.
FAIL_IN_CONVERSION count: **0**. The transition was a cliff, not a window
(67247 KB → fail in build, 67248 KB → full success).
2. **strace** (mmap/brk/mremap bracketed by the same markers): **0** memory-mapping
syscalls during the conversion phase, both with comfortable VA and at the boundary.
3. **LD_PRELOAD malloc interposer** (libc-call level, which strace cannot see):
conversion performs a **constant 11 malloc calls / 428 requested bytes** regardless of
a **1024× input-length difference** (4,096 vs 4,194,304 mantissa digits). Controls:
marker-only run = 0 allocations; a low-level fwrite() marker variant reproduced the
same 11/428B figure, ruling out instrumentation overhead. Preliminary return-address
attribution places one call near zend_strtod+0xee4.
In other words: for the tested input domain, dtoa's memory need is bounded (digit
truncation before Bigint construction; freelist/private pool absorbs the rest), so
RLIMIT_AS pressure always surfaces in the string BUILD phase, where the new
delayed-OOM machinery handles it gracefully. We did **not** find a way to make the
libc allocator fail *inside* the conversion.
## Questions
1. Is leaving these on zend_error_noreturn intentional — i.e. does the delayed-OOM
layer on master also cover a libc-malloc failure deep inside dtoa, making the 7.4-style
conversion unnecessary there?
2. If it is an oversight, would a small PR converting the four call sites above to the
fail-fast pattern (mirroring 712e60f9ed) be welcome? Happy to send it.
3. Separately: is the fact that dtoa allocates via raw malloc() (outside
memory_limit
accounting) considered acceptable? Our measurements suggest per-conversion usage is
tiny and bounded, so this may be fine as-is — asking for completeness.
## Reproduction context
The interest in this area came from lab work on the php2shell chains (public PoC and a
public Metasploit module PR exist for CVE-2026-109618/109619). We validated the patched
build behavior end-to-end: on 8.4.27 the public chain dies at the heap-leak stage with
zero worker crashes, consistent with the fix cutting off stale-state reuse. No new
exploitation is reported or implied by this issue.