From f60ff15ac502406a1212f00f728af07339242b5c Mon Sep 17 00:00:00 2001 From: Walter Perdan Date: Sat, 8 Aug 2026 14:21:32 +0200 Subject: [PATCH 1/8] doc(ci): add Miri UB verification plan (#81) Documents which code runs under Miri and why, the measured runtime that drove the exclusion list, and the known parallel+simd coverage gap. Also records four factual corrections to issue #81: structural.rs has no unsafe block, data_ptr/data_ptr_mut live in matrix.rs and contain no unsafe, excluding the simd feature would audit zero production unsafe, and --workspace contradicts the stated wasm exclusion. .agents/* is gitignored, so the plan is un-ignored by exception in the same style as instructions.md and workflows/. Co-Authored-By: Claude Opus 5 --- .agents/MIRI_PLAN.md | 465 +++++++++++++++++++++++++++++++++++++++++++ .gitignore | 1 + 2 files changed, 466 insertions(+) create mode 100644 .agents/MIRI_PLAN.md diff --git a/.agents/MIRI_PLAN.md b/.agents/MIRI_PLAN.md new file mode 100644 index 0000000..3f9fa89 --- /dev/null +++ b/.agents/MIRI_PLAN.md @@ -0,0 +1,465 @@ +# Miri UB Verification Plan + +> **Status:** Design accepted, implementation pending. +> **Issue:** [webarkit/purecv#81](https://github.com/webarkit/purecv/issues/81) +> **Branch:** `feat/issue-81-miri-ci` (from `dev`) + +--- + +## 1. Purpose & Scope + +PureCV's headline claim is memory safety. That claim is currently *asserted* rather +than *verified*: a small number of `unsafe` slice reinterpretations back the SIMD +fast paths. This plan adds [Miri](https://github.com/rust-lang/miri) to CI so those +blocks are machine-checked for undefined behavior on every push and pull request. + +**What Miri checks:** out-of-bounds accesses, use-after-free, invalid pointer +provenance, aliasing violations (Stacked Borrows), misaligned access, uninitialized +memory reads, and data races. + +**What Miri cannot check here:** anything compiled for `wasm32` (unsupported target), +anything not reached by a `#[test]`, and — pending verification — code paths using +SIMD intrinsics that Miri has no shim for. + +--- + +## 2. Inventory of `unsafe` + +Verified by direct inspection, **not** copied from the issue text, which contained +errors (see §7). + +Line numbers are as of the `dev` branch at v0.7.0 (`52dc212`) and **will drift**. +Regenerate the inventory rather than trusting them: + +```bash +grep -rn "unsafe {" src/ +``` + +If that command returns anything other than the 8 blocks below, this section is stale +and the counts in §3 and §5 need rechecking. + +### Production code — 6 blocks, all `simd`-gated + +| File | Lines | Context | Reachable without `simd`? | +|------|-------|---------|---------------------------| +| `src/core/arithm.rs` | 99, 248 | `binary_op!` / `unary_op!` — `from_raw_parts_mut` on a rayon chunk | No — `simd` **and** `parallel` | +| `src/core/arithm.rs` | 119, 267 | Same macros, `cfg(not(feature = "parallel"))` branch — whole-buffer `from_raw_parts_mut` | No — `simd` only | +| `src/imgproc/derivatives.rs` | 346, 349 | `fast_deriv_3x3` — `from_raw_parts` / `from_raw_parts_mut` reinterpreting `&[T]` as `&[f32]` after a `TypeId` check | No — `simd` only | + +**Consequence:** running Miri *without* `--features simd` audits **zero** production +`unsafe`. This is why the plan uses two runs (§3) rather than the single no-feature +run the issue originally proposed. + +### Test code — 2 blocks + +| File | Lines | Context | +|------|-------|---------| +| `src/core/tests.rs` | 1533, 1544 | Raw pointer deref of `Matrix::data_ptr()` / `data_ptr_mut()` | + +These are the only `unsafe` blocks reachable in the default feature set, and the only +thing the baseline leg meaningfully verifies. + +### Files with *no* `unsafe` despite appearances + +- `src/core/structural.rs` — a comment describing unsafe that was **not** written; + the code takes a sequential path instead. +- `src/core/matrix.rs` — `# Safety` doc comments on `data_ptr`/`data_ptr_mut`. + The functions themselves are safe; only *dereferencing* their return value is not. +- `src/video/simd.rs` — module doc noting the absence of `unsafe`. + +--- + +## 3. What Runs Under Miri + +Two matrix legs, both `-p purecv` (never `--workspace`, which would pull in the wasm +crate) and both `--no-default-features` (the default set silently enables `parallel`). + +### Leg 1 — `baseline` · **required** + +```bash +cargo miri test -p purecv --lib --no-default-features --features std +``` + +Covers all safe code paths and the `data_ptr` tests. Expected to pass trivially. +Its job is to be a fast, stable gate that catches UB regressions in safe code and +in any future non-SIMD `unsafe`. + +### Leg 2 — `simd` · **advisory at first** (`continue-on-error: true`) + +```bash +cargo miri test -p purecv --lib --no-default-features --features std,simd +``` + +The leg that does the real work: it reaches `arithm.rs:119,267` and +`derivatives.rs:346,349`. + +pulp is **confirmed Miri-compatible** (A1, §6) and this leg passes clean locally. +It is nonetheless kept advisory for its first CI run, because Miri's intrinsic +support is target-dependent and all local evidence is from +`x86_64-pc-windows-msvc` rather than CI's `ubuntu-latest`. **Promote to required +by setting `experimental: false`** as soon as it has been observed green on Linux — +this is expected to be immediate, not a long-term state. + +### Aliasing model and MIRIFLAGS + +``` +MIRIFLAGS: -Zmiri-deterministic-floats +``` + +**Aliasing:** Miri's defaults are kept — **strict provenance + Stacked Borrows**. +Stacked Borrows is retained over Tree Borrows (`-Zmiri-tree-borrows`) because it is +the stricter guarantee. It raised **no** complaints against the `from_raw_parts_mut` +patterns in practice, so the Tree Borrows fallback was not needed. + +**Not** `-Zmiri-strict-provenance` as the issue proposed: strict provenance is now +Miri's default and that flag is deprecated (the opt-*out* is +`-Zmiri-permissive-provenance`). Passing a removed `-Z` flag risks hard-failing the +job on a future nightly. + +**`-Zmiri-deterministic-floats`** is required, for the reason documented in §6 — +Miri's intentional float error injection breaks `test_randn_determinism`. Preferred +over ignoring that test, since it keeps the RNG determinism contract under test. + +--- + +## 4. What Is Excluded, and Why + +| Excluded | Rationale | +|----------|-----------| +| `crates/wasm` (`purecv-wasm`) | Miri has no `wasm32-unknown-unknown` support. Excluded structurally via `-p purecv` rather than `--workspace --exclude`. | +| `benches/` | Not built by `cargo miri test`; Criterion's sampling under interpretation would be meaninglessly slow. | +| `parallel` feature | Rayon under Miri is slow and its thread support is limited. Accepted cost: a coverage gap (§5). | +| 9 individual tests | Excluded **on measured evidence only** (see below), never speculatively. | + +### Excluded tests — measured + +Threshold: **>30s under Miri**. Nine tests qualified. They are iteration-heavy +algorithms (RANSAC, ORB, Lucas-Kanade), and **none of them contain or reach +`unsafe`** — so excluding them costs no UB coverage. + +| Test | Miri time | File | +|------|-----------|------| +| `calib3d::…::test_find_fundamental_mat_ransac` | 927.9s | `src/calib3d/tests.rs` | +| `features2d::tests::test_orb_full_pipeline` | 806.3s | `src/features2d/tests.rs` | +| `video::…::test_lk_pure_translation_x` | 105.2s | `src/video/tests.rs` | +| `core::rng::tests::test_randn_statistics` | 87.5s | `src/core/rng.rs` | +| `video::…::test_lk_use_initial_flow` | 61.7s | `src/video/tests.rs` | +| `video::…::test_lk_stationary_point_identical_frames` | 61.7s | `src/video/tests.rs` | +| `video::…::test_lk_min_eigenvals_flag` | 61.5s | `src/video/tests.rs` | +| `video::…::test_build_pyramid_with_derivatives` | 45.3s | `src/video/tests.rs` | +| `features2d::tests::test_orb_pyramid_dimensions` | 30.2s | `src/features2d/tests.rs` | + +**Why so few exclusions suffice.** The distribution is extremely skewed: the top 5 +tests are 82% of total runtime, while the remaining **293 tests complete in 139s +combined**. Nine annotations take the suite from 41 minutes to roughly 4. + +**Coverage check on the one borderline case.** `test_build_pyramid_with_derivatives` +exercises the Sobel path, which under `simd` reaches the `unsafe` in +`derivatives.rs:346,349`. That coverage is **not** lost: `imgproc::tests::test_sobel` +calls `sobel(&src_f32, 1, 0, 3, …)` (`src/imgproc/tests.rs:138`), matching +`fast_deriv_3x3`'s `TypeId == f32 && ksize == 3` trigger, and runs in 0.8s under Miri. + +### Annotation convention + +Any test excluded later must use `#[cfg_attr(miri, ignore)]` with a reason comment +directly above it: + +```rust +// miri: +#[cfg_attr(miri, ignore)] +#[test] +fn some_test() { … } +``` + +`cfg(miri)` is set automatically by Miri — no Cargo feature, no `Cargo.toml` change. +Under normal `cargo test` the attribute vanishes entirely. + +The reason comment is mandatory so this document's exclusion list stays **derivable +by grep** instead of drifting out of sync: + +```bash +grep -rn -B1 "cfg_attr(miri, ignore)" src/ +``` + +The codebase currently has **zero** `#[ignore]` and **zero** `cfg_attr` annotations, +so every future match is unambiguously Miri-related. + +### If pulp proves wholly incompatible + +Do **not** mass-annotate the ~63 tests in `src/core/simd.rs`, `src/imgproc/simd.rs`, +and `src/video/simd.rs`. That would be noise masquerading as progress. Instead: keep +the simd leg permanently `continue-on-error`, record the limitation here, and revisit +when pulp or Miri advances. + +--- + +## 5. Known Coverage Gap + +The **`parallel` + `simd`** combination is not verified by this plan. + +`arithm.rs:99` and `:248` — `from_raw_parts_mut` on a rayon chunk, arguably the most +delicate `unsafe` in the codebase, since it reinterprets a *slice of a slice* being +mutated across threads — live behind `cfg(feature = "parallel")`. Neither leg enables +it, so neither leg reaches them. The sequential equivalents at `:119`/`:267` **are** +covered, and they share the same reinterpretation logic, which mitigates but does not +eliminate the gap. + +Closing it requires a third leg (`--features std,parallel,simd`) relying on Miri's +thread support. Deferred: it is the slowest configuration and the most likely to +produce false positives from rayon internals. Worth a follow-up issue once the simd +leg is stable. + +--- + +## 6. Assumptions + +Measured on `x86_64-pc-windows-msvc`, nightly, 2026-08-08. + +| ID | Assumption | Status | +|----|------------|--------| +| A1 | Miri's x86-64 intrinsic shims cover what pulp emits | ✅ **CONFIRMED.** 53 `core::simd` tests pass in 24s with zero unsupported-operation errors. pulp is Miri-compatible. | +| A2 | Full-suite Miri runtime fits a ~20 min budget | ❌ **REFUTED as originally run** — the unfiltered baseline took **2469s (41 min)**. ✅ **Satisfied after exclusions** (§4): 5 tests accounted for 82% of the total. | +| A3 | `Instant::now` works under Miri's virtual clock without `-Zmiri-disable-isolation` | ✅ Confirmed — no isolation errors in any run. | +| A4 | `panic = "abort"` does not affect `miri test` | ✅ Confirmed — test profile unaffected. | +| A5 | Work branches from and targets `dev` | Per `CLAUDE.md`. | + +### Unanticipated finding: Miri's float non-determinism + +`core::rng::tests::test_randn_determinism` failed on the first run. **This is not UB +and not a purecv bug.** Miri deliberately injects a small random error into +transcendental float operations to catch code relying on exact results. The +Box-Muller transform at `src/core/rng.rs:115-117` uses `ln()`, `cos()` and `sin()`, +and the test asserts two identically-seeded runs are bit-identical. The observed +arrays differed only in the last 1–2 ULP: + +``` +left: 0.18517594738681525 +right: 0.18517594738681534 +``` + +Resolved with `MIRIFLAGS: -Zmiri-deterministic-floats`, which keeps the test running +rather than ignoring it — the RNG's determinism contract is still verified. Confirmed +passing with the flag set. + +### Note on what the SIMD leg actually interprets + +pulp performs runtime feature detection. Under Miri it most likely takes its scalar +fallback rather than a vectorized kernel, so the AVX/SSE kernels themselves may not be +interpreted. This does **not** weaken the result that matters: the `unsafe` +`from_raw_parts`/`from_raw_parts_mut` reinterpretations in `arithm.rs` and +`derivatives.rs` sit *outside* pulp and execute regardless of which kernel pulp +dispatches to. Those are the blocks with UB risk, and they are covered. + +--- + +## 7. Corrections to Issue #81 + +Recorded so the errors are not reintroduced by a later reader of the issue. + +1. **`src/core/structural.rs` split/merge has no `unsafe`.** The issue lists it as an + audit target; line 278 is a comment explaining why unsafe was *avoided*. +2. **`data_ptr`/`data_ptr_mut` live in `matrix.rs`,** not `tests.rs`, and contain no + `unsafe`. Only their tests dereference raw pointers. +3. **"Exclude pulp SIMD" and "verify all unsafe code" are mutually exclusive** as + written, since 100% of production `unsafe` is `simd`-gated. Resolved by the + two-leg design (§3). +4. **`--workspace` contradicts the wasm exclusion,** and a bare `cargo miri test` + enables `parallel` via default features, contradicting "without parallel + features initially." Resolved by explicit `-p` and `--no-default-features` (§3). + +Additionally: **badges are per-workflow, not per-job.** The issue asks for a job +inside `ci.yml` *and* a distinct Miri badge; those are incompatible. Resolved by +giving Miri its own workflow file. + +--- + +## 8. Implementation Sequence + +1. **Feasibility spike** — resolve A1/A2 before writing YAML: + ```bash + rustup toolchain install nightly --component miri + cargo +nightly miri setup + cargo +nightly miri test -p purecv --no-default-features --features std -- --report-time + cargo +nightly miri test -p purecv --no-default-features --features std,simd -- --report-time + ``` + **Caveat:** local runs are `x86_64-pc-windows-msvc`; CI is `ubuntu-latest`. Miri's + intrinsic support is target-dependent, so a local pass does not guarantee a CI + pass. Local is for iteration speed; CI is the source of truth. +2. Update §6 and §9 with measured results. +3. Add `.github/workflows/miri.yml` (§9). +4. Annotate any failing tests per the §4 convention. +5. README: add badge, correct the SIMD `unsafe` claim (§10). +6. Quality gate per `CLAUDE.md`, in order: `cargo fmt` → `cargo clippy` (zero + warnings) → `cargo test`. + +### Commits + +| # | Message | +|---|---------| +| 1 | `doc(ci): add Miri UB verification plan (#81)` | +| 2 | `chore(ci): add Miri UB check workflow (#81)` | +| 3 | `test(core): annotate Miri-incompatible tests (#81)` — *only if the spike requires it* | +| 4 | `doc(readme): add Miri badge, correct SIMD unsafe claim (#81)` | + +--- + +## 9. Workflow + +`.github/workflows/miri.yml`: + +```yaml +name: Miri + +on: + push: + branches: [ "main", "dev" ] + pull_request: + workflow_dispatch: + +env: + CARGO_TERM_COLOR: always + MIRIFLAGS: -Zmiri-deterministic-floats + +jobs: + miri: + name: Miri UB Check (${{ matrix.name }}) + runs-on: ubuntu-latest + timeout-minutes: 30 + continue-on-error: ${{ matrix.experimental }} + strategy: + fail-fast: false + matrix: + include: + # Safe paths + data_ptr tests. Required gate. + - { name: baseline, features: "std", experimental: false } + # Reaches the real unsafe blocks. Advisory until pulp/Miri + # compatibility is proven — see .agents/MIRI_PLAN.md §3. + - { name: simd, features: "std,simd", experimental: true } + steps: + - uses: actions/checkout@v6 + + - uses: dtolnay/rust-toolchain@nightly + with: + components: miri + + - uses: Swatinem/rust-cache@v2 + with: + key: miri-${{ matrix.name }} + + # Separate step: when a broken nightly ships without a usable miri + # component, the failure points at the toolchain, not at our code. + - name: Build Miri sysroot + run: cargo miri setup + + # -p purecv excludes crates/wasm (Miri has no wasm32 support). + # --no-default-features suppresses `parallel`, which default = ["std", "parallel"] + # would otherwise enable silently. + # --lib skips doc-tests (see "Doc-tests" below). + - name: Run Miri + run: cargo miri test -p purecv --lib --no-default-features --features ${{ matrix.features }} +``` + +> The file itself is authoritative; this listing is abridged. See +> `.github/workflows/miri.yml` for the full inline commentary. + +### Doc-tests are excluded via `--lib` + +`cargo miri test` runs doc-tests by default, and they are expensive: the two ORB +examples in `src/features2d/mod.rs` cost **582.9s and 184.7s** — 767s combined, more +than three times the entire unit-test suite. They duplicate the coverage of +`test_orb_full_pipeline`, itself already excluded on time grounds. + +`--lib` is safe here because there is **no `tests/` directory** — every test in the +project lives under `src/`, so `--lib` is the whole suite. Doc-tests continue to be +exercised by the ordinary `cargo test` in `ci.yml`; they are skipped only under Miri. + +This cost was invisible during the first baseline run, which aborted at the +`test_randn_determinism` failure before reaching the doc-test phase. + +### Operational notes + +- **Broken nightly.** If `cargo miri setup` fails because a given nightly shipped + without miri, temporarily pin a known-good date + (`dtolnay/rust-toolchain@nightly-YYYY-MM-DD`) and revert once upstream recovers. +- **Timeout.** 30 minutes, against a measured ~4 min of test execution. Generous + headroom for a cold cache and a slower runner, while still failing fast if a + future change reintroduces a pathological test. +- **Reproducing locally.** Use the exact commands in §8. +- **Measured runtime** (`x86_64-pc-windows-msvc`, 2026-08-08): + + Final figures are the two legs exactly as CI runs them, on `dev` @ v0.7.0: + + | Run | Result | Test time | + |-----|--------|-----------| + | **Leg 1 — `std`, `--lib`** | **299 passed, 0 failed, 9 ignored** | **271s** | + | **Leg 2 — `std,simd`, `--lib`** | **355 passed, 0 failed, 9 ignored** | **229s** | + + Neither leg reported undefined behaviour, an unsupported operation, or a Stacked + Borrows violation. **All six production `unsafe` blocks reachable without + `parallel` are verified UB-free.** + + For reference, the discarded configurations that motivated the exclusions: + + | Run | Result | Test time | + |-----|--------|-----------| + | Baseline before exclusions | 307 passed, 1 failed | 2469s (41 min) | + | Doc-tests (now excluded via `--lib`) | passed | 767s for 2 of them | + + Note leg 1 is *slower* than leg 2 despite running 56 fewer tests: under `simd`, + several kernels process data in chunks that cost Miri less to interpret than the + equivalent scalar loops. + +--- + +## 10. README Changes + +Badge, placed after the existing Rust CI badge: + +```markdown +[![Miri](https://github.com/webarkit/purecv/actions/workflows/miri.yml/badge.svg)](https://github.com/webarkit/purecv/actions/workflows/miri.yml) +``` + +**Accuracy fix.** The Portable SIMD bullet currently reads *"Zero `unsafe`, zero +`#[cfg(target_arch)]`."* The second half is true; the first is not — `arithm.rs:99` +and `derivatives.rs:346` use `from_raw_parts` to feed pulp. Proposed replacement: + +> **Portable SIMD:** Optional SIMD acceleration via [`pulp`](https://crates.io/crates/pulp) — +> auto-detects x86 SSE/AVX, ARM NEON, and WASM `simd128` at runtime. Zero +> `#[cfg(target_arch)]`, and the few `unsafe` slice reinterpretations feeding the +> SIMD kernels are verified UB-free by [Miri](https://github.com/rust-lang/miri) in CI. + +This turns a claim Miri would contradict into one Miri actively backs. + +--- + +## 11. Decision Log + +| # | Decision | Alternatives considered | Rationale | +|---|----------|-------------------------|-----------| +| 1 | Two runs: `std` required + `std,simd` advisory | simd-only required; no-simd only (as issue) | Only option that inspects real `unsafe` without risking a permanently-red required gate | +| 2 | Measure runtime before excluding anything | Pre-emptive `cfg_attr` ignores; shrink inputs under `cfg(miri)` | Don't disable tests on speculation; §6/A2 shows the risk is lower than feared | +| 3 | `-p purecv --no-default-features --features std[,simd]` | `--workspace`; `--workspace --exclude`; default features | Excludes wasm structurally; makes the absent `parallel` explicit rather than accidental | +| 4 | No `MIRIFLAGS`; rely on Miri defaults | Issue's `-Zmiri-strict-provenance`; Tree Borrows; both models | Flag is redundant and deprecated; Stacked Borrows is the stricter guarantee | +| 5 | Floating `@nightly` + documented pin fallback | Dated pin; permanently non-blocking job | Lowest maintenance for a solo maintainer; rare breakage is recoverable in one line | +| 6 | Triggers match existing CI (push + PR), plus `workflow_dispatch` | Cron-only; PR + nightly schedule | Feedback at review time; dispatch allows re-running the simd leg without pushing | +| 7 | Separate `.github/workflows/miri.yml` | Job inside `ci.yml`; manual shields.io badge | Badges are per-workflow; also isolates nightly flakiness from the main CI badge | +| 8 | Plan document at `.agents/MIRI_PLAN.md` | Workflow comments only; both | Matches existing convention (`SIMD_PLAN_v2.md`, `roadmap.md`) | +| 9 | Matrix strategy, one job | Two discrete jobs; one job with two sequential steps | Matches the problem shape; parallel legs; promotion is a one-word diff. YAGNI on divergence that isn't needed yet | + +### Amendments after the spike + +| # | Decision | Alternatives considered | Rationale | +|---|----------|-------------------------|-----------| +| 10 | Set `MIRIFLAGS: -Zmiri-deterministic-floats` — **revises #4** | `#[cfg_attr(miri, ignore)]` on `test_randn_determinism`; leave the failure | #4 said "no flags", but measurement found a genuine need. The flag keeps the RNG determinism contract under test instead of disabling it, and targets a documented Miri behaviour rather than a real defect | +| 11 | Exclude the 9 tests over 30s — **implements #2** | Exclude >10s (15 tests); shrink inputs under `cfg(miri)`; exclude nothing and raise the timeout | Measurement showed an extreme skew: 5 tests = 82% of runtime, 293 tests = 139s. Nine annotations buy a 10× speedup; none of the nine touch `unsafe`, so no UB coverage is lost | + +--- + +## 12. Acceptance Criteria Mapping + +| Criterion (issue #81) | Addressed by | +|-----------------------|--------------| +| Miri CI job passes on `dev` | ✅ Both legs green locally (§9). Pending confirmation on `ubuntu-latest`. | +| All existing unsafe verified UB-free, or documented exceptions | ✅ 6 of 6 reachable production blocks verified clean. Exception: the `parallel`-only pair, documented in §5. | +| Incompatible tests annotated `#[cfg_attr(miri, ignore)]` | ✅ 9 tests, each with a reason comment (§4). Excluded for runtime, not incompatibility — nothing in the suite proved Miri-incompatible. | +| Plan document identifying included/excluded code with rationale | ✅ This document, tracked in git via a `.gitignore` exception. | diff --git a/.gitignore b/.gitignore index 64777fc..95fcd37 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,7 @@ cpp_ref/opencv !.agents/instructions.md !.agents/workflows/ !.agents/workflows/add-license-headers.md +!.agents/MIRI_PLAN.md # Examples examples/data/out/ From b5bac2ee2872e16b60786a158f2f239fe1da9172 Mon Sep 17 00:00:00 2001 From: Walter Perdan Date: Sat, 8 Aug 2026 14:21:42 +0200 Subject: [PATCH 2/8] chore(ci): add Miri UB check workflow (#81) Two matrix legs against `-p purecv --lib`: a required baseline on `std`, and an advisory leg on `std,simd` that reaches the unsafe slice reinterpretations in arithm.rs and derivatives.rs. Kept in its own workflow file because GitHub badges are per-workflow, not per-job, and to keep nightly flakiness out of the main CI badge. MIRIFLAGS uses -Zmiri-deterministic-floats, not the -Zmiri-strict-provenance the issue proposed: strict provenance is now Miri's default and that flag is deprecated, while Miri's float error injection breaks the Box-Muller determinism test in rng.rs. --lib skips doc-tests, where two ORB examples cost 767s under interpretation while duplicating unit-test coverage. Co-Authored-By: Claude Opus 5 --- .github/workflows/miri.yml | 79 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 .github/workflows/miri.yml diff --git a/.github/workflows/miri.yml b/.github/workflows/miri.yml new file mode 100644 index 0000000..3ad6d34 --- /dev/null +++ b/.github/workflows/miri.yml @@ -0,0 +1,79 @@ +name: Miri + +# Undefined-behaviour verification for the `unsafe` slice reinterpretations +# backing the SIMD fast paths. Full rationale, inclusion/exclusion table and +# known coverage gaps: .agents/MIRI_PLAN.md + +on: + push: + branches: [ "main", "dev" ] + pull_request: + workflow_dispatch: + +env: + CARGO_TERM_COLOR: always + # By default Miri injects a small random error into transcendental float ops + # (ln/sin/cos/exp) to catch code that relies on exact results. The Box-Muller + # transform in src/core/rng.rs uses all three, which makes + # test_randn_determinism fail on a last-ULP difference — a false positive, not + # UB. This flag restores deterministic float semantics. + # NOT the issue's `-Zmiri-strict-provenance`: that is now Miri's default and + # the flag is deprecated. See .agents/MIRI_PLAN.md §3. + MIRIFLAGS: -Zmiri-deterministic-floats + +jobs: + miri: + name: Miri UB Check (${{ matrix.name }}) + runs-on: ubuntu-latest + # Measured ~4 min of test execution locally; this is headroom for a cold + # cache and a slower runner. See .agents/MIRI_PLAN.md §9. + timeout-minutes: 30 + continue-on-error: ${{ matrix.experimental }} + + strategy: + fail-fast: false + matrix: + include: + # Safe code paths plus the data_ptr/data_ptr_mut tests in + # src/core/tests.rs — the only `unsafe` reachable without `simd`. + # Required gate. + - { name: baseline, features: "std", experimental: false } + # Reaches the real production `unsafe`: src/core/arithm.rs (119, 267) + # and src/imgproc/derivatives.rs (346, 349). + # pulp is confirmed Miri-compatible and this leg passes clean locally + # (355 passed / 0 failed on x86_64-pc-windows-msvc). Kept advisory only + # until it has been seen green on ubuntu-latest, since Miri's intrinsic + # support is target-dependent. Promote by setting experimental: false. + # See .agents/MIRI_PLAN.md §3. + - { name: simd, features: "std,simd", experimental: true } + + steps: + - uses: actions/checkout@v6 + + - name: Install Rust nightly + Miri + uses: dtolnay/rust-toolchain@nightly + with: + components: miri + + - name: Rust Cache + uses: Swatinem/rust-cache@v2 + with: + key: miri-${{ matrix.name }} + + # Kept as its own step: when a nightly ships without a usable miri + # component, the failure points at the toolchain rather than at our code. + # Fix is to temporarily pin `dtolnay/rust-toolchain@nightly-YYYY-MM-DD`. + - name: Build Miri sysroot + run: cargo miri setup + + # `-p purecv` excludes crates/wasm — Miri has no wasm32 support. + # `--no-default-features` suppresses `parallel`, which default = + # ["std", "parallel"] would otherwise enable silently. Rayon under Miri is + # slow and its thread support is limited; the resulting coverage gap on + # src/core/arithm.rs (99, 248) is documented in .agents/MIRI_PLAN.md §5. + # `--lib` skips doc-tests: two ORB examples in src/features2d/mod.rs alone + # cost 767s under interpretation while duplicating unit-test coverage. There + # is no tests/ directory, so --lib is the whole suite. Doc-tests remain fully + # exercised by `cargo test` in ci.yml. + - name: Run Miri + run: cargo miri test -p purecv --lib --no-default-features --features ${{ matrix.features }} From 343d740838176a5b116d648b06741813efa3155f Mon Sep 17 00:00:00 2001 From: Walter Perdan Date: Sat, 8 Aug 2026 14:21:50 +0200 Subject: [PATCH 3/8] test(core): annotate Miri-slow tests with cfg_attr(miri, ignore) (#81) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nine tests exceed 30s under Miri interpretation and together account for 94% of the suite's runtime; the remaining 293 tests finish in 139s. The annotations take a full run from 41 minutes to roughly 4. Excluded for runtime only — none of the nine contains or reaches unsafe, and nothing in the suite proved Miri-incompatible. Each carries a reason comment so the exclusion list stays derivable by grep. test_build_pyramid_with_derivatives was the one borderline case: it exercises the Sobel unsafe fast path, but that coverage is retained by imgproc::tests::test_sobel, which hits the same f32/ksize-3 trigger in 0.8s under Miri. The attributes vanish under normal cargo test — all tests still run there. Co-Authored-By: Claude Opus 5 --- src/calib3d/tests.rs | 3 +++ src/core/rng.rs | 3 +++ src/features2d/tests.rs | 6 ++++++ src/video/tests.rs | 16 ++++++++++++++++ 4 files changed, 28 insertions(+) diff --git a/src/calib3d/tests.rs b/src/calib3d/tests.rs index b353dfe..b306d52 100644 --- a/src/calib3d/tests.rs +++ b/src/calib3d/tests.rs @@ -517,6 +517,9 @@ mod calib3d_tests { } } + // miri: RANSAC iteration loop takes ~928s under interpretation — by far the + // slowest test in the suite. No `unsafe` on this path. See .agents/MIRI_PLAN.md §4. + #[cfg_attr(miri, ignore)] #[test] fn test_find_fundamental_mat_ransac() { use crate::calib3d::{find_fundamental_mat, FundamentalMatMethod}; diff --git a/src/core/rng.rs b/src/core/rng.rs index af4028f..f2635bc 100644 --- a/src/core/rng.rs +++ b/src/core/rng.rs @@ -380,6 +380,9 @@ mod tests { assert!(max > min, "randu produced no variation"); } + // miri: draws a large sample to check distribution moments — ~88s under + // interpretation. No `unsafe` on this path. See .agents/MIRI_PLAN.md §4. + #[cfg_attr(miri, ignore)] #[test] fn test_randn_statistics() { set_rng_seed(7); diff --git a/src/features2d/tests.rs b/src/features2d/tests.rs index 40fefc5..a7440e5 100644 --- a/src/features2d/tests.rs +++ b/src/features2d/tests.rs @@ -302,6 +302,9 @@ fn test_orb_pyramid_grayscale_validation() { } } +// miri: ORB scale-pyramid construction takes ~30s under interpretation. +// No `unsafe` on this path. See .agents/MIRI_PLAN.md §4. +#[cfg_attr(miri, ignore)] #[test] fn test_orb_pyramid_dimensions() { use crate::core::Matrix; @@ -415,6 +418,9 @@ fn test_orb_descriptors() { assert_ne!(desc0, desc90); } +// miri: full ORB detect+describe pipeline takes ~806s under interpretation. +// No `unsafe` on this path. See .agents/MIRI_PLAN.md §4. +#[cfg_attr(miri, ignore)] #[test] fn test_orb_full_pipeline() { use crate::core::Matrix; diff --git a/src/video/tests.rs b/src/video/tests.rs index 2517d52..37bbf95 100644 --- a/src/video/tests.rs +++ b/src/video/tests.rs @@ -84,6 +84,10 @@ mod video_tests { assert_eq!(pyr.levels[3].rows, 8); } + // miri: ~45s under interpretation. The Sobel `unsafe` fast path it exercises + // is still covered by imgproc::tests::test_sobel (f32/ksize 3, ~0.8s under + // Miri), so no UB coverage is lost here. See .agents/MIRI_PLAN.md §4. + #[cfg_attr(miri, ignore)] #[test] fn test_build_pyramid_with_derivatives() { let img = Matrix::::new(64, 64, 1); @@ -212,6 +216,9 @@ mod video_tests { /// Tracking a stationary point in two identical frames should return a /// flow vector close to zero and status = 1. + // miri: Lucas-Kanade pyramidal iteration — ~62s under interpretation. + // No `unsafe` on this path. See .agents/MIRI_PLAN.md §4. + #[cfg_attr(miri, ignore)] #[test] fn test_lk_stationary_point_identical_frames() { // Create a 64×64 frame with a small bright blob so there are gradients. @@ -251,6 +258,9 @@ mod video_tests { } /// Simulate a pure translation of +3 pixels in x by shifting the image. + // miri: Lucas-Kanade pyramidal iteration — ~105s under interpretation. + // No `unsafe` on this path. See .agents/MIRI_PLAN.md §4. + #[cfg_attr(miri, ignore)] #[test] fn test_lk_pure_translation_x() { let rows = 64usize; @@ -311,6 +321,9 @@ mod video_tests { } /// Test using the `OPTFLOW_LK_GET_MIN_EIGENVALS` flag. + // miri: Lucas-Kanade pyramidal iteration — ~61s under interpretation. + // No `unsafe` on this path. See .agents/MIRI_PLAN.md §4. + #[cfg_attr(miri, ignore)] #[test] fn test_lk_min_eigenvals_flag() { let mut data = vec![0u8; 64 * 64]; @@ -343,6 +356,9 @@ mod video_tests { } /// Test the `OPTFLOW_USE_INITIAL_FLOW` flag with a good initial guess. + // miri: Lucas-Kanade pyramidal iteration — ~62s under interpretation. + // No `unsafe` on this path. See .agents/MIRI_PLAN.md §4. + #[cfg_attr(miri, ignore)] #[test] fn test_lk_use_initial_flow() { let mut data = vec![0u8; 64 * 64]; From 184d24da57d982b9d8acc9afbe605ec513446350 Mon Sep 17 00:00:00 2001 From: Walter Perdan Date: Sat, 8 Aug 2026 14:21:59 +0200 Subject: [PATCH 4/8] doc(readme): add Miri badge, correct SIMD unsafe claim (#81) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Portable SIMD bullet claimed "Zero unsafe, zero #[cfg(target_arch)]". The second half is true; the first is not — arithm.rs and derivatives.rs use from_raw_parts to feed pulp. Reworded to state that those reinterpretations are Miri-checked in CI, turning a claim Miri would contradict into one it actively backs. Co-Authored-By: Claude Opus 5 --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 4fa6042..86befb2 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ ![PureCv Banner](./assets/purecv_banner.png) [![Rust CI](https://github.com/webarkit/purecv/actions/workflows/ci.yml/badge.svg)](https://github.com/webarkit/purecv/actions/workflows/ci.yml) +[![Miri](https://github.com/webarkit/purecv/actions/workflows/miri.yml/badge.svg)](https://github.com/webarkit/purecv/actions/workflows/miri.yml) [![Crates.io](https://img.shields.io/crates/v/purecv.svg)](https://crates.io/crates/purecv) [![Crates.io Downloads](https://img.shields.io/crates/d/purecv.svg)](https://crates.io/crates/purecv) [![NPM version](https://img.shields.io/npm/v/@webarkit/purecv-wasm.svg)](https://www.npmjs.com/package/@webarkit/purecv-wasm) @@ -20,7 +21,7 @@ Unlike existing wrappers, **PureCV** is a native rewrite. It aims to provide: * **Zero-FFI:** No complex linking or C++ toolchain requirements. * **Memory Safety:** Elimination of segmentation faults and buffer overflows via Rust's ownership model. * **Modern Parallelism:** Native integration with **Rayon** for effortless multi-core processing. -* **Portable SIMD:** Optional SIMD acceleration via [`pulp`](https://crates.io/crates/pulp) — auto-detects x86 SSE/AVX, ARM NEON, and WASM `simd128` at runtime. Zero `unsafe`, zero `#[cfg(target_arch)]`. +* **Portable SIMD:** Optional SIMD acceleration via [`pulp`](https://crates.io/crates/pulp) — auto-detects x86 SSE/AVX, ARM NEON, and WASM `simd128` at runtime. Zero `#[cfg(target_arch)]`, and the few `unsafe` slice reinterpretations that feed the SIMD kernels are checked for undefined behaviour by [Miri](https://github.com/rust-lang/miri) in CI. * **Embedded-ready:** Builds under `no_std` + `alloc` for bare-metal targets such as the ESP32 — the `core`, `imgproc`, `calib3d`, and `video` modules run without the standard library ([see below](#no_std--embedded-support)). ## ✨ Features From fbbbbc328412ef5fc0f95a23ebccc0cfd59b4da0 Mon Sep 17 00:00:00 2001 From: Walter Perdan Date: Mon, 10 Aug 2026 16:14:18 +0200 Subject: [PATCH 5/8] chore(ci): promote the Miri simd leg to a required check (#81) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The simd leg was introduced advisory in case Miri's target-dependent intrinsic support differed between the local Windows host and CI. It ran green on ubuntu-latest with counts identical to local — 355 passed, 0 failed, 9 ignored on both — so the hedge has served its purpose. Leaving it advisory would mean the only leg that reaches production unsafe could not actually block a bad merge. Plan document updated to match: leg 2 is now documented as required, and the header records the verification result and links to the follow-up issue for the parallel+simd coverage gap. Co-Authored-By: Claude Opus 5 --- .agents/MIRI_PLAN.md | 29 +++++++++++++++-------------- .github/workflows/miri.yml | 12 +++++------- 2 files changed, 20 insertions(+), 21 deletions(-) diff --git a/.agents/MIRI_PLAN.md b/.agents/MIRI_PLAN.md index 3f9fa89..a8c68dd 100644 --- a/.agents/MIRI_PLAN.md +++ b/.agents/MIRI_PLAN.md @@ -1,8 +1,10 @@ # Miri UB Verification Plan -> **Status:** Design accepted, implementation pending. -> **Issue:** [webarkit/purecv#81](https://github.com/webarkit/purecv/issues/81) -> **Branch:** `feat/issue-81-miri-ci` (from `dev`) +> **Status:** Implemented. Both legs green on `ubuntu-latest` and locally — **no +> undefined behaviour found** in any `unsafe` block they reach. +> **Issue:** [webarkit/purecv#81](https://github.com/webarkit/purecv/issues/81) · +> **PR:** [#93](https://github.com/webarkit/purecv/pull/93) · +> **Follow-up:** [#94](https://github.com/webarkit/purecv/issues/94) (the `parallel+simd` gap, §5) --- @@ -18,8 +20,8 @@ provenance, aliasing violations (Stacked Borrows), misaligned access, uninitiali memory reads, and data races. **What Miri cannot check here:** anything compiled for `wasm32` (unsupported target), -anything not reached by a `#[test]`, and — pending verification — code paths using -SIMD intrinsics that Miri has no shim for. +and anything not reached by a `#[test]`. Note that pulp's intrinsics turned out to be +fully supported (§6, A1), so no SIMD path was lost to a missing shim. --- @@ -84,7 +86,7 @@ Covers all safe code paths and the `data_ptr` tests. Expected to pass trivially. Its job is to be a fast, stable gate that catches UB regressions in safe code and in any future non-SIMD `unsafe`. -### Leg 2 — `simd` · **advisory at first** (`continue-on-error: true`) +### Leg 2 — `simd` · **required** ```bash cargo miri test -p purecv --lib --no-default-features --features std,simd @@ -93,12 +95,10 @@ cargo miri test -p purecv --lib --no-default-features --features std,simd The leg that does the real work: it reaches `arithm.rs:119,267` and `derivatives.rs:346,349`. -pulp is **confirmed Miri-compatible** (A1, §6) and this leg passes clean locally. -It is nonetheless kept advisory for its first CI run, because Miri's intrinsic -support is target-dependent and all local evidence is from -`x86_64-pc-windows-msvc` rather than CI's `ubuntu-latest`. **Promote to required -by setting `experimental: false`** as soon as it has been observed green on Linux — -this is expected to be immediate, not a long-term state. +pulp is **confirmed Miri-compatible** (A1, §6). This leg was designed to start +advisory (`experimental: true`) in case Miri's target-dependent intrinsic support +differed on CI, and was promoted to required once it ran green on `ubuntu-latest` +with counts identical to local Windows — 355 passed, 0 failed, 9 ignored on both. ### Aliasing model and MIRIFLAGS @@ -325,13 +325,14 @@ jobs: name: Miri UB Check (${{ matrix.name }}) runs-on: ubuntu-latest timeout-minutes: 30 - continue-on-error: ${{ matrix.experimental }} + continue-on-error: ${{ matrix.experimental }} # both legs now false strategy: fail-fast: false matrix: include: # Safe paths + data_ptr tests. Required gate. - { name: baseline, features: "std", experimental: false } + - { name: simd, features: "std,simd", experimental: false } # Reaches the real unsafe blocks. Advisory until pulp/Miri # compatibility is proven — see .agents/MIRI_PLAN.md §3. - { name: simd, features: "std,simd", experimental: true } @@ -436,7 +437,7 @@ This turns a claim Miri would contradict into one Miri actively backs. | # | Decision | Alternatives considered | Rationale | |---|----------|-------------------------|-----------| -| 1 | Two runs: `std` required + `std,simd` advisory | simd-only required; no-simd only (as issue) | Only option that inspects real `unsafe` without risking a permanently-red required gate | +| 1 | Two runs: `std` + `std,simd`, the latter advisory until proven on Linux, then required | simd-only required; no-simd only (as issue) | Only option that inspects real `unsafe` without risking a permanently-red required gate | | 2 | Measure runtime before excluding anything | Pre-emptive `cfg_attr` ignores; shrink inputs under `cfg(miri)` | Don't disable tests on speculation; §6/A2 shows the risk is lower than feared | | 3 | `-p purecv --no-default-features --features std[,simd]` | `--workspace`; `--workspace --exclude`; default features | Excludes wasm structurally; makes the absent `parallel` explicit rather than accidental | | 4 | No `MIRIFLAGS`; rely on Miri defaults | Issue's `-Zmiri-strict-provenance`; Tree Borrows; both models | Flag is redundant and deprecated; Stacked Borrows is the stricter guarantee | diff --git a/.github/workflows/miri.yml b/.github/workflows/miri.yml index 3ad6d34..1ee339e 100644 --- a/.github/workflows/miri.yml +++ b/.github/workflows/miri.yml @@ -39,13 +39,11 @@ jobs: # Required gate. - { name: baseline, features: "std", experimental: false } # Reaches the real production `unsafe`: src/core/arithm.rs (119, 267) - # and src/imgproc/derivatives.rs (346, 349). - # pulp is confirmed Miri-compatible and this leg passes clean locally - # (355 passed / 0 failed on x86_64-pc-windows-msvc). Kept advisory only - # until it has been seen green on ubuntu-latest, since Miri's intrinsic - # support is target-dependent. Promote by setting experimental: false. - # See .agents/MIRI_PLAN.md §3. - - { name: simd, features: "std,simd", experimental: true } + # and src/imgproc/derivatives.rs (346, 349). Required gate. + # pulp is confirmed Miri-compatible: this leg passes clean on both + # ubuntu-latest and x86_64-pc-windows-msvc (355 passed / 0 failed, + # identical counts). See .agents/MIRI_PLAN.md §3. + - { name: simd, features: "std,simd", experimental: false } steps: - uses: actions/checkout@v6 From e8a03c6d435aa86acb6b579b567d44871a66bc17 Mon Sep 17 00:00:00 2001 From: Walter Perdan Date: Mon, 10 Aug 2026 16:37:13 +0200 Subject: [PATCH 6/8] doc(ci): fix the local reproduction commands in the Miri plan (#81) Section 8 was written before the spike and its commands no longer worked. Copy-pasting them hit three walls in sequence: --report-time is rejected without -Zunstable-options, the missing MIRIFLAGS made test_randn_determinism fail on a last-ULP float difference, and the missing --lib pulled in ~13 minutes of ORB doc-tests. Replaced with the exact commands CI runs, plus a table of what each omitted flag does so a failure is recognisable as configuration rather than a real regression. Also reframes the section as a record of what shipped rather than a pending checklist, and corrects commit 3's title to the one actually used. Co-Authored-By: Claude Opus 5 --- .agents/MIRI_PLAN.md | 79 ++++++++++++++++++++++++++++++-------------- 1 file changed, 55 insertions(+), 24 deletions(-) diff --git a/.agents/MIRI_PLAN.md b/.agents/MIRI_PLAN.md index a8c68dd..4c50e52 100644 --- a/.agents/MIRI_PLAN.md +++ b/.agents/MIRI_PLAN.md @@ -273,33 +273,64 @@ giving Miri its own workflow file. --- -## 8. Implementation Sequence - -1. **Feasibility spike** — resolve A1/A2 before writing YAML: - ```bash - rustup toolchain install nightly --component miri - cargo +nightly miri setup - cargo +nightly miri test -p purecv --no-default-features --features std -- --report-time - cargo +nightly miri test -p purecv --no-default-features --features std,simd -- --report-time - ``` - **Caveat:** local runs are `x86_64-pc-windows-msvc`; CI is `ubuntu-latest`. Miri's - intrinsic support is target-dependent, so a local pass does not guarantee a CI - pass. Local is for iteration speed; CI is the source of truth. -2. Update §6 and §9 with measured results. -3. Add `.github/workflows/miri.yml` (§9). -4. Annotate any failing tests per the §4 convention. -5. README: add badge, correct the SIMD `unsafe` claim (§10). -6. Quality gate per `CLAUDE.md`, in order: `cargo fmt` → `cargo clippy` (zero - warnings) → `cargo test`. - -### Commits - -| # | Message | -|---|---------| +## 8. Running Miri Locally + +One-time setup: + +```bash +rustup toolchain install nightly --component miri +cargo +nightly miri setup +``` + +Then reproduce either CI leg exactly. **Use these commands verbatim** — each flag +below is load-bearing, and dropping one produces a failure that looks like a real +problem but is not: + +```bash +# Leg 1 — baseline +MIRIFLAGS=-Zmiri-deterministic-floats \ + cargo +nightly miri test -p purecv --lib --no-default-features --features std + +# Leg 2 — simd +MIRIFLAGS=-Zmiri-deterministic-floats \ + cargo +nightly miri test -p purecv --lib --no-default-features --features std,simd +``` + +On PowerShell, set the variable separately: `$env:MIRIFLAGS="-Zmiri-deterministic-floats"`. + +| Omitting… | Symptom | +|-----------|---------| +| `MIRIFLAGS=-Zmiri-deterministic-floats` | `test_randn_determinism` fails on a last-ULP float difference (§6) — looks like a real regression, isn't | +| `--lib` | Doc-tests run, adding ~13 min of ORB examples | +| `--no-default-features` | `parallel` is enabled silently, pulling rayon into the interpreter | +| `-p purecv` | `crates/wasm` is included; Miri has no wasm32 support | + +To profile per-test timings, append `-- -Zunstable-options --report-time`. Note that +`--report-time` **alone is rejected** — libtest requires `-Zunstable-options` with it. + +**Platform caveat.** Local runs here were `x86_64-pc-windows-msvc`; CI is +`ubuntu-latest`. Miri's intrinsic support is target-dependent, so a local pass does +not guarantee a CI pass. In practice the two agreed exactly (§9), but CI remains the +source of truth. + +### How this was delivered + +Shipped in [#93](https://github.com/webarkit/purecv/pull/93), in this order: plan +document → workflow → test annotations → README. The feasibility spike ran *before* +any YAML was written, which is what surfaced the 41-minute runtime, the doc-test +cost, and the float-determinism false positive — all three would have landed as +broken CI otherwise. + +| # | Commit | +|---|--------| | 1 | `doc(ci): add Miri UB verification plan (#81)` | | 2 | `chore(ci): add Miri UB check workflow (#81)` | -| 3 | `test(core): annotate Miri-incompatible tests (#81)` — *only if the spike requires it* | +| 3 | `test(core): annotate Miri-slow tests with cfg_attr(miri, ignore) (#81)` | | 4 | `doc(readme): add Miri badge, correct SIMD unsafe claim (#81)` | +| 5 | `chore(ci): promote the Miri simd leg to a required check (#81)` | + +Quality gate per `CLAUDE.md` before each commit, in order: `cargo fmt` → +`cargo clippy` (zero warnings) → `cargo test`. --- From 9c05e2d2825e766da54072fd6c9887ccf90767f6 Mon Sep 17 00:00:00 2001 From: Walter Perdan Date: Mon, 10 Aug 2026 19:12:18 +0200 Subject: [PATCH 7/8] chore(ci): gate releases on the Miri UB checks (#81) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both Miri legs are required checks on PRs and dev pushes, but release.yml only waited on Build and Test, WASM Dual Build, and Benchmarks — so a UB regression could still reach crates.io. Adds wait-on-check steps for both legs, and the tag trigger to miri.yml that makes them possible: ci.yml already ran on `v*` tags but miri.yml did not, so waiting on a Miri check would have hung the release indefinitely. Job names are matched exactly against miri.yml's matrix output. Co-Authored-By: Claude Opus 5 --- .github/workflows/miri.yml | 4 ++++ .github/workflows/release.yml | 20 ++++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/.github/workflows/miri.yml b/.github/workflows/miri.yml index 1ee339e..1cdf67a 100644 --- a/.github/workflows/miri.yml +++ b/.github/workflows/miri.yml @@ -7,6 +7,10 @@ name: Miri on: push: branches: [ "main", "dev" ] + # Tags matter here: release.yml waits on both Miri legs before publishing to + # crates.io, and that wait would hang forever if this workflow never ran on + # the tag. Keep in sync with ci.yml's tag trigger. + tags: [ "v*" ] pull_request: workflow_dispatch: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5bbfc95..e79e3a1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -42,6 +42,26 @@ jobs: repo-token: ${{ secrets.GITHUB_TOKEN }} wait-interval: 30 + # Both Miri legs are required checks, so a UB regression must not be + # publishable to crates.io. These names must match the job names produced + # by miri.yml's matrix, and miri.yml must keep its `tags: [ "v*" ]` + # trigger — without it these steps would wait forever. + - name: Wait for CI (Miri UB Check - baseline) + uses: lewagon/wait-on-check-action@v1.5.0 + with: + ref: ${{ github.ref }} + check-name: "Miri UB Check (baseline)" + repo-token: ${{ secrets.GITHUB_TOKEN }} + wait-interval: 30 + + - name: Wait for CI (Miri UB Check - simd) + uses: lewagon/wait-on-check-action@v1.5.0 + with: + ref: ${{ github.ref }} + check-name: "Miri UB Check (simd)" + repo-token: ${{ secrets.GITHUB_TOKEN }} + wait-interval: 30 + - name: Generate Release Notes id: git-cliff uses: orhun/git-cliff-action@v4 From 81e48eae4f0e4ca1c587dc020dab3d9a2d3508fe Mon Sep 17 00:00:00 2001 From: Walter Perdan Date: Mon, 10 Aug 2026 19:20:09 +0200 Subject: [PATCH 8/8] chore(release): prepare for v0.7.1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps package and workspace versions in Cargo.toml, the root package.json, and the generated crates/wasm/pkg/package.json, and prepends the git-cliff changelog entry. The wasm pkg manifest was still at 0.6.1 — it was not refreshed during the v0.7.0 release, even though 0.7.0 was published to npm from a locally built pkg. Running `npm run build` resyncs it, so this also clears that drift. No library code changed since v0.7.0 — the release carries the Miri UB verification work: CI workflow, plan document, README badge and safety-claim correction, and cfg_attr(miri, ignore) annotations on nine slow tests. The published crate differs from v0.7.0 only in its README. Co-Authored-By: Claude Opus 5 --- CHANGELOG.md | 18 ++++++++++++++++++ Cargo.toml | 4 ++-- crates/wasm/pkg/package.json | 2 +- package.json | 2 +- 4 files changed, 22 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 52ab057..ce645a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,24 @@ All notable changes to this project will be documented in this file. +## [0.7.1] - 2026-08-10 + +### ⚙️ Miscellaneous Tasks + +- *(ci)* Add Miri UB check workflow (#81) +- *(ci)* Promote the Miri simd leg to a required check (#81) +- *(ci)* Gate releases on the Miri UB checks (#81) + +### 📚 Documentation + +- *(ci)* Add Miri UB verification plan (#81) +- *(readme)* Add Miri badge, correct SIMD unsafe claim (#81) +- *(ci)* Fix the local reproduction commands in the Miri plan (#81) + +### 🧪 Testing + +- *(core)* Annotate Miri-slow tests with cfg_attr(miri, ignore) (#81) + ## [0.7.0] - 2026-08-07 ### ⚙️ Miscellaneous Tasks diff --git a/Cargo.toml b/Cargo.toml index 2ed90aa..5a1b509 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "purecv" -version = "0.7.0" +version = "0.7.1" authors = ["Walter Perdan "] edition = "2021" rust-version = "1.88" @@ -86,7 +86,7 @@ members = ["crates/wasm"] exclude = ["crates/no-std-smoke"] [workspace.package] -version = "0.7.0" +version = "0.7.1" authors = ["Walter Perdan "] edition = "2021" description = "A pure Rust, high-performance computer vision library focused on safety and portability." diff --git a/crates/wasm/pkg/package.json b/crates/wasm/pkg/package.json index e15768a..923a255 100644 --- a/crates/wasm/pkg/package.json +++ b/crates/wasm/pkg/package.json @@ -5,7 +5,7 @@ "Walter Perdan \u003chttps://github.com/kalwalt\u003e" ], "description": "A pure Rust, high-performance computer vision library focused on safety and portability.", - "version": "0.6.1", + "version": "0.7.1", "license": "LGPL-2.1-or-later", "repository": { "type": "git", diff --git a/package.json b/package.json index 19cd6ec..dada5ed 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "purecv", - "version": "0.7.0", + "version": "0.7.1", "description": "A pure Rust, high-performance computer vision library focused on safety and portability.", "private": true, "scripts": {