CVE-2026-13799
Overview
Files Changed
osp/impl/quic/open_screen_session_base.ccosp/impl/quic/open_screen_session_base.h
Patch
From 37ff938a93cb04c6b77e019b52328c8e9b320317 Mon Sep 17 00:00:00 2001
From: Adam Rice <ricea@chromium.org>
Date: Fri, 22 May 2026 14:19:46 +0900
Subject: [PATCH] Remove deprecated CreateIncomingStream method
This patch removes the `CreateIncomingStream(quic::PendingStream*)`
overload from `OpenScreenSessionBase`. This method is deprecated and was
unused, with its implementation only triggering an unreachable code
assertion (`OSP_NOTREACHED()`). Removing it cleans up the class
interface and eliminates dead code. It also makes Open Screen compatible
with newer versions of QUICHE that do not have the method in their base
class.
Bug: 499252371
Change-Id: Ic046cd0b7e83018f3dbff42e5402619eb6e8d705
Reviewed-on: https://chromium-review.googlesource.com/c/openscreen/+/7870270
Reviewed-by: Muyao Xu <muyaoxu@google.com>
Commit-Queue: Adam Rice <ricea@chromium.org>
---
diff --git a/osp/impl/quic/open_screen_session_base.cc b/osp/impl/quic/open_screen_session_base.cc
index d8ebad0..ad79822 100644
--- a/osp/impl/quic/open_screen_session_base.cc
+++ b/osp/impl/quic/open_screen_session_base.cc
@@ -89,11 +89,6 @@
return stream_ptr;
}
-quic::QuicStream* OpenScreenSessionBase::CreateIncomingStream(
- quic::PendingStream* /*pending*/) {
- OSP_NOTREACHED();
-}
-
bool OpenScreenSessionBase::ShouldKeepConnectionAlive() const {
// OpenScreen connections stay alive until they're explicitly closed.
return true;
diff --git a/osp/impl/quic/open_screen_session_base.h b/osp/impl/quic/open_screen_session_base.h
index 9b00034..7b3c270 100644
--- a/osp/impl/quic/open_screen_session_base.h
+++ b/osp/impl/quic/open_screen_session_base.h
@@ -74,7 +74,6 @@
return crypto_stream_.get();
}
quic::QuicStream* CreateIncomingStream(quic::QuicStreamId id) override;
- quic::QuicStream* CreateIncomingStream(quic::PendingStream* pending) override;
bool ShouldKeepConnectionAlive() const override;
std::unique_ptr<quic::QuicConnection> connection_;
Original Bug Report
Potential Use-After-Free in QuicUnackedPacketMap via Re-entrant Packet Flush
Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports without the security team.
Overview: When processing a QUIC packet containing a DATAGRAM frame, QuicUnackedPacketMap::NotifyFramesAcked disables safe frame copying and reference refreshing. If a co-bundled frame (like MAX_STREAMS) triggers a synchronous packet send, the underlying deque can relocate, leaving a dangling pointer. An attacker can leverage this to achieve an unmitigated arbitrary delete primitive and potential RCE in the Network Service.
Affected files:
net/third_party/quiche/src/quiche/quic/core/quic_unacked_packet_map.ccnet/third_party/quiche/src/quiche/quic/core/quic_sent_packet_manager.ccnet/third_party/quiche/src/quiche/quic/core/quic_transmission_info.hnet/third_party/quiche/src/quiche/quic/core/quic_session.cc
Estimated timestamp from git blame: 2025-08-28
Background
When a sent QUIC packet is acknowledged, QuicUnackedPacketMap::NotifyFramesAcked is invoked to process its frames. It receives a reference QuicTransmissionInfo*& info pointing to the packet’s metadata within a QuicheCircularDeque.
Because frame processing can trigger re-entrant packet sends (which push new elements to the deque and might trigger a reallocation of the deque’s backing array), the function typically copies the frames and uses an absl::Cleanup block to refresh the info pointer after the loop.
Vulnerability Details
There is a logic flaw in NotifyFramesAcked that explicitly disables these defensive mechanisms if the packet contains a DATAGRAM_FRAME:
const bool use_copied_frames = !HasDatagramFrame(info->retransmittable_frames);
if (use_copied_frames) {
// ... copies frames ...
}
absl::Cleanup cleanup = [&]() {
if (use_copied_frames) { // <-- info is NOT refreshed if DATAGRAM is present
info = GetMutableTransmissionInfo(packet_number);
}
};
This creates a severe Use-After-Free (UAF) condition. If an attacker can force a re-entrant packet send while processing a packet containing both a DATAGRAM_FRAME and another frame (such as MAX_STREAMS_FRAME), the QuicheCircularDeque may relocate. The info pointer will become a dangling pointer to the freed backing array.
When NotifyFramesAcked returns, the caller (QuicSentPacketManager::MarkPacketHandled) continues to use this dangling info pointer, eventually calling unacked_packets_.RemoveRetransmittability(info). This method invokes DeleteFrames(&info->retransmittable_frames) on the freed memory.
Potential Exploitation Steps
Note: These are potential steps an attacker might use, as we do not yet have a working executable PoC.
- Establish Connection: The attacker hosts a malicious server and the victim connects via WebTransport (enabling
DATAGRAM_FRAMEs). - Groom Deque: The attacker shapes the traffic such that the victim’s
unacked_packets_deque is exactly at its current memory capacity. - Queue Frames: The attacker exhausts the client’s
MAX_STREAMSlimit to force the client to queue aMAX_STREAMS_FRAME. - Generate Target Packet: The client sends a packet containing both a
DATAGRAM_FRAMEand the pendingMAX_STREAMS_FRAME. - Trigger UAF: The attacker sends an
ACK_FRAMEfor the target packet, bundled with enough stream data/frames to completely fill the client’s packet creator buffer. - Re-entrant Send: During ACK processing,
NotifyFramesAckeddisablesinforefreshing due to the DATAGRAM frame. Acknowledging theMAX_STREAMS_FRAMEtriggersMaybeSendMaxStreamsFrame. Because the packet creator is full (groomed in step 5),AddFrame()triggers a synchronousFlushCurrentPacket(). - Relocation:
FlushCurrentPacket()serializes the pending frames and callsAddSentPacket(). This pushes a new element tounacked_packets_, exceeding capacity and relocating the deque’s backing array. Theinfopointer is now dangling. - Heap Grooming: Using concurrent threads or interleaved operations, the attacker reallocates the freed deque backing array with a forged
absl::InlinedVectorholding a fakeQuicFrame. - Arbitrary Delete Primitive: Because
QuicFrameuses an unprotected C-style union for its out-of-line pointers (e.g.,QuicAckFrame* ack_frame;), the attacker configures their fake frame as anACK_FRAMEand sets the pointer to an arbitrary heap address. MiraclePtr (BRP) does not protect raw union pointers. - RCE:
RemoveRetransmittabilitycallsDeleteFrames, executingdeleteon the attacker’s arbitrary pointer. The attacker uses this primitive to free a TLS context or other sensitive object, achieving a controllable Use-After-Free and ultimate RCE in the Network Service.
Suggested Fix
Do not rely on a persistent pointer/reference to an element inside QuicheCircularDeque across calls that can potentially mutate the deque.
Instead of taking QuicTransmissionInfo*& info, the methods should take the packet_number and dynamically resolve the element index (packet_number - least_unacked_) whenever the QuicTransmissionInfo needs to be accessed after potentially mutating callbacks. Furthermore, the absl::Cleanup logic in NotifyFramesAcked and MaybeAggregateAckedStreamFrame should unconditionally refresh the pointer, or the code should just avoid storing references to deque elements across session_notifier_->OnFrameAcked calls altogether.
Evaluated with Chrome root at commit: ff3d2b74fa39431785bd60e51463b08fcc71ee33
Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.