Repository navigation
odbc_prepare(): double free of param_info when SQLDescribeParam() fails (heap corruption since 8.4) #25036
Description
Description
odbc_prepare() frees result->param_info twice when SQLDescribeParam() fails, which corrupts the heap. It happens on a normal error path: a statement whose parameters the driver cannot describe. With the Windows {SQL Server} driver (SQLSRV32), this includes a parameter marker used as a function argument, in a derived table, a CTE, TOP (?) or OFFSET ? ROWS.
Reproduced with PHP 8.5.12 (NTS and ZTS). By reading the source, PHP-8.4 and master have the same code. PHP 8.3.36 is not affected (tested).
The following code (PHP started with -n -d extension=odbc, no other extension):
<?php
// DSN: Driver={SQL Server};Server=<any SQL Server>;Database=master;Trusted_Connection=Yes;
$c = odbc_connect($argv[1], '', '');
echo PHP_VERSION, ' ', PHP_ZTS ? 'ZTS' : 'NTS', "\n";
// The {SQL Server} driver cannot describe a parameter marker used as a function
// argument: SQLPrepare() succeeds, SQLDescribeParam() returns SQL_ERROR.
var_dump(@odbc_prepare($c, 'SELECT UPPER(?) AS v'));
// Small allocations (timelib copies 16-byte timezone abbreviations) reuse the
// twice-freed slot.
for ($i = 0; $i < 100; $i++) {
$d = new DateTime('2025-04-01 10:00', new DateTimeZone('Europe/Paris'));
$s = $d->format('c T');
}
echo "END\n";Resulted in this output (PHP 8.5.12 NTS and ZTS, every run):
8.5.12 NTS
bool(false)
zend_mm_heap corrupted
The process exit code is 0xC0000409. With USE_ZEND_ALLOC=0, the process dies inside odbc_prepare() with 0xC0000374 (STATUS_HEAP_CORRUPTION), right after the SQLDescribeParameter warning.
But I expected this output instead (what PHP 8.3.36 prints, NTS and ZTS, with and without USE_ZEND_ALLOC=0):
8.3.36 NTS
bool(false)
END
Cause (line numbers from master at 118f06a, ext/odbc/php_odbc.c):
PHP_FUNCTION(odbc_prepare)setsresult->conn_ptr = conn;(line 953) before theSQLDescribeParam()loop.- On
SQL_ERROR, the error branch (lines 960-966) runsefree(result->param_info);without resetting the pointer, thenzval_ptr_dtor(return_value);. - That destroys the result object:
odbc_result_free_obj()(line 262) seesconn_ptrset and callsodbc_result_free(), which runsefree(res->param_info)a second time (line 252). odbc_result_free()then callszend_hash_index_del(&conn->results, res->index)(line 258) for a result that was never inserted (odbc_insert_new_result()was not reached). That would trip theZEND_ASSERT(status == SUCCESS)in a debug build, or remove another live result that has the same index.
This looks like a side effect of the resources-to-objects migration (#12040, PHP 8.4). In 8.3, the same branch frees param_info and result directly, with no object destructor. The other early-return paths of odbc_prepare() call zval_ptr_dtor() before conn_ptr is set, so they don't double free.
A possible fix: in the SQLDescribeParam() error branch, either don't free param_info (the destructor does it) and don't let the destructor delete an entry that was never inserted, or set conn_ptr only after the loop and release the statement and param_info explicitly there.
Environment of the reproduction: official Windows builds php-8.5.12-Win32-vs17-x64 (NTS and TS), {SQL Server} driver 10.0.26100.3624, SQL Server 2025 LocalDB 17.0.4025.3. The driver error text was localized (SQLSTATE 37000, "Syntax error or access violation"). Any driver/statement pair where SQLDescribeParam() returns SQL_ERROR should take the same path.
PHP Version
PHP 8.5.12 (cli) (built: Oct 26 2026 09:51:07) (NTS Visual C++ 2022 x64)
Copyright (c) The PHP Group
Built by The PHP Group
Zend Engine v4.5.12, Copyright (c) Zend Technologies
with Zend OPcache v8.5.12, Copyright (c), by Zend Technologies
Also reproduced with PHP 8.5.12 (cli) (built: Oct 26 2026 09:50:37) (ZTS Visual C++ 2022 x64).
Operating System
Windows 11 x64
Activity
NattyNarwhal commentedon Oct 29, 2026
Simplest fix is just setting param_info to null after, since the destructor already checks for a null pointer (in master at least)
iliaal commentedon Oct 29, 2026
I went a little bit a different route, still simple enough I think
ext/odbc: Fix double free on parameter description failure