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