]> granicus.if.org Git - php/commitdiff
Fix #80783: PDO ODBC truncates BLOB records at every 256th byte
authorChristoph M. Becker <cmbecker69@gmx.de>
Thu, 25 Feb 2021 13:38:42 +0000 (14:38 +0100)
committerChristoph M. Becker <cmbecker69@gmx.de>
Thu, 18 Mar 2021 14:13:27 +0000 (15:13 +0100)
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.

We also prevent PDO::ODBC_ATTR_ASSUME_UTF8 from fetching garbage, by
fetching all chunks with the same C type.

Closes GH-6716.

NEWS
ext/pdo_odbc/odbc_stmt.c
ext/pdo_odbc/tests/bug80783.phpt [new file with mode: 0644]
ext/pdo_odbc/tests/bug80783a.phpt [new file with mode: 0644]

diff --git a/NEWS b/NEWS
index 0584f6504c2b8abdaba2669b81568daac2d7272a..d1001e22a2f0f37fee13ff6653cc5a5f9d272776 100644 (file)
--- a/NEWS
+++ b/NEWS
@@ -5,6 +5,10 @@ PHP                                                                        NEWS
 - DOM:
   . Fixed bug #66783 (UAF when appending DOMDocument to element). (cmb)
 
+- PDO_ODBC:
+  . Fixed bug #80783 (PDO ODBC truncates BLOB records at every 256th byte).
+    (cmb)
+
 01 Apr 2021, PHP 7.4.17
 
 - Core:
index 18abc475b9ebbfff4487f44ee16ce6a35197c827..7ce0bebdca0dcc1de848960b0316222a6a1ca611 100644 (file)
@@ -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 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
@@ -687,7 +690,14 @@ 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) {
+                                       SQLLEN fixed_used = orig_fetched_len - C->fetched_len;
+                                       ZEND_ASSERT(fixed_used <= used + 1);
+                                       used = fixed_used;
+                               }
 
                                /* resize output buffer and reassemble block */
                                if (rc==SQL_SUCCESS_WITH_INFO) {
diff --git a/ext/pdo_odbc/tests/bug80783.phpt b/ext/pdo_odbc/tests/bug80783.phpt
new file mode 100644 (file)
index 0000000..9794c25
--- /dev/null
@@ -0,0 +1,32 @@
+--TEST--
+Bug #80783 (PDO ODBC truncates BLOB records at every 256th byte)
+--SKIPIF--
+<?php
+if (!extension_loaded('pdo_odbc')) die('skip pdo_odbc extension not available');
+require 'ext/pdo/tests/pdo_test.inc';
+PDOTest::skip();
+?>
+--FILE--
+<?php
+require 'ext/pdo/tests/pdo_test.inc';
+$db = PDOTest::test_factory(dirname(__FILE__) . '/common.phpt');
+$db->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--
+<?php
+require 'ext/pdo/tests/pdo_test.inc';
+$db = PDOTest::test_factory(dirname(__FILE__) . '/common.phpt');
+$db->exec("DROP TABLE bug80783");
+?>
+--EXPECT--
+bool(true)
diff --git a/ext/pdo_odbc/tests/bug80783a.phpt b/ext/pdo_odbc/tests/bug80783a.phpt
new file mode 100644 (file)
index 0000000..f9e123a
--- /dev/null
@@ -0,0 +1,33 @@
+--TEST--
+Bug #80783 (PDO ODBC truncates BLOB records at every 256th byte)
+--SKIPIF--
+<?php
+if (!extension_loaded('pdo_odbc')) die('skip pdo_odbc extension not available');
+require 'ext/pdo/tests/pdo_test.inc';
+PDOTest::skip();
+?>
+--FILE--
+<?php
+require 'ext/pdo/tests/pdo_test.inc';
+$db = PDOTest::test_factory(dirname(__FILE__) . '/common.phpt');
+$db->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_STR);
+$stmt->execute();
+$stmt->fetch(PDO::FETCH_BOUND);
+
+var_dump($data === $string);
+?>
+--CLEAN--
+<?php
+require 'ext/pdo/tests/pdo_test.inc';
+$db = PDOTest::test_factory(dirname(__FILE__) . '/common.phpt');
+$db->exec("DROP TABLE bug80783a");
+?>
+--EXPECT--
+bool(true)