Commit 103c84ff4c1 for php.net
commit 103c84ff4c1bb4cdc4b078c306c2c8b8b584f1b1
Author: Ilia Alshanetsky <ilia@ilia.ws>
Date: Fri Jul 10 07:57:25 2026 -0400
Fix GH-22667: pdo_odbc heap over-read on oversized column value
odbc_stmt_describe() binds a colsize+1 buffer for short columns and stores
that capacity in datalen, but odbc_stmt_get_col() built the result string
from the driver-reported fetched_len. A conforming driver truncates an
over-long value into the buffer yet reports the full length, so
ZVAL_STRINGL_FAST over-read past the allocation and returned adjacent heap to
userland. Clamp fetched_len to the bound capacity, guarded by !is_long so the
long-column SQLGetData path (whose fixed buffer is unrelated to datalen) is
left untouched.
Fixes GH-22667
Closes GH-22672
diff --git a/NEWS b/NEWS
index e71a94ac12e..9bcb68e5271 100644
--- a/NEWS
+++ b/NEWS
@@ -45,6 +45,8 @@ PHP NEWS
- PDO_ODBC:
. Fixed bug GH-20726 (Crash with ODBC connection pooling when the DSN
carries no credentials). (iliaal)
+ . Fixed bug GH-22667 (Heap buffer over-read when a column value exceeds the
+ driver-reported display size). (iliaal)
- Phar:
. Fixed inconsistent handling of the magic ".phar" directory. Paths such as
diff --git a/ext/pdo_odbc/odbc_stmt.c b/ext/pdo_odbc/odbc_stmt.c
index c114f721a7a..5b910489585 100644
--- a/ext/pdo_odbc/odbc_stmt.c
+++ b/ext/pdo_odbc/odbc_stmt.c
@@ -744,7 +744,14 @@ static int odbc_stmt_get_col(pdo_stmt_t *stmt, int colno, zval *result, enum pdo
return 1;
} else if (C->fetched_len >= 0) {
/* it was stored perfectly */
- ZVAL_STRINGL_FAST(result, C->data, C->fetched_len);
+ SQLLEN data_len = C->fetched_len;
+ if (!C->is_long) {
+ SQLLEN max_len = C->is_unicode ? (SQLLEN)C->datalen + 1 : (SQLLEN)C->datalen;
+ if (data_len > max_len) {
+ data_len = max_len;
+ }
+ }
+ ZVAL_STRINGL_FAST(result, C->data, data_len);
if (C->is_unicode) {
goto unicode_conv;
}
diff --git a/ext/pdo_odbc/tests/config.inc b/ext/pdo_odbc/tests/config.inc
new file mode 100644
index 00000000000..185602bb7ed
--- /dev/null
+++ b/ext/pdo_odbc/tests/config.inc
@@ -0,0 +1,3 @@
+<?php
+
+const PDO_ODBC_SQLITE_DSN = "odbc:Driver=SQLite3;Database=:memory:";
diff --git a/ext/pdo_odbc/tests/gh22667.phpt b/ext/pdo_odbc/tests/gh22667.phpt
new file mode 100644
index 00000000000..bcbd715da20
--- /dev/null
+++ b/ext/pdo_odbc/tests/gh22667.phpt
@@ -0,0 +1,32 @@
+--TEST--
+GH-22667 (Heap buffer over-read when a column value exceeds the bound buffer)
+--EXTENSIONS--
+pdo_odbc
+--SKIPIF--
+<?php
+require __DIR__ . '/config.inc';
+try {
+ $pdo = new PDO(PDO_ODBC_SQLITE_DSN);
+} catch (PDOException $e) {
+ die("skip requires the SQLite3 ODBC driver");
+}
+?>
+--FILE--
+<?php
+require __DIR__ . '/config.inc';
+$pdo = new PDO(PDO_ODBC_SQLITE_DSN);
+
+// The SQLite3 driver reports a 255 byte display size for a computed column, so
+// the short-bound buffer holds at most 255 bytes while the value is far longer.
+// A conforming driver truncates into the buffer but reports the full length; the
+// returned string must stay within the buffer, not over-read past it.
+$stmt = $pdo->query("SELECT printf('%.*c', 4096, 'A') AS data");
+$row = $stmt->fetch(PDO::FETCH_ASSOC);
+$s = $row['data'];
+
+echo "clamped to buffer: "; var_dump(strlen($s) < 4096);
+echo "only value bytes: "; var_dump(strlen($s) === substr_count($s, 'A'));
+?>
+--EXPECT--
+clamped to buffer: bool(true)
+only value bytes: bool(true)