Commit 91ea928c9b4 for php.net

commit 91ea928c9b475a0fb653523ca99849883cca6d96
Author: Ilia Alshanetsky <ilia@ilia.ws>
Date:   Tue Jun 16 16:23:28 2026 -0400

    Fix heap over-read seeding the long-column buffer in pdo_odbc

    In the long-column fetch path, when the ODBC driver reports the total
    column length rather than SQL_NO_TOTAL, the result string was seeded by
    copying orig_fetched_len + 1 bytes out of C->data, which holds at most
    LONG_COLUMN_BUFFER_SIZE bytes from the first SQLGetData. For a column
    larger than that buffer this reads past C->data. Seed only the bytes
    actually present in the buffer, matching the SQL_NO_TOTAL branch; the
    remainder is still fetched by the loop.

    Closes GH-22349

diff --git a/ext/pdo_odbc/odbc_stmt.c b/ext/pdo_odbc/odbc_stmt.c
index 940cf1209b1..bc87bdb1456 100644
--- a/ext/pdo_odbc/odbc_stmt.c
+++ b/ext/pdo_odbc/odbc_stmt.c
@@ -704,8 +704,9 @@ static int odbc_stmt_get_col(pdo_stmt_t *stmt, int colno, zval *result, enum pdo
 			}
 			ssize_t to_fetch_byte = to_fetch_len + 1;
 			char *buf2 = emalloc(to_fetch_byte);
-			zend_string *str = zend_string_init(C->data, to_fetch_byte, 0);
-			size_t used = to_fetch_len;
+			ssize_t seed_len = to_fetch_len > (LONG_COLUMN_BUFFER_SIZE - 1) ? (LONG_COLUMN_BUFFER_SIZE - 1) : to_fetch_len;
+			zend_string *str = zend_string_init(C->data, seed_len + 1, 0);
+			size_t used = seed_len;

 			do {
 				C->fetched_len = 0;
diff --git a/ext/pdo_odbc/tests/gh22349.phpt b/ext/pdo_odbc/tests/gh22349.phpt
new file mode 100644
index 00000000000..58219dea0ac
--- /dev/null
+++ b/ext/pdo_odbc/tests/gh22349.phpt
@@ -0,0 +1,45 @@
+--TEST--
+GH-22349 (Heap over-read fetching a long column past the internal buffer)
+--EXTENSIONS--
+pdo_odbc
+--SKIPIF--
+<?php
+require 'ext/pdo/tests/pdo_test.inc';
+PDOTest::skip();
+?>
+--FILE--
+<?php
+require 'ext/pdo/tests/pdo_test.inc';
+$db = PDOTest::test_factory('ext/pdo_odbc/tests/common.phpt');
+$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT);
+
+$db->exec('DROP TABLE test_gh22349');
+if (false === $db->exec('CREATE TABLE test_gh22349 (data text)')
+    && false === $db->exec('CREATE TABLE test_gh22349 (data CLOB)')
+    && false === $db->exec('CREATE TABLE test_gh22349 (data longtext)')) {
+    die("BORK: no large text column type available here: " . implode(", ", $db->errorInfo()) . "\n");
+}
+
+$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
+
+// The driver fetches a long column into an internal buffer of roughly one
+// memory page and reassembles the remainder. Exercise values that span and
+// exceed that buffer so the seeded length must match the bytes present.
+foreach ([4096, 8192, 65536] as $len) {
+    $db->exec('DELETE FROM test_gh22349');
+    $text = str_repeat('A', $len);
+    $db->exec("INSERT INTO test_gh22349 VALUES ('$text')");
+    $got = $db->query('SELECT data FROM test_gh22349')->fetchColumn();
+    printf("%d: %s\n", $len, ($got === $text) ? 'ok' : ('MISMATCH len=' . strlen($got)));
+}
+?>
+--CLEAN--
+<?php
+require 'ext/pdo/tests/pdo_test.inc';
+$db = PDOTest::test_factory('ext/pdo_odbc/tests/common.phpt');
+$db->exec('DROP TABLE test_gh22349');
+?>
+--EXPECT--
+4096: ok
+8192: ok
+65536: ok