Skip to content

ci(miri): check the unsafe decode path under the interpreter - #1162

Merged
jlucaso1 merged 6 commits into
mainfrom
claude/whatsapp-rust-miri-checks-txuzs7
Jul 28, 2026
Merged

jlucaso1 merged 6 commits into
mainfrom
claude/whatsapp-rust-miri-checks-txuzs7

Conversation

@jlucaso1

@jlucaso1 jlucaso1 commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

Nothing in CI currently observes the workspace's unsafe. Clippy does not model aliasing, and a native test only fails if the miscompile has already happened — which is the point at which the bug is hardest to attribute.

What there is to check

The workspace's load-bearing unsafe is concentrated in wacore-binary, on the hot decode path:

Site The claim being made
node.rsunsafe impl Yokeable for AttrsRef<'static>, make/transform/transform_mut three lifetime transmutes over decode output that borrows the inflated buffer
node.rsunsafe impl StableDeref for BytesCart the Bytes deref target stays put while the wrapper moves, which is what lets OwnedNodeRef self-reference
zlib_pool.rsout.set_len(out.len() + produced) inflate wrote exactly produced bytes into spare_capacity_mut(), so that prefix is initialized

The rest of the tree is clean by construction: wacore-libsignal is #[deny(unsafe_code)], and the only other unsafe in the workspace is cfg(target_arch = "wasm32") Send/Sync impls plus a counting allocator in an example.

Scope

Three legs, fail-fast: false so each reports its own verdict. Measured on the final commit:

Leg Test step Why
wacore-binary --lib 37s the three sites above
wacore-binary --no-default-features --lib 1m07s the portable-SIMD scanners in the decoder/encoder and their scalar fallbacks are separate code; --no-default-features is the only way to reach the latter
wacore-noise --lib 3m16s no unsafe of its own, but it drives the zero-copy decode over real Noise frames and pulls curve25519/sha2/AES-GCM, whose unsafe backends this exercises

They run in parallel, so the gate costs about four minutes of wall clock.

wacore-appstate was tried and dropped. Miri rejects inout 0.2.2's PaddedInOutBuf::into_out, which builds a shared slice from a raw pointer that invalidates the &mut [u8] still held protected as cbc::encrypt_padded's argument — reached from wacore-libsignal's aes_256_cbc_encrypt through the documented API used the documented way, with two RustCrypto layers in between. That is the AES-CBC path every appstate record takes, so the leg cannot go green until the dependency does, and #[cfg_attr(miri, ignore)]-ing the ~20 tests that reach it would be deleting the leg while pretending to keep it. The blocker is named in the workflow so the criterion for re-adding it is written down. Full trace in this comment.

Also out: everything Tokio-backed (Miri has no epoll), e2e-tests, and --test targets. The last is not only a cost decision — wacore/binary/tests/*_alloc.rs assert exact allocation counts under a counting global allocator, which is a statement about optimized codegen, not about the interpreter; and roundtrip_proptest.rs is fixed at 2048 cases (ProptestConfig::with_cases wins over PROPTEST_CASES) and writes regression files, which isolation forbids. Running that proptest under Miri at a reduced case count would be a genuinely good UB fuzzer for the decoder, but it needs the case count to come from the environment first — worth a follow-up, not worth weakening this gate to get.

Decisions in the workflow that are not obvious

  • components: miri, rust-src. cargo miri setup compiles the interpreted sysroot from rust-src; installing miri does not pull it in. Both are published for the pinned nightly-2026-06-16 on x86_64-unknown-linux-gnu — checked against static.rust-lang.org's channel manifest rather than assumed, since Miri is dropped from a nightly whenever it fails to build.
  • No sccache. cargo-miri works by installing itself as RUSTC_WRAPPER; sccache wants the same slot. ~/.cache/miri goes through Swatinem/rust-cache's cache-directories instead — the sysroot restores in ~18s rather than being rebuilt — and each leg gets its own cache key because they interpret different feature sets into target/miri.
  • RUSTFLAGS: "". .cargo/config.toml sets lld, --icf=all and -Zshare-generics for this target. Miri never links, so those are dead weight; a set-but-empty RUSTFLAGS overrides config rustflags, the same lever main.yml's stable job already pulls.
  • Default MIRIFLAGS — Stacked Borrows, isolation on. Notably not -Zmiri-strict-provenance: bytes reconstructs its tagged Shared pointer out of an integer, and slice_bytes does its own pointer→integer arithmetic, so that flag would fail on sight, on a class of finding this gate is not looking for. Isolation stays on: nothing in these crates reads a clock or the filesystem, and Miri supplies getrandom itself.
  • push/pull_request rather than a nightly schedule. These crates change on ordinary protocol PRs, which is exactly when the aliasing claims get invalidated; a nightly result arrives after the PR merged. At four minutes it is cheaper than most jobs already in the matrix.

What the gate found before it went green

Three aliasing violations, all in dependencies, none in this workspace:

  1. inout 0.2.2, above — production AES-CBC path. Cost the wacore-appstate leg.
  2. zlib-rs 0.6.6 deflatedeflate::end frees its buffers while a &mut into them is still protected. Reached only by the test helper that built a fixture with flate2; compression is not a path this crate has, inflate is. Fixed by hand-building the fixture as a stored deflate block plus its adler32, which drops the compressor from the interpreted set entirely rather than dropping the test.
  3. Neither is a miscompile anyone has observed — they are violations of an experimental aliasing model. Worth reporting upstream; not worth blocking this on.

Tests

Two of the three unsafe sites were not reachable from any Miri leg as first written, and the second gap was subtler than the first. Both caught in review by @chatgpt-codex-connector.

OwnedNodeRef was constructed in exactly one test, inside #[cfg(feature = "serde")], which neither leg enables. And even fixing that would not have reached AttrsRef's hand-written impl: yoke's derive transmutes the whole struct in one go and never calls a field type's methods, so all three of its methods are reachable only through a yoke of AttrsRef itself, which nothing built. A gate that reports green over the regression it exists to catch is worse than no gate.

Test What it pins
yoked_attrs_ref_survives_make_transform_and_mutation (new) a yoke of AttrsRef borrowing its key and value from the cart, so make, transform and transform_mut each carry a real lifetime across — our transmutes rather than yoke's generated ones
borrowed_payloads_survive_moving_the_cart (new) an OwnedNodeRef moved through a Box and into a Vec still reads its tag, attrs and content back — the whole StableDeref claim, which nothing previously exercised
slice_bytes_views_the_backing_buffer_without_copying (new) slice_bytes returns a view at the same address, not a copy — its pointer arithmetic had no coverage at all
pooled_roundtrip_small_input (new) a 1 KB twin keeping inflate_into_spare's set_len under the interpreter, on a compressor-free fixture

The nine zlib_pool fixtures are 100 KB–4 MB, and not gratuitously — window refill, the ratio-projected growth path and shrink-on-return only happen at that scale — so they are #[cfg_attr(miri, ignore)] behind one comment explaining the pattern. lookup_matches_reference_under_byte_mutation joins them: ~3M table probes, one second natively, and it is pure safe code with no raw pointers. It was what left the wacore-binary leg still running after twelve minutes; that leg now finishes in 37 seconds.

Each ignore was paid for by a measurement. A proposal to trim the Noise crate's large fixtures too was declined for want of one — the whole leg is 3m16s, and vec[0u8; 16 MiB] lowers to __rust_alloc_zeroed, which Miri services as a single operation rather than 16 M writes.

Verification

All three legs green on 89757cd, at the times in the table above. cargo fmt --all and cargo clippy -p wacore-binary --all-targets -- -D warnings clean; cargo test -p wacore-binary --lib and --no-default-features --lib both pass 117 tests, so the ignore attributes did not quietly take anything out of the native run.

Follow-ups, deliberately not smuggled in here

  • Report the inout and zlib-rs aliasing violations upstream to RustCrypto and trifectatechfoundation.
  • Decide whether provider.rs should use the buffer-to-buffer cipher API instead. Changing production crypto to satisfy an interpreter is a trade-off worth its own discussion, not a detail of adding CI.
  • Pin third-party actions to commit SHAs repo-wide (raised by @greptile-apps). Fair in the abstract, but main.yml, wasm.yml, supply-chain.yml, e2e.yml and docker.yml all use @v6/@master; pinning one new file leaves two conventions and no rule, and the supply-chain surface is unchanged while the other five resolve the same mutable refs. Its own PR, ideally with Dependabot's github-actions ecosystem pointed at the pins so they do not rot.

The `Yokeable`/`StableDeref` impls behind `OwnedNodeRef` transmute
lifetimes over borrowed decode output, and `zlib_pool` extends a Vec's
length over inflate's uninitialized spare capacity. Neither clippy nor a
native test observes an aliasing violation or an uninit read there — the
first symptom would be a miscompile.

Adds a Miri workflow over wacore-binary (both the portable-SIMD and the
scalar decode paths), wacore-appstate and wacore-noise: the pure crates
that carry the unsafe and drive it over real protocol payloads. The
tokio/SQLite crates stay out; Miri has no epoll or FFI.

The zlib fixtures only reach window refill and buffer growth at hundreds
of KB to MB, which the interpreter cannot finish in CI time, so they are
ignored under `cfg(miri)` and a 1 KB twin keeps `set_len` covered.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V1mXhXv7K1bxPnCL3cYF2b
@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a GitHub Actions workflow for selected Rust crates under Miri with pinned tooling and caching. Adds focused OwnedNodeRef and pooled zlib tests, marks unsuitable tests ignored under Miri, and documents local validation guidance.

Changes

Miri validation

Layer / File(s) Summary
Miri workflow setup and execution
.github/workflows/miri.yml
Adds reusable and branch-based triggers, a pinned nightly matrix for wacore-binary and wacore-noise, tool installation, caching, sysroot setup, and Miri test execution.
Miri-compatible test coverage
wacore/binary/src/node.rs, wacore/binary/src/zlib_pool.rs, wacore/binary/src/token.rs, AGENTS.md
Adds OwnedNodeRef lifetime and zero-copy tests, adds a small pooled zlib roundtrip, skips selected tests under Miri, and documents local Miri validation guidance.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

  • oxidezap/whatsapp-rust#933: Introduced the pooled zlib_rs inflate path whose set_len behavior is exercised by the new Miri-focused test.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the new Miri CI workflow targeting unsafe decode-path checks.
Description check ✅ Passed The description matches the changeset and explains the new Miri workflow, coverage, and exclusions.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/whatsapp-rust-miri-checks-txuzs7

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Comment thread .github/workflows/miri.yml
@greptile-apps

greptile-apps Bot commented Jul 28, 2026

Copy link
Copy Markdown

Greptile Summary

Adds a dedicated Miri CI matrix and focused interpreter-compatible coverage for the binary decoder's unsafe paths.

  • Runs default and scalar wacore-binary library tests plus wacore-noise under a pinned nightly Miri toolchain.
  • Adds direct coverage for moving yoked decode buffers, AttrsRef transformations, zero-copy byte slicing, and pooled inflate initialization.
  • Skips prohibitively expensive safe-code and large-fixture tests under Miri while retaining native execution.
  • Documents the repository's Miri expectations for future unsafe-code changes.

Confidence Score: 4/5

The PR appears safe to merge, with the previously reported workflow reproducibility concern still outstanding.

The Miri coverage changes are internally coherent, but the workflow still resolves four third-party actions through mutable references, allowing their executed code to change independently of this repository.

Files Needing Attention: .github/workflows/miri.yml

Important Files Changed

Filename Overview
.github/workflows/miri.yml Introduces a three-leg Miri workflow with isolated cache keys, explicit sysroot setup, and interpreter-oriented configuration.
AGENTS.md Documents when and how contributors should use Miri for changes to unsafe binary-codec paths.
wacore/binary/src/node.rs Adds focused tests for the yoked ownership, lifetime-transform, and zero-copy slicing invariants.
wacore/binary/src/token.rs Excludes a computation-heavy safe lookup test from Miri while leaving native coverage intact.
wacore/binary/src/zlib_pool.rs Adds a small hand-built zlib fixture that reaches pooled inflate under Miri and excludes oversized fixtures only from interpreter runs.

Reviews (6): Last reviewed commit: "test(binary): interpret the hand-written..." | Re-trigger Greptile

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/miri.yml:
- Line 62: Update the actions/checkout@v6 step in the Miri workflow to set
persist-credentials to false, while preserving the existing read-only
permissions and all other job behavior.

In `@AGENTS.md`:
- Line 23: Update the local Miri guidance in AGENTS.md to match the CI matrix by
documenting runs for wacore-binary with and without default features,
wacore-appstate, and wacore-noise; alternatively, explicitly label the existing
command as a minimal binary-only smoke test.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 679aac57-3165-43f3-ac75-933f228778b8

📥 Commits

Reviewing files that changed from the base of the PR and between 4c343ba and 31fad9e.

📒 Files selected for processing (3)
  • .github/workflows/miri.yml
  • AGENTS.md
  • wacore/binary/src/zlib_pool.rs

Comment thread .github/workflows/miri.yml
Comment thread AGENTS.md

Copy link
Copy Markdown
Collaborator Author

First run in, and the gate found something on its first outing — just not in our code.

Miri (wacore-appstate) — Stacked Borrows violation inside inout

error: Undefined Behavior: not granting access to tag <346841> because that would
       remove [Unique for <414328>] which is strongly protected
  --> inout-0.2.2/src/reserved.rs:246:18
246 |  unsafe { slice::from_raw_parts(out_ptr as *const u8, res_len) }

stack backtrace:
  0: inout::reserved::PaddedInOutBuf::<…>::into_out
  1: <cbc::Encryptor<Aes256> as BlockModeEncrypt>::encrypt_padded_inout::<Pkcs7>
  2: <cbc::Encryptor<Aes256> as BlockModeEncrypt>::encrypt_padded::<Pkcs7>
  3: <RustCryptoProvider as SignalCryptoProvider>::aes_256_cbc_encrypt
       at wacore/libsignal/src/crypto/provider.rs:315

into_out builds a shared slice from a raw pointer that invalidates the &mut [u8] still protected as encrypt_padded's argument. Our side of the boundary is the documented API used the documented way — encryptor.encrypt_padded::<Pkcs7>(&mut out[start..], plaintext.len()) — with two RustCrypto layers (cipher 0.5.2, inout 0.2.2) between that call and the raw pointer. It is an aliasing-model violation, not an observed miscompile, and it is not fixable from this repo.

It is also not confined to tests: aes_256_cbc_encrypt/_decrypt are the production path, so any crate that encrypts under Miri hits it. wacore-appstate reaches it through create_test_record, which nearly every decode/processor test uses — so #[cfg_attr(miri, ignore)] there would not be trimming a few expensive fixtures, it would be deleting the leg while pretending to keep it.

So the gate ships as wacore-binary only (SIMD + scalar), which is where the workspace's own unsafe lives and where the check was always going to earn its keep. I will drop the wacore-appstate leg with the blocker named in the workflow, so the criterion for re-adding it is written down rather than rediscovered. wacore-noise is still running — AES-GCM does not go through the padded path, so it may well be clean; it stays if it is green.

Two follow-ups worth their own issues rather than being smuggled in here: reporting this upstream to RustCrypto, and evaluating whether provider.rs should use the buffer-to-buffer API instead. Changing production crypto to satisfy an interpreter is a decision with its own trade-offs, not a detail of adding CI.

On pinning the actions to commit SHAs (@greptile-apps)

Fair in the abstract, but this workflow is not where that policy should start. main.yml, wasm.yml, supply-chain.yml, e2e.yml and docker.yml all use @v6/@master; only signal-durability-nightly.yml pins SHAs. Pinning one new file leaves the repo with two conventions and no rule, and the supply-chain surface is unchanged as long as the other six resolve the same mutable refs. Worth doing repo-wide in its own PR — where Dependabot's github-actions ecosystem can be pointed at the pins so they do not rot — not for one job here.

The persist-credentials: false point (@coderabbitai) I am taking: this job never pushes, the repo already does it in signal-durability-nightly.yml, and it costs one line.


Generated by Claude Code

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

@jlucaso1 Agreed. A permanently failing wacore-appstate Miri leg caused by the dependency stack would be noise, and ignoring the record-creation path would make the remaining coverage misleading.

Shipping the wacore-binary SIMD and scalar legs keeps the gate focused on the workspace’s unsafe decode paths. Documenting the inout/CBC blocker and the condition for restoring appstate coverage in .github/workflows/miri.yml is the right bar. Keeping wacore-noise only if it is green also preserves that standard.

Thanks for taking persist-credentials: false. Deferring action SHA pinning to a repository-wide, Dependabot-supported migration is reasonable for this PR.


✏️ Learnings added
Learnt from: jlucaso1
URL: https://github.com/oxidezap/whatsapp-rust/pull/1162

Timestamp: 2026-07-28T15:21:03.326Z
Learning: The Miri `wacore-appstate` test leg currently triggers an external Stacked Borrows violation in `inout` 0.2.2 through RustCrypto CBC padding used by `wacore/libsignal/src/crypto/provider.rs` (`RustCryptoProvider::aes_256_cbc_encrypt`). Because `wacore-appstate` tests broadly rely on this production encryption path, selectively ignoring the affected tests would make its Miri coverage non-meaningful. Keep the Miri gate focused on `wacore-binary` SIMD and scalar decode paths until the external blocker is resolved or the crypto implementation is deliberately changed.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

@greptile-apps

greptile-apps Bot commented Jul 28, 2026

Copy link
Copy Markdown

Both points land well.

The consistency argument on SHA pinning is correct — introducing a second convention mid-repo without a repo-wide rule or Dependabot keeping the pins current would be net-negative. A dedicated PR that covers all six workflows at once is the right call.

persist-credentials: false is a clean, zero-cost hardening that's already established as a pattern in the repo — good that it's going in.

On the Miri findings: the inout Stacked Borrows violation is a well-known class of issue in the RustCrypto stack (the PaddedInOutBuf aliasing pattern has come up in upstream issues before). Dropping wacore-appstate with the blocker named explicitly is the right call — it's honest about what the leg covers and leaves a clear re-entry criterion. If wacore-noise comes back clean, that's a meaningful result on its own since AES-GCM goes through a different path.

The first run found real UB, in `inout` 0.2.2 rather than in this
workspace: `PaddedInOutBuf::into_out` builds a shared slice from a raw
pointer that invalidates the `&mut [u8]` still protected as
`cbc::encrypt_padded`'s argument, reached from
`RustCryptoProvider::aes_256_cbc_encrypt`.

That is the AES-CBC path every appstate record takes, so the leg cannot
be green until the dependency is, and per-test `cfg_attr(miri, ignore)`
would delete the coverage while appearing to keep it. The blocker is
named in the workflow so the criterion for restoring the leg is written
down. wacore-noise stays: it passed, AES-GCM not going through the
padded path.

Also stops the checkout persisting its token, as
signal-durability-nightly.yml already does — this job never pushes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V1mXhXv7K1bxPnCL3cYF2b

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 31fad9e2af

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread .github/workflows/miri.yml
claude added 2 commits July 28, 2026 15:28
`OwnedNodeRef::new` was constructed in exactly one place: a test inside
`#[cfg(feature = "serde")]`. Neither Miri leg enables that feature, so the
`Yokeable` transmutes and the `StableDeref` impl — two of the three unsafe
sites the gate exists for — were never interpreted. The gate would have
reported green over an aliasing regression it was supposed to catch.

Both tests are worth having on their own: nothing previously moved an
`OwnedNodeRef` after construction, which is the entire `StableDeref`
claim, and `slice_bytes`' pointer arithmetic had no coverage at all.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V1mXhXv7K1bxPnCL3cYF2b
`lookup_matches_reference_under_byte_mutation` walks every byte position
of every token against all 256 values — ~3M probes, a second natively and
minutes under Miri, which is what left the wacore-binary leg still running
after twelve. `token.rs` holds no `unsafe` and no raw pointers, so the
interpreter has nothing to find there.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V1mXhXv7K1bxPnCL3cYF2b
@github-actions

github-actions Bot commented Jul 28, 2026

Copy link
Copy Markdown

📦 Binary size report

Metric main PR Δ
bin size (stripped) 10.04 MiB 10.04 MiB 0
bin .text 8.08 MiB 8.08 MiB 0
bin allocated (text+data+bss) 10.04 MiB 10.04 MiB 0
llvm-lines wacore 494,866 494,866 0
llvm-lines wacore copies 16,403 16,403 0
llvm-lines whatsapp-rust lib 720,829 720,829 0
llvm-lines whatsapp-rust lib copies 22,708 22,708 0
deps crates (Cargo.lock) 471 471 0
.text per crate
Crate main PR Δ
.text whatsapp_rust 1.83 MiB 1.83 MiB 0
.text wacore 665.37 KiB 665.37 KiB 0
.text wacore_binary 89.69 KiB 89.69 KiB 0
.text wacore_libsignal 166.27 KiB 166.27 KiB 0
.text wacore_appstate 22.35 KiB 22.35 KiB 0
.text wacore_noise 21.79 KiB 21.79 KiB 0
.text waproto 1.74 MiB 1.74 MiB 0
.text whatsapp_rust_sqlite_storage 515.98 KiB 515.98 KiB 0
.text whatsapp_rust_tokio_transport 39.91 KiB 39.91 KiB 0
.text whatsapp_rust_ureq_http_client 10.33 KiB 10.33 KiB 0
.text std 1.07 MiB 1.07 MiB 0
.text other deps 1.90 MiB 1.90 MiB 0

Baseline: 4c343ba85 (latest main run) · Head: 59d86f139 · Graphs

zlib-rs 0.6.6's deflate state frees its buffers from `deflate::end` while
a `&mut` into them is still protected, which Miri rejects — so the one
zlib test left running under the interpreter died in `flate2`'s Drop,
inside the helper that built its fixture rather than in anything this
crate runs. Compression is not a path wacore-binary has; inflate is.

The fixture is now a hand-built stored deflate block plus its adler32,
which keeps `inflate_into_spare`'s `set_len` covered and drops the
compressor from the interpreted set entirely.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V1mXhXv7K1bxPnCL3cYF2b

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8f2e229b73

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread wacore/binary/src/node.rs
Comment on lines +1030 to +1031
#[test]
fn borrowed_payloads_survive_moving_the_cart() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Exercise the mutable Yokeable transmute under Miri

This test constructs, moves, and reads the yoke, which reaches Yokeable::make and the StableDeref implementation, but it never calls Yoke::with_mut; a repo-wide search finds no other with_mut call. Consequently AttrsRef::transform_mut—the second lifetime transmute advertised by the workflow—remains entirely uninterpreted, so a regression there would still leave every Miri leg green. Add a small mutation-path assertion through owned.inner.with_mut(...) in this module.

AGENTS.md reference: AGENTS.md:L23-L23

Useful? React with 👍 / 👎.

# nothing in this workspace can make it so.
- name: wacore-noise
cache-key: noise
args: -p wacore-noise --lib

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep the large Noise fixtures off Miri

This command runs every wacore-noise library unit test under the interpreter, including several 200 KiB framing fixtures and the 16 MiB allocations in test_build_edge_routing_preintro_too_large, test_build_handshake_header_with_oversized_routing, and test_encode_frame_too_large; none has #[cfg_attr(miri, ignore)], and these safe size-boundary tests do not cover the unsafe crypto backends that justify this leg. Thus every PR pays Miri's interpretation cost for the same large-fixture class deliberately excluded from the binary legs; ignore these tests under Miri and retain small crypto-path twins where needed.

AGENTS.md reference: AGENTS.md:L23-L23

Useful? React with 👍 / 👎.

`Yoke::with_mut` is called nowhere in the workspace, so `transform_mut`
was uninterpreted — and yoking a `NodeRef` would not have reached the
`AttrsRef` impl anyway: the derive transmutes the whole struct in one go
and never calls a field's methods. The hand-written impl is there to
satisfy that bound, which leaves its three transmutes reachable only
through a yoke of `AttrsRef` itself.

That yoke is what this test builds, borrowing the attribute key and value
straight out of the cart so `make`, `transform` and `transform_mut` all
carry a real lifetime across.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V1mXhXv7K1bxPnCL3cYF2b

Copy link
Copy Markdown
Collaborator Author

Two findings from @chatgpt-codex-connector, one taken and one declined on measurement.

Taken: the mutable transmute was uninterpreted — and worse than reported

Correct that Yoke::with_mut is called nowhere in the workspace, so transform_mut never ran. But yoking a NodeRef would not have reached AttrsRef's impl even with a with_mut call: yoke's derive transmutes the whole struct in one go and never calls a field type's methods. The hand-written unsafe impl Yokeable for AttrsRef<'static> exists to satisfy the derive's bound on the field, which leaves all three of its methods — not just transform_mut — reachable only through a yoke of AttrsRef itself. Nothing built one, so the interpreter was seeing yoke's generated transmutes and none of ours.

yoked_attrs_ref_survives_make_transform_and_mutation builds that yoke, borrowing the attribute key and value straight out of the cart so make (on attach), transform (on get) and transform_mut (on with_mut) each carry a real lifetime across. 89757cd.

Its compile-time half was already covered: _assert_attrs_ref_covariant fails to build if AttrsRef ever becomes invariant, which is the property the transmutes rest on.

Declined: the large Noise fixtures

The cost argument is reasonable a priori and does not survive measurement. Miri (wacore-noise) has now run green twice — 4m21s and 4m07s for the whole test step, compile included. That is the entire leg, so the fixtures named are a slice of four minutes, not a reason to reshape another crate's suite.

The mechanism explains it: vec[0x00; MAX_EDGE_ROUTING_LEN + 1] and vec[0u8; FRAME_MAX_SIZE] lower to __rust_alloc_zeroed, which Miri services as a single operation rather than 16 M writes, and both tests then hit their length check immediately. The real cost is payload(200 * 1024, _), a 200 K-iteration map — seconds, and it is buying the framing decoder's buffer-reuse path, which is the zero-copy behavior this leg is here to watch.

The three zlib-style ignores in this PR were each paid for by a measurement (hundreds of KB to MB of interpreted inflate, and a ~3M-probe table sweep that had a leg still running at twelve minutes). Trimming a four-minute leg on a prediction would be churn in someone else's tests without one.


Generated by Claude Code

@chatgpt-codex-connector

Copy link
Copy Markdown

Summary

  • Verified that the accepted finding is already addressed at the current HEAD. The new test directly yokes AttrsRef, borrows its key and value from the cart, reads through Yoke::get, and invokes Yoke::with_mut, covering the hand-written make, transform, and transform_mut lifetime conversions. wacore/binary/src/node.rsL1050-L1082
  • Confirmed the existing OwnedNodeRef test separately exercises moving the cart while retaining borrowed tag, attribute, and payload access. wacore/binary/src/node.rsL1030-L1047
  • Accepted the measurement-based decision not to alter the Noise fixtures. The Miri matrix continues to run the complete wacore-noise library suite. .github/workflows/miri.ymlL52-L64
  • No additional code changes were necessary, so no new commit or pull request was created.

Testing

  • cargo test -p wacore-binary --lib yoked_attrs_ref_survives_make_transform_and_mutation
  • git status --short --branch (working tree clean)

View task →

@jlucaso1
jlucaso1 merged commit 203e69f into main Jul 28, 2026
22 of 23 checks passed
@jlucaso1
jlucaso1 deleted the claude/whatsapp-rust-miri-checks-txuzs7 branch July 28, 2026 16:36
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.

2 participants