[php-src] PR #24156: Zend: Fix memory leak FE_FETCH value live range dropped by opcache
| From: | adapik | Date: | Tue, 06 Oct 2026 08:01:11 +0000 |
| Subject: | [php-src] PR #24156: Zend: Fix memory leak FE_FETCH value live range dropped by opcache | ||
| Groups: | php.git-pulls | ||
| Request: | Send a blank email to git-pulls+get-39196@lists.php.net to get a copy of this message | ||
Pull Request: https://github.com/php/php-src/pull/24156
Author: adapik
FE_FETCH_R/RW is the only opcode that defines two temporaries: the key in result and
the value in op2, when the foreach target isn't a plain CV, e.g. $k => func()[]
or $k => [$a, $b].
In be7eab32 @nikic approached this issue, zend_calc_live_ranges() treats op2 as a def
and emits a separate live range for it. Like every other range, that one goes through the
needs_live_range callback.
The callback only gets the defining opline, not the variable, and the optimizer's
implementation always checks the type of ssa_op->result_def. For FE_FETCH,
that's the key. When type inference knows the key is non-refcounted (int for a
packed array, which is the common case), the callback returns false for the value's range as
well and the range is dropped, even when the value may be a string, array or object.
If an exception is thrown between FE_FETCH and the value's consumer, e.g. while
evaluating the rest of the assignment target, the value is never freed. Without opcache there is no
callback, so the range is always kept.
be7eab32's test didn't catch this because it runs at top level with an interned string
literal, so the dropped range had nothing to leak. On master, the array_map() lowering from a3576bdd
emits exactly this shape (TMP key and TMP value, with an INIT_STATIC_METHOD_CALL in between that may
autoload), so the leak is now reachable from ordinary array_map() calls.
The fix moves the type check into ssa_var_needs_live_range() and, for FE_FETCH with a TMP/VAR op2,
also checks op2_def. The callback still can't tell which of the two variables it is asked
about, so the range is kept if either one needs it.
The bug has existed since 7.4.3.