Commit 2dc6626007f for php
commit 2dc6626007f9e2ad02eb44cb4c40301a2caed6fd
Author: Ilia Alshanetsky <ilia@ilia.ws>
Date: Thu Sep 17 18:50:52 2026 -0400
Fix GH-23741: pdo_dblib use-after-free of statement error state
pdo_dblib_stmt_execute() published &S->err to the DBPROCESS through
dbsetuserdata(), but FreeTDS keeps handing that pointer to the error and
message handlers for the life of the connection, long after the statement is
freed. A statement cannot retract it from its own destructor, since a GC
cycle can free the connection handle first, so every function that hands
H->link to libdblib now installs the pdo_dblib_err it owns. get_column_meta()
also stops allocating return_value before the dbcoltypeinfo() check that can
fail, which the new test would otherwise leak.
Fixes GH-23741
Closes GH-23751
diff --git a/NEWS b/NEWS
index 1bbf5df6062..a4b3101252e 100644
--- a/NEWS
+++ b/NEWS
@@ -117,6 +117,12 @@ PHP NEWS
. Fixed PDOStatement::bindParam() and bindColumn() leaking the driver
options value. (Ilia Alshanetsky)
+- PDO_DBLIB:
+ . Fixed bug GH-23741 (segfault after a failed query inside a PDO
+ transaction). Errors raised by beginTransaction(), commit(), rollBack()
+ and lastInsertId() are now reported instead of being dropped.
+ (Ilia Alshanetsky)
+
- PDO_Firebird:
. Fixed bug GH-23758 (PDO_Firebird returns null for non-null empty BLOBs).
(Lazizbek Ergashev)
diff --git a/ext/pdo_dblib/dblib_driver.c b/ext/pdo_dblib/dblib_driver.c
index f81e9e7397f..dd8fb1c320c 100644
--- a/ext/pdo_dblib/dblib_driver.c
+++ b/ext/pdo_dblib/dblib_driver.c
@@ -77,11 +77,12 @@ static void dblib_handle_closer(pdo_dbh_t *dbh)
pdo_dblib_db_handle *H = (pdo_dblib_db_handle *)dbh->driver_data;
if (H) {
- pdo_dblib_err_dtor(&H->err);
if (H->link) {
+ dbsetuserdata(H->link, (BYTE*) &H->err);
dbclose(H->link);
H->link = NULL;
}
+ pdo_dblib_err_dtor(&H->err);
if (H->login) {
dbfreelogin(H->login);
H->login = NULL;
@@ -202,6 +203,8 @@ static bool pdo_dblib_transaction_cmd(const char *cmd, pdo_dbh_t *dbh)
{
pdo_dblib_db_handle *H = (pdo_dblib_db_handle *)dbh->driver_data;
+ dbsetuserdata(H->link, (BYTE*) &H->err);
+
if (FAIL == dbcmd(H->link, cmd)) {
return false;
}
@@ -241,6 +244,8 @@ zend_string *dblib_handle_last_id(pdo_dbh_t *dbh, const zend_string *name)
* Would use scope_identity() but it's not implemented on Sybase
*/
+ dbsetuserdata(H->link, (BYTE*) &H->err);
+
if (FAIL == dbcmd(H->link, "SELECT @@IDENTITY")) {
return NULL;
}
diff --git a/ext/pdo_dblib/dblib_stmt.c b/ext/pdo_dblib/dblib_stmt.c
index e6e91b60fa2..32dd3d261c5 100644
--- a/ext/pdo_dblib/dblib_stmt.c
+++ b/ext/pdo_dblib/dblib_stmt.c
@@ -95,6 +95,8 @@ static int pdo_dblib_stmt_cursor_closer(pdo_stmt_t *stmt)
pdo_dblib_stmt *S = (pdo_dblib_stmt*)stmt->driver_data;
pdo_dblib_db_handle *H = S->H;
+ dbsetuserdata(H->link, (BYTE*) &S->err);
+
/* Cancel any pending results */
dbcancel(H->link);
@@ -152,6 +154,8 @@ static int pdo_dblib_stmt_next_rowset(pdo_stmt_t *stmt)
pdo_dblib_db_handle *H = S->H;
RETCODE ret = SUCCESS;
+ dbsetuserdata(H->link, (BYTE*) &S->err);
+
/* Ideally use dbcanquery here, but there is a bug in FreeTDS's implementation of dbcanquery
* It has been resolved but is currently only available in nightly builds
*/
@@ -201,6 +205,8 @@ static int pdo_dblib_stmt_fetch(pdo_stmt_t *stmt,
pdo_dblib_stmt *S = (pdo_dblib_stmt*)stmt->driver_data;
pdo_dblib_db_handle *H = S->H;
+ dbsetuserdata(H->link, (BYTE*) &S->err);
+
ret = dbnextrow(H->link);
if (FAIL == ret) {
@@ -226,6 +232,8 @@ static int pdo_dblib_stmt_describe(pdo_stmt_t *stmt, int colno)
return FAILURE;
}
+ dbsetuserdata(H->link, (BYTE*) &S->err);
+
if (colno == 0) {
S->computed_column_name_count = 0;
}
@@ -350,6 +358,8 @@ static int pdo_dblib_stmt_get_col(pdo_stmt_t *stmt, int colno, zval *zv, enum pd
DBCHAR *tmp_data;
DBINT data_len, tmp_data_len;
+ dbsetuserdata(H->link, (BYTE*) &S->err);
+
coltype = dbcoltype(H->link, colno+1);
data = dbdata(H->link, colno+1);
data_len = dbdatlen(H->link, colno+1);
@@ -472,7 +482,7 @@ static int pdo_dblib_stmt_get_column_meta(pdo_stmt_t *stmt, zend_long colno, zva
return FAILURE;
}
- array_init(return_value);
+ dbsetuserdata(H->link, (BYTE*) &S->err);
dbtypeinfo = dbcoltypeinfo(H->link, colno+1);
@@ -480,6 +490,8 @@ static int pdo_dblib_stmt_get_column_meta(pdo_stmt_t *stmt, zend_long colno, zva
coltype = dbcoltype(H->link, colno+1);
+ array_init(return_value);
+
add_assoc_long(return_value, "max_length", dbcollen(H->link, colno+1) );
add_assoc_long(return_value, "precision", (int) dbtypeinfo->precision );
add_assoc_long(return_value, "scale", (int) dbtypeinfo->scale );
diff --git a/ext/pdo_dblib/tests/gh23741.phpt b/ext/pdo_dblib/tests/gh23741.phpt
new file mode 100644
index 00000000000..f3fc1a5ce44
--- /dev/null
+++ b/ext/pdo_dblib/tests/gh23741.phpt
@@ -0,0 +1,38 @@
+--TEST--
+GH-23741 (pdo_dblib: segfault after a failed query inside a PDO transaction)
+--EXTENSIONS--
+pdo_dblib
+--SKIPIF--
+<?php
+require __DIR__ . '/config.inc';
+getDbConnection();
+?>
+--FILE--
+<?php
+require __DIR__ . '/config.inc';
+
+$db = getDbConnection(PDO::class, [PDO::ATTR_ERRMODE => PDO::ERRMODE_SILENT]);
+
+$db->query('CREATE TABLE gh23741 (id int)');
+
+$db->beginTransaction();
+echo 'failing query inside the transaction: ';
+var_dump($db->query('CREATE VIEW gh23741 AS SELECT 1 AS x'));
+$db->rollBack();
+
+echo 'query after the failure: ';
+var_dump($db->query('DROP TABLE IF EXISTS gh23741') instanceof PDOStatement);
+
+echo "survived connection teardown\n";
+?>
+--CLEAN--
+<?php
+require __DIR__ . '/config.inc';
+$db = getDbConnection();
+$db->exec('DROP VIEW IF EXISTS gh23741');
+$db->exec('DROP TABLE IF EXISTS gh23741');
+?>
+--EXPECT--
+failing query inside the transaction: bool(false)
+query after the failure: bool(true)
+survived connection teardown
diff --git a/ext/pdo_dblib/tests/gh23741_2.phpt b/ext/pdo_dblib/tests/gh23741_2.phpt
new file mode 100644
index 00000000000..d8297c3671e
--- /dev/null
+++ b/ext/pdo_dblib/tests/gh23741_2.phpt
@@ -0,0 +1,27 @@
+--TEST--
+GH-23741 (pdo_dblib: crash reading metadata after a sibling statement is freed)
+--EXTENSIONS--
+pdo_dblib
+--SKIPIF--
+<?php
+require __DIR__ . '/config.inc';
+getDbConnection();
+?>
+--FILE--
+<?php
+require __DIR__ . '/config.inc';
+
+$db = getDbConnection(PDO::class, [PDO::ATTR_ERRMODE => PDO::ERRMODE_SILENT]);
+
+$wide = $db->query('SELECT 1 AS a, 2 AS b, 3 AS c');
+$narrow = $db->query('SELECT 9 AS z');
+unset($narrow);
+
+echo 'metadata for a column the connection no longer has: ';
+var_dump($wide->getColumnMeta(2));
+
+echo "survived the out-of-range column\n";
+?>
+--EXPECT--
+metadata for a column the connection no longer has: bool(false)
+survived the out-of-range column