MariaDB · Report #3920647
Connector/ODBC: MADB_Stmt::ArrayOffset is not reset when an exception unwinds out of MADB_StmtExecute, so the next paramset reads out of bounds
How it was found
An aborted parameter-array execute leaves MADB_Stmt::ArrayOffset stale, and the next paramset operation on the same statement handle indexes the application's arrays from that offset. When a server-reported error interrupts a parameter array, MADB_DoExecute throws a C++ exception that unwinds out of MADB_StmtExecute, skipping Stmt->AfterExecute(), the only place ArrayOffset is reset. MA_SQLExecute catches the exception and returns SQL_ERROR, so the application sees an ordinary error and a handle that looks reusable. The next array execute reads out of bounds.
Demonstrated as an out-of-bounds read. No out-of-bounds write was found, and MADB_SetStatusArray was checked and indexes from zero.
Root cause
driver/ma_statement.cpp:1281 executes one row of the paramset and :1283 advances the offset:
ret= MADB_DoExecute(Stmt);
++Stmt->ArrayOffset;
MADB_DoExecute reaches Protocol::executePreparedQuery, which calls throwStmtError on any server error. The exception unwinds past Stmt->AfterExecute() at :1313:
void MADB_Stmt::AfterExecute()
{
/* All rows processed, so we can unset ArrayOffset */
ArrayOffset= 0;
and is caught in driver/ma_api_internal.cpp:1145:
try
{
return Stmt->Methods->Execute(Stmt, false);
}
catch (SQLException &e)
{
return MADB_FromException(Stmt->Error, e);
}
Nothing on that path restores ArrayOffset. The same reset is also skipped by every goto end in MADB_StmtExecute, since the end: label at :1315 sits after the AfterExecute() call at :1313.
The stale value is consumed by driver/ma_codec.cpp:37:
IndicatorMapper::IndicatorMapper(MADB_Stmt *Stmt) :
statusArr(Stmt->Apd->Header.ArrayStatusPtr + Stmt->ArrayOffset),
and dereferenced at :52:
bool IndicatorMapper::operator()(void *data, MYSQL_BIND *bind, uint32_t col_nr, uint32_t row_nr) {
if (statusArr && statusArr[row_nr] == SQL_PARAM_IGNORE) {
The pointer arithmetic is unconditional, so it has a second effect: when the application has not set SQL_ATTR_PARAM_OPERATION_PTR, ArrayStatusPtr is NULL and a stale offset turns it into a small non-NULL value, defeating the statusArr && guard on the next line and producing a wild read.
Stmt->Apd->Header.ArrayStatusPtr is the application's SQL_ATTR_PARAM_OPERATION_PTR array, which the ODBC specification sizes at SQL_ATTR_PARAMSET_SIZE entries, so any non-zero offset reads past it.
Steps to reproduce
poc/poc_arrayoffset_bulk.c, driven by poc/run_poc.sh. Batch 1 binds row-wise, which makes MADB_BulkInsertPossible false so the row loop runs; its last row duplicates a primary key. Batch 2 rebinds column-wise so the bulk path runs. The two runs differ only in whether batch 1's last row collides.
Negative control, every id in batch 1 unique:
[*] batch #1 (row-wise, 8 rows, last row unique -> succeeds)
SQLExecute rc=0 (ok)
[*] batch #2 (column-wise -> bulk path, 8 rows)
SQLExecute rc=0
[*] finished without a memory error
Positive, batch 1's last row violates the primary key:
[*] batch #1 (row-wise, 8 rows, last row DUPLICATE PK -> errors)
SQLExecute rc=-1 (error, as intended)
[batch1] 23000: Duplicate entry '9999' for key 'PRIMARY'
[*] batch #2 (column-wise -> bulk path, 8 rows)
==43102==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x6e0adefe6560
READ of size 2 at 0x6e0adefe6560 thread T0
#0 mariadb::IndicatorMapper::operator()(...) driver/ma_codec.cpp:52
#1 withRowCheckCallback driver/cppmini/class/ServerSidePreparedStatement.cpp:381
0x6e0adefe6560 is located 0 bytes after 16-byte region [0x6e0adefe6550,0x6e0adefe6560)
The 16-byte region is the application's 8-entry SQL_ATTR_PARAM_OPERATION_PTR array. Without that array set, the same sequence instead faults on a near-NULL address (SEGV on unknown address 0x0e, register rsi = 0x7 showing the leaked offset).
Affected versions and suggested fix
mariadb-connector-odbc master 4e175d9. HEAD is unchanged since 2026-07-06.
Reset ArrayOffset on every exit from MADB_StmtExecute, not only the successful one. A scope guard in MADB_StmtExecute, or moving the reset into the catch in ma_api_internal.cpp:1145 alongside MADB_FromException, covers both the unwind and the goto end paths. Independently, IndicatorMapper should not perform the pointer addition before testing ArrayStatusPtr for NULL, since that ordering is what converts a NULL array into a wild pointer.
ma_codec.cpp and ma_bulk.cpp had not been analysed on prior passes against this repo, and none of the upstream JIRA fixes referenced in the git log (ODBC-489, 490, 493, 494, 495, 496) touch ArrayOffset or ma_codec.cpp.
This is a client library with no MariaDB-operated instance, so no production confirmation is possible and none is claimed.
All poc/... paths above are inside the attached mariadb_connector_odbc_arrayoffset_poc_by_fg0x0.zip.
Impact
An out-of-bounds read at an offset carried over from a previous failed batch, in a client-side library, reached without any hostile server. The application does nothing wrong: it runs a parameter-array insert, one row is rejected by the server, and it runs another batch on the same handle. Batch reinsertion after a constraint violation is a normal recovery pattern, and an attacker who controls one value in a batch, for example a record that collides with an existing unique key, decides whether the condition is armed.
Two observable outcomes:
- Where the application has not set
SQL_ATTR_PARAM_OPERATION_PTR, the NULL guard is defeated and the process reliably crashes. For a shared service that is a denial of service triggered by ordinary user data. - Where the array is set, the value read out of bounds decides whether a parameter row is treated as
SQL_PARAM_IGNORE, so rows can be silently dropped from a batch the application believes it inserted. That is silent data loss with no error surfaced to the application.
The offset also survives into any later paramset operation on that handle, not only the immediately following one, because nothing else resets it.
PR:L is used in the vector because reaching the trigger requires the ability to place a value into a batch the application submits, which in practice means an authenticated or otherwise accepted user of the application rather than an anonymous network attacker.