singhpratech opened a new issue, #13537:
URL: https://github.com/apache/ignite/issues/13537

   Component: ODBC driver (`modules/platforms/cpp/odbc`), Ignite 2.17.0. Filed 
here rather than in JIRA since this repository accepts issues; happy to mirror 
it to IGNITE-* if preferred.
   
   ### Summary
   
   With `SQL_ATTR_PARAMSET_SIZE` > 1 and column-wise binding, the ODBC driver 
decides whether a parameter is NULL by looking at the indicator of *row 0* for 
every row of the array. A `SQL_NULL_DATA` indicator in any later row is 
therefore not sent as NULL:
   
   - for a `VARCHAR` column the row is stored as a non-NULL *empty string*, 
with `SQL_SUCCESS`, `SQL_ATTR_PARAMS_PROCESSED_PTR` = 3 and every parameter 
status `SQL_PARAM_SUCCESS`;
   - for a `BINARY`/`VARBINARY` column the client process segfaults inside 
`SQLExecute` (`memcpy` with length -1);
   - a `SQL_NULL_DATA` in row 0 makes *every* row of the array NULL, whatever 
the other indicators say.
   
   The server is untouched in all three cases. Row-wise binding cannot be used 
as a workaround: `SQL_ATTR_PARAM_BIND_TYPE` ≠ 0 is refused with `HYC00 Only 
binding by column is currently supported`.
   
   ### Environment
   
   - Apache Ignite 2.17.0 (`apacheignite/ignite:latest`), ODBC driver built 
from the image's `platforms/cpp` sources (CMake project version 2.17.0.25077, 
`-DWITH_ODBC=ON -DWITH_CORE=OFF`); the driver reports `SQL_DRIVER_VER` = 
`SQL_DBMS_VER` = 02.04.0000
   - Linux x86_64, unixODBC 2.3.12, gcc 13
   
   ### Reproduction (plain ODBC, no other library)
   
   ```c
   /* Column-wise parameter array with SQL_NULL_DATA below row 0, Apache Ignite 
2.17 ODBC.
    * Build: gcc -O1 -Wall null_below_row0.c -lodbc -o null_below_row0
    * Run:   
ODBC_CONN='Driver=/path/to/libignite-odbc.so;ADDRESS=127.0.0.1:10800;SCHEMA=PUBLIC;'
 \
    *        ./null_below_row0 varchar      # row 2 stored as '' instead of 
NULL, rc=0
    *        ./null_below_row0 binary       # segfault inside SQLExecute
    *        ./null_below_row0 row0         # NULL in row 0 -> all three rows 
NULL       */
   #include <sql.h>
   #include <sqlext.h>
   #include <stdio.h>
   #include <stdlib.h>
   #include <string.h>
   static SQLHENV env; static SQLHDBC dbc;
   static void diag(SQLSMALLINT t, SQLHANDLE h) {
     SQLCHAR st[6], msg[1024]; SQLINTEGER nat; SQLSMALLINT len, i = 1;
     while (SQLGetDiagRec(t, h, i++, st, &nat, msg, sizeof msg, &len) == 
SQL_SUCCESS)
       printf("    %s (%d): %s\n", st, (int)nat, msg);
   }
   static SQLRETURN exec(const char* sql) {
     SQLHSTMT h; SQLAllocHandle(SQL_HANDLE_STMT, dbc, &h);
     SQLRETURN rc = SQLExecDirect(h, (SQLCHAR*)sql, SQL_NTS);
     if (!SQL_SUCCEEDED(rc)) { printf("  %s -> rc=%d\n", sql, (int)rc); 
diag(SQL_HANDLE_STMT, h); }
     SQLFreeHandle(SQL_HANDLE_STMT, h); return rc;
   }
   int main(int argc, char** argv) {
     const char* mode = argc > 1 ? argv[1] : "varchar";
     int binary = !strcmp(mode, "binary"), row0 = !strcmp(mode, "row0");
     setvbuf(stdout, NULL, _IONBF, 0);
     SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &env);
     SQLSetEnvAttr(env, SQL_ATTR_ODBC_VERSION, (SQLPOINTER)SQL_OV_ODBC3, 0);
     SQLAllocHandle(SQL_HANDLE_DBC, env, &dbc);
     if (!SQL_SUCCEEDED(SQLDriverConnect(dbc, NULL, 
(SQLCHAR*)getenv("ODBC_CONN"), SQL_NTS, NULL, 0, NULL,
                                         SQL_DRIVER_NOPROMPT))) { 
diag(SQL_HANDLE_DBC, dbc); return 1; }
     SQLCHAR v[64]; SQLSMALLINT n;
     SQLGetInfo(dbc, SQL_DRIVER_VER, v, sizeof v, &n); 
printf("SQL_DRIVER_VER=%s\n", v);
     exec("DROP TABLE IF EXISTS probe_null");
     exec(binary ? "CREATE TABLE probe_null (a BIGINT PRIMARY KEY, b BINARY)"
                 : "CREATE TABLE probe_null (a BIGINT PRIMARY KEY, b VARCHAR)");
     SQLHSTMT h; SQLAllocHandle(SQL_HANDLE_STMT, dbc, &h);
     SQLBIGINT a[3] = {1, 2, 3}; SQLLEN ia[3] = {0, 0, 0};
     char b[3][8] = {"r1", "r2", "r3"}; SQLLEN ib[3];
     if (binary) { memset(b, 0xEE, sizeof b); ib[0] = 4; ib[1] = SQL_NULL_DATA; 
ib[2] = 4; }
     else        { ib[0] = SQL_NTS; ib[1] = SQL_NULL_DATA; ib[2] = SQL_NTS; }
     if (row0)   { ib[0] = SQL_NULL_DATA; ib[1] = SQL_NTS; }
     SQLULEN processed = 0; SQLUSMALLINT status[3] = {99, 99, 99};
     SQLSetStmtAttr(h, SQL_ATTR_PARAMSET_SIZE, (SQLPOINTER)(SQLULEN)3, 0);
     SQLSetStmtAttr(h, SQL_ATTR_PARAMS_PROCESSED_PTR, &processed, 0);
     SQLSetStmtAttr(h, SQL_ATTR_PARAM_STATUS_PTR, status, 0);
     SQLPrepare(h, (SQLCHAR*)"INSERT INTO probe_null (a, b) VALUES (?, ?)", 
SQL_NTS);
     SQLBindParameter(h, 1, SQL_PARAM_INPUT, SQL_C_SBIGINT, SQL_BIGINT, 0, 0, 
a, sizeof a[0], ia);
     if (binary) SQLBindParameter(h, 2, SQL_PARAM_INPUT, SQL_C_BINARY, 
SQL_BINARY, 8, 0, b, 8, ib);
     else        SQLBindParameter(h, 2, SQL_PARAM_INPUT, SQL_C_CHAR, 
SQL_VARCHAR, 8, 0, b, 8, ib);
     printf("mode=%s  indicators = {%ld, %ld, %ld}  (SQL_NULL_DATA = %d)\n", 
mode,
            (long)ib[0], (long)ib[1], (long)ib[2], SQL_NULL_DATA);
     SQLRETURN rc = SQLExecute(h);
     printf("SQLExecute -> rc=%d  processed=%lu  status=[%d %d %d]\n", (int)rc, 
(unsigned long)processed,
            status[0], status[1], status[2]);
     if (!SQL_SUCCEEDED(rc)) diag(SQL_HANDLE_STMT, h);
     SQLFreeHandle(SQL_HANDLE_STMT, h);
     SQLAllocHandle(SQL_HANDLE_STMT, dbc, &h);
     SQLExecDirect(h, (SQLCHAR*)"SELECT a, b IS NULL, LENGTH(b) FROM probe_null 
ORDER BY a", SQL_NTS);
     while (SQLFetch(h) == SQL_SUCCESS) {
       SQLBIGINT k = 0; SQLINTEGER isnull = -1, len = -1; SQLLEN l1, l2, l3;
       SQLGetData(h, 1, SQL_C_SBIGINT, &k, 0, &l1);
       SQLGetData(h, 2, SQL_C_SLONG, &isnull, 0, &l2);
       SQLGetData(h, 3, SQL_C_SLONG, &len, 0, &l3);
       printf("  row a=%lld  b IS NULL=%d  LENGTH(b)=%d%s\n", (long long)k, 
(int)isnull,
              l3 == SQL_NULL_DATA ? -1 : (int)len, l3 == SQL_NULL_DATA ? " 
(NULL)" : "");
     }
     SQLFreeHandle(SQL_HANDLE_STMT, h);
     exec("DROP TABLE probe_null");
     SQLDisconnect(dbc); SQLFreeHandle(SQL_HANDLE_DBC, dbc); 
SQLFreeHandle(SQL_HANDLE_ENV, env);
     return 0;
   }
   ```
   
   ```
   $ ./null_below_row0 varchar
   SQL_DRIVER_VER=02.04.0000
   mode=varchar  indicators = {-3, -1, -3}  (SQL_NULL_DATA = -1)
   SQLExecute -> rc=0  processed=3  status=[0 0 0]
     row a=1  b IS NULL=0  LENGTH(b)=2
     row a=2  b IS NULL=0  LENGTH(b)=0
     row a=3  b IS NULL=0  LENGTH(b)=2
   
   $ ./null_below_row0 row0
   SQL_DRIVER_VER=02.04.0000
   mode=row0  indicators = {-1, -3, -3}  (SQL_NULL_DATA = -1)
   SQLExecute -> rc=0  processed=3  status=[0 0 0]
     row a=1  b IS NULL=1  LENGTH(b)=-1 (NULL)
     row a=2  b IS NULL=1  LENGTH(b)=-1 (NULL)
     row a=3  b IS NULL=1  LENGTH(b)=-1 (NULL)
   
   $ ./null_below_row0 binary
   SQL_DRIVER_VER=02.04.0000
   mode=binary  indicators = {4, -1, 4}  (SQL_NULL_DATA = -1)
   Segmentation fault (SIGSEGV)
   ```
   
   gdb backtrace for the binary case:
   
   ```
   Program received signal SIGSEGV, Segmentation fault.
   #0  __memcpy_avx_unaligned_erms () at 
../sysdeps/x86_64/multiarch/memmove-vec-unaligned-erms.S:265
   #1  0x00007ffff7edf709 in 
ignite::odbc::query::BatchQuery::MakeRequestExecuteBatch(unsigned long, 
unsigned long, bool) () from <path>/libignite-odbc.so
   #2  0x00007ffff7edfe96 in ignite::odbc::query::BatchQuery::Execute() () from 
<path>/libignite-odbc.so
   #3  0x00007ffff7efd201 in ignite::odbc::Statement::ExecuteSqlQuery() () from 
<path>/libignite-odbc.so
   #4  0x00007ffff7ed0e8c in ignite::SQLExecute(void*) () from 
<path>/libignite-odbc.so
   #5  0x00007ffff7f407dd in SQLExecute () from 
/lib/x86_64-linux-gnu/libodbc.so.2
   #6  0x0000555555555826 in main (argc=<optimized out>, argv=<optimized out>) 
at null_below_row0.c:54
   ```
   
   ### Cause (`modules/platforms/cpp/odbc/src/app/parameter.cpp`, 2.17)
   
   `Parameter::Write` tests `buffer.GetInputSize()` on the *un-offset* buffer 
before it copies the buffer and applies the row's element offset:
   
   ```cpp
   void Parameter::Write(impl::binary::BinaryWriterImpl& writer, int offset, 
SqlUlen idx) const
   {
       if (buffer.GetInputSize() == SQL_NULL_DATA)   // reads *GetResLen() with 
elementOffset == 0, i.e. row 0
       {
           writer.WriteNull();
           return;
       }
   
       ApplicationDataBuffer buf(buffer);            // the copy...
       buf.SetByteOffset(offset);
       buf.SetElementOffset(idx);                    // ...is where the row 
offset is applied
   ```
   
   (`ApplicationDataBuffer::GetInputSize` returns `*GetResLen()`, and 
`GetResLen` applies `elementOffset`, which is still 0 on `buffer`.) So every 
row inherits row 0's NULL-ness. What happens next depends on the SQL-type 
branch that runs on the offset copy:
   
   - `SQL_CHAR`/`SQL_VARCHAR`: `buf.GetString(columnSize)` reads the row's own 
indicator (-1) and `utility::SqlStringToString` returns `""` for any negative 
length other than `SQL_NTS` — the row is written as an empty string.
   - `SQL_BINARY`/`SQL_VARBINARY`/`SQL_LONGVARBINARY`: the branch takes 
`*constRef.GetResLen()` — the row's own indicator, `SQL_NULL_DATA` = -1 — and 
passes it straight to `writer.WriteInt8Array(data, paramLen)` as the array 
length:
   
   ```cpp
   case SQL_BINARY:
   case SQL_VARBINARY:
   case SQL_LONGVARBINARY:
   {
       const ApplicationDataBuffer& constRef = buf;
       const SqlLen* resLenPtr = constRef.GetResLen();
       if (!resLenPtr)
           break;
       int32_t paramLen = static_cast<int32_t>(*resLenPtr);
       writer.WriteInt8Array(reinterpret_cast<const 
int8_t*>(constRef.GetData()), paramLen);
       break;
   }
   ```
   
   A length of -1 reaches `memcpy` as `size_t` and the process dies.
   
   ### Suggested fix
   
   Apply the element offset before the NULL test — build `buf` first and test 
`buf.GetInputSize() == SQL_NULL_DATA` — and have the binary branch treat a 
negative `*resLenPtr` as NULL (or `SQL_NTS`/`SQL_DATA_AT_EXEC` per the spec) 
rather than as a length. With that the three cases above become NULL, NULL and 
NULL.
   
   ### Two smaller things seen on the same path
   
   - `SQLRowCount` after a successful three-row array execute answers 1, not 3, 
although `SQL_PARAM_ARRAY_ROW_COUNTS` reports `SQL_PARC_BATCH`.
   - `SQLGetData` on a zero-length non-NULL character value returns 
`SQL_NO_DATA` and writes `SQL_NULL_DATA` into the indicator, so an empty string 
is indistinguishable from NULL on that path (which is why the program above 
reads `b IS NULL` and `LENGTH(b)` server-side).
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to