From cee157485e1e08181a37afc579bdd6f8fabf263e Mon Sep 17 00:00:00 2001 From: Ayaz Salikhov Date: Thu, 11 Jun 2026 13:59:22 +0100 Subject: [PATCH 1/3] ci: Run sanitizers on release builds too (#7527) --- .github/scripts/strategy-matrix/linux.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/scripts/strategy-matrix/linux.json b/.github/scripts/strategy-matrix/linux.json index e6c807ac95..4f45216cda 100644 --- a/.github/scripts/strategy-matrix/linux.json +++ b/.github/scripts/strategy-matrix/linux.json @@ -10,7 +10,7 @@ { "compiler": ["gcc", "clang"], - "build_type": ["Debug"], + "build_type": ["Debug", "Release"], "arch": ["amd64"], "sanitizers": ["address", "undefinedbehavior"] }, From 8e618d68cd254d44b40bb41ce588d58b8b34c2c3 Mon Sep 17 00:00:00 2001 From: Ayaz Salikhov Date: Thu, 11 Jun 2026 18:36:33 +0100 Subject: [PATCH 2/3] ci: Patch conan recipe for Nix to be able to use on macOS (#7532) --- cspell.config.yaml | 2 ++ nix/devshell.nix | 22 +++++++++++++++++++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/cspell.config.yaml b/cspell.config.yaml index 926ac06596..77f0e9df7a 100644 --- a/cspell.config.yaml +++ b/cspell.config.yaml @@ -233,8 +233,10 @@ words: - pyenv - pyparsing - qalloc + - qbsprofile - queuable - Raphson + - rcflags - replayer - rerere - retriable diff --git a/nix/devshell.nix b/nix/devshell.nix index 1bd7ea4c0c..105033eb06 100644 --- a/nix/devshell.nix +++ b/nix/devshell.nix @@ -1,6 +1,26 @@ { pkgs, ... }: let - inherit (import ./packages.nix { inherit pkgs; }) commonPackages; + # conan is in the binary cache for Linux but not for Darwin, so on Darwin + # it is always built from source — and its bundled test suite is unreliable + # in the sandbox: `test_qbsprofile_rcflags` needs gcc (absent on Darwin, see + # https://github.com/NixOS/nixpkgs/pull/528995) and the patch tests are + # flaky from source. We only use conan as a build tool, so skip its tests on + # Darwin. Scoped to the dev shell (not the CI env, which builds conan on + # Linux from the cache). Drop once the fix reaches nixos-unstable and the + # lock is bumped. + pkgs_patched = + if pkgs.stdenv.isDarwin then + pkgs.extend ( + final: prev: { + conan = prev.conan.overridePythonAttrs (_: { + doCheck = false; + }); + } + ) + else + pkgs; + + inherit (import ./packages.nix { pkgs = pkgs_patched; }) commonPackages; # Supported compiler versions gccVersion = pkgs.lib.range 13 15; From df395d685159717c1725219232b9a9b3b8dc45da Mon Sep 17 00:00:00 2001 From: Pratik Mankawde <3397372+pratikmankawde@users.noreply.github.com> Date: Thu, 11 Jun 2026 19:05:36 +0100 Subject: [PATCH 3/3] test: Add null check unit test for `Oracle::aggregatePrice` (#7306) Signed-off-by: Pratik Mankawde <3397372+pratikmankawde@users.noreply.github.com> --- src/test/rpc/GetAggregatePrice_test.cpp | 89 +++++++++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/src/test/rpc/GetAggregatePrice_test.cpp b/src/test/rpc/GetAggregatePrice_test.cpp index 37ecc54172..214bd12183 100644 --- a/src/test/rpc/GetAggregatePrice_test.cpp +++ b/src/test/rpc/GetAggregatePrice_test.cpp @@ -3,12 +3,21 @@ #include #include +#include + #include +#include #include +#include +#include +#include #include +#include +#include #include #include +#include #include #include #include @@ -312,11 +321,91 @@ public: } } + void + testNullTxReadMeta() + { + testcase("Null txRead metadata"); + using namespace jtx; + + // Verify that iteratePriceData handles a null txRead result + // gracefully (returns early) rather than crashing with a + // nullptr dereference. This simulates local data corruption + // where a transaction referenced by sfPreviousTxnID is missing + // from the ledger's transaction map. + Env env(*this); + auto const baseFee = static_cast(env.current()->fees().base.drops()); + + Account const owner{"owner"}; + env.fund(XRP(1'000), owner); + + // Create oracle with XRP/USD and XRP/EUR + Oracle oracle( + env, + {.owner = owner, + .series = {{"XRP", "USD", 740, 1}, {"XRP", "EUR", 840, 1}}, + .fee = baseFee}); + + // Update oracle to only have XRP/EUR, pushing XRP/USD into + // history. iteratePriceData will need to read historical tx + // metadata to find the XRP/USD price. + oracle.set(UpdateArg{.series = {{"XRP", "EUR", 850, 1}}, .fee = baseFee}); + + OraclesData const oracles{{owner, oracle.documentID()}}; + + // Precondition: with an uncorrupted oracle, the historical + // traversal must succeed and produce a price for XRP/USD. + // This proves the test reaches iteratePriceData's history + // path; without it, a future change that breaks the setup + // could turn the post-corruption assertion into a vacuous + // pass (objectNotFound is reachable from many unrelated + // code paths). + { + auto const ret = Oracle::aggregatePrice(env, "XRP", "USD", oracles); + BEAST_EXPECT(!ret.isMember(jss::error)); + BEAST_EXPECT(ret.isMember(jss::median)); + } + + // Simulate data corruption: modify the oracle SLE in the open + // ledger to have a bogus sfPreviousTxnID that doesn't exist in + // any ledger. sfPreviousTxnLgrSeq still points to a valid closed + // ledger, so getLedgerBySeq succeeds but txRead returns null. + auto const oracleKeylet = keylet::oracle(owner, oracle.documentID()); + uint256 const bogusTxnID{0xABCABCAB}; + bool const modified = env.app().getOpenLedger().modify( + [&oracleKeylet, &bogusTxnID](OpenView& view, beast::Journal) -> bool { + auto const sle = view.read(oracleKeylet); + if (!sle) + return false; + auto replacement = std::make_shared(*sle, sle->key()); + replacement->setFieldH256(sfPreviousTxnID, bogusTxnID); + view.rawReplace(replacement); + return true; + }); + + // Confirm the injection actually took effect: modify must + // report success, and re-reading the SLE must show the + // bogus hash. Otherwise the failure-mode assertion below + // would not be exercising the null-txRead path at all. + BEAST_EXPECT(modified); + if (auto const sle = env.current()->read(oracleKeylet); BEAST_EXPECT(sle)) + BEAST_EXPECT(sle->getFieldH256(sfPreviousTxnID) == bogusTxnID); + + // Query for XRP/USD using the "current" (open) ledger. + // The oracle SLE now has a bogus sfPreviousTxnID. The current + // oracle only has EUR, so iteratePriceData will try to read + // history. txRead returns null for the bogus hash, and the + // null check should cause a graceful early return instead of + // a nullptr dereference. + auto const ret = Oracle::aggregatePrice(env, "XRP", "USD", oracles); + BEAST_EXPECT(ret[jss::error].asString() == "objectNotFound"); + } + void run() override { testErrors(); testRpc(); + testNullTxReadMeta(); } };