1
0
Fork 0
mirror of https://github.com/sourcegraph/jsonrpc2.git synced 2026-08-16 10:08:12 +02:00

fix/conn: prevent re-entrant logger deadlock (#99)

Protocol errors invoke the configured logger while the connection is shutting down. Calling user code under the connection mutex meant a logger that sends over the same connection could never observe closure and instead deadlocked. Complete teardown before logging so re-entrant calls return ErrClosed.

Amp-Thread-ID: https://ampcode.com/threads/T-019fb774-aef7-75df-8eb3-8888d6835754

Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
Keegan Carruthers-Smith 2026-07-31 11:28:47 +02:00 committed by GitHub
parent 28070556df
commit c3088e5c5d
Signed by: GitHub
GPG key ID: B5690EEEBB952194
2 changed files with 43 additions and 6 deletions

17
conn.go
View file

@ -180,24 +180,29 @@ func (c *Conn) SendResponse(ctx context.Context, resp *Response) error {
func (c *Conn) close(cause error) error {
c.mu.Lock()
defer c.mu.Unlock()
if c.closed {
c.mu.Unlock()
return ErrClosed
}
c.closed = true
for _, call := range c.pending {
close(call.done)
}
close(c.disconnect)
c.mu.Unlock()
c.cancelCtx()
err := c.stream.Close()
// The logger may call back into c, so invoke it only after shutdown is
// complete and c.mu is unlocked.
if cause != nil && cause != io.EOF && cause != io.ErrUnexpectedEOF {
c.logger.Printf("jsonrpc2: protocol error: %v\n", cause)
}
close(c.disconnect)
c.cancelCtx()
c.closed = true
return c.stream.Close()
return err
}
func (c *Conn) readMessages(ctx context.Context) {

View file

@ -167,6 +167,38 @@ func TestConn_DisconnectNotify(t *testing.T) {
connA.Write([]byte("invalid json"))
assertDisconnect(t, c, connB)
})
t.Run("protocol error logger uses connection", func(t *testing.T) {
connA, connB := net.Pipe()
logResult := make(chan error, 1)
var c *jsonrpc2.Conn
c = jsonrpc2.NewConn(
context.Background(),
jsonrpc2.NewPlainObjectStream(connB),
noopHandler{},
jsonrpc2.SetLogger(connNotifyLogger{conn: &c, result: logResult}),
)
connA.Write([]byte("invalid json"))
assertDisconnect(t, c, connB)
select {
case err := <-logResult:
if err != jsonrpc2.ErrClosed {
t.Errorf("logger Notify: got %v, want %v", err, jsonrpc2.ErrClosed)
}
case <-time.After(200 * time.Millisecond):
t.Error("logger blocked using connection")
}
})
}
type connNotifyLogger struct {
conn **jsonrpc2.Conn
result chan<- error
}
func (l connNotifyLogger) Printf(string, ...interface{}) {
l.result <- (*l.conn).Notify(context.Background(), "log", nil)
}
func TestConn_Close(t *testing.T) {