Skip to content

Commit bccca0b

Browse files
committed
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. We also prevent PDO::ODBC_ATTR_ASSUME_UTF8 from fetching garbage, by fetching all chunks with the same C type. Closes GH-6716.
1 parent 4be867e commit bccca0b

File tree

4 files changed

+81
-2
lines changed

4 files changed

+81
-2
lines changed

NEWS

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ PHP NEWS
55
- DOM:
66
. Fixed bug #66783 (UAF when appending DOMDocument to element). (cmb)
77

8+
- PDO_ODBC:
9+
. Fixed bug #80783 (PDO ODBC truncates BLOB records at every 256th byte).
10+
(cmb)
11+
812
01 Apr 2021, PHP 7.4.17
913

1014
- Core:

ext/pdo_odbc/odbc_stmt.c

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -652,6 +652,7 @@ static int odbc_stmt_get_col(pdo_stmt_t *stmt, int colno, char **ptr, zend_ulong
652652

653653
/* if it is a column containing "long" data, perform late binding now */
654654
if (C->is_long) {
655+
SQLLEN orig_fetched_len = SQL_NULL_DATA;
655656
zend_ulong used = 0;
656657
char *buf;
657658
RETCODE rc;
@@ -662,6 +663,7 @@ static int odbc_stmt_get_col(pdo_stmt_t *stmt, int colno, char **ptr, zend_ulong
662663

663664
rc = SQLGetData(S->stmt, colno+1, C->is_unicode ? SQL_C_BINARY : SQL_C_CHAR, C->data,
664665
256, &C->fetched_len);
666+
orig_fetched_len = C->fetched_len;
665667

666668
if (rc == SQL_SUCCESS) {
667669
/* 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
673675
/* this is a 'long column'
674676
675677
read the column in 255 byte blocks until the end of the column is reached, reassembling those blocks
676-
in order into the output buffer
678+
in order into the output buffer; 255 bytes are an optimistic assumption, since the driver may assert
679+
more or less NUL bytes at the end; we cater to that later, if actual length information is available
677680
678681
this loop has to work whether or not SQLGetData() provides the total column length.
679682
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
687690
do {
688691
C->fetched_len = 0;
689692
/* read block. 256 bytes => 255 bytes are actually read, the last 1 is NULL */
690-
rc = SQLGetData(S->stmt, colno+1, SQL_C_CHAR, buf2, 256, &C->fetched_len);
693+
rc = SQLGetData(S->stmt, colno+1, C->is_unicode ? SQL_C_BINARY : SQL_C_CHAR, buf2, 256, &C->fetched_len);
694+
695+
/* adjust `used` in case we have length info from the driver */
696+
if (orig_fetched_len >= 0 && C->fetched_len >= 0) {
697+
SQLLEN fixed_used = orig_fetched_len - C->fetched_len;
698+
ZEND_ASSERT(fixed_used <= used + 1);
699+
used = fixed_used;
700+
}
691701

692702
/* resize output buffer and reassemble block */
693703
if (rc==SQL_SUCCESS_WITH_INFO) {

ext/pdo_odbc/tests/bug80783.phpt

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
--TEST--
2+
Bug #80783 (PDO ODBC truncates BLOB records at every 256th byte)
3+
--SKIPIF--
4+
<?php
5+
if (!extension_loaded('pdo_odbc')) die('skip pdo_odbc extension not available');
6+
require 'ext/pdo/tests/pdo_test.inc';
7+
PDOTest::skip();
8+
?>
9+
--FILE--
10+
<?php
11+
require 'ext/pdo/tests/pdo_test.inc';
12+
$db = PDOTest::test_factory(dirname(__FILE__) . '/common.phpt');
13+
$db->exec("CREATE TABLE bug80783 (name IMAGE)");
14+
15+
$string = str_repeat("0123456789", 50);
16+
$db->exec("INSERT INTO bug80783 VALUES('$string')");
17+
18+
$stmt = $db->prepare("SELECT name FROM bug80783");
19+
$stmt->bindColumn(1, $data, PDO::PARAM_LOB);
20+
$stmt->execute();
21+
$stmt->fetch(PDO::FETCH_BOUND);
22+
23+
var_dump($data === bin2hex($string));
24+
?>
25+
--CLEAN--
26+
<?php
27+
require 'ext/pdo/tests/pdo_test.inc';
28+
$db = PDOTest::test_factory(dirname(__FILE__) . '/common.phpt');
29+
$db->exec("DROP TABLE bug80783");
30+
?>
31+
--EXPECT--
32+
bool(true)

ext/pdo_odbc/tests/bug80783a.phpt

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
--TEST--
2+
Bug #80783 (PDO ODBC truncates BLOB records at every 256th byte)
3+
--SKIPIF--
4+
<?php
5+
if (!extension_loaded('pdo_odbc')) die('skip pdo_odbc extension not available');
6+
require 'ext/pdo/tests/pdo_test.inc';
7+
PDOTest::skip();
8+
?>
9+
--FILE--
10+
<?php
11+
require 'ext/pdo/tests/pdo_test.inc';
12+
$db = PDOTest::test_factory(dirname(__FILE__) . '/common.phpt');
13+
$db->exec("CREATE TABLE bug80783a (name NVARCHAR(MAX))");
14+
15+
$string = str_repeat("0123456789", 50);
16+
$db->exec("INSERT INTO bug80783a VALUES('$string')");
17+
18+
$stmt = $db->prepare("SELECT name FROM bug80783a");
19+
$stmt->setAttribute(PDO::ODBC_ATTR_ASSUME_UTF8, true);
20+
$stmt->bindColumn(1, $data, PDO::PARAM_STR);
21+
$stmt->execute();
22+
$stmt->fetch(PDO::FETCH_BOUND);
23+
24+
var_dump($data === $string);
25+
?>
26+
--CLEAN--
27+
<?php
28+
require 'ext/pdo/tests/pdo_test.inc';
29+
$db = PDOTest::test_factory(dirname(__FILE__) . '/common.phpt');
30+
$db->exec("DROP TABLE bug80783a");
31+
?>
32+
--EXPECT--
33+
bool(true)

0 commit comments

Comments
 (0)