From d8839f52dc736b0260489f44fc01dc39a1a8e204 Mon Sep 17 00:00:00 2001 From: Christiaan Date: Tue, 18 Aug 2026 12:38:49 +0200 Subject: [PATCH] fix: synchronize stdin writes to prevent corrupted LSP frames Concurrent calls to Client.Call, Client.Notify, and the server-request response path could interleave their writes to the LSP subprocess's stdin, since WriteMessage's header and body writes were not atomic across goroutines. Against phpactor, the workspace watcher's registration-triggered preopen scans fire concurrent textDocument/didOpen notifications on startup, and the interleaved writes corrupt the Content-Length-prefixed stream. Once desynced, a length-prefixed stream can't be resynchronized, so phpactor's LSP process crashes within about a second of connecting, on every session. Adding a mutex around the three write sites serializes full frames so concurrent notifications/requests can no longer interleave. --- internal/lsp/client.go | 9 +++++---- internal/lsp/transport.go | 15 ++++++++++++--- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/internal/lsp/client.go b/internal/lsp/client.go index fc07059d..b66fb8e8 100644 --- a/internal/lsp/client.go +++ b/internal/lsp/client.go @@ -17,10 +17,11 @@ import ( ) type Client struct { - Cmd *exec.Cmd - stdin io.WriteCloser - stdout *bufio.Reader - stderr io.ReadCloser + Cmd *exec.Cmd + stdin io.WriteCloser + stdinMu sync.Mutex + stdout *bufio.Reader + stderr io.ReadCloser // Request ID counter nextID atomic.Int32 diff --git a/internal/lsp/transport.go b/internal/lsp/transport.go index 3cc9107a..4f8204d3 100644 --- a/internal/lsp/transport.go +++ b/internal/lsp/transport.go @@ -150,7 +150,10 @@ func (c *Client) handleMessages() { } // Send response back to server - if err := WriteMessage(c.stdin, response); err != nil { + c.stdinMu.Lock() + err := WriteMessage(c.stdin, response) + c.stdinMu.Unlock() + if err != nil { lspLogger.Error("Error sending response to server: %v", err) } @@ -217,7 +220,10 @@ func (c *Client) Call(ctx context.Context, method string, params any, result any }() // Send request - if err := WriteMessage(c.stdin, msg); err != nil { + c.stdinMu.Lock() + err = WriteMessage(c.stdin, msg) + c.stdinMu.Unlock() + if err != nil { return fmt.Errorf("failed to send request: %w", err) } @@ -258,7 +264,10 @@ func (c *Client) Notify(ctx context.Context, method string, params any) error { return fmt.Errorf("failed to create notification: %w", err) } - if err := WriteMessage(c.stdin, msg); err != nil { + c.stdinMu.Lock() + err = WriteMessage(c.stdin, msg) + c.stdinMu.Unlock() + if err != nil { return fmt.Errorf("failed to send notification: %w", err) }