-
Notifications
You must be signed in to change notification settings - Fork 8.1k
Fix GH-22857: JIT wrong code for FETCH_OBJ_FUNC_ARG with property hooks #22897
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
zhaohao19941221
wants to merge
14
commits into
php:master
Choose a base branch
from
zhaohao19941221:fix/gh-22857-jit-fetch-obj-func-arg-hook
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+276
−0
Open
Changes from all commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
4add960
Fix GH-22857: JIT wrong code for FETCH_OBJ_FUNC_ARG with property hooks
1720279
Merge branch 'master' into fix/gh-22857-jit-fetch-obj-func-arg-hook
287ce80
Fix gh22857.phpt: avoid unrelated pre-existing JIT leak in the warm-u…
a447b39
Update ext/opcache/tests/jit/gh22857.phpt
zhaohao19941221 05c5cc3
Update ext/opcache/tests/jit/gh22857.phpt
zhaohao19941221 5c4816c
Merge branch 'master' into fix/gh-22857-jit-fetch-obj-func-arg-hook
10821a1
fix: Improve gh22857.phpt: use deterministic assertion for FETCH_OBJ_…
d753c5b
Merge branch 'master' into fix/gh-22857-jit-fetch-obj-func-arg-hook
b4103bb
Fix GH-22857: JIT FETCH_OBJ_FUNC_ARG property-hook getter frame guard
971f3ac
Fix GH-22857: JIT FETCH_OBJ_FUNC_ARG property-hook getter frame guard
3f08ec8
Merge branch 'fix/gh-22857-jit-fetch-obj-func-arg-hook' of github.com…
248422c
Fix JIT IR assertion failure in FETCH_OBJ_FUNC_ARG runtime by-ref branch
8641e92
Fix JIT IR assertion failure in FETCH_OBJ_FUNC_ARG runtime by-ref branch
a4e1dc0
Merge branch 'fix/gh-22857-jit-fetch-obj-func-arg-hook' of github.com…
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2447,6 +2447,106 @@ static int zend_jit(const zend_op_array *op_array, zend_ssa *ssa, const zend_op | |
| goto jit_failure; | ||
| } | ||
| goto done; | ||
| case ZEND_FETCH_OBJ_FUNC_ARG: | ||
| { | ||
| ce = NULL; | ||
| ce_is_instanceof = false; | ||
| on_this = false; | ||
| if (opline->op1_type == IS_UNUSED) { | ||
| op1_info = MAY_BE_OBJECT|MAY_BE_RC1|MAY_BE_RCN; | ||
| op1_addr = 0; | ||
| ce = op_array->scope; | ||
| /* scope is NULL for closures. */ | ||
| if (ce) { | ||
| ce_is_instanceof = !(ce->ce_flags & ZEND_ACC_FINAL); | ||
| } | ||
| on_this = true; | ||
| } else { | ||
| op1_info = OP1_INFO(); | ||
| if (!(op1_info & MAY_BE_OBJECT)) { | ||
| break; | ||
| } | ||
| op1_addr = OP1_REG_ADDR(); | ||
| if (ssa->var_info && ssa->ops) { | ||
| zend_ssa_op *ssa_op = &ssa->ops[opline - op_array->opcodes]; | ||
| if (ssa_op->op1_use >= 0) { | ||
| zend_ssa_var_info *op1_ssa = ssa->var_info + ssa_op->op1_use; | ||
| if (op1_ssa->ce && !op1_ssa->ce->create_object) { | ||
| ce = op1_ssa->ce; | ||
| ce_is_instanceof = op1_ssa->is_instanceof; | ||
| } | ||
| } | ||
| } | ||
| } | ||
| if (opline->op2_type != IS_CONST | ||
| || Z_TYPE_P(RT_CONSTANT(opline, opline->op2)) != IS_STRING | ||
| || Z_STRVAL_P(RT_CONSTANT(opline, opline->op2))[0] == '\0') { | ||
| break; | ||
| } | ||
|
|
||
| /* FETCH_OBJ_FUNC_ARG's by-value fetch dispatches into the | ||
| * FETCH_OBJ_R handler, which may take the SIMPLE_GET hook fast | ||
| * path and push a getter frame; by-ref dispatches into | ||
| * FETCH_OBJ_W. The function JIT may keep values solely in | ||
| * registers, so we must NOT exit to the VM (stale stack slots). | ||
| * Inline the by-value path through zend_jit_fetch_obj, which runs | ||
| * the hook getter inside a helper and keeps all registers live. | ||
| * The by-ref path has no SIMPLE_GET fast path, so the generic | ||
| * handler (a full C call, safe under register allocation) is used. | ||
| * This mirrors the tracing JIT fix for GH-21006 (GH-21369); the | ||
| * runtime by-ref check is required because the passing mode is | ||
| * only known once the callee is resolved via namespace fallback. | ||
| * See GH-22857. */ | ||
| { | ||
| ir_ref rx, call_info, if_by_ref, end_by_ref; | ||
|
|
||
| /* Both runtime paths must observe a consistent frame | ||
| * state. The delayed call chain would otherwise only | ||
| * be flushed inside the by-ref branch (by | ||
| * zend_jit_set_ip() in zend_jit_handler()), leaving | ||
| * EX(call) stale on the by-val path and after the | ||
| * merge. Flush it before branching. */ | ||
| if (jit->delayed_call_level) { | ||
| if (!zend_jit_save_call_chain(jit, jit->delayed_call_level)) { | ||
| goto jit_failure; | ||
| } | ||
| } | ||
|
|
||
| // JIT: if (ZEND_CALL_INFO(EX(call)) & ZEND_CALL_SEND_ARG_BY_REF) | ||
| if (jit->reuse_ip) { | ||
| rx = jit_IP(jit); | ||
| } else { | ||
| rx = ir_LOAD_A(jit_EX(call)); | ||
| } | ||
| call_info = ir_LOAD_U32(jit_CALL(rx, This.u1.type_info)); | ||
| if_by_ref = ir_IF(ir_AND_U32(call_info, ir_CONST_U32(ZEND_CALL_SEND_ARG_BY_REF))); | ||
|
|
||
| /* by-ref path: the FUNC_ARG handler re-checks the flag | ||
| * and dispatches into FETCH_OBJ_W */ | ||
| ir_IF_TRUE_cold(if_by_ref); | ||
| if (!zend_jit_handler(&ctx, opline, | ||
| zend_may_throw(opline, ssa_op, op_array, ssa))) { | ||
| goto jit_failure; | ||
| } | ||
| end_by_ref = ir_END(); | ||
|
|
||
| /* zend_jit_handler() stored IP = opline + 1 on the | ||
| * by-ref path only; that compile-time knowledge is | ||
| * invalid for the by-val path and after the merge. */ | ||
| zend_jit_reset_last_valid_opline(jit); | ||
|
|
||
| /* by-val path */ | ||
| ir_IF_FALSE(if_by_ref); | ||
| if (!zend_jit_fetch_obj(&ctx, opline, op_array, ssa, ssa_op, | ||
| op1_info, op1_addr, 0, ce, ce_is_instanceof, on_this, 0, 0, NULL, | ||
| RES_REG_ADDR(), IS_UNKNOWN, | ||
| zend_may_throw(opline, ssa_op, op_array, ssa))) { | ||
| goto jit_failure; | ||
| } | ||
| ir_MERGE_WITH(end_by_ref); | ||
| } | ||
|
Comment on lines
+2487
to
+2547
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This looks good, but please extract this part to a new function in |
||
| goto done; | ||
| } | ||
| case ZEND_FETCH_OBJ_R: | ||
| case ZEND_FETCH_OBJ_IS: | ||
| case ZEND_FETCH_OBJ_W: | ||
|
|
@@ -2854,6 +2954,7 @@ static int zend_jit(const zend_op_array *op_array, zend_ssa *ssa, const zend_op | |
| call_level--; | ||
| } | ||
| break; | ||
| case ZEND_FETCH_OBJ_FUNC_ARG: | ||
| case ZEND_FETCH_OBJ_R: | ||
| if (!zend_jit_handler(&ctx, opline, | ||
| zend_may_throw(opline, ssa_op, op_array, ssa))) { | ||
|
|
||
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,125 @@ | ||
| --TEST-- | ||
| GH-22857: Function JIT emits wrong code for FETCH_OBJ_FUNC_ARG on a property hook (SIMPLE_GET fast path) | ||
| --INI-- | ||
| opcache.enable=1 | ||
| opcache.enable_cli=1 | ||
| opcache.jit_buffer_size=64M | ||
| opcache.jit=1205 | ||
| opcache.jit_hot_func=1 | ||
| --EXTENSIONS-- | ||
| opcache | ||
| --FILE-- | ||
| <?php | ||
| namespace Test; | ||
|
|
||
| interface HandlerInterface { public function noop(): void; } | ||
|
|
||
| final class DefaultHandler implements HandlerInterface { | ||
| private static ?self $i = null; | ||
| public static function getInstance(): self { return self::$i ??= new self(); } | ||
| public function noop(): void {} | ||
| } | ||
|
|
||
| /* Original issue: virtual property hook read via FETCH_OBJ_FUNC_ARG under | ||
| * function JIT. The getter frame is pushed by the SIMPLE_GET fast path in the | ||
| * shared FETCH_OBJ_R handler, but the JIT-compiled FUNC_ARG opcode had no | ||
| * hook-enter guard, so the argument slot was read before the getter ran. | ||
| * | ||
| * The assertion is deterministic: the getter sets a static flag as a side | ||
| * effect, so we check whether the getter actually ran when the property is | ||
| * passed through FETCH_OBJ_FUNC_ARG. This avoids the previous data-dependent | ||
| * failure mode (relying on file_get_contents() fataling on whatever garbage | ||
| * the unguarded JIT read happened to land on), which made the regression | ||
| * test flaky. */ | ||
| class Container { | ||
| private static bool $getterRan = false; | ||
|
|
||
| public protected(set) HandlerInterface $handler; | ||
|
|
||
| public string $path { | ||
| get => (self::$getterRan = true) | ||
| ? self::build($this->kind, $this->id) | ||
| : self::build($this->kind, $this->id); | ||
| } | ||
|
|
||
| protected mixed $prev = null; | ||
|
|
||
| public function __construct( | ||
| public protected(set) string $kind, | ||
| public protected(set) string $id, | ||
| ) { | ||
| $this->handler = DefaultHandler::getInstance(); | ||
| } | ||
|
|
||
| public static function build(string $k, string $i): string { | ||
| return "/nonexistent/gh22857_{$k}_{$i}.dat"; | ||
| } | ||
|
|
||
| public function step(): void { | ||
| /* Unqualified namespaced-fallback call (INIT_NS_FCALL_BY_NAME) keeps | ||
| * the FETCH_OBJ_FUNC_ARG opcode instead of letting the optimizer | ||
| * rewrite it to FETCH_OBJ_R. @ preserves the opcode shape that | ||
| * triggers the bug. */ | ||
| @file_get_contents($this->path); | ||
| if (!self::$getterRan) { | ||
| throw new \RuntimeException('getter did not run via FUNC_ARG'); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| $c = new Container('alpha', 'beta'); | ||
| $c->step(); | ||
| $c->step(); | ||
| $c->step(); | ||
|
|
||
| /* Sibling-slot variant: a preceding plain FETCH_OBJ_R primes the | ||
| * SIMPLE_GET bit on the property cache slot; compact_literals shares the | ||
| * slot between FETCH_OBJ_R and FETCH_OBJ_FUNC_ARG for the same property, | ||
| * so the following FETCH_OBJ_FUNC_ARG consumes that bit and hits the | ||
| * SIMPLE_GET fast path. Without the hook-enter guard it passes whatever | ||
| * sits in an adjacent property slot instead of running the getter. The | ||
| * flag is reset after the priming FETCH_OBJ_R so the assertion reflects | ||
| * only whether the getter ran during the FETCH_OBJ_FUNC_ARG read. */ | ||
| class Container2 { | ||
| private static bool $getterRan = false; | ||
|
|
||
| public protected(set) HandlerInterface $handler; | ||
|
|
||
| public string $path { | ||
| get => (self::$getterRan = true) | ||
| ? self::build($this->kind, $this->id) | ||
| : self::build($this->kind, $this->id); | ||
| } | ||
|
|
||
| protected mixed $prev = null; | ||
|
|
||
| public function __construct( | ||
| public protected(set) string $kind, | ||
| public protected(set) string $id, | ||
| ) { | ||
| $this->handler = DefaultHandler::getInstance(); | ||
| } | ||
|
|
||
| public static function build(string $k, string $i): string { | ||
| return "/nonexistent/gh22857b_{$k}_{$i}.dat"; | ||
| } | ||
|
|
||
| public function step(): void { | ||
| $this->prev = $this->path; // FETCH_OBJ_R primes SIMPLE_GET on shared slot | ||
| self::$getterRan = false; // reset; flag now reflects only the FUNC_ARG read below | ||
| @file_get_contents($this->path); // FETCH_OBJ_FUNC_ARG consumes the primed bit | ||
| if (!self::$getterRan) { | ||
| throw new \RuntimeException('sibling-slot variant: getter did not run via FUNC_ARG'); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| $c2 = new Container2('alpha', 'beta'); | ||
| $c2->step(); | ||
| $c2->step(); | ||
| $c2->step(); | ||
|
|
||
| echo "OK\n"; | ||
| ?> | ||
| --EXPECT-- | ||
| OK |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| --TEST-- | ||
| GH-22857 (reg-alloc): FETCH_OBJ_FUNC_ARG hook read must not lose register-held vars | ||
| --ENV-- | ||
| A=1 | ||
| --INI-- | ||
| opcache.enable=1 | ||
| opcache.enable_cli=1 | ||
| opcache.jit_buffer_size=64M | ||
| opcache.jit=1205 | ||
| opcache.jit_hot_func=1 | ||
| --FILE-- | ||
| <?php | ||
|
|
||
| class C { | ||
| public $prop { | ||
| get { return 1; } | ||
| } | ||
| } | ||
|
|
||
| function f(int $a0, $obj) { | ||
| // $a lives only in a register: every use is in a supported opcode and in a | ||
| // non-entry basic block, so the register allocator never spills it to the | ||
| // VM stack. Exiting to the VM (the buggy hook-enter guard) would read a | ||
| // stale stack slot for $a. | ||
| $a = $a0; | ||
| $b = $a + 2; | ||
| $c = g($obj->prop, $a ? 1 : 2); | ||
| return $b + $c; | ||
| } | ||
|
|
||
| if (getenv('A')) { | ||
| function g($a, $b) { | ||
| var_dump($b); | ||
| return $a; | ||
| } | ||
| } | ||
|
|
||
| $c = new C(); | ||
| var_dump(f(1, $c)); | ||
| var_dump(f(1, $c)); | ||
|
|
||
| ?> | ||
| --EXPECT-- | ||
| int(1) | ||
| int(4) | ||
| int(1) | ||
| int(4) |
Oops, something went wrong.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be worth it to merge this
caseblock with the one below to deduplicate this part