Repository navigation
fix(commands): reject expire times that overflow int64 - #3680
shivampwm2020 wants to merge 6 commits into
Conversation
|
Hi @shivampwm2020, Thank you for your pull request. Please review our Contributing Guide. Please make sure you understand your changes and explain your reasoning in this pull request. Low-quality pull requests may be closed. |
|
The fix looks correct and complete. ToExpireAtMs properly caps the input so that neither the seconds-to-milliseconds scaling nor the addition of the current time can overflow, and all affected commands (SET EX/PX/EXAT/PXAT, SETEX/PSETEX, EXPIRE/PEXPIRE/EXPIREAT/PEXPIREAT) are now routed through it. One minor suggestion: the Go test covers SET EX, SETEX, and EXPIRE for the overflow case, but ToExpireAtMs has four (ttl_in_seconds, add_now) combinations. It might be worth adding assertions for PEXPIRE and EXPIREAT as well to ensure each path is exercised: LGTM otherwise. |
Per review feedback on apache#3680: ToExpireAtMs has four (ttl, add_now) combinations; add PX and EXPIREAT assertions alongside the existing SET EX / SETEX / EXPIRE coverage. Signed-off-by: Shivam Maurya <shivam@programmingwithmaurya.com>
Per review feedback on apache#3680: ToExpireAtMs has four (ttl, add_now) combinations; add PX and EXPIREAT assertions alongside the existing SET EX / SETEX / EXPIRE coverage. Signed-off-by: Shivam Maurya <shivam@programmingwithmaurya.com>
|
Done — added PX and EXPIREAT overflow assertions alongside the existing SET EX / SETSETEX / EXPIRE coverage (0befba8), each checking the error and that the old value is preserved. Thanks for the review. |
There was a problem hiding this comment.
🟡 Changes recommended
Positive-only validation rejects valid immediate-expiration requests and leaves existing keys alive.
1 open finding
What changed in this PR
This PR adds overflow-safe expiration conversion to Kvrocks commands, preventing oversized expiration values from silently expiring keys.
Changes:
- Centralizes timestamp conversion and overflow checks.
- Applies validation to string and key-expiration commands.
- Adds rejection and value-preservation regression tests.
| File | Description |
|---|---|
| tests/gocase/unit/expire/expire_test.go | Adds expiration-overflow regression cases. |
| src/commands/ttl_util.h | Introduces shared overflow-safe conversion. |
| src/commands/cmd_string.cc | Updates SETEX and PSETEX conversion. |
| src/commands/cmd_key.cc | Updates EXPIRE, PEXPIRE, and EXPIREAT parsing. |
🧠 Review effort: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| auto ttl = GET_OR_RET(ParseInt<int64_t>(args[2], TTL_RANGE<int64_t>)); | ||
| expire_at_ = GET_OR_RET(ToExpireAtMs(ttl, true, true)); |
There was a problem hiding this comment.
Thanks for the review! Fixed in 4b09629: EXPIRE/PEXPIRE/EXPIREAT parse signed values again, and non-positive ones now map to a small past timestamp (1 ms, which becomes 1 s in the 32-bit expire encoding) so the key is deleted exactly like Redis. Zero cannot be stored directly because it means "no expiration". Overflowing values are still rejected before the multiplication. Added test cases for zero and negative timeouts across all three commands.
jihuayu
left a comment
There was a problem hiding this comment.
Thanks for the fix!
This changes the behavior of EXPIRE/PEXPIRE for non-positive timeouts. According to the Redis documentation, these values should delete the key, but this PR rejects them as invalid input.
Could you preserve the existing behavior and add test cases for zero and negative timeouts?
Otherwise, LGTM.
|
Also, @shivampwm2020 please include an AI usage disclosure as required by our contribution guidelines. |
|
Apologies for missing this - disclosure per the guidelines: this PR was prepared with AI assistance. The assistant was used to reproduce the overflow behavior against a local build, draft the |
EXPIRE, PEXPIRE and EXPIREAT now parse signed values again and map non-positive ones to a small past timestamp (1 ms), so they delete the key exactly like Redis instead of rejecting the command. Overflowing values are still rejected before any multiplication can wrap. Requested in review on PR apache#3680.
Converting a user supplied expire time to milliseconds could overflow int64: values up to INT64_MAX passed the range check and were then multiplied by 1000 (or had the current time added), wrapping the expire time into the past. The key was then treated as already expired, so `SET k v EX <huge>` and `EXPIRE k <huge>` returned success and silently removed the key. Add a shared ToExpireAtMs helper that caps the input so neither the seconds-to-milliseconds scaling nor the addition of the current time can overflow, and route EX/PX/EXAT/PXAT, EXPIRE/EXPIREAT/PEXPIRE and SETEX/PSETEX through it. Overflowing values now get an "invalid expire time" error and the previous value is preserved.
Per review feedback on apache#3680: ToExpireAtMs has four (ttl, add_now) combinations; add PX and EXPIREAT assertions alongside the existing SET EX / SETEX / EXPIRE coverage. Signed-off-by: Shivam Maurya <shivam@programmingwithmaurya.com>
…-tidy narrowing check
EXPIRE, PEXPIRE and EXPIREAT now parse signed values again and map non-positive ones to a small past timestamp (1 ms), so they delete the key exactly like Redis instead of rejecting the command. Overflowing values are still rejected before any multiplication can wrap. Requested in review on PR apache#3680.
76e50c2 to
17caa70
Compare
|
Rebased onto Nothing about the change itself moved: all four touched files are byte-identical to the version jihuayu approved ( CI is re-running on the new SHA. Sorry for the churn of a second full run — say the word if you would rather have kept the old SHA and I will leave the branch alone. |
|
One thing I should flag on my own change, since it affects you rather than me: the rebase required a new CI run, and because I am a first-time contributor here both So if you would rather have the already-green SHA back than approve a fresh run, say so and I will force the branch back to If you do approve the runs and they go green, nothing else needs doing from me. |
|
Following up on my own message, because I think I overstated the problem. I checked the repo's action queue rather than reasoning about it: 8 of the 20 most recent workflow runs are in That means the only real question is the one I raised: whether clearing Sorry for the noise — two messages on this was one more than it deserved. |
|
@shivampwm2020 It don’t need you to do anything. I need to merge the PRs one by one, so please wait. The action requires manual approval from a maintainer to run. |
|
Understood — holding at 17caa70 and touching nothing. Thanks for approving the runs when you get to it. One correction to my earlier messages, for your queue's sake rather than mine: I raised the action_required state as if it might be a problem I had created. It isn't — it is just how outside-contributor runs are treated here, and I said so once I checked. It needed no reply, and this one needs none either. |
|
@shivampwm2020 I don't think it's a proper way to use AI to reply what reviewers suggested. For this PR, the fix is correct but we can improve enum class ExpireTimeMode {
kRelativeSeconds,
kRelativeMilliseconds,
kAbsoluteSeconds,
kAbsoluteMilliseconds,
};
StatusOr<uint64_t> ConvertToExpireAtMs(int64_t expire_arg, ExpireTimeMode time_mode, uint64_t now_ms) {
auto value = static_cast<uint64_t>(expire_arg);
bool seconds =
time_mode == ExpireTimeMode::kRelativeSeconds || time_mode == ExpireTimeMode::kAbsoluteSeconds;
bool relative = time_mode == ExpireTimeMode::kRelativeSeconds ||
time_mode == ExpireTimeMode::kRelativeMilliseconds;
if (seconds) {
if (value > kMaxExpireAtMs / 1000) {
return {Status::RedisExecErr, "expire time overflow"};
}
value *= 1000;
}
if (value > kMaxExpireAtMs) {
return {Status::RedisExecErr, "expire time overflow"};
}
if (relative) {
if (value > kMaxExpireAtMs - now_ms) {
return {Status::RedisExecErr, "expire time overflow"};
}
value += now_ms;
}
return value;
}So that we can reuse this in HFE as well, see kvrocks/src/commands/cmd_hash.cc Lines 36 to 158 in 8ecf828 |

Problem
SET k v EX 9223372036854775807returnsOKbut silently removes the key. Redis rejects it with(error) ERR invalid expire time in set. The same overflow also affectsEXPIRE,PX,EXAT, andPXAT.The overflowing seconds*1000 (or +now for the absolute variants) wraps int64 to a past timestamp, so the key is written and immediately expired.
Repro:
Root cause
ParseExpireFlagsvalidated TTL seconds against a fixedINT64_MAX / 1000bound (seconds) orINT64_MAX(ms). That neither catchesseconds*1000overflow-adjacent values forEX/PX, nor+ now_msforEXAT/PXAT— a value can pass the check and still overflow into a past expiration.Fix
ttl_util.hgainsToExpireAtMs(ttl, ttl_in_seconds, add_now): the upper bound is computed againstINT64_MAX - now_msat the momentnowis added, and the result is range-checked after conversion.ParseExpireFlagsroutes all four flags (EX,PX,EXAT,PXAT) through it, so seconds and milliseconds paths share one limit.After:
Testing
tests/gocase/unit/expire/expire_test.goasserts the overflow values are rejected and the old value survives, forEX,PX,EXAT,PXAT,EXPIREandPEXPIRE.go test ./unit/expire/ -args -binPath=.../build/kvrocks -workspace=...→ ok (TestExpire green).kvrocksfrombuild/withmake -j6→ exit 0.Notes
EXfamily.AI usage disclosure
Per the guidelines for AI-assisted contributions: this change was prepared with the help of an LLM coding assistant (GLM), used to reproduce the overflow behavior against a local build, draft the
ToExpireAtMsbound and the gocase regression test, and run them locally (TestExpire green, fullmakebuild OK). The change is reviewed and maintained by @shivampwm2020, who is responsible for it.