Skip to content

fix(ci): unbreak main and nightly, and close the dependabot wasmtime gap - #2715

Merged
chaliy merged 4 commits into
mainfrom
eve/clever-mccarthy-tmb6jp
Oct 13, 2026
Merged

chaliy merged 4 commits into
mainfrom
eve/clever-mccarthy-tmb6jp

Conversation

@chaliy

@chaliy chaliy commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

What changed

Three CI failures, two causes, all of them things that broke a tree nobody
had changed.

  1. main is green again. dd_normal_copies_match_gnu writes to GNU dd's
    stdin and unwraps; with count=0 dd exits without reading, so the write
    races the child's exit. EPIPE is now accepted for what it is.
  2. Nightly is green again. The Security Analysis job installs a pinned
    cargo-geiger (0.13.0) instead of the newest release, so the unsafe-code
    report is produced again after four days of the job dying before it ran.
  3. Dependabot can no longer bump the exactly-pinned wasmtime from the
    fuzz or example-workspace entries. The / entry already ignored it; the
    other two did not, and ignore is per-entry.

Also corrects the threat-model tooling table, which advertised cargo-geiger
as running on every PR. It has only ever run nightly.

Why

The dd oracle (main). One of its eight cases is
("abcde", "ibs=4 count=0 conv=sync status=none"), and GNU dd with count=0
exits without reading a byte, so the write only succeeds if it reaches the
pipe buffer first:

panicked at crates/bashkit/tests/integration/dd_tests.rs:65:14:
called `Result::unwrap()` on an `Err` value:
    Os { code: 32, kind: BrokenPipe, message: "Broken pipe" }

Unloaded the parent nearly always wins, which is why it passed when it landed.
On a loaded runner it loses, and it is already reddening main:

run head dd_normal_copies_match_gnu
6010 0f23a917 (#2712 merge) ❌
6012 462f367c (#2710 merge) ✅
6014 fa78d9b2 (#2705 merge) ❌

cargo-geiger (nightly). The install step named the tool with no version,
so it resolved to the newest release while the job builds it from source
against the toolchain pinned at 1.95.0. 0.14.0 shipped 2026-10-12 with its
MSRV raised to 1.97, and run 259 went red the next morning:

error: cannot install package `cargo-geiger 0.14.0`, it requires rustc 1.97 or
newer, while the currently active rustc version is 1.95.0
`cargo-geiger 0.13.0` supports rustc 1.85

wasmtime (dependabot). The pin is load-bearing twice over — 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 the pinned rustc
satisfies. The / entry says exactly that and ignores it, but the fuzz
lockfile resolves the root workspace, so dependabot rewrote the root
=48.0.5 pin through the gap in the fuzz entry and opened #2708, where every
rust job died at resolution with wasmtime@49.0.2 requires rustc 1.96.0.
examples/hyperlight/host had the same exposure from the other entry: it pins
its own wasmtime "48" and is built by required CI on the same toolchain.
Left alone both reopen weekly, the way the monty ignores were added only after
#2296 and #2297 came back.

The shared lesson: an unpinned tool against a pinned compiler defeats the
reason rust-toolchain.toml pins the compiler at all.

Before / After

dd — reproduced deterministically against the same dd (coreutils) 9.4 by
giving the child a head start. Only the count=0 case is affected; the other
seven read to EOF:

  ibs=4 obs=2 conv=sync status=noxfer                     ok
  bs=2 count=2 skip=1 status=noxfer                       ok
  ibs=4 count=3 iflag=count_bytes conv=sync status=noxfer ok
  ibs=4 conv=sync,ucase status=none                       ok
  ibs=4 count=0 conv=sync status=none                     EPIPE  <-- races
  ibs=4 conv=sync status=noxfer                           ok
  ibs=4 conv=sync of=/dev/null status=noxfer              ok

After, EPIPE is accepted and the case proceeds to its assertions. No test is
skipped, ignored or quarantined; a write failing for any other reason still
panics, now naming the case.

cargo-geiger — before: run 259, Install cargo-geiger fails in 1s, so the
audit and its report upload are skipped and the job is red. After, the pin
resolves under the job's own toolchain:

$ rustc --version
rustc 1.95.0 (59807616e 2026-04-14)

$ cargo install cargo-geiger --locked --version 0.13.0 --root <tmp>
    Finished `release` profile [optimized] target(s) in 4m 05s
   Installed package `cargo-geiger v0.13.0` (executable `cargo-geiger`)
[exited with code 0]

wasmtime — the new test fails on the old config, naming both gaps, and
passes on the new one:

FAIL (scope='/crates/bashkit/fuzz')
AssertionError: 'wasmtime' not found in set() : cargo entry
/crates/bashkit/fuzz can propose a wasmtime bump; ...
FAIL (scope=['/examples/hyperlight', '/examples/hyperlight/host', ...])
Ran 1 test in 0.010s — FAILED (failures=2)
$ python3 -m unittest discover -s scripts/tests -p 'test_*.py'
Ran 159 tests in 5.168s

OK

The geiger pin only selects which binary builds — the audit invocation, its
--all-features flag and its continue-on-error posture are untouched, so
what the job reports is unchanged. It was already informational and
non-blocking; this restores a signal that had gone silent rather than adding a
gate. No library or CLI code changes anywhere in this PR.

Risk

  • Low
  • Two CI config files, one test harness, one config test and one knowledge
    entry. No production code.
  • The dd change narrows what the test tolerates to exactly EPIPE on the stdin
    write; every comparison it made before, it still makes.
  • Both pins now have to be lifted deliberately when the toolchain moves. That
    is the intended trade and the comments say so, next to the equivalent note in
    rust-toolchain.toml. Left unpinned, both re-break on the next upstream MSRV
    bump.
  • 0.13.0 is an older geiger, so any detection improvement in 0.14.0 is deferred
    until the toolchain reaches 1.97.
  • The wasmtime ignore is deliberately blunt — it covers all four cargo entries,
    including fixtures that do not use wasmtime. Which entry reaches which
    manifest is not worth re-deriving each time one is added; lift them together.
  • chore(deps): bump the fuzz-dependencies group in /crates/bashkit/fuzz with 2 updates #2708 cannot merge regardless of this PR. Its bump is the thing being
    blocked, not a casualty of it.

Checklist

  • Tests added or updated — test_no_cargo_entry_can_propose_a_wasmtime_bump
    added to the dependabot-config suite that required CI already runs
    (ci.yml discovers scripts/tests), shown failing before and passing
    after. The dd change is itself a test-harness fix with its failure
    reproduced above. The geiger half is workflow configuration with no code
    under test, verified by the install above plus
    just check-workflow-parity ("release workflows match CI for: node,
    python; rust toolchain pins agree") and just check-okf ("knowledge: OKF
    v0.2 conformant").
  • Backward compatibility considered — CI and test-harness only; no public
    surface.

The nightly Security Analysis job installed cargo-geiger with no version,
so it resolved to the newest release. cargo-geiger 0.14.0 shipped on
2026-10-12 with its MSRV raised to 1.97, and the job builds it from
source against the toolchain pinned at 1.95.0. Nightly run 259 went red
the next morning on a tree that had not changed:

    error: cannot install package `cargo-geiger 0.14.0`, it requires
    rustc 1.97 or newer, while the currently active rustc version is 1.95.0

This is the same failure mode the sibling advisories job already guards
against, and the reason rust-toolchain.toml pins the compiler at all: a
same-day upstream release must not be able to break a tree nobody
touched. Pin the tool to 0.13.0 (MSRV 1.85) and bump it deliberately
with the toolchain.

Also correct the threat-model tooling table, which listed cargo-geiger
as running on every PR; it has only ever run in the nightly workflow.
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Oct 13, 2026 •

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
bashkit 101ee53 Commit Preview URL

Branch Preview URL
Oct 13 2026, 09:41 AM

chaliy commented Oct 13, 2026

Copy link
Copy Markdown
Contributor Author

Verified the pin locally against the exact toolchain the job uses, since nightly itself only runs on a schedule:

$ rustc --version
rustc 1.95.0 (59807616e 2026-04-14)

$ cargo install cargo-geiger --locked --version 0.13.0 --root <tmp>
    Finished `release` profile [optimized] target(s) in 4m 05s
  Installing <tmp>/bin/cargo-geiger
   Installed package `cargo-geiger v0.13.0` (executable `cargo-geiger`)
[exited with code 0]

$ <tmp>/bin/cargo-geiger --version
cargo-geiger 0.13.0

Same --locked install the workflow performs, same 1.95.0 that rejected 0.14.0 — exit 0 instead of the resolution error.


Generated by Claude Code

The "/" cargo entry ignores wasmtime, because the pin is load-bearing
twice over: 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 1.95.0 satisfies.

`ignore` is per-entry, and neither the fuzz entry nor the example
workspaces entry carried it. The fuzz lockfile resolves the root
workspace, so dependabot rewrote the root `=48.0.5` pin straight through
that gap and opened #2708, where every rust job died at resolution:

    wasmtime@49.0.2 requires rustc 1.96.0
    Either upgrade rustc or select compatible dependency versions

`examples/hyperlight/host` had the same exposure from the other entry: it
pins its own wasmtime "48" and is built by required CI on the same pinned
toolchain. Left alone, both reopen weekly, the same way the monty ignores
were added after #2296 and #2297 came back.

Add the ignore to both entries, and a test that fails whenever any cargo
entry can propose a wasmtime bump — which entry reaches which manifest is
not worth re-deriving each time one is added, and this is the manual step
the file already exists to catch.
@chaliy chaliy changed the title fix(ci): pin cargo-geiger so a new release can't break nightly fix(ci): stop upstream MSRV bumps breaking the pinned-toolchain tree Oct 13, 2026
Record both halves of the fix and the rule they share: an unpinned tool
against a pinned compiler defeats the point of pinning the compiler.

chaliy commented Oct 13, 2026

Copy link
Copy Markdown
Contributor Author

Test went red on the first commit here (103ffc0), and it is not this PR's — this branch changes only .github/, knowledge/ and scripts/tests/, no Rust. Reporting rather than fixing it here, because the fix belongs in a test this PR does not touch.

---- dd_tests::dd_normal_copies_match_gnu stdout ----
panicked at crates/bashkit/tests/integration/dd_tests.rs:65:14:
called `Result::unwrap()` on an `Err` value:
    Os { code: 32, kind: BrokenPipe, message: "Broken pipe" }
test result: FAILED. 2421 passed; 1 failed

It is a race, not a dd regression. The oracle loop writes the whole input to GNU dd's stdin and unwraps:

host.stdin.take().unwrap().write_all(input.as_bytes()).unwrap();

One of the eight cases is ("abcde", "ibs=4 count=0 conv=sync status=none"). With count=0 GNU dd exits without reading a byte, so that write only succeeds if it lands in the pipe buffer before the child exits. Whoever wins the race decides whether the test passes. Reproduced deterministically against the same dd (coreutils) 9.4 by giving the child a head start — only that case is affected, the other seven keep reading to EOF:

  ibs=4 obs=2 conv=sync status=noxfer                     ok
  bs=2 count=2 skip=1 status=noxfer                       ok
  ibs=4 count=3 iflag=count_bytes conv=sync status=noxfer ok
  ibs=4 conv=sync,ucase status=none                       ok
  ibs=4 count=0 conv=sync status=none                     EPIPE  <-- races
  ibs=4 conv=sync status=noxfer                           ok
  ibs=4 conv=sync of=/dev/null status=noxfer              ok

With no load the parent nearly always wins, which is why it passed on main for f16888d and on the six dependabot PRs earlier today. Under a loaded runner it loses, and it will keep reddening unrelated PRs until the write tolerates EPIPE.

Proposed patch — EPIPE is the contract under test here, not a failure, and every assertion still runs (no skip, no #[ignore]):

// GNU dd with count=0 exits without reading stdin, so this write races
// the child's exit and can fail with EPIPE. That is the case being
// exercised, not an error; the assertions below still compare output.
let mut stdin = host.stdin.take().unwrap();
match stdin.write_all(input.as_bytes()) {
    Ok(()) => {}
    Err(e) if e.kind() == std::io::ErrorKind::BrokenPipe => {}
    Err(e) => panic!("writing dd stdin for {args}: {e}"),
}
drop(stdin); // close before wait_with_output, as the temporary did

No re-run was needed: 103ffc0 has been superseded twice, so CI re-ran from scratch on 4506599 and again on 2484d9f. I'll send the patch above as its own PR rather than widening this one.


Generated by Claude Code

`dd_normal_copies_match_gnu` writes each input to GNU dd's stdin and
unwraps the result. One of its eight cases is

    ("abcde", "ibs=4 count=0 conv=sync status=none")

and GNU dd with count=0 exits without reading a byte, so that write only
succeeds if it reaches the pipe buffer before the child exits. Whoever
wins the race decides whether the test passes:

    panicked at crates/bashkit/tests/integration/dd_tests.rs:65:14:
    called `Result::unwrap()` on an `Err` value:
        Os { code: 32, kind: BrokenPipe, message: "Broken pipe" }

Unloaded, the parent nearly always wins, which is why the test passed
when it landed. On a loaded runner it loses: main is red on fa78d9b and
0f23a91 with exactly this failure, as were two commits of this branch.
Reproduced deterministically against the same dd (coreutils) 9.4 by
giving the child a head start; only the count=0 case is affected, the
other seven read to EOF.

EPIPE here is the contract under test, not an error, so accept it and
keep going. Every assertion still runs: no test is skipped, ignored or
quarantined, and a write failing for any other reason still panics, now
naming the case.

chaliy commented Oct 13, 2026

Copy link
Copy Markdown
Contributor Author

Correction to my previous comment: the dd patch is in this PR (101ee53), not a separate one.

Two things changed the call. Test failed identically a second time, on a different commit (2484d9f), so it is not the rare flake I took it for. And main is red with the same failure — not once but twice:

run head branch dd_normal_copies_match_gnu
6010 0f23a917 (#2712 merge) main ❌
6012 462f367c (#2710 merge) main ✅
6014 fa78d9b2 (#2705 merge) main ❌

So this is a base-branch failure, and the rule for one is to port the fix into the PR rather than wait on it. Holding it back would have left this PR, main, and everything queued behind them red against a one-line race I had already diagnosed.

Nothing about the diagnosis changed — same count=0 race, same patch posted above, with the panic message now naming the failing case. No test is skipped, ignored or quarantined, and a write failing for any other reason still panics.


Generated by Claude Code

@chaliy chaliy changed the title fix(ci): stop upstream MSRV bumps breaking the pinned-toolchain tree fix(ci): unbreak main and nightly, and close the dependabot wasmtime gap Oct 13, 2026
@chaliy
chaliy merged commit 801e478 into main Oct 13, 2026
33 checks passed
@chaliy
chaliy deleted the eve/clever-mccarthy-tmb6jp branch October 13, 2026 10:05

chaliy commented Oct 13, 2026

Copy link
Copy Markdown
Contributor Author

Correcting the record on this one: the cargo-geiger half did not work. The description above claims nightly is green again; it is not. The dd and wasmtime halves did land correctly.

I dispatched nightly on the merged main (run 260) to verify, and Install cargo-geiger failed again in one second:

+ cargo install -f cargo-geiger --root /opt/hostedtoolcache/cargo-geiger --locked --version 0.14.0
error: cannot install package `cargo-geiger 0.14.0`, it requires rustc 1.97 or newer,
while the currently active rustc version is 1.95.0

##[warning]Unexpected input(s) 'version', valid inputs are
['tool', 'locked', 'git', 'tag', 'rev', 'features', 'no-default-features', 'all-features']

cache-cargo-install-action has no version input. The version belongs in tool: as name@version. GitHub Actions treats an unknown input as a warning rather than an error, so my version: 0.13.0 was dropped and the step installed 0.14.0 exactly as before — a pin that read as correct and did nothing. I was misled by INPUT_VERSION: 0.14.0 in the original log, which is the action's own resolved value, not an input it accepts.

The real fix is one line:

- name: Install cargo-geiger
  uses: taiki-e/cache-cargo-install-action@9ee83... # v3
  with:
    tool: cargo-geiger@0.13.0
    locked: true

Coming as a follow-up PR, with a test (scripts/tests/test_pinned_ci_tools.py) that fails on both halves of this mistake — a source-built tool on the pinned toolchain with no @version, and any input the action silently drops. Verified it fails on the exact spelling I shipped and passes on the corrected one. A job that installs the nightly toolchain is exempt from the pin requirement, since nightly outruns any released MSRV; that is why cargo-fuzz in the fuzz job needs none.


Generated by Claude Code

chaliy added a commit that referenced this pull request Oct 13, 2026
…#2719)

The pin added in #2715 did not work. cache-cargo-install-action has no `version` input, and GitHub Actions treats an unknown input as a warning rather than an error, so `version: 0.13.0` was dropped and the step kept installing the newest release. Nightly run 260, on the commit meant to fix this, failed exactly as run 259 did with "cargo-geiger 0.14.0 requires rustc 1.97" plus a buried "Unexpected input(s) 'version'".

The version belongs in `tool:` as `name@version`; the action's own pre.sh splits on `@` and otherwise sets version=latest. The earlier log's `INPUT_VERSION: 0.14.0` was that computed value, not an input we passed, which is what made the wrong spelling look plausible.

Adds scripts/tests/test_pinned_ci_tools.py covering both halves: a source-built tool on the pinned toolchain with no @Version, and any input this action silently drops. Jobs on the nightly toolchain are exempt, since nightly outruns any released MSRV - which is why cargo-fuzz needs no pin.
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