From b2de59d486dce621c2c4ae4f2ccbe2658dc4d5a6 Mon Sep 17 00:00:00 2001 From: Ava Chow Date: Mon, 30 Mar 2026 13:22:57 -0700 Subject: [PATCH] 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. --- src/wallet/migrate.cpp | 19 ++++++++++++++++--- src/wallet/test/fuzz/wallet_bdb_parser.cpp | 17 +++++++++-------- 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/src/wallet/migrate.cpp b/src/wallet/migrate.cpp index 8cfb74f05c1..0ca4a70c3f7 100644 --- a/src/wallet/migrate.cpp +++ b/src/wallet/migrate.cpp @@ -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 pages{inner_meta.root}; + // We track the expected level of each page in order to avoid loops + std::vector> 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) { diff --git a/src/wallet/test/fuzz/wallet_bdb_parser.cpp b/src/wallet/test/fuzz/wallet_bdb_parser.cpp index f15548d4d6c..37c9869bc62 100644 --- a/src/wallet/test/fuzz/wallet_bdb_parser.cpp +++ b/src/wallet/test/fuzz/wallet_bdb_parser.cpp @@ -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); }