Skip to content

Commit 14cc19c

Browse files
committed
Validate argument count in SET-family string commands
SET, GETSET, SETEX/PSETEX, SETRANGE, APPEND and GETRANGE/SUBSTR only Debug.Assert(parseState.Count == N) their argument count (or consumed a missing EX/PX option value) instead of validating it at runtime. Malformed input such as 'SET k', 'SETEX k 10', 'SETRANGE k', 'APPEND k', 'GETRANGE k' or 'SET k v EX' therefore aborted the process via the assert in a Debug/CI build, and read an out-of-bounds parse-state slot in Release, instead of returning an error - dropping the connection and every command pipelined after it. Validate the argument count in each handler (and, for SET's EX/PX option, that a value token follows before consuming it), matching what the object-store handlers and NetworkSETWITHETAG already do. Well-formed commands are unchanged; the wire errors match Redis.
1 parent 8c54cc2 commit 14cc19c

3 files changed

Lines changed: 90 additions & 6 deletions

File tree

libs/server/Resp/BasicCommands.cs

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -351,7 +351,9 @@ bool NetworkGET_SG<TGarnetApi>(ref TGarnetApi storageApi)
351351
private bool NetworkSET<TGarnetApi>(ref TGarnetApi storageApi)
352352
where TGarnetApi : IGarnetApi
353353
{
354-
Debug.Assert(parseState.Count == 2);
354+
if (parseState.Count != 2)
355+
return AbortWithWrongNumberOfArguments(nameof(RespCommand.SET));
356+
355357
var key = parseState.GetArgSliceByRef(0);
356358
var value = parseState.GetArgSliceByRef(1);
357359

@@ -379,7 +381,9 @@ private bool NetworkSET<TGarnetApi>(ref TGarnetApi storageApi)
379381
private bool NetworkGETSET<TGarnetApi>(ref TGarnetApi storageApi)
380382
where TGarnetApi : IGarnetApi
381383
{
382-
Debug.Assert(parseState.Count == 2);
384+
if (parseState.Count != 2)
385+
return AbortWithWrongNumberOfArguments(nameof(RespCommand.GETSET));
386+
383387
var key = parseState.GetArgSliceByRef(0);
384388

385389
return NetworkSET_Conditional(RespCommand.SET, 0, key, getValue: true, highPrecision: false,
@@ -392,6 +396,9 @@ private bool NetworkGETSET<TGarnetApi>(ref TGarnetApi storageApi)
392396
private bool NetworkSetRange<TGarnetApi>(ref TGarnetApi storageApi)
393397
where TGarnetApi : IGarnetApi
394398
{
399+
if (parseState.Count != 3)
400+
return AbortWithWrongNumberOfArguments(nameof(RespCommand.SETRANGE));
401+
395402
var key = parseState.GetArgSliceByRef(0);
396403

397404
// Validate offset
@@ -433,9 +440,14 @@ private bool NetworkSetRange<TGarnetApi>(ref TGarnetApi storageApi)
433440
return true;
434441
}
435442

436-
private bool NetworkGetRange<TGarnetApi>(ref TGarnetApi storageApi)
443+
private bool NetworkGetRange<TGarnetApi>(RespCommand cmd, ref TGarnetApi storageApi)
437444
where TGarnetApi : IGarnetApi
438445
{
446+
Debug.Assert(cmd is RespCommand.GETRANGE or RespCommand.SUBSTR);
447+
448+
if (parseState.Count != 3)
449+
return AbortWithWrongNumberOfArguments(cmd.ToString());
450+
439451
var key = parseState.GetArgSliceByRef(0);
440452

441453
// Validate range
@@ -476,6 +488,9 @@ private bool NetworkGetRange<TGarnetApi>(ref TGarnetApi storageApi)
476488
private bool NetworkSETEX<TGarnetApi>(bool highPrecision, ref TGarnetApi storageApi)
477489
where TGarnetApi : IGarnetApi
478490
{
491+
if (parseState.Count != 3)
492+
return AbortWithWrongNumberOfArguments(highPrecision ? nameof(RespCommand.PSETEX) : nameof(RespCommand.SETEX));
493+
479494
var key = parseState.GetArgSliceByRef(0);
480495

481496
// Validate expiry
@@ -579,7 +594,17 @@ private bool NetworkSETEXNX<TGarnetApi>(ref TGarnetApi storageApi)
579594
// based on above check if it is not KEEPTTL, it has to be either EX or PX
580595
if (expOption != ExpirationOption.KEEPTTL)
581596
{
582-
// EX and PX optionare followed by an expiry argument; account for the expiry argument by moving past the tokenIdx
597+
// EX and PX are followed by an expiry argument. Reject the option
598+
// appearing as the final token before consuming it, otherwise the
599+
// read runs past the parse state (a Debug.Assert failure that aborts
600+
// the process, an out-of-bounds read in Release).
601+
if (tokenIdx >= parseState.Count)
602+
{
603+
errorMessage = CmdStrings.RESP_ERR_GENERIC_SYNTAX_ERROR;
604+
break;
605+
}
606+
607+
// account for the expiry argument by moving past the tokenIdx
583608
if (!parseState.TryGetInt(tokenIdx++, out expiry))
584609
{
585610
errorMessage = CmdStrings.RESP_ERR_GENERIC_VALUE_IS_NOT_INTEGER;
@@ -889,6 +914,9 @@ private bool NetworkIncrementByFloat<TGarnetApi>(ref TGarnetApi storageApi)
889914
private bool NetworkAppend<TGarnetApi>(ref TGarnetApi storageApi)
890915
where TGarnetApi : IGarnetApi
891916
{
917+
if (parseState.Count != 2)
918+
return AbortWithWrongNumberOfArguments(nameof(RespCommand.APPEND));
919+
892920
var sbKey = parseState.GetArgSliceByRef(0);
893921

894922
var input = new StringInput(RespCommand.APPEND, ref parseState, startIdx: 1);

libs/server/Resp/RespServerSession.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -832,8 +832,8 @@ private bool ProcessBasicCommands<TGarnetApi>(RespCommand cmd, ref TGarnetApi st
832832
RespCommand.EXPIRETIME => NetworkEXPIRETIME(RespCommand.EXPIRETIME, ref storageApi),
833833
RespCommand.PEXPIRETIME => NetworkEXPIRETIME(RespCommand.PEXPIRETIME, ref storageApi),
834834
RespCommand.PERSIST => NetworkPERSIST(ref storageApi),
835-
RespCommand.GETRANGE => NetworkGetRange(ref storageApi),
836-
RespCommand.SUBSTR => NetworkGetRange(ref storageApi),
835+
RespCommand.GETRANGE => NetworkGetRange(RespCommand.GETRANGE, ref storageApi),
836+
RespCommand.SUBSTR => NetworkGetRange(RespCommand.SUBSTR, ref storageApi),
837837
RespCommand.TTL => NetworkTTL(RespCommand.TTL, ref storageApi),
838838
RespCommand.PTTL => NetworkTTL(RespCommand.PTTL, ref storageApi),
839839
RespCommand.SETRANGE => NetworkSetRange(ref storageApi),

test/standalone/Garnet.test/RespTests.cs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -600,6 +600,62 @@ public void MultiSetNX()
600600
TestUtils.AssertEqualUpToExpectedLength(expectedResponse, response);
601601
}
602602

603+
[Test]
604+
public void StringCommandsWrongArityReturnErrorAndKeepSessionAlive()
605+
{
606+
using var lightClientRequest = TestUtils.CreateRequest();
607+
608+
// Each malformed command is followed by PING. On unpatched main these
609+
// reach a handler that only Debug.Assert()s the argument count (or reads
610+
// the missing argument past the parse state), aborting the process in a
611+
// Debug build and reading out of bounds in Release, instead of replying
612+
// with an error. The trailing PONG proves the session stayed alive.
613+
void ExpectError(string cmd, string expectedError)
614+
{
615+
var response = lightClientRequest.SendCommands(cmd, "PING");
616+
TestUtils.AssertEqualUpToExpectedLength($"{expectedError}\r\n+PONG\r\n", response);
617+
}
618+
619+
ExpectError("SET k", "-ERR wrong number of arguments for 'SET' command");
620+
ExpectError("SET", "-ERR wrong number of arguments for 'SET' command");
621+
ExpectError("SET k v EX", "-ERR syntax error");
622+
ExpectError("SET k v PX", "-ERR syntax error");
623+
ExpectError("GETSET k", "-ERR wrong number of arguments for 'GETSET' command");
624+
ExpectError("GETSET k v extra", "-ERR wrong number of arguments for 'GETSET' command");
625+
ExpectError("SETEX k", "-ERR wrong number of arguments for 'SETEX' command");
626+
ExpectError("SETEX k 10", "-ERR wrong number of arguments for 'SETEX' command");
627+
ExpectError("PSETEX k 10", "-ERR wrong number of arguments for 'PSETEX' command");
628+
ExpectError("SETRANGE k", "-ERR wrong number of arguments for 'SETRANGE' command");
629+
ExpectError("SETRANGE k 0", "-ERR wrong number of arguments for 'SETRANGE' command");
630+
ExpectError("APPEND k", "-ERR wrong number of arguments for 'APPEND' command");
631+
ExpectError("GETRANGE k", "-ERR wrong number of arguments for 'GETRANGE' command");
632+
ExpectError("GETRANGE k 0", "-ERR wrong number of arguments for 'GETRANGE' command");
633+
ExpectError("SUBSTR k", "-ERR wrong number of arguments for 'SUBSTR' command");
634+
ExpectError("SUBSTR k 0", "-ERR wrong number of arguments for 'SUBSTR' command");
635+
636+
// Well-formed forms are unchanged.
637+
void ExpectOk(string cmd)
638+
{
639+
var response = lightClientRequest.SendCommands(cmd, "PING");
640+
TestUtils.AssertEqualUpToExpectedLength("+OK\r\n+PONG\r\n", response);
641+
}
642+
643+
ExpectOk("SET a 1");
644+
ExpectOk("SETEX b 100 v");
645+
ExpectOk("PSETEX c 5000 v");
646+
647+
var response = lightClientRequest.SendCommands("SETRANGE a 1 XY", "PING");
648+
TestUtils.AssertEqualUpToExpectedLength(":3\r\n+PONG\r\n", response);
649+
response = lightClientRequest.SendCommands("APPEND a Z", "PING");
650+
TestUtils.AssertEqualUpToExpectedLength(":4\r\n+PONG\r\n", response);
651+
response = lightClientRequest.SendCommands("GETSET a fresh", "PING");
652+
TestUtils.AssertEqualUpToExpectedLength("$4\r\n1XYZ\r\n+PONG\r\n", response);
653+
response = lightClientRequest.SendCommands("GETRANGE a 0 2", "PING");
654+
TestUtils.AssertEqualUpToExpectedLength("$3\r\nfre\r\n+PONG\r\n", response);
655+
response = lightClientRequest.SendCommands("SUBSTR a 0 2", "PING");
656+
TestUtils.AssertEqualUpToExpectedLength("$3\r\nfre\r\n+PONG\r\n", response);
657+
}
658+
603659
[Test]
604660
public void LargeSetGet()
605661
{

0 commit comments

Comments
 (0)