-
-
Notifications
You must be signed in to change notification settings - Fork 129
ci(miri): check the unsafe decode path under the interpreter #1162
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 5 commits
31fad9e
d4031e2
d0615b5
4e4fffe
8f2e229
89757cd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,98 @@ | ||
| permissions: | ||
| contents: read | ||
| name: Miri | ||
|
|
||
| on: | ||
| push: | ||
| branches: [main] | ||
| pull_request: | ||
| branches: [main] | ||
| workflow_call: | ||
|
|
||
| env: | ||
| CARGO_TERM_COLOR: always | ||
| # Keep in sync with PROTOC_VERSION in main.yml — env is per-workflow, so the | ||
| # value cannot be shared across files without a repo-level Actions variable. | ||
| PROTOC_VERSION: '3.25.3' | ||
| CARGO_INCREMENTAL: "0" | ||
| # Miri interprets MIR and never links, so the workspace's lld/ICF and | ||
| # -Zshare-generics rustflags buy nothing here; a set-but-empty RUSTFLAGS takes | ||
| # precedence over .cargo/config.toml (same lever main.yml's stable job pulls). | ||
| RUSTFLAGS: "" | ||
|
|
||
| jobs: | ||
| miri: | ||
| name: Miri (${{ matrix.name }}) | ||
| runs-on: ubuntu-latest | ||
| # Interpretation costs two orders of magnitude over native. The fixtures the | ||
| # gate covers are small by construction (the MB-scale zlib ones are | ||
| # `#[cfg_attr(miri, ignore)]`), but a cold sysroot build alone is minutes. | ||
| timeout-minutes: 30 | ||
| strategy: | ||
| # Each leg is an independent UB question; one failing should not hide the | ||
| # verdict on the others. | ||
| fail-fast: false | ||
| matrix: | ||
| include: | ||
| # wacore-binary owns the workspace's only load-bearing `unsafe`: the | ||
| # `Yokeable`/`StableDeref` impls behind `OwnedNodeRef` (two lifetime | ||
| # transmutes over borrowed decode output) and the `set_len` over | ||
| # inflate's uninitialized spare capacity in `zlib_pool`. Both are | ||
| # invisible to clippy and to native tests — nothing observes the | ||
| # aliasing violation or the uninit read until it miscompiles. | ||
| - name: wacore-binary | ||
| cache-key: binary-simd | ||
| args: -p wacore-binary --lib | ||
| # The portable-SIMD scanners in the decoder/encoder and their scalar | ||
| # fallbacks are separate code paths, and `--no-default-features` is the | ||
| # only way to reach the latter. | ||
| - name: wacore-binary (no simd) | ||
| cache-key: binary-scalar | ||
| args: -p wacore-binary --no-default-features --lib | ||
| # No `unsafe` of its own, but it drives wacore-binary's zero-copy | ||
| # decode over real Noise frames and pulls the crypto stack | ||
| # (aes/sha2/curve25519), whose unsafe backends this exercises. | ||
| # | ||
| # wacore-appstate is deliberately absent: `inout` 0.2.2's | ||
| # `PaddedInOutBuf::into_out` invalidates the `&mut [u8]` that `cbc`'s | ||
| # `encrypt_padded` still holds protected, which Miri rejects under | ||
| # Stacked Borrows. That is on the AES-CBC path every appstate record | ||
| # takes, so the leg cannot be green until the dependency is fixed — | ||
| # nothing in this workspace can make it so. | ||
| - name: wacore-noise | ||
| cache-key: noise | ||
| args: -p wacore-noise --lib | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This command runs every AGENTS.md reference: AGENTS.md:L23-L23 Useful? React with 👍 / 👎. |
||
| steps: | ||
| - uses: actions/checkout@v6 | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| with: | ||
| persist-credentials: false | ||
| - uses: dtolnay/rust-toolchain@master | ||
|
greptile-apps[bot] marked this conversation as resolved.
|
||
| with: | ||
| toolchain: nightly-2026-06-16 | ||
| # rust-src is what `cargo miri setup` compiles the interpreter's | ||
| # sysroot from; installing `miri` does not pull it in. | ||
| components: miri, rust-src | ||
| # waproto is in the appstate/noise graphs, and its build script needs protoc. | ||
| - name: Install protoc | ||
| uses: taiki-e/install-action@v2 | ||
| with: | ||
| tool: protoc@${{ env.PROTOC_VERSION }} | ||
| # No sccache here: cargo-miri drives the build through its own | ||
| # RUSTC_WRAPPER and the two cannot share that slot. | ||
| - name: Cache Rust build (registry + target + Miri sysroot) | ||
| uses: Swatinem/rust-cache@v2 | ||
| with: | ||
| cache-targets: "true" | ||
| # `cargo miri setup` builds the interpreted sysroot here; without it | ||
| # every run recompiles core/std from rust-src. | ||
| cache-directories: ~/.cache/miri | ||
| # Each leg interprets a different feature set into target/miri. | ||
| key: ${{ matrix.cache-key }} | ||
| - name: Build Miri sysroot | ||
| run: cargo miri setup | ||
| # Default flags: Stacked Borrows, isolation on. Deliberately no | ||
| # -Zmiri-strict-provenance — `bytes` rebuilds its tagged `Shared` pointer | ||
| # out of an integer, which strict provenance rejects on sight and which is | ||
| # not the class of bug this gate is looking for. | ||
| - name: Run tests under Miri | ||
| run: cargo miri test ${{ matrix.args }} | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1008,6 +1008,57 @@ impl OwnedNodeRef { | |
| } | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod owned_node_ref_tests { | ||
| use super::*; | ||
|
|
||
| /// Raw binary-protocol bytes, as `OwnedNodeRef::new` wants them: `marshal` | ||
| /// writes a leading format byte that `unmarshal_ref` does not expect. | ||
| fn encoded(node: &Node) -> Bytes { | ||
| let bytes = crate::marshal::marshal(node).unwrap(); | ||
| Bytes::from(bytes[1..].to_vec()) | ||
| } | ||
|
|
||
| fn sample() -> Node { | ||
| Node::new( | ||
| "iq", | ||
| Attrs(vec![(Cow::Borrowed("id"), NodeValue::String("abc".into()))].into()), | ||
| Some(NodeContent::Bytes(b"payload".to_vec())), | ||
| ) | ||
| } | ||
|
|
||
| #[test] | ||
| fn borrowed_payloads_survive_moving_the_cart() { | ||
|
Comment on lines
+1030
to
+1031
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This test constructs, moves, and reads the yoke, which reaches AGENTS.md reference: AGENTS.md:L23-L23 Useful? React with 👍 / 👎. |
||
| let node = sample(); | ||
| let owned = OwnedNodeRef::new(encoded(&node)).unwrap(); | ||
|
|
||
| // Move the value twice — through a Box and into a Vec — before reading | ||
| // anything back. That is the whole `StableDeref` claim: the yoked | ||
| // `NodeRef` keeps pointing at live bytes even though the wrapper it | ||
| // borrows from has moved. Nothing but an interpreter notices when it | ||
| // stops being true, which is why this test exists separately from the | ||
| // serde one it used to be a side effect of. | ||
| let mut moved = vec![*Box::new(owned)]; | ||
| let owned = moved.pop().unwrap(); | ||
|
|
||
| assert_eq!(owned.tag(), "iq"); | ||
| assert!(owned.get_attr("id").unwrap() == "abc"); | ||
| assert_eq!(owned.content_bytes(), Some(&b"payload"[..])); | ||
| assert_eq!(owned.to_owned_node(), node); | ||
| } | ||
|
|
||
| #[test] | ||
| fn slice_bytes_views_the_backing_buffer_without_copying() { | ||
| let owned = OwnedNodeRef::new(encoded(&sample())).unwrap(); | ||
| let content = owned.content_bytes().unwrap(); | ||
|
|
||
| let view = owned.slice_bytes(content); | ||
|
|
||
| assert_eq!(view.as_ref(), b"payload"); | ||
| assert_eq!(view.as_ptr(), content.as_ptr(), "slice_bytes copied"); | ||
| } | ||
| } | ||
|
|
||
| #[cfg(feature = "serde")] | ||
| impl serde::Serialize for OwnedNodeRef { | ||
| fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> { | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.