Skip to content

harden server — startup, shutdown, overflow guards - #92

Merged
kacy merged 5 commits into
mainfrom
harden-server-startup-shutdown
Feb 12, 2026
Merged

harden server — startup, shutdown, overflow guards#92
kacy merged 5 commits into
mainfrom
harden-server-startup-shutdown

Conversation

@kacy

@kacy kacy commented Feb 12, 2026

Copy link
Copy Markdown
Owner

summary

two rounds of full codebase audits surfaced 9 hardening issues across 12 files. this PR fixes all of them:

round 1 — server lifecycle & arithmetic

  • replace .expect() on address parsing with graceful eprintln + exit(1) (main.rs, 3 sites)
  • clamp Duration::as_millis() u128→i64 cast to i64::MAX in shard TTL fields (shard.rs, 3 sites)
  • add 30s shutdown timeout so hung connections don't block exit (server.rs)
  • fix gossip port arithmetic: checked_add at startup, saturating_add in internal paths, fix hardcoded 10000 in cluster_meet to use configured gossip_port_offset (main.rs, cluster.rs, topology.rs)
  • add truncated snapshot test (snapshot.rs)

round 2 — protocol, persistence, core, cluster

  • add MAX_NESTING_DEPTH (64) to RESP3 parser to prevent stack overflow from deeply nested arrays/maps (parse.rs, error.rs)
  • clamp recovery TTL arithmetic: (seconds * 1000) as i64saturating_mul + min(i64::MAX), and milliseconds as i64min(i64::MAX) (recovery.rs)
  • replace DropHandle::spawn() .expect() with graceful degradation — logs warning and falls back to inline dropping via existing try_send Disconnected handling (dropper.rs)
  • add SlotRange::try_new() with runtime validation, use it in gossip decode path where slot data comes from the network (slots.rs, message.rs)

what was tested

  • full test suite: 471 tests across all 5 crates — all passing
  • cargo clippy --workspace -- -D warnings — clean
  • cargo fmt --all — clean

design considerations

  • parser depth limit of 64 matches redis's practical nesting limit and is well within default stack size
  • drop thread failure degrades to inline dropping with zero caller changes — the existing try_send / TrySendError::Disconnected paths handle it naturally
  • SlotRange::try_new() is separate from new() so internal callers with known-valid ranges don't pay for error handling

kacy added 5 commits February 11, 2026 19:15
the recursive check/parse functions had no depth limit, allowing a
malicious client to stack-overflow the server with deeply nested
arrays or maps. adds a MAX_NESTING_DEPTH of 64 and threads a depth
counter through both check() and parse(). returns NestingTooDeep
error when exceeded.
expire replay computed (seconds * 1000) as i64 which silently wrapped
for very large u64 values, and pexpire cast u64 directly to i64 which
sign-corrupted values above i64::MAX. both now clamp to i64::MAX.
DropHandle::spawn() used .expect() which panics if the OS can't
create the thread. now logs a warning and returns a handle that
falls back to inline dropping via the existing try_send Disconnected
handling.
SlotRange::new() only checked invariants via debug_assert!, which
is stripped in release builds. adds try_new() with runtime validation
and uses it in decode_member_info() where data comes from the
network. internal callers with known-valid ranges keep using new().
@kacy
kacy force-pushed the harden-server-startup-shutdown branch from e586853 to d7da609 Compare February 12, 2026 00:16
@kacy
kacy merged commit 2e1ec52 into main Feb 12, 2026
5 of 7 checks passed
@kacy
kacy deleted the harden-server-startup-shutdown branch February 12, 2026 00:25
kacy added a commit that referenced this pull request Feb 19, 2026
* fix: add nesting depth limit to RESP3 parser

the recursive check/parse functions had no depth limit, allowing a
malicious client to stack-overflow the server with deeply nested
arrays or maps. adds a MAX_NESTING_DEPTH of 64 and threads a depth
counter through both check() and parse(). returns NestingTooDeep
error when exceeded.

* fix: clamp ttl arithmetic in aof recovery to prevent overflow

expire replay computed (seconds * 1000) as i64 which silently wrapped
for very large u64 values, and pexpire cast u64 directly to i64 which
sign-corrupted values above i64::MAX. both now clamp to i64::MAX.

* fix: graceful degradation when drop thread fails to spawn

DropHandle::spawn() used .expect() which panics if the OS can't
create the thread. now logs a warning and returns a handle that
falls back to inline dropping via the existing try_send Disconnected
handling.

* fix: validate slot ranges at runtime in gossip decode path

SlotRange::new() only checked invariants via debug_assert!, which
is stripped in release builds. adds try_new() with runtime validation
and uses it in decode_member_info() where data comes from the
network. internal callers with known-valid ranges keep using new().

* chore: cargo fmt
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.

1 participant