summaryrefslogtreecommitdiffstats
path: root/php-bug80783.patch
blob: 2da992808359ac4b398b776a384b60caab80c499 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
From bccca0b53aa60a62e2988c750fc73c02d109e642 Mon Sep 17 00:00:00 2001
From: "Christoph M. Becker" <cmbecker69@gmx.de>
Date: Thu, 25 Feb 2021 14:38:42 +0100
Subject: [PATCH] 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.
---
 NEWS                              |  4 ++++
 ext/pdo_odbc/odbc_stmt.c          | 14 +++++++++++--
 ext/pdo_odbc/tests/bug80783.phpt  | 32 ++++++++++++++++++++++++++++++
 ext/pdo_odbc/tests/bug80783a.phpt | 33 +++++++++++++++++++++++++++++++
 4 files changed, 81 insertions(+), 2 deletions(-)
 create mode 100644 ext/pdo_odbc/tests/bug80783.phpt
 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 18abc475b9eb..7ce0bebdca0d 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 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
index 000000000000..9794c25a30ec
--- /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--
+<?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
index 000000000000..f9e123ae5426
--- /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--
+<?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)
From 25f5a1b2e15344e75d69a7140631d467e8b3f966 Mon Sep 17 00:00:00 2001
From: Remi Collet <remi@remirepo.net>
Date: Thu, 8 Apr 2021 11:04:33 +0200
Subject: [PATCH] Improve fix for #80783

---
 ext/pdo_odbc/odbc_stmt.c | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/ext/pdo_odbc/odbc_stmt.c b/ext/pdo_odbc/odbc_stmt.c
index 7ce0bebdca0d..368648c36ae2 100644
--- a/ext/pdo_odbc/odbc_stmt.c
+++ b/ext/pdo_odbc/odbc_stmt.c
@@ -665,13 +665,13 @@ static int odbc_stmt_get_col(pdo_stmt_t *stmt, int colno, char **ptr, zend_ulong
  			256, &C->fetched_len);
 		orig_fetched_len = C->fetched_len;
 
-		if (rc == SQL_SUCCESS) {
+		if (rc == SQL_SUCCESS && C->fetched_len < 256) {
 			/* all the data fit into our little buffer;
 			 * jump down to the generic bound data case */
 			goto in_data;
 		}
 
-		if (rc == SQL_SUCCESS_WITH_INFO) {
+		if (rc == SQL_SUCCESS_WITH_INFO || rc == SQL_SUCCESS) {
 			/* this is a 'long column'
 
 			 read the column in 255 byte blocks until the end of the column is reached, reassembling those blocks
@@ -700,7 +700,7 @@ static int odbc_stmt_get_col(pdo_stmt_t *stmt, int colno, char **ptr, zend_ulong
 				}
 
 				/* resize output buffer and reassemble block */
-				if (rc==SQL_SUCCESS_WITH_INFO) {
+				if (rc==SQL_SUCCESS_WITH_INFO || (rc==SQL_SUCCESS && C->fetched_len > 255)) {
 					/* point 5, in section "Retrieving Data with SQLGetData" in http://msdn.microsoft.com/en-us/library/windows/desktop/ms715441(v=vs.85).aspx
 					 states that if SQL_SUCCESS_WITH_INFO, fetched_len will be > 255 (greater than buf2's size)
 					 (if a driver fails to follow that and wrote less than 255 bytes to buf2, this will AV or read garbage into buf) */