wallet, bdbro: Validate btree page levels

BTree pages contain the level in the BTree that the page is supposed to
be at. The root starts at some level between 1 and 255, leaves are
always level 1. Internal pages must be a level that is one less than its
parent. Validating that pages are at their expected level (except for
the root page) enforces that no cycles can occur.
This commit is contained in:
Ava Chow
2026-03-30 13:22:57 -07:00
parent dc3a2b9c3b
commit b2de59d486
2 changed files with 25 additions and 11 deletions

View File

@@ -633,10 +633,17 @@ void BerkeleyRODatabase::Open()
throw std::runtime_error("BDB builtin encryption is not supported");
}
// Read the root's level from its header
// Note that we will read the root page twice in order to process it.
SeekToPage(db_file, inner_meta.root, page_size);
PageHeader root_header(inner_meta.root, inner_meta.other_endian);
db_file >> root_header;
// Do a DFS through the BTree, starting at root
std::vector<uint32_t> pages{inner_meta.root};
// We track the expected level of each page in order to avoid loops
std::vector<std::pair<uint32_t, uint32_t>> pages{{inner_meta.root, root_header.level}};
while (pages.size() > 0) {
uint32_t curr_page = pages.back();
auto [curr_page, expected_level] = pages.back();
// It turns out BDB completely ignores this last_page field and doesn't actually update it to the correct
// last page. While we should be checking this, we can't.
// This is left commented out as a reminder to not accidentally implement this in the future.
@@ -647,17 +654,23 @@ void BerkeleyRODatabase::Open()
SeekToPage(db_file, curr_page, page_size);
PageHeader header(curr_page, inner_meta.other_endian);
db_file >> header;
if (header.level != expected_level) {
throw std::runtime_error("BTree page has an unexpected level");
}
switch (header.type) {
case PageType::BTREE_INTERNAL: {
InternalPage int_page(header);
db_file >> int_page;
for (const InternalRecord& rec : int_page.records) {
if (rec.m_header.deleted) continue;
pages.push_back(rec.page_num);
pages.emplace_back(rec.page_num, header.level - 1);
}
break;
}
case PageType::BTREE_LEAF: {
if (header.level != 1) {
throw std::runtime_error("BTree Leaf page is not at level 1");
}
RecordsPage rec_page(header);
db_file >> rec_page;
if (rec_page.records.size() % 2 != 0) {

View File

@@ -74,14 +74,15 @@ FUZZ_TARGET(wallet_bdb_parser, .init = initialize_wallet_bdb_parser)
error.original == "Internal record position not in page" ||
error.original == "LSNs are not reset, this database is not completely flushed. Please reopen then close the database with a version that has BDB support" ||
error.original == "Records page has odd number of records" ||
error.original == "Bad overflow record page type") {
// Do nothing
} else if (error.original == "Subdatabase last page is greater than database last page" ||
error.original == "Page number is greater than database last page" ||
error.original == "Last page number could not fit in file" ||
error.original == "Subdatabase has an unexpected name" ||
error.original == "Unsupported BDB data file version number" ||
error.original == "BDB builtin encryption is not supported") {
error.original == "Bad overflow record page type" ||
error.original == "BTree page has an unexpected level" ||
error.original == "BTree Leaf page is not at level 1" ||
error.original == "Subdatabase last page is greater than database last page" ||
error.original == "Page number is greater than database last page" ||
error.original == "Last page number could not fit in file" ||
error.original == "Subdatabase has an unexpected name" ||
error.original == "Unsupported BDB data file version number" ||
error.original == "BDB builtin encryption is not supported") {
} else {
throw std::runtime_error(error.original);
}