From cf2f69d19c1be5d05ff14c5fe68da70d1d07796c Mon Sep 17 00:00:00 2001 From: atlas Date: Sat, 17 Oct 2026 08:54:55 +0200 Subject: [PATCH] ci: evaluate with nix-eval-jobs under a memory cap, then build the checks --- .forgejo/workflows/ci.yml | 4 ++- docs/scheduler/ci.md | 4 +-- scripts/ci-flake-check.sh | 55 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 60 insertions(+), 3 deletions(-) create mode 100755 scripts/ci-flake-check.sh diff --git a/.forgejo/workflows/ci.yml b/.forgejo/workflows/ci.yml index b7cbe931..8b2a9f2f 100644 --- a/.forgejo/workflows/ci.yml +++ b/.forgejo/workflows/ci.yml @@ -18,7 +18,9 @@ jobs: steps: - uses: actions/checkout@v3 - name: check - run: nix flake check + # The job name stays "nix flake check": branch protection requires it. + # `--inputs-from .` resolves `nixpkgs` to the flake's locked input. + run: nix shell --inputs-from . nixpkgs#nix-eval-jobs nixpkgs#jq --command sh scripts/ci-flake-check.sh tracker-tags: name: tracker-tag lint diff --git a/docs/scheduler/ci.md b/docs/scheduler/ci.md index c8692d1c..5330a97a 100644 --- a/docs/scheduler/ci.md +++ b/docs/scheduler/ci.md @@ -54,7 +54,7 @@ table can assert ahead of it happening. | Job | What it runs | Required? | | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------- | ------------------------------- | -| **nix flake check** | treefmt formatting, `cargo clippy -D warnings`, `cargo test`, module evaluation | yes | +| **nix flake check** | `scripts/ci-flake-check.sh`: nix-eval-jobs, then builds each check (treefmt, clippy, tests, module eval) | yes | | **tracker-tag lint** | flags `#NNN` issue tags in source and comments (`scripts/check-issue-refs.sh`) | yes | | **comment-block lint** | flags contiguous comment blocks over 30 lines (`scripts/check-comment-blocks.sh`) | yes | | **doc-pointer lint** | flags `docs/…md` paths and relative markdown links that no longer resolve (`scripts/check-doc-refs.sh`) | yes | @@ -270,7 +270,7 @@ A malicious `default.nix` or build script in a PR can therefore: No forge admin token enters the container: swarm-controller holds it and publishes only the registration token, which only `swarm-ci-runner-register.service` loads, so a job step can't read it. A build process can still reach the forge over the network, and it runs as the runner's own user, so it can read `.runner` and act as this runner; it can't issue admin API calls. -Note: `nix flake check --no-build` (eval-only) reduces the attack surface but doesn't eliminate it — `builtins.fetchGit`, `builtins.fetchurl`, and import-from-derivation can reach the network and filesystem during evaluation. The default CI workflow runs full `nix flake check` (builds derivations), which is the higher-risk path. +Note: `nix flake check --no-build` (eval-only) reduces the attack surface but doesn't eliminate it — `builtins.fetchGit`, `builtins.fetchurl`, and import-from-derivation can reach the network and filesystem during evaluation. The CI workflow's `nix flake check` job builds every check derivation, which is the higher-risk path. ### Mitigation diff --git a/scripts/ci-flake-check.sh b/scripts/ci-flake-check.sh new file mode 100755 index 00000000..109696c3 --- /dev/null +++ b/scripts/ci-flake-check.sh @@ -0,0 +1,55 @@ +#!/bin/sh +# CI's "nix flake check" job: the same coverage as `nix flake check` on +# x86_64-linux, as two processes so the evaluator has exited before any build +# starts and no single evaluator holds the whole suite's heap. +# +# 1. nix-eval-jobs evaluates every check, package, dev shell, the formatter +# and each nixosConfiguration's toplevel, one worker at a time. A worker +# past --max-memory-size is replaced after its current attribute, so the +# peak is about the cap plus the heaviest single attribute. +# 2. nix build builds the checks' derivations. The other attributes are +# evaluated only, as `nix flake check` does. +# +# nix-eval-jobs reports an evaluation error as an `error` field on that +# attribute's line, not as its exit status, so this script fails on any. +# +# Needs nix-eval-jobs and jq on PATH; ci.yml takes both from the flake's +# pinned nixpkgs. +set -eu + +system=x86_64-linux +work="$(mktemp -d)" +trap 'rm -rf "$work"' EXIT + +nix-eval-jobs \ + --workers 1 \ + --max-memory-size 4096 \ + --gc-roots-dir "$work/gcroots" \ + --flake . \ + --select "flake: let o = flake.outputs; group = s: s // { recurseForDerivations = true; }; in { + checks = group o.checks.$system; + packages = group o.packages.$system; + devShells = group o.devShells.$system; + formatter = o.formatter.$system; + nixosConfigurations = group (builtins.mapAttrs (_: c: c.config.system.build.toplevel) o.nixosConfigurations); + }" >"$work/jobs.json" + +jq -r 'select(has("error")) | "\(.attr): \(.error)"' "$work/jobs.json" >"$work/errors" +if [ -s "$work/errors" ]; then + echo "evaluation failed for $(jq -s 'map(select(has("error"))) | length' "$work/jobs.json") attribute(s):" >&2 + cat "$work/errors" >&2 + exit 1 +fi + +jq -r 'select(.attrPath[0] == "checks") | .drvPath + "^*"' "$work/jobs.json" >"$work/build" +if [ ! -s "$work/build" ]; then + echo "nix-eval-jobs returned no checks; the --select expression matched nothing" >&2 + exit 1 +fi +echo "evaluated $(jq -s length "$work/jobs.json") attributes; building $(wc -l <"$work/build") checks" + +set -- +while IFS= read -r drv; do + set -- "$@" "$drv" +done <"$work/build" +nix build --no-link --keep-going "$@"