Repository navigation
Conversation
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
requested changes
Oct 15, 2026
Girgias
left a comment
Member
There was a problem hiding this comment.
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes GH-10026. Supersedes #10283 and keeps its first two commits by @ndossche (the API and the regression test).
Problem
zend_bailout()callsgc_protect(1)on the assumption that the request is ending.SoapClientcatches the bailout, turns the fatal error into aSoapFaultand 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 anyE_FATAL_ERRORSinto aSoapFault, 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.phptcovers exactly that case.Approach
The decision is made where the error is raised, not where it is caught:
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.zend_bailout_without_gc_protect(). Engine fatal errors still usezend_bailout()and stay protected. An unmarked SOAP error behaves exactly as today, so the change can only narrow the problem, not widen it.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).Tests
gh10026.phpt(from Fix GH-10026: Garbage collection stops after exception in SoapClient #10283): GC runs continue after a failednew SoapClient().gh10026_protected.phpt: a malformed local WSDL leavesgc_status()['protected']false. No network involved.gh10026_engine_fatal.phpt: amemory_limitfatal inside__doRequest()still leaves the GC protected.Without the ext/soap commit, the first two fail and the third passes. With it, all three pass.
ext/soapandZend/testsshow no regressions on a debug NTS build.Scope and open points
gc_protected. The general fix (savinggc_protectedinzend_tryand restoring it in "safe"zend_catchblocks, as @dstogov suggested in Fix GH-10026: Garbage collection stops after exception in SoapClient #10283) needs an audit of every bailout catcher, so it is left out here.zend_bailout_without_gc_protect()from Fix GH-10026: Garbage collection stops after exception in SoapClient #10283. @dstogov preferredzend_bailout_safe()/zend_bailout_clean(), and I'm happy to rename.CG(unclean_shutdown)is still set by both variants, as noted by @dstogov, and is left unchanged here.🤖 Generated with Claude Code