fix(q-w0): six audit fix-nows — linked-lag stereo SOLA, playable-span prime bound, declick dead-state, provenance cursor hardening, rate-derived gain ramp + fade ceiling

This commit is contained in:
2026-07-28 18:44:52 -04:00
parent 35f02cece2
commit 15d293b42f
12 changed files with 428 additions and 67 deletions
+56
View File
@@ -7,6 +7,8 @@
// track GUIDs, FX-chain identity) -> a MISMATCH (different string / recipe).
// * fxChainIdentity fold: order-sensitive, field-injection-proof, empty-stable.
// * parse of malformed / wrong-version / truncated input -> nullopt (graceful).
// * hardened wire cursor (Q-W0 T2-01a): hostile digit-run lengths, wrap-magnitude
// lengths, and huge GUID counts -> nullopt with no overflow and no over-allocation.
// * parent-detection decision: positive, negative, ambiguous, empty, and the
// edge where a source file is not in the bank (missing-from-bank).
//
@@ -190,6 +192,58 @@ static void testMalformedFingerprint() {
"1:0" "0:").has_value());
}
// --- Q-W0 T2-01a: hardened wire cursor (backported from assignment_request /
// sample_usage) — corrupt or crafted persisted fingerprints must fail the parse
// cleanly (nullopt), never wrap an integer, never throw, never over-allocate. ----
// Mirrors buildFingerprint's field order with benign values, except the GUID-count
// field carries caller-supplied raw text — the attack surface under test.
static void putF(std::string& out, const std::string& f) {
out += std::to_string(f.size());
out += ':';
out += f;
}
static std::string forgedFingerprint(const std::string& guidCountText) {
std::string out = "rsprov1";
putF(out, "0"); // scope = Item
putF(out, "0"); // sourceMode
putF(out, "0"); // startSeconds
putF(out, "1"); // endSeconds
putF(out, "0"); // tailMode
putF(out, "0"); // tailMs
putF(out, "48000"); // sampleRate
putF(out, "2"); // channelCount
putF(out, guidCountText); // GUID count (no GUID fields follow)
putF(out, ""); // fxChainIdentity (empty)
return out;
}
static void testHardenedCursorRejectsHostileLengths() {
// A 200-digit length run: pre-hardening the accumulate wrapped std::size_t silently
// (the digit cap + overflow guard now reject it outright).
CHECK(!parseFingerprint("rsprov1" + std::string(200, '9') + ":x").has_value());
// A SIZE_MAX-magnitude length: the additive bounds check `start + len > size` could
// itself wrap and pass; the subtraction-first form rejects.
CHECK(!parseFingerprint("rsprov118446744073709551615:x").has_value());
// One past SIZE_MAX: the per-digit overflow guard fires during the accumulate.
CHECK(!parseFingerprint("rsprov118446744073709551616:x").has_value());
}
static void testHugeGuidCountRejectedBeforeReserve() {
// A GUID count astronomically larger than the wire could hold must return nullopt
// WITHOUT reaching trackGuids.reserve(count) — pre-fix this drove reserve(10^16)
// into std::length_error / bad_alloc thrown through the shell.
CHECK(!parseFingerprint(forgedFingerprint("9999999999999999")).has_value());
// A count merely past the wire-size sanity bound (each GUID field needs >= 2 wire
// bytes) is provably bogus and rejected before the field loop.
CHECK(!parseFingerprint(forgedFingerprint("1000")).has_value());
// A digit run past 20 fails the count parser's cap.
CHECK(!parseFingerprint(forgedFingerprint(std::string(25, '9'))).has_value());
// Sanity (non-vacuous forgery): the honest zero-count version of the same forged
// shape parses fine — the rejections above are the count's doing, not the shape's.
CHECK(parseFingerprint(forgedFingerprint("0")).has_value());
}
// --- recorded-recipe model round-trips through the Sample JSON ----------------
// The fingerprint rides in Provenance.fxChainSnapshot (one string), which M1's
// BankIndex JSON already round-trips. Prove a real recipe survives that path intact.
@@ -296,6 +350,8 @@ int main() {
testFxChainIdentityInjectionProof();
testCombineChainIdentities();
testMalformedFingerprint();
testHardenedCursorRejectsHostileLengths();
testHugeGuidCountRejectedBeforeReserve();
testRecipeThroughSampleJson();
testDetectParentPositive();
testDetectParentMultipleSameParent();