Skip to content

Conversation

Crypt-iQ
Copy link
Contributor

@Crypt-iQ Crypt-iQ commented Aug 8, 2021

There are two main changes: introduce LevelDbOptions to manage the leveldb::Options struct and changing CDBWrapper's penv member to a unique_ptr. The first allows LevelDbOptions to destroy leveldb::Options members without CDBWrapper having to directly. The second change allows the leveldb::Env to be managed by penv. Both of these changes prevent a LeakSanitizer exception if the CDBWrapper ctor throws.

Fixes #22592

@DrahtBot
Copy link
Contributor

DrahtBot commented Aug 8, 2021

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

Conflicts

Reviewers, this pull request conflicts with the following ones:

  • #25862 (refactor, kernel: Remove gArgs accesses from dbwrapper and txdb by ryanofsky)
  • #25740 (assumeutxo: background validation completion by jamesob)
  • #25667 (assumeutxo: snapshot initialization by jamesob)

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.

@Crypt-iQ
Copy link
Contributor Author

Crypt-iQ commented Aug 8, 2021

One build is failing due to a leak while fuzzing coins_view and I cannot see why with the CI. I am unable to reproduce this on my machine.

@DrahtBot DrahtBot mentioned this pull request Aug 8, 2021
18 tasks
leveldb::Status status = leveldb::DB::Open(options, path.string(), &pdb);
leveldb::Status status = leveldb::DB::Open(*options, path.string(), &pdb);
if (!status.ok()) {
delete penv;

Choose a reason for hiding this comment

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

If penv is deleted, then ~CDBWrapper() gonna delete it again? Missing = nullptr?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If !status.ok then HandleError throws and the destructor isn't called

Choose a reason for hiding this comment

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

Maybe worth making penv a unique_ptr too, if we need manual handling with delete? Or do you think this would be kinda out of scope for this PR?

Adding addition delete into new code does not seem... "right" (https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rr-newdelete).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

good point, I can look into making it a unique_ptr

Copy link
Contributor

Choose a reason for hiding this comment

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

re: #22663 (comment)

If penv is deleted, then ~CDBWrapper() gonna delete it again? Missing = nullptr?

In commit "dbwrapper: wrap options in unique_ptr and use DeleteOptions" (6770f06)

It does seem fragile to rely on HandleError bypassing the destructor to avoid a double-delete. Would second the suggestion to add = nullptr to make the destructor safe if it were called. Or could add a "// Deleting env here without resetting it is safe because the destructor will never be called" comment to clarify. Or could make env a unique_ptr, which also seemed like a good suggestion

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The latest change makes penv a unique_ptr

Copy link
Contributor

@ryanofsky ryanofsky left a comment

Choose a reason for hiding this comment

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

6770f06 looks pretty good if the leak is fixed. It generally seems to moves things in a good direction.

I do think more ideally, if the goal is to make the options struct more RAII-like and exception-safe, instead of requiring it to be allocated separately on the heap and requiring it to be paired with a custom deleter, it would be better to just inherit from it and write a normal destructor:

struct LevelDbOptions : public leveldb::Options {
  ~LevelDbOptions() {
    delete filter_policy;
    delete info_log;
    ...
  }
}

Comment on lines 100 to 111
static void GetOptions(size_t nCacheSize, leveldb::Options* options)
{
leveldb::Options options;
options.block_cache = leveldb::NewLRUCache(nCacheSize / 2);
options.write_buffer_size = nCacheSize / 4; // up to two write buffers may be held in memory simultaneously
options.filter_policy = leveldb::NewBloomFilterPolicy(10);
options.compression = leveldb::kNoCompression;
options.info_log = new CBitcoinLevelDBLogger();
options->block_cache = leveldb::NewLRUCache(nCacheSize / 2);
options->write_buffer_size = nCacheSize / 4; // up to two write buffers may be held in memory simultaneously
options->filter_policy = leveldb::NewBloomFilterPolicy(10);
options->compression = leveldb::kNoCompression;
options->info_log = new CBitcoinLevelDBLogger();
if (leveldb::kMajorVersion > 1 || (leveldb::kMajorVersion == 1 && leveldb::kMinorVersion >= 16)) {
// LevelDB versions before 1.16 consider short writes to be corruption. Only trigger error
// on corruption in later versions.
options.paranoid_checks = true;
options->paranoid_checks = true;
}
SetMaxOpenFiles(&options);
return options;
SetMaxOpenFiles(options);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

In commit "dbwrapper: wrap options in unique_ptr and use DeleteOptions" (6770f06)

It's not too important, but I don't think there's an actual need to change this function, and the previous version of this that just returned a struct instead of dereferencing a pointer seemed more straightforward.

To keep the previous version, I believe you would just need to write new leveldb::Options{GetOptions(...)} below.

Would be nice to add [[nodiscard]] to the previous version, if you did keep it though.

Copy link
Member

@laanwj laanwj Nov 10, 2021

Choose a reason for hiding this comment

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

I agree with @ryanofsky. I don't think there's a strong need to change this function, but if there is, I would prefer it to return the target type std::unique_ptr<leveldb::Options, void (*)(leveldb::Options*) directly, instead of using a pointer positional out argument (which, strictly, seems a deterioration of API clarity to me).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Will work on a patch with this, thanks for the review

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Latest change lifts the GetOptions logic into the ctor of LevelDbOptions

options->info_log = nullptr;
delete options->block_cache;
options->block_cache = nullptr;
options->env = nullptr;
Copy link
Contributor

Choose a reason for hiding this comment

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

In commit "dbwrapper: wrap options in unique_ptr and use DeleteOptions" (6770f06)

It seems dodgy to set env to null without freeing it here. I think it would be better to either:

  • Set this to null immediately in the places where the env is deleted, in which case you could assert(!options->env) here
  • Or, to just drop this line and treat env and options lifetimes as more independent
  • Or, to add a comment like "env was previously deleted and should not be freed here" here to make it clearer the inconsistency here is intentional

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I opted for the second approach and dropped the line

leveldb::Status status = leveldb::DB::Open(options, path.string(), &pdb);
leveldb::Status status = leveldb::DB::Open(*options, path.string(), &pdb);
if (!status.ok()) {
delete penv;
Copy link
Contributor

Choose a reason for hiding this comment

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

re: #22663 (comment)

If penv is deleted, then ~CDBWrapper() gonna delete it again? Missing = nullptr?

In commit "dbwrapper: wrap options in unique_ptr and use DeleteOptions" (6770f06)

It does seem fragile to rely on HandleError bypassing the destructor to avoid a double-delete. Would second the suggestion to add = nullptr to make the destructor safe if it were called. Or could add a "// Deleting env here without resetting it is safe because the destructor will never be called" comment to clarify. Or could make env a unique_ptr, which also seemed like a good suggestion

@@ -170,14 +183,7 @@ CDBWrapper::~CDBWrapper()
{
delete pdb;
Copy link
Contributor

Choose a reason for hiding this comment

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

In commit "dbwrapper: wrap options in unique_ptr and use DeleteOptions" (6770f06)

I liked the suggestion to make env a unique_ptr #22663 (comment), and maybe pdb could be a unique_ptr too?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I was not sure how to make pdb a unique_ptr and also pass it in to Open as leveldb::DB** when it is nullptr at this point. Maybe I am missing something.

Copy link
Contributor

Choose a reason for hiding this comment

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

re: #22663 (comment)

I was not sure how to make pdb a unique_ptr and also pass it in to Open as leveldb::DB** when it is nullptr at this point. Maybe I am missing something.

No need to do this, but you would need to pass a temporary pointer to Open, and switch to unique_ptr after it returns:

leveldb::DB* new_db = nullptr;
leveldb::Status status = leveldb::DB::Open(options, fs::PathToString(path), &new_db);
pdb.reset(new_db);

}

// Custom deleter for unique_ptr<leveldb::Options>.
static void DeleteOptions(leveldb::Options* options)
Copy link
Contributor

Choose a reason for hiding this comment

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

In commit "dbwrapper: wrap options in unique_ptr and use DeleteOptions" (6770f06)

Custom deleter seems to be forgetting to delete options, which would explain the fuzzing leak? #22663 (comment)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No longer using a custom deleter and instead using ~LevelDbOptions

@laanwj
Copy link
Member

laanwj commented Dec 15, 2021

@Crypt-iQ can you please respond to @ryanofsky's comments and rebase ?

@Crypt-iQ
Copy link
Contributor Author

Hi yes, I can get to it this weekend

@Crypt-iQ Crypt-iQ force-pushed the unique_ptr_options_08072021 branch from 6770f06 to 7ba6aa2 Compare December 19, 2021 22:11
@Crypt-iQ Crypt-iQ changed the title dbwrapper: wrap options in unique_ptr and use DeleteOptions dbwrapper: properly destroy objects in case CDBWrapper ctor throws Dec 20, 2021
@Crypt-iQ
Copy link
Contributor Author

I've rebased and hopefully addressed all of the comments

Copy link
Contributor

@ryanofsky ryanofsky 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 ACK 2a5d230. This all looks good and is straightforward. Thanks for implementing all the suggested changes

@@ -170,14 +183,7 @@ CDBWrapper::~CDBWrapper()
{
delete pdb;
Copy link
Contributor

Choose a reason for hiding this comment

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

re: #22663 (comment)

I was not sure how to make pdb a unique_ptr and also pass it in to Open as leveldb::DB** when it is nullptr at this point. Maybe I am missing something.

No need to do this, but you would need to pass a temporary pointer to Open, and switch to unique_ptr after it returns:

leveldb::DB* new_db = nullptr;
leveldb::Status status = leveldb::DB::Open(options, fs::PathToString(path), &new_db);
pdb.reset(new_db);

Crypt-iQ added 2 commits May 5, 2022 18:36
This change allows LevelDbOptions to destroy leveldb::Options
members instead of doing so in CDBWrapper's destructor. This change
prevents a LeakSanitizer exception if the CDBWrapper constructor
throws.
This changes the naming of penv to m_env and also makes it a
unique_ptr. Like the previous commit, this prevents a LeakSanitizer
exception if the constructor throws since the unique_ptr will clean
itself up.
@achow101
Copy link
Member

cc @jamesob

@DrahtBot
Copy link
Contributor

🐙 This pull request conflicts with the target branch and needs rebase.

Want to unsubscribe from rebase notifications on this pull request? Just convert this pull request to a "draft".

@Crypt-iQ
Copy link
Contributor Author

unfortunately don't have time to work on this, lmk if i should close it

@fanquake
Copy link
Member

unfortunately don't have time to work on this, lmk if i should close it

Thanks. Closed and added up-for-grabs.

@fanquake fanquake closed this Oct 19, 2022
@bitcoin bitcoin locked and limited conversation to collaborators Oct 19, 2023
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.

LeakSanitizer detects memory leak if CDBWrapper ctor fails
8 participants