Skip to content

Tracing JIT: segfault on $f($arr[k]) when $f alternates between by-value and by-reference functions #24438

Closed

Description

@waroad

With the tracing JIT, a dynamic call $f($arr[k]) where $f alternates between a by-value and a by-reference function crashes.

<?php
function byval($a) { return (int)$a; }
function byref(&$a) { return (int)$a; }
$fns = ["byval", "byref"]; $arr = [];
for ($i = 0; $i < 120; $i++) { $f = $fns[$i % 2]; $f($arr[1]); $f($arr[0]); }
php -n -d opcache.enable=1 -d opcache.enable_cli=1 -d opcache.file_update_protection=0 -d opcache.jit_buffer_size=64M -d opcache.jit=tracing -d opcache.jit_hot_loop=1 -d opcache.jit_hot_func=1 -d opcache.jit_hot_return=1 -d opcache.jit_hot_side_exit=1 -d opcache.jit_max_polymorphic_calls=0 reduced.php

Debug build:

Warning: Undefined array key 1 in reduced.php on line 5

Warning: Undefined array key 0 in reduced.php on line 5
php: Zend/zend_operators.c:1016: zval_get_long_func: Assertion `0' failed.

Release build: the same two warnings, then SIGSEGV (exit 139).

With opcache.jit=off the script only prints the two warnings and exits normally, so I expected the same under the JIT. jit_max_polymorphic_calls=0 is not required: with the default value, a variant with five callees (four by-value, one by-ref) and a defined $arr = [5, 6] crashes the same way. The hot counters of 1 are needed to form the traces in a short script.

It reproduces at 9253b03 (latest upstream when checked): the debug build hits the assertion above, the release build segfaults (rc 139).

Looks like the megamorphic INIT_DYNAMIC_CALL (call->func unknown) makes zend_jit_trace.c skip the specialised FETCH_DIM_FUNC_ARG, but zend_jit_send_var (zend_jit_ir.c, ZEND_SEND_FUNC_ARG) still compiles the by-ref branch using the recorded type of the fetched value, which does not include MAY_BE_INDIRECT. ZEND_FETCH_OBJ_FUNC_ARG has a zend_jit_func_arg_by_ref_guard for this case and the DIM variant does not. This is from reading jit_debug trace output, not confirmed in a debugger. Open PR #18392 (reworks EX(call) handling in the call/send path) may be related; I did not test it, its diff does not apply on the latest commit.

I can send a patch if that helps.

Found with an LLM-based testing tool; I used Claude to reduce it and look for the cause.

Activity

  1. aasul commented on Oct 25, 2026

    @aasul
    Contributor

    i'd like to look into this

    i'll first try reproducing the crash on current PHP builds with tracing JIT enabled and compare the behavior with JIT disabled

    then i'll inspect the generated traces around FETCH_DIM_FUNC_ARG and ZEND_SEND_FUNC_ARG, particularly how the by-reference case handles MAY_BE_INDIRECT and whether the missing guard is actually responsible for the crash

    i'll also check whether #18392 affects the same path before attempting a fix

    have you already started working on a patch? if so i'd be happy to compare findings or help test it rather than duplicate the work

    i'll share anything useful i find here

  2. aasul commented on Oct 25, 2026

    @aasul
    Contributor

    quick update, i managed to reproduce this on php 8.5.13-dev with tracing jit enabled. the same input runs normally with jit disabled, but the tracing jit hits the zval_get_long_func() assertion.

    after looking through the trace output, i noticed that a side trace starts directly at SEND_FUNC_ARG with an indirect operand, but its tssa type mask is missing MAY_BE_INDIRECT. this seems to come from the temporary fallback in zend_jit_trace_build_tssa(), which doesn't include that bit when regular ssa information isn't available. zend_jit_send_ref() relies on it to emit the indirect dereference, so the mismatch looks like the actual issue rather than a missing dim guard.

    i've tested a one-line change that adds MAY_BE_INDIRECT to that fallback, along with a phpt that checks the by-value/by-reference behavior and the resulting array values. on the same php revision and debug build configuration, the original fails the new test with the assertion, while the patched version passes with the expected output. seven related jit tests also pass with jit both enabled and disabled.

    i'm checking the broader opcache jit tests now before opening a pr, but the results so far look promising.

  3. aasul commented on Oct 25, 2026

    @aasul
    Contributor

    another update, i've finished validating the candidate fix.

    the issue appears to come from the fallback type information used when a side trace starts with a live-in temporary. it doesn't include MAY_BE_INDIRECT, so when the trace resumes directly at SEND_FUNC_ARG, zend_jit_send_ref() can miss the indirect dereference.

    i added MAY_BE_INDIRECT to that fallback and wrote a phpt checking the actual by-value/by-reference behavior. i rebuilt both versions from the same php 8.5 revision and configuration. the original fails the test with the assertion, while the patched version passes with the expected array values. both pass with jit disabled.

    the full ext/opcache/tests/jit/ directory also passed on the patched build: 515 passes, no failures, and 10 skips due to missing extensions or platform requirements. the original reproducer now completes with exactly the same warnings as the interpreter, and additional checks for existing references and copy-on-write behavior also matched.

    i also confirmed the original issue reproduces on an existing php 8.4 debug build, although i haven't tested the patch on 8.4 yet.

    i haven't found any regressions in the tested configurations, so i'll prepare the pr for review.

  4. aasul commented on Oct 25, 2026

    @aasul
    Contributor

    i've opened a pr for this now: #24699

    the issue appears to be caused by the side-trace fallback type mask missing MAY_BE_INDIRECT, which can prevent zend_jit_send_ref() from correctly handling an indirect argument.

    the fix is a one-line change with a regression test. i confirmed that the original implementation fails the test while the patched version passes. the full jit test directory also completed with 515 passes, no failures and 10 expected skips.

    i've targeted PHP-8.4 since the original issue reproduces there as well.

    thanks for the detailed reproducer, it was really helpful in tracking this down. feedback on the pr is welcome!

  5. ndossche commented on Oct 25, 2026

    @ndossche
    Member

    Thanks for investigating aasul, I'll have a look.

  6. added a commit that references this issue on Oct 26, 2026
    f2e088e
  7. added 4 commits that reference this issue on Oct 26, 2026
    061f0ce
    018b0a8
    ca0030b
    746a684
  8. added a commit that references this issue on Oct 27, 2026
    a31751c
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions