Summary
Reassigning the caught exception variable to a new object inside the catch body leaks both objects: the release of the previous slot value is typed as Mixed even though the slot holds an Object, so the decref misreads the pointer as a boxed Mixed cell and becomes a no-op.
Verified on
main (macOS ARM64), via --heap-debug. The #448 fix (PR #477) is neutral on this shape — identical leak counts before and after.
Minimal reproduction
<?php
for ($n = 0; $n < 50; $n++) {
try { throw new TypeError("x"); } catch (\Throwable $e) {
$e = new TypeError("y"); // object reassignment
}
}
echo "done";
Actual (--heap-debug)
HEAP DEBUG: leak summary: live_blocks=149 live_bytes=6760
~3 blocks leak per iteration (the thrown object, the reassigned object, plus an auxiliary block). In the EIR (--emit-ir), the release before the store loads the slot as php=mixed:
v9: Heap(Mixed) php=mixed = load_local slot[0] ; the slot's declared type is Object("Throwable")
release v9 ; decref-as-Mixed on an object pointer → no-op
Controls (work)
$e = null; in the catch → clean.
unset($e); in the catch → clean.
- Reassigning an ordinary object local (
$o = new T(); $o = new T();) in a loop → clean.
Notes
The mistyped load suggests the assignment's release-previous path falls back to Mixed for catch-bound locals specifically (the ordinary-local path resolves the declared slot type). Distinct from #448 (the missing release of the binding), which is fixed in #477.
Summary
Reassigning the caught exception variable to a new object inside the
catchbody leaks both objects: the release of the previous slot value is typed asMixedeven though the slot holds anObject, so the decref misreads the pointer as a boxed Mixed cell and becomes a no-op.Verified on
main(macOS ARM64), via--heap-debug. The #448 fix (PR #477) is neutral on this shape — identical leak counts before and after.Minimal reproduction
Actual (
--heap-debug)~3 blocks leak per iteration (the thrown object, the reassigned object, plus an auxiliary block). In the EIR (
--emit-ir), the release before the store loads the slot asphp=mixed:Controls (work)
$e = null;in the catch → clean.unset($e);in the catch → clean.$o = new T(); $o = new T();) in a loop → clean.Notes
The mistyped load suggests the assignment's release-previous path falls back to
Mixedfor catch-bound locals specifically (the ordinary-local path resolves the declared slot type). Distinct from #448 (the missing release of the binding), which is fixed in #477.