Commit b97f7881de0 for php
commit b97f7881de0f806c7294a2bd00015b0d287ca59b
Author: Matyapiro31 <matyapiro31@gmail.com>
Date: Sat Oct 24 02:16:36 2026 +0000
Fix GH-24582: Memory leaks in ext/ffi parser when finalize_type() bails out
zend_ffi_finalize_type() reported unsupported type specifier combinations
(e.g. _Complex) through zend_ffi_parser_error(), which longjmp()s out of the
parser. Several callbacks (zend_ffi_make_pointer_type(),
zend_ffi_make_func_type(), zend_ffi_add_arg(), zend_ffi_nested_declaration())
call it while already holding memory that is not owned by the declaration
yet, so that memory was leaked on every failed
FFI::cdef()/FFI::new()/FFI::type().
Do not bail out from zend_ffi_finalize_type(). Report the error, set
FFI_G(parse_failed) and continue with a placeholder type. Later parser errors
are not reported again, and zend_ffi_parse_decl()/zend_ffi_parse_type()
return FAILURE once parsing is done, so every callback can finish and release
what it owns through its normal path.
Co-authored-by: David Carlier <devnexen@gmail.com>
Close GH-24630
diff --git a/NEWS b/NEWS
index 041d775722e..e07d065e385 100644
--- a/NEWS
+++ b/NEWS
@@ -46,6 +46,10 @@ PHP NEWS
. Fixed DOMXPath crash wrapping a foreign node after a nodeset callback.
(Ilia Alshanetsky)
+- FFI:
+ . Fixed bug GH-24582 (Memory leaks in ext/ffi parser when finalize_type()
+ bails out). (matyapiro31, David Carlier)
+
- FPM:
. Fixed bug GH-24229 (fcgi_close() passes shutdown() a literal 1, which is
SHUT_RD in WASI). (Ryosuke Ishibashi)
diff --git a/ext/ffi/ffi.c b/ext/ffi/ffi.c
index 79783441c1d..73e738202c8 100644
--- a/ext/ffi/ffi.c
+++ b/ext/ffi/ffi.c
@@ -3644,6 +3644,11 @@ static void zend_ffi_throw_parser_error(const char *format, ...) /* {{{ */
va_list va;
char *message = NULL;
+ if (FFI_G(parse_failed)) {
+ /* An error was already reported, parsing only continues to unwind cleanly */
+ return;
+ }
+
va_start(va, format);
zend_vspprintf(&message, 0, format, va);
@@ -5752,20 +5757,22 @@ ZEND_GET_MODULE(ffi)
/* parser callbacks */
void zend_ffi_parser_error(const char *format, ...) /* {{{ */
{
- va_list va;
- char *message = NULL;
+ if (!FFI_G(parse_failed)) {
+ va_list va;
+ char *message = NULL;
- va_start(va, format);
- zend_vspprintf(&message, 0, format, va);
+ va_start(va, format);
+ zend_vspprintf(&message, 0, format, va);
- if (EG(current_execute_data)) {
- zend_throw_exception(zend_ffi_parser_exception_ce, message, 0);
- } else {
- zend_error(E_WARNING, "FFI Parser: %s", message);
- }
+ if (EG(current_execute_data)) {
+ zend_throw_exception(zend_ffi_parser_exception_ce, message, 0);
+ } else {
+ zend_error(E_WARNING, "FFI Parser: %s", message);
+ }
- efree(message);
- va_end(va);
+ efree(message);
+ va_end(va);
+ }
LONGJMP(FFI_G(bailout), FAILURE);
}
@@ -5851,10 +5858,16 @@ static void zend_ffi_finalize_type(zend_ffi_dcl *dcl) /* {{{ */
case ZEND_FFI_DCL_FLOAT|ZEND_FFI_DCL_COMPLEX:
case ZEND_FFI_DCL_DOUBLE|ZEND_FFI_DCL_COMPLEX:
case ZEND_FFI_DCL_DOUBLE|ZEND_FFI_DCL_LONG|ZEND_FFI_DCL_COMPLEX:
- zend_ffi_parser_error("Unsupported type _Complex at line %d", FFI_G(line));
+ zend_ffi_throw_parser_error("Unsupported type _Complex at line %d", FFI_G(line));
+ /* Do not bail out here: callers may hold memory that is not owned by dcl yet.
+ * Continue with a placeholder type, zend_ffi_parse_*() report the failure. */
+ FFI_G(parse_failed) = 1;
+ dcl->type = (zend_ffi_type*)&zend_ffi_type_sint32;
break;
default:
- zend_ffi_parser_error("Unsupported type specifier combination at line %d", FFI_G(line));
+ zend_ffi_throw_parser_error("Unsupported type specifier combination at line %d", FFI_G(line));
+ FFI_G(parse_failed) = 1;
+ dcl->type = (zend_ffi_type*)&zend_ffi_type_sint32;
break;
}
dcl->flags &= ~ZEND_FFI_DCL_TYPE_SPECIFIERS;
diff --git a/ext/ffi/ffi.g b/ext/ffi/ffi.g
index 36483724289..8102e8f73a8 100644
--- a/ext/ffi/ffi.g
+++ b/ext/ffi/ffi.g
@@ -890,9 +890,15 @@ zend_result zend_ffi_parse_decl(const char *str, size_t len) {
FFI_G(attribute_parsing) = 0;
yy_buf = (unsigned char*)str;
yy_end = yy_buf + len;
+ FFI_G(parse_failed) = 0;
parse();
+ if (FFI_G(parse_failed)) {
+ FFI_G(parse_failed) = 0;
+ return FAILURE;
+ }
return SUCCESS;
} else {
+ FFI_G(parse_failed) = 0;
return FAILURE;
}
}
@@ -906,13 +912,19 @@ zend_result zend_ffi_parse_type(const char *str, size_t len, zend_ffi_dcl *dcl)
yy_pos = yy_text = yy_buf = (unsigned char*)str;
yy_end = yy_buf + len;
yy_line = 1;
+ FFI_G(parse_failed) = 0;
sym = parse_type_name(get_sym(), dcl);
if (sym != YY_EOF) {
yy_error_sym("<EOF> expected, got", sym);
}
zend_ffi_validate_type_name(dcl);
+ if (FFI_G(parse_failed)) {
+ FFI_G(parse_failed) = 0;
+ return FAILURE;
+ }
return SUCCESS;
} else {
+ FFI_G(parse_failed) = 0;
return FAILURE;
};
}
diff --git a/ext/ffi/ffi_parser.c b/ext/ffi/ffi_parser.c
index 26d623a4029..bda13d323ed 100644
--- a/ext/ffi/ffi_parser.c
+++ b/ext/ffi/ffi_parser.c
@@ -3623,9 +3623,15 @@ zend_result zend_ffi_parse_decl(const char *str, size_t len) {
FFI_G(attribute_parsing) = 0;
yy_buf = (unsigned char*)str;
yy_end = yy_buf + len;
+ FFI_G(parse_failed) = 0;
parse();
+ if (FFI_G(parse_failed)) {
+ FFI_G(parse_failed) = 0;
+ return FAILURE;
+ }
return SUCCESS;
} else {
+ FFI_G(parse_failed) = 0;
return FAILURE;
}
}
@@ -3639,13 +3645,19 @@ zend_result zend_ffi_parse_type(const char *str, size_t len, zend_ffi_dcl *dcl)
yy_pos = yy_text = yy_buf = (unsigned char*)str;
yy_end = yy_buf + len;
yy_line = 1;
+ FFI_G(parse_failed) = 0;
sym = parse_type_name(get_sym(), dcl);
if (sym != YY_EOF) {
yy_error_sym("<EOF> expected, got", sym);
}
zend_ffi_validate_type_name(dcl);
+ if (FFI_G(parse_failed)) {
+ FFI_G(parse_failed) = 0;
+ return FAILURE;
+ }
return SUCCESS;
} else {
+ FFI_G(parse_failed) = 0;
return FAILURE;
};
}
diff --git a/ext/ffi/php_ffi.h b/ext/ffi/php_ffi.h
index 430b8a2e568..69ee099528f 100644
--- a/ext/ffi/php_ffi.h
+++ b/ext/ffi/php_ffi.h
@@ -57,6 +57,7 @@ ZEND_BEGIN_MODULE_GLOBALS(ffi)
bool allow_vla;
bool attribute_parsing;
bool persistent;
+ bool parse_failed;
uint32_t default_type_attr;
ZEND_END_MODULE_GLOBALS(ffi)
diff --git a/ext/ffi/tests/finalize_type_bailout_leak.phpt b/ext/ffi/tests/finalize_type_bailout_leak.phpt
new file mode 100644
index 00000000000..11b487806b0
--- /dev/null
+++ b/ext/ffi/tests/finalize_type_bailout_leak.phpt
@@ -0,0 +1,64 @@
+--TEST--
+FFI: no memory leak when zend_ffi_finalize_type() bails out inside a declarator
+--EXTENSIONS--
+ffi
+--INI--
+ffi.enable=1
+--FILE--
+<?php
+
+function leaks(string $code): bool {
+ $n = 200;
+ for ($i = 0; $i < 20; $i++) {
+ try { FFI::cdef($code); } catch (FFI\ParserException) {}
+ }
+ $before = memory_get_usage();
+ for ($i = 0; $i < $n; $i++) {
+ try { FFI::cdef($code); } catch (FFI\ParserException) {}
+ }
+ /* A leaked block is at least 16 bytes, so this catches a leak per call */
+ return memory_get_usage() - $before >= $n;
+}
+
+$cases = [
+ // zend_ffi_make_pointer_type()
+ 'double _Complex *x;',
+ 'double _Complex **x;',
+ // zend_ffi_make_func_type()
+ 'double _Complex f(int a, int b);',
+ // zend_ffi_add_arg(): first and later argument
+ 'void f(double _Complex x);',
+ 'void f(int a, double _Complex x);',
+ // zend_ffi_nested_declaration()
+ 'double _Complex (x[3]);',
+ 'double _Complex (f(int a));',
+ // parsing continues after the error: only the first one is reported
+ 'double _Complex x; int y;',
+ 'struct S { double _Complex a; int b; };',
+ 'int x; double _Complex y; double _Complex z;',
+];
+
+foreach ($cases as $code) {
+ try {
+ FFI::cdef($code);
+ echo "no error: $code\n";
+ } catch (FFI\ParserException $e) {
+ echo $e->getMessage(), "\n";
+ }
+ if (leaks($code)) {
+ echo "leak: $code\n";
+ }
+}
+
+?>
+--EXPECT--
+Unsupported type _Complex at line 1
+Unsupported type _Complex at line 1
+Unsupported type _Complex at line 1
+Unsupported type _Complex at line 1
+Unsupported type _Complex at line 1
+Unsupported type _Complex at line 1
+Unsupported type _Complex at line 1
+Unsupported type _Complex at line 1
+Unsupported type _Complex at line 1
+Unsupported type _Complex at line 1