From 538226b7eefd64a578844d7602da0d96b343cd2b Mon Sep 17 00:00:00 2001 From: Jeroen Soeters Date: Fri, 21 Aug 2026 11:42:49 -0700 Subject: [PATCH 1/2] fix(ci): cap mutation-test memory so a runaway mutant cannot kill the runner A mutant that negates a loop bound turns a bounded padding loop into one that allocates without bound. The test process exhausts the runner VM's memory within seconds, well before gremlins' per-mutant timeout, and the VM is torn down mid-run: gremlins takes the shutdown signal, exits 0 and writes no report, and the check reports the package as producing no usable result. The nightly full run dies the same way every night. Running the mutation step inside a memory-capped cgroup with swap denied turns that failure into the right one: the kernel OOM killer takes out the runaway test process, gremlins records the mutant as killed, and the run completes with a report. Reproduced and verified locally: mutating internal/cli/status uncapped OOMs the machine at the padding-loop negation; capped, the same run completes with that mutant killed. --- .github/workflows/mutation-test-pages.yml | 9 ++++++++- .github/workflows/mutation-test-pr.yml | 15 ++++++++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/.github/workflows/mutation-test-pages.yml b/.github/workflows/mutation-test-pages.yml index 4b266ae89..360f1f048 100644 --- a/.github/workflows/mutation-test-pages.yml +++ b/.github/workflows/mutation-test-pages.yml @@ -57,6 +57,8 @@ jobs: - name: Setup local-data-api run: make local-data-api-ci + # Same confinement as mutation-test-pr.yml: a mutant that allocates + # without bound must OOM its own test process, not the runner VM. - name: Run full mutation test suite env: FORMAE_TEST_AURORA_CLUSTER_ARN: arn:aws:rds:us-east-1:123456789012:cluster:local @@ -66,7 +68,12 @@ jobs: AWS_ACCESS_KEY_ID: test AWS_SECRET_ACCESS_KEY: test AWS_REGION: us-east-1 - run: ./scripts/mutation-test.sh + run: | + sudo mkdir /sys/fs/cgroup/mutation + echo 12G | sudo tee /sys/fs/cgroup/mutation/memory.max + echo 0 | sudo tee /sys/fs/cgroup/mutation/memory.swap.max + echo $$ | sudo tee /sys/fs/cgroup/mutation/cgroup.procs + ./scripts/mutation-test.sh - name: Generate coverage diff run: ./scripts/coverage-diff.sh diff --git a/.github/workflows/mutation-test-pr.yml b/.github/workflows/mutation-test-pr.yml index eb6a620b3..8804c8a18 100644 --- a/.github/workflows/mutation-test-pr.yml +++ b/.github/workflows/mutation-test-pr.yml @@ -43,5 +43,18 @@ jobs: - name: Stamp the version run: make version-semver + # A mutant can turn a bounded loop into one that allocates without + # bound, exhausting the VM before gremlins' per-mutant timeout can + # fire; the runner is then torn down mid-run and gremlins exits 0 + # without a report. Confining the run to a memory-capped cgroup makes + # the kernel kill the runaway test process instead: gremlins records + # the mutant as killed and the run completes. The cap leaves the + # 16 GB runner room for the runner agent; swap is denied so a runaway + # dies quickly instead of thrashing. - name: Run mutation tests on changed packages - run: ./scripts/mutation-test-changed.sh + run: | + sudo mkdir /sys/fs/cgroup/mutation + echo 12G | sudo tee /sys/fs/cgroup/mutation/memory.max + echo 0 | sudo tee /sys/fs/cgroup/mutation/memory.swap.max + echo $$ | sudo tee /sys/fs/cgroup/mutation/cgroup.procs + ./scripts/mutation-test-changed.sh From c137189bd321a2aae008fc5bb38cfa33b52ad98b Mon Sep 17 00:00:00 2001 From: Jeroen Soeters Date: Fri, 21 Aug 2026 11:44:10 -0700 Subject: [PATCH 2/2] chore(ci): exercise the mutation-test memory cap on a runaway-prone package --- internal/cli/status/agentview.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/cli/status/agentview.go b/internal/cli/status/agentview.go index 43620e9e5..36980a964 100644 --- a/internal/cli/status/agentview.go +++ b/internal/cli/status/agentview.go @@ -49,7 +49,8 @@ type panelSpec struct { lines []string } -// boxWidth returns the widest line of a rendered box. +// boxWidth returns the widest line of a rendered box, measured as display +// width rather than byte length. func boxWidth(box string) int { maxW := 0 for _, l := range strings.Split(box, "\n") {