[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).