From ce49c7c5375a5402fc9dc0003f87afbf45c12049 Mon Sep 17 00:00:00 2001 From: "Christoph M. Becker" Date: Mon, 22 Feb 2021 15:13:17 +0100 Subject: [PATCH 1/3] Fix #80783: PDO ODBC truncates BLOB records at every 256th byte It is not guaranteed, that the driver inserts only a single NUL byte at the end of the buffer. Apparently, there is no way to find out the actual data length in the buffer after calling `SQLGetData()`, so we adjust after the next `SQLGetData()` call. --- ext/pdo_odbc/odbc_stmt.c | 12 +++++++++++- ext/pdo_odbc/tests/bug80783.phpt | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 ext/pdo_odbc/tests/bug80783.phpt diff --git a/ext/pdo_odbc/odbc_stmt.c b/ext/pdo_odbc/odbc_stmt.c index 18abc475b9ebb..57b75dbeded2c 100644 --- a/ext/pdo_odbc/odbc_stmt.c +++ b/ext/pdo_odbc/odbc_stmt.c @@ -652,6 +652,7 @@ static int odbc_stmt_get_col(pdo_stmt_t *stmt, int colno, char **ptr, zend_ulong /* if it is a column containing "long" data, perform late binding now */ if (C->is_long) { + SQLLEN orig_fetched_len = SQL_NULL_DATA; zend_ulong used = 0; char *buf; RETCODE rc; @@ -662,6 +663,7 @@ static int odbc_stmt_get_col(pdo_stmt_t *stmt, int colno, char **ptr, zend_ulong rc = SQLGetData(S->stmt, colno+1, C->is_unicode ? SQL_C_BINARY : SQL_C_CHAR, C->data, 256, &C->fetched_len); + orig_fetched_len = C->fetched_len; if (rc == SQL_SUCCESS) { /* all the data fit into our little buffer; @@ -673,7 +675,8 @@ static int odbc_stmt_get_col(pdo_stmt_t *stmt, int colno, char **ptr, zend_ulong /* this is a 'long column' read the column in 255 byte blocks until the end of the column is reached, reassembling those blocks - in order into the output buffer + in order into the output buffer; 255 bytes are an optimistic assumption, since the driver may assert + more NUL bytes at the end; we cater to that later, if actual length information is available this loop has to work whether or not SQLGetData() provides the total column length. calling SQLDescribeCol() or other, specifically to get the column length, then doing a single read @@ -689,6 +692,13 @@ static int odbc_stmt_get_col(pdo_stmt_t *stmt, int colno, char **ptr, zend_ulong /* read block. 256 bytes => 255 bytes are actually read, the last 1 is NULL */ rc = SQLGetData(S->stmt, colno+1, SQL_C_CHAR, buf2, 256, &C->fetched_len); + /* adjust `used` in case we have length info from the driver */ + if (orig_fetched_len >= 0 && C->fetched_len >= 0) { + SQLLEN fixed_used = orig_fetched_len - C->fetched_len; + ZEND_ASSERT(fixed_used <= used); + used = fixed_used; + } + /* resize output buffer and reassemble block */ if (rc==SQL_SUCCESS_WITH_INFO) { /* point 5, in section "Retrieving Data with SQLGetData" in http://msdn.microsoft.com/en-us/library/windows/desktop/ms715441(v=vs.85).aspx diff --git a/ext/pdo_odbc/tests/bug80783.phpt b/ext/pdo_odbc/tests/bug80783.phpt new file mode 100644 index 0000000000000..9794c25a30ece --- /dev/null +++ b/ext/pdo_odbc/tests/bug80783.phpt @@ -0,0 +1,32 @@ +--TEST-- +Bug #80783 (PDO ODBC truncates BLOB records at every 256th byte) +--SKIPIF-- + +--FILE-- +exec("CREATE TABLE bug80783 (name IMAGE)"); + +$string = str_repeat("0123456789", 50); +$db->exec("INSERT INTO bug80783 VALUES('$string')"); + +$stmt = $db->prepare("SELECT name FROM bug80783"); +$stmt->bindColumn(1, $data, PDO::PARAM_LOB); +$stmt->execute(); +$stmt->fetch(PDO::FETCH_BOUND); + +var_dump($data === bin2hex($string)); +?> +--CLEAN-- +exec("DROP TABLE bug80783"); +?> +--EXPECT-- +bool(true) From a691791b5ddec6f39407ca9451ed1355aefb08ad Mon Sep 17 00:00:00 2001 From: "Christoph M. Becker" Date: Thu, 25 Feb 2021 14:30:41 +0100 Subject: [PATCH 2/3] Cater to drivers which don't insert trailing NUL bytes This is actually already catered to, but we need to adjust the assertion. --- ext/pdo_odbc/odbc_stmt.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ext/pdo_odbc/odbc_stmt.c b/ext/pdo_odbc/odbc_stmt.c index 57b75dbeded2c..57a70f572ff93 100644 --- a/ext/pdo_odbc/odbc_stmt.c +++ b/ext/pdo_odbc/odbc_stmt.c @@ -676,7 +676,7 @@ static int odbc_stmt_get_col(pdo_stmt_t *stmt, int colno, char **ptr, zend_ulong read the column in 255 byte blocks until the end of the column is reached, reassembling those blocks in order into the output buffer; 255 bytes are an optimistic assumption, since the driver may assert - more NUL bytes at the end; we cater to that later, if actual length information is available + more or less NUL bytes at the end; we cater to that later, if actual length information is available this loop has to work whether or not SQLGetData() provides the total column length. calling SQLDescribeCol() or other, specifically to get the column length, then doing a single read @@ -695,7 +695,7 @@ static int odbc_stmt_get_col(pdo_stmt_t *stmt, int colno, char **ptr, zend_ulong /* adjust `used` in case we have length info from the driver */ if (orig_fetched_len >= 0 && C->fetched_len >= 0) { SQLLEN fixed_used = orig_fetched_len - C->fetched_len; - ZEND_ASSERT(fixed_used <= used); + ZEND_ASSERT(fixed_used <= used + 1); used = fixed_used; } From fab2e5f4b19af8a2a951549c6c8e34ac6d86dd82 Mon Sep 17 00:00:00 2001 From: "Christoph M. Becker" Date: Thu, 25 Feb 2021 14:38:42 +0100 Subject: [PATCH 3/3] Prevent PDO::ODBC_ATTR_ASSUME_UTF8 from fetching garbage This is actually an own issue, but can't be fixed without fixing #80783 first, so I add this to this PR. The point is that we need to fetch all chunks with the same C type. --- ext/pdo_odbc/odbc_stmt.c | 2 +- ext/pdo_odbc/tests/bug80783a.phpt | 33 +++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 ext/pdo_odbc/tests/bug80783a.phpt diff --git a/ext/pdo_odbc/odbc_stmt.c b/ext/pdo_odbc/odbc_stmt.c index 57a70f572ff93..7ce0bebdca0dc 100644 --- a/ext/pdo_odbc/odbc_stmt.c +++ b/ext/pdo_odbc/odbc_stmt.c @@ -690,7 +690,7 @@ static int odbc_stmt_get_col(pdo_stmt_t *stmt, int colno, char **ptr, zend_ulong do { C->fetched_len = 0; /* read block. 256 bytes => 255 bytes are actually read, the last 1 is NULL */ - rc = SQLGetData(S->stmt, colno+1, SQL_C_CHAR, buf2, 256, &C->fetched_len); + rc = SQLGetData(S->stmt, colno+1, C->is_unicode ? SQL_C_BINARY : SQL_C_CHAR, buf2, 256, &C->fetched_len); /* adjust `used` in case we have length info from the driver */ if (orig_fetched_len >= 0 && C->fetched_len >= 0) { diff --git a/ext/pdo_odbc/tests/bug80783a.phpt b/ext/pdo_odbc/tests/bug80783a.phpt new file mode 100644 index 0000000000000..c8b696e081e70 --- /dev/null +++ b/ext/pdo_odbc/tests/bug80783a.phpt @@ -0,0 +1,33 @@ +--TEST-- +Bug #80783 (PDO ODBC truncates BLOB records at every 256th byte) +--SKIPIF-- + +--FILE-- +exec("CREATE TABLE bug80783a (name NVARCHAR(MAX))"); + +$string = str_repeat("0123456789", 50); +$db->exec("INSERT INTO bug80783a VALUES('$string')"); + +$stmt = $db->prepare("SELECT name FROM bug80783a"); +$stmt->setAttribute(PDO::ODBC_ATTR_ASSUME_UTF8, true); +$stmt->bindColumn(1, $data, PDO::PARAM_LOB); +$stmt->execute(); +$stmt->fetch(PDO::FETCH_BOUND); + +var_dump($data === $string); +?> +--CLEAN-- +exec("DROP TABLE bug80783a"); +?> +--EXPECT-- +bool(true)