php / php/php-src

ext/soap: heap use-after-free while encoding a `MultipleIterator`

Aperta
#22,895 0 commenti 0 reazioni 1 assegnatario Vedi su GitHub

@devnexen ci sta già lavorando.

Dal 27/7/2026.

  • #22896 di @devnexen — aperta
Bug Extension: soap Status: Verified
Lingua principale
C
Stelle
40.4k
Fork
8.1k
Merge medio
2g 13h
PR unite (30g)
96

Descrizione

Description
Summary

PHP's bundled SOAP extension can access a freed array while encoding a
MultipleIterator passed as an encoded SOAP array to
SoapClient::__soapCall(). The supplied PHP 8.4.20 ASan run demonstrates a
native heap-use-after-free in zend_gc_addref() after the SOAP encoder has
already destroyed the iterator's temporary current value. The demonstrated
consequence is PHP process termination under ASan and possible instability in
an unsanitized process.

Details
  • Affected component: bundled ext/soap in PHP 8.4.20, together with the
    built-in SPL MultipleIterator
  • Affected entry point: SoapClient::__soapCall() with a SoapVar using
    SOAP_ENC_ARRAY
  • Affected source: https://github.com/php/php-src, PHP 8.4.20 source;
    ext/soap/php_encoding.c and ext/spl/spl_observer.c
  • Root cause: the SOAP encoder ignores the result of
    array_set_zval_key(), destroys the iterator's current zval, and then
    unconditionally increments the reference count through that same pointer
  • Trigger condition: a MultipleIterator with at least one attached
    iterator is passed as a SOAP_ENC_ARRAY. Its current() and key() values
    are arrays, so the keyed insertion fails because an array cannot be used as a
    PHP array key.

In ext/soap/php_encoding.c:2289-2310, the generic Traversable encoder gets
the current value and key, inserts the value, and then performs unconditional
cleanup and reference counting:

while (iter->funcs->valid(iter) == SUCCESS) {
    val = iter->funcs->get_current_data(iter);
    if (iter->funcs->get_current_key) {
        zval key;
        iter->funcs->get_current_key(iter, &key);

        array_set_zval_key(Z_ARRVAL(array_copy), &key, val);
        zval_ptr_dtor(val);
        zval_ptr_dtor(&key);
    } else {
        add_next_index_zval(&array_copy, val);
    }
    Z_TRY_ADDREF_P(val);
    iter->funcs->move_forward(iter);
}

The return value of array_set_zval_key() is ignored. For the default
MultipleIterator configuration, key() returns an array containing the
sub-iterator keys, and current() returns an array containing the
sub-iterator values. array_set_zval_key() therefore rejects the array key.
The subsequent zval_ptr_dtor(val) releases the temporary array returned by
MultipleIterator::current(). The later Z_TRY_ADDREF_P(val) then reads the
freed array header to update its reference count.

The temporary arrays are created in
ext/spl/spl_observer.c:1288-1351 by spl_multiple_iterator_get_all():

array_init_size(return_value, num_elements);
...
if (SPL_MULTIPLE_ITERATOR_GET_ALL_CURRENT == get_type) {
    zend_call_known_instance_method_with_0_params(
        it->ce->iterator_funcs_ptr->zf_current, it, &retval);
} else {
    zend_call_known_instance_method_with_0_params(
        it->ce->iterator_funcs_ptr->zf_key, it, &retval);
}
...
add_next_index_zval(return_value, &retval);

MultipleIterator::current() and MultipleIterator::key() both call this
helper, so both results are newly constructed arrays. The ASan trace confirms
that the array allocated by spl_multiple_iterator_get_all() is freed by
to_xml_array() at line 2305 and subsequently accessed by the reference-count
increment at line 2310.

The PoC's __doRequest() override only returns an empty transport response so
that the test does not perform network I/O. It does not reenter the heap,
mutate the iterator, or participate in the invalid memory access.

PoC
Environment and configuration
  • Container/image: php-asan-8420-filter-audit2, image
    php-asan:8.4.20

  • PHP: PHP 8.4.20 CLI, NTS, DEBUG build

  • PHP source revision: PHP 8.4.20 source tree; the container did not
    retain a usable Git commit identifier

  • PHP compiler/linker flags:

CFLAGS='-fsanitize=address,undefined -fno-omit-frame-pointer -g -O0'
CXXFLAGS='-fsanitize=address,undefined -fno-omit-frame-pointer -g -O0'
LDFLAGS='-fsanitize=address,undefined -L/opt/openssl-3.2/lib64 -Wl,-rpath,/opt/openssl-3.2/lib64'
  • Extension: bundled ext/soap, enabled by --enable-soap and present
    in /src/php/sapi/cli/php; no separate SOAP module was loaded
  • Relevant native dependencies: OpenSSL libraries from
    /opt/openssl-3.2/lib64 were present through the recorded build and run
    environment; they are not involved in the vulnerable operation
  • Sanitizer runtime:
    LD_LIBRARY_PATH=/opt/openssl-3.2/lib64, USE_ZEND_ALLOC=0,
    ASAN_OPTIONS=detect_leaks=0:halt_on_error=1:abort_on_error=1, and
    UBSAN_OPTIONS=halt_on_error=1:abort_on_error=1
Reproducer

The complete PoC is retained as
SOAP-NEW-009_multiple_iterator_uaf_poc.php:

<?php

class LocalSoapClient extends SoapClient
{
    public function __doRequest(
        $request,
        $location,
        $action,
        $version,
        $one_way = false
    ): ?string {
        return '';
    }
}

$iterator = new MultipleIterator();
$iterator->attachIterator(new ArrayIterator([0]));

$client = new LocalSoapClient(null, [
    'location' => 'http://127.0.0.1/',
    'uri' => 'urn:audit',
]);

$client->__soapCall(
    'audit',
    [new SoapVar($iterator, SOAP_ENC_ARRAY)]
);

Run the PoC with the same sanitizer settings used for the captured evidence:

docker cp SOAP-NEW-009_multiple_iterator_uaf_poc.php \
  php-asan-8420-filter-audit2:/tmp/SOAP-NEW-009_multiple_iterator_uaf_poc.php
docker exec php-asan-8420-filter-audit2 sh -lc \
  'env LD_LIBRARY_PATH=/opt/openssl-3.2/lib64 \
   USE_ZEND_ALLOC=0 \
   ASAN_OPTIONS=detect_leaks=0:halt_on_error=1:abort_on_error=1 \
   UBSAN_OPTIONS=halt_on_error=1:abort_on_error=1 \
   /src/php/sapi/cli/php \
   /tmp/SOAP-NEW-009_multiple_iterator_uaf_poc.php \
   > /tmp/SOAP-NEW-009_multiple_iterator_uaf_asan.txt 2>&1'

The recorded command exited with status 134. The complete, unedited output
is retained as
SOAP-NEW-009_multiple_iterator_uaf_asan.txt.

Sanitizer report

The following excerpt is copied from the complete ASan output captured from
the minimal PoC:

=================================================================
==1623721==ERROR: AddressSanitizer: heap-use-after-free on address 0x506000082640 at pc 0x5604cfa03987 bp 0x7ffe9664c080 sp 0x7ffe9664c070
READ of size 4 at 0x506000082640 thread T0
    #0 0x5604cfa03986 in zend_gc_addref /src/php/Zend/zend_types.h:1340
    #1 0x5604cfa03deb in zval_addref_p /src/php/Zend/zend_types.h:1389
    #2 0x5604cfa2fce8 in to_xml_array /src/php/ext/soap/php_encoding.c:2310
freed by thread T0 here:
    #7 0x5604cfa2fbf3 in to_xml_array /src/php/ext/soap/php_encoding.c:2305
previously allocated by thread T0 here:
    #4 0x5604d0d01d02 in _zend_new_array /src/php/Zend/zend_hash.c:291
    #5 0x5604cfcdfd34 in spl_multiple_iterator_get_all /src/php/ext/spl/spl_observer.c:1301
    #6 0x5604cfce0d29 in zim_MultipleIterator_current /src/php/ext/spl/spl_observer.c:1368
SUMMARY: AddressSanitizer: heap-use-after-free /src/php/Zend/zend_types.h:1340 in zend_gc_addref
==1623721==ABORTING
Aborted
PHP Version
PHP 8.4.20 CLI, NTS, DEBUG build
Operating System

No response

Guida per i contributori

Apri la guida per i contributori

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Valutazione

Questa issue non è ancora stata valutata.

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.