fix: bound QGETDATA request tracking and reject requester-supplied nError - #7519
fix: bound QGETDATA request tracking and reject requester-supplied nError#7519PastaPastaPasta wants to merge 3 commits into
Conversation
Add a unit test that scores a peer for smuggling a QDATA error code on QGETDATA, plus a functional coverage path and wire helper so the same attack can be driven over P2P. Pre-fix the unit test expects score 10 and gets 0.
QGETDATA must not accept a requester-supplied nError (response-only); that field previously selected the *_MISSING paths that skip the rate-limit ban while still doing expensive work. Also validate llmqType/block before RegisterDataRequest, and run IsQuorumActive (plus dedup/backpressure) before GetQuorum on recovered sigs so historical hashes cannot force DMN-list rebuilds.
The preceding commit reordered QGETDATA validation so garbage llmqType and unknown quorumHash values are rejected before a tracking entry is created, but that only narrows the memory-growth window it set out to close. Any real block hash still registers an entry, and a fresh quorumHash is by construction never request_limit_exceeded, so the existing rate limit never fires for a peer that simply never repeats a hash. mapQuorumDataRequests is only pruned per-block (and not at all during IBD), so such a peer grows it unboundedly for the whole 300s+bias expiry window. Add CQuorumManager::RegisterInboundDataRequest, which refuses to create a new peer-initiated entry once the requesting identity already holds MAX_INBOUND_DATA_REQUESTS live ones, tracked in a per-identity counter released by CleanupExpiredDataRequests. The budget is per identity rather than global so one peer cannot evict or starve another; qwatch peers share the null proRegTx identity, matching the existing rate-limit behaviour for that class. Exhaustion scores 25, consistent with the sibling rate-limit path, and is reached before any response work. Also drop the two PeerMisbehaving branches on the new early-return paths: sendQDATA is called there with request_limit_exceeded hardcoded false, so it can only return false and the score was unreachable. Both replies are legitimate for an honest peer and carry no amplification, so nothing is lost. In VerifyAndProcessRecoveredSig, fold the duplicated pre-GetQuorum dedup and backpressure block back into the single post-GetQuorum block. Once IsQuorumActive gates quorumHash to the active set that ScanQuorums just cached, GetQuorum is a cache hit rather than a rebuild, so the early copy no longer avoids any meaningful work. Extend llmq_qgetdata_tests with a case covering the budget: MAX_INBOUND_DATA_REQUESTS distinct hashes register, the next new key is refused, the handler turns the refusal into a score of 25, and a second identity retains its own budget.
|
Warning Review limit reached
Next review available in: 46 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (8)
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. Comment |
|
✅ Final review complete — no blockers (commit 9461b8b) |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9461b8b97a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| // Cheap gates first. IsQuorumActive only scans the small cached set of recent | ||
| // quorums (keepOldConnections). GetQuorum, by contrast, rebuilds arbitrary | ||
| // historical mined commitments (DMN list replay + member selection) on a cache | ||
| // miss — do not let an unsolicited QSIGREC force that work for inactive hashes. | ||
| // Caller (NetSigning) has already rejected unknown llmq types. | ||
| if (!IsQuorumActive(llmq_type, qman, quorum_hash)) { |
There was a problem hiding this comment.
Split the unrelated QSIGREC behavior change
This reorders recovered-signature validation in VerifyAndProcessRecoveredSig, but the commit is scoped and tested as a QGETDATA fix. If the QGETDATA mitigation later needs to be cherry-picked or reverted, this independent QSIGREC behavior change must travel with it despite having no dedicated regression test, making a sensitive LLMQ signing-path change harder to validate and maintain. Move this hunk to a separate commit with focused coverage.
AGENTS.md reference: AGENTS.md:L13-L14
Useful? React with 👍 / 👎.
thepastaclaw
left a comment
There was a problem hiding this comment.
Final validation — Codex + Sonnet
The final implementation correctly rejects requester-supplied QGETDATA errors, bounds inbound tracking entries per identity, and validates cheap fields before expensive quorum work; the relevant call sites and tests support the intended behavior. No code-correctness blocker remains, but two commit-history issues should be cleaned up so the preserved stack stays bisectable and avoids add-then-remove blame noise.
Review provenance
- Codex reviewers:
gpt-5.6-sol— general (completed),gpt-5.6-sol— dash-core-commit-history (completed) - Verifier:
gpt-5.6-sol— final-verifier (fallback) - Sonnet reviewers:
claude-sonnet-5— general (failed),claude-sonnet-5— dash-core-commit-history (failed),claude-sonnet-5— dash-core-commit-history (completed),claude-sonnet-5— general (completed)
🟡 2 suggestion(s)
2 additional finding(s) omitted (not in diff).
🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.
In `<commit:044055168c2>`:
- [SUGGESTION] <commit:044055168c2>:1: Keep the regression-test commit green
Commit 044055168c2 deliberately precedes the implementation while adding unit and functional tests that require a misbehavior score of 10. At that revision, src/llmq/net_quorum.cpp has no early nError rejection, so the new unit test observes 0 and fails exactly as the commit message and PR description acknowledge; the functional case likewise waits for a score the handler cannot produce. This leaves a knowingly red permanent revision and an avoidable bisect trap. Squash these tests into bed4f81137a, or place the test commit after the fix, so every preserved commit has internally consistent implementation and test expectations.
In `<commit:bed4f81137a>`:
- [SUGGESTION] <commit:bed4f81137a>:1: Fold transient corrections into the commit that introduced them
Commit bed4f81137a introduces two PeerMisbehaving branches whose sendQDATA calls hardcode request_limit_exceeded=false, making those branches unreachable, and duplicates the recovered-signature deduplication/backpressure block before and after GetQuorum. Commit 9461b8b97a1 immediately removes those exact branches and folds the duplicated block back into one, explicitly describing them as dead guards and unnecessary duplication. Because neither intermediate construct was intended to survive and the commits have not shipped independently, fix up those corrective hunks into bed4f81137a. The durable per-identity cap and its tests can remain in 9461b8b97a1 as a separate hardening change.
Potential PR merge conflictsThis is advisory only. It does not block CI, but it marks PRs that will likely need a rebase depending on merge order. If these PRs merge firstThis PR will likely need a rebase:
|
Issue being fixed or feature implemented
Two defects in the
QGETDATAbranch ofNetQuorum::ProcessMessage.1. Requester-supplied
nErrorsuppressed the ban.CQuorumDataRequest's serialisation reads an optional trailingnErrorbyte. It is a response-only field - writers skip it when undefined - but the request handler read it back and passed it intosendQDATA. In that switch,QUORUM_VERIFICATION_VECTOR_MISSINGandENCRYPTED_CONTRIBUTIONS_MISSINGdeliberately skip the "request limit exceeded" misbehaviour score, so a requester that supplied its own error byte evaded the score-25 ban while still forcing verification-vector serialisation and a LevelDB read on every repeat.2. Registration happened before validation.
RegisterDataRequestwas called before any validation, so everyQGETDATAinserted amapQuorumDataRequestsentry keyed on an attacker-chosenquorumHash. A fresh hash is never "already pending", so the rate limit never fired and never bounded the map. Entries live 300+60 s and are reaped only fromCleanupExpiredDataRequestsviaUpdatedBlockTip, which is skipped during IBD or when unsynced.Reachability is broader than it first appears: the handler requires the victim to be a masternode and the peer to be either MNAuth-verified or
qwatch, butqwatchis set by a bareQWATCHmessage from any peer. A single TCP connection from an unauthenticated peer is therefore enough to grow the map without bound.What was done?
QGETDATAcarrying a non-undefinednError. Honest senders never emit it on a request.RegisterDataRequestafter validation and add a per-identity cap ofMAX_INBOUND_DATA_REQUESTS = 64, decremented at the single erase site.Known remaining gap: all
qwatchpeers share the null-proRegTx budget, so one attacker can deny that budget to every legitimate watch peer for up to 360 s - previously this was only a soft score. Because the budget is released only byCleanupExpiredDataRequests, a stalled tip or IBD leaves those identities locked out for longer. Splitting the budget per peer rather than per identity is left as follow-up.How Has This Been Tested?
The first commit adds a regression test proving the
nErrorpath bypasses the rate-limit ban, ordered before the fix.Full build and test validation is delegated to CI on this PR; the changes were not built locally.
Breaking Changes
None.
nErroron an inbound request was never meaningful.Checklist: