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]