Skip to content

fix(ci): cap mutation-test memory so a runaway mutant cannot kill the runner - #665

Merged
JeroenSoeters merged 1 commit into
mainfrom
fix/mutation-test-oom-confinement
Aug 21, 2026
Merged

fix(ci): cap mutation-test memory so a runaway mutant cannot kill the runner#665
JeroenSoeters merged 1 commit into
mainfrom
fix/mutation-test-oom-confinement

Conversation

@JeroenSoeters

Copy link
Copy Markdown
Collaborator

Summary

The mutation-test-pr check has been failing on any PR whose changed packages carry heavy test suites, and the nightly mutation-test-pages run has failed every night for weeks. All of these deaths share one log signature: gremlins is mid-run when the runner logs "The runner has received a shutdown signal" and the job is killed.

The mechanism, reproduced locally: a mutant can negate a loop bound and turn a bounded padding loop (for example the panel-height padding in internal/cli/status/agentview.go) into one that appends forever. The test process allocates gigabytes within seconds, faster than gremlins' per-mutant timeout can fire, and exhausts the VM. The runner is torn down, gremlins takes the shutdown signal, exits 0, and writes no report, which the check correctly classifies as "no usable result".

Fix

Run the mutation step inside a memory-capped cgroup with swap denied, in both workflows. When a mutant runs away, the kernel OOM killer now takes out the ballooning test process inside the cgroup; gremlins observes a failed test, records the mutant as killed (which is the semantically right verdict for a mutant that would crash the program), and the run completes with a report.

Verified locally by running gremlins v0.6.0 on internal/cli/status:

  • Uncapped: the test process grows past 12 GB in seconds on the padding-loop negation mutant and the machine OOMs, gremlins exits 0 with no report, mirroring CI.
  • Capped (memory.max + memory.swap.max=0, OOM-continue semantics): the same run completes in 40 seconds with a valid report, and the runaway mutant is recorded as KILLED.

The 12 GB cap is sized against the observed baseline of a normal run (2 to 6 GB aggregate across gremlins' four workers) while leaving the 16 GB runner room for the runner agent. If a legitimate suite ever exceeds the cap, the cost is a falsely killed mutant in an advisory score, not a dead runner.

Complementary to #653, which confines each gremlins invocation to the invoked package: that shrinks runs, this makes a runaway mutant in any run survivable.

… 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.
@JeroenSoeters
JeroenSoeters merged commit 04d439d into main Aug 21, 2026
30 checks passed
@JeroenSoeters
JeroenSoeters deleted the fix/mutation-test-oom-confinement branch August 21, 2026 19:40
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