From ca37fb50d99daddc94d6ee52969ca85a72caaf8e Mon Sep 17 00:00:00 2001 From: matthew-pilot Date: Fri, 29 May 2026 22:47:52 +0000 Subject: [PATCH] fix(daemon): gate acceptLoop on done channel to close untracked-handleClient race (PILOT-253) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Close() now signals done before closing the listener so acceptLoop — which may be mid-Accept — refuses any conn that raced past listener.Close(). Without this gate, a concurrently-accepted connection spawns an untracked handleClient goroutine that holds resources past Close(). Adds closeOnce + done chan to IPCServer; acceptLoop checks s.done after acquiring s.mu but before spawning handleClient. --- pkg/daemon/ipc.go | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/pkg/daemon/ipc.go b/pkg/daemon/ipc.go index c0f15b79..effcd57f 100644 --- a/pkg/daemon/ipc.go +++ b/pkg/daemon/ipc.go @@ -445,6 +445,8 @@ type IPCServer struct { daemon *Daemon mu sync.Mutex clients map[*ipcConn]bool + closeOnce sync.Once + done chan struct{} } func NewIPCServer(socketPath string, d *Daemon) *IPCServer { @@ -452,6 +454,7 @@ func NewIPCServer(socketPath string, d *Daemon) *IPCServer { socketPath: socketPath, daemon: d, clients: make(map[*ipcConn]bool), + done: make(chan struct{}), } } @@ -483,6 +486,13 @@ func (s *IPCServer) Start() error { } func (s *IPCServer) Close() error { + // PILOT-253: Close the done channel BEFORE closing the listener + // so that acceptLoop — which may be mid-Accept — sees the signal + // and refuses any newly-accepted connection that races past the + // listener close. Without this gate, a conn accepted concurrently + // with listener.Close() would spawn an untracked handleClient + // goroutine that outlives Close(). + s.closeOnce.Do(func() { close(s.done) }) if s.listener != nil { s.listener.Close() } @@ -518,6 +528,19 @@ func (s *IPCServer) acceptLoop() { continue } s.mu.Lock() + // PILOT-253: Reject connections that raced past listener.Close(). + // Accept may succeed concurrently with Close() closing the + // listener — the kernel-level accept dequeues a pending + // connection before the close takes effect. Without this check, + // the conn spawns an untracked handleClient goroutine that + // holds resources past Close(). + select { + case <-s.done: + s.mu.Unlock() + conn.Close() + return + default: + } full := len(s.clients) >= MaxIPCClients if !full { ic := newIPCConn(conn)