From 90af9dd8ae9f59e6d912489f5f843ffb8660a12d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Papp?= Date: Tue, 16 Jun 2026 14:51:17 +0200 Subject: [PATCH] [client] fix WaitStreamConnected stale-channel race The StreamConnected check and the wait-channel creation took the mutex separately, so notifyStreamConnected could set the status and close/clear connectedCh in between: the waiter then created a fresh channel nobody would ever close and blocked forever. Also, the status read was unlocked while notify wrote it under the mutex (a data race). Do the check and the channel fetch in one locked section; drop the now-unused getStreamStatusChan helper. Pre-existing bug, not introduced by this branch. --- shared/signal/client/grpc.go | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/shared/signal/client/grpc.go b/shared/signal/client/grpc.go index db23ed75b..e8bcb2fc7 100644 --- a/shared/signal/client/grpc.go +++ b/shared/signal/client/grpc.go @@ -213,15 +213,6 @@ func (c *GrpcClient) notifyStreamConnected() { } } -func (c *GrpcClient) getStreamStatusChan() <-chan struct{} { - c.mux.Lock() - defer c.mux.Unlock() - if c.connectedCh == nil { - c.connectedCh = make(chan struct{}) - } - return c.connectedCh -} - func (c *GrpcClient) connect(ctx context.Context, key string) (proto.SignalExchange_ConnectStreamClient, error) { c.stream = nil @@ -283,12 +274,21 @@ func (c *GrpcClient) IsHealthy() bool { // WaitStreamConnected waits until the client is connected to the Signal stream func (c *GrpcClient) WaitStreamConnected(ctx context.Context) { - + // Check the status and obtain the wait channel atomically: otherwise + // notifyStreamConnected could flip the status and close/clear the channel + // between the check and the channel creation, leaving us waiting forever on + // a stale channel. + c.mux.Lock() if c.status == StreamConnected { + c.mux.Unlock() return } + if c.connectedCh == nil { + c.connectedCh = make(chan struct{}) + } + ch := c.connectedCh + c.mux.Unlock() - ch := c.getStreamStatusChan() select { case <-ctx.Done(): case <-c.ctx.Done():