Description
SoapServer::handle() reads $_SERVER for an HTTP_SOAPACTION entry outside the NULL/IS_ARRAY guard that protects the surrounding code. If $_SERVER's symbol-table entry is missing (NULL) or not an array when handle() runs, Z_ARRVAL_P() reinterprets a non-array zval's value union as a HashTable* (or dereferences NULL) and zend_hash_str_find() faults → deterministic SIGSEGV.
In ext/soap/soap.c (master):
if ((server_vars = zend_hash_find(&EG(symbol_table), server)) != NULL && Z_TYPE_P(server_vars) == IS_ARRAY) {
// HTTP_CONTENT_ENCODING handling — correctly guarded
} // <-- NULL + IS_ARRAY guard ends here
// server_vars may be NULL or non-array here, but is dereferenced anyway:
if ((soap_action_z = zend_hash_str_find(Z_ARRVAL_P(server_vars), ZEND_STRL("HTTP_SOAPACTION"))) != NULL
&& Z_TYPE_P(soap_action_z) == IS_STRING) {
soap_action = Z_STRVAL_P(soap_action_z);
}
The HTTP_SOAPACTION lookup is outside both the server_vars != NULL check and the Z_TYPE_P(server_vars) == IS_ARRAY check that guard everything above it, so both a NULL and a non-array server_vars are crash paths.
Reproduce
Two minimal triggers, no extensions required. Each forces $_SERVER into a state the lookup doesn't expect.
s1.php — non-array $_SERVER:
<?php
$_SERVER = 79; // IS_LONG; its value union (79 = 0x4f) is read as a HashTable*
$s = new SoapServer(null, ['uri' => 'http://localhost/repro']);
$s->handle();
s2.php — missing $_SERVER (NULL lookup):
<?php
unset($_SERVER); // server_vars = zend_hash_find(...) returns NULL
$s = new SoapServer(null, ['uri' => 'http://localhost/repro']);
$s->handle();
request.xml:
<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body><Ping xmlns="http://localhost/repro"/></soap:Body>
</soap:Envelope>
Serve and POST the envelope:
php -S 0.0.0.0:8085 s1.php &
curl --data @request.xml -H 'Content-Type: text/xml' http://127.0.0.1:8085/
On PHP 8.5.6 both scripts segfault the worker (exit 139, empty reply); s1 reproduces 10/10. On 8.4.21 the same scripts return a normal HTTP 500 and the process survives — the HTTP_SOAPACTION lookup does not exist on 8.4.
gdb at the crash for s1:
Program received signal SIGSEGV, Segmentation fault.
#0 zend_hash_str_find ()
#1 zim_SoapServer_handle () from soap.so
#2 execute_ex ()
...
x0 0x4f 79 <- the IS_LONG value, passed as HashTable*
(x0 on aarch64; this is rdi on x86-64 — the faulting argument register holds 0x4f, i.e. the scalar 79 interpreted as a pointer.)
Bisect
Introduced by 63e0b9c ("Fix #49169: SoapServer calls wrong function, although 'SOAP action' header is correct", 2024-09-20), which added the HTTP_SOAPACTION lookup after the guarded block without re-checking server_vars. First shipped in 8.5; 8.4 is unaffected.
Real-world trigger
We did not hit this by mutating $_SERVER ourselves. In production it was triggered by an APM extension (NewRelic PHP agent) whose observer hook leaves a non-array zval in $_SERVER's symbol-table slot across the handle() call. The two scripts above isolate the same fault with no extension loaded.
Suggested fix and verification
Apply the same NULL + IS_ARRAY guard the surrounding code already uses (note: a bare Z_TYPE_P(server_vars) == IS_ARRAY is not sufficient — Z_TYPE_P itself dereferences server_vars, so the NULL path still crashes; the server_vars != NULL short-circuit is required):
if (server_vars != NULL && Z_TYPE_P(server_vars) == IS_ARRAY
&& (soap_action_z = zend_hash_str_find(Z_ARRVAL_P(server_vars), ZEND_STRL("HTTP_SOAPACTION"))) != NULL
&& Z_TYPE_P(soap_action_z) == IS_STRING) {
soap_action = Z_STRVAL_P(soap_action_z);
}
(Equivalently, move the lookup inside the existing if (server_vars != NULL && Z_TYPE_P(server_vars) == IS_ARRAY) { ... } block, which already holds exactly this guard.)
I built PHP-8.5 with this change and re-ran both repros against the patched binary:
| Repro |
Stock 8.5.6 |
Patched (this fix) |
s1 non-array $_SERVER |
SIGSEGV (exit 139) |
HTTP 500, worker survives |
s2 NULL $_SERVER |
SIGSEGV (exit 139) |
HTTP 500, worker survives |
PHP Version
PHP 8.5.6 (also present on master / 8.5.8-dev). Not present in 8.4.x.
Operating System
Reproduced in the official php:8.5-cli Docker image (Debian); crash signature matches an x86-64 production host. Fix verified by compiling PHP-8.5 in debian:bookworm.
Description
SoapServer::handle()reads$_SERVERfor anHTTP_SOAPACTIONentry outside theNULL/IS_ARRAYguard that protects the surrounding code. If$_SERVER's symbol-table entry is missing (NULL) or not an array whenhandle()runs,Z_ARRVAL_P()reinterprets a non-array zval's value union as aHashTable*(or dereferences NULL) andzend_hash_str_find()faults → deterministic SIGSEGV.In
ext/soap/soap.c(master):The
HTTP_SOAPACTIONlookup is outside both theserver_vars != NULLcheck and theZ_TYPE_P(server_vars) == IS_ARRAYcheck that guard everything above it, so both a NULL and a non-arrayserver_varsare crash paths.Reproduce
Two minimal triggers, no extensions required. Each forces
$_SERVERinto a state the lookup doesn't expect.s1.php— non-array$_SERVER:s2.php— missing$_SERVER(NULL lookup):request.xml:Serve and POST the envelope:
On PHP 8.5.6 both scripts segfault the worker (exit 139, empty reply);
s1reproduces 10/10. On 8.4.21 the same scripts return a normal HTTP 500 and the process survives — theHTTP_SOAPACTIONlookup does not exist on 8.4.gdb at the crash for
s1:(
x0on aarch64; this isrdion x86-64 — the faulting argument register holds0x4f, i.e. the scalar79interpreted as a pointer.)Bisect
Introduced by 63e0b9c ("Fix #49169: SoapServer calls wrong function, although 'SOAP action' header is correct", 2024-09-20), which added the
HTTP_SOAPACTIONlookup after the guarded block without re-checkingserver_vars. First shipped in 8.5; 8.4 is unaffected.Real-world trigger
We did not hit this by mutating
$_SERVERourselves. In production it was triggered by an APM extension (NewRelic PHP agent) whose observer hook leaves a non-array zval in$_SERVER's symbol-table slot across thehandle()call. The two scripts above isolate the same fault with no extension loaded.Suggested fix and verification
Apply the same
NULL+IS_ARRAYguard the surrounding code already uses (note: a bareZ_TYPE_P(server_vars) == IS_ARRAYis not sufficient —Z_TYPE_Pitself dereferencesserver_vars, so the NULL path still crashes; theserver_vars != NULLshort-circuit is required):(Equivalently, move the lookup inside the existing
if (server_vars != NULL && Z_TYPE_P(server_vars) == IS_ARRAY) { ... }block, which already holds exactly this guard.)I built
PHP-8.5with this change and re-ran both repros against the patched binary:s1non-array$_SERVERs2NULL$_SERVERPHP Version
PHP 8.5.6 (also present on master / 8.5.8-dev). Not present in 8.4.x.
Operating System
Reproduced in the official
php:8.5-cliDocker image (Debian); crash signature matches an x86-64 production host. Fix verified by compilingPHP-8.5indebian:bookworm.