Preserve runtime dir across restart so reconcile re-finds VMs

v0.1.4 fixed the binary-level reconcile path for jailer'd VMs but
left a hole at the systemd layer: bangerd.service and bangerd-root.service
both defaulted to RuntimeDirectoryPreserve=no, so /run/banger was
wiped on every daemon stop. The api-sock symlinks the helper creates
for live VMs (`/run/banger/fc-<id>.sock` → `<chroot>/firecracker.socket`)
went with it, and findByJailerPidfile — which derives the chroot
from the symlink target — couldn't resolve them. Reconcile then fell
through to "stale_vm" and tore down the surviving FC's dm-snapshot.

Add RuntimeDirectoryPreserve=yes to both unit templates so the
symlinks survive the restart window. Live-verified end-to-end on
the dev host: started a VM under v0.1.5, restarted helper +
daemon, confirmed the FC PID was unchanged and `banger vm ssh`
returned the same boot_id pre and post.

Daemon-lifecycle tests updated to assert the new directive is
present in both rendered units so future regressions show up at
test time.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Thales Maciel 2026-04-29 17:17:25 -03:00
parent e1acb0384b
commit 1be90a7af5
No known key found for this signature in database
GPG key ID: 33112E6833C34679
3 changed files with 38 additions and 1 deletions

View file

@ -329,6 +329,13 @@ func renderSystemdUnit(meta installmeta.Metadata) string {
"CacheDirectoryMode=0700",
"RuntimeDirectory=banger",
"RuntimeDirectoryMode=0700",
// Keep /run/banger across stop/restart so the api-sock symlinks
// the helper creates for live VMs aren't wiped between the daemon
// stopping and the new daemon's reconcile re-attaching to them.
// Without this, `banger update` restarts the daemon, /run/banger
// is wiped, the api-sock symlinks vanish, and rediscoverHandles
// can't resolve the chroot path it needs to read jailer's pidfile.
"RuntimeDirectoryPreserve=yes",
}
if coverDir := strings.TrimSpace(os.Getenv(systemCoverDirEnv)); coverDir != "" {
lines = append(lines, "Environment=GOCOVERDIR="+systemdQuote(coverDir))
@ -390,6 +397,12 @@ func renderRootHelperSystemdUnit() string {
"ReadWritePaths=/var/lib/banger",
"RuntimeDirectory=banger-root",
"RuntimeDirectoryMode=0711",
// Same rationale as bangerd.service: the helper-managed
// /run/banger-root holds the helper's RPC socket and any
// per-VM scratch state; preserving it across restart keeps
// the daemon's reconnect path and reconcile re-attachment
// from racing against systemd's runtime-dir cleanup.
"RuntimeDirectoryPreserve=yes",
}
if coverDir := strings.TrimSpace(os.Getenv(rootCoverDirEnv)); coverDir != "" {
lines = append(lines, "Environment=GOCOVERDIR="+systemdQuote(coverDir))

View file

@ -164,6 +164,7 @@ func TestRenderSystemdUnitIncludesHardeningDirectives(t *testing.T) {
"CacheDirectoryMode=0700",
"RuntimeDirectory=banger",
"RuntimeDirectoryMode=0700",
"RuntimeDirectoryPreserve=yes",
`ReadOnlyPaths="/home/alice/dev home"`,
} {
if !strings.Contains(unit, want) {
@ -189,6 +190,7 @@ func TestRenderRootHelperSystemdUnitIncludesRequiredCapabilities(t *testing.T) {
"ReadWritePaths=/var/lib/banger",
"RuntimeDirectory=banger-root",
"RuntimeDirectoryMode=0711",
"RuntimeDirectoryPreserve=yes",
} {
if !strings.Contains(unit, want) {
t.Fatalf("unit = %q, want %q", unit, want)