Skip to content

Fix GH-22857: virtual property hook mis-primes SIMPLE_GET under FUNC_ARG#22867

Open
zhaohao19941221 wants to merge 4 commits into
php:masterfrom
zhaohao19941221:fix-22857-JIT-virtual-property-hook-as-arg-to-unqualified-namespaced-fallback-coderzhao-2026-07-22
Open

Fix GH-22857: virtual property hook mis-primes SIMPLE_GET under FUNC_ARG#22867
zhaohao19941221 wants to merge 4 commits into
php:masterfrom
zhaohao19941221:fix-22857-JIT-virtual-property-hook-as-arg-to-unqualified-namespaced-fallback-coderzhao-2026-07-22

Conversation

@zhaohao19941221

Copy link
Copy Markdown

Summary

Fixes GH-22857: heap corruption / spurious ValueErrors / TypeErrors when a virtual property hook is read via FETCH_OBJ_FUNC_ARG under function-mode JIT (opcache.jit=1205).

Root cause

zend_std_read_property() primed the SIMPLE_GET property-hook cache-slot bit unconditionally after every successful hook invocation, ignoring which opcode had triggered the read.

ZEND_FETCH_OBJ_FUNC_ARG dispatches into the ZEND_FETCH_OBJ_R handler for by-value arguments via ZEND_VM_TAIL_CALL, keeping EX(opline) on the FUNC_ARG opcode. When the cache-slot bit was cached under such a mismatched opline, the next read went through the SIMPLE_GET fast path in zend_vm_def.h, which pushes a hook call frame and returns opline | ZEND_VM_ENTER_BIT to signal the caller to re-enter the VM.

Function-mode JIT handles this only for FETCH_OBJ_R (via the specialised hook-enter guard in zend_jit.c) — for FETCH_OBJ_FUNC_ARG the generic zend_jit_handler path is used, which has no such guard. The JIT-compiled SEND_FUNC_ARG therefore 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 bytes
  • TypeError: <fn>(): Argument #N ($arg) must be of type ?string, <adjacent-class> given
  • zend_mm_heap corrupted (SIGABRT)

All three have the same root cause.

Fix

Only prime SIMPLE_GET when the currently-executing opline is a plain ZEND_FETCH_OBJ_R. This mirrors the guard already used for SIMPLE_READ a few lines above in the same function.

Reproducer (before the fix)

$ php -d opcache.enable_cli=1 -d opcache.jit_buffer_size=64M \
      -d opcache.jit=1205 Zend/tests/property_hooks/virtual_hook_as_func_arg.phpt

was 20/20 crashing; with -d opcache.jit=disable or -d opcache.jit=tracing was 20/20 clean. After the fix, both modes are clean.

The reproducer needs all of:

  1. opcache.jit=1205 (function JIT).
  2. Class inside a namespace.
  3. Call is unqualified (no leading \, no use function) so that INIT_NS_FCALL_BY_NAME + FETCH_OBJ_FUNC_ARG is emitted.
  4. Argument is a virtual property hook (get-only, no backing storage).
  5. Hook passed directly — assigning to a local first uses FETCH_OBJ_R and hides the bug.
  6. Call wrapped in @ (BEGIN_SILENCE / END_SILENCE).
  7. Class layout with adjacent asymmetric-visibility fields and two promoted asymmetric-visibility constructor parameters.
  8. Hook 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 under jit=1205), passes after.

Backport

Bug is present since 8.4 (property hooks introduced in 8.4). Targeting PHP-8.4 so it lands in 8.4/8.5/master via the normal merge chain.

coderzhao 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)
…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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Function JIT emits wrong code for FETCH_OBJ_FUNC_ARG on a virtual property hook when calling an unqualified namespaced-fallback function

1 participant