Skip to content

SoapServer::handle() segfaults: HTTP_SOAPACTION lookup reads $_SERVER outside IS_ARRAY guard #22218

@Rex-Reynolds

Description

@Rex-Reynolds

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.

Metadata

Metadata

Assignees

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions