Skip to content

Fix GH-23628: Tracing JIT reads undefined property slots of lazy proxies - #1

Draft
lisachenko wants to merge 1 commit into
masterfrom
claude/php-8.5-issue-23628-rh2179
Draft

Fix GH-23628: Tracing JIT reads undefined property slots of lazy proxies#1
lisachenko wants to merge 1 commit into
masterfrom
claude/php-8.5-issue-23628-rh2179

Conversation

@lisachenko

Copy link
Copy Markdown
Owner

Fixes php#23628 (see also goaop/framework bug report).

Note on the base branch: this branch is based on upstream PHP-8.5 (b9c64e21), as the fix targets PHP 8.5. This fork has no PHP-8.5 branch, so the PR is opened against master and the "Files changed" view also shows unrelated master vs PHP-8.5 differences. The actual change is the single commit 6270c83f (git diff b9c64e21..claude/php-8.5-issue-23628-rh2179). To get a clean view, push upstream PHP-8.5 to this fork and retarget the PR, or open the upstream PR against php:PHP-8.5.

Problem

A lazy proxy keeps its own property slots IS_UNDEF | IS_PROP_LAZY even after it has been initialized; the object handlers forward each property access to the real instance. The tracing JIT was not aware of this:

  1. ext/opcache/jit/zend_jit_ir.c, zend_jit_fetch_obj() – when a FETCH_OBJ_R/IS/W on a known property was recorded with an IS_UNDEF slot (val(undef) in the trace), the known-offset fast path was compiled anyway. For a lazy proxy that path never succeeds and deoptimized on every execution. The fix uses the generic code path (run-time cache + read_property/get_property_ptr_ptr fallback for undefined slots) when the slot was undefined at recording time. This also covers uninitialized/unset properties, and it mirrors what zend_jit_assign_obj() already does for prop_type == IS_UNDEF.

  2. ext/opcache/jit/zend_jit_trace.c, zend_jit_trace_exit() – when a result type guard after FETCH_OBJ_IS failed on an IS_UNDEF slot, deoptimization turned the result into NULL, assuming an undefined property. That is what made isset($this->map['start']['next']) return false starting at the iteration where the trace kicked in. For a slot flagged IS_PROP_LAZY the opline is now re-executed in the VM (same mechanism already used for FETCH_OBJ_R), so the read is forwarded to the real instance. Plain undefined properties keep the previous NULL shortcut.

Tests

  • ext/opcache/tests/jit/gh23628_001.phpt – proxy from the start (isset on nested dim, ??, plain read, write, ++) and a lazy ghost. Exercises fix 1.
  • ext/opcache/tests/jit/gh23628_002.phpt – trace recorded on a regular object, then executed on a proxy. Exercises fix 2 (deoptimization path).

Both tests fail on the unpatched build (isset false at N) and pass with the patch.

Verified locally (debug build, x86_64):

  • ext/opcache/tests/jit + Zend/tests/lazy_objects: 708 passed, 0 failed.
  • Zend/tests, ext/opcache/tests, ext/reflection/tests with opcache.jit=tracing forced and jit_hot_loop/func/return/side_exit=1: 6588 passed, 0 failed.
  • The issue's reproducer prints 1000 under tracing JIT, and the compiled trace no longer contains a deoptimizing exit on the property fetch.

🤖 Generated with Claude Code

https://claude.ai/code/session_01ECekMqERF8jnqBxo1cXe3V


Generated by Claude Code

…roxies

A lazy proxy keeps its own property slots IS_UNDEF|IS_PROP_LAZY even after
it has been initialized, and the object handlers forward every property
access to the real instance. The tracing JIT was not aware of this in two
places:

1. When the recorded trace contained a FETCH_OBJ_R/IS/W on a known property
   whose slot was IS_UNDEF, the known-offset fast path was still compiled.
   For a lazy proxy this path never succeeds, and it deoptimized on every
   execution. Use the generic code path (that falls back to the object
   handlers for undefined slots) when the slot was IS_UNDEF at recording
   time. This also covers uninitialized and unset properties.

2. During deoptimization of a failed result type guard after FETCH_OBJ_IS,
   an IS_UNDEF slot was turned into NULL, assuming an undefined property.
   For a slot flagged IS_PROP_LAZY the fetch has to be forwarded to the
   real instance instead, so re-execute the opline in the VM, the same way
   it is already done for FETCH_OBJ_R.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ECekMqERF8jnqBxo1cXe3V

Copy link
Copy Markdown
Owner Author

CI status on 6270c83f: Suite / MACOS_ARM64_DEBUG_NTS is red, with a single failing test out of 20797 run:

sapi/cli/tests/php_cli_server_expect_100_continue_iua.phpt
001- bool(true)
001+ bool(false)

This failure is not this PR's:

  • The test exercises the built-in CLI web server's handling of Expect: 100-continue when the client resets the connection with SO_LINGER (l_linger=0) and ignore_user_abort=1, and waits up to 5 s for an Invalid request log line. It depends on socket reset timing on the macOS runner, not on anything the diff touches (the diff only changes ext/opcache/jit/zend_jit_ir.c, ext/opcache/jit/zend_jit_trace.c and adds two JIT tests).
  • The test exists only on master (added in c96585e, "Protect the cached chunk list against corruption"); it is not part of PHP-8.5 at all. CI runs on the merge ref with master, which is how it got executed here.
  • All opcache/JIT tests, including the two new gh23628_* tests, passed in that job; the same suite is green on the other platforms that have finished so far (Linux x32 debug, FreeBSD, benchmarking).

No fix exists to port for that test. I will re-run the failed job once the workflow run has finished (a re-run is refused while the other suites are still running).


Generated by Claude Code

Copy link
Copy Markdown
Owner Author

Re-run result for Suite / MACOS_ARM64_DEBUG_NTS on 6270c83f: the previously failing php_cli_server_expect_100_continue_iua.phpt passed this time. The job is still red because a sibling test from the same master-only commit (c96585e) failed instead:

sapi/cli/tests/php_cli_server_expect_100_continue_socket.phpt

It exercises the same built-in web server Expect: 100-continue handling, with a 1 ms stream_select() timeout and an SO_LINGER reset, and it does not exist on PHP-8.5 either. Together with gh23425.phpt (another CLI server test, which passed only on its automatic retry in the same job) this points to socket timing on the macOS runner, not to the JIT change. All Linux (x64 release, x32 debug, x64 ASAN, Alpine ASAN), Windows and FreeBSD suites are green on this commit.

I'm not spending another re-run on it; the failure is outside this PR's scope and cannot be made robust from a PHP-8.5 based branch.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[PHP8.5] [JIT] Tracing JIT wrong optimization for LazyProxy instances

1 participant