Skip to content

feat(read): signed readers and try_bytes window - #3

Draft
h4x0r wants to merge 3 commits into
mainfrom
feat/signed-readers-and-try-bytes
Draft

feat(read): signed readers and try_bytes window#3
h4x0r wants to merge 3 commits into
mainfrom
feat/signed-readers-and-try-bytes

Conversation

@h4x0r

@h4x0r h4x0r commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Two additions to safe-read per ADR-0019 and §2.1 of the fleet DRY audit, built under strict TDD (RED commit d5ced20, GREEN commit 9d73f28), plus one documentation fix (fa568d6).

What's added

Signed readersle_i16/be_i16/le_i32/be_i32/le_i64/be_i64 and their try_ twins. They fall out of the existing bounded_reader! macro with from_le_bytes/from_be_bytes on 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: winevt cursor.rs:122-152, forensicnomicon decode.rs (read_i32_le/read_i64_le), lnk lib.rs:208, snss lib.rs:293, segb common.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). The bounded_reader! macro already did this internally; this surfaces it. Re-derived today by ntfs arr, vsc read_guid (bytes.rs:36) and vhdx guid_at (integrity.rs:67).

Semantics preserved exactly

checked_add on off + width; plain readers return 0 out of range, try_ twins return None; never a panic.

Crate properties preserved

  • #![no_std], zero dependencies — unchanged.
  • unsafe_code = "forbid" — unchanged.
  • MSRV 1.75cargo +1.75.0 check passes. try_bytes uses const generics (stable since 1.51) and slice::get + copy_from_slice; no 1.82-era API (is_none_or is not used).

Cargo.toml is untouched: no dependency added, and the package version is left for release-plz to compute from the feat commit.

The README caret, and why it moves in this PR

fa568d6 widens the README install snippet from safe-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 published 0.2.1, because cargo update never 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's feat commit, 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 own version = 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-read cannot carry the typed error a forensic varint decoder needs) and UTF-16 decoding (needs alloc; it belongs to a separate safe-decode crate per ADR-0019). Both were proposed and withdrawn in the audit.

Gate

Check Result
cargo build pass
cargo test 17 passed (15 lib + 2 doctest), 0 failed
cargo clippy --all-targets -- -D warnings clean
cargo fmt --check clean
cargo +1.75.0 check pass (declared MSRV floor)
cargo llvm-cov --all-features --fail-under-functions 100 100.00% regions (349/349), functions (20/20), lines (151/151)

Fuzzfuzz/fuzz_targets/readers.rs extended, not replaced: it now drives the six signed readers and try_bytes at N ∈ {0, 1, 4, 8, 16, 32}, both at the adversarial usize::MAX-adjacent offset and across the past-the-end boundary sweep. 14,389,581 executions in 46 s, no crash.

🤖 Generated with Claude Code

h4x0r and others added 3 commits August 2, 2026 02:19
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant