You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
fix(supervisor): stop apps under the shipped unit; never hang on a failed kill - #20
Since v0.1.3 every app runs as its own UID. Under init/creekd.service creekd is the non-root creekd user holding CAP_NET_BIND_SERVICE CAP_SETUID CAP_SETGID — but not CAP_KILL, which the kernel requires to signal a process of another UID. So:
DELETE, restart and blue-green deploy send SIGTERM/SIGKILL that fail with EPERM, logged at debug as "process likely gone";
the old process keeps running;
escalateAndWait then blocks on <-doneforever, and so does the admin request. Observed: a DELETE hung for 23 minutes; SIGQUIT showed the goroutine in escalateAndWait → stopApp → StopWithTimeout → adminapi.StopApp.
Reach today: the shipped unit cannot spawn apps as-is (#18), so this bites the #18 workaround (clearing the cgroup variables). Root mode is unaffected.
Change
Unit:CAP_KILL added to CapabilityBoundingSet and AmbientCapabilities; hardening-check expects it. Apps never inherit it (setpriv drops inheritable + ambient).
Startup: creekd logs an error when per-app UIDs are on but it cannot signal other UIDs (Supervisor.CanSignalApps). Per-app UIDs stay on — leaking secrets is worse than a failing stop, and the error says what to grant.
Stop never hangs: a SIGKILL that cannot be delivered fails Stop after KillWaitTimeout (default 5s) with the PID; the app is logged as "no longer supervised".
Visibility: signal failures other than os.ErrProcessDone log as warnings.
Granting CAP_KILL lets a compromised creekd signal any process on the host. The capability-free alternative is for creekd's spawn helper to stay in the app as its init and relay signals (with #17); that is a larger change being prototyped for #18.
Verification (2026-09-30, Ubuntu 26.04, shipped unit, cgroup vars cleared)
with CAP_KILL (CapEff 0x4e0)
without (0x4c0)
startup
—
error logged
restart a handler-less app
200, new PID
—
blue-green deploy (SIGTERM-handling June app)
200 in 215 ms, v1 gone
—
DELETE
204 in 8–9 ms, nothing left running
returns after 35 s (reply lost to #17's write timeout), log: cannot stop app … pid 3282 … operation not permitted — no longer hangs
Unit tests: TestStopReturnsWhenSignalsCannotBeDelivered (EPERM injected → Stop errors in ~300 ms), TestSignalFailed, TestParseCapBitsKill, hardening baseline. go test ./... passes.
…iled kill
Since 0.1.3 each app runs as its own UID, and under init/creekd.service
creekd is the non-root creekd user without CAP_KILL: SIGTERM and SIGKILL
to an app failed with EPERM (logged at debug as 'process likely gone').
Stop, restart and deploy left the old process running, and
escalateAndWait then blocked on <-done forever, hanging the admin
request (seen: 23 minutes, goroutine dump in escalateAndWait).
- init/creekd.service grants CAP_KILL (bounding + ambient);
hardening-check expects it.
- creekd logs an error at startup when per-app UIDs are on but it cannot
signal other UIDs (CanSignalApps).
- A SIGKILL that cannot be delivered fails Stop after KillWaitTimeout
(5s) with the PID, and the app is reported as no longer supervised.
- Signal failures other than 'process already finished' log as warnings.
Verified 2026-09-30 on Ubuntu 26.04 under the shipped unit (cgroup vars
cleared, see #18): with CAP_KILL, restart 200, blue-green deploy 200
with v1 gone, DELETE 204 in 8-9 ms for a SIGTERM-handling and a
handler-less app; without CAP_KILL, the startup error is logged and
DELETE returns after 35 s with 'cannot stop app ... operation not
permitted' in the log instead of hanging.
The reason will be displayed to describe this comment to others. Learn more.
Fixed in 554f218. stopApp now skips its teardown when the process did not stop, and StopWithTimeout puts the app back in the registry: it stays supervised, routed and persisted, consistent across GET/DELETE, and a retry works once signals do. The test asserts it is still registered after the failed stop and that a retry succeeds. On a VM without CAP_KILL: after the failed DELETE, GET 200, still served, still in state.json.
The reason will be displayed to describe this comment to others. Learn more.
Right — fixed in 554f218. The startup error now says stop and restart fail while a deploy starts the new version and leaves the old one running; the v1 stop failure in Deploy is now an error log with v1_pid.
- **Fails closed**: an app with a recorded UID does not start when creekd cannot switch UIDs (no root, no `CAP_SETUID`/`CAP_SETGID`); it is never silently run as creekd's own user.
- **User namespaces**: an app spawned with `sandbox.user_namespace` gets no allocated UID — its identity is its UID mapping. A `run_as` for such an app must be a UID mapped inside its namespace.
- **Requires**: creekd running as root, or holding `CAP_SETUID` + `CAP_SETGID` (the shipped `init/creekd.service` grants both). Without them creekd warns at startup and every app shares its UID.
- **Requires**: creekd running as root, or holding `CAP_SETUID` + `CAP_SETGID` to switch UIDs and `CAP_KILL` to signal the apps afterwards (the shipped `init/creekd.service` grants all three). Without `CAP_SETUID`/`CAP_SETGID` creekd warns at startup and every app shares its UID; without `CAP_KILL` it logs an error at startup, and stop, restart and deploy fail with `EPERM` while the old process keeps running.
The reason will be displayed to describe this comment to others. Learn more.
Fixed in 554f218: the CanSignalApps comment now describes the split deployment (v2 promoted, v1 left running unsupervised) instead of a failing deploy.
The child advanced its counter file before printing run-N, and the test
stops the app as soon as the counter reads 2: a run stopped between the
two steps had counted without printing, and the log lacked run-2. It
failed once on CI for #20 (race detector on); locally 1 in 300 runs on
this branch and 0 in 300 on main, a rate too low to tell apart. With the
line printed first, reading 2 implies run-2 was written: 0 in 300.
The test comment promises both the KillWaitTimeout delay and an error naming the PID, but this branch only checks for any non-nil error within five seconds. It would still pass if Stop returned immediately without using the new timeout or omitted the PID. Assert a lower bound near the configured 300 ms and that err.Error() contains pid.
…loy as it is
Review on #20:
- StopWithTimeout removed the app from the registry before stopping it;
when the stop failed, the handler returned early and the process kept
running, still routed and in state.json, behind 404s on GET and
DELETE. stopApp now skips its teardown (log rotator, cgroup, network)
when the process did not stop, and StopWithTimeout puts the app back:
supervised, visible, and stoppable once the cause is fixed.
- Deploy promotes v2 even when v1 cannot be stopped, so the hazard is two
versions running, not a failing deploy: the v1 failure now logs at
error with v1_pid, and the startup message, CanSignalApps, CONFIG.md
and the changelog say so.
- TestStopReturnsWhenSignalsCannotBeDelivered asserts the wait (graceful
window + KillWaitTimeout), the PID in the error, that the app is still
registered afterwards, and that a retry succeeds once signals work.
Verified 2026-09-30 on Ubuntu 26.04 without CAP_KILL: after the failed
DELETE, GET 200, still served, still in state.json; a deploy leaves v1
(pid logged) beside v2 with the error logged.
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
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.
Problem
Since v0.1.3 every app runs as its own UID. Under
init/creekd.servicecreekd is the non-rootcreekduser holdingCAP_NET_BIND_SERVICE CAP_SETUID CAP_SETGID— but notCAP_KILL, which the kernel requires to signal a process of another UID. So:DELETE, restart and blue-green deploy send SIGTERM/SIGKILL that fail withEPERM, logged at debug as "process likely gone";escalateAndWaitthen blocks on<-doneforever, and so does the admin request. Observed: aDELETEhung for 23 minutes;SIGQUITshowed the goroutine inescalateAndWait → stopApp → StopWithTimeout → adminapi.StopApp.Reach today: the shipped unit cannot spawn apps as-is (#18), so this bites the #18 workaround (clearing the cgroup variables). Root mode is unaffected.
Change
CAP_KILLadded toCapabilityBoundingSetandAmbientCapabilities;hardening-checkexpects it. Apps never inherit it (setpriv drops inheritable + ambient).Supervisor.CanSignalApps). Per-app UIDs stay on — leaking secrets is worse than a failing stop, and the error says what to grant.StopafterKillWaitTimeout(default 5s) with the PID; the app is logged as "no longer supervised".os.ErrProcessDonelog as warnings.Granting
CAP_KILLlets a compromised creekd signal any process on the host. The capability-free alternative is for creekd's spawn helper to stay in the app as its init and relay signals (with #17); that is a larger change being prototyped for #18.Verification (2026-09-30, Ubuntu 26.04, shipped unit, cgroup vars cleared)
CAP_KILL(CapEff 0x4e0)0x4c0)DELETEcannot stop app … pid 3282 … operation not permitted— no longer hangsUnit tests:
TestStopReturnsWhenSignalsCannotBeDelivered(EPERM injected →Stoperrors in ~300 ms),TestSignalFailed,TestParseCapBitsKill, hardening baseline.go test ./...passes.