Skip to content

[v2] autoexec validation always fails with score 0.2 on cgroup-v2 hosts — sandbox cgroup is derived from the daemon's own (non-distributable) cgroup #631

Description

@Chow36-coder

Summary

On a standard cgroup-v2 + systemd Linux host, evolver autoexec can never pass validation: every executed cycle ends as status: failed / resolutionStatus: regressed with score: 0.10.2, even when the produced diff is correct (proofOfWork.git_diff.files > 0).

The cause is in the validation sandbox, not in the runner or the model.

Environment

  • evolver 2.0.37 (npm global, evolver autoexec --solo, runner: "llm")
  • Ubuntu 24.04 under WSL2, kernel 6.6.87.2-microsoft-standard-WSL2
  • systemd 255 (255.4-1ubuntu8.17), cgroup v2 unified (/sys/fs/cgroup/cgroup.controllers present)
  • Node v24.20.0
  • Daemon runs as a systemd user service with Delegate=yes (recommended practice)

Symptom

Every smoke/real cycle:

{
  "taskId": "smoke-valcmd-v6-20260917",
  "status": "failed",
  "finalStage": "failed",
  "reason": "失败 score=0.2 → regressed",
  "outcome": { "status": "failed", "score": 0.2 },
  "proofOfWork": { "kind": "git_diff", "git_diff": { "files": 1, "lines": 1 } }
}

The diff was applied correctly — validation simply never ran.

Root cause chain

  1. @evomap/evolver-cli/dist/requiredSandboxValidation.js:23requireIsolation: true is hardcoded:

    const result = await runner(safeCommands, cwd, { ...options, requireIsolation: true });

    (Same hardcoded value also in evolver-cli/dist/recipe.js:505, evolver-cli/dist/modelCompatibility.js:232, evolver-proxy/dist/bin/evolver-proxy.js:721. We grepped the whole installed tree — there is no env var or config key that turns this off.)

  2. @evomap/evolver-core/dist/verify/sandboxedValidation.js:130-133 — when isolation is unavailable, validation is skipped and a hardcoded 0.2 is returned:

    const isolationCheck = opts.unshareCheck ?? (opts.requireIsolation ? readOnlyIsolationAvailable : unshareNetAvailable);
    const isolated = isolationCheck();
    if (opts.requireIsolation && !isolated) {
        return { passed: false, ..., score: 0.2, results: [], skipped: [], isolated: false };
    }
  3. readOnlyIsolationAvailable() is sandboxResourceLimitsAvailable() && readOnlyFilesystemIsolationAvailable().

  4. sandboxResourceLimitsAvailable()createSandboxResourceGroup() (evolver-core/dist/verify/sandboxRunner.js) creates the sandbox cgroup under the process's own cgroup:

    const parent = currentCgroupPath();          // = readFileSync('/proc/self/cgroup')
    path = join(parent, `evolver-validation-${process.pid}-${Date.now()}-...`);
    mkdirSync(path, { mode: 0o700 });
    if (!configureSandboxResourceGroup(path)) throw new Error('required cgroup v2 controllers are not delegated');

    configureSandboxResourceGroup() requires all seven files to exist: cgroup.procs, cgroup.kill, memory.max, memory.swap.max, memory.oom.group, pids.max, cpu.max. One missing → null → isolation unavailable → hardcoded 0.2.

  5. So the sandbox requires cpu/memory/pids to be listed in cgroup.subtree_control of the daemon's own cgroup.

Why that is structurally impossible

The kernel enforces the "No Internal Process Constraint": a non-root cgroup can distribute domain controllers to children only when it has no processes of its own. The daemon process necessarily lives in the cgroup that currentCgroupPath() returns — so that cgroup can never carry those controllers.

Measured on the host above:

# daemon's own cgroup
cgroup.type      : domain
procs            : 1
subtree_control  : ''            <-- empty, so the sandbox dir gets no memory.max etc.

# enable controllers while the cgroup is empty (possible)
$ echo "+cpu +memory +pids" > .../cgroup.subtree_control
$ cat .../cgroup.subtree_control
cpu memory pids
$ ls .../probe-child            # every required file appears
memory.max  memory.swap.max  memory.oom.group  pids.max  cpu.max

# ... but now nothing can live there:
$ echo <pid> > .../cgroup.procs
write error: Device or resource busy     <-- both for the original process and for a fresh one

Same result through systemd when the main process is started after delegation is prepared:

systemd: Failed to attach to cgroup /user.slice/.../evolver-cg-probe.service: Device or resource busy
systemd: Main process exited, code=exited, status=219/CGROUP

Host-wide scan — the root cgroup is the only one that has both (it is exempt from the constraint):

procs=221  'cpu memory pids'   (root)
procs=0    'cpu memory pids'   /user.slice/user-1000.slice/user@1000.service
procs=0    'cpu memory pids'   /user.slice/.../app.slice
procs=0    'memory pids'       /system.slice

Delegation was verified to work up to the unit level (Delegate=yes, DelegateControllers=cpu cpuset io memory pids, and the chain user.slice → user-1000.slice → user@1000.service → app.slice all distribute cpu memory pids). We also tried DelegateSubgroup=payload (systemd 254+), which leaves the unit cgroup process-free — the controllers enable fine there, but then currentCgroupPath() points at the subgroup, which holds the daemon again: same dead end.

Impact

On any modern Linux with systemd + cgroup v2, evolver autoexec executes nothing beyond "prompt → diff", because no host setup can satisfy step 4. The only configuration that could work is a process running directly in the real root cgroup (exempt), which normally means privileged/root execution outside a delegating manager.

Suggested fixes (any one would unblock this)

  1. Configurable sandbox parent: allow the sandbox cgroup root to be specified (env/config), and/or allocate it as a sibling under the parent of currentCgroupPath() — a cgroup that the host manager can keep process-free and delegated.
  2. Degrade gracefully: don't hard-fail with the sentinel 0.2 when only some controllers/limits are available. Better: run validation anyway (with the limits that exist, or with unshare-only isolation) and report isolated: false in the result instead of scoring it as a regression. A documented escape hatch (e.g. EVOLVER_REQUIRE_ISOLATION=0) would also help — currently none exists.
  3. Document the requirement: if sandboxed validation genuinely requires the root cgroup, that should be stated explicitly, because no standard systemd-managed deployment can satisfy it.

Happy to re-run any probe or provide full logs — just say which artifact you want.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions