Skip to content

boo kill leaves orphaned grandchildren alive (signals shell only, not process group) #97

Description

@bermudi

Summary

boo kill (and boo kill --all) only signals the PTY's direct child — the shell — with SIGHUP. Grandchild processes that ignore/block SIGHUP, were backgrounded/daemonized, or escaped the shell's job table survive the kill and keep running with the session gone. They hold onto ports, files, etc., and a subsequent session started to replace them fails (e.g. Address already in use).

Mechanism

boo kill sends the quit control command, handled in src/daemon.zig:

} else if (std.mem.eql(u8, cmd, "quit")) {
    self.retireListener();
    conn.send(.ok, "");
    if (self.win) |w| {
        posix.kill(w.child_pid, posix.SIG.HUP) catch {};   // <-- positive pid: shell only
    }
    ...

child_pid is the shell. A positive pid to kill(2) reaches exactly that one process. SIGHUP normally cascades to the shell's jobs via huponexit, so kill appears to work — until a job ignores SIGHUP or has re-parented/re-pgrp'd out, which is exactly what long-running servers and daemons tend to do. The session daemon exits, the listener socket is removed, but the orphaned grandchild keeps its resources.

The PTY child does call setsid() (src/pty.zig), so it is a session/process-group leader and child_pid == pgid.

Reproduction

  1. Start a session and run a SIGHUP-ignoring server bound to a fixed port:
    boo new srv
    # inside the session:
    python3 -c 'import signal,socket; signal.signal(signal.SIGHUP, signal.SIG_IGN); \
      s=socket.socket(); s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1); \
      s.bind(("127.0.0.1",8000)); s.listen(1); print("up"); signal.pause()'
    
  2. From another terminal: boo kill srv
  3. Observe the python process is still alive and still holds 127.0.0.1:8000:
    ss -ltnp | grep :8000
    ps -p $(pgrep -f 'signal.pause') -o pid,ppid,cmd
    
    Its PPID is now 1 (reparented to init), confirming it escaped the session.

Expected: the whole process tree started by the session is torn down.
Actual: only the shell dies; the orphaned server survives.

Suggested fix

Signal the process group rather than the single pid:

posix.kill(-w.child_pid, posix.SIG.HUP) catch {};   // -pid == whole pgrp

Since child_pid == pgid (child runs setsid()), -child_pid targets every process in the session's group. This closes the common case (grandchildren still in the shell's group).

Honest limitation

A pgrp signal still cannot reach grandchildren that:

  • were placed in their own process group by the shell's job control,
  • called setsid() themselves / double-forked into a new session,
  • were started via nohup/setsid/disown.

For a true full-tree kill, boo would need to enumerate descendants (/proc/*/stat PPids on Linux, proc_listchildpids on macOS) and signal each pid, escalating TERM → KILL. That is more code and two-platform, but it's the only thing that catches a daemonized grandchild. Worth deciding whether boo promises full-tree teardown or documents the same caveat tmux/screen live with.

Environment

  • boo v0.6.3 (main @ 4af4bdf)
  • Linux (repro), mechanism is platform-independent

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions