[php-src] Issue #22218: SoapServer::handle() segfaults: HTTP_SOAPACTION lookup reads $_SERVER outside IS_ARRAY guard
[email protected] (Rex-Reynolds) Wed, 3 Jun 2026 13:26:00 +0000
| Newsgroups | php.bugs |
|---|---|
| Message-ID | <[email protected]> |
Issue: https://github.com/php/php-src/issues/22218
Author: Rex-Reynolds
### Description
`SoapServer::handle()` reads `$_SERVER` for an `HTTP_SOAPACTION` entry **outside** the `IS_ARRAY` type guard that protects the surrounding code. If `$_SERVER`'s zval is not an array when `handle()` runs, `Z_ARRVAL_P()` reinterprets the zval's value union as a `HashTable*` and `zend_hash_str_find()` dereferences garbage → deterministic SIGSEGV.
In [`ext/soap/soap.c` (master)](https://github.com/php/php-src/blob/master/ext/soap/soap.c#L1389-L1420):
```c
if ((server_vars = zend_hash_find(&EG(symbol_table), server)) != NULL && Z_TYPE_P(server_vars) == IS_ARRAY) {
// HTTP_CONTENT_ENCODING handling — correctly guarded
} // <-- 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 `Z_TYPE_P(server_vars) == IS_ARRAY` check **and** the `!= NULL` check, so both a non-array and a NULL `server_vars` are crash paths.
### Reproduce
`server.php`:
```php
<?php
// $_SERVER is normally an array; force it to a scalar before handle() runs.
$_SERVER = 79; // IS_LONG; its value union (79 = 0x4f) is read as a HashTable*
$s = new SoapServer(null, ['uri' => 'http://localhost/repro']);
$s->handle();
```
`request.xml`:
```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 via the built-in server and POST the envelope:
```sh
php -S 0.0.0.0:8085 server.php &
curl --data @request.xml -H 'Content-Type: text/xml' http://127.0.0.1:8085/
```
Segfaults **10/10** on PHP 8.5.6 (the worker exits 139, curl gets an empty reply). On 8.4.21 the same script returns a normal HTTP 500 and the process survives — the `HTTP_SOAPACTION` lookup does not exist on 8.4.
gdb at the crash:
```
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 would be `rdi` on x86-64 — the faulting argument register holds `0x4f`, i.e. the scalar `79` interpreted as a pointer.)
### Bisect
Introduced by 63e0b9cc ("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 the type. First shipped in 8.5; 8.4 is unaffected.
### Real-world trigger
We did not hit this by overwriting `$_SERVER` ourselves. In production it was triggered by an APM extension (NewRelic PHP agent) whose observer hook invalidates the `$_SERVER` zval slot across the `handle()` call, leaving a non-array zval at lookup time. The scalar-assignment repro above isolates the same fault with no extension loaded.
### Suggested fix
Move the `HTTP_SOAPACTION` lookup inside the existing `IS_ARRAY` guard, or re-test `server_vars`:
```c
if (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);
}
```
### PHP Version
PHP 8.5.6 (also present on master). Not present in 8.4.x.
### Operating System
Reproduced in the official `php:8.5-cli` Docker image (Debian, aarch64 and confirmed crash signature matches an x86-64 production host).