Repository navigation
Fix GH-24682: JIT IR context (malloc) is not freed on zend_bailout() - #24682
matyapiro31 wants to merge 3 commits into
Conversation
When zend_bailout() is taken during JIT compilation, the IR context (allocated with ir_mem_malloc / malloc, not emalloc) is never freed because the outer zend_catch blocks in zend_jit_hot_func(), zend_runtime_jit(), zend_jit_compile_root_trace(), zend_jit_compile_side_trace(), zend_jit_blacklist_trace_exit(), and zend_jit_trace() only set do_bailout without calling zend_jit_free_ctx(). The ir_mem_malloc buffer is not part of the Zend memory manager and is not reclaimed by php_request_shutdown(), so in a long-lived php-fpm worker each bailout event leaks >= 20 KiB (and more if the buffer has been grown with ir_mem_realloc). Fix: add zend_jit_ctx *active_ctx to zend_jit_globals. After ir_init (via zend_jit_start / zend_jit_trace_start / zend_jit_deoptimizer_start) set JIT_G(active_ctx) = &ctx. Clear it to NULL before each normal zend_jit_free_ctx() call. Extend every outer zend_catch block to call zend_jit_free_ctx(JIT_G(active_ctx)) and set it back to NULL. The field is zero-initialized at module startup; in ZTS builds JIT_G() is already per-thread (TSRMG), so no additional locking is needed. The internals of zend_jit() and zend_jit_trace() are unchanged. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
d968511 to
19a4afb
Compare
orig_trigger was assigned inside the zend_try block (after the setjmp
point), so under the C standard its value in zend_catch is unspecified
if longjmp is taken. Move the save to before the zend_try and add an
explicit restore in both zend_catch blocks so that JIT_G(trigger) is
always left in its pre-compilation state even when zend_jit_trace()
raises a bailout.
Affected functions:
- zend_jit_compile_root_trace(): orig_trigger moved before zend_try {}
- zend_jit_compile_side_trace(): orig_trigger moved before SHM_UNPROTECT()
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Added commit Problem: In both zend_try {
orig_trigger = JIT_G(trigger); // ← assigned INSIDE try (after setjmp)
JIT_G(trigger) = ZEND_JIT_ON_HOT_TRACE;
handler = zend_jit_trace(...);
JIT_G(trigger) = orig_trigger; // restore on success path only
} zend_catch {
do_bailout = 1;
// ← orig_trigger NOT restored here
...
} zend_end_try();Per the C standard (CERT MSC22-C), automatic variables modified between Fix: Move Note: |
…_compile_file() `orig_compiler_options` was assigned inside the `zend_try` block, making it an automatic variable modified after setjmp(). Per CERT MSC22-C, the value of such a variable is indeterminate in the `zend_catch` handler. At -O2 the compiler may keep it in a register; after longjmp() that register is restored to its setjmp()-time value (zero from the initialiser), so the catch block could restore CG(compiler_options) to 0 instead of the original value. Move the save before `zend_try` so `orig_compiler_options` holds the correct value regardless of when a bailout occurs. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Follow-up commit:
uint32_t orig_compiler_options = 0; // ← initialised to 0 here
...
zend_try {
orig_compiler_options = CG(compiler_options); // ← saved AFTER setjmp()
CG(compiler_options) |= ...;
...
op_array = accelerator_orig_compile_file(...); // ← bailout most likely here
CG(compiler_options) = orig_compiler_options;
} zend_catch {
CG(compiler_options) = orig_compiler_options; // ← which value does the compiler use?
} zend_end_try();The C standard (C11 §7.13.2.1 p3) says that after a Fix: move the save to before |
Summary
When
zend_bailout()is taken during JIT compilation, their_mem_malloc()-basedIR context (
zend_jit_ctx) is never freed. The buffer is not managed by theZend memory manager and is not reclaimed by
php_request_shutdown(), so in along-lived php-fpm worker each bailout event leaks ≥ 20 KiB (more if the buffer
has been grown with
ir_mem_realloc).Root cause
zend_jit()andzend_jit_trace()hold azend_jit_ctx ctxas a local variable.The outer
zend_catchblocks in their callers (zend_jit_hot_func,zend_runtime_jit,zend_jit_compile_root_trace,zend_jit_compile_side_trace,zend_jit_blacklist_trace_exit) only setdo_bailoutand never callzend_jit_free_ctx(), so themallocbuffer leaks.What triggers the bailout
Between
ir_init()andjit_failure:there areemalloc-based allocations thatcan hit
memory_limit:zend_arena_calloc()inzend_jit_allocate_registers()(arena usesemalloc)zend_hash_index_lookup()→zend_hash_real_init_mixed_ex()foraddr_hash(uses
fpemalloc(..., persistent=false, fallible=false))Fix
Add
zend_jit_ctx *active_ctxtozend_jit_globals. Afterir_init(viazend_jit_start/zend_jit_trace_start/zend_jit_deoptimizer_start) setJIT_G(active_ctx) = &ctx. Clear it toNULLbefore each normalzend_jit_free_ctx()call. Extend every outerzend_catchblock:The field is zero-initialized at module startup. In ZTS builds
JIT_G()is alreadyper-thread (TSRMG), so no additional locking is needed. The internals of
zend_jit()andzend_jit_trace()are unchanged.Verification
Measured with CDB on a debug build (8.4.28-dev,
--enable-opcache-jit), countingir_init(malloc) vszend_jit_free_ctx(free) calls with a forcedE_ERRORinjected after
ir_initinsidezend_jit():Files changed
ext/opcache/jit/zend_jit.h— forward declaration +active_ctxfieldext/opcache/jit/zend_jit.c— set/clear inzend_jit(), extend 2 catch blocksext/opcache/jit/zend_jit_trace.c— set/clear inzend_jit_trace()andzend_jit_trace_exit_to_vm(), extend 4 catch blocks🤖 Generated with Claude Code