Commit a7539ff8344 for php.net

commit a7539ff8344c7290b3e0e275e2b095a572d84b28
Author: Ilia Alshanetsky <ilia@ilia.ws>
Date:   Fri Jul 10 08:21:24 2026 -0400

    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

diff --git a/NEWS b/NEWS
index a9ad00c2be6..76be38bcaa7 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 5b910489585..8940143da09 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 473d70ff707..ea93c58b2b6 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 00000000000..b6ba2265b9b
--- /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"