Skip to content

Commit 10f8b5d

Browse files
authored
fix: Handle non-STRING types in ParseRedis to prevent crash (#5937)
Fixed: #5931
1 parent 2023a64 commit 10f8b5d

File tree

2 files changed

+56
-2
lines changed

2 files changed

+56
-2
lines changed

src/facade/dragonfly_connection.cc

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1231,8 +1231,17 @@ Connection::ParserStatus Connection::ParseRedis(unsigned max_busy_cycles) {
12311231
result = redis_parser_->Parse(read_buffer.slice, &consumed, &tmp_parse_args_);
12321232
request_consumed_bytes_ += consumed;
12331233
if (result == RedisParser::OK && !tmp_parse_args_.empty()) {
1234-
if (RespExpr& first = tmp_parse_args_.front(); first.type == RespExpr::STRING)
1235-
DVLOG(2) << "Got Args with first token " << ToSV(first.GetBuf());
1234+
// Check if the first argument is a STRING type. Commands must be strings.
1235+
// If we get a non-STRING type (e.g., ARRAY from empty array *0), it's a protocol error.
1236+
if (tmp_parse_args_.front().type != RespExpr::STRING) {
1237+
LOG(WARNING) << "Invalid command - expected STRING type, got type: "
1238+
<< tmp_parse_args_.front().type;
1239+
// Treat this as a parser error
1240+
result = RedisParser::BAD_STRING;
1241+
break;
1242+
}
1243+
1244+
DVLOG(2) << "Got Args with first token " << ToSV(tmp_parse_args_.front().GetBuf());
12361245

12371246
if (io_req_size_hist)
12381247
io_req_size_hist->Add(request_consumed_bytes_);

tests/dragonfly/connection_test.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1382,3 +1382,48 @@ async def test_client_migrate(df_server: DflyInstance):
13821382
assert resp == 0 # Not migrated as the client does not exist
13831383
resp = await client2.execute_command("CLIENT", "MIGRATE", client_id, dest_tid)
13841384
assert resp == 1 # migrated successfully
1385+
1386+
1387+
@dfly_args({})
1388+
async def test_issue_5931_malformed_protocol_crash(df_server: DflyInstance):
1389+
"""
1390+
Regression test for #5931
1391+
1392+
The crash.txt file contains malformed RESP protocol that caused the server to crash
1393+
with: "Check failed: RespExpr::STRING == arg.type" in FromArgs()
1394+
1395+
This test sends the exact bytes from crash.txt to verify the server handles it
1396+
gracefully without crashing.
1397+
"""
1398+
# Open raw TCP connection to send malformed protocol
1399+
reader, writer = await asyncio.open_connection("127.0.0.1", df_server.port)
1400+
1401+
try:
1402+
# Send the exact bytes from crash.txt:
1403+
# *0\r\n$5\r\nMULTI\r\n*3\r\n$3\r\nSET\r\n$1\r\na\r\n$1\r\n1\r<0xf4>)1\r\n$4\r\nEXEC\r\n
1404+
crash_data = b"*0\r\n$5\r\nMULTI\r\n*3\r\n$3\r\nSET\r\n$1\r\na\r\n$1\r\n1\r"
1405+
crash_data += bytes([0xF4]) # Binary byte instead of \n
1406+
crash_data += b")1\r\n$4\r\nEXEC\r\n"
1407+
1408+
writer.write(crash_data)
1409+
await writer.drain()
1410+
1411+
try:
1412+
response = await asyncio.wait_for(reader.read(1024), timeout=2.0)
1413+
# If we get a response, it should be an error, not a crash
1414+
# The server is still running if we got here
1415+
except asyncio.TimeoutError:
1416+
# Timeout is acceptable - connection might be closed
1417+
pass
1418+
except ConnectionError:
1419+
# Connection closed is acceptable - server detected bad protocol
1420+
pass
1421+
1422+
finally:
1423+
writer.close()
1424+
await writer.wait_closed()
1425+
1426+
# Verify server is still running by making a normal request
1427+
client = df_server.client()
1428+
await client.ping()
1429+
assert await client.ping() == True

0 commit comments

Comments
 (0)