quic: avoid deadlock when updating inbound conn-level flow control

handleStreamBytesReadOffLoop sends a message to the conn
indicating that we need to send a MAX_DATA update.

Calling this with a stream's gate locked can lead to a deadlock,
when the conn's loop is processing an inbound frame for the
same stream: The conn can't acquire the stream's ingate, and
the gate won't be unlocked until the conn processes another
event from its queue.

Move the handleStreamBytesReadOffLoop calls out of the gate.

No test in this CL, but a following CL contains a test which
reliably exercises the condition.

For golang/go#58547

Change-Id: Ic98888947f67408a4a1f6f4a3aaf68c3a2fe8e7f
Reviewed-on: https://go-review.googlesource.com/c/net/+/527580
Reviewed-by: Jonathan Amsterdam <jba@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
diff --git a/internal/quic/conn_flow.go b/internal/quic/conn_flow.go
index 265fdaf..cd9a6a9 100644
--- a/internal/quic/conn_flow.go
+++ b/internal/quic/conn_flow.go
@@ -47,6 +47,9 @@
 //
 // This is called indirectly by the user, via Read or CloseRead.
 func (c *Conn) handleStreamBytesReadOffLoop(n int64) {
+	if n == 0 {
+		return
+	}
 	if c.shouldUpdateFlowControl(c.streams.inflow.credit.Add(n)) {
 		// We should send a MAX_DATA update to the peer.
 		// Record this on the Conn's main loop.
diff --git a/internal/quic/conn_flow_test.go b/internal/quic/conn_flow_test.go
index 28559b4..2cd4e62 100644
--- a/internal/quic/conn_flow_test.go
+++ b/internal/quic/conn_flow_test.go
@@ -180,6 +180,7 @@
 			max: 128 + 32 + 1 + 1 + 1,
 		})
 
+	tc.ignoreFrame(frameTypeStopSending)
 	streams[2].CloseRead()
 	tc.wantFrame("closed stream triggers another MAX_DATA update",
 		packetType1RTT, debugFrameMaxData{
diff --git a/internal/quic/stream.go b/internal/quic/stream.go
index 923ff23..9310811 100644
--- a/internal/quic/stream.go
+++ b/internal/quic/stream.go
@@ -181,11 +181,13 @@
 	if s.IsWriteOnly() {
 		return 0, errors.New("read from write-only stream")
 	}
-	// Wait until data is available.
 	if err := s.ingate.waitAndLock(ctx, s.conn.testHooks); err != nil {
 		return 0, err
 	}
-	defer s.inUnlock()
+	defer func() {
+		s.inUnlock()
+		s.conn.handleStreamBytesReadOffLoop(int64(n)) // must be done with ingate unlocked
+	}()
 	if s.inresetcode != -1 {
 		return 0, fmt.Errorf("stream reset by peer: %w", StreamErrorCode(s.inresetcode))
 	}
@@ -205,7 +207,6 @@
 	start := s.in.start
 	end := start + int64(len(b))
 	s.in.copy(start, b)
-	s.conn.handleStreamBytesReadOffLoop(int64(len(b)))
 	s.in.discardBefore(end)
 	if s.insize == -1 || s.insize > s.inwin {
 		if shouldUpdateFlowControl(s.inmaxbuf, s.in.start+s.inmaxbuf-s.inwin) {
@@ -334,7 +335,6 @@
 		return
 	}
 	s.ingate.lock()
-	defer s.inUnlock()
 	if s.inset.isrange(0, s.insize) || s.inresetcode != -1 {
 		// We've already received all data from the peer,
 		// so there's no need to send STOP_SENDING.
@@ -343,8 +343,10 @@
 	} else {
 		s.inclosed.set()
 	}
-	s.conn.handleStreamBytesReadOffLoop(s.in.end - s.in.start)
+	discarded := s.in.end - s.in.start
 	s.in.discardBefore(s.in.end)
+	s.inUnlock()
+	s.conn.handleStreamBytesReadOffLoop(discarded) // must be done with ingate unlocked
 }
 
 // CloseWrite aborts writes on the stream.