http2: don't crash when writing RST_STREAM on idle or closed streams

https://go-review.googlesource.com/33238 fixed scheduling of RST_STREAMs so
they are added to the appropriate stream queue. However, RST_STREAM may be
sent before OpenStream or after CloseStream in certain error cases, such as:

https://github.com/golang/net/blob/00ed5e97ea3a5ac46658b98e50259941947cec04/http2/server.go#L1395
https://github.com/golang/net/blob/00ed5e97ea3a5ac46658b98e50259941947cec04/http2/frame.go#L866
https://github.com/golang/net/blob/00ed5e97ea3a5ac46658b98e50259941947cec04/http2/frame.go#L947

In these cases, the failing stream has no queue and in priorityWriteScheduler.Push
will panic. A simple fix is to add RST_STREAMs to the root queue when the stream
queue doesn't exist. This is correct because:

- RST_STREAM is tiny and doesn't use flow control bytes, so prioritization is
  not important.

- The server should not send any frames on a stream after sending RST_STREAM,
  but even if that happens, the RST_STREAM will be ordered before those other
  frames because the control queue has the highest priority.

Fixes golang/go#17919

Change-Id: I2e8101f015822ef975303a1fe87634b36afbdc6b
Reviewed-on: https://go-review.googlesource.com/33248
Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
Run-TryBot: Brad Fitzpatrick <bradfitz@golang.org>
diff --git a/http2/writesched.go b/http2/writesched.go
index fb5da35..caa77c7 100644
--- a/http2/writesched.go
+++ b/http2/writesched.go
@@ -25,7 +25,9 @@
 	// https://tools.ietf.org/html/rfc7540#section-5.1
 	AdjustStream(streamID uint32, priority PriorityParam)
 
-	// Push queues a frame in the scheduler.
+	// Push queues a frame in the scheduler. In most cases, this will not be
+	// called with wr.StreamID()!=0 unless that stream is currently open. The one
+	// exception is RST_STREAM frames, which may be sent on idle or closed streams.
 	Push(wr FrameWriteRequest)
 
 	// Pop dequeues the next frame to write. Returns false if no frames can
diff --git a/http2/writesched_priority.go b/http2/writesched_priority.go
index 40108b0..0113272 100644
--- a/http2/writesched_priority.go
+++ b/http2/writesched_priority.go
@@ -388,7 +388,15 @@
 	} else {
 		n = ws.nodes[id]
 		if n == nil {
-			panic("add on non-open stream")
+			// id is an idle or closed stream. wr should not be a HEADERS or
+			// DATA frame. However, wr can be a RST_STREAM. In this case, we
+			// push wr onto the root, rather than creating a new priorityNode,
+			// since RST_STREAM is tiny and the stream's priority is unknown
+			// anyway. See issue #17919.
+			if wr.DataSize() > 0 {
+				panic("add DATA on non-open stream")
+			}
+			n = &ws.root
 		}
 	}
 	n.q.push(wr)
diff --git a/http2/writesched_priority_test.go b/http2/writesched_priority_test.go
index 174b447..2b23204 100644
--- a/http2/writesched_priority_test.go
+++ b/http2/writesched_priority_test.go
@@ -524,3 +524,18 @@
 		t.Error(err)
 	}
 }
+
+func TestPriorityRstStreamOnNonOpenStreams(t *testing.T) {
+	ws := NewPriorityWriteScheduler(&PriorityWriteSchedulerConfig{
+		MaxClosedNodesInTree: 0,
+		MaxIdleNodesInTree:   0,
+	})
+	ws.OpenStream(1, OpenStreamOptions{})
+	ws.CloseStream(1)
+	ws.Push(FrameWriteRequest{write: streamError(1, ErrCodeProtocol)})
+	ws.Push(FrameWriteRequest{write: streamError(2, ErrCodeProtocol)})
+
+	if err := checkPopAll(ws, []uint32{1, 2}); err != nil {
+		t.Error(err)
+	}
+}