feat(read): signed readers and try_bytes window - #3
Draft
h4x0r wants to merge 3 commits into
Draft
Conversation
Failing tests for the two safe-read 0.3 additions (ADR-0019, fleet DRY audit section 2.1), written before the implementation: - the six signed readers (le/be i16/i32/i64) and their try_ twins: valid offsets, offset-honouring reads, out-of-range returning 0 (plain) and None (try_), and off + width overflowing usize; - negative round-trips, the reason the signed readers exist at all: -1, the three MIN sign-bit patterns, and ordinary negative deltas must come back negative rather than as their huge unsigned twins; - try_bytes<N>: in-range windows (including a 16-byte GUID and a zero-width window), windows running past the end, and offsets near usize::MAX that an unchecked off + N would wrap into a seemingly-valid window. All 58 failures are E0425 cannot find function - the readers do not exist yet. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two additions the fleet DRY audit (section 2.1) found re-derived across reader
crates, per ADR-0019:
- the six signed readers le/be i16/i32/i64 and their try_ twins, falling out of
the existing bounded_reader! macro with from_le_bytes/from_be_bytes on the
signed types. Callers that today read a declared-signed field through the
unsigned reader get its huge positive twin instead of the negative value
(winevt cursor.rs, forensicnomicon decode.rs read_i32_le/read_i64_le, lnk,
snss, segb);
- try_bytes::<N>, the array flavour of the same bounded read, for the
fixed-width windows that are not integers - GUIDs, signatures, digests. The
bounded_reader! macro already did this internally; ntfs arr, vsc read_guid
and vhdx guid_at each re-derived it.
Semantics are unchanged from the existing readers: checked_add on off + width,
0 out of range for the plain readers, None for the try_ twins, never a panic.
Still no_std, still zero dependencies, still unsafe_code = forbid, still
compiles on the declared 1.75 MSRV floor.
fuzz/fuzz_targets/readers.rs is extended (not replaced) to drive the six signed
readers and try_bytes at N in {0, 1, 4, 8, 16, 32}, both at the adversarial
usize::MAX-adjacent offset and across the boundary sweep.
Deliberately out of scope, per the audit: LEB128 varints (a format, not a
bounded read) and UTF-16 decoding (needs alloc; belongs to a separate
safe-decode crate).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The install snippet still told readers to pin `safe-read = "0.1"`, a caret that cannot reach the published 0.2.1 and will not reach the 0.3 this PR's feat commit cuts. `cargo update` never crosses a minor within a caret, so the requirement goes stale silently - layer 1 of the three-layer staleness in the fleet dependency-freshness policy. This is the CAUSE of a live finding, not just a stale doc: six consuming repos pin "0.1" and are therefore stranded below 0.2.1. They pin it because the README said to. Leaving it keeps minting the defect on every new consumer. "0.3" is the version release-plz cuts from the feat commit in this PR, so it is correct by the time the merged README is read. This edits a consumer's dependency requirement in documentation, not the package's own `version =` field - that stays release-plz's to compute. The six consuming repos are deliberately not touched here; that sweep is separate work. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
Two additions to
safe-readper ADR-0019 and §2.1 of the fleet DRY audit, built under strict TDD (RED commitd5ced20, GREEN commit9d73f28), plus one documentation fix (fa568d6).What's added
Signed readers —
le_i16/be_i16/le_i32/be_i32/le_i64/be_i64and theirtry_twins. They fall out of the existingbounded_reader!macro withfrom_le_bytes/from_be_byteson the signed types; no hand-written function bodies.A caller that reads a declared-signed field through the unsigned reader gets the huge positive twin rather than the negative value. The consumers that do this today:
winevtcursor.rs:122-152,forensicnomicondecode.rs(read_i32_le/read_i64_le),lnklib.rs:208,snsslib.rs:293,segbcommon.rs:77.try_bytes::<N>(data, off) -> Option<[u8; N]>— the array flavour of the same bounded read, for fixed-width windows that aren't integers (GUIDs, signatures, digests). Thebounded_reader!macro already did this internally; this surfaces it. Re-derived today by ntfsarr, vscread_guid(bytes.rs:36) and vhdxguid_at(integrity.rs:67).Semantics preserved exactly
checked_addonoff + width; plain readers return0out of range,try_twins returnNone; never a panic.Crate properties preserved
#![no_std], zero dependencies — unchanged.unsafe_code = "forbid"— unchanged.cargo +1.75.0 checkpasses.try_bytesuses const generics (stable since 1.51) andslice::get+copy_from_slice; no 1.82-era API (is_none_oris not used).Cargo.tomlis untouched: no dependency added, and the packageversionis left for release-plz to compute from thefeatcommit.The README caret, and why it moves in this PR
fa568d6widens the README install snippet fromsafe-read = "0.1"to"0.3".That snippet is the cause of a live finding, not merely a stale doc: six consuming repos pin
"0.1"and are therefore stranded below the published0.2.1, becausecargo updatenever crosses a minor inside a caret. They pin"0.1"because the README told them to, so leaving it keeps minting the defect on each new consumer."0.3"is the version release-plz cuts from this PR'sfeatcommit, so it is correct by the time anyone reads the merged README. This edits a consumer's dependency requirement in documentation, which is distinct from the package's ownversion =field — that stays release-plz's to compute and is untouched. The six consuming repos are deliberately not touched here; that sweep is separate work.Out of scope, deliberately
LEB128 varints (a format, not a bounded read — and
safe-readcannot carry the typed error a forensic varint decoder needs) and UTF-16 decoding (needs alloc; it belongs to a separatesafe-decodecrate per ADR-0019). Both were proposed and withdrawn in the audit.Gate
cargo buildcargo testcargo clippy --all-targets -- -D warningscargo fmt --checkcargo +1.75.0 checkcargo llvm-cov --all-features --fail-under-functions 100Fuzz —
fuzz/fuzz_targets/readers.rsextended, not replaced: it now drives the six signed readers andtry_bytesatN ∈ {0, 1, 4, 8, 16, 32}, both at the adversarialusize::MAX-adjacent offset and across the past-the-end boundary sweep. 14,389,581 executions in 46 s, no crash.🤖 Generated with Claude Code