Skip to content

Conversation

@Chand-ra
Copy link

The fuzz-amount test doesn't fuzz the arithmetic operations for struct amount_sat and struct amount_msat. Add a test for the same.

Checklist

Before submitting the PR, ensure the following tasks are completed. If an item is not applicable to your PR, please mark it as checked:

  • The changelog has been updated in the relevant commit(s) according to the guidelines.
  • Tests have been added or modified to reflect the changes.
  • Documentation has been reviewed and updated as needed.
  • Related issues have been listed and linked, including any that this PR closes.

@Chand-ra
Copy link
Author

Hey @morehouse , running this test throws the following errors:

  • common/amount.c:359:18: runtime error: -nan is outside the range of representable values of type 'unsigned long'
  • common/amount.c:345:23: runtime error: -inf is outside the range of representable values of type 'unsigned long'
  • common/amount.c:345:23: runtime error: -1.10313e+217 is outside the range of representable values of type 'unsigned long'

among a few others, which are probably due to faulty harness logic. The above failures happen in the following two functions:

WARN_UNUSED_RESULT bool amount_msat_scale(struct amount_msat *val,
					  struct amount_msat msat,
					  double scale)
{
	double scaled = msat.millisatoshis * scale;

	/* If mantissa is < 64 bits, a naive "if (scaled >
	 * UINT64_MAX)" doesn't work.  Stick to powers of 2. */
	if (scaled >= (double)((u64)1 << 63) * 2)
		return false;
345	val->millisatoshis = scaled;
	return true;
}

WARN_UNUSED_RESULT bool amount_sat_scale(struct amount_sat *val,
					 struct amount_sat sat,
					 double scale)
{
	double scaled = sat.satoshis * scale;

	/* If mantissa is < 64 bits, a naive "if (scaled >
	 * UINT64_MAX)" doesn't work.  Stick to powers of 2. */
	if (scaled >= (double)((u64)1 << 63) * 2)
		return false;
359	val->satoshis = scaled;
	return true;
}

Has the fuzzer found an actual bug or is it just another impossible edge case?

@morehouse
Copy link
Contributor

Has the fuzzer found an actual bug or is it just another impossible edge case?

I think technically these are bugs -- amount_msat_scale and amount_sat_scale are supposed to return false when scaling fails and already do this for the positive-overflow case. Since there's no usage comment requiring scale to be positive, probably the functions should handle negative overflow as well.

In practice, all current usages of these functions in the codebase have positive values for scale, so the bug can never manifest in the existing codebase.

@Chand-ra
Copy link
Author

In practice, all current usages of these functions in the codebase have positive values for scale, so the bug can never manifest in the existing codebase.

Makes me wonder why scale's type isn't unsigned in the first place...

@morehouse
Copy link
Contributor

Makes me wonder why scale's type isn't unsigned in the first place...

Probably because there's no way to represent an unsigned float.

@Chand-ra Chand-ra force-pushed the fuzz-amount-arith branch from 2e97725 to 108f7a6 Compare May 22, 2025 11:23
@Chand-ra
Copy link
Author

Chand-ra commented Jun 2, 2025

Hey @morehouse,

As we discussed over email, I spent some time last week investigating the potential bug caused by an assertion failure in amount_msat_sub_fee() to determine whether it could be triggered externally. I grepped the codebase for all callers of amount_msat_sub_fee() and found that the only caller resides in plugins/askrene/refine.c. This, in turn, ultimately traces back to askrene’s getroutes command.

To test this path, I added a user-level Python test in tests/test_askrene.py to try passing the error-causing parameters to amount_msat_sub_fee(). I found that any parameter values that get too large are truncated to a set of ‘safe values’, making it impossible to reproduce the breakage through this command.

In summary, it doesn’t appear that any external message can trigger the failure seen in the fuzz test. I’ve pushed the test in case you’d like to take a look.

@Chand-ra Chand-ra force-pushed the fuzz-amount-arith branch from c587612 to e29abd8 Compare July 23, 2025 10:42
@Chand-ra Chand-ra marked this pull request as ready for review July 23, 2025 10:42
@Chand-ra Chand-ra force-pushed the fuzz-amount-arith branch from e29abd8 to 5dfa115 Compare July 23, 2025 10:43
@Chand-ra
Copy link
Author

The test works without any breakage now and is ready to be merged.

Copy link
Contributor

@morehouse morehouse left a comment

Choose a reason for hiding this comment

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

I am hesitant to merge this fuzz target, since it basically has to reimplement all the operations in common/amount.c so we have something to compare against. It seems a bit silly when we could just directly review the implementations in common/amount.c.

That said, the target did find a couple cases of undefined behavior, which makes me think it would be worth merging this target at least as regression tests for those bugs.

static struct amount_sat fromwire_amount_sat_bounded(const u8 **cursor, size_t *max)
{
struct amount_sat amt = fromwire_amount_sat(cursor, max);
amt.satoshis %= (MAX_BTC + 1);
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: the whole file needs to be clang-format-ed. There's a mix of indentation styles going on.

Comment on lines 24 to 28
struct amount_msat amt;
struct amount_sat amt_bounded = fromwire_amount_sat_bounded(cursor, max);
if (!amount_sat_to_msat(&amt, amt_bounded))
assert(false && "amount_sat_to_msat failed!");
return amt;
Copy link
Contributor

Choose a reason for hiding this comment

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

This will always return amts that are multiples of 1000. Ideally we would test the full precision.

struct amount_sat sb = amount_msat_to_sat_round_down(b);

u64 u64_param;
memcpy(&u64_param, &f, sizeof(u64_param));
Copy link
Contributor

Choose a reason for hiding this comment

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

This seems odd. Why are we copying the double into a u64?

Since we have all these different params we might use, maybe we should just put them in a struct for simplicity:

struct fuzzing_params {
  enum op op;
  struct amount_msat a;
  struct amount_msat b;
  double f;
  u64 u64_param;
};

void run(const u8 *data, size_t size) {
  struct fuzzing_params f;
  if (size < sizeof(f)) return;

  memcpy(&f, data, sizeof(f));
  ...
}

static struct amount_sat fromwire_amount_sat_bounded(const u8 **cursor, size_t *max)
{
struct amount_sat amt = fromwire_amount_sat(cursor, max);
amt.satoshis %= (MAX_BTC + 1);
Copy link
Contributor

Choose a reason for hiding this comment

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

By limiting sats in this way, we will probably miss the overflow code paths during fuzzing.

Copy link
Author

Choose a reason for hiding this comment

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

Right, but isn't it a valid assumption that in production we won't be doing arithmetic on anything greater than the greatest possible value for satoshis/millisatoshis?

Comment on lines 144 to 146
/* Guard against NaN and -ve factors. */
if (f != f || f < 0)
break;
Copy link
Contributor

Choose a reason for hiding this comment

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

This is here to avoid the UBSan error that is fixed in: #8306. Same for OP_SAT_SCALE.

I think it would be better to merge that PR first and remove this guard here.

Comment on lines 238 to 242
struct amount_msat expected_total;
assert(amount_msat_add(&expected_total, original, fee));
assert(amount_msat_eq(total, expected_total));

a = total;
Copy link
Contributor

Choose a reason for hiding this comment

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

It looks like this is testing that original + fee == original + fee, which will always be true. I think this whole case is probably pointless.

Copy link
Author

Choose a reason for hiding this comment

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

I agree that this check is redundant but do think there is some merit to the test case, it at least makes sure amount_msat_fee doesn't crash with arbitrary input.

if (amount_msat_fee(&fee, output, fee_base, fee_prop)) {
struct amount_msat sum;
if (amount_msat_add(&sum, output, fee))
assert(amount_msat_less_eq(sum, input));
Copy link
Contributor

Choose a reason for hiding this comment

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

Should they be exactly equal?

Copy link
Author

Choose a reason for hiding this comment

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

In amount_msat_fee's defintion:

/* BOLT #7:
*
*   - SHOULD accept HTLCs that pay a fee equal to or greater than:
*    - fee_base_msat + ( amount_to_forward * fee_proportional_millionths / 1000000 )
*/

So it is valid for sum to be <= input.

Comment on lines 265 to 266
if (b.millisatoshis > SIZE_MAX)
break;
Copy link
Contributor

Choose a reason for hiding this comment

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

This check seems pointless, since the cast to size_t below will ensure that weight < SIZE_MAX.

{
if (b.millisatoshis > SIZE_MAX)
break;
u32 fee_per_kw = (u32)(a.millisatoshis & UINT32_MAX);
Copy link
Contributor

Choose a reason for hiding this comment

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

Masking is pointless if we're casting to u32 anyway.

Comment on lines 270 to 272
/* weights > 2^32 are not real tx and hence, discarded */
if (mul_overflows_u64(fee_per_kw, weight))
break;
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe we should just mask the weight with UINT32_MAX and remove this check.

Chandra Pratap added 2 commits July 25, 2025 08:43
Changelog-None: The `fuzz-amount` test doesn't fuzz the arithmetic
operations for `struct amount_sat` and `struct amount_msat`. Add a
test for them.
Add a minimal input set as a seed corpus for the newly introduced
test. This leads to discovery of interesting code paths faster.
@Chand-ra Chand-ra force-pushed the fuzz-amount-arith branch from 5dfa115 to 5fdb42c Compare July 25, 2025 08:43
@Chand-ra
Copy link
Author

The corpus for this test is now generated with the fix in #8306 applied, so that PR needs to be merged before this one can.

@rustyrussell rustyrussell added this to the v25.12 milestone Nov 7, 2025
@madelinevibes madelinevibes modified the milestones: v25.12, v26.03 Nov 12, 2025
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.

4 participants