diff --git a/.github/dependabot.yml b/.github/dependabot.yml index ccea22fea..0caf0d865 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -122,6 +122,14 @@ updates: fuzz-dependencies: patterns: - "*" + ignore: + # The fuzz lockfile resolves the root workspace, so a bump here rewrites + # the root `wasmtime = "=48.0.5"` pin — and `ignore` is per-entry, so the + # "/" entry's wasmtime block above does not cover this one. That gap + # opened #2708, where every rust job failed at resolution with + # "wasmtime@49.0.2 requires rustc 1.96.0" against the pinned 1.95.0. + # Keep in step with the "/" entry; see the rationale there. + - dependency-name: "wasmtime" # The remaining cargo workspaces. Each has its own lockfile, so neither the # "/" entry nor the fuzz entry above reaches them. @@ -158,6 +166,12 @@ updates: example-cargo: patterns: - "*" + ignore: + # `examples/hyperlight/host` pins wasmtime "48" and is built by required + # CI (the "WASM component (no JS, no WASI)" job) on the same pinned + # 1.95.0 toolchain, so a 49.x bump here fails resolution exactly as it + # did in #2708. Keep in step with the "/" entry; see the rationale there. + - dependency-name: "wasmtime" # npm / pnpm. # diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 6fbf5e549..3a661b192 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -94,10 +94,16 @@ jobs: - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2 + # Built from source against the pinned toolchain, so the tool version is + # pinned too: an unpinned `tool:` resolves to the newest release, and + # cargo-geiger 0.14.0 raised its MSRV to 1.97 on the day it shipped, + # which broke this job on a tree that had not changed. Bump deliberately + # alongside rust-toolchain.toml; 0.13.0 builds on 1.85+. - name: Install cargo-geiger uses: taiki-e/cache-cargo-install-action@9ee83daaa7b96a6fab930949ecf1122bba04a389 # v3 with: tool: cargo-geiger + version: 0.13.0 locked: true - name: Run cargo-geiger (unsafe code audit) diff --git a/crates/bashkit/tests/integration/dd_tests.rs b/crates/bashkit/tests/integration/dd_tests.rs index a45c63ee5..48d1232f1 100644 --- a/crates/bashkit/tests/integration/dd_tests.rs +++ b/crates/bashkit/tests/integration/dd_tests.rs @@ -58,11 +58,17 @@ async fn dd_normal_copies_match_gnu() { .stderr(Stdio::piped()) .spawn() .unwrap(); - host.stdin - .take() - .unwrap() - .write_all(input.as_bytes()) - .unwrap(); + // GNU dd with count=0 exits without reading a byte, so this write + // races the child's exit and fails with EPIPE whenever the child + // wins. That is the case being exercised, not an error: the + // assertions below still compare stdout, stderr and exit code. + let mut stdin = host.stdin.take().unwrap(); + match stdin.write_all(input.as_bytes()) { + Ok(()) => {} + Err(error) if error.kind() == std::io::ErrorKind::BrokenPipe => {} + Err(error) => panic!("writing dd stdin for {args}: {error}"), + } + drop(stdin); // close before wait_with_output, as the temporary did let expected = host.wait_with_output().unwrap(); let actual = Bash::new() .exec_with_options(&format!("dd {args}"), ExecOptions::new().stdin(input)) diff --git a/knowledge/log.md b/knowledge/log.md index 84b39015f..38bfe70ac 100644 --- a/knowledge/log.md +++ b/knowledge/log.md @@ -1,5 +1,9 @@ # Bashkit Knowledge Update Log +## 2026-10-13 + +* **Operations**: Two CI holes with one cause — something was free to move to a version the pinned rustc cannot build, so an upstream release broke a tree nobody had touched. Nightly's `cargo-geiger` was installed with no version and built from source against the pinned toolchain; 0.14.0 shipped with its MSRV raised to 1.97 and run 259 went red the next morning, skipping the unsafe-code audit entirely. It is now pinned to 0.13.0 (MSRV 1.85), bumped deliberately with the toolchain, like `cargo-audit` before it. Separately, `wasmtime` is ignored in the `/` dependabot entry because the pin is exactly `=48.0.5`, but `ignore` is per-entry: the fuzz entry had none, its lockfile resolves the root workspace, and dependabot rewrote the root pin to 49.x (MSRV 1.96) in #2708, failing every rust job at resolution. `examples/hyperlight/host` had the same exposure from the example-workspaces entry. Both now carry the ignore, and `scripts/tests/test_dependabot_coverage.py` fails if any cargo entry can propose a wasmtime bump, since which entry reaches which manifest is not worth re-deriving each time one is added. The general rule: an unpinned tool against a pinned compiler defeats the point of pinning the compiler. Also corrected the [threat model](security/threat-model.md) tooling table, which listed `cargo-geiger` as running on every PR; it has only ever run nightly. + ## 2026-10-12 * **Test**: GPT-6.1 Sol (76/88, generate 14/15) and GPT-6 Luna (71/88, 14/15) added to the 2026-10-09 tagged `bashkit_bash` and `bashkit_generate` runs via `mira run --resume`; all seven models now on the homepage table. A resume writes `report.json` for the resumed targets only, so re-render with `mira report ` before publishing. diff --git a/knowledge/security/threat-model.md b/knowledge/security/threat-model.md index d4eaa5462..f9d4ec3d0 100644 --- a/knowledge/security/threat-model.md +++ b/knowledge/security/threat-model.md @@ -1645,7 +1645,7 @@ This section documents the security tools used to detect and prevent vulnerabili | **OSV** | CVE scanning for PyPI dependencies | ✅ Required | Every PR + nightly | | **cargo-deny** | License + advisory checks | ✅ Required | Every PR | | **cargo-clippy** | Lint with security-focused warnings | ✅ Required | Every PR | -| **cargo-geiger** | Count unsafe code blocks | ✅ Informational | Every PR | +| **cargo-geiger** | Count unsafe code blocks | ✅ Informational | Nightly | **cargo-audit** scans every `Cargo.lock` in the tree against the RustSec Advisory Database, **pnpm audit** every `pnpm-lock.yaml` against the GitHub Advisory diff --git a/scripts/tests/test_dependabot_coverage.py b/scripts/tests/test_dependabot_coverage.py index 619351f1c..00d72d054 100644 --- a/scripts/tests/test_dependabot_coverage.py +++ b/scripts/tests/test_dependabot_coverage.py @@ -111,6 +111,47 @@ def test_the_two_random_fs_fixtures_are_both_covered(self): with self.subTest(fixture=fixture): self.assertIn(fixture, covered) + def test_no_cargo_entry_can_propose_a_wasmtime_bump(self): + # wasmtime is held at =48.0.5 for two independent reasons: the + # `cpython` feature loads a module precompiled by build.rs, which only + # loads into the identical wasmtime version, and 48.x is the newest + # line whose MSRV the pinned rustc satisfies. 49.x needs 1.96. + # + # The "/" entry carried that ignore; the fuzz and example entries did + # not, and the ignore is per-entry. The fuzz lockfile resolves the root + # workspace, so dependabot edited the root `=48.0.5` pin straight + # through the gap and opened #2708, where every rust job died at + # resolution with "wasmtime@49.0.2 requires rustc 1.96.0". The + # hyperlight host pins its own wasmtime "48" and is built by required + # CI on the same pinned toolchain, so it had the same exposure. + # + # Which entry happens to reach which manifest is not a useful thing to + # re-derive each time one is added, so the invariant is the blunt one: + # no cargo entry proposes a wasmtime bump. Lift it everywhere at once + # when the toolchain moves. + config = yaml.safe_load(CONFIG.read_text()) + cargo_entries = [ + u for u in config['updates'] if u['package-ecosystem'] == 'cargo' + ] + self.assertGreaterEqual(len(cargo_entries), 3, cargo_entries) + for entry in cargo_entries: + scope = entry.get('directory') or entry.get('directories') + with self.subTest(scope=scope): + ignored = { + i.get('dependency-name') + for i in entry.get('ignore') or [] + # An ignore narrowed to update-types still lets other + # bump kinds through, so it does not count here. + if not i.get('update-types') + } + self.assertIn( + 'wasmtime', + ignored, + f'cargo entry {scope} can propose a wasmtime bump; ' + 'it is pinned to =48.0.5 by MSRV and by precompiled-module ' + 'compatibility, so every cargo entry must ignore it', + ) + def test_config_is_a_well_formed_dependabot_v2_file(self): config = yaml.safe_load(CONFIG.read_text()) self.assertEqual(config['version'], 2)