Skip to content

Validate argument count in SET-family string commands - #2050

Open
hexonal (hexonal) wants to merge 1 commit into
microsoft:mainfrom
hexonal:fix-string-command-arity-validation
Open

Validate argument count in SET-family string commands#2050
hexonal (hexonal) wants to merge 1 commit into
microsoft:mainfrom
hexonal:fix-string-command-arity-validation

Conversation

@hexonal

Copy link
Copy Markdown
Contributor

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 GarnetServer at 4706f3f, raw RESP with a trailing PING so a dropped reply is visible:

C: SET k
C: PING
S: (connection closed, zero bytes)

server log:

Process terminated. Assertion Failed
  at Garnet.server.RespServerSession.NetworkSET[TGarnetApi](...) in libs/server/Resp/BasicCommands.cs:line 354

The same happens for, all unauthenticated and needing no prior state:

SET                 SET k               SET k v EX      (EX/PX with no value)
GETSET k            GETSET k v extra
SETEX k             SETEX k 10          PSETEX k 10
SETRANGE k          SETRANGE k 0
APPEND k
GETRANGE k          GETRANGE k 0        SUBSTR k 0

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 only Debug.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 a Debug.Assert on parseState.Count; NetworkGetRange (GETRANGE/SUBSTR) has no count check at all. A malformed arity reaches GetArgSliceByRef/TryGetInt at an index >= Count, which asserts (Debug) or reads out of bounds (Release).
  • NetworkSETEXNX (the SET key value [EX seconds | PX milliseconds | NX | XX | GET | KEEPTTL] option parser) consumes the EX/PX value with parseState.TryGetInt(tokenIdx++, ...) without first checking tokenIdx < parseState.Count, so SET k v EX (option in final position) reads past the parse state.

Only bare SET k v reaches NetworkSET; 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 different RespCommand and 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 NetworkSETWITHETAG already do:

  • SET, GETSET, APPEND → exactly 2 args, else AbortWithWrongNumberOfArguments.
  • SETEX/PSETEX, SETRANGE, GETRANGE/SUBSTR → exactly 3 args. (GETRANGE and SUBSTR share NetworkGetRange, so a malformed SUBSTR reports 'getrange' — a pre-existing shared-handler quirk, not introduced here.)
  • NetworkSETEXNX → reject EX/PX as the final token (tokenIdx >= parseState.Count → syntax error) before consuming its value, mirroring the existing guard in NetworkSETWITHETAG. KEEPTTL (which takes no value) is unaffected, as the guard is only reached when the option is EX/PX.

Well-formed commands are unchanged; the error wire text matches Redis (wrong number of arguments for the arity cases, syntax error for a dangling EX/PX).

Tests

RespTests.StringCommandsWrongArityReturnErrorAndKeepSessionAlive (raw RESP via LightClientRequest, each malformed command followed by PING):

  • Every malformed form above returns the expected error and then +PONG — proving the session survived. On unpatched main the process aborts / the reply is dropped, so the request times out and the test fails.
  • Well-formed 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 k aborts), and the ETag extension commands GETWITHETAG/GETIFNOTMATCH in BasicEtagCommands.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.

Copilot AI balanced review requested due to automatic review settings August 8, 2026 10:43

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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/PX options.
  • 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.

Comment on lines +446 to +447
if (parseState.Count != 3)
return AbortWithWrongNumberOfArguments(nameof(RespCommand.GETRANGE));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is correct and needs to be addressed.

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.
@hexonal
hexonal (hexonal) force-pushed the fix-string-command-arity-validation branch from 8052ccf to ff43efb Compare August 10, 2026 01:46
@kevin-montrose kevin-montrose self-assigned this Aug 11, 2026

@kevin-montrose kevin-montrose left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot comment is correct, need to distinguish between two different commands using same impl.

Comment on lines +446 to +447
if (parseState.Count != 3)
return AbortWithWrongNumberOfArguments(nameof(RespCommand.GETRANGE));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is correct and needs to be addressed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants