Skip to content

fix(faucet): close per-recipient cooldown TOCTOU bypass via Entry guard#760

Merged
github-actions[bot] merged 1 commit into
mainfrom
fix/faucet-cooldown-toctou
Jun 1, 2026
Merged

fix(faucet): close per-recipient cooldown TOCTOU bypass via Entry guard#760
github-actions[bot] merged 1 commit into
mainfrom
fix/faucet-cooldown-toctou

Conversation

@satyakwok
Copy link
Copy Markdown
Collaborator

@satyakwok satyakwok commented Jun 1, 2026

Summary

check_rate_limits in `bin/sentrix-faucet/src/main.rs` had a TOCTOU race on the per-recipient cooldown check. The old code:

```rust
if let Some(last) = state.addr_last_drip.get(recipient)
&& now.duration_since(*last) < state.addr_cooldown
{
return Err(...);
}
state.addr_last_drip.insert(recipient.to_string(), now);
```

`DashMap::get` returns a `Ref` that releases the lock as soon as it drops. Between the read+check and the subsequent `insert`, another concurrent request for the same recipient address can interleave its own `get` and see the same "no prior drip" or "prior drip stale enough" outcome — both requests pass the check, both insert `now`, and both proceed to drip. Net effect: one address can receive multiple drips inside what was supposed to be a single cooldown window. The per-IP rate limit (above) already uses `entry()` which holds the per-key write lock for the whole closure, so it's atomic; the per-recipient block was the only async bypass.

Fix

Replace the get + insert pair with a `match entry()` block:

```rust
match state.addr_last_drip.entry(recipient.to_string()) {
Entry::Occupied(mut o) => {
let last = *o.get();
if now.duration_since(last) < state.addr_cooldown { return Err(...); }
o.insert(now);
}
Entry::Vacant(v) => { v.insert(now); }
}
```

`Entry` holds the per-key write lock for the entire check + update, so concurrent callers serialize cleanly. Same pattern as the per-IP block.

Test plan

  • `cargo clippy --workspace --tests -- -D warnings` clean (matches CI).
  • Manual reasoning above: only path now mutating `addr_last_drip` is inside an `Entry` arm holding the lock.

A focused integration test that spawns N parallel drips against a mock RPC would be valuable but is out of scope for the minimal fix.

How I found it

Audit pass against all workspace crates one-by-one (operator request). `sentrix-faucet` was the 5th crate reviewed; this was the first real bug — earlier crates (`sentrix-proto`, `sentrix-precompiles`, `sentrix-rpc-types`, `sentrix-codec`) had only minor style notes.

Summary by CodeRabbit

Bug Fixes

  • Fixed a race condition in the faucet's per-recipient cooldown system that could allow concurrent requests to bypass rate limits. Cooldown enforcement is now properly serialized across simultaneous requests.

@github-actions github-actions Bot enabled auto-merge (squash) June 1, 2026 13:27
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 1, 2026

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 15424d65-0770-41d2-a0c1-21e9f53d546d

📥 Commits

Reviewing files that changed from the base of the PR and between e0fe500 and eb043f6.

📒 Files selected for processing (1)
  • bin/sentrix-faucet/src/main.rs

📝 Walkthrough

Walkthrough

This PR fixes a race condition in the faucet's per-recipient rate limiting. Previously, two concurrent requests for the same recipient could both observe no prior entry in the cooldown map and both pass the check. The fix uses DashMap's entry API to atomically check for an existing timestamp and update it in a single operation, ensuring concurrent requests for the same recipient serialize and enforce the cooldown consistently. The change includes updating imports to include Entry from dashmap::mapref::entry and rewriting the cooldown logic inside check_rate_limits.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: fixing a TOCTOU race in the per-recipient cooldown check using DashMap's Entry guard pattern.
Description check ✅ Passed The description includes Summary, test plan, and detailed explanation of the TOCTOU issue and fix, though the Scope, Checks, Linked issue, and Deploy impact sections from the template are missing.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/faucet-cooldown-toctou

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov
Copy link
Copy Markdown

codecov Bot commented Jun 1, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

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