Skip to content

Fix GETEX tearing down the session on an out-of-range expiry - #2094

Open
hexonal (hexonal) wants to merge 1 commit into
microsoft:mainfrom
hexonal:fix-getex-out-of-range-expiry
Open

Fix GETEX tearing down the session on an out-of-range expiry#2094
hexonal (hexonal) wants to merge 1 commit into
microsoft:mainfrom
hexonal:fix-getex-out-of-range-expiry

Conversation

@hexonal

Copy link
Copy Markdown
Contributor

What

GETEX is the only command that converts its expiry option into a relative TimeSpan (TimeSpan.FromSeconds/FromMilliseconds for EX/PX) or an absolute instant (DateTimeOffset.FromUnixTimeSeconds/FromUnixTimeMilliseconds for EXAT/PXAT). Each of those throws on a value it cannot represent — an OverflowException for TimeSpan, an ArgumentOutOfRangeException past year 9999 for DateTimeOffset — and the only guard in NetworkGETEX was expireTime <= 0.

Neither exception is a RespParsingException or a GarnetException, so it falls through to the RespServerSession catch-all, which calls networkSender.Dispose()dropping the client connection. Any client can tear down its session with a one-liner:

GETEX k EXAT 99999999999999

There is a second, quieter bug in the same spot: for EX/PX values large enough to convert without throwing but past DateTimeOffset.MaxValue, the absolute expiry computed as UtcNow.Ticks + tsExpiry.Ticks silently overflows the long into a negative instant, so GETEX applies a garbage TTL and replies with the value instead of an error (e.g. GETEX k EX 900000000000).

Fix

Bound every option by DateTimeOffset.MaxValue (year 9999) and reply with -ERR invalid expire time in 'getex' command, matching Redis, instead of crashing or overflowing. EX/PX are bounded relative to UtcNow so the resulting absolute expiry stays representable; EXAT/PXAT are bounded by the absolute Unix ceiling. Values within range — including far-future ones that already worked — are unaffected.

Test

Adds GetExpiryOutOfRangeIsRejectedWithoutKillingSession, a LightClientRequest test whose trailing PING proves the connection survives (a disposed session never answers it). It covers all four options for the throwing case and EX/PX for the silent-overflow case.

On unpatched main all six cases fail — four with GarnetException: Disconnected, two with the wrong reply ($6\r\nvalueA\r\n instead of the error). With the fix the full GetExpiry / SetAndGetExpiry suite is green (32/32). Verified in Release on .NET 10.

GETEX is the only command that turns its expiry option into a relative
TimeSpan via TimeSpan.FromSeconds/FromMilliseconds (EX/PX) or an absolute
instant via DateTimeOffset.FromUnixTimeSeconds/Milliseconds (EXAT/PXAT).
Each of those throws on a value it cannot represent - an OverflowException
for TimeSpan, an ArgumentOutOfRangeException past year 9999 for
DateTimeOffset - and the only guard was expireTime <= 0. Neither exception
is a RespParsingException or a GarnetException, so it escaped to the
RespServerSession catch-all and disposed the network sender, dropping the
client connection. A single "GETEX k EXAT 99999999999999" from any client
tears down the session.

There was a second, quieter bug in the same spot: for EX/PX values large
enough to convert without throwing but past DateTimeOffset.MaxValue, the
absolute expiry computed as UtcNow.Ticks + tsExpiry.Ticks silently
overflowed the long into a negative instant, so GETEX applied a garbage
TTL and replied with the value instead of an error.

Bound every option by DateTimeOffset.MaxValue (year 9999) and reply with
"ERR invalid expire time in 'getex' command", matching Redis, instead of
crashing or overflowing. Values within the representable range - including
far-future ones that worked before - are unaffected.

Adds a LightClientRequest regression test whose trailing PING proves the
connection survives; it covers all four options for the throwing case and
EX/PX for the silent-overflow case, and fails on main (four with a
Disconnected exception, two with the wrong reply).
Copilot AI balanced review requested due to automatic review settings August 31, 2026 05:25

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

Prevents out-of-range GETEX expiries from terminating client sessions or overflowing expiry calculations.

Changes:

  • Validates all GETEX expiry options against representable limits.
  • Adds a command-specific RESP error.
  • Tests rejection, connection survival, and unchanged TTL.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

File Description
libs/server/Resp/BasicCommands.cs Adds expiry bounds validation.
libs/server/Resp/CmdStrings.cs Adds the GETEX expiry error.
test/standalone/Garnet.test/RespTests.cs Covers overflow cases and session survival.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

// computed below as UtcNow.Ticks + tsExpiry.Ticks does not silently overflow the
// long. Both are bounded by refusing any expiry beyond DateTimeOffset.MaxValue
// (year 9999), reporting an error the way Redis does rather than crashing.
var maxDeltaTicks = DateTimeOffset.MaxValue.Ticks - DateTimeOffset.UtcNow.Ticks;
@kevin-montrose kevin-montrose self-assigned this Sep 8, 2026
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