From f7ea30a49408adf88d956ddc49354e26f57c2dbc Mon Sep 17 00:00:00 2001 From: mram Date: Tue, 22 Sep 2026 08:31:13 +0200 Subject: [PATCH] Fix control pipe races: treat ERROR_PIPE_CONNECTED as success, flush+disconnect before close so replies are never discarded --- internal/control/control_windows.go | 33 +++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/internal/control/control_windows.go b/internal/control/control_windows.go index 3229452..d0a1ba1 100644 --- a/internal/control/control_windows.go +++ b/internal/control/control_windows.go @@ -7,6 +7,7 @@ import ( "fmt" "log/slog" "os" + "syscall" "unsafe" "golang.org/x/sys/windows" @@ -78,19 +79,43 @@ func Serve(ctx context.Context, h Handler, log *slog.Logger) error { } go func() { // Blocks until a client connects; on process exit the - // handle goes away with everything else. - if err := windows.ConnectNamedPipe(pipe, nil); err != nil { + // handle goes away with everything else. A client that + // raced us and connected between CreateNamedPipe and + // ConnectNamedPipe reports ERROR_PIPE_CONNECTED — that is + // a success, not a failure. + if err := windows.ConnectNamedPipe(pipe, nil); err != nil && err != errnoPipeConnected { windows.CloseHandle(pipe) return } - f := os.NewFile(uintptr(pipe), pipePath) - serveConn(f, h) // closes f, and with it the pipe handle + serveConn(&pipeConn{f: os.NewFile(uintptr(pipe), pipePath), h: pipe}, h) }() } }() return nil } +// errnoPipeConnected is ConnectNamedPipe's "the client connected before we +// called" result, which means the connection is established. +var errnoPipeConnected = syscall.Errno(535) // ERROR_PIPE_CONNECTED + +// pipeConn adapts a pipe handle to io.ReadWriteCloser. Close flushes first +// (FlushFileBuffers blocks until the client has read the reply) and then +// disconnects — closing the bare handle right after writing can discard +// unread reply bytes, which clients see as an empty, failed request. +type pipeConn struct { + f *os.File + h windows.Handle +} + +func (c *pipeConn) Read(p []byte) (int, error) { return c.f.Read(p) } +func (c *pipeConn) Write(p []byte) (int, error) { return c.f.Write(p) } + +func (c *pipeConn) Close() error { + windows.FlushFileBuffers(c.h) //nolint:errcheck // best effort + windows.DisconnectNamedPipe(c.h) //nolint:errcheck // best effort + return c.f.Close() +} + // Ask sends one command to the running service and returns its reply. func Ask(cmd string) (string, error) { name, err := windows.UTF16PtrFromString(pipePath)