Describe the bug, including details regarding any error messages, version, and platform.
In the Arrow Flight SQL ODBC driver, SQLDescribeCol returns garbage in the
upper bytes of the column_size (a.k.a. ColumnSizePtr) output argument for
numeric / decimal columns. Only the low 2 bytes are written; the upper 6 bytes
are left uninitialized.
Root cause — a write-width mismatch.
DescriptorRecord::precision is declared SQLSMALLINT (2 bytes) in
cpp/src/arrow/flight/sql/odbc/odbc_impl/odbc_descriptor.h:
SQLSMALLINT precision = 0; // ~line 58
...
SQLSMALLINT scale = 0; // ~line 60
ODBCDescriptor::GetField(SQL_DESC_PRECISION, ...) forwards to the GetAttribute
template in cpp/src/arrow/flight/sql/odbc/odbc_impl/odbc_descriptor.cc
(~line 417):
case SQL_DESC_PRECISION:
GetAttribute(record.precision, value, buffer_length, output_length);
break;
and that template (cpp/src/arrow/flight/sql/odbc/odbc_impl/attribute_utils.h)
copies exactly sizeof(T) bytes — T being deduced from the field, i.e.
SQLSMALLINT (2 bytes). The buffer_length argument is ignored for the
write:
template <typename T, typename O>
inline void GetAttribute(T attribute_value, SQLPOINTER output, O output_size,
O* output_len_ptr) {
if (output) {
T* typed_output = reinterpret_cast<T*>(output); // reinterprets the caller's pointer
*typed_output = attribute_value; // writes only sizeof(T) = 2 bytes
}
...
}
But in SQLDescribeCol
(cpp/src/arrow/flight/sql/odbc/odbc_api.cc, ~line 1584) the numeric path
passes the SQLULEN* column_size_ptr (8 bytes) directly and claims
sizeof(SQLULEN):
case SQL_DOUBLE: {
ird->GetField(column_number, SQL_DESC_PRECISION, column_size_ptr,
sizeof(SQLULEN), nullptr);
break;
}
GetAttribute then reinterprets the SQLULEN* as a SQLSMALLINT* and writes
only 2 bytes. The upper 6 bytes of the caller's SQLULEN remain
uninitialized garbage. An application that reads the full 8-byte column_size
(the correct ODBC type for ColumnSizePtr) sees a wildly wrong value for every
DECIMAL / NUMERIC / integer / REAL / FLOAT / DOUBLE column.
Two related, lower-severity smells at the decimal_digits sites. The
SQL_DESC_SCALE path (~line 1606) and the datetime/interval SQL_DESC_PRECISION
path (~line 1624) pass sizeof(SQLULEN) (8) as the buffer size even though
decimal_digits_ptr is a SQLSMALLINT* (2 bytes) and the underlying field is a
2-byte SQLSMALLINT. These do not corrupt memory today (the write width is
2 bytes, matching the 2-byte target), but the claimed buffer size is wrong and
misleading. They should pass sizeof(SQLSMALLINT).
Trace of the affected call sites (all in odbc_api.cc::SQLDescribeCol):
SQL_DESC_PRECISION → column_size_ptr (numeric/decimal) — the real bug:
2-byte write into an 8-byte SQLULEN, upper 6 bytes garbage.
SQL_DESC_SCALE → decimal_digits_ptr (exact numeric) — wrong sizeof, no
corruption.
SQL_DESC_PRECISION → decimal_digits_ptr (datetime / interval-with-seconds)
— wrong sizeof, no corruption.
Introduced by
PR #48052 (GH-47724, "[C++][FlightRPC][ODBC] implement
SQLDescribeCol"). The bug is still present on main.
Component(s)
C++, FlightRPC
Describe the bug, including details regarding any error messages, version, and platform.
In the Arrow Flight SQL ODBC driver,
SQLDescribeColreturns garbage in theupper bytes of the
column_size(a.k.a.ColumnSizePtr) output argument fornumeric / decimal columns. Only the low 2 bytes are written; the upper 6 bytes
are left uninitialized.
Root cause — a write-width mismatch.
DescriptorRecord::precisionis declaredSQLSMALLINT(2 bytes) incpp/src/arrow/flight/sql/odbc/odbc_impl/odbc_descriptor.h:ODBCDescriptor::GetField(SQL_DESC_PRECISION, ...)forwards to theGetAttributetemplate in
cpp/src/arrow/flight/sql/odbc/odbc_impl/odbc_descriptor.cc(~line 417):
and that template (
cpp/src/arrow/flight/sql/odbc/odbc_impl/attribute_utils.h)copies exactly
sizeof(T)bytes —Tbeing deduced from the field, i.e.SQLSMALLINT(2 bytes). Thebuffer_lengthargument is ignored for thewrite:
But in
SQLDescribeCol(
cpp/src/arrow/flight/sql/odbc/odbc_api.cc, ~line 1584) the numeric pathpasses the
SQLULEN* column_size_ptr(8 bytes) directly and claimssizeof(SQLULEN):GetAttributethen reinterprets theSQLULEN*as aSQLSMALLINT*and writesonly 2 bytes. The upper 6 bytes of the caller's
SQLULENremainuninitialized garbage. An application that reads the full 8-byte
column_size(the correct ODBC type for
ColumnSizePtr) sees a wildly wrong value for everyDECIMAL/NUMERIC/ integer /REAL/FLOAT/DOUBLEcolumn.Two related, lower-severity smells at the
decimal_digitssites. TheSQL_DESC_SCALEpath (~line 1606) and the datetime/intervalSQL_DESC_PRECISIONpath (~line 1624) pass
sizeof(SQLULEN)(8) as the buffer size even thoughdecimal_digits_ptris aSQLSMALLINT*(2 bytes) and the underlying field is a2-byte
SQLSMALLINT. These do not corrupt memory today (the write width is2 bytes, matching the 2-byte target), but the claimed buffer size is wrong and
misleading. They should pass
sizeof(SQLSMALLINT).Trace of the affected call sites (all in
odbc_api.cc::SQLDescribeCol):SQL_DESC_PRECISION → column_size_ptr(numeric/decimal) — the real bug:2-byte write into an 8-byte
SQLULEN, upper 6 bytes garbage.SQL_DESC_SCALE → decimal_digits_ptr(exact numeric) — wrongsizeof, nocorruption.
SQL_DESC_PRECISION → decimal_digits_ptr(datetime / interval-with-seconds)— wrong
sizeof, no corruption.Introduced by
PR #48052 (GH-47724, "[C++][FlightRPC][ODBC] implement
SQLDescribeCol"). The bug is still present on
main.Component(s)
C++, FlightRPC