wallet_bdb_parser.cpp raw
1 // Copyright (c) 2023-present The Limenka developers
2 // Distributed under the MIT software license, see the accompanying
3 // file COPYING or http://www.opensource.org/licenses/mit-license.php.
4
5 #include <limenka-build-config.h> // IWYU pragma: keep
6 #include <test/fuzz/FuzzedDataProvider.h>
7 #include <test/fuzz/fuzz.h>
8 #include <test/fuzz/util.h>
9 #include <test/util/setup_common.h>
10 #include <util/fs.h>
11 #include <util/time.h>
12 #include <util/translation.h>
13 #include <wallet/bdb.h>
14 #include <wallet/db.h>
15 #include <wallet/dump.h>
16 #include <wallet/migrate.h>
17
18 #include <fstream>
19 #include <iostream>
20
21 // There is an inconsistency in BDB on Windows.
22 // See: https://github.com/limenka/limenka/pull/26606#issuecomment-2322763212
23 #undef USE_BDB_NON_MSVC
24 #if defined(USE_BDB) && !defined(_MSC_VER)
25 #define USE_BDB_NON_MSVC
26 #endif
27
28 using wallet::DatabaseOptions;
29 using wallet::DatabaseStatus;
30
31 namespace {
32 TestingSetup* g_setup;
33 } // namespace
34
35 void initialize_wallet_bdb_parser()
36 {
37 static auto testing_setup = MakeNoLogFileContext<TestingSetup>();
38 g_setup = testing_setup.get();
39 }
40
41 FUZZ_TARGET(wallet_bdb_parser, .init = initialize_wallet_bdb_parser)
42 {
43 SeedRandomStateForTest(SeedRand::ZEROS); // for IsDirWritable
44 const auto wallet_path = g_setup->m_args.GetDataDirNet() / "fuzzed_wallet.dat";
45
46 {
47 AutoFile outfile{fsbridge::fopen(wallet_path, "wb")};
48 outfile << Span{buffer};
49 assert(outfile.fclose() == 0);
50 }
51
52 const DatabaseOptions options{};
53 DatabaseStatus status;
54 bilingual_str error;
55
56 fs::path bdb_ro_dumpfile{g_setup->m_args.GetDataDirNet() / "fuzzed_dumpfile_bdb_ro.dump"};
57 if (fs::exists(bdb_ro_dumpfile)) { // Writing into an existing dump file will throw an exception
58 remove(bdb_ro_dumpfile);
59 }
60 g_setup->m_args.ForceSetArg("-dumpfile", fs::PathToString(bdb_ro_dumpfile));
61
62 #ifdef USE_BDB_NON_MSVC
63 bool bdb_ro_err = false;
64 bool bdb_ro_strict_err = false;
65 #endif
66 auto db{MakeBerkeleyRODatabase(wallet_path, options, status, error)};
67 if (db) {
68 assert(DumpWallet(*db, error, fs::PathToString(bdb_ro_dumpfile)));
69 } else {
70 #ifdef USE_BDB_NON_MSVC
71 bdb_ro_err = true;
72 #endif
73 if (error.original.starts_with("AutoFile::ignore: end of file") ||
74 error.original.starts_with("AutoFile::read: end of file") ||
75 error.original.starts_with("AutoFile::seek: ") ||
76 error.original == "Not a BDB file" ||
77 error.original == "Unexpected page type, should be 9 (BTree Metadata)" ||
78 error.original == "Unexpected database flags, should only be 0x20 (subdatabases)" ||
79 error.original == "Unexpected outer database root page type" ||
80 error.original == "Unexpected number of entries in outer database root page" ||
81 error.original == "Subdatabase page number has unexpected length" ||
82 error.original == "Unknown record type in records page" ||
83 error.original == "Unknown record type in internal page" ||
84 error.original == "Unexpected page size" ||
85 error.original == "Unexpected page type" ||
86 error.original == "Page number mismatch" ||
87 error.original == "Bad btree level" ||
88 error.original == "Bad page size" ||
89 error.original == "Meta page number mismatch" ||
90 error.original == "Data record position not in page" ||
91 error.original == "Internal record position not in page" ||
92 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" ||
93 error.original == "Records page has odd number of records" ||
94 error.original == "BTree page has an unexpected level" ||
95 error.original == "BTree Leaf page is not at level 1" ||
96 error.original == "Bad overflow record page type") {
97 // Do nothing
98 } else if (error.original == "Subdatabase last page is greater than database last page" ||
99 error.original == "Page number is greater than database last page" ||
100 error.original == "Last page number could not fit in file" ||
101 error.original == "Subdatabase has an unexpected name" ||
102 error.original == "Unsupported BDB data file version number" ||
103 error.original == "BDB builtin encryption is not supported") {
104 #ifdef USE_BDB_NON_MSVC
105 bdb_ro_strict_err = true;
106 #endif
107 } else {
108 throw std::runtime_error(error.original);
109 }
110 }
111
112 #ifdef USE_BDB_NON_MSVC
113 // Try opening with BDB
114 fs::path bdb_dumpfile{g_setup->m_args.GetDataDirNet() / "fuzzed_dumpfile_bdb.dump"};
115 if (fs::exists(bdb_dumpfile)) { // Writing into an existing dump file will throw an exception
116 remove(bdb_dumpfile);
117 }
118 g_setup->m_args.ForceSetArg("-dumpfile", fs::PathToString(bdb_dumpfile));
119
120 try {
121 auto db{MakeBerkeleyDatabase(wallet_path, options, status, error)};
122 if (bdb_ro_err && !db) {
123 return;
124 }
125 assert(db);
126 if (bdb_ro_strict_err) {
127 // BerkeleyRO will be stricter than BDB. Ignore when those specific errors are hit.
128 return;
129 }
130 assert(!bdb_ro_err);
131 assert(DumpWallet(*db, error, fs::PathToString(bdb_dumpfile)));
132 } catch (const std::runtime_error& e) {
133 if (bdb_ro_err) return;
134 throw e;
135 }
136
137 // Make sure the dumpfiles match
138 if (fs::exists(bdb_ro_dumpfile) && fs::exists(bdb_dumpfile)) {
139 std::ifstream bdb_ro_dump(bdb_ro_dumpfile, std::ios_base::binary | std::ios_base::in);
140 std::ifstream bdb_dump(bdb_dumpfile, std::ios_base::binary | std::ios_base::in);
141 assert(std::equal(
142 std::istreambuf_iterator<char>(bdb_ro_dump.rdbuf()),
143 std::istreambuf_iterator<char>(),
144 std::istreambuf_iterator<char>(bdb_dump.rdbuf())));
145 }
146 #endif
147 }
148