CVE-2024-22189: Insufficient Information (NVD-CWE-noinfo)
value: quic-go is an implementation of the QUIC protocol in Go. Prior to version 0.42.0, an attacker can cause its peer to run out of memory sending a large number of `NEW_CONNECTION_ID` frames that retire old connection IDs. The receiver is supposed to respond to each retirement frame with a `RETIRE_CONNECTION_ID` frame. The attacker can prevent the receiver from sending out (the vast majority of) these `RETIRE_CONNECTION_ID` frames by collapsing the peers congestion window (by selectively acknowledging received packets) and by manipulating the peer's RTT estimate. Version 0.42.0 contains a patch for the issue. No known workarounds are available.
CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
Published: 2024-04-04T15:15Z
Repository: https://github.com/quic-go/quic-go
Language: Go
Files changed: connection.go, framer.go, framer_test.go
Commit date: 2024-03-17 17:29:00 -0700
Commit message: close connection when an abnormally large number of frames are queued (#4369)

Under normal circumstances, we should be able to send out control frames
right away, so we don't expect any queue to build up. To defend against
resource exhaustion attacks, we limit the control frame queue to 16384
elements.
Diff: diff --git a/connection.go b/connection.go
index 8c66cb0b..f8bcd613 100644
--- a/connection.go
+++ b/connection.go
@@ -519,10 +519,13 @@ func (s *connection) run() error {
 
 	var sendQueueAvailable <-chan struct{}
 
 runLoop:
 	for {
+		if s.framer.QueuedTooManyControlFrames() {
+			s.closeLocal(&qerr.TransportError{ErrorCode: InternalError})
+		}
 		// Close immediately if requested
 		select {
 		case closeErr = <-s.closeChan:
 			break runLoop
 		default:
diff --git a/framer.go b/framer.go
index df8c2841..1e6219a4 100644
--- a/framer.go
+++ b/framer.go
@@ -19,25 +19,36 @@ type framer interface {
 
 	AddActiveStream(protocol.StreamID)
 	AppendStreamFrames([]ackhandler.StreamFrame, protocol.ByteCount, protocol.Version) ([]ackhandler.StreamFrame, protocol.ByteCount)
 
 	Handle0RTTRejection() error
+
+	// QueuedTooManyControlFrames says if the control frame queue exceeded its maximum queue length.
+	// This is a hack.
+	// It is easier to implement than propagating an error return value in QueueControlFrame.
+	// The correct solution would be to queue frames with their respective structs.
+	// See https://github.com/quic-go/quic-go/issues/4271 for the queueing of stream-related control frames.
+	QueuedTooManyControlFrames() bool
 }
 
-const maxPathResponses = 256
+const (
+	maxPathResponses = 256
+	maxControlFrames = 16 << 10
+)
 
 type framerI struct {
 	mutex sync.Mutex
 
 	streamGetter streamGetter
 
 	activeStreams map[protocol.StreamID]struct{}
 	streamQueue   ringbuffer.RingBuffer[protocol.StreamID]
 
-	controlFrameMutex sync.Mutex
-	controlFrames     []wire.Frame
-	pathResponses     []*wire.PathResponseFrame
+	controlFrameMutex          sync.Mutex
+	controlFrames              []wire.Frame
+	pathResponses              []*wire.PathResponseFrame
+	queuedTooManyControlFrames bool
 }
 
 var _ framer = &framerI{}
 
 func newFramer(streamGetter streamGetter) framer {
@@ -71,10 +82,15 @@ func (f *framerI) QueueControlFrame(frame wire.Frame) {
 			return
 		}
 		f.pathResponses = append(f.pathResponses, pr)
 		return
 	}
+	// This is a hack.
+	if len(f.controlFrames) >= maxControlFrames {
+		f.queuedTooManyControlFrames = true
+		return
+	}
 	f.controlFrames = append(f.controlFrames, frame)
 }
 
 func (f *framerI) AppendControlFrames(frames []ackhandler.Frame, maxLen protocol.ByteCount, v protocol.Version) ([]ackhandler.Frame, protocol.ByteCount) {
 	f.controlFrameMutex.Lock()
@@ -103,10 +119,14 @@ func (f *framerI) AppendControlFrames(frames []ackhandler.Frame, maxLen protocol
 		f.controlFrames = f.controlFrames[:len(f.controlFrames)-1]
 	}
 	return frames, length
 }
 
+func (f *framerI) QueuedTooManyControlFrames() bool {
+	return f.queuedTooManyControlFrames
+}
+
 func (f *framerI) AddActiveStream(id protocol.StreamID) {
 	f.mutex.Lock()
 	if _, ok := f.activeStreams[id]; !ok {
 		f.streamQueue.PushBack(id)
 		f.activeStreams[id] = struct{}{}
diff --git a/framer_test.go b/framer_test.go
index bd376a10..6d364423 100644
--- a/framer_test.go
+++ b/framer_test.go
@@ -107,10 +107,27 @@ var _ = Describe("Framer", func() {
 			Expect(framer.Handle0RTTRejection()).To(Succeed())
 			fs, length := framer.AppendControlFrames(nil, protocol.MaxByteCount, protocol.Version1)
 			Expect(fs).To(HaveLen(2))
 			Expect(length).To(Equal(ping.Length(version) + ncid.Length(version)))
 		})
+
+		It("detects when too many frames are queued", func() {
+			for i := 0; i < maxControlFrames-1; i++ {
+				framer.QueueControlFrame(&wire.PingFrame{})
+				framer.QueueControlFrame(&wire.PingFrame{})
+				Expect(framer.QueuedTooManyControlFrames()).To(BeFalse())
+				frames, _ := framer.AppendControlFrames([]ackhandler.Frame{}, 1, protocol.Version1)
+				Expect(frames).To(HaveLen(1))
+				Expect(framer.(*framerI).controlFrames).To(HaveLen(i + 1))
+			}
+			framer.QueueControlFrame(&wire.PingFrame{})
+			Expect(framer.QueuedTooManyControlFrames()).To(BeFalse())
+			Expect(framer.(*framerI).controlFrames).To(HaveLen(maxControlFrames))
+			framer.QueueControlFrame(&wire.PingFrame{})
+			Expect(framer.QueuedTooManyControlFrames()).To(BeTrue())
+			Expect(framer.(*framerI).controlFrames).To(HaveLen(maxControlFrames))
+		})
 	})
 
 	Context("handling PATH_RESPONSE frames", func() {
 		It("packs a single PATH_RESPONSE per packet", func() {
 			f1 := &wire.PathResponseFrame{Data: [8]byte{1, 2, 3, 4, 5, 6, 7, 8}}

Vulnerable code: const maxPathResponses = 256
	controlFrameMutex sync.Mutex
	controlFrames     []wire.Frame
	pathResponses     []*wire.PathResponseFrame
Fixed code: 		if s.framer.QueuedTooManyControlFrames() {
			s.closeLocal(&qerr.TransportError{ErrorCode: InternalError})
		}

	// QueuedTooManyControlFrames says if the control frame queue exceeded its maximum queue length.
	// This is a hack.
	// It is easier to implement than propagating an error return value in QueueControlFrame.
	// The correct solution would be to queue frames with their respective structs.
	// See https://github.com/quic-go/quic-go/issues/4271 for the queueing of stream-related control frames.
	QueuedTooManyControlFrames() bool
const (
	maxPathResponses = 256
	maxControlFrames = 16 << 10
)
	controlFrameMutex          sync.Mutex
	controlFrames              []wire.Frame
	pathResponses              []*wire.PathResponseFrame
	queuedTooManyControlFrames bool
	// This is a hack.
	if len(f.controlFrames) >= maxControlFrames {
		f.queuedTooManyControlFrames = true
		return
	}
func (f *framerI) QueuedTooManyControlFrames() bool {
	return f.queuedTooManyControlFrames
}


		It("detects when too many frames are queued", func() {
			for i := 0; i < maxControlFrames-1; i++ {
				framer.QueueControlFrame(&wire.PingFrame{})
				framer.QueueControlFrame(&wire.PingFrame{})
				Expect(framer.QueuedTooManyControlFrames()).To(BeFalse())
				frames, _ := framer.AppendControlFrames([]ackhandler.Frame{}, 1, protocol.Version1)
				Expect(frames).To(HaveLen(1))
				Expect(framer.(*framerI).controlFrames).To(HaveLen(i + 1))
			}
			framer.QueueControlFrame(&wire.PingFrame{})
			Expect(framer.QueuedTooManyControlFrames()).To(BeFalse())
			Expect(framer.(*framerI).controlFrames).To(HaveLen(maxControlFrames))
			framer.QueueControlFrame(&wire.PingFrame{})
			Expect(framer.QueuedTooManyControlFrames()).To(BeTrue())
			Expect(framer.(*framerI).controlFrames).To(HaveLen(maxControlFrames))
		})