|
| 1 | +// SPDX-License-Identifier: MPL-2.0 |
| 2 | +// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk> |
| 3 | += ADR 0006 — SNIF host runtime: pinned, patched, no-WASI wasmtime |
| 4 | + |
| 5 | +Date: 2026-06-25 |
| 6 | + |
| 7 | +== Status |
| 8 | + |
| 9 | +Accepted. |
| 10 | + |
| 11 | +== Context |
| 12 | + |
| 13 | +A SNIF's whole value proposition is *sandbox integrity*: a guest fault must be |
| 14 | +trapped, never an escape into the BEAM's address space (ADR 0005). That guarantee |
| 15 | +is only as strong as the host wasmtime the sandbox runs in. |
| 16 | + |
| 17 | +Two **critical** wasmtime advisories break exactly that guarantee: |
| 18 | + |
| 19 | +* **GHSA-xx5w-cvp6-jv83** — Winch compiler backend on aarch64: a sandbox-escaping |
| 20 | + memory access. Patched in `42.0.2` (for the `>= 37.0.0, < 42.0.2` line). |
| 21 | +* **GHSA-jhxm-h53p-jm7w** — miscompiled guest heap access on aarch64 Cranelift: |
| 22 | + sandbox escape. Patched in `42.0.2`. |
| 23 | + |
| 24 | +Both are *sandbox escapes* — the one failure mode a SNIF exists to prevent — on |
| 25 | +aarch64 (Apple Silicon, ARM servers/CI). |
| 26 | + |
| 27 | +The host wasmtime is supplied transitively by `wasmex`. The current pin |
| 28 | +(`{:wasmex, "~> 0.14"}`) resolves to wasmex `0.14.0`, whose bundled wasmtime is in |
| 29 | +the **vulnerable** range, and which ships as a **precompiled** NIF |
| 30 | +(`rustler_precompiled`) — so the vulnerable runtime is baked into a downloaded |
| 31 | +binary, not even visible at the demo's build. Critically, **upstream wasmex has |
| 32 | +no release with a patched wasmtime** (latest `0.14.0` bundles wasmtime `39.0.1`), |
| 33 | +so "bump wasmex" is not a fix. wasmex also pulls in `wasmtime-wasi` / |
| 34 | +`wasi-common`, which SNIF guests never use — they are `wasm32-freestanding`, no |
| 35 | +WASI (ADR 0005) — so that surface is pure, unused attack surface. |
| 36 | + |
| 37 | +== Decision |
| 38 | + |
| 39 | +SNIF owns its host runtime version and configuration rather than inheriting |
| 40 | +whatever wasmex happens to bundle. The SNIF host runtime is: |
| 41 | + |
| 42 | +. **Patched wasmtime — pinned by SNIF, ≥ `42.0.2`** (current pin: the latest |
| 43 | + stable patched line, `43.0.1`+). Achieved by building wasmex **from source** |
| 44 | + (`force_build: true`) against a SNIF-maintained wasmex pin |
| 45 | + (`hyperpolymath/wasmex`, branch `snif-wasmtime-patched`) whose |
| 46 | + `native/wasmex/Cargo.toml` bumps `wasmtime`/`wasmtime-wasi`/`wasmtime-wasi-http` |
| 47 | + to the patched line and is kept current by Dependabot + the estate dep-security |
| 48 | + sweep. No precompiled NIFs for the host. |
| 49 | +. **Cranelift only — Winch disabled.** Winch is not used; removing it closes |
| 50 | + GHSA-xx5w-cvp6-jv83 by construction and shrinks the codegen surface. |
| 51 | +. **No WASI.** `wasmtime-wasi` and `wasi-common` are dropped from the host build |
| 52 | + (default features off; only the `cranelift` feature on). Guests are |
| 53 | + freestanding (ADR 0005); WASI is dead weight and attack surface. |
| 54 | +. **Fuel-metered, store-capped** as today (ADR 0004) — unchanged; this ADR is |
| 55 | + about *which* wasmtime and *how configured*, not the liveness guards. |
| 56 | + |
| 57 | +The pinned-and-patched property is a **gate**: CI fails if the resolved host |
| 58 | +wasmtime is in any advisory's vulnerable range (a `cargo audit` / advisory check |
| 59 | +over the built `Cargo.lock`). |
| 60 | + |
| 61 | +== Consequences |
| 62 | + |
| 63 | +=== Positive |
| 64 | +- The two critical sandbox-escape CVEs are removed; the SNIF integrity claim holds |
| 65 | + on aarch64 as well as x86_64. |
| 66 | +- SNIF's sandbox integrity no longer depends on upstream wasmex's release cadence |
| 67 | + or its choice of wasmtime version — SNIF pins and patches it directly. |
| 68 | +- Smaller host: no Winch, no WASI → less codegen and less unused surface. |
| 69 | + |
| 70 | +=== Negative / Neutral |
| 71 | +- The host NIF must be **built from source** (no precompiled fast path); CI and |
| 72 | + contributor setup need a Rust toolchain (already required for the guest). |
| 73 | +- SNIF carries a thin wasmex fork pin (`hyperpolymath/wasmex@snif-wasmtime-patched`) |
| 74 | + to bump wasmtime ahead of upstream. It tracks upstream wasmex otherwise. |
| 75 | +- Until the fork pin lands + CI verifies the build, the demo's `mix.exs` still |
| 76 | + references upstream wasmex; this ADR is the spec, and the migration steps below |
| 77 | + are the implementation. |
| 78 | + |
| 79 | +== Implementation steps |
| 80 | + |
| 81 | +. Fork wasmex → `hyperpolymath/wasmex`, branch `snif-wasmtime-patched`: bump |
| 82 | + `wasmtime*` to ≥ `42.0.2` in `native/wasmex/Cargo.toml`, set |
| 83 | + `default-features = false, features = ["cranelift"]`, drop the WASI deps, and |
| 84 | + fix any wasmtime 41→43 API breakage in wasmex's Rust. |
| 85 | +. `demo/mix.exs`: `{:wasmex, github: "hyperpolymath/wasmex", branch: |
| 86 | + "snif-wasmtime-patched", force_build: true}`. |
| 87 | +. Add a CI gate: advisory scan of the built `Cargo.lock` must show no wasmtime in |
| 88 | + a vulnerable range (fail the build otherwise). |
| 89 | +. Re-run `demo` tests + the metamorphic/pool suites against the new host. |
| 90 | + |
| 91 | +== References |
| 92 | + |
| 93 | +- ADR 0005 — Rust wasm32 guest + verifier (freestanding, no-WASI guest). |
| 94 | +- ADR 0004 — fuel-metered liveness guard. |
| 95 | +- GHSA-xx5w-cvp6-jv83, GHSA-jhxm-h53p-jm7w (wasmtime aarch64 sandbox escapes). |
0 commit comments