Bug #81688 [Com]: PDO_ODBC doesn't handle fixed-length character columns with character conversio

From: Date: Tue, 07 Dec 2021 16:54:56 +0000
Subject: Bug #81688 [Com]: PDO_ODBC doesn't handle fixed-length character columns with character conversio
References: 1  Groups: php.bugs 
Request: Send a blank email to php-bugs+get-238267@lists.php.net to get a copy of this message
Edit report at https://bugs.php.net/bug.php?id=81688&edit=1

 ID:                 81688
 Comment by:         calvin at cmpct dot info
 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:

Interesting observation: if you try to cast so PHP picks a bigger buffer size that might be able to
accommodate the conversion, you still get garbage at the end:

i.e. select cast(x as char(150)) as X from calvin.bigchars

gets you

```
Record
string(180)
"éééééééééééééééééééééééééééééé
                                                                                         0
(*Wind!@.0*) Gecko* "
Record
string(165) "ééééééééééééééé                      
                                                                                                 0
(*Windo"
Record
string(155) "ééééé                                                              
                                                                             0 (*"
```

(I'm guessing that's garbage in memory left over from parsing browscap, since this is
CLI...)


Previous Comments:
------------------------------------------------------------------------
[2021-12-07 16:13:58] kadler at us dot ibm dot com

I know my colleague has done some investigation here as to how different databases/drivers react
when running in different encodings between the database and the client. IIRC most drivers
don't do any conversion and just give back the data in the original encoding (which works
"ok" for the most part when everyone is some ASCII variant, but not so when one is
EBCDIC).

As to how the PHP code works using SQL_DESC_DISPLAY_SIZE, this is the docs:

https://docs.microsoft.com/en-us/sql/odbc/reference/syntax/sqlcolattribute-function?view=sql-server-ver15

"SQL_DESC_DISPLAY_SIZE - Maximum number of characters required to display data from the column.
For more information about display size, see Column Size, Decimal Digits, Transfer Octet Length, and
Display Size in Appendix D: Data Types."

Of course, the link to the Appendix isn't any more enlightening:

https://docs.microsoft.com/en-us/sql/odbc/reference/appendixes/column-size-decimal-digits-transfer-octet-length-and-display-size?view=sql-server-ver15

"The display size value for all data types corresponds to the value in a single descriptor
field, SQL_DESC_DISPLAY_SIZE."


In the straightforward reading of the spec, I suppose one could argue that the driver should account
for any encoding conversion of the data that it will do.

------------------------------------------------------------------------
[2021-12-03 19:05:15] calvin at cmpct dot info

I agree overallocation is ugly and something we want to avoid if we can help it.

SQL_DESC_OCTET_LENGTH is the same as the display length for the column (in this case, 30) when I
write a program to confirm this in C. I'm guessing this is technically correct because
it's what it's true for how the database may be representing it, but not after the driver
does conversion for the client's encoding.

My comment about the overflow is more about how there's garbage at the end there; I suspect
it's more not truncating and letting it go out reading stuff in the abyss. Or I'm also
reading the situation wrong, and it's reallocating the buffer, just not doing it correctly in a
way that leaves garbage. Orrrrrrr the driver isn't truncating properly; either way, I'll
probably bug the IBM people responsible for the driver and get their read on the situation.

(Apologies if you see dupes, bugs.php keeps timing out...)

------------------------------------------------------------------------
[2021-12-03 18:03:22] cmb@php.net

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>

------------------------------------------------------------------------
[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


Thread (37 messages)

« previous php.bugs (#238267) next »