[php-src] PR #24486: Fix GH-24480: finally block running twice when a VM interrupt throws in FAST_RET
| From: | cscott | Date: | Wed, 21 Oct 2026 16:17:43 +0000 |
| Subject: | [php-src] PR #24486: Fix GH-24480: finally block running twice when a VM interrupt throws in FAST_RET | ||
| Groups: | php.git-pulls | ||
| Request: | Send a blank email to git-pulls+get-39662@lists.php.net to get a copy of this message | ||
Pull Request: https://github.com/php/php-src/pull/24486
Author: cscott
Fixes GH-24480 and Wikimedia [T440568](https://phabricator.wikimedia.org/T440568).
ZEND_FAST_RET jumps back to the RETURN (or
JMP/FAST_CALL) that follows the FAST_CALL, with
ZEND_VM_JMP_EX(), which checks for interrupts after setting opline. That
op is inside the try block, so if the interrupt function throws, **the finally block runs a second
time** and the pending return value leaks (hybrid VM).
In the CALL VM the interrupt is handled at FAST_RET itself, after the user function
called from the finally block returns. FAST_RET is finally_end, which
ZEND_HANDLE_EXCEPTION and zend_dispatch_try_catch_finally_helper count as
outside the try/finally, so the return value and any delayed exception leak.
This PR:
- checks for interrupts in FAST_RET *before* the jump, while opline is
still FAST_RET;
- treats an exception thrown at FAST_RET as thrown inside its finally
block (< finally_end → <= finally_end). This frees the pending
return
value and chains the delayed exception, as for any other exception in the
finally block. FAST_RET's own rethrow path reaches that branch with an
empty fast_call, so it is not affected;
- adds zend_test.observer.set_vm_interrupt_on_end, which makes an
interrupt pending and throwing as an observed function returns, so the
tests are deterministic.
Tested on debug builds of the hybrid and CALL VMs, without opcache and with opcache with JIT off,
function JIT and tracing JIT. The new tests fail without the fix on both VM kinds.
On upmerge to 8.5+, observer_end() needs atomic_store(&EG(vm_interrupt),
true).