From 23c710f719c22430f8dccd306f090f8a905626b0 Mon Sep 17 00:00:00 2001 From: Pavel Gushcha Date: Fri, 9 Oct 2026 18:09:43 +0000 Subject: [PATCH 1/3] Fix JIT stub veneers being reused after an OPcache restart on AArch64 On AArch64 a branch from JIT code to a stub may be out of range, and IR emits a veneer next to the new code. zend_jit_commit_veneers() then replaces the address of the stub in zend_jit_stub_handlers[] by the address of the veneer, so that later code reuses it. These veneers live after the JIT restart point: zend_jit_restart() rewinds the buffer below them, but zend_jit_stub_handlers[] keeps pointing to them. Code compiled after the restart branches to the old addresses, which are overwritten by new traces, and crashes (e.g. on an exception or when leaving a function through a stub). This needs more than 1MB of JIT code (the range of conditional branches) before the restart. Keep the original stub addresses after the pending veneers and restore them in zend_jit_restart(). --- ext/opcache/jit/zend_jit.c | 7 +++++-- ext/opcache/jit/zend_jit_ir.c | 21 +++++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/ext/opcache/jit/zend_jit.c b/ext/opcache/jit/zend_jit.c index a958a2453c4e..e43f04f6be10 100644 --- a/ext/opcache/jit/zend_jit.c +++ b/ext/opcache/jit/zend_jit.c @@ -3702,8 +3702,8 @@ void zend_jit_startup(void *buf, size_t size, bool reattached) *dasm_ptr = (void**)*dasm_ptr + sizeof(zend_jit_stubs) / sizeof(zend_jit_stubs[0]); #elif defined(IR_TARGET_AARCH64) zend_jit_stub_handlers = dasm_buf; - *dasm_ptr = (void**)*dasm_ptr + (sizeof(zend_jit_stubs) / sizeof(zend_jit_stubs[0])) * 2; - memset(zend_jit_stub_handlers, 0, (sizeof(zend_jit_stubs) / sizeof(zend_jit_stubs[0])) * 2 * sizeof(void*)); + *dasm_ptr = (void**)*dasm_ptr + (sizeof(zend_jit_stubs) / sizeof(zend_jit_stubs[0])) * 3; + memset(zend_jit_stub_handlers, 0, (sizeof(zend_jit_stubs) / sizeof(zend_jit_stubs[0])) * 3 * sizeof(void*)); #endif *dasm_ptr = (void*)ZEND_MM_ALIGNED_SIZE_EX(((size_t)(*dasm_ptr)), 16); zend_jit_protect(); @@ -3889,6 +3889,9 @@ void zend_jit_restart(void) /* restore JIT buffer pos */ dasm_ptr[0] = dasm_ptr[1]; +#if defined(IR_TARGET_AARCH64) + zend_jit_restore_stub_handlers(); +#endif zend_jit_trace_restart(); diff --git a/ext/opcache/jit/zend_jit_ir.c b/ext/opcache/jit/zend_jit_ir.c index 50851d75abb5..8e5678fee492 100644 --- a/ext/opcache/jit/zend_jit_ir.c +++ b/ext/opcache/jit/zend_jit_ir.c @@ -449,6 +449,24 @@ static void zend_jit_commit_veneers(void) } } } + +/* zend_jit_commit_veneers() replaces the address of a stub by the address of a veneer emitted + * after it. zend_jit_restart() rewinds the buffer below such veneers, so it restores the + * original addresses, kept after the pending veneers. */ +static void zend_jit_save_stub_handlers(void) +{ + int count = sizeof(zend_jit_stubs) / sizeof(zend_jit_stubs[0]); + + memcpy(zend_jit_stub_handlers + count * 2, zend_jit_stub_handlers, count * sizeof(void*)); +} + +static void zend_jit_restore_stub_handlers(void) +{ + int count = sizeof(zend_jit_stubs) / sizeof(zend_jit_stubs[0]); + + memcpy(zend_jit_stub_handlers, zend_jit_stub_handlers + count * 2, count * sizeof(void*)); + memset(zend_jit_stub_handlers + count, 0, count * sizeof(void*)); +} #endif static bool zend_jit_prefer_const_addr_load(zend_jit_ctx *jit, uintptr_t addr) @@ -2969,6 +2987,9 @@ static void zend_jit_setup_stubs(void) } zend_jit_free_ctx(&jit); } +#if defined(IR_TARGET_AARCH64) + zend_jit_save_stub_handlers(); +#endif } #define REGISTER_HELPER(n) \ From 46b03d65a75161400097feb834afdd6966deb14e Mon Sep 17 00:00:00 2001 From: Pavel Gushcha Date: Sat, 17 Oct 2026 15:43:24 +0000 Subject: [PATCH 2/3] Add a test for JIT stub veneers after an OPcache restart Based on the reproducer by Derrick Austin from #24383: more than 1 MB of tracing JIT code, opcache_reset(), then different code. Without the fix the server crashes on AArch64 after the reset. Co-authored-by: Derrick Austin --- .../tests/jit/restart_stub_veneers.phpt | 116 ++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 ext/opcache/tests/jit/restart_stub_veneers.phpt diff --git a/ext/opcache/tests/jit/restart_stub_veneers.phpt b/ext/opcache/tests/jit/restart_stub_veneers.phpt new file mode 100644 index 000000000000..d45761a321da --- /dev/null +++ b/ext/opcache/tests/jit/restart_stub_veneers.phpt @@ -0,0 +1,116 @@ +--TEST-- +JIT stub handlers must not point to veneers discarded by an OPcache restart +--DESCRIPTION-- +On AArch64, traces that are out of branch range of a stub get a veneer, and the address of the +stub in zend_jit_stub_handlers[] is replaced by the address of that veneer. Code compiled after a +restart must not branch to veneers in the discarded part of the JIT buffer. Needs more than 1 MB +of JIT code, and different code after the restart than before it. +--EXTENSIONS-- +opcache +--SKIPIF-- + +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.jit=disable +opcache.jit_buffer_size=16M +--CONFLICTS-- +server +--FILE-- + \$v) { if ((\$v + \$k) % " . ($m + 2) . " === 0) { \$s += \$v * $m; } else { \$s ^= \$j; } \$s .= ''; \$s = (int)\$s; }\n" + . " \$str = str_repeat('x', \$k % 7); \$s += strlen(\$str);\n" + . " return \$s;\n }\n"; + } + file_put_contents("$dir/a/C$i.php", $src . "}\n"); +} + +/* Workload B: polymorphic branches, closures and caught exceptions */ +for ($i = 0; $i < 400; $i++) { + $src = "d[\$o]); }\n" + . " public function offsetGet(mixed \$o): mixed { return \$this->d[\$o] ?? null; }\n" + . " public function offsetSet(mixed \$o, mixed \$v): void { \$this->d[\$o] = \$v; }\n" + . " public function offsetUnset(mixed \$o): void { unset(\$this->d[\$o]); }\n" + . " public function run(array \$a): mixed { \$r = 0; foreach (\$a as \$k => \$v) {\n" + . " if (is_int(\$v)) { \$r += \$v * $i; } elseif (is_string(\$v)) { \$r .= substr(\$v, 0, 2); } elseif (is_array(\$v)) { \$r = count(\$v) + (int)\$r; } else { \$r = \$v ?? \$r; }\n" + . " \$this[\$k % 17] = \$r; \$x = fn(\$y) => \$y . \$k; \$r = is_string(\$r) ? strlen(\$x(\$r)) : \$r; }\n" + . " try { if (\$r % " . (($i % 9) + 2) . " === 0) { throw new RuntimeException('x'); } } catch (Throwable \$e) { \$r++; }\n" + . " return \$r; }\n}\n"; + file_put_contents("$dir/b/P$i.php", $src); +} + +file_put_contents("$dir/a.inc", <<<'PHP' +run($mix); +} +PHP); +file_put_contents("$dir/b.php", " +--CLEAN-- + +--EXPECT-- +b ok +b ok +bool(true) +ab ok +ab ok From 7664a5fb73151a9445e95ea893bc4a20cb5ca655 Mon Sep 17 00:00:00 2001 From: Pavel Gushcha Date: Sat, 17 Oct 2026 17:19:59 +0000 Subject: [PATCH 3/3] Fix loading OPcache in the CLI server of the test on Windows Use php_opcache.dll on Windows and pass extension_dir explicitly, like file_cache_frameless_handler.phpt. --- ext/opcache/tests/jit/restart_stub_veneers.phpt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ext/opcache/tests/jit/restart_stub_veneers.phpt b/ext/opcache/tests/jit/restart_stub_veneers.phpt index d45761a321da..d6b048ef662a 100644 --- a/ext/opcache/tests/jit/restart_stub_veneers.phpt +++ b/ext/opcache/tests/jit/restart_stub_veneers.phpt @@ -85,8 +85,9 @@ $ini = '-d opcache.enable=1 -d opcache.enable_cli=1 -d opcache.jit=tracing -d op . ' -d opcache.jit_max_root_traces=100000 -d opcache.jit_max_side_traces=100000' . ' -d opcache.jit_max_exit_counters=65536 -d opcache.validate_timestamps=0' . ' -d opcache.file_update_protection=0 -d error_reporting=' . (E_ALL & ~E_DEPRECATED); -if (file_exists(ini_get('extension_dir') . '/opcache.' . PHP_SHLIB_SUFFIX)) { - $ini .= ' -d zend_extension=opcache.' . PHP_SHLIB_SUFFIX; +$ext = ini_get('extension_dir'); +if (file_exists($ext . DIRECTORY_SEPARATOR . (PHP_OS_FAMILY === 'Windows' ? 'php_opcache.dll' : 'opcache.so'))) { + $ini .= ' -d extension_dir=' . $ext . ' -d zend_extension=opcache'; } php_cli_server_start($ini);