Skip to content

Conversation

kallewoof
Copy link
Contributor

@kallewoof kallewoof commented Mar 6, 2020

Initialize the nFeeRequired variable to avoid using an uninitialized value for errors happening before it is set to 0.

Note: this originally fixed nFeeRet in wallet.cpp.

@practicalswift
Copy link
Contributor

practicalswift commented Mar 6, 2020

Concept ACK

Good catch!

FWIW I still think it would be a good idea to consider moving towards a "initialize-to-zero-or-sane-value-by-default" policy. Realistically and empirically it seems like that is the only way we'll permanently avoid the steady stream of uninitialised reads we're seeing in the project.

From the C++ Core Guidlines (edited by Herb Sutter & Bjarne Stroustrup):

ES.20: Always initialize an object

Reason: Avoid used-before-set errors and their associated undefined behavior. Avoid problems with comprehension of complex initialization. Simplify refactoring.

The "always initialize rule" is deliberately stronger than the an "object must be set before used" language rule. The latter, more relaxed rule, catches the technical bugs, but:

  • It leads to less readable code
  • It encourages people to declare names in greater than necessary scopes
  • It leads to harder to read code
  • It leads to logic bugs by encouraging complex code
  • It hampers refactoring

J F Bastien of Clang/LLVM fame from the "Automatic variable initialization" LLVM patch thread:

Is it a good idea? Security-minded folks think so, and apparently so does the Microsoft Visual Studio team [1] who say "Between 2017 and mid 2018, this feature would have killed 49 MSRC cases that involved uninitialized struct data leaking across a trust boundary. It would have also mitigated a number of bugs involving uninitialized struct data being used directly.". They seem to use pure zero initialization, and claim to have taken the overheads down to within noise. Don't just trust Microsoft though, here's another relevant person asking for this [2]. It's been proposed for GCC [3] and LLVM [4] before.

Kees (“Case”) Cook of the Kernel Self-Protection Project (KSPP):

Always-initialized local variables: just do it

Kostya Serebryany - the father of AddressSanitizer, libFuzzer, OSS-Fuzz and Clang Control Flow Integrity - on the topic of auto-initialization in Clang/LLVM:

Very exciting, and long overdue. Thanks for doing this! Countless security bugs would have been mitigated by this, see below. […] Here are some links to bugs, vulnerabilities and full exploits based on uses of uninitialized memory. The list is not exhaustive by any means, and we keep finding them every day. The problem is, of course, that we don't find all of them.

I know about the "but if you initialize then Valgrind won't find the issue" argument. I love Valgrind and MSan - I use them daily, but we're facing a trade-off here: I think the security benefits of avoiding uninitialized reads by initializing outweighs the discoverability benefits of not initializing.

@sanjaykdragon
Copy link
Contributor

FWIW I still think it would be a good idea to consider moving towards a "initialize-to-zero-or-sane-value-by-default" policy. Realistically and empirically it seems like that is the only way we'll permanently avoid the steady stream of uninitialized reads we're seeing in the project.

I agree with this, would it be worth the time to start refactoring code to initialize to zero/sane?

@Sjors
Copy link
Member

Sjors commented Mar 6, 2020

Concept ACK. Don't forget to remove the original from line 2664. Can you also initialize nFeeRequired to 0?

Maybe instead of initializing inside CreateTransaction() you could assert that it's zero?

I agree with this, would it be worth the time to start refactoring code to initialize to zero/sane?

Pure refactor PRs usually don't work well. There is already a code guideline to do this in new places, and in code that's being touched by a PR: https://github.com/bitcoin/bitcoin/blob/master/doc/developer-notes.md#c-data-structures

@practicalswift
Copy link
Contributor

practicalswift commented Mar 6, 2020

I agree with this, would it be worth the time to start refactoring code to initialize to zero/sane?

Pure refactor PRs usually don't work well.

Can a type of change that is known to have prevented literally countless security bugs in other projects (had the type of change been in place) really be called a "pure refactoring"? :)

Perhaps mitigation is a better term.

@maflcko
Copy link
Member

maflcko commented Mar 6, 2020

NACK. I have a hard time following the discussion here and why people seem to agree that this is a good change. My rationale for the NACK:

  • Reading a return value in the case of failure, where it was not properly written to, is always illegal. Setting it to 0 or any other value does not make it legal to read it. IIUC, In this case it will print Error: This transaction requires a transaction fee of at least 0, which is obviously the wrong error message.

  • Blind Zero-or-otherwise initialization completely defeats the purpose of valgrind and other address sanitizers to find these bugs. When we remove the chance to fix these bugs, we might as well remove valgrind et al again.

So NACK on hiding the bug and not actually fixing it. If it is too hard to fix the bug without major code changes, we are lucky that C++ offers us many ways to safely return values from functions that may fail:

  • Optional, which is not initialized on error. Only initialized when successfully written to.
  • Return an enum to distinguish different error cases or maybe even pass out the error string, so that the caller can simply use that error string and does not have to guess.

@maflcko
Copy link
Member

maflcko commented Mar 6, 2020

I am pretty sure we have had this exact discussion about uninitialized reads the 7th time. I wonder if it helps to write up a policy about that.

@maflcko
Copy link
Member

maflcko commented Mar 6, 2020

FWIW I still think it would be a good idea to consider moving towards a "initialize-to-zero-or-sane-value-by-default" policy. Realistically and empirically it seems like that is the only way we'll permanently avoid the steady stream of uninitialized reads we're seeing in the project.

I agree with this, would it be worth the time to start refactoring code to initialize to zero/sane?

If we do this for all builds, we might as well remove valgrind et al (as I mentioned above). I think it could make sense to enable it for release builds, but I see no way where it could make sense to enable this for developer builds.

@maflcko
Copy link
Member

maflcko commented Mar 6, 2020

If we decide to do initialization everywhere, it should be done with the ftrivial-auto-var-init compiler flag (or similar), and not in the code.

@ryanofsky
Copy link
Contributor

ryanofsky commented Mar 6, 2020

Tend to agree with Marco and think that a policy of initializing variables to 0 or -1 in cases where these values would still be bugs could be counterproductive and reduce our ability to find these bugs with fuzzing and static analysis.

A different rule to either declare every variable with a valid initial value or to declare it as std::optional could be preferable because it could remove nondeterminism while still allowing tools to detect unset values. It could also improve code review because the need to dereference would alert reviewers to the possibility that a variable might be unset.

@laanwj
Copy link
Member

laanwj commented Mar 6, 2020

ACK b585873
This is clearly correct and fixes an actual (though minor) issue.

@kallewoof kallewoof force-pushed the 2003-reset-nfeeret branch from b585873 to e918006 Compare March 7, 2020 01:59
@kallewoof
Copy link
Contributor Author

Pushed removal of duplicate nFeeRet = 0; statement.

@kallewoof
Copy link
Contributor Author

kallewoof commented Mar 7, 2020

@MarcoFalke I see what you're saying, but I'm not sure how trivial/hard it would be to actually track down the cases where the current "error message override" behavior is triggered. Also, I don't think

Reading a return value in the case of failure, where it was not properly written to, is always illegal. Setting it to 0 or any other value does not make it legal to read it. IIUC, In this case it will print Error: This transaction requires a transaction fee of at least 0, which is obviously the wrong error message.

is correct, because nValue + nFeeRequired > curBalance will not be true (for nFeeRequired = 0 case, we already test this on L#339).

@DrahtBot
Copy link
Contributor

DrahtBot commented Mar 7, 2020

The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

Conflicts

Reviewers, this pull request conflicts with the following ones:

If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first.

@practicalswift
Copy link
Contributor

practicalswift commented Mar 7, 2020

@kallewoof

Are you sure there is no reasonable way to fix this at the call site instead? I think that would be much preferred. I'm a bit hesitant to ACK the code as currently written.

Generally, given bool f(T& o) I really think we should avoid assuming that f has written to o in the case that f(o) has returned false.


Context for reviewers - this is the call site:

    // Create and send the transaction                                                                                                                                                                      
    CAmount nFeeRequired;
    std::string strError;
    std::vector<CRecipient> vecSend;
    int nChangePosRet = -1;
    CRecipient recipient = {scriptPubKey, nValue, fSubtractFeeFromAmount};
    vecSend.push_back(recipient);
    CTransactionRef tx;
    if (!pwallet->CreateTransaction(locked_chain, vecSend, tx, nFeeRequired, nChangePosRet, strError, coin_control)) {
        if (!fSubtractFeeFromAmount && nValue + nFeeRequired > curBalance)
            strError = strprintf("Error: This transaction requires a transaction fee of at least %s", FormatMoney(nFeeRequired));
        throw JSONRPCError(RPC_WALLET_ERROR, strError);
    }

AFAICT that is the only place in our code base where an uninitialized nFeeRequired parameter is passed to [cC]reateTransaction and subsequently read.

$ git grep -E '[Cc]reateTransaction\(' -- "*.h" "*.cpp"
src/interfaces/wallet.cpp:    CTransactionRef createTransaction(const std::vector<CRecipient>& recipients,
src/interfaces/wallet.cpp:        if (!m_wallet->CreateTransaction(*locked_chain, recipients, tx, fee, change_pos,
src/interfaces/wallet.h:    virtual CTransactionRef createTransaction(const std::vector<CRecipient>& recipients,
src/qt/walletmodel.cpp:        newTx = m_wallet->createTransaction(vecSend, coinControl, !privateKeysDisabled() /* sign */, nChangePosRet, nFeeRequired, strFailReason);
src/wallet/feebumper.cpp:    if (!wallet.CreateTransaction(*locked_chain, recipients, tx_new, fee_ret, change_pos_in_out, fail_reason, new_coin_control, false)) {
src/wallet/rpcwallet.cpp:    if (!pwallet->CreateTransaction(locked_chain, vecSend, tx, nFeeRequired, nChangePosRet, strError, coin_control)) {
src/wallet/rpcwallet.cpp:    bool fCreated = pwallet->CreateTransaction(*locked_chain, vecSend, tx, nFeeRequired, nChangePosRet, strFailReason, coin_control);
src/wallet/test/wallet_tests.cpp:            BOOST_CHECK(wallet->CreateTransaction(*locked_chain, {recipient}, tx, fee, changePos, error, dummy));
src/wallet/wallet.cpp:    if (!CreateTransaction(*locked_chain, vecSend, tx_new, nFeeRet, nChangePosInOut, strFailReason, coinControl, false)) {
src/wallet/wallet.cpp:bool CWallet::CreateTransaction(interfaces::Chain::Lock& locked_chain, const std::vector<CRecipient>& vecSend, CTransactionRef& tx, CAmount& nFeeRet,
src/wallet/wallet.h:     * calling CreateTransaction();
src/wallet/wallet.h:    bool CreateTransaction(interfaces::Chain::Lock& locked_chain, const std::vector<CRecipient>& vecSend, CTransactionRef& tx, CAmount& nFeeRet, int& nChangePosInOut,

@kallewoof
Copy link
Contributor Author

@practicalswift Yeah, either way works, to be honest, but since we are currently using the result in one place, there's no guarantee we won't use it in another in the future. That's my only reasoning, really.

@promag
Copy link
Contributor

promag commented Mar 9, 2020

+1 on fixing at the call site. A refactor could come next IMO.

@kallewoof kallewoof force-pushed the 2003-reset-nfeeret branch from e918006 to a652ba6 Compare March 9, 2020 01:19
@kallewoof kallewoof changed the title wallet: set nFeeRet to 0 to avoid garbage value upon error rpc/wallet: initialize nFeeRequired to avoid using garbage value on failure Mar 9, 2020
@kallewoof
Copy link
Contributor Author

I have changed the code & description of the PR to fix in rpcwallet.cpp instead.

Copy link
Contributor

@promag promag left a comment

Choose a reason for hiding this comment

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

ACK a652ba6.

Copy link
Contributor

@meshcollider meshcollider left a comment

Choose a reason for hiding this comment

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

utACK a652ba6

@practicalswift
Copy link
Contributor

ACK a652ba6 -- patch looks correct

@Sjors
Copy link
Member

Sjors commented Mar 9, 2020

utACK a652ba6

Generally, given bool f(T& o) I really think we should avoid assuming that f has written to o in the case that f(o) has returned false.

That sounds reasonable, and this fix is in line with that.

@fanquake fanquake merged commit 6ddf435 into bitcoin:master Mar 9, 2020
@kallewoof kallewoof deleted the 2003-reset-nfeeret branch March 12, 2020 12:44
MarkLTZ added a commit to litecoinz-core/litecoinz that referenced this pull request Apr 9, 2020
- rpc/wallet: initialize nFeeRequired to avoid using garbage value on failure bitcoin#18274
- httpserver: use own HTTP status codes bitcoin#18168
- tests: Add fuzzing harnesses for various Base{32,58,64} and hex related functions bitcoin#17229
- util: Don't allow Base32/64-decoding or ParseMoney(…) on strings with embedded NUL characters. Add tests. bitcoin#17753
- util: Fail to parse empty string in ParseMoney bitcoin#18225
- util: Fail to parse whitespace-only strings in ParseMoney(...) (instead of parsing as zero) bitcoin#18270
- Replace the LogPrint function with a macro bitcoin#17218
- Fix wallet unload race condition bitcoin#18338
- qt: Fix Window -> Minimize menu item bitcoin#18549
- windows: Enable heap terminate-on-corruption bitcoin#17916
Fabcien pushed a commit to Bitcoin-ABC/bitcoin-abc that referenced this pull request Jan 6, 2021
Summary:
I do not think that we have the same issue as Core, but it is
still nice to see an explicit initialization to 0 in the code.

This is a backport of Core [[bitcoin/bitcoin#18274 | PR18274]]

Test Plan: `ninja all check-all`

Reviewers: #bitcoin_abc, majcosta

Reviewed By: #bitcoin_abc, majcosta

Differential Revision: https://reviews.bitcoinabc.org/D8805
@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.
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.