-
Notifications
You must be signed in to change notification settings - Fork 38.7k
refactor: Check for overflow when calculating sum of tx outputs #18383
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
What is the concrete effect of this? Does this affect validation? Can you please add a test that fails without this change? |
Good question. technically this code is part of validation, it's called from 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. |
|
This is not possible to trigger from a network node or RPC, because |
|
This was fixed in |
|
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:
|
|
How could it overflow, given that |
|
@vasild |
|
Concept ACK: However, I think your fix can be simplified :) Note that In other words, simply do Note that the Perhaps something along the lines of: |
|
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 :) |
What unused result are you referring to? :) |
|
@practicalswift nValueOut is computed but never inspected in case tx_out.nValue is out of range. |
|
Ah, in @elichai's suggested fix. I thought @MarcoFalke meant in current 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). |
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. |
|
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;
} |
|
I prefer @practicalswift solution above |
I honestly don't know if it's a problem overflowing without reading the value, probably isn't as you said.
It is a somewhat nicer fix :) |
|
I also prefer #18383 (comment). |
|
@practicalswift do you wan to open a new PR with your suggestion? or should I replace my diff here with yours? |
|
@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 :) |
9a04da1 to
a4c79f8
Compare
|
ACK a4c79f8cf6e0407f7e937fb41c73127f630231fa |
|
utACK a4c79f8 |
a4c79f8 to
f65c9ad
Compare
| #include <tinyformat.h> | ||
| #include <util/strencodings.h> | ||
|
|
||
| #include <assert.h> |
There was a problem hiding this comment.
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 :)
|
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 timestampSignature: Timestamp of file with hash |
vasild
left a comment
There was a problem hiding this 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/
|
ACK f65c9ad |
I'm not sure I follow your reasoning here TBH. " Test case compiled with Overflow never happens? :) |
|
@MarcoFalke Friendly ping: can you clarify? :) |
|
@instagibbs You reviewed a previous version of this PR -- would you mind re-reviewing the current version? :) |
|
FWIW. But now: https://godbolt.org/z/oT7RJb it's first compare then add |
|
I used clang, not gcc. Anyway, I didn't say it is wrong to fix this ;) |
|
utACK f65c9ad |
…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
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
…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
…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
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 thenstd::numeric_limits<CAmount>::max() - tx_out.nValue < nValueOutchecks that the addition cannot overflow (if we won't check that the amount is positive this check can also overflow! (by doing something likemax - -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:
Closes: #18046