[php-src] Issue #24582: Memory leaks in ext/ffi parser when zend_ffi_finalize_type() bails out (pointer / function / argument / nested declarators)

From: 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

« previous php.bugs (#253118) next »