Commit d044f28b808 for php
commit d044f28b808e13dcbf4ab3f7529e6e59d4327435
Author: David Carlier <devnexen@gmail.com>
Date: Thu Oct 1 19:12:01 2026 +0100
ext/soap: to_xml_array() heap use-after-free with illegal iterator keys.
Fix #22895
The return value of array_set_zval_key() was ignored, so when the key is
not a legal array offset, e.g. MultipleIterator::key() returning an
array, it threw and took no reference. The unconditional zval_ptr_dtor()
then dropped the iterator's only reference to the borrowed value and the
following Z_TRY_ADDREF_P() read freed memory. array_set_zval_key() now
also fails when a float or resource key diagnostic throws, and the loop
stops on failure.
Close GH-22896
diff --git a/NEWS b/NEWS
index 41ec25f4f1f..439f7c83f2c 100644
--- a/NEWS
+++ b/NEWS
@@ -167,6 +167,10 @@ PHP NEWS
. Fixed reconstructing a SimpleXMLElement freeing a child element that
another variable still references. (Ilia Alshanetsky)
+- SOAP:
+ . Fixed bug GH-22895 (Heap use-after-free while encoding a Traversable with
+ an illegal key). (David Carlier)
+
- Sockets:
. Fixed socket_select() silently truncating sets larger than FD_SETSIZE on
Windows. (David Carlier)
diff --git a/Zend/zend_API.c b/Zend/zend_API.c
index 2b3068dadef..2a61b689106 100644
--- a/Zend/zend_API.c
+++ b/Zend/zend_API.c
@@ -2252,6 +2252,9 @@ ZEND_API zend_result array_set_zval_key(HashTable *ht, zval *key, zval *value) /
break;
case IS_RESOURCE:
zend_use_resource_as_offset(key);
+ if (UNEXPECTED(EG(exception))) {
+ return FAILURE;
+ }
result = zend_hash_index_update(ht, Z_RES_HANDLE_P(key), value);
break;
case IS_FALSE:
@@ -2263,9 +2266,14 @@ ZEND_API zend_result array_set_zval_key(HashTable *ht, zval *key, zval *value) /
case IS_LONG:
result = zend_hash_index_update(ht, Z_LVAL_P(key), value);
break;
- case IS_DOUBLE:
- result = zend_hash_index_update(ht, zend_dval_to_lval_safe(Z_DVAL_P(key)), value);
+ case IS_DOUBLE: {
+ zend_long lval = zend_dval_to_lval_safe(Z_DVAL_P(key));
+ if (UNEXPECTED(EG(exception))) {
+ return FAILURE;
+ }
+ result = zend_hash_index_update(ht, lval, value);
break;
+ }
default:
zend_illegal_container_offset(ZSTR_KNOWN(ZEND_STR_ARRAY), key, BP_VAR_W);
result = NULL;
diff --git a/ext/soap/php_encoding.c b/ext/soap/php_encoding.c
index f60b2b877f5..b344af77b3f 100644
--- a/ext/soap/php_encoding.c
+++ b/ext/soap/php_encoding.c
@@ -2317,13 +2317,15 @@ static xmlNodePtr to_xml_array(encodeTypePtr type, zval *data, int style, xmlNod
if (EG(exception)) {
goto iterator_done;
}
- array_set_zval_key(Z_ARRVAL(array_copy), &key, val);
- zval_ptr_dtor(val);
+ zend_result status = array_set_zval_key(Z_ARRVAL(array_copy), &key, val);
zval_ptr_dtor(&key);
+ if (status == FAILURE) {
+ goto iterator_done;
+ }
} else {
+ Z_TRY_ADDREF_P(val);
add_next_index_zval(&array_copy, val);
}
- Z_TRY_ADDREF_P(val);
iter->funcs->move_forward(iter);
if (EG(exception)) {
diff --git a/ext/soap/tests/bugs/gh22895.phpt b/ext/soap/tests/bugs/gh22895.phpt
new file mode 100644
index 00000000000..a253f33d3da
--- /dev/null
+++ b/ext/soap/tests/bugs/gh22895.phpt
@@ -0,0 +1,96 @@
+--TEST--
+GH-22895 (Heap use-after-free while encoding a Traversable with an illegal key)
+--CREDITS--
+Amorsec
+--EXTENSIONS--
+soap
+--FILE--
+<?php
+class LocalSoapClient extends SoapClient
+{
+ public function __doRequest(
+ $request,
+ $location,
+ $action,
+ $version,
+ $one_way = false
+ ): ?string {
+ return '';
+ }
+}
+
+class ArrayKeyIterator implements Iterator
+{
+ private int $i = 0;
+
+ public function current(): mixed
+ {
+ return new stdClass();
+ }
+
+ public function key(): mixed
+ {
+ return ['illegal', 'key'];
+ }
+
+ public function next(): void
+ {
+ $this->i++;
+ }
+
+ public function rewind(): void
+ {
+ $this->i = 0;
+ }
+
+ public function valid(): bool
+ {
+ return $this->i < 2;
+ }
+}
+
+$client = new LocalSoapClient(null, [
+ 'location' => 'http://127.0.0.1/',
+ 'uri' => 'urn:audit',
+ 'trace' => 1,
+]);
+
+$multiple = new MultipleIterator();
+$multiple->attachIterator(new ArrayIterator([0]));
+
+foreach ([$multiple, new ArrayKeyIterator()] as $iterator) {
+ try {
+ $client->__soapCall('audit', [new SoapVar($iterator, SOAP_ENC_ARRAY)]);
+ } catch (TypeError $e) {
+ echo $e::class, ': ', $e->getMessage(), PHP_EOL;
+ }
+}
+
+function gen($key) {
+ yield $key => new stdClass();
+ yield 2 => new stdClass();
+}
+
+set_error_handler(function ($errno, $errstr) {
+ throw new Exception($errstr);
+});
+foreach ([1.5, STDIN] as $key) {
+ try {
+ $client->__soapCall('audit', [new SoapVar(gen($key), SOAP_ENC_ARRAY)]);
+ } catch (Exception $e) {
+ echo $e::class, ': ', $e->getMessage(), PHP_EOL;
+ }
+}
+restore_error_handler();
+
+/* A key the encoder can use must still be serialized, without leaking. */
+$client->__soapCall('audit', [new SoapVar(new ArrayIterator(['a' => 1]), SOAP_ENC_ARRAY)]);
+echo $client->__getLastRequest();
+?>
+--EXPECTF--
+TypeError: Cannot access offset of type array on array
+TypeError: Cannot access offset of type array on array
+Exception: Implicit conversion from float 1.5 to int loses precision
+Exception: Resource ID#%d used as offset, casting to integer (%d)
+<?xml version="1.0" encoding="UTF-8"?>
+<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns1="urn:audit" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><SOAP-ENV:Body><ns1:audit><param0 SOAP-ENC:arrayType="xsd:int[1]" xsi:type="SOAP-ENC:Array"><item xsi:type="xsd:int">1</item></param0></ns1:audit></SOAP-ENV:Body></SOAP-ENV:Envelope>