Skip to content

Commit ff200c3

Browse files
author
coderzhao
committed
Fix Fix GH-22857: virtual property hook mis-primes SIMPLE_GET under 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 #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 GH-22857.
1 parent ff926db commit ff200c3

1 file changed

Lines changed: 29 additions & 27 deletions

File tree

Zend/tests/property_hooks/virtual_hook_as_func_arg.phpt

Lines changed: 29 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
--TEST--
2-
Virtual property hook read via FETCH_OBJ_FUNC_ARG must invoke the getter (function JIT)
2+
Virtual property hook read via FETCH_OBJ_FUNC_ARG must not prime SIMPLE_GET (function JIT)
33
--INI--
44
opcache.enable=1
55
opcache.enable_cli=1
@@ -40,36 +40,38 @@ class Container {
4040
return "/nonexistent/regression_{$k}_{$i}.dat";
4141
}
4242

43-
/* Passes a virtual property hook directly as the first argument of an
44-
* unqualified (namespace-fallback) global function. That fallback path
45-
* emits INIT_NS_FCALL_BY_NAME + CHECK_FUNC_ARG + FETCH_OBJ_FUNC_ARG,
46-
* and the FETCH_OBJ_FUNC_ARG handler tail-calls into the FETCH_OBJ_R
47-
* handler for by-value arguments.
43+
/* Reads a virtual property hook directly as arg #1 of an unqualified
44+
* (namespace-fallback) global builtin. The emitted opcode chain is
4845
*
49-
* zend_std_read_property() must not prime the SIMPLE_GET property-hook
50-
* cache-slot bit while the currently-executing opline is anything
51-
* other than a plain ZEND_FETCH_OBJ_R. If it does, subsequent hook
52-
* reads (potentially via a JIT-compiled fast path for FUNC_ARG that
53-
* has no hook-enter check) go through the SIMPLE_GET path with a
54-
* mismatched opline and read garbage from an adjacent property slot,
55-
* typically surfacing as ValueError "must not contain any null bytes"
56-
* or a TypeError referencing the neighbour's class.
46+
* INIT_NS_FCALL_BY_NAME "Regression\\file_get_contents"
47+
* CHECK_FUNC_ARG 1
48+
* FETCH_OBJ_FUNC_ARG THIS, "path" -> tmp
49+
* SEND_FUNC_ARG tmp, 1
50+
* DO_FCALL_BY_NAME
5751
*
58-
* Also exercises the same fetch under @-silencing (BEGIN_SILENCE /
59-
* END_SILENCE) which is the exact shape observed in the wild. */
52+
* FETCH_OBJ_FUNC_ARG dispatches into the FETCH_OBJ_R handler for
53+
* by-value arguments while keeping opline pointing at the FUNC_ARG
54+
* opcode. If zend_std_read_property() primes the SIMPLE_GET
55+
* property-hook cache-slot bit under such an opline, subsequent
56+
* FETCH_OBJ_FUNC_ARG dispatches take the SIMPLE_GET fast path in
57+
* zend_vm_def.h -- which pushes a hook call frame and returns with
58+
* ZEND_VM_ENTER_BIT set. JIT function mode dispatches FETCH_OBJ_FUNC_ARG
59+
* via the generic zend_jit_handler path in zend_jit.c which, unlike
60+
* the specialised FETCH_OBJ_R handler, has no "if hook entered, exit
61+
* to VM" guard and continues into JIT-compiled SEND_FUNC_ARG code
62+
* before the hook has actually run. The pending argument slot then
63+
* holds an adjacent property's raw bytes, typically surfacing as
64+
*
65+
* ValueError: file_get_contents(): Argument #1 ($filename) must
66+
* not contain any null bytes
67+
* TypeError: expected string, <adjacent class> given
68+
* zend_mm_heap corrupted (SIGABRT)
69+
*
70+
* All three symptoms have the same root cause. */
6071
public function step(): void {
61-
// First: a bare FETCH_OBJ_FUNC_ARG under a namespaced call.
62-
$len = strlen($this->path);
63-
if ($len === 0) {
64-
throw new \RuntimeException('empty path from hook');
65-
}
66-
// Second: a silenced namespaced call with the hook as arg #1.
67-
// file_get_contents() is a by-value string parameter so the
68-
// FETCH_OBJ_FUNC_ARG dispatches into the FETCH_OBJ_R handler.
72+
// Namespace-fallback, by-value string arg -> FETCH_OBJ_FUNC_ARG.
73+
// Wrapped in @ to match the real-world observed shape.
6974
$r = @file_get_contents($this->path);
70-
// The file does not exist, so a false return with a warning is
71-
// expected. The important thing is that no ValueError / TypeError
72-
// / heap corruption occurs while producing the argument.
7375
if ($r !== false) {
7476
throw new \RuntimeException('unexpected non-false return');
7577
}

0 commit comments

Comments
 (0)