Repository navigation
Client can abort ProxySQL with COM_REGISTER_SLAVE/COM_BINLOG_DUMP followed by another packet (assert in enter_permanent_fast_forward) #6494
Description
Summary
Any authenticated MySQL client can abort the whole ProxySQL process by sending COM_REGISTER_SLAVE, COM_BINLOG_DUMP or COM_BINLOG_DUMP_GTID together with another command packet (for example COM_PING) in the same write(). ProxySQL logs UNEXPECTED PACKET FROM CLIENT -- PLEASE REPORT A BUG and calls assert(0). ProxySQL builds do not define NDEBUG, so the assert is active in release packages too.
Affected versions / tiers
- All tiers (Stable v3.0.x, v3.1.x, v4.0.x).
- Pre-existing, not a regression: the assert came with fb47409 ("Commands that read the binlog enable fast forward", first released in 2.5.0) and is present in v3.0.11.
- Client command pipelining (Java MariaDB JDBC socket error with ProxySQL: unexpected end of stream / Unexpected packet from client during Hibernate update flow #5930, 0f7d02d) adds a second way to trigger it: a replication command and a following packet pipelined behind a running query are deferred and then replayed together.
Root cause
get_pkts_from_client() removes one packet from client_myds->PSarrayIN and dispatches replication commands to GPFC_Replication_SwitchToFastForward() (lib/MySQL_Session.cpp:6305-6309). The only check there is session_type. It then calls enter_permanent_fast_forward(), which re-queues the command so that it is the first packet forwarded to the backend, and asserts that nothing else is queued (lib/MySQL_Session.cpp:5710-5716):
int MySQL_Session::enter_permanent_fast_forward(PtrSize_t& pkt, int destination_hostgroup) {
session_fast_forward = SESSION_FORWARD_TYPE_PERMANENT;
if (client_myds->PSarrayIN->len) {
proxy_error("UNEXPECTED PACKET FROM CLIENT -- PLEASE REPORT A BUG\n");
assert(0);
}
client_myds->PSarrayIN->add(pkt.ptr, pkt.size);MySQL_Data_Stream::read_pkts() splits everything read from the socket into PSarrayIN, so a second packet in the same TCP read is enough to reach the assert. The query-rule fast-forward switch (GPFC_QueryRule_SwitchToFastForward(), lib/MySQL_Session.cpp:5891-5894) already rejects this case with an error; the replication path has no such check.
How to reproduce
Authenticate as any MySQL user (no replication privilege or fast_forward needed), then write these two packets in a single send():
05 00 00 00 15 ... COM_REGISTER_SLAVE (any body)
01 00 00 00 0e COM_PING
ProxySQL aborts. The same happens with COM_BINLOG_DUMP (0x12) or COM_BINLOG_DUMP_GTID (0x1e) instead of COM_REGISTER_SLAVE, or with COM_QUERY "SELECT SLEEP(1)", COM_REGISTER_SLAVE, COM_PING written together.
Impact
Denial of service: one authenticated client can stop the proxy for every other client. No backend is contacted before the abort.
Proposed fix
In GPFC_Replication_SwitchToFastForward(), before any state changes, check client_myds->PSarrayIN->len. If other client packets are already queued, free the packet, reply with an ERR packet (1815, HY000, "Replication commands cannot be pipelined with other client packets") and keep the session in WAITING_CLIENT_DATA, exactly as the existing non-MySQL-session branch of the same function does. The remaining packets are then processed normally, each with its own response in order. The warning is rate limited because a client can trigger it.
Rejecting only the command, rather than closing the connection as the query-rule path does, is possible here because nothing has changed yet: no query was started and no qpo was allocated. The query-rule path has to close because RequestEnd() has already freed qpo. Forwarding the queued packets to the backend after the replication command would also be possible, but it would mean reordering PSarrayIN and replaying an arbitrary command stream on a replication connection. Real replicas wait for the reply to COM_REGISTER_SLAVE and never send anything after COM_BINLOG_DUMP, so no real replica can pipeline these commands.
Is this really a bug?
Yes. A process-wide assert(0) on client-controlled input is a remote crash, whatever the client is doing. The only question is which response is correct. A real MySQL server would answer both commands, but no legitimate replica pipelines these commands. An error for the replication command, followed by normal handling of the remaining packets, is protocol-correct and avoids any partial switch to fast-forward.
Activity
fix(mysql): reject pipelined replication commands instead of asserting
fix(mysql): reject pipelined replication commands instead of asserting