Skip to content

Security hygiene: clear all RUSTSEC advisories, correct the MSRV, and add workspace supply-chain gates - #203

Merged
chrishayuk merged 5 commits into
mainfrom
chore/security-hygiene
Aug 7, 2026
Merged

Security hygiene: clear all RUSTSEC advisories, correct the MSRV, and add workspace supply-chain gates#203
chrishayuk merged 5 commits into
mainfrom
chore/security-hygiene

Conversation

@chrishayuk

Copy link
Copy Markdown
Owner

Closes #165, #166, #167, #168, #169.

cargo audit goes from 2 vulnerabilities to 0 against this workspace, and CI gains the gate that would have caught them.

What the issues asked for, and what was actually there

#166 understated the exposure. It scoped wasmtime to crates/model-compute, where it sits behind the optional wasm feature, and concluded that anyone not enabling that feature was unaffected. But larql-inference also carried wasmtime = "29" and wasmtime-wasi = "29" as mandatory [dependencies], and it is in default-members — so every default build resolved the vulnerable version. Both crates move together here.

Two advisories nobody had filed. After the named fixes, cargo audit still flagged:

Advisory Crate Severity
RUSTSEC-2026-0185 quinn-proto 0.11.14 7.5 high — remote memory exhaustion, unbounded out-of-order stream reassembly
RUSTSEC-2026-0204 crossbeam-epoch 0.9.18

Both were pure lockfile bumps. Two unsoundness notices cleared the same way: anyhow (RUSTSEC-2026-0190) and memmap2 (RUSTSEC-2026-0186).

#169 needed no code changes. The ~82/~112 clippy errors it describes have already been cleared by other work since 2026-05-10. Both feature shapes report zero under -D warnings; only the CI step had to come back.

Version moves

from to
wasmtime (+ wasmtime-wasi) 29.0.1 36.0.13 5 advisories
pyo3 0.24.2 0.29.2 2 advisories
numpy 0.24 0.29 version-locked to pyo3
quinn-proto 0.11.14 0.11.16 1 advisory
crossbeam-epoch 0.9.18 0.9.20 1 advisory
anyhow 1.0.102 1.0.104 unsoundness
memmap2 0.9.10 0.9.11 unsoundness
rust-version 1.80 1.88 was unresolvable

wasmtime is pinned to ^36.0.7 — the LTS line — not the open >=36.0.7 the issue suggested. wasmtime 45+ declares rust-version 1.93, so an open range would let a routine cargo update drag the MSRV up seven releases as a side effect of a security patch.

1.80 was not merely optimistic, it was unusable: time, image, cookie_store and home all declare 1.88, so resolution itself failed. 1.88 is taken from the resolved graph and verified by compiling. The 17 crates that were not already inheriting it now carry rust-version.workspace = true.

The new gate

quality.yml runs cargo-audit, cargo-deny (four-way matrix), an MSRV check, buf lint, and cargo-mutants on the PR diff (informational). It has no paths: filter and runs weekly — that is the point of it, since advisories are published against code that has not changed.

Two deliberate scope limits, both documented inline:

  • bans.wildcards is warn, because intra-workspace path deps without a version count as wildcards and the fix (versioned path deps) is required for publishing anyway — ADR-0026's work, not this PR's.
  • buf lints BASIC minus two layout rules. STANDARD would demand renaming ServerMessage/RouterMessage, ExpertLayerInput/Output and AdminAck — generated Rust types and every call site — which should not happen as a side effect of adding a lint config.

Verification

cargo fmt --all --check; clippy on larql-cli (both feature shapes), larql-inference, model-compute (both shapes), larql-python — all with each crate's own CI flags; cargo +1.88.0 check --workspace --all-targets; cargo deny check all four sub-checks individually; cargo audit exit 0; buf lint + buf build; larql-python release cdylib; full cargo test --workspace.

One gap: cross-compiling to x86_64-unknown-linux-gnu needs a cross-gcc this machine lacks, so larql-cli's six cfg(windows) / cfg(not(target_os = "macos")) sites are verified only through the --no-default-features shape on macOS. CI is the first real test of those.

wasmtime 29.0.1 -> 36.0.13, closing RUSTSEC-2026-0021 / -0085 / -0086 /
-0087 / -0088.

Issue #166 scoped this to `crates/model-compute`, where wasmtime sits
behind the optional `wasm` feature, and concluded that anyone not enabling
that feature was unexposed. That is not the whole picture. `larql-inference`
also carried `wasmtime = "29"` *and* `wasmtime-wasi = "29"` as mandatory
entries in `[dependencies]`, and it is in `default-members` — so every
default build of this workspace resolved the vulnerable version. Both
crates move together here.

The requirement is `^36.0.7` (the 36.0.x LTS line) rather than the open
`>=36.0.7` the issue suggested. wasmtime 45+ declares rust-version 1.93,
so an open range would let a routine `cargo update` drag the workspace
MSRV up by seven releases as a side effect of a security patch.

Compiled-module caches survive this: `load_module` deserializes a `.cwasm`
only inside `if let Ok(..)`, and wasmtime rejects artefacts from a
different version, so a stale cache costs one failed read and a recompile.

pyo3 0.24.2 -> 0.29.2 with numpy 0.24 -> 0.29 (version-locked to pyo3),
closing RUSTSEC-2026-0176 / -0177. The bindings already used the
`Bound<'py, T>` API, so nothing in the 4k lines of binding code needed
rewriting. The one change is opting the six `Clone` + `#[pyclass]` types
into `from_py_object`: 0.29 deprecates the automatic `FromPyObject` derive
ahead of making it opt-in. `PyEdge` genuinely needs it — `Graph::add_edges`
extracts a `Vec<PyEdge>` — and the other five are opted in to keep today's
behaviour byte-for-byte rather than to quietly narrow the Python API.

Two more advisories that no issue had recorded, both fixed by lockfile
bumps alone:

  quinn-proto      0.11.14 -> 0.11.16   RUSTSEC-2026-0185, 7.5 high:
                                        remote memory exhaustion from
                                        unbounded out-of-order stream
                                        reassembly
  crossbeam-epoch  0.9.18  -> 0.9.20    RUSTSEC-2026-0204

and two unsoundness notices cleared the same way: anyhow 1.0.102 -> 1.0.104
(RUSTSEC-2026-0190) and memmap2 0.9.10 -> 0.9.11 (RUSTSEC-2026-0186).

`cargo audit` now reports zero vulnerabilities against this lockfile. Four
warnings remain — number_prefix, paste, rustls-pemfile, scc — every one of
them pinned by an upstream dependant we do not control, and each recorded
with its reasoning in deny.toml.

Lastly `rust-version` moves 1.80 -> 1.88. 1.80 was not merely optimistic,
it was unusable: time, image, cookie_store and home all declare 1.88, so
resolution itself failed and the declared minimum described a build nobody
could perform. 1.88 is the real floor, taken from the resolved graph and
verified with `cargo +1.88.0 check --workspace --all-targets`. The 17
member crates that were not already inheriting it now carry
`rust-version.workspace = true`, so the claim is checkable per crate
instead of being a single unenforced line at the workspace root.

Closes #166
Closes #167
Closes #168
The per-crate workflows each cover fmt / check / clippy / test / coverage
for one crate. Nothing looked at the dependency graph as a whole, so a
RUSTSEC advisory against a transitive crate was invisible to CI — which is
exactly how five wasmtime advisories and two pyo3 ones sat in a Cargo.lock
nobody had reason to touch.

`quality.yml` adds five jobs:

  audit       cargo-audit over Cargo.lock. Vulnerabilities fail; the
              unmaintained/unsound warnings do not, because all four
              outstanding ones are pinned by upstream crates we cannot
              move, and a permanently red gate is one people learn to
              ignore. They still print, and deny.toml records why.
  deny        cargo-deny, split into a four-way matrix so a licence
              failure and an advisory failure are distinguishable at a
              glance rather than both reading "cargo-deny failed".
  msrv        `cargo check` at the toolchain named by the workspace's own
              `rust-version`, read out of Cargo.toml at run time so there
              is no second place to forget to bump. This is deliberately
              not `cargo msrv verify`: that bisects many toolchains to
              *discover* a minimum, and the only question CI needs
              answered is whether the number we already claim is true.
  proto-lint  buf lint + buf build over the four gRPC schemas.
  mutants     cargo-mutants on the PR diff, informational.

The workflow has no `paths:` filter and runs weekly on a schedule. That is
the point of it: advisories are published against code that has not
changed, so a gate that only fires when Cargo.lock is edited cannot catch
the class of problem that motivated it.

deny.toml sets the licence allow-list from the licences actually present
(every third-party crate in the graph declares an SPDX expression, so
there is nothing to clarify from licence-file text). MPL-2.0 is the only
non-permissive entry, carried by `colored` and `option-ext`; its copyleft
is file-level and both are consumed unmodified, so Apache-2.0 distribution
is unaffected.

`bans.wildcards` is "warn" rather than "deny", and that is temporary. The
workspace's crates depend on each other by bare `path` with no `version`,
which cargo-deny counts as a wildcard, and its `allow-wildcard-paths`
escape hatch only exempts crates marked `publish = false`. Giving each
intra-workspace path dependency a `version` is required before any of
these can be published anyway, so it belongs with the release work in
ADR-0026, not here.

buf.yaml lints BASIC minus the two rules that would require moving .proto
files on disk and rewriting both crates' tonic_build include paths.
STANDARD stays off for a stronger reason: it demands a dedicated
`<Method>Request`/`<Method>Response` pair per RPC, which would rename
GridService.Join's ServerMessage/RouterMessage, ExpertService's
ExpertLayerInput/Output and the shared AdminAck — a change to generated
Rust types and every call site, and not something to do as a side effect
of adding a lint config. What BASIC does still enforce is schema content:
field, message, enum and service naming, enum zero-values, reserved-field
hygiene. That catches mistakes in new schemas without demanding a layout
migration first.

Separately, larql-cli's clippy step comes back. It was commented out on
2026-05-10 against ~82 pre-existing errors under default features and ~112
under `--no-default-features`. That backlog has since been cleared by
other work: both feature shapes now report zero under `-D warnings`, so
this needed no code changes at all, only the step. The feature split
mirrors the Check steps above it, and `--no-deps` matches the other
crates' workflows so a lint in a path dependency reds that dependency's
workflow rather than this one.

Verified locally: cargo-deny all four checks pass individually as the
matrix runs them, cargo-audit exits 0, buf lint and buf build pass,
`cargo +1.88.0 check --workspace --all-targets` passes, and clippy is
clean on larql-cli in both feature shapes. The Linux and Windows cfg paths
in larql-cli could not be checked here — cross-compiling needs a cross-gcc
this machine lacks — so CI is the first real test of those six cfg sites.

Closes #165
Closes #169
…ob by platform

Two CI failures, both consequences of the previous commit rather than of
anything in the crates they surfaced in.

**clippy in larql-kv** (all three platforms, and larql-vindex too)

Clippy gates some lints on the declared `rust-version`: it will not suggest
an API that does not exist at your stated minimum. At 1.80 it stayed quiet
about `is_multiple_of` (stabilised 1.87) and `is_none_or` (1.82). Moving
the declaration to 1.88 makes both fair game, so three lints in larql-kv —
a crate this branch never edited — went from suppressed to `-D warnings`
errors:

  boundary_kv/engine.rs:140        manual_is_multiple_of
  markov_residual/compute.rs:348   unnecessary_map_or
  turbo_quant/engine.rs:256        manual_is_multiple_of

`compute.rs` is the instructive one: it carried a comment reading
"`is_none_or` is MSRV 1.82; project pins MSRV 1.80" — the workaround
existed *because* of the stale pin, so raising the pin retires both the
code and the comment.

Both `is_multiple_of` rewrites are exact, not approximate. `x % n == 0` and
`x.is_multiple_of(n)` differ only when `n == 0`, where the former panics and
the latter returns `x == 0`; neither divisor can be zero. `chunk_tokens()`
ends in `.max(1)` and has a test pinning that floor, and `detect_head_dim`
draws `hd` from the literal `[256, 128, 64, 32]`.

larql-vindex was failing on the *same three* lints, not a fourth problem:
its workflow lints without `--no-deps`, so it reports its path
dependencies' lints as its own.

The wider lesson is about scope. I checked clippy on the crates whose files
I had edited, but a `rust-version` change is a workspace-wide lint change —
the blast radius is every crate, not the diff. All 15 of CI's clippy
invocations now pass locally.

**MSRV job**

Not an MSRV problem at all: `--workspace --all-targets` on a Linux runner
tried to build `larql-compute-metal`'s examples, which `extern crate
blas_src` unconditionally while the crate takes `blas-src` only under
`cfg(target_os = "macos")`. They cannot compile on Linux at any toolchain,
which is why that crate's own workflow is macOS-only. This job was simply
the first Linux job to ever attempt a workspace-wide `--all-targets` build.

Rather than drop `--all-targets` — which would silently stop MSRV-checking
every example in the workspace — the job is now a two-arm matrix: Linux
checks the workspace minus that crate, covering the cfg(linux) OpenBLAS
paths, and macOS checks the crate Linux cannot. Between them every member
crate is gated, with no silent hole.

Verified: all 15 CI clippy invocations, `cargo fmt --all --check`,
`cargo test -p larql-kv` (861 passing), and both matrix arms at 1.88 with
`--locked`.
@chrishayuk
chrishayuk force-pushed the chore/security-hygiene branch from 06b85cb to 56afa00 Compare August 6, 2026 23:55
…ture

The new MSRV job builds `--workspace --all-targets` on ubuntu. Feature
unification turns `larql-inference/gpu` on for that build, which satisfies
the example's `required-features = ["gpu"]`, so cargo compiles it on Linux —
where `larql_compute_metal::MetalBackend` is `#[cfg(target_os = "macos")]`
and therefore absent:

    error[E0433]: failed to resolve: could not find `MetalBackend`
                  in `larql_compute_metal`

`required-features` cannot express a target_os constraint, so the example has
to carry the cfg itself. This mirrors the sibling `cpu_gpu_diag.rs`, which
already gates every item on `all(feature = "gpu", target_os = "macos")` and
provides a stub `main` elsewhere — that example passes the same ubuntu job
today.

Pre-existing, not introduced by the MSRV bump; the job is simply the first
Linux build that reaches this example.
Third instance of the same defect class in this branch's blast radius, and
the one that is breaking `main`: `shannon-verify` passed on 2a7c408 and
fails on e7ae7d2, where #208 re-homed the decode-pass diff into
`shannon_trace/decode_diff.rs`.

    error[E0433]: cannot find `MetalBackend` in `larql_compute_metal`
      --> crates/larql-cli/src/commands/primary/shannon_trace/decode_diff.rs:71:44

`feature = "gpu"` compiles on every target, but
`larql_compute_metal::MetalBackend` is `#[cfg(target_os = "macos")]`. A
Linux release build with the feature on therefore reaches for a type that
is not there. Cargo cannot express "this feature, on this OS" — not via
`required-features`, not via a feature predicate — so the call site has to
carry the target_os itself.

Same shape as the `decode_vs_prefill` gate earlier in this branch, and the
same shape as `cpu_gpu_diag.rs`, which has always carried it and has always
passed. The refusal message now names the OS as well as the feature, since
a Linux build with `--features gpu` now lands there rather than failing to
compile.

Checked both ways locally: `-p larql-cli --features gpu` on macOS takes the
real path, and the CPU-only build takes the refusal arm.
@chrishayuk
chrishayuk merged commit 2520677 into main Aug 7, 2026
48 of 59 checks passed
@chrishayuk
chrishayuk deleted the chore/security-hygiene branch August 7, 2026 01:01
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.

ci: no quality workflow — missing cargo-audit, cargo-deny, MSRV, mutation testing

1 participant