Validate argument count in SET-family string commands - #2050
Validate argument count in SET-family string commands#2050hexonal (hexonal) wants to merge 1 commit into
Conversation
There was a problem hiding this comment.
Pull request overview
Adds runtime arity validation for core string commands to prevent crashes and preserve sessions.
Changes:
- Validates fixed-arity string commands.
- Safely rejects dangling
SET EX/PXoptions. - Adds malformed-command and session-survival tests.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
libs/server/Resp/BasicCommands.cs |
Adds argument validation and safe option parsing. |
test/standalone/Garnet.test/RespTests.cs |
Tests errors, connection survival, and valid commands. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| if (parseState.Count != 3) | ||
| return AbortWithWrongNumberOfArguments(nameof(RespCommand.GETRANGE)); |
There was a problem hiding this comment.
This is correct and needs to be addressed.
8052ccf to
ff43efb
Compare
kevin-montrose
left a comment
There was a problem hiding this comment.
Copilot comment is correct, need to distinguish between two different commands using same impl.
| if (parseState.Count != 3) | ||
| return AbortWithWrongNumberOfArguments(nameof(RespCommand.GETRANGE)); |
There was a problem hiding this comment.
This is correct and needs to be addressed.
ff43efb to
9353c44
Compare
|
Addressed, and rebased onto current
Only the error name is command-dependent: |
b523d23 to
14cc19c
Compare
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. GETWITHETAG and GETIFNOTMATCH in BasicEtagCommands.cs carry the same bare assert and are covered here too, since every other handler in that file already validates at runtime. 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. SET needs more than an arity check. It is declared with arity -3, but the fast parser only routes it to the option parser for array lengths 3..7, so a longer option-bearing SET lands in NetworkSET - where "not exactly 2" is not a wrong-argument-count error. 'SET k v GET GET GET GET GET GET' is accepted by Redis, and 'SET k v GET GET GET GET GET GET GET EX 100' was silently dropping its EX and replying +OK in Release. NetworkSET now hands anything longer than two arguments to NetworkSETEXNX, which already parses the full option set correctly.
14cc19c to
5881671
Compare
|
Pushed a follow-up. Two things beyond the 1. The arity check was wrong for Redis accepts both of the rejected forms. Worse, the second one was silently dropping its 2. New assertions cover all of it, and I verified each one fails on the unpatched build before adding it. One deliberate divergence worth stating: the emitted names are uppercase ( |
Symptom
Several core string commands abort the whole session — and, in a Debug/CI build, terminate the server process — when a required argument is missing, instead of returning an error. The reply is empty and the connection is dropped, taking every pipelined command after it with it.
Debug build of
GarnetServerat 4706f3f, raw RESP with a trailingPINGso a dropped reply is visible:server log:
The same happens for, all unauthenticated and needing no prior state:
In a Release build the process does not crash — the assert is compiled out and the code instead reads the missing argument from a stale, in-allocation parse-state slot, so e.g.
SETEX k 10(no value) silently stores an empty value rather than erroring. Either way the command does not behave as it should.The hash/list/set command handlers already validate their argument counts and return
-ERR wrong number of arguments; these string handlers onlyDebug.Assert(parseState.Count == N)(or consume an option value without checking a token is present), relying on an argument-count guarantee the parser does not actually provide for these commands.Root cause
libs/server/Resp/BasicCommands.cs:NetworkSET(SET),NetworkGETSET(GETSET),NetworkSETEX(SETEX/PSETEX),NetworkSetRange(SETRANGE),NetworkAppend(APPEND) read fixed argument positions after only aDebug.AssertonparseState.Count;NetworkGetRange(GETRANGE/SUBSTR) has no count check at all. A malformed arity reachesGetArgSliceByRef/TryGetIntat an index>= Count, which asserts (Debug) or reads out of bounds (Release).NetworkSETEXNX(theSET key value [EX seconds | PX milliseconds | NX | XX | GET | KEEPTTL]option parser) consumes theEX/PXvalue withparseState.TryGetInt(tokenIdx++, ...)without first checkingtokenIdx < parseState.Count, soSET k v EX(option in final position) reads past the parse state.Only bare
SET k vreachesNetworkSET; every option-bearing form (SET k v EX 10,SET k v NX,SET k v GET,SET k v KEEPTTL, …) is parsed to a differentRespCommandand a different handler, so requiring exactly two arguments here cannot reject a valid SET.Fix
Validate the argument count at runtime, matching what the object-store handlers and the sibling
NetworkSETWITHETAGalready do:SET,GETSET,APPEND→ exactly 2 args, elseAbortWithWrongNumberOfArguments.SETEX/PSETEX,SETRANGE,GETRANGE/SUBSTR→ exactly 3 args. (GETRANGEandSUBSTRshareNetworkGetRange, so a malformedSUBSTRreports'getrange'— a pre-existing shared-handler quirk, not introduced here.)NetworkSETEXNX→ rejectEX/PXas the final token (tokenIdx >= parseState.Count→ syntax error) before consuming its value, mirroring the existing guard inNetworkSETWITHETAG.KEEPTTL(which takes no value) is unaffected, as the guard is only reached when the option isEX/PX.Well-formed commands are unchanged; the error wire text matches Redis (
wrong number of argumentsfor the arity cases,syntax errorfor a danglingEX/PX).Tests
RespTests.StringCommandsWrongArityReturnErrorAndKeepSessionAlive(raw RESP viaLightClientRequest, each malformed command followed byPING):+PONG— proving the session survived. On unpatched main the process aborts / the reply is dropped, so the request times out and the test fails.SET a 1,SETEX b 100 v,PSETEX c 5000 v,SETRANGE a 1 XY(:3),APPEND a Z(:4),GETSET a fresh($4 1XYZ),GETRANGE a 0 2($3 fre) still behave exactly as before.Full
RespTests(352) stays green.Scope
The same missing-validation pattern remains in two other string commands —
INCRBYFLOAT(INCRBYFLOAT kaborts), and the ETag extension commandsGETWITHETAG/GETIFNOTMATCHinBasicEtagCommands.cs. I left those out to keep this PR focused on the common read/write string commands, but I'm happy to follow up with them (or fold them in here) if you'd prefer to close the whole class at once.