wallet: check the final BDB page LSN during migration #35227

pull l0rinc wants to merge 1 commits into bitcoin:master from l0rinc:l0rinc/check-bdb-last-page-lsn changing 1 files +1 −1
  1. l0rinc commented at 9:16 PM on May 6, 2026: contributor

    Problem

    Legacy wallet migration uses the read-only BDB parser to verify that every page LSN is reset before reading records without BDB log files.

    The BDB last_page metadata field stores the last valid page number, but the parser treated it like a page count and scanned only 0..<last_page: https://github.com/bitcoin/bitcoin/blob/e2b0984f99519f76423ce26ce9077ca765b2b30b/src/wallet/migrate.cpp#L87 This skipped the final page, so a database whose last page still depended on BDB logs could be accepted.

    Fix

    Scan LSNs through last_page inclusively.

  2. wallet: check BDB last page LSN
    The BDB metadata field `last_page` stores the last valid page number, not the number of pages.
    The read-only wallet migration parser currently checks reset LSNs with a half-open loop, so it skips the final page and may accept a database whose last page still depends on BDB log files.
    e2b0984f99
  3. DrahtBot added the label Wallet on May 6, 2026
  4. DrahtBot commented at 9:16 PM on May 6, 2026: contributor

    <!--e57a25ab6845829454e8d69fc972939a-->

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

    <!--006a51241073e994b41acfe9ec718e94-->

    Code Coverage & Benchmarks

    For details see: https://corecheck.dev/bitcoin/bitcoin/pulls/35227.

    <!--021abf342d371248e50ceaed478a90ca-->

    Reviews

    See the guideline for information on the review process.

    Type Reviewers
    ACK w0xlt, achow101, sedited

    If your review is incorrectly listed, please copy-paste <code>&lt;!--meta-tag:bot-skip--&gt;</code> into the comment that the bot should ignore.

    <!--5faf32d7da4f0f540f40219e4f7537a3-->

  5. l0rinc commented at 9:17 PM on May 6, 2026: contributor

    I've had a few iterations on how to test this but ended up only pushing the fix - if anyone can come up with a good way to exercise this, let me know.

    <details><summary>Cumbersome test</summary>

    BOOST_AUTO_TEST_CASE(berkeley_ro_checks_last_page_lsn)
    {
        constexpr size_t page_size{512};
        std::string bdb(3 * page_size, '\0');
        auto ptr = [&](size_t pos) { return reinterpret_cast<unsigned char*>(bdb.data() + pos); };
        auto write_meta = [&](size_t page_offset, uint32_t page_num, uint32_t root_page) {
            WriteLE32(ptr(page_offset + 0), page_num == 2 ? 1 : 0); // Corrupt only the final page LSN.
            WriteLE32(ptr(page_offset + 4), 1);
            WriteLE32(ptr(page_offset + 8), page_num);
            WriteLE32(ptr(page_offset + 12), 0x00053162);
            WriteLE32(ptr(page_offset + 16), 9);
            WriteLE32(ptr(page_offset + 20), page_size);
            bdb.at(page_offset + 25) = 9;
            WriteLE32(ptr(page_offset + 32), 2);
            WriteLE32(ptr(page_offset + 48), 0x20);
            WriteLE32(ptr(page_offset + 88), root_page);
        };
    
        write_meta(0, /*page_num=*/0, /*root_page=*/1);
        write_meta(2 * page_size, /*page_num=*/2, /*root_page=*/1);
    
        WriteLE32(ptr(page_size + 0), 0);
        WriteLE32(ptr(page_size + 4), 1);
        WriteLE32(ptr(page_size + 8), 1);
        WriteLE16(ptr(page_size + 20), 2);
        bdb.at(page_size + 24) = 1;
        bdb.at(page_size + 25) = 5;
        WriteLE16(ptr(page_size + 26), 128);
        WriteLE16(ptr(page_size + 28), 135);
        WriteLE16(ptr(page_size + 128), 4);
        bdb.at(page_size + 130) = 1;
        bdb.at(page_size + 131) = 'm';
        bdb.at(page_size + 132) = 'a';
        bdb.at(page_size + 133) = 'i';
        bdb.at(page_size + 134) = 'n';
        WriteLE16(ptr(page_size + 135), 4);
        bdb.at(page_size + 137) = 1;
        bdb.at(page_size + 141) = 2;
    
        const fs::path path{m_path_root / "bad_last_page_lsn.dat"};
        BOOST_REQUIRE(WriteBinaryFile(path, bdb));
    
        DatabaseOptions options;
        DatabaseStatus status;
        bilingual_str error;
        auto database{MakeBerkeleyRODatabase(path, options, status, error)};
        BOOST_CHECK(!database);
        BOOST_CHECK_EQUAL(status, DatabaseStatus::FAILED_LOAD);
        BOOST_CHECK_EQUAL(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");
    }
    

    </details>

  6. w0xlt commented at 7:42 AM on May 7, 2026: contributor

    ACK e2b0984f99519f76423ce26ce9077ca765b2b30b

    Some research:

    DBMETA::last_pgno is documented in the page layout as the page number of the last page, not the number of pages: https://github.com/observerdev/db-4.8.30/blob/3c75267f11336acb1be06bed419e8056cc689d09/dbinc/db_page.h#L70-L90

    Mpool computes it as a zero-based page number: https://github.com/observerdev/db-4.8.30/blob/3c75267f11336acb1be06bed419e8056cc689d09/mp/mp_fopen.c#L411-L425

    BDB treats pages as valid while pgno <= last_pgno (implied by ret = *pgnoaddr > mfp->last_pgno ? DB_PAGE_NOTFOUND : 0;): https://github.com/observerdev/db-4.8.30/blob/3c75267f11336acb1be06bed419e8056cc689d09/mp/mp_fget.c#L573-L593

    And BDB's own LSN reset walks every readable page: https://github.com/observerdev/db-4.8.30/blob/3c75267f11336acb1be06bed419e8056cc689d09/db/db_setlsn.c#L122-L135

    So changing the loop to include outer_meta.last_page matches Berkeley DB behavior.

  7. achow101 commented at 11:44 AM on May 7, 2026: member

    ACK e2b0984f99519f76423ce26ce9077ca765b2b30b

    The condition that this fixes is unlikely to be hit in practice. It would mean that every page except the last has a reset LSN, which I believe is actually DB corruption.

  8. luke-jr referenced this in commit 2069130d80 on May 7, 2026
  9. sedited approved
  10. sedited commented at 9:53 AM on May 10, 2026: contributor

    ACK e2b0984f99519f76423ce26ce9077ca765b2b30b

  11. sedited merged this on May 10, 2026
  12. sedited closed this on May 10, 2026

  13. l0rinc deleted the branch on May 10, 2026
  14. fanquake referenced this in commit 409b2d8475 on May 11, 2026
  15. fanquake commented at 10:24 AM on May 11, 2026: member

    Backported to 31.x in #35231.


github-metadata-mirror

This is a metadata mirror of the GitHub repository bitcoin/bitcoin. This site is not affiliated with GitHub. Content is generated from a GitHub metadata backup.
generated: 2026-05-11 12:12 UTC

This site is hosted by @0xB10C
More mirrored repositories can be found on mirror.b10c.me