carlos / platform Public
Zero-downtime flag readiness, round 3: placement under the hardened edge unit
> For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.
Goal: Make armed generation starts work on the shipped, hardened carlos-edge.service by replacing clone3 CLONE_INTO_CGROUP placement with an exec-shim, make arming prove placement before it arms, and fix the three smaller defects the second dev-box check found.
Architecture: Every cgroup.Manager start (flat, generation, worker, legacy sidecar, seed) starts the carlos binary itself as /proc/self/exe __cgexec. The shim receives the tenant's exec triple over a pipe, writes its own PID into the target cgroup's cgroup.procs through an already-validated directory fd, checks /proc/self/cgroup and the directory's identity, then execves the tenant. The parent learns the outcome over a close-on-exec status pipe before Start returns, so a shim that exits before exec is a placement failure (cgroup.ErrLimitNotApplied for generations) and the tenant never runs outside its cgroup. Arming does one real shim placement into a scratch cgroup and declines with a reason code if it fails.
Tech Stack: Go 1.26, golang.org/x/sys/unix, cgroup v2, systemd 257 (transient user units for kernel tests).
Spec: /home/paulca/amadan.net/carlos/platform-design-zero-downtime-deploys/docs/superpowers/specs/2026-10-07-zero-downtime-deploys-design.md (rev 17), with the approved rev-18 amendment on main: docs/superpowers/specs/2026-10-12-zero-downtime-deploys-rev18-amendment.md. §7.3 (containment) is the section this plan serves; the spec does not name a placement mechanism, so no spec text changes.
Inputs: amadan discussion carlos/platform #272, latest reply ("Dev-box checks, round 2", 2026-10-13); command log /home/paulca/.cache/tmp/claude-1001/zdd-dev-checks2.log; roll notes docs/superpowers/plans/2026-10-07-zero-downtime-deploys-6-roll-notes.md.
Base: branch zdd-flag-readiness-3 from origin/main 34a4328f.
Global Constraints
- Paul's ruling (2026-10-13): do NOT relax
RestrictNamespaces=yesor any other line ofinternal/host/units/carlos-edge.service. That file is not edited by this plan. - A generation's tenant code never runs outside its cgroup: no PID-move, no start-then-move, no retry outside the cgroup on the required (generation) path.
- The shim never runs tenant code, never reads tenant-controlled environment before placement, and passes argv, env, cwd, fds 0..2+len(ExtraFiles), and the
SysProcAttrthe caller set to the tenant exactly as a directexec.Cmd.Startwould. - The PID
cmd.Process.Pidthe activator tracks is the shim's PID, which is the tenant's afterexecve;cmd.Waitreports the tenant's exit. - The strict path (
PrepareStrict,PrepareStrictParent,(*Prepared).Start,internal/cgroup/strict*.go) keeps its clone3 placement unchanged. - No new member-facing strings unless unavoidable; any that are added carry draft text and a
TODO-COPYmarker and go intocopy-review/strings.json, and the controller stops and reports that path. - Commits use the identity
claude <claude~paul@keymail.dev>:git -c user.name=claude -c user.email=claude~paul@keymail.dev commit .... Never push; the controller pushes. - Sandboxed Go:
GOCACHE=$TMPDIR/gocacheon every go command. Kernel and socket tests run unsandboxed withTMPDIR=/tmp/k. Never set GOCACHE/GOMODCACHE/GOTMPDIR under/tmp. - Scratch for kernel and socket runs lives in
/tmp/k/zddr3/, which this plan owns; remove it (rm -rf /tmp/k/zddr3) at the end of each run, and never touch anything else under/tmp/k. The GOCACHE and/tmp/krules are the user's instructions for this run (Ruling 15). - Kernel tests on DOCS-T1 run in a transient delegated user unit, for example:
systemd-run --user --wait --pipe --collect -p Delegate=yes -E TMPDIR=/tmp/k/zddr3 -E CARLOS_SHIM_KERNEL=1 <test binary> -test.run '<regex>' -test.v -test.count=1(build the binary first withgo test -c -o /tmp/k/zddr3/cgroup.test ./internal/cgroup). Package test runs that use sockets keepTMPDIR=/tmp/kitself, since the activator's socket paths only fit under a root that short, andt.TempDircleans up after itself there. User delegation on DOCS-T1 offerscpu memory pids, notio. - Do not run anything on real boxes. Never run
git worktree prune. Never push to main or runamadan branch merge. - Before committing:
gofmt -lclean on touched files,go veton touched packages, andGOOS=darwin go build ./... && GOOS=windows go build ./...when a touched file is not linux-only (linux-only code needs//go:build linuxand a!linuxstub). - Heavy test loops run under
nice -n 19. - Integrity: reports paste real command output only.
Rulings recorded by this plan
Each is ledgered by the controller as Ruling: <what> — <why> — <cost if wrong>.
- (Paul, 2026-10-13) The exec-shim replaces clone3 placement; no hardening on carlos-edge is relaxed. — RestrictNamespaces makes systemd's seccomp filter answer clone3 with ENOSYS, so CLONEINTOCGROUP is impossible in the agent. — Cost if wrong: one extra exec of the carlos binary per spawn.
- Flat starts (the non-required path, flag off included) use the shim too. On a shim placement failure, which by the protocol's classification is reported only when non-execution is proven, a flat start falls back to today's start-then-move by PID; a generation start never does. An uncertain start is reported started, never retried, so no tenant runs twice. — The flat path's contract is that accounting never breaks a spawn, and every flat start under the shipped unit already uses start-then-move today. — Cost if wrong: a flag-off tenant can still run for a moment outside its cgroup when the shim fails, as every flag-off start does today.
- The clone3 attempt and its latch (
Manager.fallback) leave the Manager start paths. The strict claims path keeps clone3. — Paul: keep the strict-claims path's existing placement. — Cost if wrong: with host claims on under the shipped unit, strict launches answerErrStrictUnsupported(refused, never uncapped) until that path gets the shim. Claims are off everywhere and claims-on boxes decline arming; the roll notes record it as a claims-on-swaps precondition. - The shim is
/proc/self/exe __cgexec, used only whencgroup.RunShimIfRequested()has run in this process. —/proc/self/exeis the running build even after a roll replaces the file on disk; the hook guard stops a test binary without the hook from re-running its own tests as a "shim". — Cost if wrong: a binary that forgets the hook declines arming and its flat starts use start-then-move: loud, never unsafe. - The tenant's path, argv and env travel over a pipe in a NUL-delimited encoding, never in the shim's argv or environment, and the shim runs with an empty environment. — Secrets stay out of
/proc/<pid>/cmdline; no tenant variable (GODEBUG,LD_*,GOMAXPROCS) shapes the shim before placement; JSON would rewrite invalid UTF-8. — Cost if wrong: one pipe per spawn. - A placement in flight counts as a member of its target cgroup and every ancestor up to the host directory:
Cleanup.Populatedanswers true,Cleanup.MembersanswersErrPlacementInFlight,Cleanup.KillAndEmptywaits for it to resolve (bounded by its ctx), and the legacy-parent guard counts one aimed at the host directory itself. — With clone3 the child appeared in its cgroup atomically; the shim spends a few milliseconds in the agent's cgroup first, and an emptiness proof must not miss it. — Cost if wrong: a stop that lands during a spawn's few milliseconds waits for it or answers unproven (the lease is kept), never a false empty. - A new decline code
cgroup-placement-failedreuses the approved declined phrase (unavailableDeclined), so it adds no copy. — Same meaning as the other declined codes for a member. — Cost if wrong: a console older than the box shows the generic unknown-code phrase for it and, because it treats an unknown code as not declined (internal/rolloutwords/words.go:109,internal/console/instancepage.go:43), cannot offer or sign a host retry for that box until it is updated (consoles roll after boxes). - The arm-time placement check is box-wide: a failure declines the whole box, nested rows included. — One flag, one decision, as for
host-claims-on. — Cost if wrong: a box whose nested rows could swap declines until flat placement works. - The shim's deadline is 10 s (
shimDeadline), once up to placement and once from the release to exec. BeforeRis seen, expiry SIGKILLs the shim, drains the status pipe, and is a placement failure only if noRwas read (non-execution proven); withRread the start is reported started and nothing is killed. Failed starts are reaped withos.Process.Wait, which is bounded once the shim is dead, and exec.Cmd's I/O goroutines are released in the background. — A wake's budget is 30 s; a Go binary that cannot start in 10 s is a box condition. — Cost if wrong: a box that loaded refuses generation starts. reset-failedafter a generation stop is best-effort: a failure is logged and the stop still answers 204. — The stop itself succeeded; the failed-list entry is cosmetic. — Cost if wrong: a failed generation unit can linger insystemctl --failed.- The roll notes' probe app moves into the module as
cmd/zddprobe, with a test that launches it with the platform's argv. — A heredoc cannot be tested. — Cost if wrong: one morecmd/directory. - The 3000-seed simulation sweep runs only if the branch diff touches
internal/rolloutor the simulation's files; the shim, the arming check and the stop path are outside the modelled scheduler. — Cost if wrong: an unswept scheduler change; the final review checks the diff's paths. before(the caller's last ownership check) moves from before the fork to after placement, immediately before exec, behind a placed/release handshake (Astra plan 2). —internal/activator/genbackend.go:69requires the check after preparation and immediately before execution; with the shim, placement is preparation. — Cost if wrong: a refused start now costs a fork and a placement that are killed and reaped.- The shim's exec thread gets an empty signal mask; dispositions are left to
execve. — systemd starts every service with an empty mask, and Go's own forked child restores the mask the agent started with; the kernel test compares a shim start with a direct start under an inherited ignored SIGHUP,Pdeathsig,Setpgid, capabilities andRLIMIT_NOFILE. — Cost if wrong: a tenant started by an agent that inherited blocked signals would see them unblocked. GOCACHE=$TMPDIR/gocachein the sandbox and/tmp/kfor socket and kernel runs stand (Astra plan 11). — They are the user's instructions for this run; scratch is confined to/tmp/k/zddr3, removed after each run. — Cost if wrong: none.- The tenant's
cmd.Diris applied by the shim after placement, not by exec.Cmd at the shim's fork (Astra plan 5). — A badDiris then the command's own error (*os.PathError{Op: "chdir"}), never misread as a placement failure. — Cost if wrong: onechdirper spawn moves from Go's fork path to the shim.
Review Focus
- Exact pass-through. A tenant env with invalid UTF-8, an empty value,
=inside a value and a 100 KiB value, and an argv with an empty element, a newline and spaces reach the tenant byte-for-byte, the same as a direct start; a value over Linux's per-string limit fails with the sameE2BIGa direct start gets (Task 1,TestShimKernelPassesArgvEnvExactly). - Inherited state. A
build()that setsExtraFiles(two pipes) gets them at fds 3 and 4 in the tenant, and the tenant has no other fd above 2: none of the shim's spec, directory or status fds, and none the agent opened without close-on-exec. Signal mask and ignored set, capabilities,Pdeathsig, process group andRLIMIT_NOFILEequal a direct start's (Task 1,TestShimKernelInheritedStateMatchesADirectStart). - The agent's binary replaced on disk. After a roll replaces
/opt/carlos/carlos, the running agent's shim still execs (Task 1,TestShimKernelSurvivesDeletedExecutable). - A stop during a spawn.
KillAndEmptyorPopulatedon a host whose generation is being placed never reports empty while the shim is in flight (Task 2,TestCleanupCountsPlacementInFlight). - A missing tenant binary. The start returns the command's own
fork/exec ... no such file or directoryerror, notErrLimitNotApplied, nothing ran, the shim is reaped, and an empty generation directory is removed (Task 1 and Task 2,TestShimExecFailureIsTheCommands,TestGenerationMissingBinaryIsNotABoxCondition).
---
File structure
| File | Task | Responsibility | |---|---|---| | internal/cgroup/shim.go | 1 | Verbs, the hook (RunShimIfRequested, shimAvailable), the spec encoding, placementError, ErrPlacementInFlight. All platforms. | | internal/cgroup/shim_linux.go | 1 | runShim, runProbe, startViaShim (parent side). | | internal/cgroup/shim_other.go | 1 | !linux stubs: the shim verbs exit 127, startViaShim answers a placement error. | | internal/cgroup/main_test.go | 1 | TestMain calling RunShimIfRequested(). | | internal/cgroup/shim_test.go | 1 | Encoding round-trips (all platforms). | | internal/cgroup/shim_linux_test.go | 1 | Ungated parent/shim protocol tests that need no delegation. | | internal/cgroup/shim_kernel_linux_test.go | 1 | Gated (CARLOS_SHIM_KERNEL=1) real-cgroup shim tests. | | main.go | 1 | cgroup.RunShimIfRequested() as the first statement of main(). | | internal/cgroup/inflight.go | 1 | The in-flight placement registry on Manager. | | internal/cgroup/cgroup.go, cgroup_linux.go, cgroup_other.go, gen.go, cleanup_linux.go | 2 | startIn on the shim; latch removed; Cleanup and the parent guard count placements in flight. | | internal/cgroup/verify_linux.go, verify_other.go | 3 | Manager.VerifyPlacement. | | internal/host/rollout.go, internal/rolloutwords/words.go, internal/status/snapshot.go | 3 | The decline code and its wording. | | internal/cgroup/edge_hardening_kernel_linux_test.go | 4 | The transient-unit test under the edge unit's sandbox lines. | | cmd/zddprobe/main.go, cmd/zddprobe/main_test.go | 5 | The roll notes' probe app, tested with the platform's argv. | | internal/cgroup/strict_kernel_linux_test.go | 6 | logf on the test's second managers. | | internal/instancectl/gen.go (+ its test) | 6 | reset-failed after a generation stop. | | docs/superpowers/plans/2026-10-07-zero-downtime-deploys-6-roll-notes.md | 5, 7 | The probe section; the round-3 notes and the dev re-check list. |
---
Task 1: The exec-shim (opus)
Files:
- Create:
internal/cgroup/shim.go,internal/cgroup/shim_linux.go,internal/cgroup/shim_other.go,internal/cgroup/inflight.go,internal/cgroup/main_test.go,internal/cgroup/shim_test.go,internal/cgroup/shim_linux_test.go,internal/cgroup/inflight_linux_test.go,internal/cgroup/shim_kernel_linux_test.go - Modify:
main.go:20(first statement ofmain());internal/cgroup/cgroup.go:173-184(the registry's fields onManager)
Interfaces:
- Produces (used by Tasks 2 to 4): -
const ShimVerb = "__cgexec",const ProbeVerb = "__cgprobe"-func RunShimIfRequested()— first statement ofmain()and ofTestMain. Marks the process as able to serve the shim; ifos.Args[1]isShimVerborProbeVerbit runs that and callsos.Exit, never returning. -func shimAvailable() bool— the hook ran in this process and the platform is linux. -type placementError struct{ err error }withError()andUnwrap();func isPlacementError(err error) bool. -var ErrPlacementInFlight = errors.New("cgroup: a process is being placed in this cgroup")(defined here, used by Task 2). -func (m *Manager) startViaShim(dir string, cmd *exec.Cmd, before func() error) (refused bool, err error)— linux. On nil error the tenant is running indirandcmd.Processis it.refusedmeansbeforesaid no and nothing started. Any other error: nothing ran and any shim process is reaped; a*placementErrormeans the process could not be placed, anything else is the command's own error (asexec.Cmd.Startwould have returned it). It registers the placement in flight itself, afterbeforeagrees. - The in-flight registry:beginPlacement,placementsUnder,placementsAt,waitPlacementsUnder(signatures below). -var shimDeadline = 10 * time.Second-var shimExe = "/proc/self/exe" - Consumes:
ParseSelfCgroup(internal/cgroup/cgroup.go:155).
The protocol (revised after Astra's plan review, CODEX-FINDINGS-ZDD-READINESS3-PLAN.md findings 1, 2, 3 and 5).
The rule the classification rests on: the shim writes R immediately before execve, and never earlier. So a status stream with no R proves no tenant code ran, and any stream with R and no E after it means the tenant may have run and the start is reported as started. A start is only ever called a failure when non-execution is proven; an uncertain one is a started process the caller owns (its Wait will show how it ended), so the activator's descendant proofs (internal/activator/backend_exec.go:1037-1106) still run for it, and the flat fallback never runs a tenant twice.
Parent, in startViaShim:
- If
!shimAvailable(), return&placementError{errors.New("the placement shim is not available in this process")}. Ifcmd.Err != nil(a failedLookPathinbuild()), returncmd.Err. - Open
dirwithunix.Open(dir, O_RDONLY|O_DIRECTORY|O_NOFOLLOW|O_CLOEXEC, 0).unix.Fstatfsit; anything butunix.CGROUP2_SUPER_MAGICis a placement error. Computeexpected:dirmust start with/sys/fs/cgroup/;expectedisdirwith the/sys/fs/cgroupprefix cut (so it starts with/). Anything else is a placement error. - Capture the tenant before rewriting anything:
path := cmd.Path;argv := cmd.Args, or[]string{cmd.Path}when empty (exec.Cmd's own rule);env := cmd.Environ()(exactly whatStartwould pass,PWDincluded);wd := cmd.Dir;keep := 3 + len(cmd.ExtraFiles). os.Pipe()twice: spec (parent writes the spec and later the release byte; shim reads) and status (shim writes; parent reads). Rewritecmd:Path = shimExe,Args = []string{"carlos-cgexec", ShimVerb, itoa(keep), itoa(keep+1), itoa(keep+2)},Env = []string{},Dir = ""(the shim changes directory itself, after placement, so a badDiris reported as the command's error and never as a placement error),ExtraFiles = append(original ExtraFiles, specR, dirFile, statusW). LeaveStdin,Stdout,Stderr,SysProcAttr,Cancel,WaitDelayuntouched.- One absolute deadline
D := now + shimDeadlinecovers steps 5 to 7.done := m.beginPlacement(dir)(the in-flight registry, below), thenstartCmd(cmd)(internal/cgroup/cgroup.go:332). Close the parent'sspecR,dirFileandstatusWwhatever happened. A failedstartCmdmeans the carlos binary itself did not start (the tenant'sDiris not part of this start):done(), closespecWandstatusR, return a placement error. - Write the spec to
specW(an 8-byte big-endian length, then the encoding below) with write deadlineD. Do not closespecW: the release byte goes on it later. - Read
statusRline by line with read deadlineDuntilA,P, or EOF: -P <reason>, or EOF: the shim failed or died before placement was confirmed. No tenant code ran. - deadline: SIGKILL the shim. No release byte was sent, so it cannot have exec'd. No tenant code ran. - In both cases:reap(cmd)(below),done(), closespecWandstatusR, return a placement error naming the reason, "the shim exited before placement" or "no answer from the shim within 10s". -A: the process is indirnow, visible to every reader of the cgroup, sodone()here. The registry covers only fork-to-placement, and no caller code runs inside it. - Ask
before(when set), now, after placement and immediately before exec, asinternal/activator/genbackend.go:69requires. A refusal: closespecWwithout the release byte (the shim reads EOF and exits 125 without exec),reap(cmd), closestatusR, returntrue, before's error. - Write the release byte
XtospecWand close it. A second deadlineD2 := now + shimDeadline. ReadstatusRuntil EOF with deadlineD2: -E chdir <errno>:reap(cmd), return&os.PathError{Op: "chdir", Path: wd, Err: syscall.Errno(n)}. No tenant code ran. -E exec <errno>(afterR):reap(cmd), return&os.PathError{Op: "fork/exec", Path: path, Err: syscall.Errno(n)}. No tenant code ran:execvereturned. - EOF with noR: the shim died after placement and beforeR. No tenant code ran.reap(cmd), return a placement error, "the shim exited before exec". -Rthen EOF: started. ClosestatusR, returnfalse, nil. - deadline withRalready read: started (anexecvestill loading). ClosestatusR, returnfalse, nil, and do not kill. - deadline with noR: SIGKILL the shim, then readstatusRto EOF (the shim is dead, so the write end is closed). AnRin what was read: started, returnfalse, nil; the caller'sWaitwill show the SIGKILL. NoR:reap(cmd), placement error. No tenant code ran. reap(cmd):cmd.Process.Kill(), thencmd.Process.Wait(), which reaps the PID and is bounded (no tenant ran, so nothing else holds the child's pipes open once it is dead), thengo cmd.Wait()so exec.Cmd's copying goroutines and pipes are released in the background without the caller's I/O holding this call (os/exec/exec.go:979). A failed start'scmdis never handed back.- Every return closes
specWandstatusRexactly once (a single deferred close of whichever are still open).
Shim, in runShim(args []string) int (args are the three fd numbers):
- Parse the three fds; they must be
keep,keep+1,keep+2for somekeep >= 3. SetFD_CLOEXECon the status fd first, so every later path closes it at exec. - Read the 8-byte length (at most 16 MiB) and then exactly that many bytes from the spec fd. Decode them. Keep the spec fd open: the release byte comes on it.
- Mark every fd
>= keepclose-on-exec:unix.CloseRange(uint(keep), math.MaxUint32, unix.CLOSE_RANGE_CLOEXEC); if that answersENOSYS,EPERMorEINVAL, walk/proc/self/fdand setFD_CLOEXECon each fd>= keep. This covers the spec, directory and status fds and anything the agent leaked without close-on-exec. unix.Fstatfs(dirFD)must beCGROUP2_SUPER_MAGIC.unix.Openat(dirFD, "cgroup.procs", O_WRONLY|O_NOFOLLOW|O_CLOEXEC, 0); writestrconv.Itoa(os.Getpid()); close. This moves the whole process (every Go runtime thread) while it is still carlos code.- Read
/proc/self/cgroup;ParseSelfCgroup;"/"+relmust equal the spec'sExpected. unix.Fstat(dirFD)andunix.Lstat("/sys/fs/cgroup"+Expected)must agree onDevandIno, so the cgroup the kernel reports is the directory the parent validated.- Any failure in 1 to 7: write
P <step>: <error>\nto the status fd and return 125. Tenant code has not run. - Close the directory fd. Write
A\n. - Read one byte from the spec fd. Anything but
X(EOF, an error, another byte): return 125 without writing more. The parent refused or is gone. - If the spec's
Diris not empty,os.Chdir(Dir); a failure writesE chdir <errno>\nand returns 126. - Restore the exec state a direct start would have:
runtime.LockOSThread(), then set this thread's signal mask to empty (unix.PthreadSigmask(unix.SIG_SETMASK, &unix.Sigset_t{}, nil)). Go's own forked child restores the mask the agent started with (m.sigmask), and systemd starts every service with an empty mask, so empty matches a direct start under systemd. Dispositions need no work:execveresets every signal the shim's runtime handles to default, and a signal the agent inherited as ignored (SIGHUP,SIGINT) is inherited ignored by both a direct child and the shim, whose runtime keeps it ignored.syscall.Execrestores the originalRLIMIT_NOFILE(Go sourcesyscall/exec_unix.go:286).Pdeathsig, the process group, and ambient capabilities are set at the shim's fork and survive bothexecves exactly as they survive one. - Write
R\n, thensyscall.Exec(spec.Path, spec.Argv, spec.Env). If it returns, writeE exec <errno>\nand return 126.
The shim writes nothing to fds 0 to 2.
runProbe() int (for Task 3): read /proc/self/cgroup, print "/"+rel and a newline to stdout, return 0; return 1 when there is no v2 line.
The in-flight registry (inflight.go, used by Task 2's proofs): a mutex-guarded map[string]int of target directories (cleaned paths), plus a chan struct{} that is closed and replaced whenever a count drops, so waiters wake.
func (m *Manager) beginPlacement(dir string) (done func()) // done is idempotent
func (m *Manager) placementsUnder(dir string) int // targets equal to dir or below dir+"/"
func (m *Manager) placementsAt(dir string) int // targets equal to dir
func (m *Manager) waitPlacementsUnder(ctx context.Context, dir string) error // nil at zero, ctx.Err() if ctx ends firstA nil *Manager answers zero and nil, and its beginPlacement returns a no-op.
Spec encoding (shim.go): fields each terminated by \x00, in order: the magic carlos-cgexec-1, Path, Dir, Expected, KeepFDs (decimal), TestHold (decimal, 0 for none), len(Argv) (decimal), each argv element, len(Env) (decimal), each env entry. Encoding refuses any string that contains \x00 (execve could not carry it anyway). Decoding refuses a wrong magic, a short or trailing buffer, or a count above 1<<20.
type shimSpec struct {
Path string
Dir string // the tenant's cmd.Dir; the shim changes to it after placement
Argv []string
Env []string
Expected string // "/..." as /proc/self/cgroup shows it after "0::"
KeepFDs int // fds 0..KeepFDs-1 are the tenant's
TestHold int // test binaries only: 0 none, 1 hold before step 4, 2 hold after step 10 (before R)
}
func encodeShimSpec(s shimSpec) ([]byte, error)
func decodeShimSpec(b []byte) (shimSpec, error)TestHold is honoured only when testing.Testing() is true in the shim process (the package already imports testing, cgroup.go:30), so in the carlos binary it is inert. When honoured, the shim sleeps at that point until it is signalled, with default signal dispositions (no signal.Notify): a test's SIGTERM or SIGKILL kills it there. The parent sets the field from var shimTestHold int, and calls var shimAfterRelease func() (nil in production) between writing the release byte and reading the status, which only test files set.
- [ ] Step 1: Write the failing tests
internal/cgroup/main_test.go:
package cgroup
import (
"os"
"testing"
)
func TestMain(m *testing.M) {
RunShimIfRequested()
os.Exit(m.Run())
}internal/cgroup/shim_test.go (all platforms):
func TestShimSpecRoundTripsExactBytes(t *testing.T) {
in := shimSpec{
Path: "/run/carlos/exe/app",
Argv: []string{"app", "", "two words", "line\nbreak", "--db", "/data/x.db"},
Env: []string{"A=1", "EMPTY=", "EQ=a=b", "BAD=\xff\xfe", "BIG=" + strings.Repeat("x", 1<<20)},
Expected: "/system.slice/carlos-edge.service/carlos-exec@a.example/g3",
KeepFDs: 5,
}
b, err := encodeShimSpec(in)
if err != nil {
t.Fatal(err)
}
out, err := decodeShimSpec(b)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(in, out) {
t.Fatalf("round trip changed the spec")
}
}
func TestShimSpecRefusesNUL(t *testing.T) {
if _, err := encodeShimSpec(shimSpec{Path: "/x", Argv: []string{"a\x00b"}, Expected: "/g", KeepFDs: 3}); err == nil {
t.Fatal("a NUL inside argv was encoded")
}
}
func TestShimSpecRefusesTruncatedAndTrailing(t *testing.T) {
b, _ := encodeShimSpec(shimSpec{Path: "/x", Argv: []string{"x"}, Env: []string{"A=1"}, Expected: "/g", KeepFDs: 3})
for _, bad := range [][]byte{b[:len(b)-1], append(append([]byte{}, b...), 'z', 0), []byte("carlos-cgexec-0\x00")} {
if _, err := decodeShimSpec(bad); err == nil {
t.Fatalf("decoded a bad spec %q", bad)
}
}
}internal/cgroup/shim_linux_test.go (ungated; a temp directory is not cgroupfs, so these exercise the failure paths):
// TestShimRefusesANonCgroupDirectory: the parent's own check (step 2)
// refuses before anything starts, as a placement error.
func TestShimRefusesANonCgroupDirectory(t *testing.T) {
m := &Manager{root: t.TempDir(), logf: t.Logf}
marker := filepath.Join(t.TempDir(), "ran")
cmd := exec.Command("/bin/sh", "-c", "touch "+marker)
refused, err := m.startViaShim(m.root, cmd, nil)
if refused || !isPlacementError(err) {
t.Fatalf("refused=%v err=%v, want a placement error", refused, err)
}
if cmd.Process != nil {
t.Fatal("a process was started")
}
if _, err := os.Stat(marker); err == nil {
t.Fatal("tenant code ran")
}
}
// TestShimUnavailableWithoutTheHook: a process whose main never ran the
// hook never starts a shim.
func TestShimUnavailableWithoutTheHook(t *testing.T) { ... flip the hooked flag off for the test, restore in Cleanup; startViaShim answers a placement error and cmd.Process stays nil ... }
// TestShimPlacementFailureIsReported runs a real shim (this test binary,
// through TestMain's hook) against a temp directory. Split startViaShim
// into its checked prologue (steps 1 to 3) and a core,
// func (m *Manager) startShimCore(dir string, dirFile *os.File, expected string, cmd *exec.Cmd, before func() error) (bool, error)
// and call the core with a temp directory's fd, which the prologue would
// have refused. The shim's own fstatfs check (shim step 4) answers "P".
func TestShimPlacementFailureIsReported(t *testing.T) {
m := &Manager{root: t.TempDir(), logf: t.Logf}
f, err := os.Open(m.root)
if err != nil {
t.Fatal(err)
}
defer f.Close()
marker := filepath.Join(t.TempDir(), "ran")
cmd := exec.Command("/bin/sh", "-c", "touch "+marker)
_, err = m.startShimCore(m.root, f, "/nowhere", cmd, nil)
if !isPlacementError(err) {
t.Fatalf("err=%v, want a placement error", err)
}
if _, err := os.Stat(fmt.Sprintf("/proc/%d", cmd.Process.Pid)); err == nil {
t.Fatal("the shim was not reaped")
}
if _, err := os.Stat(marker); err == nil {
t.Fatal("tenant code ran")
}
if m.placementsUnder(m.root) != 0 {
t.Fatal("the placement is still registered")
}
}
// TestShimBeforeRefusalRegistersNothing: before says no, nothing starts and
// nothing is registered.
func TestShimBeforeRefusalRegistersNothing(t *testing.T) { ... }
// TestPlacementsUnderCountsTheSubtree and TestWaitPlacementsUnderWakes
// cover the registry (inflight_linux_test.go):
func TestPlacementsUnderCountsTheSubtree(t *testing.T) {
m := &Manager{root: "/r", logf: t.Logf}
done := m.beginPlacement("/r/carlos-exec@a.example/g2")
if m.placementsUnder("/r/carlos-exec@a.example") != 1 || m.placementsAt("/r/carlos-exec@a.example") != 0 {
t.Fatal("a child's placement is under its parent, not at it")
}
if m.placementsUnder("/r/carlos-exec@a.exampleX") != 0 {
t.Fatal("a prefix without a separator matched")
}
done()
done() // idempotent
if m.placementsUnder("/r/carlos-exec@a.example") != 0 {
t.Fatal("done did not drop the count")
}
}
func TestWaitPlacementsUnderWakes(t *testing.T) { ... one in flight; a waiter with a 50 ms ctx returns context.DeadlineExceeded; a second waiter returns nil within 1 s of done() ... }The exec-failure case needs a placement that succeeds, so it lives in the kernel file below. This file also covers the two status classifiers, which shim_linux.go factors out of startViaShim:
// classifyPlacement reads the stream up to placement (parent step 7).
// placed is true only for "A". timedOut is the read deadline having passed.
func classifyPlacement(b []byte, timedOut bool) (placed bool, err error)
// classifyExec reads the stream after the release byte (parent step 9).
// killed means the deadline passed with no R, the shim was SIGKILLed, and
// b is everything read until EOF after that.
func classifyExec(path, wd string, b []byte, killed bool) (started bool, err error)
func TestShimStatusClassification(t *testing.T) {
for _, c := range []struct {
in string
timedOut bool
placed bool
}{
{"A\n", false, true},
{"P procs: permission denied\n", false, false},
{"", false, false}, // died before placement
{"", true, false}, // deadline before placement
{"garbage\n", false, false},
} { ... placed must match; !placed must give isPlacementError(err) ... }
for _, c := range []struct {
in string
killed bool
started bool
op string // "" for a placement error, else the *os.PathError's Op
errno syscall.Errno
}{
{"R\n", false, true, "", 0},
{"R\n", true, true, "", 0}, // R then killed: may have exec'd, so started
{"", false, false, "", 0}, // died after release, before R: placement error
{"", true, false, "", 0}, // killed before R: placement error
{"E chdir 2\n", false, false, "chdir", syscall.ENOENT},
{"R\nE exec 2\n", false, false, "fork/exec", syscall.ENOENT},
{"R\nE exec 7\n", false, false, "fork/exec", syscall.E2BIG},
{"garbage\n", false, false, "", 0},
} { ... started must match; when !started, op "" means isPlacementError(err), else errors.As *os.PathError with that Op, Path "/bin/app" for fork/exec and "/wd" for chdir, and Err == errno ... }
}internal/cgroup/shim_kernel_linux_test.go (gated: CARLOS_SHIM_KERNEL=1 inside a delegated unit, with genKernelManager's setup at gen_kernel_linux_test.go:27; without the variable every test skips, but WITH it any delegation failure is t.Fatal, never a skip, so a run that was asked for cannot pass by skipping). Each test places into m.root/<name> made with os.Mkdir and a TierDefault limit written through m.applyLimit(..., true). Tenants that must not run touch a marker file first thing, and each such test asserts the marker is absent:
TestShimKernelPlacesBeforeExec: tenant/bin/sh -c 'cat /proc/self/cgroup; echo $$'. AfterstartViaShimreturns:/proc/<pid>/cgroupnames the target; the tenant's printed cgroup is the target; the tenant's printed$$equalscmd.Process.Pid(the leader PID is the shim's, now the tenant's);cmd.Wait()returns nil.TestShimKernelExitCodePassesThrough: tenant/bin/sh -c 'exit 7';cmd.Wait()gives an*exec.ExitErrorwithExitCode() == 7.TestShimKernelPassesArgvEnvExactly: the test binary as tenant in a child mode (CARLOS_SHIM_CHILD=dump) writesos.Argsandos.Environ()as NUL-joined bytes to stdout. The env carriesEMPTY=,EQ=a=b,BAD=\xff\xfeand a 100 KiB value; argv carries"","two words"and"line\nbreak". Compare byte-for-byte against the same command started directly withcmd.Start(), and compare itsos.Getwd()withcmd.Dir. Then a 200 KiB single value (above Linux's 32-page per-string limit): both the shim start and the direct start fail withE2BIG, the shim's as*os.PathError{Op: "fork/exec"}, not a placement error.TestShimKernelInheritedStateMatchesADirectStart: before starting, the test process runssignal.Ignore(syscall.SIGHUP)(restored withsignal.Resetin Cleanup), and the command setsSysProcAttr{Setpgid: true, Pdeathsig: syscall.SIGKILL}plus twoExtraFilespipes. The agent side also opens one file withsyscall.OpenwithoutO_CLOEXEC(closed in Cleanup). The child mode (CARLOS_SHIM_CHILD=state) prints: its/proc/self/fdlisting (minus the listing's own fd),SigBlk,SigIgn,CapInh,CapPrm,CapEff,CapBnd,CapAmbandNoNewPrivsfrom/proc/self/status,PR_GET_PDEATHSIG,getpgid(0) == getpid(), andRLIMIT_NOFILEsoft and hard. Every line equals a direct start's, and the fd listing is exactly0 1 2 3 4.TestShimKernelChdirFailureIsTheCommands:cmd.Dirnames a missing directory: the error is*os.PathError{Op: "chdir"}withENOENT, not a placement error; reaped; marker absent.TestShimExecFailureIsTheCommands: tenant path/nonexistent/app(setcmd.Pathdirectly soLookPathdoes not fail first):*os.PathError{Op: "fork/exec"}withENOENT,!isPlacementError(err), the PID is gone from/proc.TestShimKernelPlacementFailureNeverRunsTenant: target a cgroup the shim cannot write (chmod 0444itscgroup.procs, restore in Cleanup): placement error, marker absent, reaped.TestShimKernelBeforeRunsAfterPlacement:beforereads the target'scgroup.eventsand recordspopulated 1(the shim is in place when it is asked); then a second run whosebeforerefuses:refused == true, before's error returned, marker absent, reaped, nothing registered.TestShimKernelSignalBeforePlacement:shimTestHold = 1; astartCmdseam wrapper (cgroup.go:332) starts the shim and sends it SIGTERM; a placement error ("exited before placement"), marker absent, reaped. Then the same with SIGKILL.TestShimKernelSignalBeforeExec:shimTestHold = 2(after the release byte, beforeR); SIGKILL it fromshimAfterRelease: a placement error ("exited before exec"), marker absent, reaped.TestShimKernelExecBeforeAcknowledgement:shimDeadline = 200ms(restored in Cleanup) and ashimAfterReleasehook that waits until the tenant's marker exists and then sleeps 400 ms, past the deadline. The tenant ran, so the start must be reported started: nil error,cmd.Wait()succeeds. This pins the rule that an exec that happened is never reported as a pre-exec failure.TestShimKernelDeadlineBeforePlacement:shimTestHold = 1,shimDeadline = 300ms, no signal: a placement error naming the deadline, reaped, marker absent.TestShimKernelSurvivesDeletedExecutable: copyos.Args[0]tot.TempDir()/copy.test, run the copy as a child test (-test.run '^TestShimKernelDeletedChild$', envCARLOS_SHIM_KERNEL=1 CARLOS_SHIM_DELETED_CHILD=1 CARLOS_GEN_KERNEL=1); the child deletes its own executable, then places/bin/truethroughstartViaShimand must succeed.TestShimKernelOverhead(informational, never fails on time): 20 shim starts of/bin/trueagainst 20 direct starts;t.Logfthe medians.
A reported kernel run must show --- PASS for every test named here and no --- SKIP lines; paste the -test.v output.
- [ ] Step 2: Run the tests to see them fail
Run: cd <worktree> && GOCACHE=$TMPDIR/gocache go test ./internal/cgroup -run 'Shim' -count=1 Expected: build failure (encodeShimSpec, startViaShim, RunShimIfRequested undefined).
- [ ] Step 3: Implement
shim.go:
package cgroup
import (
"errors"
"os"
"sync/atomic"
)
// ShimVerb is the carlos binary's hidden placement verb (Paul's ruling,
// #272, 2026-10-13). carlos-edge.service's RestrictNamespaces=yes makes
// systemd's seccomp filter answer clone3 with ENOSYS, so no child can be
// created inside its cgroup. Instead the child starts as this binary,
// writes its own PID into the target cgroup through a directory fd the
// parent validated, checks where it landed, and only then execs the
// tenant: the tenant's code never runs outside its limits.
const ShimVerb = "__cgexec"
// ProbeVerb prints the process's own cgroup and exits: the tenant the
// arm-time placement check (VerifyPlacement) execs through the shim.
const ProbeVerb = "__cgprobe"
var shimHooked atomic.Bool
// RunShimIfRequested must be the first statement of main(), and of TestMain
// in a test binary that places processes. It marks this process as able to
// serve the shim (shimAvailable), and when this process IS a shim or a
// probe it runs it and exits; it returns only when it is neither.
func RunShimIfRequested() {
shimHooked.Store(true)
if len(os.Args) < 2 {
return
}
switch os.Args[1] {
case ShimVerb:
os.Exit(runShim(os.Args[2:]))
case ProbeVerb:
os.Exit(runProbe())
}
}
// placementError is a start whose process could not be placed in its
// cgroup before exec. Nothing of the tenant ran.
type placementError struct{ err error }
func (e *placementError) Error() string { return "placement before exec: " + e.err.Error() }
func (e *placementError) Unwrap() error { return e.err }
func isPlacementError(err error) bool {
var p *placementError
return errors.As(err, &p)
}
// ErrPlacementInFlight is a cleanup asked for members while a process is
// being placed in its cgroup (Task 2).
var ErrPlacementInFlight = errors.New("cgroup: a process is being placed in this cgroup")shimAvailable() is shimHooked.Load() in shim_linux.go and false in shim_other.go. shim_other.go's runShim/runProbe return 127, and its startViaShim returns false, &placementError{errors.New("placement before exec needs linux")}.
shim_linux.go implements the parent and shim protocol above. Factor the status decisions into classifyPlacement and classifyExec so TestShimStatusClassification covers them. Keep every comment to what the code does and why.
main.go, first statement of main():
func main() {
// The placement shim (internal/cgroup, #272) must run before anything
// else touches the environment or the logger: a shim process carries
// none of the tenant's settings and exits from inside this call.
cgroup.RunShimIfRequested()
rootaws.ScrubEnvironment()(Import github.com/carlosframework/platform/internal/cgroup if main.go does not already.)
- [ ] Step 4: Run the tests
Run (sandboxed): GOCACHE=$TMPDIR/gocache go test ./internal/cgroup -count=1 Expected: PASS; the kernel tests SKIP.
Run (unsandboxed, kernel):
mkdir -p /tmp/k/zddr3 && GOCACHE=$TMPDIR/gocache go test -c -o /tmp/k/zddr3/cgroup.test ./internal/cgroup && \
systemd-run --user --wait --pipe --collect -p Delegate=yes -E TMPDIR=/tmp/k/zddr3 -E CARLOS_SHIM_KERNEL=1 -E CARLOS_GEN_KERNEL=1 \
/tmp/k/zddr3/cgroup.test -test.run 'ShimKernel|ShimExecFailure' -test.v -test.count=1Expected: every ShimKernel test PASS, with the overhead medians in the log. Paste the real output into the report. Then rm -rf /tmp/k/zddr3.
Also: GOOS=darwin GOCACHE=$TMPDIR/gocache go build ./... && GOOS=windows GOCACHE=$TMPDIR/gocache go build ./... and go vet ./internal/cgroup .
- [ ] Step 5: Commit
git add internal/cgroup/shim*.go internal/cgroup/inflight*.go internal/cgroup/main_test.go internal/cgroup/cgroup.go main.go
git -c user.name=claude -c user.email=claude~paul@keymail.dev commit -m "cgroup: the placement exec-shim (#272)"---
Task 2: Every Manager start places through the shim (opus)
Files:
- Modify:
internal/cgroup/cgroup.go:170-184(Manager: dropfallback),cgroup.go:330-549(startCmd,StartGateddoc,FakePlacementForTest,startIn),internal/cgroup/cgroup_linux.go(drop what only the clone3 start used; keepsetIntoandshouldLatch, whichstrict_linux.go:344-350uses),internal/cgroup/cgroup_other.go,internal/cgroup/gen.go:171-235(ensureParent),internal/cgroup/cleanup_linux.go:54-353(Cleanup), the tests that pinned the clone3 start and its latch (internal/cgroup/cgroup_linux_test.go,gen_test.go,cgroup_test.go), and comments naming the clone3 start ininternal/activator/{swap.go:205-207, backend_exec.go:2132, backend_exec.go:2920, genbackend.go:400, genbackend.go:495, workerbuild.go:102}andinternal/cgroup/recreate_linux.go:19,118-122.
Interfaces:
- Consumes (Task 1):
startViaShim,shimAvailable,isPlacementError,ErrPlacementInFlight, and the registry (beginPlacement,placementsUnder,placementsAt,waitPlacementsUnder). - Produces: -
var placeStart = func(m *Manager, dir string, cmd *exec.Cmd, before func() error) (bool, error) { return m.startViaShim(dir, cmd, before) }— the seamFakePlacementForTestswaps. - The start functions' signatures do not change.
Behaviour.
startIn(host, dir, limit, required, build, before):
- Required (generation): if
!shimAvailable(), log once and returnErrLimitNotAppliedbefore the directory is made orbuild()runs (as the latch refusal does today,cgroup.go:453-458). Make the directory and apply the limit as today.refused, err := placeStart(m, dir, build(), before)(the shim registers the placement itself). Refused: remove the directory if empty, returnbefore's error. Success:biasOOM, return the cmd. A placement error: remove the directory if empty, logcgroup: <host>: placement before exec failed (<err>) — generation start refused, returnfmt.Errorf("%w: %s: %w", ErrLimitNotApplied, host, err). Any other error is the command's: remove the directory if empty, return it unwrapped. - Flat: make the directory (failure: start plain, as today
cgroup.go:465-466) and apply the limit best-effort as today. If!shimAvailable(), go straight tostartAndMoveIn(today's fallback). Otherwise place through the shim. Success:biasOOM, return. Refused: as today. A placement error: logcgroup: <host>: placement before exec failed (<err>) — using the PID-move fallback for this spawnandstartAndMoveInwith a freshbuild()behindbefore(Ruling 2). Any other error is the command's: remove the directory if empty and return it, with no retry. Manager.fallback, the read-once comment and the unreachable required-path guard atcgroup.go:538-546go; keep a single explicit statement that the required path never reachesstartAndMoveIn.
FakePlacementForTest: keep its name, signature, panic outside tests and no-t.Parallel note; it now swaps placeStart for one that runs startGated(cmd, before) with no shim (as if the kernel had placed it), AND sets a test-scope override that makes shimAvailable() answer true, restoring both in Cleanup. The override is safe because the fake never runs a shim, so it cannot make a test binary without the hook re-run itself (Astra plan 7; the activator's TestMain at internal/activator/activator_test.go:5894 has no hook, and TestSpawnInCgroupAtPutsAGenerationUnderItsParent, internal/activator/containment_test.go:113, must keep passing through the fake). Update its doc.
startAndMoveIn (the flat fallback, cgroup.go:625) registers its target too, from after before agrees until the PID write has returned: its child runs in the agent's cgroup for that window, and a proof must not miss it either. Give startGated an optional register step so both paths take the registry at the same point.
Cleanup (cleanup_linux.go): openCleanupAt records the manager and the directory path it pinned (filepath.Join(m.root, ExecName(host)), plus /child for a child cleanup) in cleanupState. The registry goes into the shared primitives, not only the public methods, because KillAndEmpty calls them directly (cleanup_linux.go:334, :338): populationLocked answers true, nil while m.placementsUnder(path) > 0, before reading cgroup.events, and membersLocked answers nil, ErrPlacementInFlight while one is in flight. KillAndEmpty first calls m.waitPlacementsUnder(ctx, path) (an error there is unproven), and because every emptiness check inside its loop now goes through the registry-aware populationLocked, a placement that registers after the wait keeps the proof from finishing until the placed process has been killed with the rest or the ctx ends (unproven). Write the doc comment on KillAndEmpty saying so, and that callers still exclude new starts during a proof the way they do today (the lease and the before hook); the registry only closes the window a shim spends outside its cgroup.
ensureParent (gen.go:198): after parentProcs, add m.placementsAt(dir) to n, so a flat start being placed into the host directory itself counts as a direct process and the generation start is refused with ErrLegacyParentPopulated. The parentPopulated(dir) branch at gen.go:213 also counts m.placementsUnder(dir) > 0 as populated, so a sibling generation being placed is never taken for a cold parent whose limits may be rewritten (gen.go:219-229).
Check every caller of Cleanup.Members (grep -rn "\.Members()" internal) treats an error as unproven (keeps the lease, records nothing as empty). If one does not, fix that caller in this task and name it in the report.
- [ ] Step 1: Write the failing tests
TestCleanupCountsPlacementInFlight (in cleanup_linux_test.go, over the existing fake-root helper at cleanup_linux_test.go:28, which builds &Manager{root: root} — give it logf: t.Logf): with a placement begun on <host dir>/g1, a host Cleanup's Populated() is true even though the fake cgroup.events says populated 0; Members() is ErrPlacementInFlight; KillAndEmpty with a 50 ms ctx returns an error and writes nothing to cgroup.kill; after done(), Populated() reads the file again.
TestKillAndEmptySeesAPlacementRegisteredMidProof: no placement when KillAndEmpty starts; through the kill-write seam the cleanup tests already use (or one added for this test), register a placement on <host dir>/g1 right after the kill is written. With a 100 ms ctx, KillAndEmpty must return an error (unproven), never nil, although the fake cgroup.events says populated 0.
TestEnsureParentCountsAPlacementBelowTheParent (in gen_test.go): a placement in flight on <host dir>/g1 while StartChildGated starts g2 on a parent whose fake cgroup.events says populated 0: the parent's limit files are not rewritten (the cold-parent branch is not taken).
TestEnsureParentCountsAPlacementAtTheParent (in gen_test.go): a placement begun on the host directory itself makes StartChildGated answer ErrLegacyParentPopulated and build nothing.
TestGenerationMissingBinaryIsNotABoxCondition (in gen_test.go, with a placeStart stub returning &os.PathError{Op: "fork/exec", Path: "/x", Err: syscall.ENOENT}): StartChildGated returns that error, errors.Is(err, ErrLimitNotApplied) is false, and the child directory is gone.
TestGenerationPlacementFailureIsLimitNotApplied (stub returns &placementError{...}): errors.Is(err, ErrLimitNotApplied), child directory gone, build called once.
TestGenerationRefusedWithoutTheShim (flip the hook flag off): ErrLimitNotApplied, build never called, no child directory made.
TestFlatPlacementFailureFallsBackToPIDMove (stub returns a placement error): StartGated succeeds through startAndMoveIn (the fake cgroup.procs holds the child's PID), build called twice, before asked twice.
TestFlatCommandFailureIsNotRetried (stub returns a fork/exec ENOENT): returned as is, build called once.
Replace the clone3 latch tests in cgroup_linux_test.go (those asserting UseCgroupFD, shouldLatch latching, or fallback) with the tests above; keep any test of shouldLatch that the strict path still relies on.
- [ ] Step 2: Run them to see them fail
Run: GOCACHE=$TMPDIR/gocache go test ./internal/cgroup -count=1 Expected: FAIL / build errors on the new names.
- [ ] Step 3: Implement the behaviour above.
- [ ] Step 4: Run the tests
GOCACHE=$TMPDIR/gocache go test ./internal/cgroup -count=1
GOCACHE=$TMPDIR/gocache go vet ./internal/cgroup ./internal/activator
GOOS=darwin GOCACHE=$TMPDIR/gocache go build ./... && GOOS=windows GOCACHE=$TMPDIR/gocache go build ./...Then unsandboxed, the activator package and the other cgroup users (sockets need /tmp/k):
mkdir -p /tmp/k && TMPDIR=/tmp/k GOCACHE=$HOME/.cache/go-build nice -n 19 go test ./internal/activator ./internal/host ./internal/stopevidence ./internal/instancectl -count=1Then the generation kernel tests in a delegated user unit, which now place through the shim:
mkdir -p /tmp/k/zddr3 && GOCACHE=$TMPDIR/gocache go test -c -o /tmp/k/zddr3/cgroup.test ./internal/cgroup && \
systemd-run --user --wait --pipe --collect -p Delegate=yes -E TMPDIR=/tmp/k/zddr3 -E CARLOS_GEN_KERNEL=1 -E CARLOS_SHIM_KERNEL=1 \
/tmp/k/zddr3/cgroup.test -test.run 'GenKernel|GenOOMKernel|ShimKernel|GenerationStartRefusedBesideADirectParentMember|ClaimContainment' -test.v -test.count=1; rm -rf /tmp/k/zddr3Expected: PASS (paste the real output). A test that asserted clone3-specific kernel behaviour (the cgroup.kill respawn kill was clone3's) is updated to assert the shim's behaviour on the same scenario, and the report says what changed and why.
- [ ] Step 5: Commit
git -c user.name=claude -c user.email=claude~paul@keymail.dev commit -am "cgroup: every Manager start places through the shim; placements in flight count as members (#272)"(git add the new files first.)
---
Task 3: Arming verifies placement (opus)
Files:
- Create:
internal/cgroup/verify_linux.go,internal/cgroup/verify_other.go,internal/cgroup/verify_linux_test.go - Modify:
internal/host/rollout.go:22-44(the code),rollout.go:150-166(swapArming),internal/host/rollout_test.go,internal/rolloutwords/words.go:84-112(+ its test),internal/status/snapshot.go:300-304(comment)
Interfaces:
- Consumes: Task 1's
startViaShim(which registers the placement in flight),ProbeVerb,shimAvailable,shimExe. - Produces: -
func (m *Manager) VerifyPlacement(ctx context.Context) error(linux); the!linuxstub returns an error. -const placementCheckName = "carlos-placement-check"- Ininternal/host:rolloutPlacementFailed = "cgroup-placement-failed"andvar placementCheck = func(m *cgroup.Manager) error(a 15 s ctx aroundVerifyPlacement), a seam host tests replace.
Behaviour. VerifyPlacement:
- Nil manager:
ErrNoDelegation.!shimAvailable(): an error saying the shim is not available. dir := filepath.Join(m.root, placementCheckName).os.Remove(dir)(a leftover from an earlier run is empty: the unit's restart killed its members); thenos.Mkdir(dir, 0o755);defera removal that waits for the probe to be reaped first.m.applyLimit("placement check", dir, TierDefault, true); an error is returned.cmd := exec.Command(shimExe, ProbeVerb)withEnv = []string{}andStdouta buffer.m.startViaShim(dir, cmd, nil). Any error is returned (a placement error or not: either means the check failed).- Wait for the probe with
ctx(kill and reap onctx.Done()). It must exit 0 and its stdout must be exactlyexpected + "\n"(expectedis the stringstartViaShimcomputed:dirwithout/sys/fs/cgroup). memory.maxindirmust read backTierDefault.MemoryMax.- Remove
dir; a removal failure is logged, not returned.
swapArming: after swapDelegation passes (rollout.go:160), call placementCheck(eb.Cgroups). On error: log.Printf("agent: REFUSING %s=overlap (%s): a test placement into a scratch cgroup failed (%v), so generations could not be started inside their limits; this box stays on today's stop-then-wake paths", rollout.SwapEnv, rolloutPlacementFailed, err), declineRollout(rolloutPlacementFailed), return false. A box that declines this way honours failure records exactly as the other declined codes do (honourDeclinedFailures); check that path keys on "declined", not on a list of codes, and if it keys on a list, add the code there.
rolloutwords: "cgroup-placement-failed": unavailableDeclined in unavailablePhrases, and the code in UnavailableDeclines's case list. status/snapshot.go: name the code in the comment that lists the declined codes. Grep for every other list of declined codes (grep -rn '"host-claims-on"' --include=*.go .) and add the new code to each, with a test where one exists.
- [ ] Step 1: Write the failing tests
internal/host/rollout_test.go:
func TestSwapArmingDeclinesWhenPlacementFails(t *testing.T) {
// build eb as TestSwapArming's armed case does (rollout_test.go:526)
orig := placementCheck
placementCheck = func(*cgroup.Manager) error { return errors.New("probe exited before exec") }
t.Cleanup(func() { placementCheck = orig })
if swapArming(overlapEnv(pub), eb) {
t.Fatal("armed with a failed placement check")
}
if rolloutUnarmed() != rolloutPlacementFailed {
t.Fatalf("declined as %q", rolloutUnarmed())
}
}and make the existing armed-case tests (rollout_test.go:526, :568 and any other test that expects swapArming true) stub placementCheck to nil-returning, in a helper they share. A flag-off swapArming must not call placementCheck (assert with a stub that fails the test).
internal/rolloutwords: Unavailable("cgroup-placement-failed") equals the declined phrase and UnavailableDeclines is true for it.
internal/cgroup/verify_linux_test.go: ungated: a manager over a temp root answers an error (the temp directory is no cgroup) and leaves no carlos-placement-check behind. Gated (CARLOS_SHIM_KERNEL=1, genKernelManager): VerifyPlacement returns nil and the directory is gone afterwards; with shimExe pointed at /bin/true (restored in Cleanup) it returns an error and the directory is gone.
- [ ] Step 2: Run to see them fail.
GOCACHE=$TMPDIR/gocache go test ./internal/host ./internal/rolloutwords ./internal/cgroup -run 'Placement|Unavailable|SwapArming' -count=1— build errors.
- [ ] Step 3: Implement.
- [ ] Step 4: Run. The same packages sandboxed (host tests may need unsandboxed
TMPDIR=/tmp/kfor sockets: use that if a socket error appears), plus the gated verify tests in a delegated user unit as in Task 1. Paste real output.
- [ ] Step 5: Commit —
"host: arming verifies a real shim placement; decline cgroup-placement-failed (#272)".
---
Task 4: The shim under the edge unit's hardening (opus)
Files:
- Create:
internal/cgroup/edge_hardening_kernel_linux_test.go
Interfaces:
- Consumes: Task 1 (
startViaShim), Task 2 (StartChildGatedon the shim), Task 3 (VerifyPlacement),genKernelManager(gen_kernel_linux_test.go:27). - Produces:
TestEdgeHardeningPlacement(the driver) andTestEdgeHardeningInner(runs inside the unit).
Behaviour. The driver, gated by CARLOS_EDGE_HARDENING_KERNEL=1 and run outside any unit:
- Read
../host/units/carlos-edge.service(go test runs in the package directory). Collect everyKey=valueline in[Service]. - Sort each key into one of two explicit tables in the test: applied (passed as
-p Key=valuetosystemd-run --user) and excluded (with a one-line reason each). A[Service]key in neither table fails the test, so a new hardening line forces a decision here. - The applied table must contain at least:
RestrictNamespaces,SystemCallFilter,SystemCallErrorNumber,SystemCallArchitectures,NoNewPrivileges,MemoryDenyWriteExecute,LockPersonality,RestrictRealtime,RestrictAddressFamilies,ProtectProc,ProtectClock,ProtectHostname,ProtectKernelTunables,ProtectKernelModules,ProtectKernelLogs,PrivateDevices,ProtectSystem,TasksMax. The test asserts each is present in the unit file and applied. If one of these cannot be applied in a user unit on DOCS-T1, move it to excluded with the realsystemd-runerror as its reason, and say so in the report: Task 7 carries it into the dev re-check. - Expected exclusions, each with its reason:
ExecStart,ExecReload,Restart,RestartSec,EnvironmentFile(process control, not sandbox);User,Group,AmbientCapabilities,CapabilityBoundingSet(a user manager cannot change identity or capabilities);ReadWritePaths(names box paths absent here);Slice,OOMPolicy,Delegate(the test passes its ownDelegate=yes);ProtectHome,PrivateTmponly if the test binary cannot be made visible inside them (prefer keeping them and making the binary visible with-p BindReadOnlyPaths=<binary>; say which in the report). - Unrestricted control first (Astra plan 6): run the inner test in a unit with only
-p Delegate=yesand-E CARLOS_EDGE_HARDENING_INNER=control. There clone3 must WORK: the rawclone3probe answersEINVAL(notENOSYS), and a plainexec.CmdwithSysProcAttr{UseCgroupFD: true, CgroupFD: <a scratch cgroup fd>}starts and lands in that cgroup. If the control fails, the hardened run below proves nothing, so the driver fails. - Hardened run:
systemd-run --user --wait --pipe --collect -p Delegate=yes <applied...> -E CARLOS_EDGE_HARDENING_INNER=hardened -E CARLOS_SHIM_KERNEL=1 -E CARLOS_GEN_KERNEL=1 <binary> -test.run '^TestEdgeHardeningInner$' -test.v -test.count=1; fail unless it passes. Stream both runs' output to the test log.
The inner test, gated by CARLOS_EDGE_HARDENING_INNER (control or hardened). Once the variable is set, nothing in it skips: a delegation failure, or any precondition it cannot establish, is t.Fatal. In hardened mode:
- What the unit enforces, observed from inside:
/proc/self/statusshowsSeccomp: 2andNoNewPrivs: 1; a write under/usrfails withEROFS(ProtectSystem=strict);/proc/1/cmdlineis unreadable or absent (ProtectProc=invisible);socket(AF_NETLINK, ...)fails (RestrictAddressFamilies). Print a table of every applied property markedenforced(observed here) oruntested(accepted by systemd but not observed). A property inuntestedis listed in the report so Task 7 carries it to the dev re-check on the real unit. - The filter is on: the raw
clone3probe answersENOSYS, and the plainUseCgroupFDstart fails withENOSYS, where the control run showed both working. - Placement succeeds:
StartChildGated("hardening.example", GenChild(1), lim, lim, build, nil)withlim := Limit{MemoryMax: 64 << 20}and a tenant that prints its cgroup then waits on stdin: the cgroup is.../carlos-exec@hardening.example/g1, and the PID the tenant prints iscmd.Process.Pid. - The limit applies: the existing
hogchild mode (gen_kernel_linux_test.go:65) started the same way intog2is OOM-killed:memory.eventsing2showsoom_killof at least 1, the process dies by SIGKILL, andg1's tenant is still alive. - Arming would arm:
m.VerifyPlacement(ctx)returns nil.
- [ ] Step 1: Write the test (both functions, the two tables).
- [ ] Step 2: Run it against a branch where it must fail, to prove it can: temporarily make
placeStartuse the old clone3 start (a local, uncommitted edit,setInto+cmd.Start()); the inner step 2 must fail withENOSYS/ErrLimitNotApplied. Paste that output, then revert the edit (git diffmust show only the new test file). - [ ] Step 3: Run it for real:
mkdir -p /tmp/k/zddr3 && GOCACHE=$TMPDIR/gocache go test -c -o /tmp/k/zddr3/cgroup.test ./internal/cgroup && \
cd internal/cgroup && CARLOS_EDGE_HARDENING_KERNEL=1 TMPDIR=/tmp/k/zddr3 /tmp/k/zddr3/cgroup.test -test.run '^TestEdgeHardeningPlacement$' -test.v -test.count=1; rm -rf /tmp/k/zddr3(unsandboxed). Expected: PASS, with the applied and excluded tables printed. Paste the real output.
- [ ] Step 4: Commit —
"cgroup: kernel test, shim placement under carlos-edge's sandbox lines (#272)".
---
Task 5: The probe app accepts the platform's argv (sonnet)
Files:
- Create:
cmd/zddprobe/main.go,cmd/zddprobe/main_test.go - Modify:
docs/superpowers/plans/2026-10-07-zero-downtime-deploys-6-roll-notes.md:841-895(section "1. Build a probe app")
Interfaces: none shared.
Behaviour. The platform runs an exec-backed instance as <binary> --socket <sock> --db <db> (internal/activator/backend_exec.go:2147, :2335, internal/activator/claim_snapshot.go:221), and carlos deploy's vet boots it as <binary> -socket <sock> (internal/vet/vet.go:135). The probe accepts both: --socket (required) and --db (accepted and ignored: the probe keeps no data). Everything else is the roll notes' program unchanged: serve HTTP on the unix socket, every request sleeps 50 ms then answers ok <version>\n, and SIGTERM or SIGINT drains with a 15 s shutdown. version is set by -ldflags "-X main.version=...". Put the server in func run(args []string, stop <-chan os.Signal) error so main is if err := run(os.Args[1:], sigs); err != nil { fmt.Fprintln(os.Stderr, err); os.Exit(1) }.
- [ ] Step 1: Write the failing test —
main_test.golaunches the test binary itself as the probe, the way the platform launches it:
func TestMain(m *testing.M) {
if os.Getenv("ZDDPROBE_AS_MAIN") == "1" {
main()
os.Exit(0)
}
os.Exit(m.Run())
}
// launch starts this test binary as the probe with argv, as the platform
// starts an instance: argv only, no shell.
func launch(t *testing.T, argv ...string) *exec.Cmd {
cmd := exec.Command(os.Args[0], argv...)
cmd.Env = append(os.Environ(), "ZDDPROBE_AS_MAIN=1")
cmd.Stderr = os.Stderr
if err := cmd.Start(); err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = cmd.Process.Kill(); _, _ = cmd.Process.Wait() })
return cmd
}
func TestProbeServesWithThePlatformsRunArgv(t *testing.T) {
dir := t.TempDir()
sock, db := filepath.Join(dir, "app.sock"), filepath.Join(dir, "app.db")
cmd := launch(t, "--socket", sock, "--db", db) // backend_exec.go:2335
body := getOverUnix(t, sock, "/healthz") // retries until the socket answers, 10 s cap
if !strings.HasPrefix(body, "ok ") {
t.Fatalf("body %q", body)
}
// SIGTERM drains: an in-flight request still completes, then exit 0.
...
if err := cmd.Wait(); err != nil {
t.Fatalf("exit after SIGTERM: %v", err)
}
}
func TestProbeServesWithVetsArgv(t *testing.T) { ... launch(t, "-socket", sock) (vet.go:135) answers ok ... }getOverUnix is a small helper with an http.Client whose transport dials the unix socket. The drain check must know the request is inside the handler before it signals (Astra plan 8): the probe writes one line serving to stdout at handler entry when ZDDPROBE_TRACE=1 is set (nothing otherwise). The test sets it, starts a request, reads serving from the probe's stdout pipe, then sends SIGTERM, and asserts the request answers 200 and cmd.Wait() returns nil.
- [ ] Step 2: Run to see it fail:
TMPDIR=/tmp/k GOCACHE=$TMPDIR/gocache go test ./cmd/zddprobe -count=1(socket paths: if the sandbox refuses the socket, rerun unsandboxed withTMPDIR=/tmp/k). Expected: build failure. - [ ] Step 3: Implement
main.go. Then replace the roll notes' heredoc with: build from a platform checkout at the release commit,for v in zdd-1 zdd-2 zdd-3 zdd-4; do CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags "-X main.version=$v" -o "$TMPDIR/zdd-probe/probe-$v" ./cmd/zddprobe; done, and a sentence that the probe takes the platform's--socketand--db(the round-2 dev check found the old one exited at once on--db). Keep the rest of "The probe" as it is, and fix itscd/path lines to match. - [ ] Step 4: Run the test (paste output),
go vet ./cmd/zddprobe, gofmt. - [ ] Step 5: Commit —
"cmd/zddprobe: the roll notes' probe, taking the platform's --socket and --db (#272)".
---
Task 6: TestStrictKernel's managers, and reset-failed after a generation stop (sonnet)
Files:
- Modify:
internal/cgroup/strict_kernel_linux_test.go:160,:190 - Modify:
internal/instancectl/gen.go:185-238(abandonGen,stopGen) and its test file (find it withgrep -rln "stopGen\|StopGen" internal/instancectl/*_test.go)
Behaviour.
(a) TestStrictKernel full mode builds second managers with no logf (&Manager{root: m.root} at :160 and :190); production always sets one, and freshenOne (recreate_linux.go:156) logs through it, so the test panics. Give both logf: t.Logf. No production change.
(b) After a generation unit is successfully stopped or killed, and before its files go, reset its failed state. There are two such paths: stopGen (gen.go:224) and abandonGen's successful killGen followed by removeGenFiles (gen.go:189-197, Astra plan 9). Share one helper, s.resetGenFailed(ctx, unit), which runs s.Systemctl(ctx, "reset-failed", unit). An unproven stop or kill is unchanged and does not reset. An answer containing not loaded is ignored; any other error is logged with s.logf("instance-control: reset-failed %s: %v", unit, err) and the stop still answers 204 (Ruling 10). reset-failed is already one of the control service's verbs (server.go:641).
- [ ] Step 1: Write the failing test for (b): with the recording fake
Systemctlthe instancectl tests already use, a/stop?gen=N(and&kill=1) recordsreset-failed <unit>after the stop or kill; withreset-failedanswering an error, the response is still 204 and a log line was written; a stop that fails records noreset-failed. An abandoned generation start whose kill succeeds recordsreset-failed <unit>too; one whose kill fails does not. - [ ] Step 2: Run to see it fail:
GOCACHE=$TMPDIR/gocache go test ./internal/instancectl -run StopGen -count=1(unsandboxed withTMPDIR=/tmp/kif sockets are refused). - [ ] Step 3: Implement (a) and (b).
- [ ] Step 4: Run the instancectl package, then TestStrictKernel's full mode for real, in a delegated user unit with no RestrictNamespaces (the strict path keeps clone3, Ruling 3):
mkdir -p /tmp/k/zddr3 && GOCACHE=$TMPDIR/gocache go test -c -o /tmp/k/zddr3/cgroup.test ./internal/cgroup && mkdir -p /tmp/k/zddr3/strictproof && \
systemd-run --user --wait --pipe --collect -p Delegate=yes -E TMPDIR=/tmp/k/zddr3 -E CARLOS_STRICT_KERNEL=full -E CARLOS_STRICT_PROOF_DIR=/tmp/k/zddr3/strictproof \
/tmp/k/zddr3/cgroup.test -test.run '^TestStrictKernel$' -test.v -test.count=1; rm -rf /tmp/k/zddr3Expected: all five subtests PASS (round 2 saw this pass with the same fix in scratch). Paste real output. If the mode needs other variables, read strict_kernel_linux_test.go:30-60 and set them; say which in the report.
- [ ] Step 5: Commit —
"instancectl: reset-failed a stopped generation unit; strict kernel test managers log (#272)".
---
Task 7: Roll notes (sonnet)
Files:
- Modify:
docs/superpowers/plans/2026-10-07-zero-downtime-deploys-6-roll-notes.md
Behaviour. Add a section ### Roll notes from zdd-flag-readiness-3 after ### Roll notes from zdd-flag-readiness (:336), in the file's plain style (full stops, no em dashes), covering:
- The four round-2 blockers and where each is closed (Tasks 1 to 6 by commit; the controller passes the commit list).
- The shim, in two sentences, and Paul's ruling that the edge unit's hardening stays.
- Behaviour change on roll, flag off: every exec child now starts through the shim. What the agent log shows on a healthy box (no
placement before exec failedlines, no PID-move lines), and what a failing shim looks like (placement before exec failed (...) — using the PID-move fallback for this spawn): the box still serves, as today. - The new decline code
cgroup-placement-failed, its log line, and that a console older than the box shows the unknown-code phrase until consoles roll. - Host claims: the strict path keeps clone3, so under the shipped unit a claims-on box's strict launches answer
ErrStrictUnsupported; that is now a precondition of claims-on swaps. - Dev re-check for Paul (replacing round 2's blocked steps): roll; flag-off wakes answer 200 with no fallback lines; set
CARLOS_SWAP=overlapand restart; the log showsoverlap swaps armedand noREFUSING; the probe (built fromcmd/zddprobe) under load through zdd-1 → zdd-3 with 0 failed; the hold control; a generation unit stopped by the control service is not insystemctl --failed; and any edge-unit line Task 4 had to exclude or could only markuntested(the controller passes the list), checked on the real unit. - The DOCS-T1 kernel-test recipe (
systemd-run --user ... -p Delegate=yes) and the Task 4 command.
Update the "Still blocks flag-on" lists that name the four round-2 blockers so they point to this section.
- [ ] Step 1: Write the section and the list edits.
- [ ] Step 2: Check names against the code: every identifier, log line and flag the section quotes is grepped in the tree; paste the grep output in the report.
- [ ] Step 3: Commit —
"roll notes: zdd-flag-readiness-3 (#272)".