Skip to content

fix: update masternode rate limit on failed AddTrigger path - #65

Draft
PastaPastaPasta wants to merge 21 commits into
developfrom
sec/u009
Draft

fix: update masternode rate limit on failed AddTrigger path#65
PastaPastaPasta wants to merge 21 commits into
developfrom
sec/u009

Conversation

@PastaPastaPasta

Copy link
Copy Markdown
Owner

Audit finding U009 (high, CONFIRMED — downgraded from critical), plus U003, the same defect reported from a second audit region.

Issue

CGovernanceManager::AddGovernanceObjectInternal called MasternodeRateUpdate only after the AddTrigger check. The failed-trigger path returned early, so the rate buffer was never advanced.

MasternodeRateCheck short-circuits return true when mapLastMasternodeObject has no entry for the outpoint, and MasternodeRateUpdate is the sole writer of that map. A masternode operator key submitting only unparseable triggers therefore stayed absent from the map forever and was never throttled — one key could flood mapObjects without limit.

Fix

  • src/governance/governance.cpp: call MasternodeRateUpdate before the AddTrigger check, so every attempt counts against the per-masternode buffer.

The review follow-up fixes a problem introduced by that hoist. MasternodeRateUpdate did two unrelated things — advance the rate buffer, and insert into setAdditionalRelayObjects for triggers created close to MAX_TIME_FUTURE_DEVIATION. Moving it above the check therefore armed deferred re-announcement for objects the node had just undone and PrepareDeletion-ed. Since CheckPostponedObjects only inspects fValid/fReady and never the delete flag, those objects would be announced to every peer and served on GETDATA — restoring the fan-out amplification the fix exists to remove.

The relay scheduling is now a separate ScheduleAdditionalRelay() called only after the check passes. Rate accounting counts every attempt; only kept objects are announced.

Tests

test: failed-trigger path must advance masternode rate limit precedes the fix. Verified by reverting: without the fix 12/12 flooded objects are accepted, with it 6.

Review notes

Wrongful-lockout was checked explicitly, since rate-limit bugs cut both ways. An honest MN creates one trigger per superblock cycle via CreateGovernanceTrigger, and only if it is the projected payee — and it pre-checks MasternodeRateCheck and declines rather than burning buffer slots. With RATE_BUFFER_SIZE = 5 the buffer must span a very short window to trip, so honest cadence is nowhere near it. mapLastMasternodeObject is persisted and keyed on the collateral outpoint, so a reconnect does not reset it.

Known-remaining, deliberately out of scope: GetRate() returns 0.0 below 5 samples, so a bounded 5-object burst per MN is still free; and TRIGGER vchData has no size cap (MAX_DATA_SIZE is enforced only for PROPOSALs). The latter likely matters more than the count limit.

Based on dashpay/dash develop @ 6d04c60ef36. Not rebase-tested against a newer tip; full functional suite not run.

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented Jul 26, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 0a0096ec-00b6-4c17-8bce-20f9ce17d372

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch sec/u009

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.

@PastaPastaPasta
PastaPastaPasta force-pushed the sec/u009 branch 4 times, most recently from 1d6dda8 to 4e76c10 Compare August 3, 2026 18:49
The cppcheck linter has been silently analyzing nothing (see next commit), letting several warnings in non-backported files accumulate. Fix the ones that the linter's ALWAYS_ENABLED_WARNINGS patterns force-report: remove unused/dead locals, make single-argument constructors explicit (with an inline suppression for CBLSIdImplicit, whose implicit conversion is intentional), narrow benchmark counters to the scope they are used in, pass CSigBase and BlsCheck constructor arguments by reference/move, and inline-suppress a danglingTempReference false positive on a lifetime-extended range-for temporary.
The linter has been vacuous in two ways. First, without __GNUC__ defined, src/attributes.h hits '#error No known always_inline attribute', which aborts cppcheck's analysis of nearly every translation unit; the resulting preprocessorErrorDirective lines were then dropped by the output filter because they don't point at files from non-backported.txt. Second, even with preprocessing fixed, cppcheck 2.17.1 crashes with an assertion in TokenList::setLang under --check-level=exhaustive, and that crash was explicitly suppressed.

Define __GNUC__ so preprocessing succeeds, bump cppcheck to 2.21.0 (which no longer crashes with exhaustive checking) and drop the crash suppression, and treat analysis failures (preprocessorErrorDirective, syntaxError, internal errors) as lint failures regardless of which file they point at so the linter can never silently go vacuous again. Making syntaxError fatal immediately surfaced a real case: QT_VERSION_CHECK is a function-like macro cppcheck cannot evaluate, which aborted analysis of the Qt translation units, so define it on the command line too.

Fail on any nonzero cppcheck exit status. Without --error-exitcode, diagnostics never make cppcheck return nonzero, so a nonzero status always means the analysis itself failed (bad arguments, unloadable config, OOM kill, crash) and must not pass. Filter out 'note:'/source-context lines, which don't carry the check id that suppressions match on and would leak through when their parent warning is suppressed, while matching all real diagnostic severities (the gcc template currently renders them all as 'warning:', but match the raw severities too in case that changes).

Finally, suppress the check ids with pre-existing violations in the tree so the linter can be enforced; these should be burned down and re-enabled over time.
Avoid dangling reference in C++20 by binding the temporary RPCResult returned by CGovernanceObject::GetVotesJsonHelp to a named local variable before iterating over m_inner.
Define QT_CONFIG(x)=0 so cppcheck doesn't hit a fatal syntaxError when analyzing Qt translation units that include uic-generated headers (e.g. ui_masternodelist.h) in built working trees.
…port warnings

dc99412 fix(lint): define QT_CONFIG macro in lint-cppcheck-dash (pasta)
9c39224 fix(rpc): bind temporary RPCResult to local variable in ListObjectsHelp (pasta)
9303de4 lint: re-enable uninitMemberVarNoCtor cppcheck (pasta)
ab62dfe lint: re-enable shadowFunction cppcheck (pasta)
0bce8c3 lint: re-enable functionStatic cppcheck (pasta)
7bbba5d lint: re-enable knownConditionTrueFalse cppcheck (pasta)
99162fa lint: re-enable missingOverride cppcheck (pasta)
8155ea7 lint: re-enable constParameterReference cppcheck (pasta)
64ce038 lint: re-enable shadowMember cppcheck (pasta)
12c0711 lint: re-enable shadowVariable cppcheck (pasta)
964de2d lint: re-enable useInitializationList cppcheck (pasta)
47022ca lint: re-enable returnByReference cppcheck (pasta)
c85b5f9 lint: re-enable constVariableReference cppcheck (pasta)
9477b4d lint: re-enable constVariablePointer cppcheck (pasta)
81b33d3 lint: re-enable assertWithSideEffect cppcheck (pasta)
ddbfd7b fix(lint): make lint-cppcheck-dash actually report warnings (pasta)
1b2e756 fix: address cppcheck warnings hidden by vacuous lint-cppcheck-dash (pasta)

Pull request description:

  ## Issue being fixed or feature implemented
  `test/lint/lint-cppcheck-dash.py` has been effectively vacuous — it ran cppcheck, silently analyzed almost nothing, and always passed. Two failure modes:

  1. With the script's `-D` list, `__GNUC__` is not defined, so `src/attributes.h` hits `#error No known always_inline attribute` (`preprocessorErrorDirective`), aborting cppcheck's analysis of nearly every translation unit. The error lines were then dropped by the output filter, which only keeps lines pointing at files from `test/util/data/non-backported.txt`.
  2. Even with preprocessing fixed, cppcheck 2.17.1 with `--check-level=exhaustive` crashes with an assertion in `TokenList::setLang` (child dies with signal 6) — and that crash message was explicitly listed in `SUPPRESSED_WARNINGS`.

  Net effect: an injected canary warning in a non-backported file was not flagged, locally or in CI.

  ## What was done?
  **Linter (`test/lint/lint-cppcheck-dash.py`):**
  - Define `__GNUC__` so preprocessing succeeds.
  - Add a `FATAL_ERRORS` list (`preprocessorErrorDirective`, `syntaxError`, internal errors/crashes) that fails the lint regardless of which file the line points at, so analysis failures can never again be silently filtered away. Making `syntaxError` fatal immediately surfaced real cases: `QT_VERSION_CHECK` and `QT_CONFIG` are function-like macros cppcheck cannot evaluate on its own, which aborted analysis of Qt translation units (especially in working trees with `uic`-generated headers like `ui_masternodelist.h`) — they are now defined on the cppcheck command line (`-DQT_VERSION_CHECK=...`, `-DQT_CONFIG(x)=0`).
  - Fail on any nonzero cppcheck exit status. Without `--error-exitcode`, diagnostics never make cppcheck return nonzero, so a nonzero status always means the analysis itself failed (bad arguments, unloadable config, OOM kill, crash) and cannot be allowed to pass.
  - Drop the signal-6 crash suppression (the TODO said to remove it with a newer cppcheck).
  - Skip `note:`/source-context lines: they don't carry the check id that suppressions match on, so orphaned notes of suppressed warnings leaked through the filter.
  - Suppress pre-existing violations so the linter can be enforced now, documented as a burn-down TODO. Most suppressions are deliberately narrow — targeted message/class-scoped regexes for `knownConditionTrueFalse` (always-false `state.Invalid(...)`/`state.Error(...)` returns), `shadowFunction` (the `_` translation function), and `uninitMemberVarNoCtor` (`ActiveDKG`/`UtilParameters` members) — so new violations of these checks elsewhere in the tree are still reported. `duplInheritedMember` and `useStlAlgorithm` are suppressed wholesale by check id: the former as a plain burn-down entry, the latter deliberately — earlier revisions of this branch rewrote the flagged raw loops into `std::ranges` algorithms, but lambda-based algorithms lose Clang thread-safety-analysis lock context (`cs_wallet`, `cs_coinjoin`, `cs_store`) and obscure otherwise-clear control flow, so the loops stay and the check is disabled instead. Messages matching `ALWAYS_ENABLED_WARNINGS` still override these suppressions.

  **Container (`contrib/containers/ci/ci-slim.Dockerfile`):** bump cppcheck 2.17.1 → 2.21.0, which no longer crashes under `--check-level=exhaustive`.

  **Code fixes** for the ~11 warnings that `ALWAYS_ENABLED_WARNINGS` patterns force-report (these cannot be suppressed by check id): removed dead locals (`src/active/dkgsession.cpp`, `src/rpc/evo.cpp`), made single-argument constructors `explicit` (`chainlock::Chainlocks`, `CDSTXManager`; no implicit-conversion call sites exist), inline-suppressed `CBLSIdImplicit`'s intentionally implicit constructor, narrowed three static benchmark counters to their usage scope in `src/evo/specialtxman.cpp` (static storage duration unchanged), passed `CSigBase` by const reference in `InitSession` (callers already sliced identically by value; accessors are non-virtual), moved `BlsCheck`'s by-value constructor parameters into members, and bound the temporary `RPCResult` returned by `GetVotesJsonHelp` to a local variable in `src/rpc/governance.cpp` (avoiding a C++20 dangling reference).

  ## How Has This Been Tested?
  With cppcheck 2.21.0 locally (matching the bumped container version):
  - Injected a canary (`unreadVariable`) into `src/spork.cpp`: the linter reports exactly that warning and exits 1.
  - Canary removed: full 273-file run is clean and exits 0 (tested both clean and built working trees).
  - Removed `-D__GNUC__` to simulate failure mode 1: the `attributes.h` `#error` line surfaces via `FATAL_ERRORS` and the linter exits 1.
  - All touched translation units pass `clang -fsyntax-only` with the project's compile flags; `test/lint/lint-python.py` passes.

  Note: the previously-suppressed `count_if`/`find_if` `useStlAlgorithm` variants are now covered by the wholesale id suppression along with the rest of the burn-down list.

  ## Breaking Changes
  None. CI lint may take somewhat longer since cppcheck now actually analyzes all 273 non-backported files with `--check-level=exhaustive`.

  ## Checklist:
  - [x] I have performed a self-review of my own code
  - [x] I have commented my code, particularly in hard-to-understand areas
  - [ ] I have added or updated relevant unit/integration/functional/e2e tests
  - [ ] I have made corresponding changes to the documentation
  - [ ] I have assigned this pull request to a milestone _(for repository code-owners and collaborators only)_

Top commit has no ACKs.

Tree-SHA512: 3da03772c968eebd5e2945cfc000c4249fc5cf630db1738f38dbb54bdc70e16fce8b851e984125cac1a1676acd5f0687610eecb3d1c7f24c21bb309ca2e91902
AddGovernanceObjectInternal emplaced a trigger into mapObjects, attempted AddTrigger, and on failure returned early before MasternodeRateUpdate. MasternodeRateCheck treats a missing rate-buffer entry as allow, so a masternode that never lands a successful trigger was never rate-limited at all, defeating precisely the limiter meant to stop it. Each rejected trigger still cost a BLS verification and left a mapErasedGovernanceObjects entry retained for roughly 60 days on mainnet.

Advance the rate buffer on the failure path too. Relay scheduling is extracted so a trigger just marked deleted is not added to the additional-relay set; that part is a self-correction for a regression this change would otherwise introduce, not a pre-existing bug.
Move the failed-trigger rate regressions into governance_inv_tests.cpp,
use SetMockTime(0s), and rename ScheduleAdditionalRelay to
ScheduleTriggerRelay since it only applies to triggers.
Fold the failed-trigger rate regressions into the single
governance_inv_tests suite. GovernanceInvSetup now owns the DIP3 /
ProRegTx path used by those cases and also covers the INV/vote tests.
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