-
Notifications
You must be signed in to change notification settings - Fork 37.7k
dbwrapper: properly destroy objects in case CDBWrapper ctor throws #22663
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
The following sections might be updated with supplementary metadata relevant to reviewers and maintainers. ConflictsReviewers, 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. |
One build is failing due to a leak while fuzzing |
src/dbwrapper.cpp
Outdated
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; |
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.
If penv
is deleted, then ~CDBWrapper()
gonna delete it again? Missing = nullptr
?
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.
If !status.ok
then HandleError
throws and the destructor isn't called
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.
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).
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.
good point, I can look into making it a unique_ptr
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.
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
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.
The latest change makes penv
a unique_ptr
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.
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;
...
}
}
src/dbwrapper.cpp
Outdated
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); | ||
} |
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.
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.
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.
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).
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.
Will work on a patch with this, thanks for the review
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.
Latest change lifts the GetOptions
logic into the ctor of LevelDbOptions
src/dbwrapper.cpp
Outdated
options->info_log = nullptr; | ||
delete options->block_cache; | ||
options->block_cache = nullptr; | ||
options->env = nullptr; |
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.
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
andoptions
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
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.
I opted for the second approach and dropped the line
src/dbwrapper.cpp
Outdated
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; |
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.
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; |
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.
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?
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.
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.
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.
re: #22663 (comment)
I was not sure how to make
pdb
a unique_ptr and also pass it in toOpen
asleveldb::DB**
when it isnullptr
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);
src/dbwrapper.cpp
Outdated
} | ||
|
||
// Custom deleter for unique_ptr<leveldb::Options>. | ||
static void DeleteOptions(leveldb::Options* options) |
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.
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)
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.
No longer using a custom deleter and instead using ~LevelDbOptions
@Crypt-iQ can you please respond to @ryanofsky's comments and rebase ? |
Hi yes, I can get to it this weekend |
6770f06
to
7ba6aa2
Compare
I've rebased and hopefully addressed all of the comments |
7ba6aa2
to
541b66f
Compare
541b66f
to
2a5d230
Compare
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.
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; |
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.
re: #22663 (comment)
I was not sure how to make
pdb
a unique_ptr and also pass it in toOpen
asleveldb::DB**
when it isnullptr
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);
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.
2a5d230
to
b6a1677
Compare
cc @jamesob |
🐙 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". |
unfortunately don't have time to work on this, lmk if i should close it |
Thanks. Closed and added up-for-grabs. |
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