Skip to content

Commit 1f7cacf

Browse files
authored
fix(engine): fix additional instances of writing/reading on a closed conn (#20982)
1 parent 20aaeb8 commit 1f7cacf

2 files changed

Lines changed: 155 additions & 7 deletions

File tree

‎pkg/engine/internal/scheduler/wire/wire_http2.go‎

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package wire
33
import (
44
"context"
55
"crypto/tls"
6+
"errors"
67
"fmt"
78
"io"
89
"net"
@@ -106,7 +107,7 @@ func (l *HTTP2Listener) ServeHTTP(w http.ResponseWriter, r *http.Request) {
106107
return
107108
}
108109

109-
conn := newHTTP2Conn(l.Addr(), remoteAddr, r.Body, w, rc, l.codec)
110+
conn := newHTTP2Conn(r.Context(), l.Addr(), remoteAddr, r.Body, w, rc, l.codec)
110111
defer conn.Close()
111112

112113
// Wait until connection is accepted by HTTP2Listener.Accept(ctx)
@@ -164,6 +165,8 @@ func (l *HTTP2Listener) Addr() net.Addr {
164165

165166
// http2Conn implements Conn for HTTP/2-based connections.
166167
type http2Conn struct {
168+
ctx context.Context
169+
167170
localAddr net.Addr
168171
remoteAddr net.Addr
169172

@@ -187,8 +190,9 @@ type incomingFrame struct {
187190

188191
var _ Conn = (*http2Conn)(nil)
189192

190-
// newHTTP2Conn creates a new HTTP/2 connection.
193+
// newHTTP2Conn creates a new HTTP/2 connection tied to the lifetime of ctx.
191194
func newHTTP2Conn(
195+
ctx context.Context,
192196
localAddr net.Addr,
193197
remoteAddr net.Addr,
194198
reader io.ReadCloser,
@@ -197,6 +201,7 @@ func newHTTP2Conn(
197201
codec *protobufCodec,
198202
) *http2Conn {
199203
c := &http2Conn{
204+
ctx: ctx,
200205
localAddr: localAddr,
201206
remoteAddr: remoteAddr,
202207
codec: codec,
@@ -225,25 +230,32 @@ func (c *http2Conn) readLoop(ctx context.Context) {
225230

226231
// Send sends a frame over the connection.
227232
func (c *http2Conn) Send(ctx context.Context, frame Frame) error {
233+
c.writeMu.Lock()
234+
defer c.writeMu.Unlock()
235+
228236
select {
237+
case <-c.ctx.Done():
238+
return ErrConnClosed
229239
case <-ctx.Done():
230240
return ctx.Err()
231241
case <-c.closed:
232242
return ErrConnClosed
233243
default:
234244
}
235245

236-
c.writeMu.Lock()
237-
defer c.writeMu.Unlock()
238-
239-
if err := c.codec.EncodeTo(c.writer, frame); err != nil {
246+
err := c.codec.EncodeTo(c.writer, frame)
247+
if err != nil && isStreamClosedError(err) {
248+
return ErrConnClosed
249+
} else if err != nil {
240250
return fmt.Errorf("write frame: %w", err)
241251
}
242252

243253
// Flush after each frame to ensure immediate delivery
244254
if c.responseController != nil {
245255
err := c.responseController.Flush()
246-
if err != nil {
256+
if err != nil && isStreamClosedError(err) {
257+
return ErrConnClosed
258+
} else if err != nil {
247259
return fmt.Errorf("flush response: %w", err)
248260
}
249261
}
@@ -254,6 +266,8 @@ func (c *http2Conn) Send(ctx context.Context, frame Frame) error {
254266
// Recv receives a frame from the connection.
255267
func (c *http2Conn) Recv(ctx context.Context) (Frame, error) {
256268
select {
269+
case <-c.ctx.Done():
270+
return nil, ErrConnClosed
257271
case <-ctx.Done():
258272
return nil, ctx.Err()
259273
case <-c.closed:
@@ -265,6 +279,9 @@ func (c *http2Conn) Recv(ctx context.Context) (Frame, error) {
265279

266280
// Close closes the connection.
267281
func (c *http2Conn) Close() error {
282+
c.writeMu.Lock()
283+
defer c.writeMu.Unlock()
284+
268285
var err error
269286
c.closeOnce.Do(func() {
270287
close(c.closed)
@@ -341,6 +358,7 @@ func (d *HTTP2Dialer) Dial(ctx context.Context, from, to net.Addr) (Conn, error)
341358

342359
// Create connection
343360
conn := newHTTP2Conn(
361+
req.Context(),
344362
from,
345363
to,
346364
resp.Body,
@@ -364,3 +382,19 @@ func (d *HTTP2Dialer) Dial(ctx context.Context, from, to net.Addr) (Conn, error)
364382

365383
return conn, nil
366384
}
385+
386+
// isStreamClosedError returns true if err represents an `http2: stream closed`
387+
// error.
388+
func isStreamClosedError(err error) bool {
389+
// NOTE(rfratto): At the time of writing, the http2 package doesn't have a
390+
// guaranteed way to check if an error is a stream closed error, so we look
391+
// at the error message instead.
392+
for {
393+
next := errors.Unwrap(err)
394+
if next == nil {
395+
break
396+
}
397+
err = next
398+
}
399+
return err.Error() == "http2: stream closed"
400+
}

‎pkg/engine/internal/scheduler/wire/wire_http2_test.go‎

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -541,6 +541,120 @@ func TestHTTP2MessageFrameSerialization(t *testing.T) {
541541
}
542542
}
543543

544+
func TestHTTP_NoPanicOnServerWriteAfterClose(t *testing.T) {
545+
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
546+
defer cancel()
547+
548+
// Start HTTP/2 listener
549+
listener, shutdown := prepareHTTP2Listener(t)
550+
defer shutdown()
551+
552+
// Accept connection
553+
var serverConn wire.Conn
554+
acceptErr := make(chan error, 1)
555+
go func() {
556+
var err error
557+
serverConn, err = listener.Accept(ctx)
558+
acceptErr <- err
559+
}()
560+
561+
// Dial from client
562+
clientConn, err := wire.NewHTTP2Dialer("/").Dial(ctx, listener.Addr(), listener.Addr())
563+
require.NoError(t, err)
564+
defer clientConn.Close()
565+
566+
// Wait for server to accept
567+
require.NoError(t, <-acceptErr)
568+
require.NotNil(t, serverConn)
569+
defer serverConn.Close()
570+
571+
var wg sync.WaitGroup
572+
573+
wg.Go(func() {
574+
for {
575+
_, err := clientConn.Recv(ctx)
576+
if err != nil && errors.Is(err, wire.ErrConnClosed) {
577+
return
578+
} else if ctx.Err() != nil {
579+
return
580+
}
581+
require.NoError(t, err)
582+
}
583+
})
584+
585+
for range 100 {
586+
wg.Go(func() {
587+
for range 10_000 {
588+
err := serverConn.Send(ctx, wire.AckFrame{ID: 1})
589+
if err != nil && errors.Is(err, wire.ErrConnClosed) {
590+
return
591+
}
592+
require.NoError(t, err)
593+
}
594+
})
595+
}
596+
597+
wg.Go(func() { _ = clientConn.Close() })
598+
wg.Wait()
599+
}
600+
601+
func TestHTTP_NoPanicOnClientWriteAfterClose(t *testing.T) {
602+
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
603+
defer cancel()
604+
605+
// Start HTTP/2 listener
606+
listener, shutdown := prepareHTTP2Listener(t)
607+
defer shutdown()
608+
609+
// Accept connection
610+
var serverConn wire.Conn
611+
acceptErr := make(chan error, 1)
612+
go func() {
613+
var err error
614+
serverConn, err = listener.Accept(ctx)
615+
acceptErr <- err
616+
}()
617+
618+
// Dial from client
619+
clientConn, err := wire.NewHTTP2Dialer("/").Dial(ctx, listener.Addr(), listener.Addr())
620+
require.NoError(t, err)
621+
defer clientConn.Close()
622+
623+
// Wait for server to accept
624+
require.NoError(t, <-acceptErr)
625+
require.NotNil(t, serverConn)
626+
defer serverConn.Close()
627+
628+
var wg sync.WaitGroup
629+
630+
wg.Go(func() {
631+
for {
632+
_, err := serverConn.Recv(ctx)
633+
if err != nil && errors.Is(err, wire.ErrConnClosed) {
634+
return
635+
} else if ctx.Err() != nil {
636+
return
637+
}
638+
require.NoError(t, err)
639+
}
640+
})
641+
642+
for range 100 {
643+
wg.Go(func() {
644+
for range 10_000 {
645+
err := clientConn.Send(ctx, wire.AckFrame{ID: 1})
646+
if err != nil && errors.Is(err, wire.ErrConnClosed) {
647+
return
648+
}
649+
require.NoError(t, err)
650+
}
651+
})
652+
}
653+
654+
wg.Go(func() { _ = clientConn.Close() })
655+
wg.Wait()
656+
}
657+
544658
func prepareHTTP2Listener(t *testing.T) (*wire.HTTP2Listener, func()) {
545659
l, err := net.Listen("tcp", "127.0.0.1:0")
546660
require.NoError(t, err)

0 commit comments

Comments
 (0)