[php-src] PR #24363: Fix SoapClient not restoring EG(vm_stack_top) after a caught bailout
| From: | likemusic | Date: | Thu, 15 Oct 2026 19:22:23 +0000 |
| Subject: | [php-src] PR #24363: Fix SoapClient not restoring EG(vm_stack_top) after a caught bailout | ||
| Groups: | php.git-pulls | ||
| Request: | Send a blank email to git-pulls+get-39488@lists.php.net to get a copy of this message | ||
Pull Request: https://github.com/php/php-src/pull/24363
Author: likemusic
SOAP_CLIENT_END_CODE() catches a bailout raised during a SoapClient call, frees the VM
stack pages pushed during the call and restores the saved stack top. Since 0390cde428f (2014), the
live stack top is EG(vm_stack_top), but the macro still writes the saved value to
EG(vm_stack)->top. That field only matters for a page that is not the current one.
So EG(vm_stack_top) keeps pointing to where the stack was when the bailout happened. If
the call grew the stack beyond the current page, that is inside a page that has just been freed.
This patch restores EG(vm_stack_top) instead, the same way
zend_vm_stack_free_call_frame_ex() does when it pops a page.
### Reproducer
The test included in this PR triggers it. __doRequest() recurses 50 000 levels deep,
then raises a fatal error that the client turns into a SoapFault
(eval("class Dup {} class Dup {}"), no memory exhaustion involved). After the
catch, ordinary function calls continue.
On a debug build, without the fix:
```
php: Zend/zend_execute.h:338: zend_vm_stack_free_call_frame_ex: Assertion
`(executor_globals.vm_stack_top) > (zval *) (executor_globals.vm_stack) &&
(executor_globals.vm_stack_end) > (zval *) (executor_globals.vm_stack) &&
(executor_globals.vm_stack_top) <= (executor_globals.vm_stack_end)' failed.
Termsig=6
```
The same assertion fires on master (zend_execute.h:445).
With the fix, the test passes on PHP-8.4 and on master.
### Scope
I could only observe this on debug builds. A release build with ASAN (with and without
USE_ZEND_ALLOC=0) stays silent on the same script: the next
zend_vm_stack_free_call_frame_ex() takes the page-pop path and overwrites the stale
pointer before anything reads through it. So I am not claiming a user-visible crash on release
builds. The fix is to make the restore do what it was written to do.
### Testing
- ext/soap + Zend/tests on PHP-8.4, debug build: 5345 passed, 0 failed, 1
expected fail (interface_constructor_prototype_002.phpt), 246 skipped.
- ext/soap on master with this commit cherry-picked, debug build: 517 passed, 0 failed,
109 skipped. The cherry-pick conflicts only in NEWS.
Found while auditing the zend_catch sites that continue execution after a bailout. The
audit started from #10026.
🤖 Generated with [Claude Code](https://claude.com/claude-code)