Commit f09a81ddee3 for php.net

commit f09a81ddee3e58a8c5af5b311a101b3c1b20a637
Author: Ilia Alshanetsky <ilia@ilia.ws>
Date:   Sat Aug 29 08:16:19 2026 -0400

    [SOAP] Fix WSDL cache corruption when header defines headerfaults

    sdl_serialize_soap_body() counted j headerfaults per header but then
    serialized body->headers instead of tmp->headerfaults, writing N header
    records where j fault records were expected by sdl_deserialize_soap_body(),
    misaligning the cache stream and crashing on load whenever a soap:header
    carries headerfaults. Iterate tmp->headerfaults instead; sibling audit of
    the other serialize/deserialize loops in php_sdl.c found no further
    hash-mismatched iteration. Bump WSDL_CACHE_VERSION so existing on-disk
    caches are discarded.

    Closes GH-23502

diff --git a/NEWS b/NEWS
index 921ca372b29..208ccaef35f 100644
--- a/NEWS
+++ b/NEWS
@@ -66,6 +66,8 @@ PHP                                                                        NEWS
 - SOAP:
   . Fixed bug GH-23447 (Segfault when a class passed to SoapServer::setClass()
     fails to initialize). (Lazizbek Ergashev)
+  . Fixed WSDL cache corruption when a soap:header defines headerfaults.
+    (Ilia Alshanetsky)

 - Standard:
   . Fixed a segfault when a stream filter callback unsets StreamBucket::$data
diff --git a/ext/soap/php_sdl.c b/ext/soap/php_sdl.c
index a44fc16f971..7aa9e9fae25 100644
--- a/ext/soap/php_sdl.c
+++ b/ext/soap/php_sdl.c
@@ -1155,7 +1155,7 @@ static sdlPtr load_wsdl(zval *this_ptr, char *struri)
 	return ctx.sdl;
 }

-#define WSDL_CACHE_VERSION 0x10
+#define WSDL_CACHE_VERSION 0x11

 #define WSDL_CACHE_GET(ret,type,buf)   memcpy(&ret,*buf,sizeof(type)); *buf += sizeof(type);
 #define WSDL_CACHE_GET_INT(ret,buf)    ret = ((unsigned char)(*buf)[0])|((unsigned char)(*buf)[1]<<8)|((unsigned char)(*buf)[2]<<16)|((unsigned)(*buf)[3]<<24); *buf += 4;
@@ -2066,7 +2066,7 @@ static void sdl_serialize_soap_body(const sdlSoapBindingFunctionBodyPtr body, co
 				sdlSoapBindingFunctionHeaderPtr tmp2;
 				const zend_string *key_inner;

-				ZEND_HASH_MAP_FOREACH_STR_KEY_PTR(body->headers, key_inner, tmp2) {
+				ZEND_HASH_MAP_FOREACH_STR_KEY_PTR(tmp->headerfaults, key_inner, tmp2) {
 					sdl_serialize_key(key_inner, out);
 					WSDL_CACHE_PUT_1(tmp2->use, out);
 					if (tmp2->use == SOAP_ENCODED) {
diff --git a/ext/soap/tests/headerfault_cache.phpt b/ext/soap/tests/headerfault_cache.phpt
new file mode 100644
index 00000000000..6da747902d8
--- /dev/null
+++ b/ext/soap/tests/headerfault_cache.phpt
@@ -0,0 +1,45 @@
+--TEST--
+WSDL cache corruption when soap:header has headerfaults
+--EXTENSIONS--
+soap
+--INI--
+soap.wsdl_cache_enabled=1
+--FILE--
+<?php
+$dir = __DIR__ . '/headerfault_cache_dir';
+@mkdir($dir);
+ini_set('soap.wsdl_cache_dir', $dir);
+
+$options = ['cache_wsdl' => WSDL_CACHE_DISK];
+
+$c1 = new SoapClient(__DIR__ . '/headerfault_cache.wsdl', $options);
+var_dump($c1->__getFunctions());
+
+$c2 = new SoapClient(__DIR__ . '/headerfault_cache.wsdl', $options);
+var_dump($c2->__getFunctions());
+
+echo "ok\n";
+?>
+--CLEAN--
+<?php
+$dir = __DIR__ . '/headerfault_cache_dir';
+if (is_dir($dir)) {
+    foreach (scandir($dir) as $f) {
+        if ($f === '.' || $f === '..') {
+            continue;
+        }
+        @unlink($dir . '/' . $f);
+    }
+    @rmdir($dir);
+}
+?>
+--EXPECT--
+array(1) {
+  [0]=>
+  string(32) "string testHeader(string $param)"
+}
+array(1) {
+  [0]=>
+  string(32) "string testHeader(string $param)"
+}
+ok
diff --git a/ext/soap/tests/headerfault_cache.wsdl b/ext/soap/tests/headerfault_cache.wsdl
new file mode 100644
index 00000000000..8a844c0b899
--- /dev/null
+++ b/ext/soap/tests/headerfault_cache.wsdl
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<wsdl:definitions xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" xmlns:tns="http://example.com/hf" xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" targetNamespace="http://example.com/hf">
+  <wsdl:message name="message">
+    <wsdl:part name="param" type="xsd:string"/>
+  </wsdl:message>
+  <wsdl:message name="authToken">
+    <wsdl:part name="authToken" type="xsd:string"/>
+  </wsdl:message>
+  <wsdl:message name="traceId">
+    <wsdl:part name="traceId" type="xsd:string"/>
+  </wsdl:message>
+  <wsdl:message name="faultToken">
+    <wsdl:part name="faultToken" type="xsd:string"/>
+  </wsdl:message>
+  <wsdl:message name="faultTrace">
+    <wsdl:part name="faultTrace" type="xsd:string"/>
+  </wsdl:message>
+  <wsdl:portType name="hf">
+    <wsdl:operation name="testHeader">
+      <wsdl:input message="tns:message"/>
+      <wsdl:output message="tns:message"/>
+    </wsdl:operation>
+  </wsdl:portType>
+  <wsdl:binding name="hfBinding" type="tns:hf">
+    <soap:binding style="rpc" transport="http://schemas.xmlsoap.org/soap/http"/>
+    <wsdl:operation name="testHeader">
+      <soap:operation soapAction="http://example.com/hf/testHeader"/>
+      <wsdl:input>
+        <soap:body namespace="http://example.com/hf" use="literal"/>
+        <soap:header namespace="http://example.com/hf" use="literal" part="authToken" message="tns:authToken">
+          <soap:headerfault namespace="http://example.com/hf" use="literal" part="faultToken" message="tns:faultToken"/>
+        </soap:header>
+        <soap:header namespace="http://example.com/hf" use="literal" part="traceId" message="tns:traceId">
+          <soap:headerfault namespace="http://example.com/hf" use="literal" part="faultTrace" message="tns:faultTrace"/>
+        </soap:header>
+      </wsdl:input>
+      <wsdl:output>
+        <soap:body namespace="http://example.com/hf" use="literal"/>
+      </wsdl:output>
+    </wsdl:operation>
+  </wsdl:binding>
+  <wsdl:service name="hfService">
+    <wsdl:port binding="tns:hfBinding" name="hfPort">
+      <soap:address location="http://localhost/hf.php"/>
+    </wsdl:port>
+  </wsdl:service>
+</wsdl:definitions>