From 1be706959208010bb9af026baa4512fe93199624 Mon Sep 17 00:00:00 2001 From: Alexandre Rulleau Date: Wed, 10 Jun 2026 14:25:19 +0200 Subject: [PATCH] fix(tracing): reset jit_trace_num on PHP 8.5 sandbox bailout The #3964 regression test tests/opcache/jit_trace_num_bailout.phpt crashes with an ASAN SEGV only on PHP 8.5 (bad JIT side-exit reading a stale trace stack map). PHP 8.5's IR-based tracing JIT made zend_jit_trace_exit() less tolerant of a stale EG(jit_trace_num) after a caught sandbox bailout, so restoring the captured (possibly already-stale) trace id re-arms the crash. zend_call_function() only ever save/restores EG(jit_trace_num), and opcache exposes no symbol to reset JIT_G(tracing) from another extension, so on 8.5 we reset jit_trace_num to 0 on the bailout-catch path -- the 'not in a trace' invariant the resumed interpreter expects. <=8.4 behaviour is unchanged. --- zend_abstract_interface/sandbox/sandbox.h | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/zend_abstract_interface/sandbox/sandbox.h b/zend_abstract_interface/sandbox/sandbox.h index d5769c32e1..63d164d888 100644 --- a/zend_abstract_interface/sandbox/sandbox.h +++ b/zend_abstract_interface/sandbox/sandbox.h @@ -195,7 +195,24 @@ inline void zai_sandbox_engine_state_backup(zai_engine_state *es) { inline void zai_sandbox_engine_state_restore(zai_engine_state *es) { EG(current_execute_data) = es->current_execute_data; +#if PHP_VERSION_ID >= 80500 + /* PHP 8.5's IR-based tracing JIT made zend_jit_trace_exit() less tolerant of + * a stale trace id after a caught bailout: it removed the GCC_GLOBAL_REGS + * guard around the exception/opline correction (php-src zend_jit_trace.c, + * ~8773-8779), so the resumed caller reaches the side-exit deopt path far + * more readily and dereferences &zend_jit_traces[EG(jit_trace_num)]. On the + * sandbox bailout-catch path the captured jit_trace_num can itself be stale + * (the hook may have been entered while already inside a trace), so simply + * restoring it re-arms the crash on 8.5. zend_call_function() only ever + * save/restores EG(jit_trace_num) (Zend/zend_execute_API.c) and opcache + * exposes no symbol to reset JIT_G(tracing) from another extension, so we + * reset to 0 here -- the "not currently in a trace" invariant the resumed + * interpreter expects; the engine re-establishes jit_trace_num on the next + * trace entry. This path is reached only from zai_sandbox_bailout(). */ + EG(jit_trace_num) = 0; +#else EG(jit_trace_num) = es->jit_trace_num; +#endif } inline void zai_sandbox_open(zai_sandbox *sandbox) {