Fix GH-22857: virtual property hook mis-primes SIMPLE_GET under FUNC_ARG#22867
Open
zhaohao19941221 wants to merge 4 commits into
Conversation
added 4 commits
July 23, 2026 11:42
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.