Remove --headless (systemd transient-scope detach); logging, tee fix, and native shell-out removal#12
Merged
Merged
Conversation
ananthb
force-pushed
the
fix/headless-cgroup-escape
branch
from
July 15, 2026 20:16
94f8647 to
f771539
Compare
Supervise gave cmd.Stdout and cmd.Stderr two separate MultiWriters both teeing into the same LogWriter. os/exec copies the child's stdout and stderr on distinct goroutines, so the shared bytes.Buffer was written concurrently without synchronization — a data race that dropped lines (TestSuperviseTeesToLogWriter flaked in CI). Share one mutex-guarded writer between the two tees.
--headless daemonized via a raw clone()+setsid() fork, which is silently reaped when launched inside a systemd .service (run0, systemd-run): the service deactivates the moment the forked parent exits and KillMode=control-group SIGKILLs the leftover child. Found dogfooding a remote reprovision over 'ssh … run0 xmorph pivot --headless'. Remove --headless and the internal/daemon package entirely. On systemd hosts xmorph now relocates the *running* process into a transient scope via Manager.StartTransientUnit over D-Bus — the move 'systemd-run --scope' makes, but with no fork and no re-exec — so the pivot outlives the launching session/service. Skipped under --systemd-mode (already a unit). Detect SSH and, when we could not detach, warn + pause a grace window so a disconnect doesn't silently kill the pivot. Logging: keep --log-dir but as a general additional file sink (not headless-only); add a syslog sink when journald is absent (journald already captures stderr); more breadcrumbs across the pivot pipeline. log.Handler grows AddSink, shared across clones. - add initsys.RelocateToTransientScope + initsys.RunningOverSSH - godbus/dbus/v5 promoted to a direct dependency
…less Detaching from an SSH session is now automatic on systemd (transient-scope relocation); non-systemd hosts get a warning + grace window. Drop --headless from every example, add a 'Surviving your SSH session' section, and point unit-driven runs at --systemd-mode.
ananthb
force-pushed
the
fix/headless-cgroup-escape
branch
from
July 15, 2026 20:43
f771539 to
40685a3
Compare
…ftables) xmorph shelled out to systemctl/openrc/telinit, modprobe, and iptables/nft. Replace all of it with native calls: - initsys/coordinator: systemd transition + job wait go over D-Bus (Manager.StartUnit isolate / ListJobs) instead of exec'ing systemctl. Non-systemd inits (OpenRC/SysVinit/…) have no native control channel, so drop the advisory openrc/telinit shell-out — process.Terminate already stops what's running natively. - initsys/scope: mark the transient scope IgnoreOnIsolate so isolating basic.target (above) doesn't stop our own scope and kill the pivot. - postpivot/watchdog: load softdog via finit_module(2) (locating the .ko under /lib/modules, decompressing .ko.zst/.xz/.gz via the kernel) instead of exec'ing modprobe. - postpivot/firewall: flush the ruleset natively via google/nftables (already in the module graph) rather than iptables/ip6tables/nft. Split into firewall_linux.go + a no-op firewall_other.go stub. The only remaining exec.Command sites run the user's own program — the entrypoint (supervise, contain) and the SSH session shell (sshd) — which is xmorph's purpose, not a utility shell-out. go.sum unchanged (nftables was already an indirect dep), so the nix vendorHash is unaffected.
ec7d853 captured only the firewall.go deletion (git add aborted on a stale pathspec, leaving the real changes uncommitted). Land them: - initsys/coordinator: systemd transition + job wait over D-Bus (StartUnit isolate / ListJobs); drop openrc/telinit exec (process.Terminate covers non-systemd inits natively). - initsys/scope: transient scope marked IgnoreOnIsolate so isolating basic.target doesn't stop our own scope mid-pivot. - postpivot/watchdog: load softdog via finit_module(2), not modprobe. - postpivot/firewall_{linux,other}: native google/nftables flush + a non-linux no-op stub, replacing iptables/ip6tables/nft. - pivot/contain: correct a stale comment about a /bin/sh -c wrapper. go.sum unchanged; nix vendorHash unaffected.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Started as the
--headlessfix; grew to remove--headlessentirely and, per review, eliminate every system-utility shell-out.--headless→ systemd transient-scope detach--headlessdaemonized via a rawclone()+setsid()fork, silently reaped when launched inside a systemd.service(run0,systemd-run): the service deactivates when the forked parent exits andKillMode=control-groupSIGKILLs the child. Found dogfooding a remote reprovision.--headlessand theinternal/daemonpackage (no fork, no re-exec).StartTransientUnitover D-Bus (systemd-run --scopesemantics), markedIgnoreOnIsolateso the init coordinator'sisolate basic.targetdoesn't stop it. Skipped under--systemd-mode.Logging
--log-dirkept as a general additional file sink (not headless-only), flushed into the new rootfs before pivot.log.HandlergainsAddSink, shared across clones.No more shell-outs
Replaced every system-utility
execwith native calls:systemctl isolate/list-jobs→ D-BusManager.StartUnit/ListJobs; droppedopenrc/telinit(non-systemd relies on the nativeprocess.Terminate).modprobe softdog→finit_module(2)(locates the.ko, decompresses.ko.zst/.xz/.gzvia the kernel).iptables/nft→ nativegoogle/nftablesruleset flush (already in the module graph;firewall_linux.go+ non-linux stub).Remaining
exec.Commandsites run the user's own program — the entrypoint (supervise,contain) and the SSH session shell (sshd) — which is xmorph's purpose, not a utility shell-out.CI fix
fix(postpivot): serialize supervisor log tee— the flakyTestSuperviseTeesToLogWriterwas a real data race (twoexeccopy goroutines writing onebytes.Buffer); serialized through a locked writer, 30×-raceclean.Test
CGO_ENABLED=0 GOOS=linux go build ./...,go vet, gofmt, and native-racetests all clean.go.sumunchanged, so the nixvendorHashis unaffected. The D-Bus/finit_module/nftables paths are compile-verified here; runtime validation is on a real systemd host (the reprovision this was found on).Companion: #11 (flatcar remote doc, updated to
--systemd-mode).