CVE-2024-22189 (published 2024-04-04T15:15Z)

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.


Weakness information:
cwe_id: NVD-CWE-noinfo
cwe_name: Insufficient Information
description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
extended_description: Insufficient Information

Fix information:
repo_url: https://github.com/quic-go/quic-go
commit_details: repo_url: https://github.com/quic-go/quic-go
author: Marten Seemann
author_date: 2024-03-18 09:59:00+09:30
msg: 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.
repository: repo_url: https://github.com/quic-go/quic-go
repo_name: quic-go/quic-go
description: A QUIC implementation in pure Go
repo_language: Go
owner: quic-go
file_changes: filename: connection.go
change_type: ModificationType.MODIFY
diff: @@ -521,6 +521,9 @@ func (s *connection) run() error {
 
 runLoop:
 	for {
+		if s.framer.QueuedTooManyControlFrames() {
+			s.closeLocal(&qerr.TransportError{ErrorCode: InternalError})
+		}
 		// Close immediately if requested
 		select {
 		case closeErr = <-s.closeChan:

programming_language: Go
filename: framer.go
change_type: ModificationType.MODIFY
diff: @@ -21,9 +21,19 @@ type framer interface {
 	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
@@ -33,9 +43,10 @@ type framerI struct {
 	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{}
@@ -73,6 +84,11 @@ func (f *framerI) QueueControlFrame(frame wire.Frame) {
 		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)
 }
 
@@ -105,6 +121,10 @@ func (f *framerI) AppendControlFrames(frames []ackhandler.Frame, maxLen protocol
 	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 {

programming_language: Go
filename: framer_test.go
change_type: ModificationType.MODIFY
diff: @@ -109,6 +109,23 @@ var _ = Describe("Framer", func() {
 			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() {

programming_language: Go