Skip to content

fix: close rache 0.2 release blockers - #10

Merged
tisonkun merged 6 commits into
mainfrom
codex/release-0.2-api-safety
Sep 2, 2026
Merged

fix: close rache 0.2 release blockers#10
tisonkun merged 6 commits into
mainfrom
codex/release-0.2-api-safety

Conversation

@tisonkun

@tisonkun tisonkun commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Reject XXH3 custom-secret storage whose exposed slice length changes after validation, before any unchecked stripe access.
  • Remove the inherent write aliases that shadow std::io::Write::write on the two 128-bit streaming states, and document both direct update input and the std::io::Write adapter from the caller's perspective.
  • Route the release guide through cargo-release so publishing, signed tagging, and pushing follow the configured workflow.
  • Add a focused release-profile Miri job for the dependency-free library tests without default features.

Design Notes

XXH3 custom-secret safety

Safe Rust does not require an AsRef<[u8]> implementation to return the same slice length on every call. The previous streaming implementation validated one borrow at construction, derived block metadata from it, and could later use a shorter borrow in unchecked 64-byte stripe loads.

The fix makes the following scoped changes:

  1. Split length validation from slice validation so constructors borrow generic storage once, capture that exact length, and validate it.
  2. Retain the validated secret_len in the accumulator metadata slot that previously held the derived stripes_per_block; this does not grow Xxh3 or Xxh3_128 states.
  3. Check the current length once at the start of each custom-secret operation, then pass that same borrowed slice through all unsafe stripe processing and final merging.
  4. Derive stripes_per_block from the retained length inside the accumulator. The resulting schedule is unchanged, while the original safety premise remains available for later checks and reset.
  5. Preserve the checked length in Xxh3SecretBuilder, validate it before BuildHasher::build_hasher, and avoid calling potentially stateful AsRef from Debug.
  6. Add a safe regression AsRef backed by Cell that changes from a valid 136-byte secret to an empty slice. Update, digest, and builder construction must now panic before unsafe access.

Short seeded-with-secret inputs still use the default secret and therefore do not touch custom storage unnecessarily. Ordinary one-shot slice APIs do not pay the new dynamic check.

Streaming documentation

The documentation now leads with the user action: use update for byte slices, or use a streaming state as the destination of std::io::copy and other std::io::Write producers when the default std feature is enabled. It explicitly states that written bytes become hash input, the entire buffer is accepted, flush is a no-op, and the caller obtains the digest separately after the producer finishes. Rustdoc uses intra-doc links to std::io, std::io::copy, and std::io::Write.

Miri scope

The Miri job deliberately excludes the reference-heavy integration suites. On Linux x86_64, the selected command covers the scalar implementation and available SSE2 kernel in about 35 seconds of interpretation locally. Release profile is intentional because debug assertions can stop execution before Miri observes an unsafe precondition violation.

Performance

I compared origin/main and this branch in a detached worktree on the same machine, using optimized Divan builds and both execution orders.

Custom-secret path Input origin/main median Branch median Paired result
XXH3-64 one-shot 4 KiB 83.73 / 84.38 ns 84.39 / 84.39 ns +0.79% / +0.01%
XXH3-64 one-shot 1 MiB 21.62 / 22.12 us 21.70 / 21.87 us +0.37% / -1.13%
XXH3-64 streaming, 64 KiB chunks 1 MiB 21.70 / 21.95 us 21.79 / 21.95 us +0.41% / 0.00%
XXH3-128 streaming, 64 KiB chunks 1 MiB 21.79 / 21.99 us 22.08 / 21.95 us +1.33% / -0.18%

The deltas reverse or disappear across repeated pairs and are within normal run-to-run noise; there is no repeatable throughput regression. The custom-storage check occurs once per streaming update or finalization rather than once per stripe.

Validation

  • cargo x lint
  • cargo x check
  • cargo x test
  • RUSTUP_TOOLCHAIN=1.85.0 cargo x test
  • cargo x bench --no-run
  • MIRIFLAGS='-Zmiri-strict-provenance' cargo +nightly miri test --package rache --lib --no-default-features --target x86_64-unknown-linux-gnu --release
  • cargo +stable package -p rache --locked --list
  • cargo release 0.2.0 --package rache

An AsRef implementation may expose slices of different lengths across calls,
so constructor-only validation left unchecked stripe loads relying on stale
metadata. Retain the validated length, borrow the secret once per operation,
and reject length changes before any unsafe access.
The inherent write aliases on the 128-bit states won method resolution over
std::io::Write::write, so callers could not use the advertised trait method
with normal syntax. Keep update as the raw streaming API and document the
0.1-to-0.2 migration.
The release guide still prescribed manual publish and tag commands, bypassing
the signed-tag automation added to the workspace. Make the dry-run boundary
and irreversible execute step explicit, and leave GitHub Release creation as a
separate post-publish action.
A release-profile Miri run can catch unsafe precondition failures that normal
tests and debug assertions miss. Limit it to the dependency-free library tests
without default features so pull requests gain scalar and SSE2 coverage without
interpreting the much larger reference-comparison suites.
@tisonkun
tisonkun marked this pull request as ready for review September 2, 2026 07:31
Mentioning the Write implementation without its data flow left users to infer
whether hashes consumed bytes or emitted digests. Describe update as the direct
API, explain the standard-I/O adapter's behavior and boundary, and demonstrate
how to finish a streamed hash.
The io::copy example is only valid when the std feature is enabled, but doctests are also compiled by the no-default-features workflow. Hide a cfg guard in the rendered example so std builds still exercise it without breaking no_std validation.
@tisonkun
tisonkun merged commit 0cc7036 into main Sep 2, 2026
10 checks passed
@tisonkun
tisonkun deleted the codex/release-0.2-api-safety branch September 2, 2026 07:49
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