Skip to content

Commit ff43efb

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 fcdb5fc commit ff43efb

2 files changed

Lines changed: 82 additions & 3 deletions

File tree

libs/server/Resp/BasicCommands.cs

Lines changed: 29 additions & 3 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
@@ -436,6 +443,9 @@ private bool NetworkSetRange<TGarnetApi>(ref TGarnetApi storageApi)
436443
private bool NetworkGetRange<TGarnetApi>(ref TGarnetApi storageApi)
437444
where TGarnetApi : IGarnetApi
438445
{
446+
if (parseState.Count != 3)
447+
return AbortWithWrongNumberOfArguments(nameof(RespCommand.GETRANGE));
448+
439449
var key = parseState.GetArgSliceByRef(0);
440450

441451
// Validate range
@@ -476,6 +486,9 @@ private bool NetworkGetRange<TGarnetApi>(ref TGarnetApi storageApi)
476486
private bool NetworkSETEX<TGarnetApi>(bool highPrecision, ref TGarnetApi storageApi)
477487
where TGarnetApi : IGarnetApi
478488
{
489+
if (parseState.Count != 3)
490+
return AbortWithWrongNumberOfArguments(highPrecision ? nameof(RespCommand.PSETEX) : nameof(RespCommand.SETEX));
491+
479492
var key = parseState.GetArgSliceByRef(0);
480493

481494
// Validate expiry
@@ -579,7 +592,17 @@ private bool NetworkSETEXNX<TGarnetApi>(ref TGarnetApi storageApi)
579592
// based on above check if it is not KEEPTTL, it has to be either EX or PX
580593
if (expOption != ExpirationOption.KEEPTTL)
581594
{
582-
// EX and PX optionare followed by an expiry argument; account for the expiry argument by moving past the tokenIdx
595+
// EX and PX are followed by an expiry argument. Reject the option
596+
// appearing as the final token before consuming it, otherwise the
597+
// read runs past the parse state (a Debug.Assert failure that aborts
598+
// the process, an out-of-bounds read in Release).
599+
if (tokenIdx >= parseState.Count)
600+
{
601+
errorMessage = CmdStrings.RESP_ERR_GENERIC_SYNTAX_ERROR;
602+
break;
603+
}
604+
605+
// account for the expiry argument by moving past the tokenIdx
583606
if (!parseState.TryGetInt(tokenIdx++, out expiry))
584607
{
585608
errorMessage = CmdStrings.RESP_ERR_GENERIC_VALUE_IS_NOT_INTEGER;
@@ -889,6 +912,9 @@ private bool NetworkIncrementByFloat<TGarnetApi>(ref TGarnetApi storageApi)
889912
private bool NetworkAppend<TGarnetApi>(ref TGarnetApi storageApi)
890913
where TGarnetApi : IGarnetApi
891914
{
915+
if (parseState.Count != 2)
916+
return AbortWithWrongNumberOfArguments(nameof(RespCommand.APPEND));
917+
892918
var sbKey = parseState.GetArgSliceByRef(0);
893919

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

test/standalone/Garnet.test/RespTests.cs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -600,6 +600,59 @@ 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 0", "-ERR wrong number of arguments for 'GETRANGE' command");
634+
635+
// Well-formed forms are unchanged.
636+
void ExpectOk(string cmd)
637+
{
638+
var response = lightClientRequest.SendCommands(cmd, "PING");
639+
TestUtils.AssertEqualUpToExpectedLength("+OK\r\n+PONG\r\n", response);
640+
}
641+
642+
ExpectOk("SET a 1");
643+
ExpectOk("SETEX b 100 v");
644+
ExpectOk("PSETEX c 5000 v");
645+
646+
var response = lightClientRequest.SendCommands("SETRANGE a 1 XY", "PING");
647+
TestUtils.AssertEqualUpToExpectedLength(":3\r\n+PONG\r\n", response);
648+
response = lightClientRequest.SendCommands("APPEND a Z", "PING");
649+
TestUtils.AssertEqualUpToExpectedLength(":4\r\n+PONG\r\n", response);
650+
response = lightClientRequest.SendCommands("GETSET a fresh", "PING");
651+
TestUtils.AssertEqualUpToExpectedLength("$4\r\n1XYZ\r\n+PONG\r\n", response);
652+
response = lightClientRequest.SendCommands("GETRANGE a 0 2", "PING");
653+
TestUtils.AssertEqualUpToExpectedLength("$3\r\nfre\r\n+PONG\r\n", response);
654+
}
655+
603656
[Test]
604657
public void LargeSetGet()
605658
{

0 commit comments

Comments
 (0)