[php-src] PHP-8.4: Fix GH-22666: pdo_odbc heap overflow on oversized output param

Ilia Alshanetsky <[email protected]>
Newsgroups gmane.comp.php.cvs.general
Message-ID <[email protected]>
Author: Ilia Alshanetsky (iliaal)
Date: 2026-07-14T15:45:21-04:00

Commit: https://github.com/php/php-src/commit/a7539ff8344c7290b3e0e275e2b095a572d84b28
Raw diff: https://github.com/php/php-src/commit/a7539ff8344c7290b3e0e275e2b095a572d84b28.diff

Fix GH-22666: pdo_odbc heap overflow on oversized output param

odbc_stmt_param_hook() sizes an output-param buffer from the declared maxlen
but then copies the runtime value into it using the value's own length, and
at fetch-back hands ZVAL_STRINGL the driver-reported length. Both trust a
length that can exceed the buffer: the input copy overflows the heap
(controlled write), and on a truncated OUTPUT value the driver reports the
full length via the shared indicator, so the readback over-reads. Record the
bound capacity in a new outbuflen field and clamp the copy and the readback to
it. LOB output is rejected at bind, so those copy sites stay unreachable.

Fixes GH-22666
Closes GH-22674

Changed paths:
  A  ext/pdo_odbc/tests/gh22666.phpt
  M  NEWS
  M  ext/pdo_odbc/odbc_stmt.c
  M  ext/pdo_odbc/php_pdo_odbc_int.h


Diff:

diff --git a/NEWS b/NEWS
index a9ad00c2be65..76be38bcaa70 100644
--- a/NEWS
+++ b/NEWS
@@ -51,6 +51,8 @@ PHP                                                                        NEWS
     carries no credentials). (iliaal)
   . Fixed bug GH-22667 (Heap buffer over-read when a column value exceeds the
     driver-reported display size). (iliaal)
+  . Fixed bug GH-22666 (Heap buffer overflow when an output parameter value is
+    longer than the declared maxlen). (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 5b9104895854..8940143da09c 100644
--- a/ext/pdo_odbc/odbc_stmt.c
+++ b/ext/pdo_odbc/odbc_stmt.c
@@ -360,6 +360,7 @@ static int odbc_stmt_param_hook(pdo_stmt_t *stmt, struct pdo_bound_param_data *p
 				param->driver_data = P;
 
 				P->len = 0; /* is re-populated each EXEC_PRE */
+				P->outbuflen = 0;
 				P->outbuf = NULL;
 
 				P->is_unicode = pdo_odbc_sqltype_is_unicode(S, sqltype);
@@ -384,6 +385,7 @@ static int odbc_stmt_param_hook(pdo_stmt_t *stmt, struct pdo_bound_param_data *p
 							P->len *= 2;
 						}
 						P->outbuf = emalloc(P->len + (P->is_unicode ? 2:1));
+						P->outbuflen = P->len;
 					}
 				}
 
@@ -481,10 +483,16 @@ static int odbc_stmt_param_hook(pdo_stmt_t *stmt, struct pdo_bound_param_data *p
 							case PDO_ODBC_CONV_FAIL:
 							case PDO_ODBC_CONV_NOT_REQUIRED:
 								P->len = Z_STRLEN_P(parameter);
+								if (P->len > P->outbuflen) {
+									P->len = P->outbuflen;
+								}
 								memcpy(P->outbuf, Z_STRVAL_P(parameter), P->len);
 								break;
 							case PDO_ODBC_CONV_OK:
 								P->len = ulen;
+								if (P->len > P->outbuflen) {
+									P->len = P->outbuflen;
+								}
 								memcpy(P->outbuf, S->convbuf, P->len);
 								break;
 						}
@@ -506,6 +514,9 @@ static int odbc_stmt_param_hook(pdo_stmt_t *stmt, struct pdo_bound_param_data *p
 					zval_ptr_dtor(parameter);
 
 					if (P->len >= 0) {
+							if (P->len > P->outbuflen) {
+								P->len = P->outbuflen;
+							}
 							ZVAL_STRINGL(parameter, P->outbuf, P->len);
 							switch (pdo_odbc_ucs22utf8(stmt, P->is_unicode, parameter)) {
 								case PDO_ODBC_CONV_FAIL:
diff --git a/ext/pdo_odbc/php_pdo_odbc_int.h b/ext/pdo_odbc/php_pdo_odbc_int.h
index 473d70ff7076..ea93c58b2b60 100644
--- a/ext/pdo_odbc/php_pdo_odbc_int.h
+++ b/ext/pdo_odbc/php_pdo_odbc_int.h
@@ -153,6 +153,7 @@ typedef struct {
 
 typedef struct {
 	SQLLEN len;
+	SQLLEN outbuflen;
 	SQLSMALLINT paramtype;
 	char *outbuf;
 	unsigned is_unicode:1;
diff --git a/ext/pdo_odbc/tests/gh22666.phpt b/ext/pdo_odbc/tests/gh22666.phpt
new file mode 100644
index 000000000000..b6ba2265b9be
--- /dev/null
+++ b/ext/pdo_odbc/tests/gh22666.phpt
@@ -0,0 +1,30 @@
+--TEST--
+GH-22666 (Heap buffer overflow when an output param value exceeds its maxlen)
+--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);
+
+// An INPUT_OUTPUT parameter declares a maxlen of 4, so the output buffer is 4
+// bytes, but the runtime value is longer. The copy into that buffer must be
+// bounded to the declared capacity, not overflow it.
+$value = str_repeat('A', 64);
+$stmt = $pdo->prepare('SELECT ?');
+$stmt->bindParam(1, $value, PDO::PARAM_STR | PDO::PARAM_INPUT_OUTPUT, 4);
+$stmt->execute();
+
+echo "bounded to maxlen: "; var_dump($value);
+?>
+--EXPECT--
+bounded to maxlen: string(4) "AAAA"
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.