From a9f3165b361d6cf65dbda97586b967312c802a5d Mon Sep 17 00:00:00 2001 From: Max Murshed Date: Fri, 12 Jun 2026 14:38:28 -0700 Subject: [PATCH] test: seeded adversarial-limb fuzz as a CI gate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three silent wrong-answer classes shipped in 2026 (MFA inverse twiddle #103, BZ odd-size fallback #116, FastDivision MG top-equal qhat in #120) and none was catchable by uniformly random operands — each required a structured limb pattern with ~zero probability under the existing random sweeps. Adds 5 deterministic fuzz tests to the unit suite (~60 ms total) using the 0 / 1 / 2^64-1 / 2^63-biased limb distribution that surfaced the MG bug: 2000 division cases cross-checked digit-for-digit against the KnuthDivision reference plus the q*b+r==a identity, larger BZ/Newton/ QSized shapes (including odd sizes for the BZ padding path) via the identity, 1000 multiplication cases vs classic schoolbook plus NTT-band spot checks, Square vs Multiply(a,a), and ToString/parse round-trips. Co-Authored-By: Claude Fable 5 --- tests/unit/test_adversarial_fuzz.cpp | 164 +++++++++++++++++++++++++++ 1 file changed, 164 insertions(+) create mode 100644 tests/unit/test_adversarial_fuzz.cpp diff --git a/tests/unit/test_adversarial_fuzz.cpp b/tests/unit/test_adversarial_fuzz.cpp new file mode 100644 index 0000000..7cb610d --- /dev/null +++ b/tests/unit/test_adversarial_fuzz.cpp @@ -0,0 +1,164 @@ +// Seeded adversarial-limb fuzz: cross-checks the dispatch paths against +// O(n^2) references on operands biased toward 0 / 1 / 2^64-1 / 2^63 limbs. +// +// Why this exists: three silent wrong-answer bugs shipped in 2026 (MFA +// inverse twiddle #103, BZ odd-size fallback #116, FastDivision MG top-equal +// qhat) and none was catchable by uniformly random operands — each needed a +// structured limb pattern with probability ~0 under the random sweeps. +// Deterministic seeds keep CI reproducible; bump the per-test case counts +// locally for deeper runs. + +#include "unit_test_framework.h" + +#include +#include + +#include "biginteger/BigInteger.h" +#include "biginteger/common/Builder.h" +#include "biginteger/common/Parser.h" +#include "biginteger/algorithms/Addition.h" +#include "biginteger/algorithms/Multiplication.h" +#include "biginteger/algorithms/Squaring.h" +#include "biginteger/algorithms/Division.h" +#include "biginteger/algorithms/division/KnuthDivision.h" +#include "biginteger/algorithms/multiplication/ClassicMultiplication.h" + +using namespace BigMath; + +namespace +{ + constexpr DataT kMax = ~(DataT)0; + constexpr DataT kTop = (DataT)1 << 63; + + // ~25% all-ones, ~25% zero, then 1 / top-bit / uniform — the distribution + // that surfaced the FastDivision MG bug. + std::vector AdversarialLimbs(SizeT n, std::mt19937_64 &rng) + { + std::vector v(n); + for (auto &x : v) + { + switch (rng() % 6) + { + case 0: + case 1: x = kMax; break; + case 2: + case 3: x = 0; break; + case 4: x = (rng() % 2) ? 1 : kTop; break; + default: x = (DataT)rng(); break; + } + } + while (v.size() > 1 && v.back() == 0) + v.pop_back(); + if (IsZero(v)) + v[0] = 1; + return v; + } +} + +// ── division: dispatch vs KnuthDivision reference + q*b + r == a ─────────── +// Sizes span the FastDivision band and the small ends of the BZ/Newton +// bands; the Knuth O(n^2) reference keeps the runtime bounded. + +REGISTER_TEST(AdversarialFuzz, DivisionDispatchVsKnuth) +{ + std::mt19937_64 rng(0xD1F1D1F1ULL); + for (int t = 0; t < 2000; ++t) + { + SizeT na = 2 + (SizeT)(rng() % 40); + SizeT nb = 1 + (SizeT)(rng() % na); + auto a = AdversarialLimbs(na, rng); + auto b = AdversarialLimbs(nb, rng); + + auto [q, r] = DivideAndRemainder(a, b, BigInteger::Base(), true); + auto k = KnuthDivision::DivideAndRemainder(a, b, BigInteger::Base()); + ASSERT_EQ(Compare(q, k.first), 0); + ASSERT_EQ(Compare(r, k.second), 0); + + auto back = Add(Multiply(q, b, BigInteger::Base()), r, BigInteger::Base()); + ASSERT_EQ(Compare(back, a), 0); + ASSERT_EQ(Compare(r, b) < 0, true); // r < b + } +} + +REGISTER_TEST(AdversarialFuzz, DivisionLargeBandsIdentity) +{ + // Larger shapes hit BZ / Newton / QSized; verify via the identity only + // (Knuth would dominate the runtime here). + std::mt19937_64 rng(0xB16B00B5ULL); + const SizeT shapes[][2] = { + {700, 600}, // BZ near-balanced + {1300, 640}, // BZ 2:1 + {1025, 513}, // odd sizes (BZ padding path) + {2100, 600}, // big-and-skewed BZ band + {1500, 160}, // FastDivision long skew + }; + for (auto const &sh : shapes) + { + for (int t = 0; t < 3; ++t) + { + auto a = AdversarialLimbs(sh[0], rng); + auto b = AdversarialLimbs(sh[1], rng); + auto [q, r] = DivideAndRemainder(a, b, BigInteger::Base(), true); + auto back = Add(Multiply(q, b, BigInteger::Base()), r, BigInteger::Base()); + ASSERT_EQ(Compare(back, a), 0); + ASSERT_EQ(Compare(r, b) < 0, true); + } + } +} + +// ── multiplication: dispatch vs classic schoolbook ────────────────────────── + +REGISTER_TEST(AdversarialFuzz, MultiplicationDispatchVsClassic) +{ + std::mt19937_64 rng(0x5EEDBEEFULL); + for (int t = 0; t < 1000; ++t) + { + SizeT na = 1 + (SizeT)(rng() % 96); + SizeT nb = 1 + (SizeT)(rng() % 96); + auto a = AdversarialLimbs(na, rng); + auto b = AdversarialLimbs(nb, rng); + auto fast = Multiply(a, b, BigInteger::Base()); + auto slow = ClassicMultiplication::Multiply(a, b, BigInteger::Base()); + ASSERT_EQ(Compare(fast, slow), 0); + } + // A few NTT-band cases (sum >= 1280 limbs) against classic. + for (int t = 0; t < 3; ++t) + { + auto a = AdversarialLimbs(900, rng); + auto b = AdversarialLimbs(700, rng); + auto fast = Multiply(a, b, BigInteger::Base()); + auto slow = ClassicMultiplication::Multiply(a, b, BigInteger::Base()); + ASSERT_EQ(Compare(fast, slow), 0); + } +} + +REGISTER_TEST(AdversarialFuzz, SquareMatchesMultiply) +{ + std::mt19937_64 rng(0x5145A4E5ULL); + for (SizeT n : {40u, 700u, 1100u}) // Karatsuba band + CRT band + { + for (int t = 0; t < 3; ++t) + { + auto a = AdversarialLimbs(n, rng); + auto sq = Square(a, BigInteger::Base()); + auto mu = Multiply(a, a, BigInteger::Base()); + ASSERT_EQ(Compare(sq, mu), 0); + } + } +} + +// ── decimal I/O round-trip on adversarial limbs ────────────────────────────── + +REGISTER_TEST(AdversarialFuzz, ToStringParseRoundTrip) +{ + std::mt19937_64 rng(0x70537217ULL); + for (SizeT n : {1u, 7u, 60u, 900u}) + { + for (int t = 0; t < 3; ++t) + { + BigInteger v(AdversarialLimbs(n, rng), (t % 2) == 1); + BigInteger back = BigIntegerBuilder::From(ToString(v).c_str()); + ASSERT_EQ(back.CompareTo(v), 0); + } + } +}