Skip to content

extract() with EXTR_REFS: use-after-free still present in modes other than EXTR_OVERWRITE (follow-up to #18209) #24730

Open

Description

@IzaiahSun

Description

The following code:

<?php
class Old { public function __destruct() { $GLOBALS['b'] = 43; } }
$b = new Old;
$array = ['b' => 42];
extract($array, EXTR_REFS | EXTR_IF_EXISTS);

Resulted in this output:

==72028==ERROR: AddressSanitizer: heap-use-after-free on address 0x604000028f90 at pc 0x000103a9b2f4 bp 0x00016d37fa90 sp 0x00016d37fa88
READ of size 4 at 0x604000028f90 thread T0
    #0 zend_gc_delref zend_types.h:1370
    #1 zend_objects_store_del zend_objects_API.c:182
    #2 rc_dtor_func zend_variables.c:57
    #3 i_zval_ptr_dtor zend_variables.h:45
    #4 zval_ptr_dtor zend_variables.c:84
    #5 php_extract_ref_if_exists array.c:1911
    #6 zif_extract array.c:2663
    #7 ZEND_DO_ICALL_SPEC_RETVAL_UNUSED_HANDLER zend_vm_execute.h:1287
    #8 execute_ex zend_vm_execute.h:58729
    #9 zend_execute zend_vm_execute.h:64381
    #10 zend_execute_script zend.c:1943

freed by thread T0 here:
    #0 free(libclang_rt.asan_osx_dynamic.dylib:arm64e)
    #1 __zend_free zend_alloc.c:3728
    #2 _efree zend_alloc.c:2889
    #3 zend_objects_store_del zend_objects_API.c:200
    #4 zend_object_release zend_objects_API.h:77
    #5 zend_objects_destroy_object zend_objects.c:209
    #6 zend_objects_store_del zend_objects_API.c:181
    #7 rc_dtor_func zend_variables.c:57
    #8 i_zval_ptr_dtor zend_variables.h:45
    #9 zval_ptr_dtor zend_variables.c:84
    #10 php_extract_ref_if_exists array.c:1911


SUMMARY: AddressSanitizer: heap-use-after-free zend_types.h:1370 in zend_gc_delref

But I expected this output instead:

(no output, clean exit)

Debug + ASan build, run with USE_ZEND_ALLOC=0.

The fix for #18209 was applied to php_extract_ref_overwrite() only. The five sibling
functions still destroy the old variable before overwriting the slot, so the same
use-after-free is reachable through the other EXTR_REFS modes.

ext/standard/array.c, e.g. php_extract_ref_if_exists():

zval_ptr_dtor(orig_var);
ZVAL_REF(orig_var, Z_REF_P(entry));

Affected: php_extract_ref_if_exists, php_extract_ref_prefix_if_exists,
php_extract_ref_prefix_all, php_extract_ref_prefix_same, php_extract_ref_prefix_invalid.
EXTR_REFS|EXTR_OVERWRITE is clean, as expected after #18209.

The other four modes crash the same way with the variable named to match the mode:

Mode Reproducer differs by
EXTR_REFS|EXTR_PREFIX_ALL, prefix p $p_b = new Old;
EXTR_REFS|EXTR_PREFIX_SAME, prefix p $b = 1; $p_b = new Old;
EXTR_REFS|EXTR_PREFIX_IF_EXISTS, prefix p $b = 1; $p_b = new Old;
EXTR_REFS|EXTR_PREFIX_INVALID, prefix p $p_0 = new Old; with [0 => 42]

PHP Version

PHP 8.4.28-dev (cli) (built: Oct 25 2026 01:02:13) (NTS DEBUG)
Copyright (c) The PHP Group
Zend Engine v4.4.28-dev, Copyright (c) Zend Technologies

Also reproduced on master (`29b5e168bf`):

PHP 8.7.0-dev (cli) (built: Oct 24 2026 21:13:17) (NTS DEBUG)
Copyright © The PHP Group and Contributors
Zend Engine v4.7.0-dev, Copyright © Zend by Perforce
    with Zend OPcache v8.7.0-dev, Copyright ©, by Zend by Perforce

Operating System

macOS 27.0.1 (arm64)

Activity

  1. self-assigned this
    on Oct 26, 2026
  2. iliaal commented on Oct 26, 2026

    @iliaal
    Member

    Fixed by #22696, which covers all five modes; each of them is valgrind-clean on current master with it applied. That PR is on hold until the general fix for reentrant destructors lands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions