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