Skip to content

Commit a3d9633

Browse files
likemusicclaude
andcommitted
ext/soap: report encoding errors without a bailout
Errors in encoding a request or decoding a response were raised with soap_errorN(E_ERROR, ...). SoapClient turned the fatal error into a SoapFault and caught the bailout in SOAP_CLIENT_END_CODE(), but zend_bailout() had already called gc_protect(1), so the cycle collector stayed off for the rest of the request (GH-10026). These errors are now reported with soap_encoding_error(). In a call of a SoapClient that uses exceptions it throws the same SoapFault as before (same message, fault code "Client", also stored in __soap_fault) and returns: the encoder returns a placeholder value that its callers can handle, and the call discards the request or the response. Only the first error is thrown; once it is pending, master_to_xml() and master_to_zval() skip the rest of the value, the request is not sent, and decoded values are no longer stored, so a typed property or the shared array of output headers cannot be written to. Skipping the rest also keeps a cycle of references that fails with "Nesting level too deep" from taking exponential time. SoapServer and SoapClient with the "exceptions" option disabled still get the same fatal error as before. get_conversion() can now return NULL after the error, so guess_xml_convert() and get_array_type() handle that instead of recursing or dereferencing it. One case behaves differently: if a callback (for example a typemap) has already thrown an exception when the error happens, that exception becomes the previous exception of the SoapFault. Before, the fatal error printed it as an uncaught exception warning and discarded it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 31ac172 commit a3d9633

8 files changed

Lines changed: 489 additions & 56 deletions

File tree

‎NEWS‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,9 @@ PHP NEWS
106106
. SoapClient no longer bails out on WSDL and XML schema errors, which kept
107107
the cycle collector disabled for the rest of the request (GH-10026).
108108
(Valery Ivashchanka)
109+
. SoapClient no longer bails out on errors in encoding a request or decoding
110+
a response, which kept the cycle collector disabled for the rest of the
111+
request (GH-10026). (Valery Ivashchanka)
109112

110113
- SPL:
111114
. Fixed bug GH-23385 (SplDoublyLinkedList::serialize() use-after-free when

‎ext/soap/php_encoding.c‎

Lines changed: 104 additions & 49 deletions
Large diffs are not rendered by default.

‎ext/soap/php_packet_soap.c‎

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -261,17 +261,22 @@ bool parse_packet_soap(zval *this_ptr, zend_string *buffer, sdlFunctionPtr fn, z
261261
master_to_zval_with_doc_cleanup(&details, NULL, tmp, response);
262262
}
263263
}
264-
add_soap_fault(this_ptr, faultcode, faultstring ? ZSTR_VAL(faultstring) : NULL, faultactor, &details, lang);
264+
if (SOAP_ENCODING_ERROR_THROWN()) {
265+
/* Decoding the fault failed, the client reports that error instead */
266+
zval_ptr_dtor(&details);
267+
} else {
268+
add_soap_fault(this_ptr, faultcode, faultstring ? ZSTR_VAL(faultstring) : NULL, faultactor, &details, lang);
269+
if (Z_REFCOUNTED(details)) {
270+
Z_DELREF(details);
271+
}
272+
}
265273
if (faultstring) {
266274
zend_string_release_ex(faultstring, 0);
267275
}
268276
if (faultactor) {
269277
zend_string_release_ex(faultactor, 0);
270278
}
271279
zend_string_release_ex(lang, false);
272-
if (Z_REFCOUNTED(details)) {
273-
Z_DELREF(details);
274-
}
275280
xmlFreeDoc(response);
276281
return false;
277282
}
@@ -442,12 +447,23 @@ bool parse_packet_soap(zval *this_ptr, zend_string *buffer, sdlFunctionPtr fn, z
442447
smart_str_free(&key);
443448
}
444449
master_to_zval_with_doc_cleanup(&val, enc, trav, response);
450+
if (SOAP_ENCODING_ERROR_THROWN()) {
451+
/* The trace of the SoapFault shares the array of output headers */
452+
zval_ptr_dtor(&val);
453+
break;
454+
}
445455
add_assoc_zval(soap_headers, (const char*)trav->name, &val);
446456
}
447457
trav = trav->next;
448458
}
449459
}
450460

461+
if (SOAP_ENCODING_ERROR_THROWN()) {
462+
/* The result is incomplete */
463+
zval_ptr_dtor(return_value);
464+
ZVAL_NULL(return_value);
465+
}
466+
451467
xmlFreeDoc(response);
452468
return true;
453469
}

‎ext/soap/php_soap.h‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,34 @@ void add_soap_fault(zval *obj, const char *fault_code, const char *fault_string,
202202

203203
ZEND_COLD void soap_throw_error(const char *format, ...) ZEND_ATTRIBUTE_FORMAT(printf, 1, 2);
204204

205+
typedef enum {
206+
SOAP_ENCODING_ERROR_FATAL,
207+
SOAP_ENCODING_ERROR_THROW,
208+
SOAP_ENCODING_ERROR_IGNORE,
209+
} soap_encoding_error_mode;
210+
211+
soap_encoding_error_mode soap_get_encoding_error_mode(void);
212+
213+
/* Reports an error in encoding or decoding a value. It is a fatal error, as with soap_errorN(E_ERROR, ...), except in a
214+
* call of a SoapClient that uses exceptions: there it is thrown as a SoapFault and does not bail out, so the encoder
215+
* must return a value its callers can handle. The call discards the result once encoding is done. */
216+
#define soap_encoding_error(...) do { \
217+
switch (soap_get_encoding_error_mode()) { \
218+
case SOAP_ENCODING_ERROR_FATAL: \
219+
php_error(E_ERROR, "SOAP-ERROR: " __VA_ARGS__); \
220+
break; \
221+
case SOAP_ENCODING_ERROR_THROW: \
222+
soap_throw_error(__VA_ARGS__); \
223+
break; \
224+
case SOAP_ENCODING_ERROR_IGNORE: \
225+
break; \
226+
} \
227+
} while (0)
228+
229+
/* An encoding error has been thrown as a SoapFault: the rest of the call is skipped */
230+
#define SOAP_ENCODING_ERROR_THROWN() \
231+
(UNEXPECTED(EG(exception)) && soap_get_encoding_error_mode() == SOAP_ENCODING_ERROR_IGNORE)
232+
205233
#define soap_error0(severity, format) \
206234
php_error(severity, "SOAP-ERROR: " format)
207235

‎ext/soap/soap.c‎

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2027,6 +2027,24 @@ ZEND_COLD void soap_throw_error(const char *format, ...) /* {{{ */
20272027
}
20282028
/* }}} */
20292029

2030+
/* Encoding errors are thrown only where a fatal error used to become a SoapFault: in a call of a SoapClient that uses
2031+
* exceptions. Only the first one is thrown, the encoding that is still running after it reports nothing. */
2032+
soap_encoding_error_mode soap_get_encoding_error_mode(void) /* {{{ */
2033+
{
2034+
zval *client = &SOAP_GLOBAL(error_object);
2035+
2036+
if (!Z_OBJ_P(client) ||
2037+
!instanceof_function(Z_OBJCE_P(client), soap_class_entry) ||
2038+
Z_TYPE_P(Z_CLIENT_EXCEPTIONS_P(client)) == IS_FALSE) {
2039+
return SOAP_ENCODING_ERROR_FATAL;
2040+
}
2041+
if (EG(exception) && instanceof_function(EG(exception)->ce, soap_fault_class_entry)) {
2042+
return SOAP_ENCODING_ERROR_IGNORE;
2043+
}
2044+
return SOAP_ENCODING_ERROR_THROW;
2045+
}
2046+
/* }}} */
2047+
20302048
/* SoapServer and SoapClient with the "exceptions" option disabled report
20312049
* errors as fatal errors. Turns the SoapFault thrown by soap_throw_error()
20322050
* into the same fatal error that soap_errorN(E_ERROR, ...) raises. */
@@ -2587,6 +2605,7 @@ static void do_soap_call(zend_execute_data *execute_data,
25872605
SOAP_GLOBAL(features) = 0;
25882606
}
25892607

2608+
ZVAL_NULL(&response);
25902609
zend_try {
25912610
if (sdl != NULL) {
25922611
fn = get_function(sdl, ZSTR_VAL(function), ZSTR_LEN(function));
@@ -2610,10 +2629,14 @@ static void do_soap_call(zend_execute_data *execute_data,
26102629
if (binding->bindingType == BINDING_SOAP) {
26112630
sdlSoapBindingFunctionPtr fnb = (sdlSoapBindingFunctionPtr)fn->bindingAttributes;
26122631
request = serialize_function_call(this_ptr, fn, NULL, fnb->input.ns, real_args, arg_count, soap_version, soap_headers);
2613-
ret = do_request(this_ptr, request, location_c_str, fnb->soapAction, soap_version, one_way, &response);
2632+
if (!SOAP_ENCODING_ERROR_THROWN()) {
2633+
ret = do_request(this_ptr, request, location_c_str, fnb->soapAction, soap_version, one_way, &response);
2634+
}
26142635
} else {
26152636
request = serialize_function_call(this_ptr, fn, NULL, sdl->target_ns, real_args, arg_count, soap_version, soap_headers);
2616-
ret = do_request(this_ptr, request, location_c_str, NULL, soap_version, one_way, &response);
2637+
if (!SOAP_ENCODING_ERROR_THROWN()) {
2638+
ret = do_request(this_ptr, request, location_c_str, NULL, soap_version, one_way, &response);
2639+
}
26172640
}
26182641

26192642
xmlFreeDoc(request);
@@ -2667,7 +2690,9 @@ static void do_soap_call(zend_execute_data *execute_data,
26672690
}
26682691
smart_str_0(&action);
26692692

2670-
ret = do_request(this_ptr, request, ZSTR_VAL(location), ZSTR_VAL(action.s), soap_version, 0, &response);
2693+
if (!SOAP_ENCODING_ERROR_THROWN()) {
2694+
ret = do_request(this_ptr, request, ZSTR_VAL(location), ZSTR_VAL(action.s), soap_version, 0, &response);
2695+
}
26712696

26722697
smart_str_free(&action);
26732698
xmlFreeDoc(request);
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
--TEST--
2+
GH-10026 (Garbage collection stops after exception in SoapClient): encoding errors
3+
--EXTENSIONS--
4+
soap
5+
--FILE--
6+
<?php
7+
class Node {
8+
public $self;
9+
}
10+
11+
function make_cycles(int $n): void {
12+
for ($i = 0; $i < $n; $i++) {
13+
$node = new Node;
14+
$node->self = $node;
15+
}
16+
}
17+
18+
class TestSoapClient extends SoapClient {
19+
public function __doRequest($request, $location, $action, $version, $oneWay = false, ?string $uriParserClass = null): string {
20+
return <<<'XML'
21+
<?xml version="1.0" encoding="UTF-8"?>
22+
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"
23+
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
24+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
25+
<SOAP-ENV:Body>
26+
<ns1:testResponse xmlns:ns1="urn:test">
27+
<return xsi:type="xsd:int">not a number</return>
28+
</ns1:testResponse>
29+
</SOAP-ENV:Body>
30+
</SOAP-ENV:Envelope>
31+
XML;
32+
}
33+
}
34+
35+
$client = new TestSoapClient(null, ['location' => 'test://', 'uri' => 'urn:test']);
36+
37+
// Encoding the request fails
38+
try {
39+
$client->test("\xff");
40+
} catch (SoapFault $e) {
41+
echo $e->faultcode, ": ", $e->getMessage(), "\n";
42+
}
43+
var_dump(gc_status()['protected']);
44+
45+
// Decoding the response fails
46+
try {
47+
$client->test("ok");
48+
} catch (SoapFault $e) {
49+
echo $e->faultcode, ": ", $e->getMessage(), "\n";
50+
}
51+
var_dump(gc_status()['protected']);
52+
53+
make_cycles(20000);
54+
gc_collect_cycles();
55+
var_dump(gc_status()['collected']);
56+
?>
57+
--EXPECT--
58+
Client: SOAP-ERROR: Encoding: string '\xff...' is not a valid utf-8 string
59+
bool(false)
60+
Client: SOAP-ERROR: Encoding: Invalid value for type 'int'
61+
bool(false)
62+
int(20000)
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
--TEST--
2+
SoapClient reports "Nesting level too deep" as SoapFault without a bailout
3+
--EXTENSIONS--
4+
soap
5+
--SKIPIF--
6+
<?php
7+
if (getenv('SKIP_ASAN')) {
8+
die('skip ASAN needs different stack limit setting due to more stack space usage');
9+
}
10+
?>
11+
--FILE--
12+
<?php
13+
class TestSoapClient extends SoapClient {
14+
public string $response;
15+
16+
public function __doRequest($request, $location, $action, $version, $oneWay = false, ?string $uriParserClass = null): string {
17+
return $this->response;
18+
}
19+
}
20+
21+
$client = new TestSoapClient(null, ['location' => 'test://', 'uri' => 'urn:test']);
22+
23+
// An array that contains itself 1 and 8 times: decoding stops at the first error
24+
foreach ([1, 8] as $n) {
25+
$client->response = '<?xml version="1.0"?>'
26+
. '<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"'
27+
. ' xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:xsd="http://www.w3.org/2001/XMLSchema"'
28+
. ' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><SOAP-ENV:Body><ns1:testResponse xmlns:ns1="urn:test">'
29+
. '<return xsi:type="SOAP-ENC:Array" SOAP-ENC:arrayType="xsd:anyType[' . $n . ']" id="x">'
30+
. str_repeat('<i href="#x"/>', $n)
31+
. '</return></ns1:testResponse></SOAP-ENV:Body></SOAP-ENV:Envelope>';
32+
try {
33+
$client->test();
34+
} catch (SoapFault $e) {
35+
echo "$n: $e->faultcode: $e->faultstring\n";
36+
}
37+
}
38+
var_dump(gc_status()['protected']);
39+
?>
40+
--EXPECT--
41+
1: Client: SOAP-ERROR: Encoding: Nesting level too deep
42+
8: Client: SOAP-ERROR: Encoding: Nesting level too deep
43+
bool(false)

0 commit comments

Comments
 (0)