Skip to content

Commit edf5f25

Browse files
authored
fix: reject non-STRING arguments in RESP protocol to prevent crash (#5955)
* fix: reject non-STRING arguments in RESP protocol to prevent crash * fix: review comments
1 parent 4f8b677 commit edf5f25

File tree

2 files changed

+50
-6
lines changed

2 files changed

+50
-6
lines changed

src/facade/dragonfly_connection.cc

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1231,12 +1231,11 @@ 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-
// 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
1234+
// If we get a non-STRING type (e.g., NIL, ARRAY), it's a protocol error.
1235+
bool valid_input = std::all_of(tmp_parse_args_.begin(), tmp_parse_args_.end(),
1236+
[](const auto& arg) { return arg.type == RespExpr::STRING; });
1237+
if (!valid_input) {
1238+
LOG(WARNING) << "Invalid argument - expected all STRING types";
12401239
result = RedisParser::BAD_STRING;
12411240
break;
12421241
}

tests/dragonfly/connection_test.py

100755100644
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1427,3 +1427,48 @@ async def test_issue_5931_malformed_protocol_crash(df_server: DflyInstance):
14271427
client = df_server.client()
14281428
await client.ping()
14291429
assert await client.ping() == True
1430+
1431+
1432+
@dfly_args({})
1433+
async def test_issue_5949_nil_bulk_string_crash(df_server: DflyInstance):
1434+
"""
1435+
Regression test for #5949
1436+
1437+
The crash1.txt and crash2.txt files contain malformed RESP protocol with NIL bulk
1438+
strings ($-1) as command arguments, which caused the server to crash with:
1439+
"Check failed: RespExpr::STRING == arg.type" in FromArgs()
1440+
1441+
According to RESP protocol spec, NIL bulk strings are valid for server responses
1442+
but NOT for command arguments sent by clients. Commands must be arrays of bulk strings.
1443+
"""
1444+
# Open raw TCP connection to send malformed protocol
1445+
reader, writer = await asyncio.open_connection("127.0.0.1", df_server.port)
1446+
1447+
try:
1448+
# Test crash1.txt: MULTI followed by SET with NIL bulk string argument
1449+
# *1\r\n$5\r\nMULTI\r\n*3\r\n$3\r\nSET\r\n$1\r\na\r\n$-1\r\n1\r\n*1\r\n$4\r\nEXEC\r\n
1450+
crash_data = (
1451+
b"*1\r\n$5\r\nMULTI\r\n*3\r\n$3\r\nSET\r\n$1\r\na\r\n$-1\r\n1\r\n*1\r\n$4\r\nEXEC\r\n"
1452+
)
1453+
1454+
writer.write(crash_data)
1455+
await writer.drain()
1456+
1457+
try:
1458+
response = await asyncio.wait_for(reader.read(1024), timeout=2.0)
1459+
# If we get a response, it should be an error, not a crash
1460+
except asyncio.TimeoutError:
1461+
# Timeout is acceptable - connection might be closed
1462+
pass
1463+
except ConnectionError:
1464+
# Connection closed is acceptable - server detected bad protocol
1465+
pass
1466+
1467+
finally:
1468+
writer.close()
1469+
await writer.wait_closed()
1470+
1471+
# Verify server is still running by making a normal request
1472+
client = df_server.client()
1473+
await client.ping()
1474+
assert await client.ping() == True

0 commit comments

Comments
 (0)