You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Latent memory leak in ext/ffi:zend_ffi_make_pointer_type() when validate_vla fails #24530
zend_ffi_make_pointer_type() in ext/ffi/ffi.c allocates type via pemalloc() (line 6358) before calling zend_ffi_validate_vla(). If
validation fails the function calls zend_ffi_cleanup_dcl(dcl) and LONGJMP() without freeing type, leaking sizeof(zend_ffi_type) bytes.
Impact
Currently unreachable in practice. make_pointer_type is only called
from the pointer grammar rule, which runs before array_or_function_declarators; thus dcl->type never carries ZEND_FFI_ATTR_VLA at that point and validate_vla always succeeds.
However the pattern is inconsistent with the sibling functions zend_ffi_make_array_type() and zend_ffi_make_func_type(), which both pemallocafter all validation. A future refactor could accidentally
make the failure path reachable.
Correction: the leak is reachable. Claude Code discovered new way.
Claude only analyzed the zend_ffi_validate_vla() failure path (unreachable). The same function has a second bailout after the allocation: zend_ffi_finalize_type().
voidzend_ffi_make_pointer_type(zend_ffi_dcl*dcl)
{
zend_ffi_type*type=pemalloc(sizeof(zend_ffi_type), FFI_G(persistent)); // allocated first
...
zend_ffi_finalize_type(dcl); // can call zend_ffi_parser_error()
Claude overlooked last time that zend_ffi_parser_error() always ends in LONGJMP(FFI_G(bailout), FAILURE). It is called from zend_ffi_finalize_type() for unsupported specifier combinations. The generated parser (parse_pointer()) does not finalize the base type before calling zend_ffi_make_pointer_type(). When dcl->type is still unresolved at the first *, finalize_type() bails out with type allocated and not yet owned by anything.
Reproducer
<?phpFFI::cdef("double _Complex *x;"); // FFI\ParserException: Unsupported type _Complex at line 1
Measured on PHP 8.5.12 (NTS, release build Windows 11 26H2). Claude called FFI::cdef() 20000 times in a loop and compared memory_get_usage() before and after:
declaration
growth after 20000 calls
double _Complex *x;
1,920,000 bytes (96 B/call)
double _Complex **x;
1,920,000 bytes (bails at the first *)
double _Complex x; (no pointer, control)
0
int *x; (success, control)
0
On a debug build this is reported as a Zend MM leak at request end. With ffi.preload the allocation is persistent, so it is not reclaimed.
Why the originally proposed pefree() is not enough
Adding pefree(type, ...) before the LONGJMP in the validate_vla branch does not fix this. The bailout happens inside zend_ffi_finalize_type(), where type is not accessible.
Proposed fix
Allocate only after all checks have passed. This matches zend_ffi_make_array_type() and zend_ffi_make_func_type(), and needs no pefree().
type->attr depends only on dcl->attr and FFI_G(default_type_attr). zend_ffi_finalize_type() modifies neither of them, so moving it earlier doesn't change its behavior.
Safety check
Statically checked these vulnerabilities. But I have not compiled patched version yet.
Double free: none. type is not linked into dcl->type or any hash table, so zend_ffi_cleanup_dcl() never sees it.
Use-after-free: none. type is a local that has not been published yet.
Threads (ZTS):FFI_G is per-thread and the parser state is thread-local. type is not shared before it is stored in dcl->type.
I don't have enough development environment today, so I'll create PR later.
Activity
ndossche commentedon Oct 22, 2026
Feel free to open a PR!
matyapiro31 commentedon Oct 22, 2026
Correction: the leak is reachable. Claude Code discovered new way.
Claude only analyzed the
zend_ffi_validate_vla()failure path (unreachable). The same function has a second bailout after the allocation:zend_ffi_finalize_type().Claude overlooked last time that
zend_ffi_parser_error()always ends inLONGJMP(FFI_G(bailout), FAILURE). It is called fromzend_ffi_finalize_type()for unsupported specifier combinations. The generated parser (parse_pointer()) does not finalize the base type before callingzend_ffi_make_pointer_type(). Whendcl->typeis still unresolved at the first*,finalize_type()bails out withtypeallocated and not yet owned by anything.Reproducer
Measured on PHP 8.5.12 (NTS, release build Windows 11 26H2). Claude called
FFI::cdef()20000 times in a loop and comparedmemory_get_usage()before and after:double _Complex *x;double _Complex **x;*)double _Complex x;(no pointer, control)int *x;(success, control)On a debug build this is reported as a Zend MM leak at request end. With
ffi.preloadthe allocation is persistent, so it is not reclaimed.Why the originally proposed
pefree()is not enoughAdding
pefree(type, ...)before theLONGJMPin thevalidate_vlabranch does not fix this. The bailout happens insidezend_ffi_finalize_type(), wheretypeis not accessible.Proposed fix
Allocate only after all checks have passed. This matches
zend_ffi_make_array_type()andzend_ffi_make_func_type(), and needs nopefree().void zend_ffi_make_pointer_type(zend_ffi_dcl *dcl) { - zend_ffi_type *type = pemalloc(sizeof(zend_ffi_type), FFI_G(persistent)); - type->kind = ZEND_FFI_TYPE_POINTER; - type->attr = FFI_G(default_type_attr) | (dcl->attr & ZEND_FFI_POINTER_ATTRS); - type->size = sizeof(void*); - type->align = _Alignof(void*); + zend_ffi_type *type; + zend_ffi_finalize_type(dcl); if (zend_ffi_validate_vla(ZEND_FFI_TYPE(dcl->type)) == FAILURE) { zend_ffi_cleanup_dcl(dcl); LONGJMP(FFI_G(bailout), FAILURE); } + + type = pemalloc(sizeof(zend_ffi_type), FFI_G(persistent)); + type->kind = ZEND_FFI_TYPE_POINTER; + type->attr = FFI_G(default_type_attr) | (dcl->attr & ZEND_FFI_POINTER_ATTRS); + type->size = sizeof(void*); + type->align = _Alignof(void*); type->pointer.type = dcl->type;type->attrdepends only ondcl->attrandFFI_G(default_type_attr).zend_ffi_finalize_type()modifies neither of them, so moving it earlier doesn't change its behavior.Safety check
Statically checked these vulnerabilities. But I have not compiled patched version yet.
typeis not linked intodcl->typeor any hash table, sozend_ffi_cleanup_dcl()never sees it.typeis a local that has not been published yet.FFI_Gis per-thread and the parser state is thread-local.typeis not shared before it is stored indcl->type.I don't have enough development environment today, so I'll create PR later.
ndossche commentedon Oct 22, 2026
Proposal sounds good