zdd plan 2 task 7 fix: generation units keep the trampoline's signal exits successful
The generation template runs the same instance-exec trampoline as carlos-instance@.service, which exits 128+n for a death by signal n. SuccessExitStatus=129 130 141 143 ensures a SIGTERM stop leaves the generation unit inactive, not failed. Added the directive with its #126 comment (copied from carlos-instance@.service:41-45). Updated the test to verify it is present. Also fixed stale comment in units_hardening_test.go: now four templates plus the probe, not three templates plus the probe.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3 files changed,
+10
−3
internal/host/units/carlos-instance-gen@.service+6 −0internal/host/units_hardening_test.go+2 −2internal/host/units_isolation_test.go+2 −1
diff --git a/internal/host/units/carlos-instance-gen@.service b/internal/host/units/carlos-instance-gen@.service| index 4c98493a3..9a40e4f92 100644 |
| --- a/internal/host/units/carlos-instance-gen@.service |
| +++ b/internal/host/units/carlos-instance-gen@.service |
| @@ -26,6 +26,12 @@ Group=carlos |
| Restart=no |
| KillMode=mixed |
| TimeoutStopSec=20 |
| +# The main process is the trampoline, which stays as the namespace's init |
| +# and runs the app as its child (#126): it forwards the SIGTERM and exits |
| +# 128+n when the app dies by signal n. These are SIGHUP, SIGINT, SIGPIPE |
| +# and SIGTERM, the deaths systemd counts as clean for a main process, so a |
| +# stop still leaves the unit inactive rather than failed. |
| +SuccessExitStatus=129 130 141 143 |
| StandardError=inherit |
| # --- the namespace (titogo@.service, 2026-07-31 design, on systemd 257) --- |
diff --git a/internal/host/units_hardening_test.go b/internal/host/units_hardening_test.go| index fecb8ee6b..e07da5c74 100644 |
| --- a/internal/host/units_hardening_test.go |
| +++ b/internal/host/units_hardening_test.go |
| @@ -39,8 +39,8 @@ const instanceDeny = "IPAddressDeny=localhost multicast " + |
| // Every directive the hardening adds to the shared namespace block (#214 |
| // items 2 and 3, #178), pinned exactly: the block is byte-identical across |
| -// the three templates and the probe (TestInstanceTemplatesShareOneNamespace), |
| -// so pinning it on carlos-instance@ pins it on all four. |
| +// the four templates and the probe (TestInstanceTemplatesShareOneNamespace), |
| +// so pinning it on carlos-instance@ pins it on all five. |
| func TestNamespaceBlockCarriesTheHardening(t *testing.T) { |
| for _, name := range []string{"carlos-instance@.service", "carlos-instance-gen@.service", "carlos-instance-seed@.service", "carlos-instance-sidecar@.service", "carlos-instance-probe.service"} { |
| u := readUnit(t, name) |
diff --git a/internal/host/units_isolation_test.go b/internal/host/units_isolation_test.go| index 0ddb2c801..c56c46235 100644 |
| --- a/internal/host/units_isolation_test.go |
| +++ b/internal/host/units_isolation_test.go |
| @@ -277,7 +277,8 @@ func TestInstanceTemplatesShareOneNamespace(t *testing.T) { |
| "SuccessExitStatus=129 130 141 143"}, |
| "carlos-instance-gen@.service": {"ExecStart=/opt/carlos/carlos instance-exec run %I", "ExecStopPost=/opt/carlos/carlos instance-exit run %I", |
| "Restart=no", "KillMode=mixed", "TimeoutStopSec=20", "BindsTo=carlos-edge.service", |
| - "Requires=data-instances.mount run-carlos-instances.mount run-carlos-logs.mount", "OOMScoreAdjust=500", "OOMPolicy=kill", "TasksMax=256", "Type=simple"}, |
| + "Requires=data-instances.mount run-carlos-instances.mount run-carlos-logs.mount", "OOMScoreAdjust=500", "OOMPolicy=kill", "TasksMax=256", "Type=simple", |
| + "SuccessExitStatus=129 130 141 143"}, |
| "carlos-instance-seed@.service": {"Type=oneshot", "RemainAfterExit=yes", "TimeoutStartSec=120", "ExecStart=/opt/carlos/carlos instance-exec seed %I", |
| "ExecStopPost=/opt/carlos/carlos instance-exit seed %I", "StandardOutput=file:/run/carlos/logs/%I.seed.sock", |
| "KillMode=mixed", "TimeoutStopSec=20", "WorkingDirectory=/data/instances/%I"}, |