Commit 083e1a6d79d for php

commit 083e1a6d79d8e913cec95f251c3c830709f5da92
Author: Ilia Alshanetsky <ilia@ilia.ws>
Date:   Thu Oct 29 15:22:47 2026 -0400

    ext/odbc: Fix double free on parameter description failure

    Register the result before describing its parameters, so its destructor
    can remove the correct connection entry on every failure path. Release it
    through odbc_result_free() when SQLDescribeParam() fails instead of freeing
    param_info separately and leaving it to be freed again by the destructor.

    Fixes GH-25036
    Closes GH-25044

diff --git a/NEWS b/NEWS
index 718e381b746..d84ff40a31a 100644
--- a/NEWS
+++ b/NEWS
@@ -125,6 +125,8 @@ PHP                                                                        NEWS
 - ODBC:
   . Fixed bug GH-24736 (php_odbc_connstr_is_quoted() treats a value with no
     closing brace as quoted). (lazerg)
+  . Fixed bug GH-25036 (odbc_prepare() corrupts the heap when the driver
+    cannot describe a parameter). (Ilia Alshanetsky)

 - Opcache:
   . Fixed tracing JIT returning a wrapped integer instead of a float on
diff --git a/ext/odbc/php_odbc.c b/ext/odbc/php_odbc.c
index 798245c4ca9..20302d56cb8 100644
--- a/ext/odbc/php_odbc.c
+++ b/ext/odbc/php_odbc.c
@@ -997,6 +997,7 @@ PHP_FUNCTION(odbc_prepare)
 	}
 	result->conn_ptr = conn;
 	result->fetched = 0;
+	odbc_insert_new_result(conn, return_value);

 	result->param_info = (odbc_param_info *) safe_emalloc(sizeof(odbc_param_info), result->numparams, 0);
 	for (i=0;i<result->numparams;i++) {
@@ -1004,14 +1005,11 @@ PHP_FUNCTION(odbc_prepare)
 													&result->param_info[i].scale, &result->param_info[i].nullable);
 		if (rc == SQL_ERROR) {
 			odbc_sql_error(result->conn_ptr, result->stmt, "SQLDescribeParameter");
-			SQLFreeStmt(result->stmt, SQL_RESET_PARAMS);
-			efree(result->param_info);
+			odbc_result_free(result);
 			zval_ptr_dtor(return_value);
 			RETURN_FALSE;
 		}
 	}
-
-	odbc_insert_new_result(conn, return_value);
 }
 /* }}} */

diff --git a/ext/odbc/tests/gh25036.phpt b/ext/odbc/tests/gh25036.phpt
new file mode 100644
index 00000000000..16b3226361c
--- /dev/null
+++ b/ext/odbc/tests/gh25036.phpt
@@ -0,0 +1,39 @@
+--TEST--
+GH-25036 (odbc_prepare() double free when SQLDescribeParam() fails)
+--EXTENSIONS--
+odbc
+--SKIPIF--
+<?php include 'skipif.inc'; ?>
+--FILE--
+<?php
+
+include 'config.inc';
+
+$conn = odbc_connect($dsn, $user, $pass);
+
+$live = odbc_exec($conn, 'SELECT 1 AS a');
+
+var_dump(odbc_prepare($conn, 'SELECT ? FROM gh25036_missing'));
+var_dump(odbc_prepare($conn, 'SELECT ?, ? FROM gh25036_missing'));
+
+var_dump(odbc_fetch_array($live));
+
+odbc_close($conn);
+
+try {
+    odbc_fetch_row($live);
+} catch (Error $e) {
+    echo $e::class, ": ", $e->getMessage(), PHP_EOL;
+}
+?>
+--EXPECTF--
+Warning: odbc_prepare(): SQL error: %s, SQL state %s in SQLDescribeParameter in %s on line %d
+bool(false)
+
+Warning: odbc_prepare(): SQL error: %s, SQL state %s in SQLDescribeParameter in %s on line %d
+bool(false)
+array(1) {
+  ["a"]=>
+  string(1) "1"
+}
+Error: ODBC result has already been closed