fix: tear down the session's whole process tree on kill#98
Merged
Conversation
boo kill signaled only the session shell (a single SIGHUP to child_pid), so grandchildren that ignore SIGHUP, were placed in their own process group by the shell's job control, or were backgrounded survived the session and kept holding ports and files. A replacement session then failed with errors like "Address already in use". Snapshot the shell's descendant tree while it is still intact, hang up its process group, SIGTERM every descendant by pid (reaching jobs the shell put in their own process groups), then SIGKILL any survivors after a bounded grace. A per-process start time recorded at snapshot guards each bare-pid signal against pid reuse. Enumeration is /proc on Linux and libproc on macOS; a descendant that already double-forked into its own session and reparented to init before the kill is still out of reach, the same caveat tmux and screen carry. Fixes #97.
proc_listallpids/proc_listchildpids return the number of pids, not a byte count, so dividing the result by sizeof(pid) rounded a small tree down to zero descendants and the teardown found nothing on macOS. Enumerate the whole process table with proc_listallpids plus proc_pidinfo (which yields ppid and start time together) and rebuild the descendant tree with the same parent-link fixpoint used on Linux. Adds a buildTree unit test covering transitive descendants and a self-loop.
This was referenced Jul 5, 2026
Merged
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.
Summary
boo killsent a singleSIGHUPto the session shell's pid. Any descendant that ignoredSIGHUP, was placed in its own process group by the shell's job control, or was backgrounded survived the session and kept holding ports and files, so a replacement session could fail with errors likeAddress already in use.This snapshots the shell's descendant tree while it is still intact, hangs up the shell's process group,
SIGTERMs every descendant by pid, then escalates toSIGKILLfor any survivors after a bounded grace period. A per-process start time recorded at snapshot guards every bare-pid signal against pid reuse. Enumeration uses/procon Linux andlibprocon macOS, matching the existingsrc/cwd.zigapproach.Fixes #97.
Why not the one-line fix suggested in the issue?
The issue suggests
kill(-child_pid, SIGHUP)(signal the process group). That is a reasonable improvement, but it does not fix the issue's own reproduction:SIGHUP, so even a delivered hangup is dropped. (The kernel already sendsSIGHUPto the terminal's foreground group when the session leader dies, so a group-HUP adds little for a HUP-ignoring process.)Reliably tearing such a process down requires enumerating the descendant tree and escalating to
SIGKILL. The new integration test encodes this: it fails onmain(orphaned descendant survived kill) and passes here.Behavior notes
boo killstill returns promptly: the ack and the client exit notices are flushed before the teardown, so the grace period never blocks the caller (each session has its own daemon process).SIGTERM.Known limitation (documented, same as tmux/screen)
A descendant that double-forks into its own session and reparents to init before the kill cannot be discovered from the shell and is not reached. This is called out in
src/reap.zig's doc comment.Implementation plan and decision log
Root cause (
src/daemon.zig,quitcommand):posix.kill(w.child_pid, posix.SIG.HUP)targets a single pid (the shell). Grandchildren that ignore SIGHUP or left the shell's process group survive.Options considered
kill(-child_pid, SIGHUP)(issue suggestion). Rejected as insufficient: does not tear down the issue's reproduction (own process group + SIGHUP ignored). Verified by a failing test.Design
src/reap.zigterminateTree(alloc, leader):/proc, resolve to a fixpoint over parent links. macOS: BFS viaproc_listchildpids.SIGHUPthe leader's process group (-leader);child_pid == pgidbecause the shell ransetsid().SIGTERMevery descendant by pid (reaches jobs in their own process groups).SIGKILLremaining survivors./proc/<pid>/statfield 22; macOSproc_pidinfo/PROC_PIDTBSDINFO). Re-verify before every bare-pid signal so a delayedSIGKILLnever hits a pid that was recycled. Zombies are treated as dead.max_procs) so a fork bomb cannot make teardown scan without limit.src/cwd.zig), so drift fails the build loudly. Verified via cross-compile to both macOS arches.Ordering change in the
quithandler: notify clients anddrainOutbound()beforeterminateTree, so the kill stays responsive while teardown escalates.Test plan
zig build test-> 166/166 (addsreap.zigparseStatunit tests).zig build test-integration-> 82/82 (addskill tears down the whole process tree, not just the session shell).zig build test-all -Doptimize=ReleaseSafe(CI release-safe gate) passes.zig build -Dtarget=aarch64-macosand-Dtarget=x86_64-macoscompile (darwin path + comptime layout asserts).mainand passes on this branch.Disclosure: this pull request was created by Coder Agents on behalf of @kylecarbs.