Fix GH-22857: virtual property hook mis-primes SIMPLE_GET under FUNC_ARG#22867
Conversation
cache-slot bit while the currently-executing opline was anything
other than a plain ZEND_FETCH_OBJ_R (in particular
ZEND_FETCH_OBJ_FUNC_ARG, which dispatches into the same handler for
by-value argument fetches). Once primed, subsequent hook reads went
through the SIMPLE_GET fast path with a mismatched opline and read
garbage from an adjacent property slot, typically surfacing as a
ValueError "must not contain any null bytes", a TypeError
referencing the neighbouring slot's class, or a heap-corruption
abort. Mirrors the opline check already used for SIMPLE_READ.
(coderzhao)
… SIMPLE_GET (function JIT)
…r FUNC_ARG zend_std_read_property() primed the SIMPLE_GET property-hook cache-slot bit after every successful hook invocation, regardless of which opcode triggered the read. When the caller was ZEND_FETCH_OBJ_FUNC_ARG (which dispatches into the FETCH_OBJ_R handler for by-value argument fetches via ZEND_VM_TAIL_CALL, keeping EX(opline) pointing at the FUNC_ARG opcode), a subsequent hook read of the same slot would take the SIMPLE_GET fast path in zend_vm_def.h. That fast path pushes a hook call frame and returns opline | ZEND_VM_ENTER_BIT, expecting the caller to re-enter the VM to run the hook. Function-mode JIT dispatches FETCH_OBJ_R via a specialised path in zend_jit.c that emits an "if IP != opline+1, exit to VM" guard, but FETCH_OBJ_FUNC_ARG falls into the generic zend_jit_handler path which has no such guard. Once the cache slot is primed under a FUNC_ARG opline, the JIT-compiled FUNC_ARG code continues straight into the JIT-compiled SEND_FUNC_ARG before the hook has actually run. The pending argument slot then holds an adjacent property's raw bytes, typically surfacing as one of: * ValueError: file_get_contents(): Argument php#1 ($filename) must not contain any null bytes * TypeError: expected string, <adjacent class> given * zend_mm_heap corrupted (SIGABRT) All three symptoms have the same root cause. Restrict priming of SIMPLE_GET to a plain ZEND_FETCH_OBJ_R opline, mirroring the guard already used for SIMPLE_READ a few lines above. Closes phpGH-22857.
…ed-namespaced-fallback-coderzhao-2026-07-22' of github.com:zhaohao19941221/php-src into fix-22857-JIT-virtual-property-hook-as-arg-to-unqualified-namespaced-fallback-coderzhao-2026-07-22
|
The issue is similar to GH-21006 (but for the function JIT) which was fixed by a JIT-only change in GH-21369: I believe that this could be fixed in JIT only as well: We could compile
wdyt @zhaohao19941221 @iliaal? |
|
Agree, this should be JIT-only. The interpreter is already correct: for a by-value arg, The interpreter patch here also doesn't fully close it. public function step(): void {
$warm = $this->path; // FETCH_OBJ_R primes SIMPLE_GET on the shared slot
@file_get_contents($this->path); // FETCH_OBJ_FUNC_ARG consumes it
}Built on this branch with Your split is what the VM already does: |
Summary
Fixes GH-22857: heap corruption / spurious
ValueErrors /TypeErrors when a virtual property hook is read viaFETCH_OBJ_FUNC_ARGunder function-mode JIT (opcache.jit=1205).Root cause
zend_std_read_property()primed theSIMPLE_GETproperty-hook cache-slot bit unconditionally after every successful hook invocation, ignoring which opcode had triggered the read.ZEND_FETCH_OBJ_FUNC_ARGdispatches into theZEND_FETCH_OBJ_Rhandler for by-value arguments viaZEND_VM_TAIL_CALL, keepingEX(opline)on the FUNC_ARG opcode. When the cache-slot bit was cached under such a mismatched opline, the next read went through theSIMPLE_GETfast path inzend_vm_def.h, which pushes a hook call frame and returnsopline | ZEND_VM_ENTER_BITto signal the caller to re-enter the VM.Function-mode JIT handles this only for
FETCH_OBJ_R(via the specialised hook-enter guard inzend_jit.c) — forFETCH_OBJ_FUNC_ARGthe genericzend_jit_handlerpath is used, which has no such guard. The JIT-compiledSEND_FUNC_ARGtherefore reads from the argument slot before the hook has actually run.Depending on the adjacent property slot's contents, symptoms are:
ValueError: file_get_contents(): Argument #1 ($filename) must not contain any null bytesTypeError: <fn>(): Argument #N ($arg) must be of type ?string, <adjacent-class> givenzend_mm_heap corrupted(SIGABRT)All three have the same root cause.
Fix
Only prime
SIMPLE_GETwhen the currently-executing opline is a plainZEND_FETCH_OBJ_R. This mirrors the guard already used forSIMPLE_READa few lines above in the same function.Reproducer (before the fix)
was 20/20 crashing; with
-d opcache.jit=disableor-d opcache.jit=tracingwas 20/20 clean. After the fix, both modes are clean.The reproducer needs all of:
opcache.jit=1205(function JIT).namespace.\, nouse function) so thatINIT_NS_FCALL_BY_NAME+FETCH_OBJ_FUNC_ARGis emitted.FETCH_OBJ_Rand hides the bug.@(BEGIN_SILENCE/END_SILENCE).get =>calls a static method reading the promoted asymmetric properties.Delta-debugging established that removing any one of these hides the bug.
Test
Zend/tests/property_hooks/virtual_hook_as_func_arg.phpt— 200 iterations exercising the exact eight-condition shape from the issue. Fails before the fix (20/20 underjit=1205), passes after.Backport
Bug is present since 8.4 (property hooks introduced in 8.4). Targeting
PHP-8.4so it lands in 8.4/8.5/master via the normal merge chain.