Skip to content

Conversation

@elichai
Copy link
Contributor

@elichai elichai commented Mar 19, 2020

This was reported by practicalswift here #18046
The exact order of the if, is important, we first do !MoneyRange(tx_out.nValue) to make sure the amount is non-negative. and then std::numeric_limits<CAmount>::max() - tx_out.nValue < nValueOut checks that the addition cannot overflow (if we won't check that the amount is positive this check can also overflow! (by doing something like max - -max))
and only then we make sure that the some is also valid !MoneyRange(nValueOut + tx_out.nValue)
if any of these conditions fail we throw.

the overflowing logic:

a + b > max // we want to fail if a+b is more than the maximum -> will overflow
b > max - a
max - a < b

Closes: #18046

@laanwj
Copy link
Member

laanwj commented Mar 19, 2020

What is the concrete effect of this? Does this affect validation?

Can you please add a test that fails without this change?

@elichai
Copy link
Contributor Author

elichai commented Mar 19, 2020

What is the concrete effect of this? Does this affect validation?

Good question. technically this code is part of validation, it's called from tx_verify.cpp
I do not know if validation can ever execute this with an overflowing value, if so it's a real problem which is why I assume it probably can't. (i'll try writing a test now)
so if it can't happen and assuming this change is correct then this shouldn't affect validation itself

EDIT: Ok, testing it is a bit more complex, because this is only called on ConnectBlock or in mempool, so might take me some time, no rush.

@maflcko
Copy link
Member

maflcko commented Mar 19, 2020

This is not possible to trigger from a network node or RPC, because CheckTransaction is called before CheckInputs is called. (This call order is also a requirement for other reasons)

@maflcko
Copy link
Member

maflcko commented Mar 19, 2020

This was fixed in CVE-2010-5139

@maflcko
Copy link
Member

maflcko commented Mar 19, 2020

I am pretty sure the compiler will optimize out the unused result anyway (and thus the overflow), but if you strongly feel like this needs to be fixed, then you could reorder the code, so that the overflow in the unused result never happens:

  • First check the output value with MoneyRange
  • Calculate the result
  • Check the result with MoneyRange

@vasild
Copy link
Contributor

vasild commented Mar 19, 2020

How could it overflow, given that CAmount is int64_t. Expressed in satoshi - about 4000 times more than the total supply of bitcoin?

@maflcko
Copy link
Member

maflcko commented Mar 19, 2020

@vasild CAmount can be std::numeric_limits<CAmount>::max() or any other value in range for a serialized transaction. The deserialization code does not check for consensus rules.

@maflcko maflcko changed the title Check for overflow when calculating sum of outputs in a transaction refactor: Check for overflow when calculating sum of tx outputs Mar 19, 2020
@practicalswift
Copy link
Contributor

practicalswift commented Mar 19, 2020

Concept ACK: CTransaction::GetValueOut() should handle crazy amounts by throwing the expected exception. I think that was the intention behind the current code, so this is simply a bug AFAICT. (Luckily only reachable by the fuzzers at the moment (AFAICT).)

However, I think your fix can be simplified :)

Note that a + b cannot overflow if MoneyRange(a) == true and MoneyRange(b) == true. Where a and b are of type CAmount.

In other words, simply do if (!MoneyRange(a) || !MoneyRange(b)) { throw std::runtime_error(…); } and you should be safe to proceed with a += b? :)

Note that the MoneyRange(a + b) must obviously be checked too to make sure it is within the money bounds :)

Perhaps something along the lines of:

    if (!MoneyRange(a) || !MoneyRange(b) || !MoneyRange(a + b)) {
        throw std::runtime_error(…);
    }
    a += b;

@practicalswift
Copy link
Contributor

practicalswift commented Mar 19, 2020

@elichai

This is the fix I've used in my local fuzzing tree to avoid hitting this signed integer overflow over and over :)

diff --git a/src/primitives/transaction.cpp b/src/primitives/transaction.cpp
index 28c145f71..a1567e805 100644
--- a/src/primitives/transaction.cpp
+++ b/src/primitives/transaction.cpp
@@ -84,9 +84,10 @@ CAmount CTransaction::GetValueOut() const
 {
     CAmount nValueOut = 0;
     for (const auto& tx_out : vout) {
-        nValueOut += tx_out.nValue;
-        if (!MoneyRange(tx_out.nValue) || !MoneyRange(nValueOut))
+        if (!MoneyRange(nValueOut) || !MoneyRange(tx_out.nValue) || !MoneyRange(nValueOut + tx_out.nValue)) {
             throw std::runtime_error(std::string(__func__) + ": value out of range");
+        }
+        nValueOut += tx_out.nValue;
     }
     return nValueOut;
 }

Would love to see it upstreamed -- that would simplify my fuzzing setup :)

I agree with @laanwj that a test case which fails under UBSan prior to this PR should be included :)

@practicalswift
Copy link
Contributor

@MarcoFalke

I am pretty sure the compiler will optimize out the unused result anyway (and thus the overflow)

What unused result are you referring to? :)

@sipa
Copy link
Member

sipa commented Mar 19, 2020

@practicalswift nValueOut is computed but never inspected in case tx_out.nValue is out of range.

@practicalswift
Copy link
Contributor

practicalswift commented Mar 19, 2020

@sipa

Ah, in @elichai's suggested fix. I thought @MarcoFalke meant in current master :)

The patch I'm suggesting in the comment above looks robust and correct?

@vasild
Copy link
Contributor

vasild commented Mar 20, 2020

The patch I'm suggesting in the comment above looks robust and correct?

Yes, and also more readable than the one in this PR (c5dd8eb625).

@maflcko
Copy link
Member

maflcko commented Mar 20, 2020

I thought @MarcoFalke meant in current master :)

I do mean current master :)

as pointed out by pieter: "nValueOut is computed but never inspected in case tx_out.nValue is out of range." So I think the compiler will likely never produce a binary that computes nValueOut in the first place, or even if it did, it wouldn't matter because it is never read.

@practicalswift
Copy link
Contributor

An even simpler patch which also tests the post-condition:

diff --git a/src/primitives/transaction.cpp b/src/primitives/transaction.cpp
index 28c145f71..76acb08b0 100644
--- a/src/primitives/transaction.cpp
+++ b/src/primitives/transaction.cpp
@@ -9,6 +9,8 @@
 #include <tinyformat.h>
 #include <util/strencodings.h>

+#include <cassert>
+
 std::string COutPoint::ToString() const
 {
     return strprintf("COutPoint(%s, %u)", hash.ToString().substr(0,10), n);
@@ -84,10 +86,11 @@ CAmount CTransaction::GetValueOut() const
 {
     CAmount nValueOut = 0;
     for (const auto& tx_out : vout) {
-        nValueOut += tx_out.nValue;
-        if (!MoneyRange(tx_out.nValue) || !MoneyRange(nValueOut))
+        if (!MoneyRange(tx_out.nValue) || !MoneyRange(nValueOut + tx_out.nValue))
             throw std::runtime_error(std::string(__func__) + ": value out of range");
+        nValueOut += tx_out.nValue;
     }
+    assert(MoneyRange(nValueOut));
     return nValueOut;
 }

@instagibbs
Copy link
Member

I prefer @practicalswift solution above

@elichai
Copy link
Contributor Author

elichai commented Mar 21, 2020

as pointed out by pieter: "nValueOut is computed but never inspected in case tx_out.nValue is out of range." So I think the compiler will likely never compute nValueOut in the first place, or even if it did, it wouldn't matter because it is never read.

I honestly don't know if it's a problem overflowing without reading the value, probably isn't as you said.

An even simpler patch which also tests the post-condition:

It is a somewhat nicer fix :)

@promag
Copy link
Contributor

promag commented Mar 23, 2020

I also prefer #18383 (comment).

@elichai
Copy link
Contributor Author

elichai commented Mar 23, 2020

@practicalswift do you wan to open a new PR with your suggestion? or should I replace my diff here with yours?

@practicalswift
Copy link
Contributor

practicalswift commented Mar 23, 2020

@elichai Feel free to use the patch I suggested. That way we'll keep the review to this PR and I can keep my work-in-progress PR count down :)

@elichai elichai force-pushed the 2020-03-value-overflow branch 2 times, most recently from 9a04da1 to a4c79f8 Compare March 23, 2020 12:58
@practicalswift
Copy link
Contributor

ACK a4c79f8cf6e0407f7e937fb41c73127f630231fa

@instagibbs
Copy link
Member

utACK a4c79f8

@elichai elichai force-pushed the 2020-03-value-overflow branch from a4c79f8 to f65c9ad Compare March 23, 2020 14:36
#include <tinyformat.h>
#include <util/strencodings.h>

#include <assert.h>
Copy link
Contributor

@practicalswift practicalswift Mar 23, 2020

Choose a reason for hiding this comment

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

Nit: #include <assert.h> is deprecated in C++. Use the idiomatic form #include <cassert> :)

No need to invalidate ACK:s for this :)

@maflcko
Copy link
Member

maflcko commented Mar 23, 2020

As pointed out earlier, this indeed is not an issue, because gcc will produce identical binaries for before this PR and after this PR. The overflow never happens.

ACK f65c9ad, checked that clang with O2 produces identical binaries 💕

Show signature and timestamp

Signature:

-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA512

ACK f65c9ad40f2f5cdc581bdaf72e7dc68e9d7f7a80, checked that gcc with O2 produces identical binaries 💕
-----BEGIN PGP SIGNATURE-----

iQGzBAEBCgAdFiEE+rVPoUahrI9sLGYTzit1aX5ppUgFAlwqrYAACgkQzit1aX5p
pUj+xQv/d/bubEHp/tcMadyZVysqF3siEHt78ZoTKQRhFP5lw4uXb6ovzfzBja/a
kvrU8JXI9uhrb5crSgHSG7usI7c1hNh0dIN0xttD114U3irQJYdNkXT6OdJvwXMx
J+FB/95iD3SRz/WnDW1HI8s7D3eDRsPi66g+i/qVtiACuZnH0v8upNplGExv+IHF
ZDQrdNeTvwSXogBGmzNmNAXKI8YRXW2rqM+anQCPQPCLiSckitQoo7R8MffLkQWn
5czTy4+DOaz7iydxwDvEavu0MQ/VboC3pv8QzdD+Hna6in2kHAZ0b95DC73Xo123
Gp/LlwaYdkIWLgl15HUOGp0WZ0M8btc1yFFt69XhdS82gV0/AT+MWgDVLsZiCVnc
hcjvFFmxlv1Kx7HtOYWdkIwXSX9etJkbVcbi0nWn+8lIjhqI4K6FPeZmUkO8VxgZ
3ixw0PthqJD8R43kCLBy/W7JqFwRLYjE952d4Zwbu5Ub5ZAXzHS+pEiexn1rFRA/
EjBVwzaq
=5AXK
-----END PGP SIGNATURE-----

Timestamp of file with hash 25065ff511c22aa1d923f7b1fe85f03fe3fe6ee9ddaa063d16dd44d09119c844 -

Copy link
Contributor

@vasild vasild left a comment

Choose a reason for hiding this comment

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

ACK f65c9ad modulo s/assert.h/cassert/

@practicalswift
Copy link
Contributor

ACK f65c9ad

@practicalswift
Copy link
Contributor

@MarcoFalke

As pointed out earlier, this indeed is not an issue, because gcc will produce identical binaries for before this PR and after this PR. The overflow never happens.

I'm not sure I follow your reasoning here TBH.

"gcc will produce identical binaries for before this PR and after this PR" does not necessarily imply anything about the behaviour of other compilers we care about, no?

Test case compiled with clang as shown in #18046:

$ xxd -p -r <<< "fb67656c70000000000200ff0000ff0000000000000000ffffff7f0000000000" > crash-transaction
$ src/test/fuzz/transaction crash-transaction
primitives/transaction.cpp:87:19: runtime error: signed integer overflow: 1095216725760 + 9223372032559808512 cannot be represented in type 'long'
    #0 0x5574b725f6c1 in CTransaction::GetValueOut() const src/primitives/transaction.cpp:87:19
    #1 0x5574b611e5bb in test_one_input(std::vector<unsigned char, std::allocator<unsigned char> > const&) src/test/fuzz/transaction.cpp:71:18

Overflow never happens? :)

@practicalswift
Copy link
Contributor

@MarcoFalke Friendly ping: can you clarify? :)

@practicalswift
Copy link
Contributor

@instagibbs You reviewed a previous version of this PR -- would you mind re-reviewing the current version? :)

@elichai
Copy link
Contributor Author

elichai commented Apr 1, 2020

FWIW.
You can see that before: https://godbolt.org/z/wHZVFr
The logic was first add then compare:

        mov     rcx, QWORD PTR [rax]
        add     r8, rcx
        cmp     rcx, rsi
        ja      .L3
        cmp     r8, rsi
        ja      .L3
        add     rax, 8
        cmp     rdx, rax
        jne     .L6

But now: https://godbolt.org/z/oT7RJb it's first compare then add

        mov     rcx, QWORD PTR [rax]
        cmp     rcx, rsi
        ja      .L3
        add     r8, rcx
        cmp     r8, rsi
        jg      .L3
        add     rax, 8
        cmp     rdx, rax
        jne     .L6

@maflcko
Copy link
Member

maflcko commented Apr 1, 2020

I used clang, not gcc. Anyway, I didn't say it is wrong to fix this ;)

@instagibbs
Copy link
Member

utACK f65c9ad

maflcko pushed a commit to bitcoin-core/qa-assets that referenced this pull request Apr 2, 2020
@maflcko maflcko merged commit dce6f3b into bitcoin:master Apr 2, 2020
sidhujag pushed a commit to syscoin/syscoin that referenced this pull request Apr 3, 2020
…m of tx outputs

f65c9ad Check for overflow when calculating sum of outputs (Elichai Turkel)

Pull request description:

  This was reported by practicalswift here bitcoin#18046
  The exact order of the if, is important, we first do `!MoneyRange(tx_out.nValue)` to make sure the amount is non-negative. and then `std::numeric_limits<CAmount>::max() - tx_out.nValue < nValueOut` checks that the addition cannot overflow (if we won't check that the amount is positive this check can also overflow! (by doing something like `max - -max`))
  and only then we make sure that the some is also valid `!MoneyRange(nValueOut + tx_out.nValue)`
  if any of these conditions fail we throw.

  the overflowing logic:
  ```
  a + b > max // we want to fail if a+b is more than the maximum -> will overflow
  b > max - a
  max - a < b
  ```

  Closes: bitcoin#18046

ACKs for top commit:
  MarcoFalke:
    ACK f65c9ad, checked that clang with O2 produces identical binaries 💕
  practicalswift:
    ACK f65c9ad
  instagibbs:
    utACK bitcoin@f65c9ad
  vasild:
    ACK f65c9ad modulo `s/assert.h/cassert/`

Tree-SHA512: 512d6cf4762f24c41cf9a38da486b17b19c634fa3f4efbdebfe6608779e96fc3014d5d2d29adb8001e113152c0217bbd5b3900ac4edc7b8abe77f82f36209e33
@elichai elichai deleted the 2020-03-value-overflow branch April 4, 2020 11:33
Fabcien pushed a commit to Fabcien/qa-assets that referenced this pull request Oct 27, 2020
Fabcien pushed a commit to Bitcoin-ABC/bitcoin-abc that referenced this pull request Mar 31, 2021
Summary:
Address a possible undefined behavior when summing outputs for a transaction, when the first outputs are valid but a following output causes an overflow.

> The exact order of the if, is important, we first do `!MoneyRange(tx_out.nValue)` to make sure the amount is non-negative. and then `std::numeric_limits<CAmount>::max() - tx_out.nValue < nValueOut` checks that the addition cannot overflow (if we won't check that the amount is positive this check can also overflow! (by doing something like `max - -max`))
> and only then we make sure that the sum is also valid `!MoneyRange(nValueOut + tx_out.nValue`)
> if any of these conditions fail we throw.

This is a backport of Core [[bitcoin/bitcoin#18383 | PR18383]]

Test Plan:
```
mkdir build_ubsan
cd build_ubsan

cmake -GNinja .. \
  -DCMAKE_BUILD_TYPE=Debug \
  -DENABLE_SANITIZERS=undefined

ninja check
```

Reviewers: #bitcoin_abc, deadalnix

Reviewed By: #bitcoin_abc, deadalnix

Subscribers: deadalnix

Differential Revision: https://reviews.bitcoinabc.org/D8899
PastaPastaPasta pushed a commit to PastaPastaPasta/dash that referenced this pull request Jul 1, 2021
…m of tx outputs

f65c9ad Check for overflow when calculating sum of outputs (Elichai Turkel)

Pull request description:

  This was reported by practicalswift here bitcoin#18046
  The exact order of the if, is important, we first do `!MoneyRange(tx_out.nValue)` to make sure the amount is non-negative. and then `std::numeric_limits<CAmount>::max() - tx_out.nValue < nValueOut` checks that the addition cannot overflow (if we won't check that the amount is positive this check can also overflow! (by doing something like `max - -max`))
  and only then we make sure that the some is also valid `!MoneyRange(nValueOut + tx_out.nValue)`
  if any of these conditions fail we throw.

  the overflowing logic:
  ```
  a + b > max // we want to fail if a+b is more than the maximum -> will overflow
  b > max - a
  max - a < b
  ```

  Closes: bitcoin#18046

ACKs for top commit:
  MarcoFalke:
    ACK f65c9ad, checked that clang with O2 produces identical binaries 💕
  practicalswift:
    ACK f65c9ad
  instagibbs:
    utACK bitcoin@f65c9ad
  vasild:
    ACK f65c9ad modulo `s/assert.h/cassert/`

Tree-SHA512: 512d6cf4762f24c41cf9a38da486b17b19c634fa3f4efbdebfe6608779e96fc3014d5d2d29adb8001e113152c0217bbd5b3900ac4edc7b8abe77f82f36209e33
PastaPastaPasta pushed a commit to PastaPastaPasta/dash that referenced this pull request Jul 14, 2021
…m of tx outputs

f65c9ad Check for overflow when calculating sum of outputs (Elichai Turkel)

Pull request description:

  This was reported by practicalswift here bitcoin#18046
  The exact order of the if, is important, we first do `!MoneyRange(tx_out.nValue)` to make sure the amount is non-negative. and then `std::numeric_limits<CAmount>::max() - tx_out.nValue < nValueOut` checks that the addition cannot overflow (if we won't check that the amount is positive this check can also overflow! (by doing something like `max - -max`))
  and only then we make sure that the some is also valid `!MoneyRange(nValueOut + tx_out.nValue)`
  if any of these conditions fail we throw.

  the overflowing logic:
  ```
  a + b > max // we want to fail if a+b is more than the maximum -> will overflow
  b > max - a
  max - a < b
  ```

  Closes: bitcoin#18046

ACKs for top commit:
  MarcoFalke:
    ACK f65c9ad, checked that clang with O2 produces identical binaries 💕
  practicalswift:
    ACK f65c9ad
  instagibbs:
    utACK bitcoin@f65c9ad
  vasild:
    ACK f65c9ad modulo `s/assert.h/cassert/`

Tree-SHA512: 512d6cf4762f24c41cf9a38da486b17b19c634fa3f4efbdebfe6608779e96fc3014d5d2d29adb8001e113152c0217bbd5b3900ac4edc7b8abe77f82f36209e33
@bitcoin bitcoin locked as resolved and limited conversation to collaborators Feb 15, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Some fuzzing crashes when running under ASan and/or UBSan

8 participants