Repository navigation
Hold every holds-check to an edited claim or the test that kills its drop (#344) - #345
Merged
Merged
Conversation
…drop (#344) The cut sweep in TestReadsAllocateOnlyWhatTheyHold sees a check that holds a claim to the bytes after it only by what dropping it costs at a cut, and a cut keeps its writer's claim. Where that claim stays small, a price or read that skipped the check costs less than the sweep's bound at the cuts that hold it, and the sweep passes with the check gone. #332 found this for DpswSketch and #340 tested that one. This finds the rest. The audit. The roster is the code's: every call of PayloadReader.RequireHolds in the library (a parse with comments and strings blanked, each with the method enclosing it), and the checks inside PayloadReader that hold a claimed run length to the bytes remaining (every RequireAhead a claimed length reaches, and the two comparisons with Remaining). Each was dropped in turn through scripts/mutate.sh (MUTATION_JOBS=2, Release), against the whole suite at 6e2e5ec. Two of the checks are a method's whole expression body, where deleting the statement does not compile, so those were dropped by giving the method an empty body. Lines are at 6e2e5ec. check method holds drop CountMinSketch:616 CheckMatrix counters killed, 6 tests CuckooBloomFilter:663 RequireTable words of table and state killed ({ }), 4 DpswSketch:839 ReadPayload substreams killed, 2 DpswSketch:897 Price substreams killed, 2 Grafite:1221 ReadLowWords words of low bits killed, 4 Grafite:1240 ReadHighWords words of high bits killed, 3 HeavyKeeper:686 ReadElementCount elements SURVIVED HeavyKeeper:742 CheckShape buckets killed, 4 HyperLogLogPlus:777 ReadPayload stored hashes killed, 2 HyperLogLogPlus:858 Price stored hashes killed, 3 InfiniFilter:687 ReadPayload words killed, 5 InfiniFilter:727 Price words killed, 6 MementoFilter:836 ReadPayload words killed, 3 MementoFilter:920 Price words killed, 4 MinHashSignature:169 ReadPayload values killed, 2 MinHashSignature:205 Price values killed, 3 PersistenceFormat:1460 ReadBuckets64 arrays of buckets killed, 6 PersistenceFormat:1510 PriceBuckets64 arrays of buckets killed, 11 PrivateCountMinSketch:561 CheckShape counters killed, 7 ScalableBloomFilter:677 ReadFilterCount contained filters killed, 3 SetSketch:839 ReadPayload registers killed, 3 SetSketch:941 Price registers killed, 3 SublimeCountMinSketch:1002 RequireCounts counters killed ({ }), 3 SublimeCountMinSketch:1166 CheckShape counters SURVIVED ThetaSketch:474 ReadPayload values killed, 1 ThetaSketch:535 Price values killed, 2 TopK:408 ReadElementCount elements SURVIVED TupleSketch:696 ReadPayload keys killed, 1 TupleSketch:764 Price keys killed, 2 VarOpt:905 ReadPayload items killed, 1 VarOpt:988 Price items killed, 2 PayloadBuffer:723 ReadBytes a streamed run killed, 2 PayloadBuffer:776 ReadHeldRunLength a run's length killed, 5 PayloadBuffer:811 BeginRun a run killed, 40 PayloadBuffer:862 ReadBlock a streamed piece killed, 1 PayloadBuffer:923 Skip a run skipped killed, 1 PayloadBuffer:974 Slice a nested envelope SURVIVED PayloadBuffer:1038 RequireHolds its own comparison killed, 75 A checked price and reader that share one helper (CheckMatrix, ReadElementCount and the like) are one row: dropping it drops both. CountSketch, InvertibleBloomLookupTable, DDSketch and QuotientFilter call no RequireHolds; their runs are held by BeginRun and Skip, rows above. A second run, keeping each failing test's name, found which tests kill each killed drop; that is where HeldElsewhere's names come from. The four survivors: - HeavyKeeper's and TopK's element counts. A count is at most k, a few hundred and a thousand in their budget samples, and the only cuts that hold it come after the buckets or the nested sketch, where the bound has room for a heap of k elements on top of them. - Sublime's shape check holds the current rows to the bytes after the shape, and every block, the current rows' among them, is held again where it begins, so every cut it refuses is refused at the current rows at the latest, having read only blocks it holds. It is reachable, though: a depth edited past what the current rows can have is still held by the records' blocks, and unchecked, the price prices them at that depth. Probed on the budget sample with depth 64: priced 0 and stopped at offset 32 with the check, 140,848 and offset 32,838 without it, the read allocating 82,768 against 118,024. - Slice's check is what holds a nested envelope's length to the run it is stored in, in its outer price, and no cut can make the two disagree. Without it a held payload's slice throws ArgumentOutOfRangeException where every refusal is InvalidDataException. The tests. - TestAClaimNoCutCanSeeIsRefusedBeforeItIsPricedOrAllocatedWhenEdited, over EditedClaims: HeavyKeeper's and TopK's element counts, with k raised to the heap's limit so the count is held only by the bytes, and Sublime's depth. Each claim is edited, as #340's was, in the budget sample the sweep cuts, its checksum resealed, to one past what the bytes after it hold and to the largest the field can make. Its price stops exactly where the check refuses, from FORMAT.md, having priced exactly what a cut just before the field does. Its read is refused for what it lacks under its own price, the default budget and none, tightest first, stopping at the first problem so that a reader charging billions before its check is not then read unbudgeted. It allocates no more than the cut's read plus the envelope's difference and 1 KiB. Measured, each allocated 56 to 696 bytes less than that cut's read. The most the bytes hold is the control: its price walks past the check and prices more, and its read allocates past that bound, by 15,472 (Sublime), 76,328 (HeavyKeeper) and 154,848 (TopK) bytes. - TestANestedEnvelopeClaimingMoreThanItsRunIsRefusedAsData: for TopK and ScalableBloomFilter, the structures whose price prices a nested one (from the IL), the nested envelope's length edited to the least its run cannot hold. The price and the read under no budget and the default refuse it as data. Four bytes fewer, which the run holds by taking the checksum's bytes, is not refused by the price. - TestEveryHoldsCheckIsHeldByAnEditedClaimOrNamesTheTestThatHoldsIt: the roster is the library's IL, every call of RequireHolds with the string pushed before it, which is what it counts. It agrees with the parse above. Every check must be an edited claim or name, in HeldElsewhere, the test that kills its drop, which must be a test. An entry the IL no longer has fails, and the edited claims' checks must be found. Theta's, Tuple's and VarOpt's checks, price and reader, are killed by the cut sweep alone, because their budget samples' claims are large; their entries say so, since a sample that came to hold few would leave them unseen. Watched failing, each drop applied by hand: - HeavyKeeper:686: the price priced 116,536 bytes past a cut before the count and stopped at 91,961, not 81,961; at the largest claim, 257,698,046,896 bytes. - TopK:408: 232,496 bytes, stopped at 128,827, not 108,827. - SublimeCountMinSketch:1166: depth 10 priced 22,048 bytes and stopped at 5,190, not 32; depth 710 stopped at 66. - PayloadBuffer:974: both nesting structures' prices threw ArgumentOutOfRangeException, at nested lengths of 108,798 (TopK) and 2,558 (ScalableBloomFilter). Mutation of the new tests, through scripts/mutate.sh, against the whole suite and then against the three new tests alone: mutant whole suite new tests alone E1 HeavyKeeper elements dropped killed, 2 killed, 2 E2 TopK elements dropped killed, 2 killed, 2 E3 Sublime shape dropped killed, 2 killed, 2 E4 Slice dropped killed, 1 killed, 1 E5 HeavyKeeper elements at 11 bytes each killed, 1 killed, 1 E6 HeavyKeeper elements at 13 bytes each killed, 1 killed, 1 E7 TopK elements at 11 bytes each killed, 1 killed, 1 E8 TopK elements at 13 bytes each killed, 1 killed, 1 E9 Sublime shape at 7 bytes each killed, 2 killed, 1 E10 Sublime shape at 9 bytes each killed, 47 killed, 1 E11 Slice a byte laxer killed, 1 killed, 1 E12 Slice a byte stricter killed, 19 killed, 2 E13 HeavyKeeper price checks after pricing killed, 3 killed, 2 E14 TopK price checks after pricing killed, 3 killed, 2 E15 HeavyKeeper read checks after charging killed, 4 killed, 2 E16 TopK read checks after charging killed, 3 killed, 2 R1 a check added to BloomFilter.Price killed, 1 killed, 1 R2 the TopK edited claim removed killed, 1 killed, 1 R3 a stale entry in HeldElsewhere killed, 1 killed, 1 R4 a nesting structure left out of the test killed, 1 killed, 1 The laxer bounds (E5, E7, E9, E11) let one claim past the bytes through, and the stricter ones (E6, E8, E10, E12) refuse the most the bytes hold, which the controls see. E13 to E16 move a check after its price or its charge in one of the price and the reader, keeping it in the other; each puts a RequireHolds where there was none, so the roster fails on it too. R1 adds a RequireHolds that refuses nothing. Probed under the segments GC macOS uses (DOTNET_GCName=libclrgc.so, DOTNET_PROCESSOR_COUNT=3): the new tests and #340's pass. Test-only, and no CHANGELOG entry, as for earlier test-only fixes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KK3MspqAcczxT45xCfLgyx
… claim (#344) The note the Decision asks for, in Persistence after the bullet on what no cut can show: why the cut sweep cannot see such a check, which checks the audit found it could not see, how the edited-claim test holds them and to what bound, and the roster taken from the library's IL that makes a new check say what holds it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KK3MspqAcczxT45xCfLgyx
This was referenced Nov 6, 2026
mattlorimor
added a commit
that referenced
this pull request
Nov 6, 2026
Brings in #345 and #348 (2a4dfbc). CHANGELOG merged without conflict. #345's TestANestedEnvelopeClaimingMoreThanItsRunIsRefusedAsData fails on this merge, as expected: it pins Slice's refusal, and the price now refuses first, in RequireHeld. The commit after this holds it to that. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KK3MspqAcczxT45xCfLgyx
mattlorimor
added a commit
that referenced
this pull request
Nov 6, 2026
TestANestedEnvelopeClaimingMoreThanItsRunIsRefusedAsData, from #345, expected the price to refuse a nested envelope claiming more than its run with Slice's message ("while reading N more at offset 14"). On the merge of master it failed for TopK and ScalableBloomFilter, refused instead by RequireHeld, which since #347 holds a nested envelope's length to what its run holds before anything is priced. RequireHeld counts the checksum as well, so the boundary the test held moved. A claim of the run less its header was #345's control, held and then refused by the nested read for its checksum; it is now refused by the price, as is any claim past the writer's. So the claims are now the coarsest pair at the new boundary: the writer's own, which the run holds with its checksum and which has to read, and a byte more, which the price and the read both have to refuse with RequireHeld's message, and no other. The summary and remarks say so; they described the refusal as coming from Slice. Mutation through scripts/mutate.sh, against this test alone: N1 nested price: RequireHeld dropped killed (1) N3 nested price: one byte looser killed (1) N4 nested price: one byte stricter killed (1) R1 read: RequireHeld dropped killed (1) H4 RequireHeld: checksum not counted killed (1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KK3MspqAcczxT45xCfLgyx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #344.
The cut sweep can't see a check that holds a count to the bytes after it, when the writer keeps that count small (#332). This PR audits every such check in the library, adds edited-claim tests for the four nothing caught, and adds a roster test so a new check can't go untested. It's test-only, plus a TESTING.md note, and follows the coordinator's plan on #344, approved by Matt.
The audit
The roster comes from the code: a parse of the library with comments and strings blanked. It found 31
RequireHoldscalls, plus the 7 checks insidePayloadReaderthat hold a claimed length to the bytes left. Each check was dropped throughscripts/mutate.shand run against the whole suite at6e2e5ec.ccd3f49's message.CountSketch, IBLT,DDSketchandQuotientFiltercall noRequireHolds. Their runs are held byBeginRunandSkip, and both drops are killed.The four survivors:
HeavyKeeper:686TopK:408SublimeCountMinSketch:1166PayloadBuffer:974(Slice)ArgumentOutOfRangeExceptionin place ofInvalidDataException.None of the four is unreachable, so none is proposed for removal.
The tests
All are in
TestReadsAllocateOnlyWhatTheyHold.cs.TestAClaimNoCutCanSeeIsRefusedBeforeItIsPricedOrAllocatedWhenEdited: three edited-claim cases, in Test DpswSketch's substream price check with an edited count (#332) #340's pattern. They are HeavyKeeper's and TopK's element counts, with k raised to the heap's limit, and Sublime's depth.envelope + Fixedbound was too loose here, since Sublime's control fell inside 16 KiB.TestANestedEnvelopeClaimingMoreThanItsRunIsRefusedAsData: the nested envelope's length is edited to the least its run can't hold. Its roster is the structures whose price callsPriceNested, taken from IL:TopKandScalableBloomFilter.TestEveryHoldsCheckIsHeldByAnEditedClaimOrNamesTheTestThatHoldsIt: the roster is the library's IL, everyRequireHoldscall. Each check must be one of the edited claims, or be listed inHeldElsewherewith the test that kills its drop, verified to be a real test.A sweep over FORMAT.md or
StructureIdwasn't feasible. FORMAT.md's layouts are prose code blocks, with version conditions and nested groups that no test parses.StructureIddoesn't name checks. The IL is the roster that can be derived.Watched failing, each drop applied by hand:
Slice:ArgumentOutOfRangeExceptionfrom both nesting structures.Mutation of the new tests: 20 mutants, all killed, against the whole suite and by the three new tests alone. They are:
BloomFilter.Price, the TopK case removed, a stale entry added, and a nesting structure left out.TESTING.md
A note under Persistence: the cut sweep can't see a check on a count the writer keeps small, so such a check needs an edited claim. It also says what this audit found.
Left for later
An envelope's claimed length is held only by the read budget. A held 128,827-byte TopK payload whose header claims 2^30 bytes allocates 1,073,743,704 bytes with no budget. Under the default budget it's refused for its budget, not as malformed. Nested envelopes behave the same way. Matt has decided that held lengths should be checked against the bytes held, and a separate round will do it.
Verification
-warnaserror: exit 0, 0 warnings.🤖 Generated with Claude Code
https://claude.ai/code/session_01KK3MspqAcczxT45xCfLgyx