Commit 567bc5cb431 for php.net
commit 567bc5cb431ee912751cf5ce01791b4b26482610
Author: Lazizbek Ergashev <lazerg2@gmail.com>
Date: Mon Aug 31 21:45:02 2026 +0500
Fix GH-23444: ODBC_ATTR_ASSUME_UTF8 corrupts Unicode data outside Windows (#23445)
The attribute is documented as Windows only, and the UTF-8 conversion it
relies on is compiled under #ifdef PHP_WIN32. Everywhere else
pdo_odbc_sqltype_is_unicode() still reported wide types as Unicode, so
parameters and columns were bound SQL_C_BINARY and then passed through
unconverted. Raw UTF-8 reached the server for an nvarchar parameter and
raw UTF-16 came back for an nvarchar column, and msodbcsql18 rejects a
parameter of odd byte length with HY090. Report it as not Unicode
outside Windows, which leaves the encoding to the driver as the default
already does.
On Windows the conversion runs but the data-at-exec branch declared the
unconverted byte length in SQL_LEN_DATA_AT_EXEC() while SQLPutData()
sent the converted bytes, so binding a non-ASCII parameter failed with
22026.
Closes GH-23444
diff --git a/NEWS b/NEWS
index 608bacc2910..927ebe121e8 100644
--- a/NEWS
+++ b/NEWS
@@ -59,6 +59,10 @@ PHP NEWS
. Fixed a leak when a persistent connection failed a liveness check
with no other live PDO handle. (iliaal)
+- PDO_ODBC:
+ . Fixed bug GH-23444 (ODBC_ATTR_ASSUME_UTF8 corrupts Unicode data outside
+ Windows). (Calvin Buckley, Lazizbek Ergashev)
+
- Phar:
. Fixed bug GH-23418 (Use-after-free when looking up mounted directories).
(Weilin Du)
diff --git a/ext/pdo_odbc/odbc_stmt.c b/ext/pdo_odbc/odbc_stmt.c
index 8786f2563e5..042c4684a88 100644
--- a/ext/pdo_odbc/odbc_stmt.c
+++ b/ext/pdo_odbc/odbc_stmt.c
@@ -34,6 +34,7 @@ enum pdo_odbc_conv_result {
static int pdo_odbc_sqltype_is_unicode(pdo_odbc_stmt *S, SQLSMALLINT sqltype)
{
+#ifdef PHP_WIN32
if (!S->assume_utf8) return 0;
switch (sqltype) {
#ifdef SQL_WCHAR
@@ -51,6 +52,9 @@ static int pdo_odbc_sqltype_is_unicode(pdo_odbc_stmt *S, SQLSMALLINT sqltype)
default:
return 0;
}
+#else
+ return 0;
+#endif
}
static int pdo_odbc_utf82ucs2(pdo_stmt_t *stmt, int is_unicode, const char *buf,
@@ -548,7 +552,15 @@ static int odbc_stmt_param_hook(pdo_stmt_t *stmt, struct pdo_bound_param_data *p
break;
}
} else {
- P->len = SQL_LEN_DATA_AT_EXEC(Z_STRLEN_P(parameter));
+ zend_ulong ulen;
+ if (pdo_odbc_utf82ucs2(stmt, P->is_unicode,
+ Z_STRVAL_P(parameter),
+ Z_STRLEN_P(parameter),
+ &ulen) == PDO_ODBC_CONV_OK) {
+ P->len = SQL_LEN_DATA_AT_EXEC(ulen);
+ } else {
+ P->len = SQL_LEN_DATA_AT_EXEC(Z_STRLEN_P(parameter));
+ }
}
}
return 1;
diff --git a/ext/pdo_odbc/tests/gh23444.phpt b/ext/pdo_odbc/tests/gh23444.phpt
new file mode 100644
index 00000000000..f71fc733839
--- /dev/null
+++ b/ext/pdo_odbc/tests/gh23444.phpt
@@ -0,0 +1,34 @@
+--TEST--
+GH-23444 (Unicode data is corrupted with ODBC_ATTR_ASSUME_UTF8)
+--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(__DIR__ . '/common.phpt');
+$db->exec("CREATE TABLE gh23444 (v NVARCHAR(100))");
+
+// 13 bytes as UTF-8, so an unconverted parameter is an odd number of bytes
+$string = "\u{6e2c}\u{8a66}\u{4e2d}\u{1f418}";
+
+$db->setAttribute(PDO::ODBC_ATTR_ASSUME_UTF8, true);
+$stmt = $db->prepare("INSERT INTO gh23444 VALUES(?)");
+$stmt->execute([$string]);
+
+$stmt = $db->prepare("SELECT v FROM gh23444 WHERE v = ?");
+$stmt->execute([$string]);
+var_dump($stmt->fetchColumn() === $string);
+?>
+--CLEAN--
+<?php
+require 'ext/pdo/tests/pdo_test.inc';
+$db = PDOTest::test_factory(__DIR__ . '/common.phpt');
+$db->exec("DROP TABLE IF EXISTS gh23444");
+?>
+--EXPECT--
+bool(true)