Security hygiene: clear all RUSTSEC advisories, correct the MSRV, and add workspace supply-chain gates - #203
Merged
Merged
Conversation
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
force-pushed
the
chore/security-hygiene
branch
from
August 6, 2026 23:55
06b85cb to
56afa00
Compare
…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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #165, #166, #167, #168, #169.
cargo auditgoes 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 optionalwasmfeature, and concluded that anyone not enabling that feature was unaffected. Butlarql-inferencealso carriedwasmtime = "29"andwasmtime-wasi = "29"as mandatory[dependencies], and it is indefault-members— so every default build resolved the vulnerable version. Both crates move together here.Two advisories nobody had filed. After the named fixes,
cargo auditstill flagged:Both were pure lockfile bumps. Two unsoundness notices cleared the same way:
anyhow(RUSTSEC-2026-0190) andmemmap2(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
rust-versionwasmtime is pinned to
^36.0.7— the LTS line — not the open>=36.0.7the issue suggested. wasmtime 45+ declares rust-version 1.93, so an open range would let a routinecargo updatedrag 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_storeandhomeall 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 carryrust-version.workspace = true.The new gate
quality.ymlruns cargo-audit, cargo-deny (four-way matrix), an MSRV check, buf lint, and cargo-mutants on the PR diff (informational). It has nopaths: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.wildcardsiswarn, because intra-workspacepathdeps without aversioncount as wildcards and the fix (versioned path deps) is required for publishing anyway — ADR-0026's work, not this PR's.BASICminus two layout rules.STANDARDwould demand renamingServerMessage/RouterMessage,ExpertLayerInput/OutputandAdminAck— 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 checkall four sub-checks individually;cargo auditexit 0;buf lint+buf build; larql-python release cdylib; fullcargo test --workspace.One gap: cross-compiling to
x86_64-unknown-linux-gnuneeds a cross-gcc this machine lacks, so larql-cli's sixcfg(windows)/cfg(not(target_os = "macos"))sites are verified only through the--no-default-featuresshape on macOS. CI is the first real test of those.