|
php.net | support | documentation | report a bug | advanced search | search howto | statistics | random bug | login |
[2020-12-02 02:46 UTC] mirish at ibm dot com
Description: ------------ The ODBC extension (ext/odbc/php_odbc.c) causes a segmentation fault when SQL_NO_TOTAL indicator is returned to a bound column from a call to SQLFetch and similar functions. There are two major issues: 1. There is an assumption that the number of bytes returned from a call to SQLColAttribute with a type of SQL_DESC_OCTET_LENGTH will return the number of bytes required to store the value. There are cases when data is stored in an encoding which stores character data in a single byte, which then needs to be converted to variable multi-byte when SQLFetch is actually called (eg. EBCDIC 278 to UTF8 conversion). This might be a broken driver implementation on our part, but the ODBC docs are rather thin and at times seem contradictory on this point. This will cause a buffer to be allocated of size X, where up to X * 4 may be needed to store a string. There is a variable in the code (charextraalloc) to allocate buffers of this size, but none of the checks work for ODBC v 3.0+ drivers. When the buffer is too small, the driver may return SQL_NO_TOTAL when in the StrLen_or_IndPtr, as it didn't have enough space for the column, and might not know how many bytes are left. 2. The bigger issue is that calls to SQLFetch can return SQL_NO_TOTAL to the StrLen_or_IndPtr stored on SQLBindCol when the buffer bound isn't large enough for the data, but the code isn't checking for this. Instead, the code checks if the value of StrLen_or_IndPtr is SQL_NULL_DATA and assigns a value to null. If it isn't SQL_NULL_DATA, it assumes that it holds the length of the string, then calls functions like ZVAL_STRINGL and RETURN_STRINGL with SQL_NO_TOTAL (which evaluates to -4) as the length. Trying to create a string with length of -4 causes a segmentation fault, which causes the program to crash. As mentioned, the first issue might be a problem in driver implementation, but the second definitely needs to be fixed. SQL_NO_TOTAL is definitely a valid StrLen_or_IndPtr value, and there are no checks for it in the code, causing a segfault. PatchesPull Requests
Pull requests:
HistoryAllCommentsChangesGit/SVN commits
|
|||||||||||||||||||||||||||||||||||||
Copyright © 2001-2025 The PHP GroupAll rights reserved. |
Last updated: Thu Oct 30 18:00:02 2025 UTC |
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);@mirish, could you please test the provided patch[1]? It would also be great if you could run the test suite before and after applying the patch, to see whether we already have tests covering this scenario. If you build from source, you can run the test suite by running make test TESTS=ext/odbc/tests You need to set the environment variables ODBC_TEST_DSN, ODBC_TEST_USER and ODBC_TEST_PASS for that to work. If the existing tests do not (sufficently) cover this scenario, it would be great if you could provide (an) additional test(s). [1] <https://github.com/php/php-src/pull/6809>I have tested the patch and it does work (no more segfaults!), but I have a few thoughts: 1. I'm not sure just throwing an error is the best solution. If the driver returns SQL_NO_TOTAL, there is still good data in the buffer, it just isn't ALL of the data (and the driver doesn't want to calculate how much is left, probably because it would have to translate to a different character encoding). The string data in the buffer should be null-terminated[1], so using ZVAL_STRING instead of ZVAL_STRINGL should retrieve all of the good data from the buffer. Its possible this would be a good change for ALL ZVAL_STRINGLs in the code, but to minimize the amount of regressions added to the code I think it would be fine adding to the if-else blocks you've already created. I think users might still want to know that not all data was returned, so keeping the warning is probably a good idea: ... } else if (result->values[i].vallen == SQL_NO_TOTAL) { ZVAL_STRING(&tmp, buf); php_error_docref(NULL, E_WARNING, "Cannot get all data of column #%d (driver cannot determine length)", i + 1, rc); } else { ... 2. Although the case of SQL_NO_TOTAL should still be handled, I think most of it can be avoided if the variable charextraalloc is always set to true, multiplying whatever is returned from SQL_DESC_OCTET_LENGTH by 4. Currently it only sets this variable to true in a few cases, but it should probably be broadened. I have tested several ODBC 3.0-compliant drivers and they don't seem to treat SQL_DESC_OCTET_LENGTH in a standardized way when the schema/table/field character encoding differs from the client character encoding. For instance, a field with a character encoding of Windows-1251 could have a CHAR(1) field that holds the † character in a single byte (0x86). If the client wants data returned in UTF-8, this expands to become 0xE2 0x80 0xA0, requiring 3 bytes. Like I said, drivers seem to handle this case differently: FreeTDS and the IBM i Access driver would return 1, while the MySQL driver actually checks to see the maximum UTF-8 expansion and returns 3. The safest solution, even if it wastes a tiny amount of space in the buffer for a short amount of time, is to multiply by 4. Always multiplying by 4 SHOULD ensure enough buffer space so that all of the data is returned and so SQL_NO_TOTAL isn't encountered. In my test case, when I set charextraalloc to always be true, my queries return data perfectly. 3. I am not having much luck running the ODBC tests, I pass in the environment variables (or even export them) and try to run it, but 20 of the 21 tests are skipped as "could not connect". Will continue to try. I don't think there are any tests that check for SQL_NO_TOTAL, and since it happens in a very particular circumstance it would be hard to force, but I can write some tests that at least change the StrLen_or_IndPtr value after it is returned to force the code to take the SQL_NO_TOTAL path. [1]: There is an option in SQLSetEnvAttr called SQL_ATTR_OUTPUT_NTS that controls whether character data returned is null-terminated. It looks like you CAN set it to SQL_FALSE, but the docs note: "A call to SQLSetEnvAttr to set it to SQL_FALSE returns SQL_ERROR and SQLSTATE HYC00 (Optional feature not implemented)." Not sure if that is MSSQL-specific, but I have never known any one to try to use this feature.