HexBrain

MariaDB · Report #3891337

Connector/ODBC SQLGetDescRec treats descriptor handle as statement handle, heap OOB write on every call

Validstatus
mediumseverity
3upvotes
4 Nov 2026disclosed

How it was found

Connector/ODBC SQLGetDescRec treats a descriptor handle as a statement handle

SQLGetDescRec and SQLGetDescRecW in MariaDB Connector/ODBC clear the error record on their handle using the statement-handle macro, on a handle that is a descriptor. The driver therefore writes through a MADB_Stmt* that actually points at a smaller MADB_Desc object, overrunning it by 13 bytes on every call. The offset of a second write in the same macro is read from a field that the driver fills with the server's version string, so a malicious or MITM server chooses where that write lands.

Details

The two functions use the wrong macro at driver/odbc_3_api.cpp:855 and :874:

SQLRETURN SQL_API SQLGetDescRec(SQLHDESC DescriptorHandle, ...)
{
  CHECK_STMT_CLEAR_ERROR(DescriptorHandle);
  return MADB_DescGetRec(DescriptorHandle, ...);

The two functions immediately above them, SQLGetDescField at :824 and SQLGetDescFieldW at :837, use CHECK_DESC_CLEAR_ERROR on the same parameter, which is the correct one.

CHECK_STMT_CLEAR_ERROR expands to MA_ClearError(SQL_HANDLE_STMT, handle), whose statement branch is MADB_CLEAR_ERROR(&((MADB_Stmt*)Handle)->Error) at driver/ma_api_internal.cpp:58. Taken from the debug information of the built driver:

sizeof(MADB_Desc)                 = 720
offsetof(MADB_Desc,  Error)       = 96
offsetof(MADB_Stmt,  Error)       = 192
offsetof(MADB_Error, SqlErrorMsg) = 24
offsetof(MADB_Error, SqlState)    = 537

So the driver treats desc + 192 as a MADB_Error. Two consequences follow from the body of the macro in driver/ma_error.h:166:

#define MADB_CLEAR_ERROR(a) do { \
  (a)->SqlState[0]= (a)->SqlState[1]= (a)->SqlState[2]= (a)->SqlState[3]= (a)->SqlState[4]= '0'; \
  (a)->SqlErrorMsg[(a)->PrefixLen]= 0; \

First, the SqlState writes land at desc + 729 through desc + 733, past the end of the 720-byte allocation. This happens on every call, with no server involvement.

Second, SqlErrorMsg[PrefixLen] reads PrefixLen from desc + 192. That address falls inside the descriptor's own Error.SqlErrorMsg buffer at index 72, and MADB_DescInit at driver/ma_desc.cpp:96 fills that buffer by calling MADB_PutErrorPrefix, which writes "[ma-<version>][" + mysql_get_server_info() + "]". The literal prefix ahead of the server version is 12 bytes, so once the server sends a version string of 61 bytes or more, bytes 72 to 79 of that buffer are server-chosen, and they are used as the size_t offset for the write.

The driver does not require TLS on a default DSN. It only enables encryption when an SSL option is explicitly set, at driver/ma_connection.cpp:778, so the handshake is attacker-controlled on a plain connection.

Confirmed on the current master HEAD 4e175d9, MARIADB_ODBC_VERSION 03.02.0010, against MariaDB 11.4.12 through unixODBC 2.3.14, with an unmodified driver/ tree.

Key evidence. Allocating an explicit descriptor and calling SQLGetDescRec on it, driver built with AddressSanitizer, ordinary honest server:

==15407==ERROR: AddressSanitizer: heap-buffer-overflow
WRITE of size 1
    #0 MA_ClearError driver/ma_api_internal.cpp:58
    #1 SQLGetDescRec driver/odbc_3_api.cpp:855

0x7305ae4e185d is located 13 bytes after 720-byte region [0x7305ae4e1580,0x7305ae4e1850)
allocated by MADB_DescInit driver/ma_desc.cpp:92

On a stock build with no sanitizer and the same honest server, the 13-byte overrun corrupts the neighbouring heap chunk and glibc aborts the process, exit code 134.

Through a MITM that rewrites only the handshake version string to 100 bytes, with the eight bytes that land at index 72 set to MMMMMMMM, the stock build faults here:

Program received signal SIGSEGV
MA_ClearError (...) at driver/ma_api_internal.cpp:58
=> 0x7ffff7ca11a8 <MA_ClearError+152>:	mov    BYTE PTR [rsi+rax*1+0xd8],0x0
rax            0x4d4d4d4d4d4d4d4d
rsi            0x5555555e53f0

rsi holds the descriptor, 0xd8 is 216, which is offsetof(MADB_Stmt,Error) plus offsetof(MADB_Error,SqlErrorMsg), and rax is exactly the eight bytes the MITM placed in the version string. The server selects the offset of the write.

Fixed control: changing only the two macros to CHECK_DESC_CLEAR_ERROR, matching SQLGetDescField, removes both effects. AddressSanitizer reports no MA_ClearError overflow, and the stock driver through the same MITM exits 0.

PoC

Full runnable evidence including the fixed control is in the attached bundle, run_poc.sh plus poc_evidence.txt. The MITM is mitm_version.py, about 100 lines, which rewrites only the version string in the initial handshake and relays every other byte untouched.

  1. Build the driver from master HEAD 4e175d9 with AddressSanitizer: cmake .. -DCMAKE_C_FLAGS="-fsanitize=address -g" -DCMAKE_CXX_FLAGS="-fsanitize=address -g" -DWITH_SSL=OPENSSL -DWITH_EXTERNAL_ZLIB=OFF -DWITH_UNIT_TESTS=OFF && make 2. Connect to any MariaDB server with a plain DSN such as DRIVER=<built libmaodbc.so>;SERVER=127.0.0.1;PORT=3306;UID=root;PWD=...;DATABASE=test;SSLVERIFY=0; 3. Call SQLAllocHandle(SQL_HANDLE_DESC, dbc, &desc) then SQLGetDescRec(desc, 1, name, sizeof(name), &namelen, &type, &subtype, &length, &prec, &scale, &nullable). AddressSanitizer reports a heap-buffer-overflow write 13 bytes past the 720-byte descriptor allocation, in MA_ClearError at driver/ma_api_internal.cpp:58. 4. Repeat step 3 on a stock non-sanitized build. glibc aborts the process with exit code 134 on the corrupted neighbouring chunk. 5. Start mitm_version.py 14001 3306 100 and repeat step 4 pointing the DSN at port 14001. The process faults on mov BYTE PTR [rsi+rax*1+0xd8],0 with rax equal to the bytes the MITM placed in the version string. 6. Fixed control: change CHECK_STMT_CLEAR_ERROR to CHECK_DESC_CLEAR_ERROR at driver/odbc_3_api.cpp:855 and :874, rebuild, and repeat steps 3 through 5. AddressSanitizer is silent and the stock driver exits 0.

CVSS 3.1: AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:L/A:H = 6.5 Medium. AC:H reflects the two preconditions, an application that calls SQLGetDescRec and, for the server-influenced write, a network position or a hostile server.

Suggested fix: use CHECK_DESC_CLEAR_ERROR in both functions, matching the adjacent SQLGetDescField and SQLGetDescFieldW.

Impact

Memory corruption in the client process that loads the driver. The 13-byte overwrite past the descriptor allocation is unconditional for any application that calls SQLGetDescRec or SQLGetDescRecW, and on a stock build it is already enough to abort the process through glibc's heap consistency check against an ordinary honest server.

With a malicious or MITM server the second write becomes remotely influenced: the server supplies eight bytes that are used as the offset, so the driver writes a zero byte at a location the server picks relative to the descriptor. The offset bytes have to be legal characters inside a version string, so reachable offsets are large and coarse, and what has been demonstrated is a remotely triggered crash with memory corruption rather than precise targeting. No authentication to the real server is required, because the handshake is processed before authentication completes and the driver does not enforce TLS on a default DSN.

This report was disclosed publicly on HackerOne and belongs to its author and the program. See the original