Skip to content

fix: resolve signed integer overflow UB in CoinJoin priority and timeout#7236

Open
thepastaclaw wants to merge 3 commits intodashpay:developfrom
thepastaclaw:fix-coinjoin-ub
Open

fix: resolve signed integer overflow UB in CoinJoin priority and timeout#7236
thepastaclaw wants to merge 3 commits intodashpay:developfrom
thepastaclaw:fix-coinjoin-ub

Conversation

@thepastaclaw
Copy link

Summary

Fix two signed integer overflow UB issues in CoinJoin code, found during fuzz testing.

CalculateAmountPriority (common.h)

The return type is int but the computation -(nInputAmount / COIN) operates on
int64_t values. When nInputAmount is extremely large (e.g. near MAX_MONEY),
the result exceeds INT_MAX and the implicit narrowing to int is undefined
behavior under UBSan.

Fix: Clamp the int64_t result to [INT_MIN, INT_MAX] before returning.
This preserves the existing sort ordering for all realistic inputs while making
extreme values well-defined.

IsTimeOutOfBounds (coinjoin.cpp)

The expression current_time - nTime overflows when the two int64_t values
differ by more than INT64_MAX (e.g. one large positive, one large negative).

Fix: Compute the absolute difference using unsigned arithmetic, which is
well-defined for all inputs.

Validation

  • Both functions are non-consensus (CoinJoin sort priority and queue timeout only)
  • Neither overflow is exploitable — CoinJoin queue entries require valid MN signatures,
    and the priority function only affects local sort order
  • The fixes preserve identical behavior for all realistic inputs
  • Found via UBSan-instrumented fuzz testing on the ci/fuzz-regression branch

@thepastaclaw
Copy link
Author

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Mar 18, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@github-actions
Copy link

github-actions bot commented Mar 18, 2026

✅ No Merge Conflicts Detected

This PR currently has no conflicts with other open PRs.

@coderabbitai
Copy link

coderabbitai bot commented Mar 18, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: d5a0b9c2-a130-458b-9f5e-8e83f880ef0b

📥 Commits

Reviewing files that changed from the base of the PR and between 4248612 and f969cc5.

📒 Files selected for processing (2)
  • src/coinjoin/coinjoin.cpp
  • src/coinjoin/common.h
✅ Files skipped from review due to trivial changes (1)
  • src/coinjoin/common.h
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/coinjoin/coinjoin.cpp

Walkthrough

The pull request contains two safety and optimization improvements to the coinjoin module. The first change simplifies timeout validation in IsTimeOutOfBounds by replacing a two-branch conditional check with a single unsigned difference computation that checks if the absolute difference between timestamps exceeds the threshold. The second change adds overflow prevention to CalculateAmountPriority by using std::clamp to constrain the computed value within valid integer range before casting. Both changes are localized modifications with no alterations to function signatures or public APIs.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and specifically describes the main change: resolving signed integer overflow undefined behavior in CoinJoin priority and timeout functions.
Description check ✅ Passed The description is well-related to the changeset, providing detailed context about the two UB issues being fixed, their root causes, solutions, and validation information.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/coinjoin/coinjoin.cpp`:
- Around line 57-61: Run clang-format on the indicated block (or the whole file)
to fix the whitespace/line-wrapping so the ternary expression and return
statement match project style; reformat the block containing current_time, nTime
and COINJOIN_QUEUE_TIMEOUT (the diff calculation and return) using your
project's clang-format config and commit the changes so the Clang Diff Format
Check passes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 82b1eff3-1f1e-4510-a19f-8c06e26a762b

📥 Commits

Reviewing files that changed from the base of the PR and between 1d212a1 and 4248612.

📒 Files selected for processing (2)
  • src/coinjoin/coinjoin.cpp
  • src/coinjoin/common.h

CalculateAmountPriority in common.h could overflow when assigning a
negated int64_t division result to an int return type with extreme
CAmount values. Clamp the result to INT_MIN/INT_MAX before returning.

IsTimeOutOfBounds in coinjoin.cpp could overflow on signed subtraction
when current_time and nTime differ by more than INT64_MAX. Use unsigned
arithmetic to compute the absolute difference safely.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Author

@thepastaclaw thepastaclaw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

Clean, correct two-line fix for signed integer overflow UB in CoinJoin timeout and priority. Both arithmetic changes are mathematically sound and preserve original semantics. The only gap is the absence of regression tests exercising the extreme-value inputs that motivated these fixes.

Reviewed commit: ab4bea6

🟡 2 suggestion(s) | 💬 1 nitpick(s)

1 additional finding

💬 nitpick: Pre-existing: float-to-int implicit narrowing on denomination priority return

src/coinjoin/common.h (line 125)

Line 125 returns (float)COIN / *optDenom * 10000, which undergoes implicit float-to-int conversion. With current denominations the maximum result is ~10,000,000 (for the smallest denomination COIN/1000+1), safely within INT_MAX. But this is the same class of narrowing issue this PR fixes for the nondenom path. Not blocking since current values are safe, but worth noting for consistency.

🤖 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 `src/coinjoin/coinjoin.cpp`:
- [SUGGESTION] lines 60-62: No regression test for extreme timestamp values in IsTimeOutOfBounds
  The fix is correct: unsigned subtraction after a signed comparison produces the exact mathematical absolute difference for all int64_t pairs, including mixed-sign and extreme values. However, existing tests in coinjoin_queue_tests.cpp (deltas of ±15, ±60) and coinjoin_inouts_tests.cpp (delta of COINJOIN_QUEUE_TIMEOUT+1) only cover small time deltas around realistic timestamps. No test exercises the scenario that triggered the UBSan finding — e.g., current_time = INT64_MAX and nTime = INT64_MIN, or vice versa. A targeted test would prevent regressions and document the fix motivation.

In `src/coinjoin/common.h`:
- [SUGGESTION] lines 132-133: No regression test for CalculateAmountPriority clamp boundary
  The clamp is correct. Note that with valid money amounts, the old code would not have overflowed int: -(MAX_MONEY / COIN) = -21000000, well within INT_MIN..INT_MAX. The UB requires nInputAmount > INT_MAX * COIN (~2.1×10¹⁷), which exceeds MAX_MONEY but is representable in int64_t. Since CalculateAmountPriority accepts CAmount (int64_t) without a MoneyRange guard, fuzz testing can reach these values. There are no tests for this function at all — the function is constexpr, so a static_assert or compile-time test could verify the clamping directly.

Comment on lines +60 to +62
const uint64_t diff = (current_time > nTime) ? static_cast<uint64_t>(current_time) - static_cast<uint64_t>(nTime)
: static_cast<uint64_t>(nTime) - static_cast<uint64_t>(current_time);
return diff > static_cast<uint64_t>(COINJOIN_QUEUE_TIMEOUT);
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: No regression test for extreme timestamp values in IsTimeOutOfBounds

The fix is correct: unsigned subtraction after a signed comparison produces the exact mathematical absolute difference for all int64_t pairs, including mixed-sign and extreme values. However, existing tests in coinjoin_queue_tests.cpp (deltas of ±15, ±60) and coinjoin_inouts_tests.cpp (delta of COINJOIN_QUEUE_TIMEOUT+1) only cover small time deltas around realistic timestamps. No test exercises the scenario that triggered the UBSan finding — e.g., current_time = INT64_MAX and nTime = INT64_MIN, or vice versa. A targeted test would prevent regressions and document the fix motivation.

source: ['claude', 'codex']

🤖 Fix this 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 `src/coinjoin/coinjoin.cpp`:
- [SUGGESTION] lines 60-62: No regression test for extreme timestamp values in IsTimeOutOfBounds
  The fix is correct: unsigned subtraction after a signed comparison produces the exact mathematical absolute difference for all int64_t pairs, including mixed-sign and extreme values. However, existing tests in coinjoin_queue_tests.cpp (deltas of ±15, ±60) and coinjoin_inouts_tests.cpp (delta of COINJOIN_QUEUE_TIMEOUT+1) only cover small time deltas around realistic timestamps. No test exercises the scenario that triggered the UBSan finding — e.g., current_time = INT64_MAX and nTime = INT64_MIN, or vice versa. A targeted test would prevent regressions and document the fix motivation.

Comment on lines +132 to +133
const int64_t val = -(nInputAmount / COIN);
return int(std::clamp<int64_t>(val, std::numeric_limits<int>::min(), std::numeric_limits<int>::max()));
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: No regression test for CalculateAmountPriority clamp boundary

The clamp is correct. Note that with valid money amounts, the old code would not have overflowed int: -(MAX_MONEY / COIN) = -21000000, well within INT_MIN..INT_MAX. The UB requires nInputAmount > INT_MAX * COIN (~2.1×10¹⁷), which exceeds MAX_MONEY but is representable in int64_t. Since CalculateAmountPriority accepts CAmount (int64_t) without a MoneyRange guard, fuzz testing can reach these values. There are no tests for this function at all — the function is constexpr, so a static_assert or compile-time test could verify the clamping directly.

source: ['claude', 'codex']

🤖 Fix this 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 `src/coinjoin/common.h`:
- [SUGGESTION] lines 132-133: No regression test for CalculateAmountPriority clamp boundary
  The clamp is correct. Note that with valid money amounts, the old code would not have overflowed int: -(MAX_MONEY / COIN) = -21000000, well within INT_MIN..INT_MAX. The UB requires nInputAmount > INT_MAX * COIN (~2.1×10¹⁷), which exceeds MAX_MONEY but is representable in int64_t. Since CalculateAmountPriority accepts CAmount (int64_t) without a MoneyRange guard, fuzz testing can reach these values. There are no tests for this function at all — the function is constexpr, so a static_assert or compile-time test could verify the clamping directly.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The value for this function is coming as effective_value:

    /** The value of the UTXOs after deducting the cost of spending them at the effective feerate. */
    CAmount effective_value{0};

which is never bigger than MAX_MONEY.

This fix is unexpected complication of code IMO

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right that in practice effective_value is always bounded by MAX_MONEY, so -(MAX_MONEY / COIN) = -21000000 fits comfortably in int. The narrowing is safe for all realistic inputs.

The clamp was added because UBSan flagged this during fuzz testing — the fuzz harness can feed arbitrary CAmount values to CalculateAmountPriority without going through the wallet code path that enforces the money range constraint. The implicit int64_t → int narrowing is technically undefined behavior per the standard when the value doesn't fit, even if it's unreachable in production.

That said, I can see the argument that defending against inputs that can't occur in practice adds unnecessary complexity. A few options:

  1. Keep the clamp (current) — zero-cost for realistic values, makes the function correct for all inputs
  2. Replace with Assume() — e.g. Assume(std::abs(val) <= INT_MAX) to document the precondition without the clamp
  3. Remove the clamp entirely — just add a comment that narrowing is safe because callers always pass values ≤ MAX_MONEY

Happy to go with whichever you prefer. The IsTimeOutOfBounds fix (unsigned arithmetic) is the more critical one — it can trigger on any signed subtraction with large deltas.

Copy link
Author

@thepastaclaw thepastaclaw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

Test-only follow-up commit adding comprehensive regression tests for both UB fixes. All prior review suggestions are now addressed: queue_timestamp_extreme_values covers INT64_MIN/INT64_MAX edge cases for IsTimeOutOfBounds, and calculate_amount_priority_clamp (plus two static_asserts) verifies the clamping boundary for CalculateAmountPriority. Tests are well-structured and exercise the exact scenarios that motivated the fixes.

Reviewed commit: 1053f37

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@thepastaclaw
Copy link
Author

Updated per knst's feedback: replaced magic numbers in tests with symbolic expressions (-(MAX_MONEY / COIN), -INT_MAX) so the expected values are self-documenting.

Copy link
Author

@thepastaclaw thepastaclaw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incremental Review (push 4cc88eaf vs prior reviewed 1053f373)

Delta: 1 file changed — src/test/coinjoin_queue_tests.cpp (4 insertions, 5 deletions)

Test-only commit replacing hardcoded magic numbers with symbolic constants: -21000000-(MAX_MONEY / COIN), -2147483647-INT_MAX. Pure readability improvement, no behavioral change.

Prior review stands — no new findings. ✅

}

static_assert(CoinJoin::CalculateAmountPriority(MAX_MONEY) == -(MAX_MONEY / COIN));
static_assert(CoinJoin::CalculateAmountPriority(static_cast<CAmount>(INT64_MAX)) == std::numeric_limits<int>::min());
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this unit test doesn't make any sense, because CAmount for CJ never could be more than MAX_MONEY, isn't it?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed — the calculate_amount_priority_clamp test and static_assert are only meaningful if we keep the clamp. If we drop the clamp per the discussion above, these tests should go too.

The queue_timestamp_extreme_values tests (for IsTimeOutOfBounds) are still important though — those exercise the actual UB that existed in the signed subtraction path.

@PastaPastaPasta PastaPastaPasta marked this pull request as ready for review March 24, 2026 14:42
@UdjinM6
Copy link

UdjinM6 commented Mar 24, 2026

We could simply bail out on invalid values instead of trying to handle them i.e. if (current_time < 0 || nTime < 0) return true; in IsTimeOutOfBounds() and if (!MoneyRange(nInputAmount)) return 0; in CalculateAmountPriority()

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.

3 participants