Skip to content

JIT: function with >~1.7 MB of machine code corrupts JIT buffer accounting; next compilation crashes (SIGBUS on arm64) #24675

Open

Description

@ChrisOrlando

Description

With function JIT (opcache.jit=function, also 1204), compiling a single function whose
generated machine code exceeds roughly 1.7 MB makes opcache_get_status()['jit']['buffer_free']
increase by about the size of that code, instead of decreasing. The next compilation that
emits code then crashes the process with SIGBUS (exit status 138).

The following code:

<?php
// php -d opcache.enable_cli=1 -d opcache.jit=function -d opcache.jit_buffer_size=64M \
//     -d opcache.file_update_protection=0 -d memory_limit=1G repro.php

$body = '';
for ($k = 0; $k < 3000; $k++) {
    $body .= "    \$r[" . ($k % 32) . "] = \$v + $k;\n";
}
file_put_contents(__DIR__ . '/big.php', "<?php\nfunction big(array &\$r, int \$v) {\n$body}\n");
file_put_contents(__DIR__ . '/next.php', "<?php\necho \"next file compiled\\n\";\n");

$free = fn() => opcache_get_status(false)['jit']['buffer_free'];
$before = $free();
include __DIR__ . '/big.php';
printf("JIT buffer_free changed by %+d bytes (should be negative: code was added)\n", $free() - $before);
include __DIR__ . '/next.php';
echo "survived\n";

Resulted in this output:

JIT buffer_free changed by +1859668 bytes (should be negative: code was added)
Bus error: 10      (exit status 138)

But I expected this output instead:

JIT buffer_free changed by -1859668 bytes (should be negative: code was added)
next file compiled
survived

Observations

  • The threshold is the size of one function's machine code, not the input: with this shape,
    2850 statements add 1,766,964 bytes and work; 2851 statements report buffer_free going
    up by 1,767,384 and the next compilation crashes. Closures and named functions, by-value
    and by-reference array parameters all behave the same.
  • The big function itself can be called fine afterwards; it's the next compilation that
    faults, which fits the buffer's free pointer having moved backwards.
  • Reproduces with opcache.jit=function and 1204 and JIT buffer sizes of 16M, 64M and 128M.
    Doesn't with 1201 (optimization level 1), 1215 (compile on first call, since big()
    isn't called here), tracing (nothing compiled at load), or a 1M buffer (presumably it doesn't
    fit at all).
  • opcache.file_update_protection=0 is only there so the freshly written files are cached
    and JIT-compiled; with pre-existing files the flag isn't needed.
  • Found in a PHP emulator that generates large PHP functions at run time; it showed up there as
    "zend_mm_heap corrupted", "Undefined array key" on impossible lines, and bus errors.

PHP Version

PHP 8.5.12 (cli) (built: Oct 19 2026 17:02:46) (NTS)   [Homebrew]

Operating System

macOS (Darwin 27.0.0), Apple M1 (arm64)

Activity

  1. ndossche commented on Oct 25, 2026

    @ndossche
    Member

    Thanks for the report. It's probably related to veneers (if code is too far away) on aarch64.
    I don't have an ARM machine handy, but is it please possible to test this patch?

    diff --git a/ext/opcache/jit/ir/ir_aarch64.dasc b/ext/opcache/jit/ir/ir_aarch64.dasc
    index fc4bb84f1e0..dae5837403d 100644
    --- a/ext/opcache/jit/ir/ir_aarch64.dasc
    +++ b/ext/opcache/jit/ir/ir_aarch64.dasc
    @@ -6945,7 +6945,7 @@ void *ir_emit_code(ir_ctx *ctx, size_t *size_ptr)
     		if (ctx->code_buffer) {
     			if (ctx->code_buffer->pos == (char*)entry + size) {
     				/* rollback */
    -				ctx->code_buffer->pos = (char*)entry - size;
    +				ctx->code_buffer->pos = (char*)entry;
     			}
     		} else {
     			ir_mem_unmap(entry, size);
    @@ -7068,7 +7068,7 @@ const void *ir_emit_exitgroup(uint32_t first_exit_point, uint32_t exit_points_pe
     		dasm_free(&dasm_state);
     		if (code_buffer->pos == (char*)entry + size) {
     			/* rollback */
    -			code_buffer->pos = (char*)entry - size;
    +			code_buffer->pos = (char*)entry;
     		}
     		return NULL;
     	}
    diff --git a/ext/opcache/jit/ir/ir_x86.dasc b/ext/opcache/jit/ir/ir_x86.dasc
    index f5efb66698d..a62e49fc40e 100644
    --- a/ext/opcache/jit/ir/ir_x86.dasc
    +++ b/ext/opcache/jit/ir/ir_x86.dasc
    @@ -12267,7 +12267,7 @@ next_block:;
     		if (ctx->code_buffer) {
     			if (ctx->code_buffer->pos == (char*)entry + size) {
     				/* rollback */
    -				ctx->code_buffer->pos = (char*)entry - size;
    +				ctx->code_buffer->pos = (char*)entry;
     			}
     		} else {
     			ir_mem_unmap(entry, size);
    @@ -12393,7 +12393,7 @@ const void *ir_emit_exitgroup(uint32_t first_exit_point, uint32_t exit_points_pe
     		dasm_free(&dasm_state);
     		if (code_buffer->pos == (char*)entry + size) {
     			/* rollback */
    -			code_buffer->pos = (char*)entry - size;
    +			code_buffer->pos = (char*)entry;
     		}
     		return NULL;
     	}
    diff --git a/ext/opcache/jit/zend_jit_ir.c b/ext/opcache/jit/zend_jit_ir.c
    index c10c904a5a6..96761702272 100644
    --- a/ext/opcache/jit/zend_jit_ir.c
    +++ b/ext/opcache/jit/zend_jit_ir.c
    @@ -472,6 +472,15 @@ static void zend_jit_commit_veneers(void)
     		}
     	}
     }
    +
    +static void zend_jit_discard_veneers(void)
    +{
    +	int i, count = sizeof(zend_jit_stubs) / sizeof(zend_jit_stubs[0]);
    +
    +	for (i = 0; i < count; i++) {
    +		zend_jit_stub_handlers[count + i] = NULL;
    +	}
    +}
     #endif
     
     static bool zend_jit_prefer_const_addr_load(zend_jit_ctx *jit, uintptr_t addr)
    @@ -2954,7 +2963,11 @@ static void *zend_jit_ir_compile(ir_ctx *ctx, size_t *size, const char *name)
     
     #if defined(IR_TARGET_AARCH64)
     	if (ctx->flags2 & IR_HAS_VENEERS) {
    -		zend_jit_commit_veneers();
    +		if (entry) {
    +			zend_jit_commit_veneers();
    +		} else {
    +			zend_jit_discard_veneers();
    +		}
     	}
     #endif
     
    
  2. ChrisOrlando commented on Oct 25, 2026

    @ChrisOrlando
    Author

    Tested the patch on 8.5.12, macOS arm64 (M1). Fixes the crash: the repro now prints "next file compiled" / "survived" with jit=function and 1204 at 16M/64M/128M. Functions over the threshold are no longer JIT-compiled (buffer_free delta is ~−50 bytes, the function runs in the VM and gives correct results); 2850 statements still JIT fine (−1,766,776).

  3. ndossche commented on Oct 25, 2026

    @ndossche
    Member

    Thanks a lot. This needs a PR upstream to Dmitry's IR repo, I'll do that soon.

  4. self-assigned this
    on Oct 25, 2026
  5. added a commit that references this issue on Oct 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions