MariaDB · Report #3890352
Connector/C++ aborts client process on server session-tracking packet via std::stoi
How it was found
QueryProtocol::handleStateChange reads the session-tracking system-variable entry from an OK packet, compares it against the literal "auto_increment_increment", and then calls std::stoi on that same string. The string is the variable NAME, not its value, so whenever the branch is taken std::stoi is guaranteed to throw std::invalid_argument. sql::SQLException derives from std::runtime_error, so a std::logic_error is caught by nothing in the driver and nothing in a correctly written application. It unwinds out of main and terminates the process. Two packets from a malicious or MITM-positioned server abort the client.
Details
src/protocol/capi/QueryProtocol.cpp:1520-1540:
if (mysql_session_track_get_first(connection, static_cast<enum capi::enum_session_state_type>(type), &value, &len) == 0)
{
std::string str(value, len);
switch (type) {
case StateChange::SESSION_TRACK_SYSTEM_VARIABLES:
if (str.compare("auto_increment_increment") == 0)
{
autoIncrementIncrement= std::stoi(str);
For SESSION_TRACK_SYSTEM_VARIABLES the protocol carries a name followed by a value, and mysql_session_track_get_first returns the name. The value requires a follow-up call to mysql_session_track_get_next, which never happens. So the code has just proved str is the non-numeric literal "auto_increment_increment" and then asks std::stoi to parse it. The throw is not an edge case, it is the only possible outcome of entering the branch.
The exception type is what makes this a process abort rather than a handled error. include/conncpp/Exception.hpp:32:
class SQLException : public ::std::runtime_error
std::invalid_argument derives from std::logic_error, a sibling of std::runtime_error. An application following the documented contract and catching sql::SQLException does not catch it, and neither do the driver's own catch(SQLException&) and catch(std::runtime_error&) handlers on the execute path.
The trigger is entirely server-controlled. handleStateChange is called at :1512 whenever the server sets the session-state-changed bit in the status flags:
if ((serverStatus & ServerStatus::SERVER_SESSION_STATE_CHANGED_)!=0) {
handleStateChange(results);
}
The client advertises session tracking as part of libmariadb's default capability set, so no non-default option is needed. A genuine server reaches this too if an administrator adds auto_increment_increment to session_track_system_variables.
PoC
-
Run a server that completes a normal handshake advertising CLIENT_SESSION_TRACK, then answers any statement with an OK packet whose status flags include SERVER_SESSION_STATE_CHANGED and whose session_state_changes payload carries a SESSION_TRACK_SYSTEM_VARIABLES entry for auto_increment_increment:
python3 poc/rogue_server.py --port 13742 --session-track
-
Run a client that connects with default options, executes one statement, and catches only sql::SQLException as the API documents.
-
Negative control first, with the same server and the session-tracking payload disabled:
-- negative control: identical server, no session-track payload --
[client] completed normally
exit=0
- Positive, with the payload enabled:
-- positive: SESSION_TRACK_SYSTEM_VARIABLES=auto_increment_increment --
terminate called after throwing an instance of 'std::invalid_argument'
what(): stoi
exit=134 (signal 6 = SIGABRT)
The process dumps core. A client that additionally catches std::exception, which the API does not require, sees the escaped type directly:
[client] std::exception (St16invalid_argument): stoi
Scenario A (sanity): rogue server without the session-track payload -> [client] completed normally, exit=0 Scenario B (negative control): same server, same client binary, payload disabled -> clean exit Scenario C (attack): SESSION_TRACK_SYSTEM_VARIABLES=auto_increment_increment -> terminate called after throwing std::invalid_argument, exit=134 (SIGABRT), core dumped
Artifacts:
- poc/poc_evidence.txt
- poc/poc_sessiontrack.cpp, poc/rogue_server.py (--session-track)
- Affected: src/protocol/capi/QueryProtocol.cpp:1535, reached from :1512, at HEAD c57f17f
Impact
A malicious or MITM-positioned server terminates the client process at will, immediately after the handshake and before the application does anything meaningful. There is no way for the application to defend itself while still honouring the documented exception contract, because the type thrown is outside that contract. For a long-running service holding a connection pool this is a remote crash of the whole process, not a failed query.
Who is affected: any application that connects to a database endpoint an attacker controls or can intercept. Default configuration, no application misuse required.
The correct fix is to fetch the value with mysql_session_track_get_next before parsing it, and to parse it with something that reports failure rather than throwing, or to wrap the conversion so only SQLException escapes. Auditing the driver for other std::stoi and std::string::at calls on server-supplied data is worthwhile, since the same escape route applies to all of them.