Skip to content

Fix SoapClient not restoring EG(vm_stack_top) after a caught bailout - #24363

Open
likemusic wants to merge 1 commit into
php:PHP-8.4from
likemusic:fix-soap-vm-stack-top
Open

likemusic wants to merge 1 commit into
php:PHP-8.4from
likemusic:fix-soap-vm-stack-top

Conversation

@likemusic

Copy link
Copy Markdown

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 0390cde (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

SOAP_CLIENT_END_CODE() catches the bailout, frees the VM stack pages that were pushed during the call and then restores the saved stack top. Since 0390cde (2014) the live stack top is EG(vm_stack_top), but the macro still writes the saved value to EG(vm_stack)->top, which only matters for a page that is not the current one. EG(vm_stack_top) keeps pointing to where the stack was when the bailout happened: into a page that has just been freed if the call grew the stack beyond the current page. The next call frame release then trips the assertion in zend_vm_stack_free_call_frame_ex() on debug builds.

Restore EG(vm_stack_top) instead, the same way zend_vm_stack_free_call_frame_ex() does when it pops a page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@likemusic
likemusic force-pushed the fix-soap-vm-stack-top branch from cd33509 to 691ebad Compare October 15, 2026 20:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant