MariaDB · Report #3891337
Connector/ODBC SQLGetDescRec treats descriptor handle as statement handle, heap OOB write on every call
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.
- Build the driver from
masterHEAD4e175d9with 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 && make2. Connect to any MariaDB server with a plain DSN such asDRIVER=<built libmaodbc.so>;SERVER=127.0.0.1;PORT=3306;UID=root;PWD=...;DATABASE=test;SSLVERIFY=0;3. CallSQLAllocHandle(SQL_HANDLE_DESC, dbc, &desc)thenSQLGetDescRec(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, inMA_ClearErroratdriver/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. Startmitm_version.py 14001 3306 100and repeat step 4 pointing the DSN at port 14001. The process faults onmov BYTE PTR [rsi+rax*1+0xd8],0withraxequal to the bytes the MITM placed in the version string. 6. Fixed control: changeCHECK_STMT_CLEAR_ERRORtoCHECK_DESC_CLEAR_ERRORatdriver/odbc_3_api.cpp:855and: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.