[php-src] PR #24682: Fix GH-XXXXX: JIT IR context (malloc) is not freed on zend_bailout()

From: Date: Sun, 25 Oct 2026 12:26:07 +0000
Subject: [php-src] PR #24682: Fix GH-XXXXX: JIT IR context (malloc) is not freed on zend_bailout()
Groups: php.git-pulls 
Request: Send a blank email to git-pulls+get-39861@lists.php.net to get a copy of this message
Pull Request: https://github.com/php/php-src/pull/24682 Author: matyapiro31 ## Summary When zend_bailout() is taken during JIT compilation, the ir_mem_malloc()-based IR context (zend_jit_ctx) is never freed. The buffer is **not** managed by 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 (more if the buffer has been grown with ir_mem_realloc). ### Root cause zend_jit() and zend_jit_trace() hold a zend_jit_ctx ctx as a local variable. The outer zend_catch blocks 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 set do_bailout and never call zend_jit_free_ctx(), so the malloc buffer leaks. ### What triggers the bailout Between ir_init() and jit_failure: there are emalloc-based allocations that can hit memory_limit: - zend_arena_calloc() in zend_jit_allocate_registers() (arena uses emalloc) - zend_hash_index_lookup() → zend_hash_real_init_mixed_ex() for addr_hash (uses fpemalloc(..., persistent=false, fallible=false)) ### 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: ```c } zend_catch { do_bailout = true; if (JIT_G(active_ctx)) { zend_jit_free_ctx(JIT_G(active_ctx)); JIT_G(active_ctx) = NULL; } } zend_end_try(); ``` 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.** ### Verification Measured with CDB on a debug build (8.4.28-dev, --enable-opcache-jit), counting ir_init (malloc) vs zend_jit_free_ctx (free) calls with a forced E_ERROR injected after ir_init inside zend_jit(): | Scenario | ir_init | zend_jit_free_ctx | diff | |---|---|---|---| | No bailout | 38 | 38 | **0** ✓ | | Bailout, before patch | 34 | 33 | **1** ← leak | | Bailout, after patch | 34 | 34 | **0** ✓ | ### Files changed - ext/opcache/jit/zend_jit.h — forward declaration + active_ctx field - ext/opcache/jit/zend_jit.c — set/clear in zend_jit(), extend 2 catch blocks - ext/opcache/jit/zend_jit_trace.c — set/clear in zend_jit_trace() and zend_jit_trace_exit_to_vm(), extend 4 catch blocks 🤖 Generated with [Claude Code](https://claude.com/claude-code)

« previous php.git-pulls (#39861) next »