Edit report at https://bugs.php.net/bug.php?id=81688&edit=1
ID: 81688
Updated by: cmb@php.net
Reported by: calvin at cmpct dot info
Summary: PDO_ODBC doesn't handle fixed-length character
columns with character conversio
Status: Open
Type: Bug
Package: ODBC related
Operating System: IBM i 7.2
PHP Version: 8.0.13
Block user comment: N
Private report: N
New Comment:
Well, the relevant differenec to the Node ODBC library is
apparently that PDO does not rely on SQLDescribeCol() to detect
the size, but rather on SQLColAttribute(SQL_DESC_DISPLAY_SIZE)[1].
Whether that is *supposed* to return the "correct" length is not
clear to me. Could you please check what
SQLColAttribute(SQL_DESC_OCTET_LENGTH) would yield? Might be the
same value as SQL_DESC_DISPLAY_SIZE, but maybe not.
> there's seemingly a possibility of a buffer overflow
I don't think that is possible, since the buffer size is passed
to SQLBindCol(), so only truncation may happen.
Note that I'm not generally against overallocation, but would try
to avoid it for the general case. Some setting (not necessarily
INI) might be an option. But please check SQL_DESC_OCTET_LENGTH
first.
[1] <https://github.com/php/php-src/blob/php-8.0.13/ext/pdo_odbc/odbc_stmt.c#L604>
Previous Comments:
------------------------------------------------------------------------
[2021-12-02 18:30:32] calvin at cmpct dot info
Description:
------------
(was kinda wanting to wait until the transition to GH issues, but deciding to post this now... let
me know if i should)
It seems PDO_ODBC doesn't handle situations where if characters are converted as part of a data
bind. For example, if the CHAR(30) column on the database stores as one encoding and it gets
converted to something requiring more bytes to represent, there's seemingly a possibility of a
buffer overflow (what looks like to me, anyways). The example script has one such example; an é
takes one byte to represent in some encodings, and two in UTF-8.
Db2 is a pathological case because most columns are stored as some kind of EBCDIC encoding and gets
converted to usually UTF-8 on the client, but it's quite possible for other databases too (i.e.
ISO-8859-1 columns to UTF-8). The example I posted would probably trigger the same issue if the
driver is converting.
Looking at the unixODBC trace a user provided, I can see PHP just allocates what the driver claims
is the CHAR(n) size plus one.
PHP PDO_ODBC:
```
[ODBC][24294][1638369671.619132][SQLDescribeCol.c][504]
Exit:[SQL_SUCCESS]
Column Name = [VARTE1]
Data Type = 70000000009b318 -> 1
Column Size = fffffffffffce78 -> 30 (64 bits)
Decimal Digits = NULLPTR
Nullable = NULLPTR
[ODBC][24294][1638369671.619175][SQLColAttribute.c][294]
Entry:
Statement = 180847ad0
Column Number = 1
Field Identifier = SQL_DESC_DISPLAY_SIZE
Character Attr = 0
Buffer Length = 0
String Length = 0
Numeric Attribute = fffffffffffce70
[ODBC][24294][1638369671.619219][SQLColAttribute.c][709]
Exit:[SQL_SUCCESS]
[ODBC][24294][1638369671.619261][SQLBindCol.c][245]
Entry:
Statement = 180847ad0
Column Number = 1
Target Type = 1 SQL_CHAR
Target Value = 700000000056540
Buffer Length = 31
StrLen Or Ind = 70000000009b310
[ODBC][24294][1638369671.619304][SQLBindCol.c][353]
Exit:[SQL_SUCCESS]
```
For comparison, Node ODBC library:
```
[ODBC][24271][1638369403.771225][SQLDescribeCol.c][504]
Exit:[SQL_SUCCESS]
Column Name = [VARTE1]
Data Type = 1805796a4 -> 1
Column Size = 1805796a8 -> 30 (64 bits)
Decimal Digits = 1805796b0 -> 0
Nullable = 1805796c0 -> 0
[ODBC][24271][1638369403.771312][SQLBindCol.c][245]
Entry:
Statement = 180552e70
Column Number = 1
Target Type = 1 SQL_CHAR
Target Value = 180583130
Buffer Length = 121
StrLen Or Ind = 1805437d0
[ODBC][24271][1638369403.771392][SQLBindCol.c][353]
Exit:[SQL_SUCCESS]
```
It looks to be overallocating, per: https://github.com/markdirish/node-odbc/blob/master/src/odbc_connection.cpp#L1659
ibm_db2 actually has a flag (i5_dbcs_alloc) in case of these situations. It is a bit crude, but
seeing other ODBC consumers do it is kind of suggesting to me it may be a valid solution.
The other option might be to reallocate more aggressively if the buffer is inadequate. I'm
unsure how tricky that'd be to implement. The annoying option for users but simplest to prevent
crashes in PHP is to emit a warning if the driver is trying to return more that what PHP allocated.
Test script:
---------------
<?php
// SQL to repro
create table calvin.bigchars(
x char(30)
);
// character that must be representable as a single char in DB but expanded on client (i.e. accented
e in ISO-8859 or EBCDIC is multiple bytes in UTF-8)
insert into calvin.bigchars
values('éééééééééééééééééééééééééééééé');
// 30
insert into calvin.bigchars
values('ééééééééééééééé'); // 15
insert into calvin.bigchars values('ééééé'); // 5
// End SQL
//Connect to IBM i
$user = 'user';
$pw = 'password';
// CCSID 1208 for UTF-8, 819 for ISO-8859-1
$dsn = 'Driver={IBM i Access ODBC
Driver};System=127.0.0.1;AlwaysCalculateResultLength=1;CCSID=1208';
$persistence = false;
// create pdo odbc connection
try {
$pdo_odbc = new PDO("odbc:$dsn", $user, $pw, array(
PDO::ATTR_PERSISTENT => $persistence,
PDO::ATTR_ERRMODE => PDO::ERRMODE_WARNING,
));
} catch (PDOException $e) {
die($e->getMessage());
}
$sql = "select * from calvin.bigchars";
$pdoStmt = $pdo_odbc->prepare($sql);
echo "before PDO\n";
var_dump($pdoStmt->execute());
echo "after PDO\n";
while($record = $pdoStmt->fetch(PDO::FETCH_ASSOC)){
echo "Record\n";
var_dump($record["X"]);
}
Actual result:
--------------
before PDO
bool(true)
after PDO
Record
string(60) "éééééééééééééééT`�
��sql"
Record
string(45) "éééééééééééééééT`�
"
Record
string(35) "ééééé "
------------------------------------------------------------------------
--
Edit this bug report at https://bugs.php.net/bug.php?id=81688&edit=1