HexBrain

MariaDB · Report #3872248

Connector/C++: heap out-of-bounds read when malicious server field length exceeds declared column length in binary result path

Validstatus
mediumseverity
6upvotes
12 Oct 2026disclosed

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 == 0 but not length_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.

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