Skip to content

ODBC: column-wise parameter array reads the NULL indicator of row 0 for every row — a NULL below row 0 stores an empty string (VARCHAR) or segfaults the client in SQLExecute (BINARY) #13537

Description

@singhpratech

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)

/* 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:

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:
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).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions