MariaDB · Report #3872248
Connector/C++: heap out-of-bounds read when malicious server field length exceeds declared column length in binary result path
How it was found
MariaDB Connector/C++ sizes each result-column bind buffer to the column length the server declared at PREPARE time, but then reads bind.length_value - the actual field length the server reports in each row packet - bytes out of that buffer. libmariadb sets length_value to the full server-sent field length even when it only copied MIN(len, buffer_length) bytes into the smaller buffer. The C++ layer never clamps MIN(length_value, buffer_length). A malicious or MITM server that declares a small column but sends a larger field value drives a heap out-of-bounds read, leaking adjacent process memory into application-visible column values and crashing the client.
Confirmed end-to-end with ASAN (heap-buffer-overflow READ) and a non-ASAN release build (heap leak + crash) at HEAD c57f17f (VERSION 1.0.8 dev).
Root cause
Buffer allocation, frozen at PREPARE time (src/protocol/capi/BinRowProtocolCapi.cpp:55-73):
length = columnInfo->getLength(); // server-declared column length
bind.back().buffer_length =
columnInfo->getColumnType().binarySize() != 0 ?
columnInfo->getColumnType().binarySize() :
getLengthMaxFieldSize(); // == declared column length for string/blob types
bind.back().buffer = new uint8_t[bind.back().buffer_length]; // line 69: small allocation
bind.back().length = &bind.back().length_value;
libmariadb reports untruncated field length (libmariadb/ma_stmt_codec.c:599):
// convert_froma_string: copies MIN(len, buffer_length) bytes but reports full length
*r_param->length = (ulong)len; // full server field length, uncapped
The C++ layer trusts length_value as the readable extent:
// setPosition BinRowProtocolCapi.cpp:107-108
length = bind[index].length_value; // server field length, uncapped
fieldBuf.wrap(static_cast<char*>(bind[index].buffer), length); // wraps small buffer with large length
// cacheCurrentRow BinRowProtocolCapi.cpp:1716
rowDataCache.emplace_back(static_cast<const char*>(b.buffer),
b.length_value); // copies length_value bytes from buffer_length-sized allocation
When length_value > buffer_length, the difference is read out of bounds from the heap.
The vulnerable binary path is reached with useServerPrepStmts=true or ANY prepareCall(...) stored procedure call (which always creates a ServerSidePreparedStatement regardless of the option - MariaDbProcedureStatement.cpp:59,83).
Steps To Reproduce
Environment: Linux x86-64, connector built from HEAD c57f17f with AddressSanitizer.
Step 1 - Start the rogue server (rogue_server.py attached):
python3 rogue_server.py --port 13399 --col-length 4 --field-len 40000 --once &
The server speaks enough protocol for the binary prepared-statement flow: handshake, session SELECTs, COM_STMT_PREPARE (one STRING column, declared length 4), COM_STMT_EXECUTE (binary row with field length 40000).
Step 2 - POSITIVE (attack) - real ASAN-built connector (poc_client.cpp attached):
./poc_client 127.0.0.1 13399
Output:
==ERROR: AddressSanitizer: heap-buffer-overflow ... READ of size 40000
#1 sql::CArray<char>::CArray(const char*, unsigned long) CArrayImp.h:68
#5 BinRowProtocolCapi::cacheCurrentRow(...) BinRowProtocolCapi.cpp:1716
#6 SelectResultSetCapi::cacheCompleteLocally() SelectResultSetCapi.cpp:2039
#11 BasePrepareStatement::executeQuery() BasePrepareStatement.cpp:1441
0x...494 is located 0 bytes after 4-byte region [0x...490,0x...494)
allocated by thread T0 here:
#1 BinRowProtocolCapi::BinRowProtocolCapi(...) BinRowProtocolCapi.cpp:69
The OOB read fires inside executeQuery() in the default buffered path.
Step 3 - NEGATIVE CONTROL (honest server: declared length = field length):
python3 rogue_server.py --port 13401 --col-length 40000 --field-len 40000 --once &
./poc_client 127.0.0.1 13401
Output:
[client] got string, length=40000
[client] done (no crash) # exit 0, zero ASAN errors
No OOB when the server doesn't lie. The mismatch is the sole cause.
Real-world impact on non-ASAN release build:
Info leak (col=4, field=64):
[client] first 64 bytes (hex): 41414141 5a770000 201b3f81 5a770000 5868fb03 16610000 ...
^^^^^^^^ 4 bytes stored ^^^^^^^^^^^^^ adjacent process heap (heap pointers visible)
DoS (col=4, field=500000000): process crash + core dump (exit 134).
Supporting Material/References
- Vulnerable allocation:
BinRowProtocolCapi.cpp:69(new uint8_t[buffer_length]) - OOB read sink:
BinRowProtocolCapi.cpp:1716(cacheCurrentRow) - Incomplete fix context: HEAD commit CONCPP-152 hardened
length_value == 0but notlength_value > buffer_length - Accepted sibling: H1 #3835450 (connector-nodejs malicious-server DoS, Medium)
- Attachments:
rogue_server.py,poc_client.cpp,run_poc.sh,poc_evidence.txt
Impact
Information disclosure: Adjacent process heap memory is copied into the value the connector returns to the application as an ordinary column string. In a connection-pooled server - the dominant deployment for a database connector - that adjacent heap can contain other requests'/users' data, credentials, or pointers usable for ASLR defeat. Demonstrated: app SELECTs a 4-wide column and receives 64 bytes containing 4 bytes of actual data followed by 60 bytes of leaked heap (with visible heap pointers).
Denial of service: An attacker-chosen length_value (up to 4 GB) drives the OOB memcpy far past the small allocation, reliably crashing the client process. Demonstrated: field length 500MB causes process abort + core dump.
Preconditions: The client connects to a malicious or MITM-controlled server and reads a result set via the binary protocol (useServerPrepStmts=true or any stored procedure call). No credentials or privileges required. A single crafted result row is sufficient.