Skip to content

Fix GH-10026: Garbage collection stops after exception in SoapClient - #24338

Open
likemusic wants to merge 3 commits into
php:PHP-8.4from
likemusic:fix-gh10026
Open

likemusic wants to merge 3 commits into
php:PHP-8.4from
likemusic:fix-gh10026

Conversation

@likemusic

Copy link
Copy Markdown

Fixes GH-10026. Supersedes #10283 and keeps its first two commits by @ndossche (the API and the regression test).

Problem

zend_bailout() calls gc_protect(1) on the assumption that the request is ending. SoapClient catches the bailout, turns the fatal error into a SoapFault and carries on, so the GC stays protected for the rest of the process. While protected, gc_possible_root() does not buffer roots, so cycles created from then on are never collected. A single failed WSDL parse (e.g. a malformed local file) is enough, and nothing in userland can undo it.

Why not #10283 as is

As @ndossche pointed out in #10026 (comment), fatal errors also go through this path and need the protection. In soap_real_error_handler() the client converts any E_FATAL_ERRORS into a SoapFault, including memory exhaustion and timeouts. #10283 switches that site to the non-protecting bailout, so an OOM inside a SOAP call would also leave the GC unprotected. gh10026_engine_fatal.phpt covers exactly that case.

Approach

The decision is made where the error is raised, not where it is caught:

  • Errors that ext/soap raises itself through soap_errorN() are marked (SOAP_GLOBAL(error_is_own)). soap_error_handler() consumes the flag immediately, so an error raised while handling one (e.g. OOM while building the fault) is not treated as a SOAP error.
  • Only for marked errors does the client-side handler bail out with zend_bailout_without_gc_protect(). Engine fatal errors still use zend_bailout() and stay protected. An unmarked SOAP error behaves exactly as today, so the change can only narrow the problem, not widen it.
  • The ext/soap sites that catch a bailout only to free memory and rethrow it now use zend_bailout_without_gc_protect(). They preserve the original decision instead of re-protecting the GC (e.g. load_wsdl()'s rethrow would otherwise re-protect after a WSDL error).
  • The SoapServer originating sites are unchanged, because the server always ends the request.
  • Unlike Fix GH-10026: Garbage collection stops after exception in SoapClient #10283, phar, oci8, pdo_oci, phpdbg and the fuzzer are not touched.

Tests

Without the ext/soap commit, the first two fail and the third passes. With it, all three pass. ext/soap and Zend/tests show no regressions on a debug NTS build.

Scope and open points

🤖 Generated with Claude Code

ndossche and others added 3 commits October 15, 2026 01:59
This allows users to bail out of a try-catch without protecting the GC
in case the error is recoverable and normal execution will continue.
Co-authored-by: abienvenu <abi@abienvenu.net>
zend_bailout() protects the GC because it assumes the request is over.
SoapClient instead catches the bailout, turns the fatal error into a
SoapFault and carries on, so the GC stays protected for the rest of the
process and no new cycle roots are buffered.

Only lift the protection where it is known to be safe: errors raised by
ext/soap itself through soap_errorN() are marked, and only for those does
the client-side error handler bail out without protecting the GC. Engine
fatal errors that SoapClient also converts into a SoapFault (e.g. memory
exhaustion or timeouts) keep the GC protected as before.

The places that catch a bailout only to clean up and rethrow it now use
zend_bailout_without_gc_protect(), so that they preserve whatever the
original bailout decided instead of re-protecting the GC.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@Girgias Girgias left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm very much not convinced by this hack.

The correct solution is to refactor ext/soap to throw exceptions rather than use the bailout mechanism. And this would be a master only fix.

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.

3 participants