Skip to content

fix(deps)!: bump wasmtime off the unpatched 45.x line (RUSTSEC-2026-0222) - #35

Merged
MichaelTaylor3d merged 6 commits into
mainfrom
loop/1898-wasmtime-cve/digs
Aug 1, 2026
Merged

fix(deps)!: bump wasmtime off the unpatched 45.x line (RUSTSEC-2026-0222)#35
MichaelTaylor3d merged 6 commits into
mainfrom
loop/1898-wasmtime-cve/digs

Conversation

@MichaelTaylor3d

@MichaelTaylor3d MichaelTaylor3d commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Closes the digstore (release-first) half of DIG-Network/dig_ecosystem#1898.

Why

cargo deny check advisories fails on main with RUSTSEC-2026-0222 ("Stores can mix up type indices between engines", GHSA-hgjw-h833-99q9) against wasmtime 45.0.3. The patched ranges are >=24.0.12<25, >=36.0.13<37, >=46.0.2<47, >=47.0.3there is no patch in the 45.x line, so cargo update -p wasmtime is a no-op and this needs a real major move. The CVE is currently blocking every open PR in hub.dig.net, which links digstore-host.

What changed

1. wasmtime 45 → 47.0.3 in crates/digstore-host and crates/digstore-compiler (dev-dep).

Chose 47.0.3 over 46.0.2: both are patched, but 47 is the current maintained line (46 is one major from EOL, so adopting it would buy another forced bump shortly), and 47.0.3 resolved with zero source churn to the embedding API — so it is simultaneously the smaller diff and the longer-lived choice.

2. A real regression the bump exposed — HostRuntime::new armed its sandbox budget in the wrong place.

crates/digstore-host/src/runtime.rs called store.set_fuel(...) / store.set_epoch_deadline(...) after Linker::instantiate. But a module's wasm start function runs inside instantiate, so instantiation is guest execution. Fuel consumption is enabled engine-wide (wcfg.consume_fuel(true)), which means an un-armed store holds zero fuel:

  • Under wasmtime 47, every module with a start function died at instantiation with wasm trap: all fuel consumed by WebAssembly. Two digstore-compiler auth_policy tests went red on the bare dep bump.
  • Under wasmtime 45 the same code left start functions outside the sandbox budget entirely — a latent §18.2 gap, not merely a 47 incompatibility.

Fix: arm fuel + the epoch deadline immediately before instantiate. Two lines, plus a WHY comment. Behaviour is a strict tightening — instantiation is now bounded by exactly the same budget an export call gets.

3. Regression test + spec. New crates/digstore-host/tests/instantiation_bounds.rs with two fixtures, and a normative SPEC.md §7.1 "Serving-module execution bounds" stating that instantiation counts as guest execution and MUST be inside the budget. docs/superpowers/DRIFT-ANALYSIS*.md updated 45 → 47.

Blast radius checked

cargo tree -i wasmtime (not memory) — wasmtime is reached only via digstore-host (real dep) and digstore-compiler (dev-dep only; its single use is tests/obfuscation.rs). Consumers: digstore-remote, digstore-stage, digstore-cli. No other path.

HostRuntime::new (the edited symbol) — grep across the tree: two production callers, crates/digstore-cli/src/ops/serve.rs:133 and crates/digstore-host/src/serve_blind.rs:229, plus 25 test call sites. Signature unchanged. Risk MEDIUM, not high: the only behavioural delta is that a start function is now metered, with the full default budget (5e9 fuel / 2 epoch ticks), and the entire workspace suite exercises both callers.

gitnexus is disabled ecosystem-wide (CLAUDE.md §2.0 temp override — it hangs in worktrees), so the blast radius above was derived from cargo tree -i + a whole-tree grep + reading both call sites.

Backwards compatibility (§5.1)

This is a runtime-only move. The compiler's own wasm-encoder stays pinned at 0.221.3 (only wasmtime's internal copy moved 0.248 → 0.252), so emitted .dig module bytes are unchanged, and wat is 1.253.0 on both sides. Old-format read proven green under 47:

  • digstore-compiler::data_section_goldennew_reader_accepts_older_golden_without_public_manifest, data_section_matches_golden_vector, structure_is_independently_valid (3 passed)
  • digstore-remote::golden_read_proofcommitted_golden_verifies_then_decrypts, golden_bytes_are_byte_stable, tampered_golden_fails_the_gate, rpc_tier_read_proof + 2 (6 passed, 2 pre-existing #[ignore])

No section id, field encoding, or guest-wasm export signature is touched.

SemVer

Workspace 0.19.3 → 0.20.0; digstore-host 0.1.0 → 0.2.0.

digstore-host re-exports wasmtime types across its public API — imports::register(&mut wasmtime::Linker<RuntimeState>) and RuntimeState { pub limits: wasmtime::StoreLimits } — so a wasmtime major is a compatibility break for any crate that links digstore-host directly (which hub.dig.net does). Under Cargo's semver rules for a 0.x crate the compatibility-breaking position is the minor field, so 0.19.3 → 0.20.0 is the major-equivalent bump the ticket asks for; it forces every consumer to opt in explicitly. Jumping to 1.0.0 would be a product statement unrelated to a CVE fix, so it is deliberately not made here. digstore-compiler stays 1.0.0 — its wasmtime dep is dev-only and no public signature moved.

How verified

Gate Result
cargo deny check advisories on main's lock error[vulnerability] ... wasmtime 45.0.3 ... RUSTSEC-2026-0222advisories FAILED
cargo deny check advisories bans sources on this branch advisories ok, bans ok, sources ok
cargo test --workspace 0 failures across ~80 test binaries
cargo clippy --workspace --all-targets -- -D warnings clean
cargo fmt --all --check clean
Installed-binary e2e (§3.5) cargo install --path crates/digstore-cli --force --lockeddig-store 0.20.0, then initaddcommit (real 128 MB .dig, root b1a6d014…) → cat returned the exact plaintext through the wasmtime HostRuntime read path

RUSTSEC-2026-0222 is not in deny.toml's ignore list — the green is a genuine upgrade, not an allowlist. No advisory exception was added.

The new test is load-bearing

Reverting only the two-line fix (from a committed state, via a file copy) re-fails benign_start_function_instantiates_and_runs while the rest of the diff stays in place.

Fixture design, deliberately two-sided so neither test can pass for the wrong reason:

  • start_benign.wat — the truthful control. A legitimate start writes 32 bytes that get_store_id reads back, so the assertion proves the start function ran to completion. The nearest wrong fixes (leave the store at zero fuel; arm a token budget) both fail it. A hostile-only test could not see this at all.
  • start_spin.wat — the hostile counterpart: a non-terminating start must be rejected promptly (bounded, not hung), guarding against a regression to no bound at all.

Baseline control: both tests' failing case passes on main's lock, confirming the regression is genuinely 47-introduced rather than a pre-existing red.

Co-Authored-By: Claude noreply@anthropic.com

MichaelTaylor3d and others added 4 commits August 1, 2026 02:31
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
A module's wasm start function runs inside Linker::instantiate. With fuel
consumption enabled engine-wide the store holds ZERO fuel until set_fuel, so
under wasmtime 47 every module with a start function trapped with 'all fuel
consumed' at instantiation time; under 45 the same code left start functions
outside the sandbox budget entirely.

Arm fuel + the epoch deadline immediately before instantiate so instantiation
is bounded like any export call (SPEC 18.2).

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
@MichaelTaylor3d
MichaelTaylor3d marked this pull request as ready for review August 1, 2026 10:29

@MichaelTaylor3d MichaelTaylor3d left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Correctness gate: PASS (head 0b120050b2d0cc8c0327d2489493e627e6f6f334)

Independently verified in a scratch worktree (C:/tmp/worktrees/digs-rev35), not from the PR description. No shared checkout was mutated.

Test-vacuity probe (the load-bearing question) — PASSES, two-sided

I committed nothing and reverted by file-copy, then restored (git status --porcelain empty afterwards).

  1. Mutation A — move the arming back after instantiate (the exact fix, reverted):
    benign_start_function_instantiates_and_runs goes RED for the right reason:
    Wasmtime("wasm trap: all fuel consumed by WebAssembly") at instantiation_bounds.rs:40.
  2. Mutation B — arm a token budget (set_fuel(64) before instantiate, the nearest wrong fix):
    same test goes RED: error while executing at wasm backtrace: 0: 0x7c - <unknown>!start.

So start_benign.wat is genuinely two-sided: it fails under zero fuel AND under a token budget, and the assert_eq!(get_store_id(), vec![0xAB; 32]) names the property (the start function ran to completion inside a real budget), not merely an outcome. This is the correct shape.

At head, cargo test -p digstore-host --test instantiation_bounds = 2 passed.

Other checks

  • §5.1 back-compat: no emit-path source changed; Cargo.lock diff touches only wasmtime-internal-winch + winch-codegen (removed) — wasm-encoder/wasmparser unmoved. data_section_golden 3/3 (incl. the older-golden read), digstore-compiler + digstore-host + digstore-remote suites fully green locally. No section id, field meaning, or guest export signature changed.
  • Advisory really gone, not silenced: deny.toml is byte-unchanged in this PR; no RUSTSEC-2026-0222 string anywhere in the tree; cargo deny check advisoriesadvisories ok locally, and CI supply-chain audit is green.
  • SemVer: digstore-host publicly exposes wasmtime types (RuntimeState { pub limits: StoreLimits } at runtime.rs:74-77, imports::register(&mut Linker<RuntimeState>)), so the wasmtime major bump is a public-API break → 0.1.0 → 0.2.0 is the right 0.x breaking position. Workspace 0.19.3 → 0.20.0 likewise. No package.json exists in this repo, so no two-file disagreement is possible; the Check version increment gate is green.
  • Blast radius (gitnexus disabled per §2.0 — done by cargo tree/grep): wasmtime is declared only by digstore-host (real dep) and digstore-compiler (dev-dep). HostRuntime::new has exactly two production callers — crates/digstore-cli/src/ops/serve.rs:133 and crates/digstore-host/src/serve_blind.rs:229 — and its signature is unchanged in the diff. Claim confirmed.
  • SPEC §7.1 is normative and factually matches the code: 5 s epoch budget, 384 MiB / MAX_MEMORY_BYTES = 6144 * 64 KiB, fuel 5_000_000_000 (crates/digstore-host/src/config.rs:22-28), per-export-call arming (arm_bounds, runtime.rs:208-214), and the instantiation-is-guest-execution rule.
  • §2.5: the new comment block states WHY (start runs inside instantiate; engine-wide fuel means an un-armed store is at zero), names are intent-revealing, let _ = set_fuel(..) matches the existing arm_bounds idiom.
  • All 10 required checks green; zero unresolved review threads.

Two non-gating notes posted inline and resolved by me so they do not block merge.

Comment thread crates/digstore-host/tests/instantiation_bounds.rs
Comment thread SPEC.md Outdated
MichaelTaylor3d and others added 2 commits August 1, 2026 04:44
…orrect execution bounds

Co-Authored-By: Claude <noreply@anthropic.com>
@MichaelTaylor3d
MichaelTaylor3d merged commit 01f32e5 into main Aug 1, 2026
10 checks passed
@MichaelTaylor3d
MichaelTaylor3d deleted the loop/1898-wasmtime-cve/digs branch August 1, 2026 15: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.

1 participant