[php-src] Issue #24582: Memory leaks in ext/ffi parser when zend_ffi_finalize_type() bails out (pointer / function / argument / nested declarators)
| From: | matyapiro31 | Date: | Fri, 23 Oct 2026 00:03:50 +0000 |
| Subject: | [php-src] Issue #24582: Memory leaks in ext/ffi parser when zend_ffi_finalize_type() bails out (pointer / function / argument / nested declarators) | ||
| Groups: | php.bugs | ||
| Request: | Send a blank email to php-bugs+get-253118@lists.php.net to get a copy of this message | ||
Issue: https://github.com/php/php-src/issues/24582
Author: matyapiro31
### Description
### Description
zend_ffi_finalize_type() calls zend_ffi_parser_error() for unsupported
specifier combinations (e.g. _Complex), and that always ends in
LONGJMP(FFI_G(bailout), FAILURE). Several parser callbacks call
zend_ffi_finalize_type() while they already own memory that is not yet reachable from
dcl->type, so that memory is never freed. Every failed FFI::cdef() /
FFI::new() / FFI::type() call leaks.
Related: #24530 (the validate_vla path of zend_ffi_make_pointer_type();
that particular path is not reachable, but the same function leaks through
zend_ffi_finalize_type(), as below).
### Reproducer
```php
<?php
$cases = [
'double _Complex *x;', // make_pointer_type
'double _Complex f(int a, int b);', // make_func_type (args)
'void f(int a, double _Complex x);', // add_arg (*args)
'double _Complex (x[3]);', // nested_declaration (nested_dcl)
];
foreach ($cases as $c) {
for ($i = 0; $i < 200; $i++) { try { FFI::cdef($c); } catch (Throwable) {} }
$a = memory_get_usage();
for ($i = 0; $i < 20000; $i++) { try { FFI::cdef($c); } catch (Throwable) {} }
printf("%-40s %.1f B/call\n", $c, (memory_get_usage() - $a) / 20000);
}
```
Measured on PHP 8.5.12 (NTS, release, Windows x64), 20000 calls each, all throw
FFI\ParserException: Unsupported type _Complex:
| declaration | leak per call | what leaks |
|---|---|---|
| double _Complex *x; | 96 B | type in
zend_ffi_make_pointer_type() (ffi.c:6358) |
| double _Complex f(int a, int b); | 216 B | args HashTable (+
nested_dcl) in zend_ffi_make_func_type() (ffi.c:6454) |
| void f(int a, double _Complex x); | 216 B | *args allocated before the
call in zend_ffi_add_arg() (ffi.c:6592 / 6595) |
| double _Complex (x[3]); | 96 B per array level | nested_dcl type in
zend_ffi_nested_declaration() (ffi.c:7091) |
| double _Complex (f(int a)); | 312 B | same, function type |
Controls that do not leak: double _Complex x;, double _Complex x[3];,
double _Complex (x);, int *x;, int (x[3]);.
With a debug build these are reported as Zend MM leaks at request end. With ffi.preload
the allocations are persistent and are never reclaimed.
### Cause
zend_ffi_finalize_type() only bails out when dcl->type == NULL, so
dcl itself owns nothing. But:
- zend_ffi_make_pointer_type() does pemalloc(type) and then calls
zend_ffi_finalize_type(dcl).
- zend_ffi_add_arg() does *args = pemalloc(...) and then calls
zend_ffi_finalize_type(arg_dcl).
- zend_ffi_make_func_type() calls zend_ffi_finalize_type(dcl) first, but
args and nested_dcl are already live. The later error paths in the same
function release them (zend_ffi_cleanup_dcl(nested_dcl),
zend_hash_destroy(args), pefree(args)), the finalize_type
path does not.
- zend_ffi_nested_declaration() calls zend_ffi_finalize_type(dcl) while
nested_dcl may already own a type built by the parenthesised declarator. Its own
failure path cleans nested_dcl, the finalize_type path does not.
zend_ffi_make_array_type() does not leak, because it calls
zend_ffi_finalize_type() before allocating.
### Suggested fix (not tested)
For make_pointer_type and add_arg, move the allocation after
zend_ffi_finalize_type(), as make_array_type already does.
For make_func_type and nested_declaration, the arguments cannot be moved.
Either clean up before the call, or make zend_ffi_finalize_type() return
zend_result using zend_ffi_throw_parser_error() instead of bailing (the
zend_ffi_validate_* helpers already follow this pattern), and let callers do:
```c
if (zend_ffi_finalize_type(dcl) == FAILURE) {
zend_ffi_cleanup_dcl(nested_dcl);
if (args) { zend_hash_destroy(args); pefree(args, FFI_G(persistent)); }
LONGJMP(FFI_G(bailout), FAILURE);
}
```
I can send a PR for whichever shape you prefer.
### PHP Version
```plain
PHP 8.5.12 (NTS, Visual C++ 2022 x64); the code in ext/ffi/ffi.c is unchanged on master.
```
### Operating System
Windows 11 25H2