Bug #80460 [Asn->Ana]: ODBC doesn't account for SQL_NO_TOTAL indicator, causing segmentation fault

From: Date: Fri, 26 Mar 2021 10:16:36 +0000
Subject: Bug #80460 [Asn->Ana]: ODBC doesn't account for SQL_NO_TOTAL indicator, causing segmentation fault
References: 1  Groups: php.bugs 
Request: Send a blank email to php-bugs+get-233004@lists.php.net to get a copy of this message
Edit report at https://bugs.php.net/bug.php?id=80460&edit=1

 ID:                 80460
 Updated by:         cmb@php.net
 Reported by:        mirish at ibm dot com
 Summary:            ODBC doesn't account for SQL_NO_TOTAL indicator,
                     causing segmentation fault
-Status:             Assigned
+Status:             Analyzed
 Type:               Bug
 Package:            ODBC related
 Operating System:   RHEL 7.9
 PHP Version:        Irrelevant
 Assigned To:        cmb
 Block user comment: N
 Private report:     N

 New Comment:

Thanks for the clarification!  Indeed, these SQL_NO_TOTAL checks
are necessary.


Previous Comments:
------------------------------------------------------------------------
[2021-03-25 20:04:34] mirish at ibm dot com

Ok, a little clarification: It looks like my particular error is generated from the StrLen_or_IndPtr
bound from SQLBindCol: https://docs.microsoft.com/en-us/sql/odbc/reference/syntax/sqlbindcol-function?view=sql-server-ver15

The valid values are still:
* The length of the data available to return
* SQL_NO_TOTAL
* SQL_NULL_DATA

Then, when odbc_fetch_row is called, SQLExtendedFetch gets called (in my particular test, but
SQLFetch and SQLGetData can equally return the same thing), populates that indicator bound in
SQLBindCol, and SQL_NO_TOTAL is returned to that StrLen_or_IndPtr, which is mapped in the php_odbc.c
code to result->values[field_ind].vallen.

Then, the code tries to generate a string from that length (-4), causing the seg fault.

Backtrace:

#0  0x00007f0d54ed9474 in __memcpy_ssse3_back () from /usr/lib64/libc.so.6
#1  0x0000000000591919 in zend_string_init (str=0x7f0d54491028 "",
len=18446744073709551612, persistent=0)
    at /home/mirish/php-src/Zend/zend_string.h:157
#2  0x0000000000596dc1 in zif_odbc_result (execute_data=0x7f0d54414150, return_value=0x7f0d54414120)
    at /home/mirish/php-src/ext/odbc/php_odbc.c:2224
#3  0x000000000085cbb1 in ZEND_DO_ICALL_SPEC_RETVAL_USED_HANDLER () at
/home/mirish/php-src/Zend/zend_vm_execute.h:1313
#4  0x00000000008bc92c in execute_ex (ex=0x7f0d54414020) at
/home/mirish/php-src/Zend/zend_vm_execute.h:53564
#5  0x00000000008c09fa in zend_execute (op_array=0x7f0d5447e400, return_value=0x0)
    at /home/mirish/php-src/Zend/zend_vm_execute.h:57664
#6  0x00000000007ef9fc in zend_execute_scripts (type=8, retval=0x0, file_count=3)
    at /home/mirish/php-src/Zend/zend.c:1663
#7  0x000000000075bba0 in php_execute_script (primary_file=0x7ffd3adb2620) at
/home/mirish/php-src/main/main.c:2619
#8  0x00000000008c31c6 in do_cli (argc=2, argv=0x2c11700) at
/home/mirish/php-src/sapi/cli/php_cli.c:961
#9  0x00000000008c4105 in main (argc=2, argv=0x2c11700) at
/home/mirish/php-src/sapi/cli/php_cli.c:1352


And for good measure, here is the ODBC trace:

...
[ODBC][88259][1616702310.287337][SQLExecDirect.c][240]
		Entry:
			Statement = 0x1f66a90
			SQL = [SELECT * FROM MIRISH.UTF8TEST][length = 29 (SQL_NTS)]
[ODBC][88259][1616702310.468968][SQLExecDirect.c][521]
		Exit:[SQL_SUCCESS]
[ODBC][88259][1616702310.469037][SQLNumResultCols.c][156]
		Entry:
			Statement = 0x1f66a90
			Column Count = 0x7effad458730
[ODBC][88259][1616702310.469087][SQLNumResultCols.c][251]
		Exit:[SQL_SUCCESS]
			Count = 0x7effad458730 -> 1
[ODBC][88259][1616702310.469128][SQLColAttribute.c][294]
		Entry:
			Statement = 0x1f66a90
			Column Number = 1
			Field Identifier = SQL_DESC_NAME
			Character Attr = 0x7effad45c140
			Buffer Length = 256
			String Length = 0x7fff4dbce140
			Numeric Attribute = (nil)
[ODBC][88259][1616702310.469188][SQLColAttribute.c][709]
		Exit:[SQL_SUCCESS]
[ODBC][88259][1616702310.469217][SQLColAttribute.c][294]
		Entry:
			Statement = 0x1f66a90
			Column Number = 1
			Field Identifier = SQL_DESC_CONCISE_TYPE
			Character Attr = (nil)
			Buffer Length = 0
			String Length = (nil)
			Numeric Attribute = 0x7effad45c250
[ODBC][88259][1616702310.469252][SQLColAttribute.c][709]
		Exit:[SQL_SUCCESS]
[ODBC][88259][1616702310.469280][SQLColAttribute.c][294]
		Entry:
			Statement = 0x1f66a90
			Column Number = 1
			Field Identifier = SQL_DESC_OCTET_LENGTH
			Character Attr = (nil)
			Buffer Length = 0
			String Length = (nil)
			Numeric Attribute = 0x7fff4dbce138
[ODBC][88259][1616702310.469309][SQLColAttribute.c][709]
		Exit:[SQL_SUCCESS]
[ODBC][88259][1616702310.469338][SQLBindCol.c][236]
		Entry:
			Statement = 0x1f66a90
			Column Number = 1
			Target Type = 1 SQL_CHAR
			Target Value = 0x7effad491028
			Buffer Length = 2
			StrLen Or Ind = 0x7effad45c248
[ODBC][88259][1616702310.469382][SQLBindCol.c][344]
		Exit:[SQL_SUCCESS]
[ODBC][88259][1616702310.469452][SQLExtendedFetch.c][166]
		Entry:
			Statement = 0x1f66a90
			Fetch Type = 1
			Row = 1
			PcRow = 0x7fff4dbce1a0
			Row Status = 0x7fff4dbce190
[ODBC][88259][1616702310.560382][SQLExtendedFetch.c][339]
		Exit:[SQL_SUCCESS]

------------------------------------------------------------------------
[2021-03-25 17:46:25] mirish at ibm dot com

Last comment should read:

result->values[i].vallen == SQL_NO_TOTAL

Similar to how it checks for SQL_NULL_DATA.

------------------------------------------------------------------------
[2021-03-25 17:44:23] mirish at ibm dot com

cmb@php.net noted:

"While the other bug report was indeed about SQL_NO_DATA, the fix
is supposed to cater to any result other than SQL_SUCCESS or
SQL_SUCCESS_WITH_INFO, and to return early in those cases."

I think I see the confusion now. When SQLGetData returns SQL_NO_TOTAL, it is doing so in the
StrLen_or_IndPtr parameter, not as the return code: https://docs.microsoft.com/en-us/sql/odbc/reference/syntax/sqlgetdata-function?view=sql-server-ver15

The issue you linked does indeed check the return code, but the issue here is that the valid values
returned from the StrLen_or_IndPtr are:

* The length of the data available to return
* SQL_NO_TOTAL
* SQL_NULL_DATA

The current code checks for SQL_NULL_DATA, and assigns a value of null if so. Otherwise, it just
assumes the value stored there is the length in bytes returned and tries to allocate a buffer of
that size. But that isn't the case when it returns SQL_NO_TOTAL (-4), which will try to
allocated a buffer of -4 and cause a segfault. There needs to be another check for:

result->values[i].vallen == SQL_NO_TOTAL

Similar to how it checks for SQL_NO_DATA.

I will work to generate a backtrace, might take a moment of relearning how to build PHP on my
system.

------------------------------------------------------------------------
[2021-02-08 13:30:14] cmb@php.net

> according to IBM support we have a related problem […]

This ticket is about a segmentation fault in the ODBC extension;
your issue is about truncation of strings in the PDO_ODBC
extension.  Please report that as separate ticket.

------------------------------------------------------------------------
[2021-02-08 13:12:34] ml at menten dot com

Hello,

according to IBM support we have a related problem reading special characters from VARGRAPHIC fields
in PHP 7.3.26 with the following driver: 

IBM i Access Client Solutions - PASE Application Package for IBM i.
Connectivity to Db2® for i using ODBC with unixODBC
ibm-iaccess.ppc64 1.1.0.14-0 @/ibm-iaccess-1.1.0.14-0.ibmi7.2.ppc64

If we write to a VARGRAPHIC field of length 70, for example, we can write the full 70 characters,
but we can only read them if the 70 characters do not consist of special characters or if there are
at most 35 special characters (e.g. ä).
For example, if we have i special characters and 69 non-special characters in the field, it cannot
be read out either. Analogously, this problem also occurs with CHAR and VARCHAR fields. 

Test Script:


<?php
ini_set('display_errors', true);
header('Content-Type: text/html; charset=utf-8');
ini_set("default_mimetype", "text/html");
ini_set("default_charset", "UTF-8");




// setup
$user = "test";
$pw = "test";


// full 70 length / no special characters -> OK
#	$test = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";

// 35 length / all special characters -> OK
#	$test =
"äääääääääääääääääääääääääääääääääää";

// 36 length / 35 special characters, 1 normal character -> no result
$test =
"äääääääääääääääääääääääääääääääääää1";

// 52 length / 35 normal characters, 17 special character -> OK
#	$test =
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaäääääääääääääääää";

// 53 length / 35 normal characters, 18 special character -> no result
#	$test =
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaääääääääääääääääää";

// full 70 length / 70 special characters -> no result
#	$test =
"ääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääääää";



try
{
	$connection = new PDO("odbc:DRIVER={IBM i Access ODBC
Driver};SYSTEM=localhost;DATABASE=*LOCAL;NAM=1;DBQ=IEFFECTDB,IEFFECT28;CCSID=1208;DEBUG=524288",
$user, $pw, array(PDO::ATTR_ERRMODE => PDO::ERRMODE_SILENT));
}
catch (Exception $e)
{
	echo($e->getMessage());
	echo "<br>";
	echo($e->getCode());
	
	die();
}


// create table
$query = "CREATE TABLE IEFFECTDB/TEST (TEST1 VARGRAPHIC(70) ccsid 1200 NOT null DEFAULT
'')";
$params = array();
$stmt = $connection->prepare($query);
$result = $stmt->execute($params);


// insert
$query = "INSERT INTO IEFFECTDB/TEST (TEST1) VALUES (?)";
$params = array($test);
$stmt = $connection->prepare($query);
$result = $stmt->execute($params);


// select
$query = "SELECT TEST1 FROM IEFFECTDB/TEST";
$params = array();
$stmt = $connection->prepare($query);
$result = $stmt->execute($params);
echo "<pre>";
var_dump($stmt->fetch(PDO::FETCH_ASSOC));
echo "<pre>";


// drop table
$query = "DROP TABLE IEFFECTDB/TEST";
$params = array();
$stmt = $connection->prepare($query);
$result = $stmt->execute($params);


$connection = null;

echo
"------------------------------------------------------------------------------------------------------------";
echo "<br><br>";


try
{
	$connection = new PDO("odbc:DRIVER={IBM i Access ODBC
Driver};SYSTEM=localhost;DATABASE=*LOCAL;NAM=1;DBQ=IEFFECTDB,IEFFECT28;CCSID=1208", $user, $pw,
array(PDO::ATTR_ERRMODE => PDO::ERRMODE_SILENT));
}
catch (Exception $e)
{
	echo($e->getMessage());
	echo "<br>";
	echo($e->getCode());
	
	die();
}


// create table
$query = "CREATE TABLE IEFFECTDB/TEST (TEST1 VARGRAPHIC(70) ccsid 1200 NOT null DEFAULT
'')";
$params = array();
$stmt = $connection->prepare($query);
$result = $stmt->execute($params);


// insert
$query = "INSERT INTO IEFFECTDB/TEST (TEST1) VALUES (?)";
$params = array($test);
$stmt = $connection->prepare($query);
$result = $stmt->execute($params);


// select
$query = "SELECT TEST1 FROM IEFFECTDB/TEST";
$params = array();
$stmt = $connection->prepare($query);
$result = $stmt->execute($params);
echo "<pre>";
var_dump($stmt->fetch(PDO::FETCH_ASSOC));
echo "<pre>";


// drop table
$query = "DROP TABLE IEFFECTDB/TEST";
$params = array();
$stmt = $connection->prepare($query);
$result = $stmt->execute($params);

------------------------------------------------------------------------


The remainder of the comments for this report are too long. To view
the rest of the comments, please view the bug report online at

    https://bugs.php.net/bug.php?id=80460


--
Edit this bug report at https://bugs.php.net/bug.php?id=80460&edit=1


Thread (19 messages)

« previous php.bugs (#233004) next »