From: cunix@mail.de Date: 2024-04-21 12:00:00 Subject: Memory Exhaustion Attack against QUIC's Connection ID Mechanism References: https://github.com/quic-go/quic-go/commit/4a99b816ae3ab03ae5449d15aac45147c85ed47a https://github.com/quic-go/quic-go/security/advisories/GHSA-c33x-xqrf-c478 https://bugzilla.suse.com/show_bug.cgi?id=1222473 This tries to backport commit https://github.com/quic-go/quic-go/commit/4a99b816ae3ab03ae5449d15aac45147c85ed47a.patch from Marten Seemann to the vendored older version of quic-go. dnscrypt-proxy upstream already vendors version 0.42 of quic-go with hack included, but is not released. Patch should be dropped with next release of dnscrypt-proxy. --- diff -r -U 5 a/vendor/github.com/quic-go/quic-go/connection.go b/vendor/github.com/quic-go/quic-go/connection.go --- a/vendor/github.com/quic-go/quic-go/connection.go +++ b/vendor/github.com/quic-go/quic-go/connection.go @@ -516,11 +516,14 @@ var sendQueueAvailable <-chan struct{} runLoop: for { - // Close immediately if requested + if s.framer.QueuedTooManyControlFrames() { + s.closeLocal(&qerr.TransportError{ErrorCode: InternalError}) + } + // Close immediately if requested select { case closeErr = <-s.closeChan: break runLoop default: } diff -r -U 5 a/vendor/github.com/quic-go/quic-go/framer.go b/vendor/github.com/quic-go/quic-go/framer.go --- a/vendor/github.com/quic-go/quic-go/framer.go +++ b/vendor/github.com/quic-go/quic-go/framer.go @@ -19,22 +19,32 @@ AddActiveStream(protocol.StreamID) AppendStreamFrames([]ackhandler.StreamFrame, protocol.ByteCount, protocol.VersionNumber) ([]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 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 + queuedTooManyControlFrames bool } var _ framer = &framerI{} func newFramer(streamGetter streamGetter) framer { @@ -56,11 +66,24 @@ f.controlFrameMutex.Unlock() return hasData } func (f *framerI) QueueControlFrame(frame wire.Frame) { + var returnearly bool f.controlFrameMutex.Lock() + // This is a hack. + if len(f.controlFrames) >= maxControlFrames { + returnearly = true + } + f.controlFrameMutex.Unlock() + if returnearly { + f.mutex.Lock() + f.queuedTooManyControlFrames = true + f.mutex.Unlock() + return + } + f.controlFrameMutex.Lock() f.controlFrames = append(f.controlFrames, frame) f.controlFrameMutex.Unlock() } func (f *framerI) AppendControlFrames(frames []ackhandler.Frame, maxLen protocol.ByteCount, v protocol.VersionNumber) ([]ackhandler.Frame, protocol.ByteCount) { @@ -78,10 +101,17 @@ } f.controlFrameMutex.Unlock() return frames, length } +func (f *framerI) QueuedTooManyControlFrames() bool { + f.mutex.Lock() + toomany := f.queuedTooManyControlFrames + f.mutex.Unlock() + return toomany +} + func (f *framerI) AddActiveStream(id protocol.StreamID) { f.mutex.Lock() if _, ok := f.activeStreams[id]; !ok { f.streamQueue.PushBack(id) f.activeStreams[id] = struct{}{}