Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions pkg/daemon/ipc.go
Original file line number Diff line number Diff line change
Expand Up @@ -445,13 +445,16 @@ 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 {
return &IPCServer{
socketPath: socketPath,
daemon: d,
clients: make(map[*ipcConn]bool),
done: make(chan struct{}),
}
}

Expand Down Expand Up @@ -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()
}
Expand Down Expand Up @@ -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)
Expand Down
Loading