Compare commits

..

4 Commits

Author SHA1 Message Date
Vito
943c92ca1e perf: Dispatch hasInvalidAmount on type tag instead of dynamic_cast
hasInvalidAmount runs on the per-transaction invariant-checking path
(ValidAmounts::finalize) and at preflight, walking every field of every
modified entry. The previous implementation tried a dynamic_cast chain
(STAmount, STObject, STArray) on each field, so every scalar non-amount
field paid three failed RTTI casts.

Dispatch on the serialized type tag via getSType() and a switch instead.
The object-like tags all denote STObject subclasses (STLedgerEntry, STTx),
so the downcast is sound; safeDowncast keeps a dynamic_cast validity
assert in debug builds while compiling to static_cast in release.
2026-06-04 15:27:22 +02:00
Ayaz Salikhov
6c543426c3 ci: Fix clang asan include dirs in nix images, add curl & gnupg (#7400) 2026-06-03 22:19:15 +00:00
yinyiqian1
e5cf1a0985 fix: Add zero NFT Offer ID check for NFTokenCancelOffer (#7391) 2026-06-03 19:30:20 +00:00
Ayaz Salikhov
023bdaeeed ci: Install gcov, nettools, cacert in nix images (#7398) 2026-06-03 19:14:17 +00:00
10 changed files with 101 additions and 12 deletions

View File

@@ -134,6 +134,7 @@ words:
- iou
- ious
- isrdc
- isystem
- itype
- jemalloc
- jlog

View File

@@ -8,13 +8,17 @@ clang++ --version
clang-format --version
cmake --version
conan --version
curl --version
g++ --version
gcc --version
gcov --version
gcovr --version
git --version
gpg --version
less --version
make --version
mold --version
netstat --version
ninja --version
perl --version
pkg-config --version
@@ -22,3 +26,9 @@ pre-commit --version
python3 --version
run-clang-tidy --help
vim --version
# A simple test to verify that git can clone a repository over HTTPS
# (i.e. the CA bundle is wired up). Clone to a temp dir and clean up.
tmp_clone="$(mktemp -d)"
git clone --depth 1 https://github.com/XRPLF/actions.git "${tmp_clone}/actions"
rm -rf "${tmp_clone}"

View File

@@ -47,6 +47,12 @@ COPY --from=builder /tmp/build/result /nix/ci-env
ENV PATH="/nix/ci-env/bin:${PATH}"
# Point HTTPS clients (git, curl, conan, ...) at the CA bundle shipped in the
# Nix CI environment, so TLS verification works without ca-certificates being
# installed in the system.
ENV SSL_CERT_FILE="/nix/ci-env/etc/ssl/certs/ca-bundle.crt"
ENV GIT_SSL_CAINFO="/nix/ci-env/etc/ssl/certs/ca-bundle.crt"
# Externally-built dynamically-linked ELF binaries hard-code the loader path
# (e.g. /lib64/ld-linux-x86-64.so.2) in their PT_INTERP header. Install it
# from the Nix store when the base image doesn't already provide one.
@@ -65,8 +71,8 @@ if [ ! -e "${target}" ]; then
fi
EOF
COPY docker/check-tool-versions.sh /tmp/check-tool-versions.sh
RUN /tmp/check-tool-versions.sh
COPY docker/check-tools.sh /tmp/check-tools.sh
RUN /tmp/check-tools.sh
# Sanity-check that the g++/clang++ are able to build binaries, including sanitizer-instrumented ones.
COPY docker/test_files/cpp_sources/ /tmp/cpp_sources/

View File

@@ -2,6 +2,13 @@
#include <cstddef>
#include <iostream>
// Regression test: the compiler-rt sanitizer interface headers must be on the
// include path. A bare on-PATH clang in the Nix CI env doesn't get them
// propagated automatically, so this include would fail to compile with clang++
// if the env isn't wired up correctly. abseil hits the same include during
// sanitizer builds. LeakSanitizer ships with AddressSanitizer.
#include <sanitizer/lsan_interface.h>
#if defined(__clang__) || defined(__GNUC__)
__attribute__((noinline))
#elif defined(_MSC_VER)

View File

@@ -43,6 +43,15 @@ let
bintools = customBinutils;
};
# gcov ships in gcc's `cc` output, but the cc-wrapper doesn't expose it.
# Surface the gcov from our rebuilt gcc (linked against the custom glibc, so
# it runs under the loader installed in the image) and matching the exact
# compiler version, so gcovr can produce coverage reports in the CI env.
customGcov = pkgs.runCommand "gcov-custom-for-ci-env" { } ''
mkdir -p "$out/bin"
ln -s "${customGccCc}/bin/gcov" "$out/bin/gcov"
'';
# stdenv built around the rebuilt gcc / custom glibc. Used to rebuild
# compiler-rt below so its sanitizer runtimes see the custom glibc
# headers.
@@ -85,6 +94,14 @@ let
ln -s "${customCompilerRt.out}/lib" "$rsrc/lib"
ln -s "${customCompilerRt.out}/share" "$rsrc/share" || true
echo "-resource-dir=$rsrc" >> $out/nix-support/cc-cflags
# compiler-rt ships the sanitizer/profile/xray interface headers (e.g.
# <sanitizer/lsan_interface.h>) in its `dev` output. In a normal Nix
# build these reach the include path because compiler-rt is propagated
# via depsTargetTargetPropagated and stdenv's setup hooks add its
# dev/include. The CI image runs clang outside a Nix stdenv (binaries
# on PATH, no setup hooks), so that never happens; add the headers
# explicitly. gcc ships its own copy, which is why this is clang-only.
echo "-isystem ${customCompilerRt.dev}/include" >> $out/nix-support/cc-cflags
'';
};
@@ -105,11 +122,16 @@ in
name = "xrpld-ci-env";
paths = commonPackages ++ [
customGcc
customGcov
customClangForCiEnv
customBinutils
# CA certificate bundle so HTTPS clients (git, curl, conan) can verify
# TLS connections without ca-certificates being installed in the system.
pkgs.cacert
];
pathsToLink = [
"/bin"
"/etc/ssl/certs"
"/lib"
"/include"
"/share"

View File

@@ -11,12 +11,15 @@ in
ccache
cmake
conan
curlMinimal # needed for codecov/codecov-action
gcovr
git
gnumake
gnupg # needed for signing commits & codecov/codecov-action
llvmPackages_22.clang-tools
less # needed for git diff
mold
nettools # provides netstat, used to debug failures in CI
ninja
patchelf
perl # needed for openssl

View File

@@ -1250,16 +1250,30 @@ hasInvalidAmount(STBase const& field, int depth, beast::Journal j)
return true;
}
if (auto const amount = dynamic_cast<STAmount const*>(&field))
return !isLegalMPT(*amount) || !isLegalNet(*amount);
// Dispatch on the serialized type tag rather than RTTI: this is on the invariant-checking path
// and a dynamic_cast chain over every field of every modified entry is measurably expensive.
// The object-like tags below all denote STObject subclasses (STLedgerEntry, STTx), so the
// downcast is sound; nested fields are only ever plain STI_OBJECT / STI_ARRAY containers.
// safeDowncast keeps a dynamic_cast validity assert in debug builds while compiling to
// static_cast in release.
switch (field.getSType())
{
case STI_AMOUNT: {
auto const& amount = safeDowncast<STAmount const&>(field);
return !isLegalMPT(amount) || !isLegalNet(amount);
}
if (auto const object = dynamic_cast<STObject const*>(&field))
return hasInvalidAmount(*object, depth + 1, j);
case STI_OBJECT:
case STI_LEDGERENTRY:
case STI_TRANSACTION:
return hasInvalidAmount(safeDowncast<STObject const&>(field), depth + 1, j);
if (auto const array = dynamic_cast<STArray const*>(&field))
return hasInvalidAmount(*array, depth + 1, j);
case STI_ARRAY:
return hasInvalidAmount(safeDowncast<STArray const&>(field), depth + 1, j);
return false;
default:
return false;
}
}
bool

View File

@@ -4,6 +4,7 @@
#include <xrpl/basics/base_uint.h>
#include <xrpl/ledger/View.h>
#include <xrpl/ledger/helpers/NFTokenHelpers.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
#include <xrpl/protocol/LedgerFormats.h>
#include <xrpl/protocol/Protocol.h>
@@ -21,8 +22,14 @@ namespace xrpl {
NotTEC
NFTokenCancelOffer::preflight(PreflightContext const& ctx)
{
if (auto const& ids = ctx.tx[sfNFTokenOffers];
ids.empty() || (ids.size() > kMaxTokenOfferCancelCount))
auto const& offerIds = ctx.tx[sfNFTokenOffers];
if (offerIds.empty() || (offerIds.size() > kMaxTokenOfferCancelCount))
return temMALFORMED;
// Zero offer IDs cannot be passed as ledger entry keys.
if (ctx.rules.enabled(fixCleanup3_2_0) &&
std::ranges::any_of(offerIds, [](uint256 const& id) { return id.isZero(); }))
return temMALFORMED;
// In order to prevent unnecessarily overlarge transactions, we

View File

@@ -892,6 +892,25 @@ class NFTokenBaseUtil_test : public beast::unit_test::Suite
BEAST_EXPECT(ownerCount(env, buyer) == 1);
}
// Only test this with fixCleanup3_2_0 enabled. Without the fix,
// an assert-enabled build can crash when Ledger::read() receives
// a zero-key offer ID.
if (features[fixCleanup3_2_0])
{
// Zero is not a valid offer ID.
env(token::cancelOffer(buyer, {uint256{}}), Ter(temMALFORMED));
env.close();
BEAST_EXPECT(ownerCount(env, buyer) == 1);
// List of offer IDs containing zero is invalid.
// craftedIndex is not a valid offer index but it is not zero.
auto const craftedIndex = keylet::nftoffer(gw, env.seq(gw)).key;
env(token::cancelOffer(buyer, {buyerOfferIndex, uint256{}, craftedIndex}),
Ter(temMALFORMED));
env.close();
BEAST_EXPECT(ownerCount(env, buyer) == 1);
}
// List of tokens to delete is too long.
{
std::vector<uint256> const offers(kMaxTokenOfferCancelCount + 1, buyerOfferIndex);

View File

@@ -1088,7 +1088,7 @@ public:
<< "; size after: " << cachedSLEs_.size();
}
// mallocTrim("doSweep", journal_);
mallocTrim("doSweep", journal_);
// Set timer to do another sweep later.
setSweepTimer();