Skip to content

SQLGetData/SQLBindCol narrow an out-of-range int8 to SQL_C_SLONG (and int4/int8 to SQL_C_SSHORT) silently: 9223372036854775807::bigint reads as -1 under SQL_SUCCESS where ODBC specifies 22003 #207

Description

@singhpratech

What happens

Reading an integer that does not fit the requested C type returns SQL_SUCCESS and the low bits: 9223372036854775807::bigint as SQL_C_SLONG is -1, as SQL_C_SSHORT -1; 4294967297::bigint as SQL_C_SLONG is 1; 70000::int4 as SQL_C_SSHORT is 4464. No diagnostic, indicator set to the C type's size. ODBC specifies SQL_ERROR with SQLSTATE 22003 (numeric value out of range) for this conversion. SQL_C_SBIGINT is fine, and values that fit convert correctly.

Where it comes from (convert.c, copy_and_convert_field(), main at 82f041e, ~line 1938):

case SQL_C_SLONG:
case SQL_C_LONG:
    len = 4;
    if (bind_size > 0)
        *((SQLINTEGER *) rgbValueBindRow) = pg_atol(neut_str);
    else
        *((SQLINTEGER *) rgbValue + bind_row) = pg_atol(neut_str);
    break;

pg_atol (and pg_atoi in the SQL_C_SSHORT/SQL_C_USHORT branches, ~line 1921) parse into a wide integer and the assignment stores the low bits; nothing compares the parsed value against the C type's range.

Environment: psqlodbc 16.00.0000 (psqlodbcw.so, Ubuntu 24.04 odbc-postgresql 1:16.00.0000-1), unixODBC 2.3.12, Linux x86_64; PostgreSQL 16.15.

Standalone reproduction (gcc -O1 -Wall int8_narrow.c -lodbc):

/* psqlodbc: SQLGetData of an out-of-range int8 into SQL_C_SLONG returns SQL_SUCCESS with a wrapped value.
 * Build: gcc -O1 -Wall int8_narrow.c -lodbc    Run: ODBC_CONN='Driver=…/psqlodbcw.so;Server=…;Port=5432;Database=…;Uid=…;Pwd=…;' ./int8_narrow */
#include <sql.h>
#include <sqlext.h>
#include <stdio.h>
#include <stdlib.h>
static void diag(SQLSMALLINT t, SQLHANDLE h) {
  SQLCHAR st[6], msg[512]; 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 void read_as(SQLHDBC dbc, const char* sql, SQLSMALLINT ctype, const char* name) {
  SQLHSTMT h; SQLAllocHandle(SQL_HANDLE_STMT, dbc, &h);
  SQLExecDirect(h, (SQLCHAR*)sql, SQL_NTS); SQLFetch(h);
  SQLBIGINT big = 0; SQLINTEGER i32 = 0; SQLSMALLINT i16 = 0; SQLLEN ind = -99; SQLRETURN rc;
  if (ctype == SQL_C_SBIGINT) rc = SQLGetData(h, 1, ctype, &big, sizeof big, &ind);
  else if (ctype == SQL_C_SLONG) rc = SQLGetData(h, 1, ctype, &i32, sizeof i32, &ind);
  else rc = SQLGetData(h, 1, ctype, &i16, sizeof i16, &ind);
  printf("%-34s as %-13s rc=%d ind=%ld value=%lld\n", sql, name, (int)rc, (long)ind,
         ctype == SQL_C_SBIGINT ? (long long)big : ctype == SQL_C_SLONG ? (long long)i32 : (long long)i16);
  if (rc != SQL_SUCCESS) diag(SQL_HANDLE_STMT, h);
  SQLFreeHandle(SQL_HANDLE_STMT, h);
}
int main(void) {
  SQLHENV env; SQLHDBC dbc; 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 ", v);
  SQLGetInfo(dbc, SQL_DBMS_VER, v, sizeof v, &n); printf("SQL_DBMS_VER=%s\n", v);
  read_as(dbc, "SELECT 9223372036854775807::bigint", SQL_C_SBIGINT, "SQL_C_SBIGINT");
  read_as(dbc, "SELECT 9223372036854775807::bigint", SQL_C_SLONG,   "SQL_C_SLONG");
  read_as(dbc, "SELECT 9223372036854775807::bigint", SQL_C_SSHORT,  "SQL_C_SSHORT");
  read_as(dbc, "SELECT 4294967297::bigint",          SQL_C_SLONG,   "SQL_C_SLONG");
  read_as(dbc, "SELECT 70000::int4",                 SQL_C_SSHORT,  "SQL_C_SSHORT");
  read_as(dbc, "SELECT 2147483647::bigint",          SQL_C_SLONG,   "SQL_C_SLONG");
  SQLDisconnect(dbc); return 0;
}
$ ./int8_narrow
SQL_DRIVER_VER=16.00.0000 SQL_DBMS_VER=16.0.15
SELECT 9223372036854775807::bigint as SQL_C_SBIGINT rc=0 ind=8 value=9223372036854775807
SELECT 9223372036854775807::bigint as SQL_C_SLONG   rc=0 ind=4 value=-1
SELECT 9223372036854775807::bigint as SQL_C_SSHORT  rc=0 ind=2 value=-1
SELECT 4294967297::bigint          as SQL_C_SLONG   rc=0 ind=4 value=1
SELECT 70000::int4                 as SQL_C_SSHORT  rc=0 ind=2 value=4464
SELECT 2147483647::bigint          as SQL_C_SLONG   rc=0 ind=4 value=2147483647

Expected: rows 2–5 to return SQL_ERROR with 22003, leaving the buffer untouched; row 1 and row 6 as they are.

Suggested fix: parse into a 64-bit value (strtoll, with errno == ERANGE checked for int8 itself), compare against INT32_MIN/MAX (INT16_MIN/MAX for the short branches) before storing, and report 22003 — a new COPY_ result code next to COPY_RESULT_TRUNCATED in convert.h, mapped in the caller the way truncation already maps to 01004. (The SQL_C_NUMERIC path already detects its own overflow through parse_to_numeric_struct(); the integer branches just need the same care.)

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