[php-src] PR #24682: Fix GH-XXXXX: JIT IR context (malloc) is not freed on zend_bailout()
| From: | matyapiro31 | 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)