From ad1aa0db11a8926e3505970b737a558526e43b6a Mon Sep 17 00:00:00 2001 From: Ralf Anton Beier Date: Tue, 19 May 2026 19:02:03 +0200 Subject: [PATCH 1/3] Hotfix CI: collapsible_else_if + tooling-job Rust versions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #6's CI failed on first run after merge. Three concrete fixes: 1. `wohl-leak/plain/src/engine.rs`: clippy 1.85 catches collapsible_else_if in the wet/dry branch. Refactored to early-return style — same logic, no nested else. (My local clippy 1.95 didn't fire the lint, which is why the merge slipped through. Now verified with `cargo +1.85.0 clippy` to match CI.) 2. `kani` job toolchain: 1.85.0 → stable. kani-verifier 0.67's transitive dep `home@0.5.12` requires rustc 1.88+. Kani itself uses its own internal nightly for the proofs; the outer toolchain only needs to build the installer. 3. `rivet-validate` job toolchain: 1.85.0 → stable. rivet-cli 0.10.1 declares rust-version 1.89 (darling 0.23, smol_str 0.3.6, rivet-core 0.10.1 all need 1.88-1.89). MSRV doesn't apply to tooling. `lint` and `test` jobs keep 1.85.0 — that's the project MSRV and we want CI to verify the project still builds against it. `fuzz-smoke` job kept on `nightly` — log was unretrievable due to GitHub API TLS hiccups; if it still fails, separate follow-up. Verification (local, +1.85.0 toolchain matching CI's lint/test): - cargo +1.85.0 clippy --workspace --all-targets -- -D warnings: clean - cargo +1.85.0 fmt -p --check: clean for all 8 members - cargo +1.85.0 test --workspace: pass - YAML parse: OK Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/ci.yml | 11 +++++------ crates/wohl-leak/plain/src/engine.rs | 19 ++++++++----------- 2 files changed, 13 insertions(+), 17 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 384750d..b792d20 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -79,9 +79,9 @@ jobs: with: repository: pulseengine/relay path: relay - - uses: dtolnay/rust-toolchain@master - with: - toolchain: "1.85.0" + # Kani-verifier (the installer) needs newer Rust than our MSRV. + # Kani itself uses an internal nightly for the proofs. + - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 with: workspaces: wohl @@ -147,9 +147,8 @@ jobs: with: repository: pulseengine/rivet path: rivet - - uses: dtolnay/rust-toolchain@master - with: - toolchain: "1.85.0" + # rivet-cli's deps need Rust 1.89+; we don't care about MSRV for tooling. + - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 with: workspaces: rivet diff --git a/crates/wohl-leak/plain/src/engine.rs b/crates/wohl-leak/plain/src/engine.rs index 8ca7286..79af308 100644 --- a/crates/wohl-leak/plain/src/engine.rs +++ b/crates/wohl-leak/plain/src/engine.rs @@ -75,19 +75,16 @@ impl LeakDetector { if wet { if was_wet { return LeakAction::AlreadyWet; - } else { - self.zones[idx].wet = true; - self.zones[idx].detected_at = timestamp_sec; - return LeakAction::NewLeak; - } - } else { - if was_wet { - self.zones[idx].wet = false; - return LeakAction::Cleared; - } else { - return LeakAction::AlreadyDry; } + self.zones[idx].wet = true; + self.zones[idx].detected_at = timestamp_sec; + return LeakAction::NewLeak; + } + if was_wet { + self.zones[idx].wet = false; + return LeakAction::Cleared; } + return LeakAction::AlreadyDry; } i += 1; } From 6f560334b00a22fc6538f923dcebb7efc1ccab75 Mon Sep 17 00:00:00 2001 From: Ralf Anton Beier Date: Tue, 19 May 2026 19:16:07 +0200 Subject: [PATCH 2/3] Hotfix CI fuzz: drop --locked on cargo-fuzz install cargo install --locked cargo-fuzz pins cargo-fuzz 0.13.1's Cargo.lock, which pins rustix 0.36.5. That old rustix uses unstable rustc_attrs: #[cfg_attr(rustc_attrs, rustc_layout_scalar_valid_range_start(0xf001))] Current nightly compilers don't accept this without `--cfg=rustc_attrs`, so the install step fails with: error: cannot find attribute `rustc_layout_scalar_valid_range_start` in this scope Removing --locked lets cargo resolve rustix to a newer version that compiles cleanly. Mild reproducibility cost (no Cargo.lock pin for the fuzz tool install), but the alternative is having no fuzz job at all. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/ci.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b792d20..e359336 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -128,7 +128,10 @@ jobs: workspaces: wohl key: fuzz - name: Install cargo-fuzz - run: cargo install --locked cargo-fuzz + # NOT --locked: cargo-fuzz 0.13.1's lockfile pins rustix 0.36.5 which + # uses unstable rustc_attrs that current nightly no longer accepts. + # Unlocked resolution picks newer rustix that compiles cleanly. + run: cargo install cargo-fuzz - name: Fuzz fuzz_leak working-directory: wohl run: cargo fuzz run fuzz_leak -- -max_total_time=60 From 6b91a027015abdca32ac710e4d9f62c196946011 Mon Sep 17 00:00:00 2001 From: Ralf Anton Beier Date: Tue, 19 May 2026 19:37:14 +0200 Subject: [PATCH 3/3] Fix integer overflow in watchpoint ID encoding (found by fuzz) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cargo-fuzz on wohl-temp panicked at engine.rs:106 because `zone_id * 2` overflows for zone_id > u32::MAX / 2. Same pattern in wohl-air (`zone_id * 6` at engine.rs:120, 151). Kani's `verify_no_panic` harnesses on these crates have `kani::assume(zone_id < 100)`, so they only proved no-panic for small zone_ids. Fuzz exposed the unproven range — exactly what fuzz is for. Fix: `.saturating_mul(N)` and `.saturating_add(1)` so adversarial u32 zone_ids cannot panic. Since MAX_ZONES is 32 (16 for air), real zone_ids cannot reach the saturation point — the collision bucket only affects pathological inputs. Verified: - cargo +1.85.0 clippy: clean - cargo +1.85.0 fmt -p wohl-{temp,air}: clean - cargo test -p wohl-temp -p wohl-air: pass - cargo +nightly fuzz run fuzz_temp -- -max_total_time=15: 863k runs, no crash (previously panicked within seconds) Co-Authored-By: Claude Opus 4.7 (1M context) --- crates/wohl-air/plain/src/engine.rs | 4 ++-- crates/wohl-temp/plain/src/engine.rs | 7 +++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/crates/wohl-air/plain/src/engine.rs b/crates/wohl-air/plain/src/engine.rs index f596bd3..ffd3a84 100644 --- a/crates/wohl-air/plain/src/engine.rs +++ b/crates/wohl-air/plain/src/engine.rs @@ -117,7 +117,7 @@ impl AirMonitor { return false; } - let base = config.zone_id * 6; + let base = config.zone_id.saturating_mul(6); let thresholds = [ config.co2_warn, config.co2_critical, @@ -148,7 +148,7 @@ impl AirMonitor { alerts: [AirAlert::empty(); MAX_ALERTS_PER_READING], alert_count: 0, }; - let base = reading.zone_id * 6; + let base = reading.zone_id.saturating_mul(6); let values = [ reading.co2_ppm, reading.co2_ppm, diff --git a/crates/wohl-temp/plain/src/engine.rs b/crates/wohl-temp/plain/src/engine.rs index 38a2282..15a937a 100644 --- a/crates/wohl-temp/plain/src/engine.rs +++ b/crates/wohl-temp/plain/src/engine.rs @@ -102,11 +102,14 @@ impl TempAlert { // Watchpoint ID encoding: zone_id * 2 + offset // offset 0 = freeze watchpoint (LessOrEqual) // offset 1 = overheat watchpoint (GreaterOrEqual) +// Uses saturating_mul/add so adversarial u32 zone_ids cannot panic on overflow. +// Real zone_ids are bounded by MAX_ZONES (32), so saturation only affects +// pathological inputs and never collides in practice. fn freeze_wp_id(zone_id: u32) -> u32 { - zone_id * 2 + zone_id.saturating_mul(2) } fn overheat_wp_id(zone_id: u32) -> u32 { - zone_id * 2 + 1 + zone_id.saturating_mul(2).saturating_add(1) } impl Default for TemperatureMonitor {