From 64e0ee4be9c5fe5d70ed2b1d2d8e9667885b43e0 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Wed, 29 Oct 2025 12:25:03 -0400 Subject: [PATCH] Add POC unit test for null vault dereference --- src/test/app/LoanBroker_test.cpp | 69 ++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/src/test/app/LoanBroker_test.cpp b/src/test/app/LoanBroker_test.cpp index 57bff0bbfa..d2a8a3656d 100644 --- a/src/test/app/LoanBroker_test.cpp +++ b/src/test/app/LoanBroker_test.cpp @@ -19,6 +19,8 @@ #include +#include + #include namespace ripple { @@ -1162,10 +1164,77 @@ class LoanBroker_test : public beast::unit_test::suite testLoanBroker({}, Set); } + void + testLoanBrokerCoverDepositNullVault() + { + // This test is lifted directly from + // https://bugs.immunefi.com/dashboard/submission/57808 + using namespace jtx; + Env env(*this); + + Account const alice{"alice"}; + env.fund(XRP(10000), alice); + env.close(); + + // Create a Vault owned by alice with an XRP asset + PrettyAsset const asset{xrpIssue(), 1}; + Vault vault{env}; + auto const [createTx, vaultKeylet] = + vault.create({.owner = alice, .asset = asset}); + env(createTx); + env.close(); + + // Predict LoanBroker key using alice's current sequence BEFORE submit + auto const brokerKeylet = + keylet::loanbroker(alice.id(), env.seq(alice)); + + // Create LoanBroker pointing to the vault + env(loanBroker::set(alice, vaultKeylet.key)); + env.close(); + + // Build the CoverDeposit STTx directly + STTx tx{ttLOAN_BROKER_COVER_DEPOSIT, [](STObject&) {}}; + tx.setAccountID(sfAccount, alice.id()); + tx.setFieldH256(sfLoanBrokerID, brokerKeylet.key); + tx.setFieldAmount(sfAmount, asset(1)); + + // Create a writable view cloned from the current ledger and remove the + // vault SLE + OpenView ov{*env.current()}; + test::StreamSink sink{beast::severities::kWarning}; + beast::Journal jlog{sink}; + ApplyContext ac{ + env.app(), + ov, + tx, + tesSUCCESS, + env.current()->fees().base, + tapNONE, + jlog}; + + if (auto sleBroker = + ac.view().peek(keylet::loanbroker(brokerKeylet.key))) + { + auto const vaultID = (*sleBroker)[sfVaultID]; + if (auto sleVault = ac.view().peek(keylet::vault(vaultID))) + { + ac.view().erase(sleVault); + } + } + + // Invoke preclaim against the mutated (ApplyView) view; triggers + // nullptr deref + PreclaimContext pctx{ + env.app(), ac.view(), tesSUCCESS, tx, tapNONE, jlog}; + (void)LoanBrokerCoverDeposit::preclaim(pctx); + } + public: void run() override { + testLoanBrokerCoverDepositNullVault(); + testDisabled(); testLifecycle(); testInvalidLoanBrokerCoverClawback();