before: 32-bit correctness audit with a wasm32 execution leg - #30
Merged
Conversation
plaidfinch
force-pushed
the
w2/wasm32-audit
branch
from
August 19, 2026 14:55
6750598 to
578b583
Compare
…nned red just wasm-check proves before compiles for a 32-bit target; nothing in the tree executed there, so 32-bit-only seams could pass every native suite while misbehaving on wasm32. This leg is the instrument: a detached guest/harness workspace (the fuzzfit idiom) whose guest drives the public byte decode doors at the exact boundary sizes under a real 32-bit usize, and whose tests pin each boundary's behavior as found — red first, per instrument discipline; the cures flip the same pins to correct-value assertions. The pinned bad baselines, verified by execution on wasm32: - 67108864 bytes (exactly 2^29 bits): a valid Version encoding is SILENTLY rejected as Truncated — bitvec's element guard admits the slice and the span encoding's len << 3 shifts the length out of a 32-bit usize, constructing an empty view. Silent wrong result, the worst class. (The recorded finding expected a panic here; the panic starts one byte up.) - 67108865 bytes: bitvec's element guard panics — a valid encoding refused by panic. - 536870912 bytes (512 MiB): panic; bitvec's cap masks the Bits::len() usize multiply overflow that begins at this size. - rank exp = 2^32 - 8: panic inside dashu — from_be_bytes sizes its buffer from input bytes (leading zeros included) and overruns the backend's 32-bit MAX_CAPACITY even though the numerator value fits. A newly found seam below the recorded Shl seam, partially masking it. - rank exp = 2^32: panic at Base's Shl<u64> usize::try_from, the recorded seam, at the smallest honest trigger (~604 MB: the format counts fraction depth from bits actually read, so no smaller stream reaches it). Green adjacency witnesses ride beside each red pin (67108863 bytes; exp = 2^32 - 32) so each flip is attributable to its seam, plus a small round-trip/typed-reject liveness pin so a dead leg cannot read green. The guest builds with overflow-checks on: the audited failure class includes silent release wraps, and the checks turn exactly those into observable traps. Wired into the gate's wasm stream (measured: ~3 s of test wall time, all eight pins). Claude-Session: https://claude.ai/code/session_01H26YwsygLvogog1aBm8Y2N
The four byte decode doors (Party, Version, Clock, Span) and the borsh ReaderCursor built one borrowed bit view over the whole input buffer. bitvec's span encoding steals three usize bits for the head-bit offset, so on a 32-bit target the view caps at usize::MAX >> 3 bits: at exactly 2^29 bits (a valid 64 MiB encoding) the length field wraps and the view constructs EMPTY — the validator walks zero bits and silently rejects a valid input as Truncated — and one byte past that, view construction panics. The ruled contract says a valid encoding that fits the target's memory decodes correctly, so the seam is engineered around, not error-wrapped: - DsiCursor now runs on u64 positions over raw (body, masked-tail) parts, with byte-door constructors (over_bytes, over_bytes_live) and a u64 end-position read; the usize conversions that remain are checked, each carrying the derivation for why an honest walk cannot trip it. - The grammar walks grew byte-door entries (parse_id_bytes, validate_prefix_bytes, validate_dominating_bytes over a raw meet) returning u64 ends; the marker-padding judge got a raw-byte form and the slice form, now caller-less, dissolves into it. - The borsh ReaderCursor buffers at u64 positions, windows gamma codes over its raw bytes, and finish() now hands back the field's canonical marker-padded bytes, adopted via from_canonical with no bitvec round-trip (one copy and one re-seal fewer than before). - Bits::from_canonical asserts the storable bound (bit count fits usize): on 32-bit that is 512 MiB of buffer, where usize bit positions themselves run out — the documented seam the len() fix addresses next — and on 64-bit the bound is 2^61 bytes, dead by allocation. Bits' deref documents the borrowed view's remaining cap: decoded values past 64 MiB on 32-bit carry a correct byte surface (equality, hashing, canonical bytes, re-encode), while their walk surface still reads through the one bitvec borrow and fails loudly there; widening it means a crate-owned view type, proposed separately. The wasm32 pins flip: both view-cap pins (64 MiB silent-empty, 64 MiB + 1 panic) now read the exact decoded bit lengths, and the 512 MiB pin's trap moved from bitvec's cap to the len() multiply it was masking — re-pinned red at the unmasked seam, cured next. Native suite: 686/686 unchanged. Claude-Session: https://claude.ai/code/session_01H26YwsygLvogog1aBm8Y2N
bytes.len() * 8 is a usize multiply: on a 32-bit target it overflows at exactly 512 MiB of buffer (2^32 bits) — a debug panic, and in release a silent wrap that is coincidentally correct at exactly 2^29 bytes and wrong (a length 2^32 short) for anything larger. The live length itself still fits usize at that boundary, because the marker spends at least one of the buffer's bits: 2^32 - 1 - trailing_zeros. So the arithmetic runs at u64 width and converts back checked, with the doc carrying the derivation for why the conversion cannot fail on a constructed stream (from_canonical asserts the storable bound; freeze's build buffer is bounded far below it). The wasm32 pin flips: a valid 512 MiB version encoding — the largest stored stream a 32-bit usize can denominate bit positions for — now decodes on wasm32 with its exact live bit length, one marker byte shy of usize::MAX. Claude-Session: https://claude.ai/code/session_01H26YwsygLvogog1aBm8Y2N
The decoder rebuilt the numerator as (integral << exp) | (groups >> pad). Two 32-bit seams lived in that line, both reachable from valid ~604 MB inputs the address space holds: Base's Shl<u64> converts the shift amount to the backend's usize, which fails at exp >= 2^32; and below it, dashu's from_be_bytes sizes its buffer from the input's byte count — leading zero bytes included — so 536870911 fraction bytes overran the backend's 32-bit MAX_CAPACITY by one word even when the numerator's value fits. The cure assembles bytes: num · 2^pad = integral · 2^(8·groups) + G, so the numerator is the concatenation of the integral's minimal big-endian bytes with the fraction groups, shifted right by the sub-byte pad (< 8, in usize range on every target), leading zero bytes stripped so the backend's buffer pays for value, not for zeros. No value-width shift exists on the decode path at all. The encode side gets the dual guard: the biased integral is decided by width (exp >= bits(num) means zero) before any shift is materialized, so re-encoding a fraction-heavy rank works at the same scales its decode admits. Base's u64 shift forms are re-derived honestly: Shr<u64> is now total (an amount at or past the value's width yields zero, so amounts past usize clamp, value-preserving); Shl<u64> keeps its checked conversion with the corrected rationale — on 32-bit it fires exactly where the shifted result exceeds the backend's representable width, the same values the backend's own capacity assert bounds, so both name an unrepresentable result loudly (the earlier could-not-be-allocated claim was false: a 512 MiB numerator allocates fine in a 4 GiB space). Base::to_be_bytes lands as from_be_bytes' width-metered dual. The rank Add/checked_sub alignment shifts and sum_ranks' accumulator shifts carry their reachability derivations inline: honest exponents stay orders of magnitude below every remaining bound. The wasm32 pins flip: both rank boundary pins (exp = 2^32 - 8 at the backend's byte-capacity seam, exp = 2^32 at the shift seam) now decode and order exactly; the adjacency pin below the backend cap stays green. Native rank suites: 40/40 unchanged. Claude-Session: https://claude.ai/code/session_01H26YwsygLvogog1aBm8Y2N
Test docs and driver prose narrated each pin's prior red state (the baseline it used to trap on, what cured it). Provenance lives in git: each doc now states the boundary the pin guards and the failure mode it forecloses, as counterfactuals about the present code, with the pins file's header naming the red-first discipline once. The trap-outcome plumbing stays (a future seam pins red through it); its unused constant goes. Claude-Session: https://claude.ai/code/session_01H26YwsygLvogog1aBm8Y2N
The audit's ledger, written where the code is, each site carrying the invariant that keeps its arithmetic exact on 32-bit targets: - PackedBuilder's committed prefix states the width bound its four byte-to-bit position sites (len, patch_bit, read_bits, bit_at) rest on: build-side outputs are emitter-bounded over storage-capped operands, orders of magnitude below a usize wrap. - BitStack::len states the same for its word widening: walk transients price depth in bits, and depth is bounded by the walked stream's capped length. - The per-bit gamma decoder's prefix-overflow reject states what its arm names on each target: a value past the big-integer backend's representable width (32-bit), an unallocatable input (64-bit). - The accumulator-shift call sites in the integral and web folds state why the shifted entry points' documented 32-bit panic (digit position past usize, shift 2^37) is unreachable from them: every shift is bounded by stored-stream content under 2^32. - Packed::as_bits states why the instrument surface may keep its borrowed view where the decode doors walk raw bytes. Two seams in this class are genuine 32-bit defects and get cures, not comments: Party::encoded_bits reads the stored form's O(1) length instead of constructing a borrowed view (which caps below the stored sizes the doors admit), and Clock::encoded_bits sums its components at u64 width with a checked conversion — each component's length fits usize on 32-bit, but their byte-rounded sum need not, and a wrap there was a silently wrong public answer. Claude-Session: https://claude.ai/code/session_01H26YwsygLvogog1aBm8Y2N
Each flagged doc's first paragraph shortens to its one-sentence summary, with the detail moved below the paragraph break; no content changes. Claude-Session: https://claude.ai/code/session_01H26YwsygLvogog1aBm8Y2N
…-door cfgs Three gate verdicts, three cures: - The rank encode's integral extraction returns to (num >> exp) + 1 whole: Shr<u64> is total (it clamps past the numerator's width), so the width-guard arm was a second spelling of the same boundary — and one that skipped the shift's width-scale limb record, reading the amp board's rank_encode limb floors from below (the floor was right: the walk genuinely reads the numerator; the record rides the shift). - validate_bits' doc names validate_prefix_bytes, the production entry beside it. - The byte-door companions gated to their one consumer's exact cfg (feature = borsh, not any(test, ...)): the default-feature test build compiles cfg(test) items without the borsh module that uses them, and clippy-default exists precisely to catch that dangling surface. Claude-Session: https://claude.ai/code/session_01H26YwsygLvogog1aBm8Y2N
The rank decoder's numerator assembly moved the operation's allocation profile, and the worst-case ranking pin caught the flip. Parent-measured attribution (worktree at the byte-assembly commit's parent, same board, same scales): - freeze-parade (fraction-heavy): 3.00 -> 2.0 B/input-byte at the default scale, 3.67 -> 2.7 at acceptance — the dominant improvement; the assembly never materializes a shifted integral, so the family that paid that transient stops paying it. - plateau-puncture: 2.74 -> 2.6 at default — slightly improved, and now the default-scale worst by overtake. - bigroot (integral-heavy): at most 3.34 -> 3.7 at acceptance — a small constant rise, now the acceptance-scale worst: the byte image pays the integral's own bytes where the shift spelling folded them into its shifted buffer. Exponents hold at 1.00 and every acceptance ceiling stays green (2096/0), so the movement is a constant-factor trade inside the linear envelope, priced against the decode-path correctness the assembly buys at every scale. Claude-Session: https://claude.ai/code/session_01H26YwsygLvogog1aBm8Y2N
The execution leg so far pinned the byte decode doors; five boundaries in the walk, emit, and rank-arithmetic surfaces had no committed baseline. Each now lands as-found (instruments before cures: every later reshaping flips a committed red), with an adjacency witness just inside the boundary so a failure is attributable to its seam: - Composite doors (Ranked byte + borsh, Span borsh): the rank re-derivation and dominance re-walk read the version through the borrowed bit view, so valid keys trap from 67108864 bytes of version component; both view-cap genres pinned, green at 67108863. - Semantic walks (Version PartialOrd, join): stored values the doors admit exactly cannot be compared or joined past the view cap; both genres pinned beside green witnesses, on values the decode pins already prove storable. - Emitter output: two operands under every per-operand bound whose join concatenates trap when the finished stream reaches 67108864 whole bytes (the builder's byte-backed buffer adopts into a bitvec whose length encoding caps at usize::MAX >> 3 bits); the largest emittable output in the family, 536870903 live bits, is the witness. - Rank numerator at the backend word cap: a valid ~604 MB rank whose numerator is 2^32 - 24 value bits traps in the backend's buffer-fill assert though value and working set fit the address space; a numerator of exactly 2^32 - 32 bits (the cap itself) decodes green. - Rank alignment gap: Add/checked_sub trap at an exponent gap of exactly 2^32 (the shift amount's usize conversion), and Add also traps just below the gap boundary when the aligned numerator outgrows the backend (gap 2^32 - 1, rank 1/2); gap 2^32 - 128 with a 64-bit numerator is exact on both arms. The guest gains the two-leaf skyline and integral-rank-stream syntheses these shapes need (everything synthesized in-guest as before), the borsh feature for the streaming doors, and a two-argument export form; the harness gains call2 and re-exports Trap so pins can name the exact trap they assert. Claude-Session: https://claude.ai/code/session_01H26YwsygLvogog1aBm8Y2N
The gate's doclint caps a doc comment's first sentence at 220 rendered characters; fourteen of the new pins' summaries ran long. Each now opens with the short claim and carries its qualifiers in the body, unchanged in content. Claude-Session: https://claude.ai/code/session_01H26YwsygLvogog1aBm8Y2N
The walk surface reads stored streams and door buffers through BitsView — borrowed bytes beside a u64 live bit length — so every semantic walk (comparisons, joins' input side, admission walks, rank folds, the id operations) is exact at every storable size on every target. Bits's Deref to a borrowed bit slice, whose 32-bit length encoding capped walks at 64 MiB, is deleted rather than widened; Bits::live() is the explicit accessor in its place. Views are byte-aligned by construction, so sub-stream ranges travel as explicit (view, start, end) bit positions at the copy and parse seams. The parallel byte-form duals existed only because the slice form capped; each collapses to one form under the plain name: require_marker_padding_bytes -> require_marker_padding, parse_id_bytes -> parse_id, validate_prefix_bytes -> validate_prefix, validate_dominating_bytes -> validate_dominating_from, decode_int_window_bytes -> decode_int_window, DsiCursor::over_bytes/over_bytes_live -> DsiCursor::new (BitsView::whole / BitsView::new carry the byte doors' forms), LeafCursor::open_bytes -> LeafCursor::open. codec::byte_view dissolves into BitsView::body_tail — views are byte-aligned by type, so its mid-byte None arm has no representable input. The never-a-borrowed-bit-view door comments are excised; prose is restated against the view. The wasm32 pins flip in the same commit, per the pins file's own red-first discipline: the composite-door pins (Ranked byte + borsh, borsh Span) and the comparison pins at the 2^29-bit boundary go from trap-assertions to correct-value assertions, with every adjacency witness unchanged, and four new pins spot-check exactness deep in the storable range (comparison at the 512 MiB storage bound; the composite doors at 128-256 MiB components). The covered-join pins stay pinned as traps with their mechanism re-attributed: the walk is exact at those sizes, and the residual trap is the emitters' build-buffer length encoding at the freeze seam — the same boundary the emitter-output pin holds, cured by the crate-owned build buffer, not by this change. The rank-cap and gap-boundary pins stay as found. The skyline decode wrap now allocates its copy once, exactly sized plus the marker bit (the meter envelopes tighten in the next commit). The mutants roster's one line-pinned recode entry re-aims 457:38 -> 468:38 (same width-tie comparison, same equivalence argument; the counts re-verify under just mutants-list). Claude-Session: https://claude.ai/code/session_01H26YwsygLvogog1aBm8Y2N
…ed copy The decode wrap allocates its copy once, exactly sized plus the marker bit, so the peak heap over each decode scenario drops to the copy itself. Parent-measured on the same idle machine, old -> new (bytes, per scenario): dense 98304 -> 49152, cliff 3072 -> 1793, wide tooth 196608 -> 100049, alt spine 98304 -> 49152; hugeleaf is unmoved at 66752 (its peak is the wide payload arithmetic, not the copy). The ceilings re-pin at the same proportional headroom: dense/alt spine 122880 -> 61440, cliff 3840 -> 2250, wide tooth 245760 -> 125100; hugeleaf keeps 83440. Limb, scan, and segment columns are unmoved in every scenario, so those pins and the limb floors stand. Claude-Session: https://claude.ai/code/session_01H26YwsygLvogog1aBm8Y2N
The internal-docs build denies the bare link as ambiguous against the encode submodule; name the function form explicitly. Claude-Session: https://claude.ai/code/session_01H26YwsygLvogog1aBm8Y2N
The build side's one remaining 32-bit boundary was the bit vector behind the emitters: its length encoding caps at usize::MAX >> 3 bits, so an emitting operation whose output crossed 2^29 bits (a covered join of a 64 MiB operand, a two-operand join concatenating past it) trapped at the freeze seam on wasm32 while every walk and door was already exact. BitsBuf is the one build buffer now: bytes beside a u64 live length, under two representation invariants every mutation maintains — the byte vector holds exactly the live bits' bytes, and dead bits in the final partial byte are zero (truncation masks the new final byte in O(1)). The byte image is therefore a function of the bit content alone: Eq is one memcmp, sealing appends only the marker, and the freeze seam adopts the allocation whole. PackedBuilder keeps its staged-register internals and word-parallel moves but runs its positions at u64 and hands its finished bytes straight to BitsBuf — no length-encoding conversion binds the hand-off on any target. The storable bound (usize-denominated bit positions in the frozen form) is asserted loudly at the freeze door, matching the decode door's existing contract. The gamma word path emits through BitsBuf::push_bits; bitvec leaves the production dependency graph (it stays a dev-dependency as emit_probe's external baseline). The build-history proptest family pins the invariants as a family: arbitrary interleavings of pushes, word appends, view copies, and truncations (byte-aligned, mid-byte, to-empty, multi-byte sheds) leave the buffer byte-identical to a clean rebuild of the surviving content at every intermediate state, freeze to the identical canonical spelling, and keep Eq and the canonical hash agreeing with bit-level equality; a second leg pins spelling injectivity across distinct contents. A transient no-zeroing mutant of truncate fails both legs (adequacy verified before landing; the mutant is not committed). wasm32-pins: the three output-side trap pins flip green in this same commit, per the pins file's own discipline — the covered joins at and past 2^29 bits now assert byte-identical emission, and the two-operand emit at the coordinate asserts its exact 536870905-bit output. The boundary prose across the pins re-denominates to the straddle coordinate (the bit count where a 32-bit length encoding would bind), which every surface is now exact across. The rank pins stay as found for the later phase that owns them. All 36 wasm32 pins pass; the resource-envelope suite passes with every pinned ceiling and floor untouched. Claude-Session: https://claude.ai/code/session_01H26YwsygLvogog1aBm8Y2N
…eter-side Stored streams, cursors, walks, and depths now speak one width: u64, the denomination the build buffer and the view already carry. Bits::len returns the u64 live length directly; the freeze and decode doors adopt any buffer whose padding validates, imposing no bound of their own; the BitCursor trait's position and read_unary are u64 (the separate u64 position read dissolves into them); BitStack heights, unary runs, tree depths, flip levels, code lengths, and open-range counts widen across the walk surface. On 32-bit targets streams are therefore bounded only by allocatable memory: no usize-derived structural cap survives on the stored, walk, or build path. Byte indexes stay usize (they index allocated buffers), and each remaining u64-to-usize seam carries a checked conversion with its rationale inline. Where a gamma mantissa names a value wider than the big-integer backend can hold on the target, both decode paths now reject with the value's own genre (NotCanonical) at the same width, per-bit and word-parallel alike. Owner ruling (Finch, direct): encoded_bits on Party, Version, and Clock leaves the unconditional public API — every consumer is a meter, a coverage suite, a fuel denominator, or a boundary pin — and re-homes behind the existing meter feature (the instrument surface), returning u64. Consumers routed: laws now implies meter (the representational laws read the observation), the fuzzfit harness and wasm32-pins guest enable it explicitly, and rumors' dev-dependency lights it for the conservation suite, whose invariant is unchanged. The surface roster's rows are unchanged: the extractor scans source text, so the gated fns keep their named rows. The recode mutant exclusion's line pin moves with its expression (rustfmt reflowed the file); same mutant, same rationale, re-pinned. Meter parity: all 300 MEASURED rows are byte-identical to the parent commit's readings.
…rdinate With the doors and walks u64 end to end, the pins hold the new upward reach and name what remains. The decode, comparison, emit, and rank surfaces cross the 2^29-byte coordinate — where a 32-bit usize runs out of bit positions — green: a 512 MiB + 1 byte version decodes with its exact bit length and compares both ways, a join emits a finished stream past 2^32 live bits, and a 768 MiB version decodes deep in the memory-bounded range. A new ladder synthesis and export drive the rank fold on numerators wider than any 32-bit quantity (2684354560 bits, green), reachable only through depth weighting, never through a door. Where the 4 GiB address space runs out, the terminal is pinned as found: allocation-failure aborts — loud, never a silent wrong value — on the ~1 GiB decode, the past-coordinate composite key, and the rank fold at the big-integer backend's capacity coordinate. Probe backtraces attribute each to accumulator buffer growth, so the backend's word cap (2^32 - 32 bits) is unreachable through the version doors and the rank fold on this target: a W-bit value or numerator keeps at least 2W bits of stream alive underneath it, and that working set exhausts memory just below the capacity. The rank wire door, which assembles its numerator from bytes with no fold transients, is where the capacity is reachable and stays pinned. A leaner working set — not a wider denomination — is what would move these terminals outward.
The Magnitude seam lends a whole UBig, and on a 32-bit target a UBig magnitude caps out near usize::MAX bits — so operands and totals wider than the backend can hold had no way in or out of the accumulator. Two entry points and one readout close that: add_limbs_shl/sub_limbs_shl stream an operand as little-endian 64-bit limbs at any shift (the same apply_limbs kernel and cost model as the wide entries, pinned touch-identical by limb_stream_matches_the_wide_entry), and sign_limbs reads the held value back as minimal limbs through the same carry pass as sign_magnitude (read_magnitude's digit collection is now the shared read_digits core). reserve_digits pre-sizes the digit buffer so a caller folding operands of known final scale pays one exact allocation instead of a growth-doubling transient — near a 32-bit target's memory ceiling that doubling is the difference between fitting and failing. The claims roster and crate-page table gain the new rows; the differential streams gain limb-stream and reservation operations; the metered suite pins the streaming entries' exact touch counts and sign_limbs' carry pass; witnesses pin the limb readout's conversion corners in both tiers and the reservation's value-neutrality. The mutants exclusion roster's read_magnitude pattern follows the moved high-part drains into read_digits, counts unchanged.
…rgets The rank numerator becomes two-arm storage (version/rank/num.rs): the backend magnitude wherever the backend can represent the value, a raw little-endian limb vector past that — canonical by construction (wide iff the width exceeds the backend's capacity), so structural equality and hashing stay value equality and the arm choice is pure routing between two exact implementations. On 64-bit targets the seam sits at 2^64 - 64 bits and never engages; on a 32-bit target it sits at 2^32 - 32 bits, inside the honestly reachable range of the rank wire door. What the wide arm totalizes, each previously a loud backend panic on wasm32 at inputs that fit the address space: - decode: both wire paths to a past-capacity numerator — the fraction image (~604 MB of input) and the integral mantissa (~512 MiB), including the biased transient at exactly the capacity — assemble limbs directly, never a backend value; - Add and checked_sub: a routing predicate keeps the backend's shift-and-combine wherever both aligned operands and the carry fit it (the historical path, cost and metering identical), and routes past that through the streaming accumulator, whose digit buffer is reserved to the aligned width up front — no aligned numerator is ever materialized in the backend, so gaps at or past usize and aligned widths past the capacity are values priced by memory; - Sum: summands enter at the width their arm stores (the wide arm via the streaming limb entry) and the total reads back out as limbs; - encode, Ord, Eq, Hash, Display: the emission, the class-tie window comparison (one shared MSB-window kernel across arms, extracted from Base::msb_cmp), equality, hashing, and exact decimal rendering (schoolbook long division on the wide arm — quadratic, the documented price of exactness past the backend's reach) all operate per arm. The wasm32 pins flip with the cure: the fraction-door decode past the capacity, both rank-addition gap-boundary pins, and the checked_sub gap-boundary pin now assert exact values (as-found: all four trap at f615220). New adjacency pins hold the integral door below, at, and past the capacity, and a full-width decode-then-re-encode byte-identity pin holds the emission at the seam's far side. The memory-terminal pins are untouched. Host-side, a test-only arm-ceiling override (thread-local, routing only) drives both arms and the seam through the public doors at small sizes: the alignment-order sweep, the RANK_TRIPLE laws, sum-vs-fold, arithmetic against backend-oracle alignment, normalization-family proptests, codec round-trips, Display, and the Ranked composite door all run in the wide regime, plus unit differentials for every wide-arm operation against the backend as oracle. The limb meter's convention extends to the wide arm — the same 64-bit-limb denomination, recorded at the num module's operations — with the base arm's recordings byte-identical to the historical numerator path.
The emit-probe example, the party bench header, the benchmark-results README, and the wire-decode oracle's doc comment all described the external bit-vector baseline as the crate's own storage or builder; each now names it as the comparison baseline it is, with the crate's word-staged PackedBuilder and BitsBuf as the shipping forms. The builder's append assert drops a disjunct for a width its own preceding assert excludes.
plaidfinch
force-pushed
the
w2/wasm32-audit
branch
from
August 19, 2026 15:07
578b583 to
35a09c5
Compare
plaidfinch
marked this pull request as ready for review
August 19, 2026 16:37
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.
CAVEAT LECTOR: This PR and its comments were submitted by Claude, to aid in my own review of these changes.
Executes the 32-bit correctness audit under the ruled contract: no valid input that fits the target's memory space may be rejected or panic — principled correctness on wasm32 exactly as on 64-bit. Instruments before cures throughout: a wasm32 execution leg lands first with today's wrong behaviors pinned red, and each cure commit flips its pins green.
The execution leg (first commit)
Detached workspace
crates/before/wasm32-pins/(guest cdylib on wasm32-unknown-unknown, inputs synthesized in-guest, overflow-checks on; wasmtime harness, fresh instance per pin), eight pins wired into the gate's wasm stream (~3–9s total). Red baselines observed by execution, not argued.Cures (each with its red→green flip)
Bits::len: usize multiply overflow at 2²⁹ bytes — sharper than the recorded finding: the release wrap is coincidentally correct at exactly 2²⁹ and wrong only above. Now u64 arithmetic, checked back to usize, derivation inline.ReaderCursor: the whole-buffer bitvec views hit a seam sharper than recorded — at exactly 2²⁹ bits, bitvec silently constructs an empty view, so a valid 64 MiB input was silently rejected asTruncated(the worst class; the panic starts one byte up). Engineered around per the ruled contract:DsiCursorwalks raw bytes at u64 positions; the doors gain byte-level grammar entries; borsh decode drops a copy and a bitvec round-trip.Shr<u64>totalized (its former expect wrongly rejected computable zero results).Party::encoded_bits/Clock::encoded_bits: view-construction panic and usize sum wrap; now O(1)/u64-checked.# Panicscontracts verified exact with reachability derivations landed at before's relying call sites (suanpan itself: zero changes).Ruled-contract boundaries remaining (documented, loud, not landed — proposals only)
from_canonicalasserts the storable bound with the derivation. Cure would be u64 storage positions (crate-wide internal reshaping).Deref → &BitsSliceborrow caps at bitvec's fat-pointer limit; documented# Panics; the byte surface (eq/hash/canonical/re-encode) is exact at all storable sizes. Cure would be a crate-owned view type (188 uses, 48 files).