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
left a comment
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.
| void zend_set_utility_values(zend_utility_values *utility_values); | ||
| void zend_unload_modules(void); | ||
|
|
||
| ZEND_API ZEND_COLD ZEND_NORETURN void _zend_bailout_without_gc_protect(const char *filename, uint32_t lineno); |
There was a problem hiding this comment.
that s also something to definitely avoid in a stable branch.
| #define soap_error0(severity, format) \ | ||
| php_error(severity, "SOAP-ERROR: " format) | ||
| do { \ | ||
| SOAP_GLOBAL(error_is_own) = true; \ |
There was a problem hiding this comment.
this sort of solution around a global is really fragile.
|
So I second Girgias here, required changes in master, thanks for the work nonetheless ! |
|
Thanks for the review, @Girgias and @devnexen. Agreed: a new bailout variant in a stable branch and a global flag were the wrong direction. I opened #24366 against master. The WSDL and XML schema loaders now throw the Encoding and decoding errors in Closing this one in favour of #24366. |
|
But even using exceptions is not that simple: the bailout mechanism is there because fatals need to be caught and converted to a soap fault |
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