php / php/php-src

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

Open
#22,064 4 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Bug Extension: ffi Status: Needs Triage
Dominant language
C
Stars
40.4k
Forks
8.1k
Avg merge
2d 13h
Merged PRs (30d)
96

Description

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

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in ext/ffi/ffi.c at zend_ffi_add, zend_ffi_cdata_dtor, and FFI::new, then reproduce the supplied PHP script with USE_ZEND_ALLOC=0 under AddressSanitizer. Trace ownership and lifetime for array offsets; done means the offset remains safe after the base is unset and the reproducer no longer reports a use-after-free.

Written by the indexing model from the issue text.

Assessment

Tech stack
c, php
Domain
backend, security
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.