{
 "number": 35990,
 "repo": "bitcoin/bitcoin",
 "url": "https://github.com/bitcoin/bitcoin/pull/35990",
 "title": "wallet: harden the read-only BDB parser against crafted files",
 "author": "shuv-amp",
 "author_association": "NONE",
 "created_at": "2026-08-16T21:15:55Z",
 "updated_at": "2026-08-17T15:16:13Z",
 "age_days": 31,
 "draft": false,
 "labels": [
  "Wallet"
 ],
 "milestone": null,
 "base": "master",
 "head_sha": "acbde35bb227ad5dfe9af04babc4c6730b702a80",
 "head_ref": "bdbro-reject-revisited-pages",
 "head_repo": "shuv-amp/bitcoin",
 "head_history": [
  {
   "t": "2026-08-17T11:14:41Z",
   "sha": "05c5766d57d5e3d785130069cd2a308917ff7db2"
  },
  {
   "t": "2026-08-17T15:16:13Z",
   "sha": "acbde35bb227ad5dfe9af04babc4c6730b702a80"
  }
 ],
 "additions": 226,
 "deletions": 7,
 "changed_files": 3,
 "commit_count": 3,
 "size_bucket": "M",
 "mergeable_state": "blocked",
 "bot": {
  "drahtbot": {
   "present": true,
   "reviews": {},
   "conflicts": []
  }
 },
 "acks_parsed": {},
 "acks_tally": {
  "ack": 0,
  "stale_ack": 0,
  "concept_ack": 0,
  "approach_ack": 0,
  "nack": 0,
  "concept_nack": 0,
  "approach_nack": 0
 },
 "reviews": {
  "approved": 0,
  "changes_requested": 0,
  "distinct_reviewers": []
 },
 "signals": {
  "needs_rebase": false,
  "ci_failed": false,
  "mergeable_state": "blocked",
  "last_author_activity": "2026-08-17T15:16:13Z",
  "last_reviewer_activity": null,
  "last_reviewer": null,
  "author_silent_days": 31,
  "waiting_on_author_days": 0,
  "days_since_update": 31
 },
 "refs": {
  "mentioned": [
   34946,
   34959,
   35150,
   35992
  ],
  "depends_on": [],
  "fixes": [],
  "linked_issues": [],
  "references": [
   {
    "number": 34959,
    "type": "pull",
    "state": "closed",
    "merged": true,
    "merged_at": "2026-07-04",
    "title": "wallet: Enforce BDB btree levels and overflow item sizes"
   },
   {
    "number": 34946,
    "type": "pull",
    "state": "closed",
    "merged": false,
    "merged_at": null,
    "title": "wallet: detect cycles in BDB page traversal during migration"
   },
   {
    "number": 35150,
    "type": "pull",
    "state": "closed",
    "merged": false,
    "merged_at": null,
    "title": "wallet: reject cyclic page references in legacy BDB parser"
   },
   {
    "number": 35992,
    "type": "pull",
    "state": "closed",
    "merged": false,
    "merged_at": null,
    "title": "wallet: Bound the records read from a BDB page to the page size"
   }
  ],
  "conflicts": []
 },
 "stack": {
  "shares_commits_with": [],
  "based_on": [],
  "base_for": []
 },
 "review_paths": [],
 "body": "The read-only BDB parser (`BerkeleyRODatabase::Open()` in migrate.cpp) is fed an\nattacker-supplied file whenever a user runs `migratewallet`, loads a legacy\n`.dat` wallet, or runs `bitcoin-wallet`. #34959 hardened it against circular\nreferences, but three crafted-file cases still get through and turn a small file\ninto unbounded CPU or memory.\n\n**Revisited btree page.** The level check rejects a page that is its own\nancestor, but a page reachable from more than one parent stays level consistent,\npasses the check, and is parsed once per path to it. An ~8 KB file of shared\nsubtrees does not finish.\n\n**Overflow chain of empty pages.** #34959 bounds an overflow chain by the stated\ndata length, but pages that carry no data never advance it, so two empty\noverflow pages pointing at each other loop forever.\n\n**Page records that do not fit.** A page's index entries can all point at the\nsame record, which is then read and kept once per entry. `entries` and the\nrecord length are both 16-bit, so a single ~64 KB page can be read into\n`entries * len` bytes. A 256 KB file takes a node from 54 MB to ~800 MB during\n`migratewallet`, and distinct such pages add up to an OOM.\n\nNone of these corrupt memory; they are CPU/memory exhaustion from a crafted or\ncorrupted wallet file, the same threat model #34959 addressed.\n\nThe first two are fixed by tracking visited pages in each traversal (the\napproach from #34946 / #35150, closed as superseded by #34959, which turns out\nnot to cover these level-consistent and empty-page cases). The third is fixed by\nbounding a page's total record size to the page. A valid BDB database references\neach page once and its records fit within the page, so none of this rejects a\nfile that parses today.\n\nEach fix is a separate commit with a regression test in db_tests.cpp that\nhand-crafts the relevant file (Core cannot write a BDB database, so the bytes are\nlaid out directly) and checks the parser rejects it. The three new error strings\nare also added to the `wallet_bdb_parser` fuzz allow-list.\n\nSupersedes #35992, which proposed the record-size bound on its own.",
 "commits": [
  {
   "sha": "381bd446ac1c78ddb67b54db0239427d006094aa",
   "date": "2026-08-17T15:14:54Z",
   "message": "wallet, bdbro: Reject revisited btree pages\n\nValidating btree page levels stops a page from being its own ancestor,\nbut a page can still be reachable from more than one parent while\nkeeping a consistent level. Such a page, and everything below it, is\nparsed again for every path that reaches it, so a small file can\ndescribe an exponential amount of work. A valid btree references each\npage once, so keep track of the pages already visited and reject any\nthat is seen again."
  },
  {
   "sha": "ba7e0100c129b75144182a762bc00837f65e2446",
   "date": "2026-08-17T15:14:54Z",
   "message": "wallet, bdbro: Reject revisited overflow pages\n\nEnforcing overflow data lengths stops a chain whose pages carry data,\nbut overflow pages that carry no data never advance the accumulated\nlength, so a chain that cycles through empty overflow pages loops\nforever. Keep track of the pages visited while following a chain and\nreject one that is seen again."
  },
  {
   "sha": "acbde35bb227ad5dfe9af04babc4c6730b702a80",
   "date": "2026-08-17T15:14:54Z",
   "message": "wallet, bdbro: Enforce that a page's records fit in the page\n\nA page's index entries each point at a record that is read and kept, but\nthe offsets are not checked against each other. A page whose entries all\npoint at the same record is therefore read into entries * len bytes of\nmemory, so a single page can be read into far more than its own size. In\na valid page the records do not overlap and cannot be larger than the\npage, so track their total size while reading and reject a page whose\nrecords do not fit."
  }
 ],
 "timeline": [
  {
   "t": "2026-08-17T11:14:41Z",
   "kind": "force_push",
   "who": "shuv-amp",
   "commit": "05c5766d57d5e3d785130069cd2a308917ff7db2"
  },
  {
   "t": "2026-08-17T15:16:13Z",
   "kind": "force_push",
   "who": "shuv-amp",
   "commit": "acbde35bb227ad5dfe9af04babc4c6730b702a80"
  }
 ],
 "labels_log": [
  {
   "t": "2026-08-16T21:15:59Z",
   "action": "labeled",
   "label": "Wallet",
   "who": "DrahtBot"
  }
 ],
 "state_log": [
  {
   "t": "2026-08-16T21:18:03Z",
   "kind": "ready_for_review",
   "who": "shuv-amp"
  },
  {
   "t": "2026-08-17T10:58:12Z",
   "kind": "convert_to_draft",
   "who": "shuv-amp"
  },
  {
   "t": "2026-08-17T11:15:57Z",
   "kind": "renamed",
   "who": "shuv-amp",
   "from": "wallet: Reject revisited pages in the read-only BDB parser",
   "to": "wallet: harden the read-only BDB parser against crafted files"
  },
  {
   "t": "2026-08-17T11:17:07Z",
   "kind": "ready_for_review",
   "who": "shuv-amp"
  }
 ],
 "text_chars": 3501,
 "text_tokens_estimate": 875,
 "changed_paths": [
  "src/wallet/migrate.cpp",
  "src/wallet/test/db_tests.cpp",
  "src/wallet/test/fuzz/wallet_bdb_parser.cpp"
 ],
 "files": [
  {
   "path": "src/wallet/migrate.cpp",
   "add": 47,
   "del": 7
  },
  {
   "path": "src/wallet/test/db_tests.cpp",
   "add": 175,
   "del": 0
  },
  {
   "path": "src/wallet/test/fuzz/wallet_bdb_parser.cpp",
   "add": 4,
   "del": 0
  }
 ],
 "test_lines": 179,
 "git": {
  "head": "acbde35bb227ad5dfe9af04babc4c6730b702a80",
  "head_matches_backup": true,
  "base": "11090c8bb359f894ef7d97b65aff52fe8191aec1",
  "commits": [
   {
    "sha": "381bd446ac",
    "subject": "wallet, bdbro: Reject revisited btree pages",
    "files": 3,
    "add": 111,
    "del": 2
   },
   {
    "sha": "ba7e0100c1",
    "subject": "wallet, bdbro: Reject revisited overflow pages",
    "files": 3,
    "add": 48,
    "del": 0
   },
   {
    "sha": "acbde35bb2",
    "subject": "wallet, bdbro: Enforce that a page's records fit in the page",
    "files": 3,
    "add": 67,
    "del": 5
   }
  ],
  "patch_truncated": false
 },
 "input_hash": "19117bcdebe5133a",
 "extracted_at": "2026-09-17T16:15:31+00:00"
}