|
1 | 1 | --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) |
3 | 3 | --INI-- |
4 | 4 | opcache.enable=1 |
5 | 5 | opcache.enable_cli=1 |
@@ -40,36 +40,38 @@ class Container { |
40 | 40 | return "/nonexistent/regression_{$k}_{$i}.dat"; |
41 | 41 | } |
42 | 42 |
|
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 |
48 | 45 | * |
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 |
57 | 51 | * |
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. */ |
60 | 71 | 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. |
69 | 74 | $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. |
73 | 75 | if ($r !== false) { |
74 | 76 | throw new \RuntimeException('unexpected non-false return'); |
75 | 77 | } |
|
0 commit comments