[php-src] PHP-8.5: Merge branch 'PHP-8.4' into PHP-8.5

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

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

Merge branch 'PHP-8.4' into PHP-8.5

* PHP-8.4:
  Fix GH-22668: odbc heap over-read on oversized column value

Changed paths:
  A  ext/odbc/tests/gh22668.phpt
  M  NEWS
  M  ext/odbc/php_odbc.c
  M  ext/odbc/php_odbc_includes.h
  M  ext/odbc/tests/config.inc


Diff:

diff --git a/NEWS b/NEWS
index de82bc685dae..2c88dee0b23c 100644
--- a/NEWS
+++ b/NEWS
@@ -40,6 +40,10 @@ PHP                                                                        NEWS
   . Fixed bug GH-18173 (ext/hash relies on implementation-defined malloc
     alignment). (iliaal)
 
+- ODBC:
+  . Fixed bug GH-22668 (Heap buffer over-read when a column value exceeds the
+    driver-reported display size). (iliaal)
+
 - Opcache:
   . Fixed bug GH-22158 (Tracing JIT dispatches the observer begin handler
     through the wrong run_time_cache slot on megamorphic calls). (ptondereau,
diff --git a/ext/odbc/php_odbc.c b/ext/odbc/php_odbc.c
index 7a6bc2871825..e75d11b554b8 100644
--- a/ext/odbc/php_odbc.c
+++ b/ext/odbc/php_odbc.c
@@ -670,6 +670,7 @@ void odbc_bindcols(odbc_result *result)
 
 	for(i = 0; i < result->numcols; i++) {
 		bool char_extra_alloc = false;
+		result->values[i].value_max_len = 0;
 		colfieldid = SQL_COLUMN_DISPLAY_SIZE;
 
 		rc = SQLColAttribute(result->stmt, (SQLUSMALLINT)(i+1), SQL_DESC_NAME,
@@ -749,6 +750,7 @@ void odbc_bindcols(odbc_result *result)
 					displaysize *= 4;
 				}
 				result->values[i].value = (char *)emalloc(displaysize + 1);
+				result->values[i].value_max_len = displaysize;
 				rc = SQLBindCol(result->stmt, (SQLUSMALLINT)(i+1), SQL_C_CHAR, result->values[i].value,
 							displaysize + 1, &result->values[i].vallen);
 				break;
@@ -1420,7 +1422,11 @@ static void php_odbc_fetch_hash(INTERNAL_FUNCTION_PARAMETERS, int result_type)
 					ZVAL_FALSE(&tmp);
 					break;
 				}
-				ZVAL_STRINGL(&tmp, result->values[i].value, result->values[i].vallen);
+				SQLLEN str_len = result->values[i].vallen;
+				if (str_len > result->values[i].value_max_len) {
+					str_len = result->values[i].value_max_len;
+				}
+				ZVAL_STRINGL(&tmp, result->values[i].value, str_len);
 				break;
 		}
 
@@ -1570,7 +1576,11 @@ PHP_FUNCTION(odbc_fetch_into)
 					ZVAL_FALSE(&tmp);
 					break;
 				}
-				ZVAL_STRINGL(&tmp, result->values[i].value, result->values[i].vallen);
+				SQLLEN str_len = result->values[i].vallen;
+				if (str_len > result->values[i].value_max_len) {
+					str_len = result->values[i].value_max_len;
+				}
+				ZVAL_STRINGL(&tmp, result->values[i].value, str_len);
 				break;
 		}
 		zend_hash_index_update(Z_ARRVAL_P(pv_res_arr), i, &tmp);
@@ -1793,7 +1803,11 @@ PHP_FUNCTION(odbc_result)
 				php_error_docref(NULL, E_WARNING, "Cannot get data of column #%d (driver cannot determine length)", field_ind + 1);
 				RETURN_FALSE;
 			} else {
-				RETURN_STRINGL(result->values[field_ind].value, result->values[field_ind].vallen);
+				SQLLEN str_len = result->values[field_ind].vallen;
+				if (str_len > result->values[field_ind].value_max_len) {
+					str_len = result->values[field_ind].value_max_len;
+				}
+				RETURN_STRINGL(result->values[field_ind].value, str_len);
 			}
 			break;
 	}
diff --git a/ext/odbc/php_odbc_includes.h b/ext/odbc/php_odbc_includes.h
index 058684c2ed03..0a99f1a756e3 100644
--- a/ext/odbc/php_odbc_includes.h
+++ b/ext/odbc/php_odbc_includes.h
@@ -92,6 +92,7 @@ typedef struct odbc_result_value {
 	char name[256];
 	char *value;
 	SQLLEN vallen;
+	SQLLEN value_max_len;
 	SQLLEN coltype;
 } odbc_result_value;
 
diff --git a/ext/odbc/tests/config.inc b/ext/odbc/tests/config.inc
index 8d74feeb6ffb..a0f24f301558 100644
--- a/ext/odbc/tests/config.inc
+++ b/ext/odbc/tests/config.inc
@@ -1,5 +1,7 @@
 <?php
 
+defined('ODBC_SQLITE_DSN') || define('ODBC_SQLITE_DSN', "Driver=SQLite3;Database=:memory:");
+
 $dsn  = getenv("ODBC_TEST_DSN");
 $user = getenv("ODBC_TEST_USER");
 $pass = getenv("ODBC_TEST_PASS");
diff --git a/ext/odbc/tests/gh22668.phpt b/ext/odbc/tests/gh22668.phpt
new file mode 100644
index 000000000000..6dbaa9797170
--- /dev/null
+++ b/ext/odbc/tests/gh22668.phpt
@@ -0,0 +1,33 @@
+--TEST--
+GH-22668 (Heap buffer over-read when a column value exceeds the bound buffer)
+--EXTENSIONS--
+odbc
+--SKIPIF--
+<?php
+include __DIR__ . "/config.inc";
+$conn = @odbc_connect(ODBC_SQLITE_DSN, null, null);
+if (!$conn) {
+    die("skip requires the SQLite3 ODBC driver");
+}
+?>
+--FILE--
+<?php
+include __DIR__ . "/config.inc";
+$conn = odbc_connect(ODBC_SQLITE_DSN, null, null);
+
+// The SQLite3 driver reports a 255 byte display size for a computed column, so
+// the 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.
+$result = odbc_exec($conn, "SELECT printf('%.*c', 4096, 'A') AS data");
+$row = odbc_fetch_array($result);
+$s = $row['data'];
+
+echo "clamped to buffer: "; var_dump(strlen($s) < 4096);
+echo "only value bytes:  "; var_dump(strlen($s) === substr_count($s, 'A'));
+
+odbc_close($conn);
+?>
+--EXPECT--
+clamped to buffer: bool(true)
+only value bytes:  bool(true)
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.