From a32f02e4c2aba54000bda01dbf8baa122deecfff Mon Sep 17 00:00:00 2001 From: Lazizbek Ergashev Date: Tue, 25 Aug 2026 12:58:03 +0500 Subject: [PATCH] Fix GH-23444: ODBC_ATTR_ASSUME_UTF8 corrupts Unicode data outside Windows 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 --- NEWS | 4 ++++ ext/pdo_odbc/odbc_stmt.c | 14 +++++++++++++- ext/pdo_odbc/tests/gh23444.phpt | 34 +++++++++++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 ext/pdo_odbc/tests/gh23444.phpt diff --git a/NEWS b/NEWS index cf28751877fa..a56c03bb9e48 100644 --- a/NEWS +++ b/NEWS @@ -54,6 +54,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 8786f2563e52..042c4684a887 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 000000000000..f71fc733839f --- /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-- + +--FILE-- +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-- +exec("DROP TABLE IF EXISTS gh23444"); +?> +--EXPECT-- +bool(true)