CVE-2026-79027
Overview
Background
- `PacketBuffer`
- the WebRTC video-coding buffer that stores incoming RTP packets by sequence number, tracks missing packets and padding, and assembles complete frames via
FindFrames. - `DescendingSeqNumComp<uint16_t>`
- a comparator built on modular 16-bit sequence-number “ahead of” logic (
AheadOf/ForwardDiff), which is not transitive and therefore not a valid strict-weak-ordering forstd::set. - `SeqNumUnwrapper<uint16_t>`
- a utility that converts wrapping 16-bit RTP sequence numbers into monotonically increasing
int64_tvalues (Unwrap). - `missing_packets_` / `received_padding_`
- sets inside
PacketBufferthat previously keyed onuint16_twith the wrap-aware comparator to decide which packets/padding are present.
Root Cause Analysis
PacketBuffer represented sequence numbers as uint16_t and ordered missing_packets_ and received_padding_ with std::set<uint16_t, DescendingSeqNumComp<uint16_t>>, but DescendingSeqNumComp relies on modular “ahead-of” comparisons that lack transitivity, so it does not satisfy the strict-weak-ordering contract std::set requires. Feeding such a comparator to std::set is undefined behavior: tree invariants can be violated, lookups (find/lower_bound) can misbehave, and node bookkeeping can become inconsistent, which for a container of heap nodes can manifest as memory corruption / use-after-free (inferred). Because the raw 16-bit values also wrap, arithmetic such as first_seq_num_ - seq_num and the ForwardDiff/AheadOf-based clearing and frame-finding logic could compute wrong ranges, driving ClearTo, FindFrames, and UpdateMissingPackets over incorrect bounds.
The fix threads unwrapped int64_t sequence numbers from SeqNumUnwrapper<uint16_t>::Unwrap all the way through PacketBuffer (InsertPacket, ClearTo, InsertPadding, FindFrames, UpdateMissingPackets, Index, first_seq_num_, newest_inserted_seq_num_) and replaces the wrap-aware set comparators with the default std::set<int64_t>, so ordering is a proper total order and comparisons/differences are plain monotonic integer arithmetic; explicit static_cast<uint16_t> is applied only at the boundaries where 16-bit RTP values are still needed (e.g. reference_finder_.ClearTo, RtpFrameObject construction, PaddingReceived).
DescendingSeqNumComp<uint16_t>) as the ordering for std::set, violating its strict-weak-ordering precondition; the fix eliminates it by unwrapping sequence numbers to monotonic int64_t and using the natural ordering, restoring valid container invariants and correct range arithmetic.Attack Path
- Send a crafted RTP sequence
An attacker controlling the RTP stream sends video/padding packets whose 16-bit sequence numbers wrap or jump so as to exercise the non-transitive comparator across
missing_packets_/received_padding_. - Corrupt set ordering
Insertions/lookups on the
std::setordered byDescendingSeqNumComp<uint16_t>violate the strict-weak-ordering requirement, leaving the container in an inconsistent state (undefined behavior). - Drive wrong-range buffer operations
Wrap-affected arithmetic in
ClearTo/FindFrames/UpdateMissingPacketscomputes incorrect iteration ranges overbuffer_, mismanaging stored packet nodes. - Trigger use-after-free
The combination of corrupted set state and mis-ranged clearing/assembly leads to freeing or reusing packet storage inconsistently, producing the use-after-free reported by the advisory
(inferred)— the patch does not exhibit a concrete freed-object dereference site.
Impact Assessment
PacketBuffer into undefined std::set behavior and incorrect buffer arithmetic, yielding a use-after-free in the renderer/WebRTC receiver context with no special preconditions beyond an established media stream. The advisory classifies this as a use-after-free in WebRTC and does not state in-the-wild exploitation. Progression from the corrupted-container/UAF condition to a controlled read/write or code execution is not demonstrated by the patch and is (inferred).Changed Functions
| Function | Change | Notes |
|---|---|---|
forapi/video/rtp_video_frame_assembler.cc |
modified | |
ifmodules/video_coding/packet_buffer.cc |
modified | |
formodules/video_coding/packet_buffer.cc |
modified | |
whilemodules/video_coding/packet_buffer.cc |
modified |
Files Changed
api/video/rtp_video_frame_assembler.ccmodules/video_coding/packet_buffer.hvideo/rtp_video_stream_receiver2.ccmodules/video_coding/packet_buffer_unittest.ccvideo/rtp_video_stream_receiver2.hmodules/video_coding/packet_buffer.cc
Audit Directions
- Custom comparators in ordered containersAudit every
std::set/std::map(and sort) that uses a modular/“ahead-of” comparator; verify it is a valid strict-weak-ordering (irreflexive, transitive), and prefer unwrapped monotonic keys. - Wrapping integer arithmetic on buffer rangesReview sequence-number subtraction and range/iteration bounds computed from
uint16_tvalues for wrap-induced miscalculations feedingClearTo/FindFrames-style loops. - Unwrap-at-the-boundary disciplineWhere RTP 16-bit values enter long-lived data structures, confirm they are unwrapped once at ingress and only re-truncated at true 16-bit sinks, rather than truncated repeatedly mid-pipeline.
Patch
From 6395da1ed7da9f3a8d97475c4e8db722d9415e6f Mon Sep 17 00:00:00 2001
From: Danil Chapovalov <danilchap@webrtc.org>
Date: Mon, 27 Jul 2026 12:14:11 +0200
Subject: [PATCH] Use unwrappred RTP sequence numbers in PacketBuffer
Thus avoid brittle DescendingSeqNumComp<uint16_t> as comparator in set.
Such comparator lacks some properties like transitivity making it
dangerous to use with the std::set
Bug: chromium:537233963
Change-Id: I400b423ab3f0d557219d459752e00d211fd97450
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/491041
Commit-Queue: Danil Chapovalov <danilchap@webrtc.org>
Reviewed-by: Ilya Nikolaevskiy <ilnik@webrtc.org>
Cr-Commit-Position: refs/heads/main@{#48232}
---
diff --git a/api/video/rtp_video_frame_assembler.cc b/api/video/rtp_video_frame_assembler.cc
index 8c03162..c02c7a2 100644
--- a/api/video/rtp_video_frame_assembler.cc
+++ b/api/video/rtp_video_frame_assembler.cc
@@ -91,12 +91,12 @@
RtpFrameVector AssembleFrames(
video_coding::PacketBuffer::InsertResult insert_result);
FrameVector FindReferences(RtpFrameVector frames);
- FrameVector UpdateWithPadding(uint16_t seq_num);
+ FrameVector UpdateWithPadding(int64_t seq_num);
bool ParseDependenciesDescriptorExtension(const RtpPacketReceived& rtp_packet,
RTPVideoHeader& video_header);
bool ParseGenericDescriptorExtension(const RtpPacketReceived& rtp_packet,
RTPVideoHeader& video_header);
- void ClearOldData(uint16_t incoming_seq_num);
+ void ClearOldData(int64_t incoming_seq_num);
std::unique_ptr<FrameDependencyStructure> video_structure_;
SeqNumUnwrapper<uint16_t> rtp_sequence_number_unwrapper_;
@@ -114,9 +114,11 @@
RtpVideoFrameAssembler::FrameVector RtpVideoFrameAssembler::Impl::InsertPacket(
const RtpPacketReceived& rtp_packet) {
+ int64_t unwrapped_sequence_number =
+ rtp_sequence_number_unwrapper_.Unwrap(rtp_packet.SequenceNumber());
if (rtp_packet.payload_size() == 0) {
- ClearOldData(rtp_packet.SequenceNumber());
- return UpdateWithPadding(rtp_packet.SequenceNumber());
+ ClearOldData(unwrapped_sequence_number);
+ return UpdateWithPadding(unwrapped_sequence_number);
}
std::optional<VideoRtpDepacketizer::ParsedRtpPayload> parsed_payload =
@@ -146,16 +148,16 @@
parsed_payload->video_header);
packet->video_payload = std::move(parsed_payload->video_payload);
- ClearOldData(rtp_packet.SequenceNumber());
+ ClearOldData(unwrapped_sequence_number);
return FindReferences(
AssembleFrames(packet_buffer_.InsertPacket(std::move(packet))));
}
-void RtpVideoFrameAssembler::Impl::ClearOldData(uint16_t incoming_seq_num) {
+void RtpVideoFrameAssembler::Impl::ClearOldData(int64_t incoming_seq_num) {
constexpr uint16_t kOldSeqNumThreshold = 2000;
- uint16_t old_seq_num = incoming_seq_num - kOldSeqNumThreshold;
+ int64_t old_seq_num = incoming_seq_num - kOldSeqNumThreshold;
packet_buffer_.ClearTo(old_seq_num);
- reference_finder_.ClearTo(old_seq_num);
+ reference_finder_.ClearTo(static_cast<uint16_t>(old_seq_num));
}
RtpVideoFrameAssembler::Impl::RtpFrameVector
@@ -184,8 +186,8 @@
// received time from RtpPacketReceived::arrival_time.
const video_coding::PacketBuffer::Packet& last_packet = *packet;
result.push_back(std::make_unique<RtpFrameObject>(
- first_packet->seq_num(), //
- last_packet.seq_num(), //
+ static_cast<uint16_t>(first_packet->seq_num()), //
+ static_cast<uint16_t>(last_packet.seq_num()), //
last_packet.marker_bit, //
/*times_nacked=*/0, //
/*first_packet_received_time=*/Timestamp::Zero(), //
@@ -224,10 +226,11 @@
}
RtpVideoFrameAssembler::FrameVector
-RtpVideoFrameAssembler::Impl::UpdateWithPadding(uint16_t seq_num) {
+RtpVideoFrameAssembler::Impl::UpdateWithPadding(int64_t seq_num) {
auto res =
FindReferences(AssembleFrames(packet_buffer_.InsertPadding(seq_num)));
- auto ref_finder_update = reference_finder_.PaddingReceived(seq_num);
+ auto ref_finder_update =
+ reference_finder_.PaddingReceived(static_cast<uint16_t>(seq_num));
for (std::unique_ptr<RtpFrameObject>& complete_frame : ref_finder_update) {
uint16_t rtp_seq_num_start = complete_frame->first_seq_num();
diff --git a/modules/video_coding/packet_buffer.cc b/modules/video_coding/packet_buffer.cc
index 815e0f4..6690f60 100644
--- a/modules/video_coding/packet_buffer.cc
+++ b/modules/video_coding/packet_buffer.cc
@@ -26,8 +26,6 @@
#include "modules/video_coding/codecs/h264/include/h264_globals.h"
#include "rtc_base/checks.h"
#include "rtc_base/logging.h"
-#include "rtc_base/numerics/mod_ops.h"
-#include "rtc_base/numerics/sequence_number_util.h"
namespace webrtc {
namespace video_coding {
@@ -67,21 +65,20 @@
std::unique_ptr<PacketBuffer::Packet> packet) {
PacketBuffer::InsertResult result;
- uint16_t seq_num = packet->seq_num();
+ const int64_t seq_num = packet->seq_num();
size_t index = Index(seq_num);
if (!first_packet_received_) {
first_seq_num_ = seq_num;
first_packet_received_ = true;
- } else if (AheadOf(first_seq_num_, seq_num)) {
+ } else if (first_seq_num_ > seq_num) {
// If we have explicitly cleared past this packet then it's old,
// don't insert it, just silently ignore it.
if (is_cleared_to_first_seq_num_) {
return result;
}
- if (ForwardDiff<uint16_t>(first_seq_num_, seq_num) >= max_size_ &&
- ForwardDiff<uint16_t>(seq_num, first_seq_num_) >= max_size_ / 2) {
+ if (first_seq_num_ - seq_num >= static_cast<int64_t>(max_size_ / 2)) {
// Large negative jump in rtp sequence number: clear the buffer and treat
// latest packet as the new first packet.
Clear();
@@ -93,7 +90,7 @@
if (buffer_[index] != nullptr) {
// Duplicate packet, just delete the payload.
- if (buffer_[index]->seq_num() == packet->seq_num()) {
+ if (buffer_[index]->seq_num() == seq_num) {
return result;
}
@@ -128,9 +125,9 @@
return result;
}
-void PacketBuffer::ClearTo(uint16_t seq_num) {
+void PacketBuffer::ClearTo(int64_t seq_num) {
// We have already cleared past this sequence number, no need to do anything.
- if (AheadOf<uint16_t>(first_seq_num_, seq_num)) {
+ if (first_seq_num_ > seq_num) {
return;
}
@@ -141,11 +138,11 @@
// Avoid iterating over the buffer more than once by capping the number of
// iterations to the `size_` of the buffer.
++seq_num;
- size_t diff = ForwardDiff<uint16_t>(first_seq_num_, seq_num);
- size_t iterations = std::min(diff, buffer_.size());
+ size_t iterations =
+ std::min<size_t>(seq_num - first_seq_num_, buffer_.size());
for (size_t i = 0; i < iterations; ++i) {
auto& stored = buffer_[Index(first_seq_num_)];
- if (stored != nullptr && AheadOf<uint16_t>(seq_num, stored->seq_num())) {
+ if (stored != nullptr && seq_num > stored->seq_num()) {
stored = nullptr;
}
++first_seq_num_;
@@ -167,11 +164,11 @@
ClearInternal();
}
-PacketBuffer::InsertResult PacketBuffer::InsertPadding(uint16_t seq_num) {
+PacketBuffer::InsertResult PacketBuffer::InsertPadding(int64_t seq_num) {
PacketBuffer::InsertResult result;
UpdateMissingPackets(seq_num);
received_padding_.insert(seq_num);
- result.packets = FindFrames(static_cast<uint16_t>(seq_num + 1));
+ result.packets = FindFrames(seq_num + 1);
return result;
}
@@ -214,7 +211,7 @@
return true;
}
-bool PacketBuffer::PotentialNewFrame(uint16_t seq_num) const {
+bool PacketBuffer::PotentialNewFrame(int64_t seq_num) const {
const auto& entry = buffer_[Index(seq_num)];
const auto& prev_entry = buffer_[Index(seq_num - 1)];
@@ -226,7 +223,7 @@
return true;
if (prev_entry == nullptr)
return false;
- if (prev_entry->seq_num() != static_cast<uint16_t>(entry->seq_num() - 1))
+ if (prev_entry->seq_num() != entry->seq_num() - 1)
return false;
if (prev_entry->timestamp != entry->timestamp)
return false;
@@ -237,13 +234,13 @@
}
std::vector<std::unique_ptr<PacketBuffer::Packet>> PacketBuffer::FindFrames(
- uint16_t seq_num) {
+ int64_t seq_num) {
std::vector<std::unique_ptr<PacketBuffer::Packet>> found_frames;
- auto start = seq_num;
+ int64_t start = seq_num;
for (size_t i = 0; i < buffer_.size(); ++i) {
- if (received_padding_.find(seq_num) != received_padding_.end()) {
- seq_num += 1;
+ if (received_padding_.contains(seq_num)) {
+ ++seq_num;
continue;
}
@@ -257,7 +254,7 @@
// If all packets of the frame is continuous, find the first packet of the
// frame and add all packets of the frame to the returned packets.
if (buffer_[index]->is_last_packet_in_frame()) {
- uint16_t start_seq_num = seq_num;
+ int64_t start_seq_num = seq_num;
// Find the start index by searching backward until the packet with
// the `frame_begin` flag is set.
@@ -339,8 +336,7 @@
// See: https://bugs.chromium.org/p/webrtc/issues/detail?id=7106
if (is_h264_descriptor &&
(buffer_[start_index] == nullptr ||
- buffer_[start_index]->seq_num() !=
- static_cast<uint16_t>(start_seq_num - 1) ||
+ buffer_[start_index]->seq_num() != start_seq_num - 1 ||
buffer_[start_index]->timestamp != frame_timestamp)) {
break;
}
@@ -387,11 +383,10 @@
}
if (is_h264_descriptor || full_frame_found) {
- const uint16_t end_seq_num = seq_num + 1;
- // Use uint16_t type to handle sequence number wrap around case.
- uint16_t num_packets = end_seq_num - start_seq_num;
+ const int64_t end_seq_number = seq_num + 1;
+ int64_t num_packets = end_seq_number - start_seq_num;
found_frames.reserve(found_frames.size() + num_packets);
- for (uint16_t j = start_seq_num; j != end_seq_num; ++j) {
+ for (int64_t j = start_seq_num; j != end_seq_number; ++j) {
std::unique_ptr<Packet>& packet = buffer_[Index(j)];
RTC_DCHECK(packet);
RTC_DCHECK_EQ(j, packet->seq_num());
@@ -412,23 +407,24 @@
return found_frames;
}
-void PacketBuffer::UpdateMissingPackets(uint16_t seq_num) {
+void PacketBuffer::UpdateMissingPackets(int64_t seq_num) {
if (!newest_inserted_seq_num_)
newest_inserted_seq_num_ = seq_num;
const int kMaxPaddingAge = 1000;
- if (AheadOf(seq_num, *newest_inserted_seq_num_)) {
- uint16_t old_seq_num = seq_num - kMaxPaddingAge;
- auto erase_to = missing_packets_.lower_bound(old_seq_num);
+ if (seq_num > *newest_inserted_seq_num_) {
+ int64_t old_seq_number = seq_num - kMaxPaddingAge;
+ auto erase_to = missing_packets_.lower_bound(old_seq_number);
missing_packets_.erase(missing_packets_.begin(), erase_to);
// Guard against inserting a large amount of missing packets if there is a
// jump in the sequence number.
- if (AheadOf(old_seq_num, *newest_inserted_seq_num_))
- *newest_inserted_seq_num_ = old_seq_num;
+ if (old_seq_number > *newest_inserted_seq_num_) {
+ *newest_inserted_seq_num_ = old_seq_number;
+ }
++*newest_inserted_seq_num_;
- while (AheadOf(seq_num, *newest_inserted_seq_num_)) {
+ while (seq_num > *newest_inserted_seq_num_) {
missing_packets_.insert(*newest_inserted_seq_num_);
++*newest_inserted_seq_num_;
}
diff --git a/modules/video_coding/packet_buffer.h b/modules/video_coding/packet_buffer.h
index f8db43f..7938b83 100644
--- a/modules/video_coding/packet_buffer.h
+++ b/modules/video_coding/packet_buffer.h
@@ -25,7 +25,6 @@
#include "modules/rtp_rtcp/source/rtp_video_header.h"
#include "rtc_base/checks.h"
#include "rtc_base/copy_on_write_buffer.h"
-#include "rtc_base/numerics/sequence_number_util.h"
namespace webrtc {
namespace video_coding {
@@ -53,7 +52,7 @@
bool is_last_packet_in_frame() const {
return video_header.is_last_packet_in_frame;
}
- uint16_t seq_num() const { return static_cast<uint16_t>(sequence_number); }
+ int64_t seq_num() const { return sequence_number; }
// If all its previous packets have been inserted into the packet buffer.
// Set and used internally by the PacketBuffer.
@@ -80,8 +79,8 @@
ABSL_MUST_USE_RESULT InsertResult
InsertPacket(std::unique_ptr<Packet> packet);
- ABSL_MUST_USE_RESULT InsertResult InsertPadding(uint16_t seq_num);
- void ClearTo(uint16_t seq_num);
+ ABSL_MUST_USE_RESULT InsertResult InsertPadding(int64_t seq_num);
+ void ClearTo(int64_t seq_num);
void Clear();
void ForceSpsPpsIdrIsH264Keyframe();
@@ -94,29 +93,27 @@
bool ExpandBufferSize();
// Test if all previous packets has arrived for the given sequence number.
- bool PotentialNewFrame(uint16_t seq_num) const;
+ bool PotentialNewFrame(int64_t seq_num) const;
// Test if all packets of a frame has arrived, and if so, returns packets to
// create frames.
- std::vector<std::unique_ptr<Packet>> FindFrames(uint16_t seq_num);
+ std::vector<std::unique_ptr<Packet>> FindFrames(int64_t seq_num);
- void UpdateMissingPackets(uint16_t seq_num);
+ void UpdateMissingPackets(int64_t seq_num);
- static size_t Index(uint16_t seq_num, size_t buffer_size) {
+ static size_t Index(int64_t seq_num, size_t buffer_size) {
RTC_DCHECK(std::has_single_bit(buffer_size));
size_t mask = buffer_size - 1;
return seq_num & mask;
}
- size_t Index(uint16_t seq_num) const {
- return Index(seq_num, buffer_.size());
- }
+ size_t Index(int64_t seq_num) const { return Index(seq_num, buffer_.size()); }
// buffer_.size() and max_size_ must always be a power of two.
const size_t max_size_;
// The fist sequence number currently in the buffer.
- uint16_t first_seq_num_;
+ int64_t first_seq_num_;
// If the packet buffer has received its first packet.
bool first_packet_received_;
@@ -128,10 +125,9 @@
// determine continuity between them.
std::vector<std::unique_ptr<Packet>> buffer_;
- std::optional<uint16_t> newest_inserted_seq_num_;
- std::set<uint16_t, DescendingSeqNumComp<uint16_t>> missing_packets_;
-
- std::set<uint16_t, DescendingSeqNumComp<uint16_t>> received_padding_;
+ std::optional<int64_t> newest_inserted_seq_num_;
+ std::set<int64_t> missing_packets_;
+ std::set<int64_t> received_padding_;
// Indicates if we should require SPS, PPS, and IDR for a particular
// RTP timestamp to treat the corresponding frame as a keyframe.
diff --git a/video/rtp_video_stream_receiver2.cc b/video/rtp_video_stream_receiver2.cc
index 20bbc32..e26c98e 100644
--- a/video/rtp_video_stream_receiver2.cc
+++ b/video/rtp_video_stream_receiver2.cc
@@ -892,13 +892,13 @@
const video_coding::PacketBuffer::Packet& last_packet = *packet;
OnAssembledFrame(std::make_unique<RtpFrameObject>(
- first_packet->seq_num(), //
- last_packet.seq_num(), //
- last_packet.marker_bit, //
- max_nack_count, //
- min_recv_time, //
- max_recv_time, //
- first_packet->timestamp, //
+ static_cast<uint16_t>(first_packet->seq_num()), //
+ static_cast<uint16_t>(last_packet.seq_num()), //
+ last_packet.marker_bit, //
+ max_nack_count, //
+ min_recv_time, //
+ max_recv_time, //
+ first_packet->timestamp, //
absolute_capture_time_ms.has_value()
? *absolute_capture_time_ms
: ntp_estimator_.Estimate(first_packet->timestamp), //
@@ -1183,8 +1183,9 @@
// Padding or keep-alive packet.
// TODO(nisse): Could drop empty packets earlier, but need to figure out how
// they should be counted in stats.
- NotifyReceiverOfEmptyPacket(packet.SequenceNumber(),
- GetCodecFromPayloadType(packet.PayloadType()));
+ NotifyReceiverOfEmptyPacket(
+ rtp_seq_num_unwrapper_.Unwrap(packet.SequenceNumber()),
... (file diff truncated)
Regression Test / PoC
diff --git a/modules/video_coding/packet_buffer_unittest.cc b/modules/video_coding/packet_buffer_unittest.cc
index 45b09e6..86e3377 100644
--- a/modules/video_coding/packet_buffer_unittest.cc
+++ b/modules/video_coding/packet_buffer_unittest.cc
@@ -54,7 +54,7 @@
for (const auto& packet : packets) {
EXPECT_EQ(frame_boundary, packet->is_first_packet_in_frame());
if (packet->is_first_packet_in_frame()) {
- result.push_back(packet->seq_num());
+ result.push_back(static_cast<uint16_t>(packet->seq_num()));
}
frame_boundary = packet->is_last_packet_in_frame();
}
Original Bug Report
Heap corruption (invalid free) in [@ webrtc::video_coding::PacketBuffer::FindFrames] via RTX-injected padding sequence numbers breaking DescendingSeqNumComp ordering
VULNERABILITY DETAILS Note, this is being reported by Mozilla, the issue is in libwebrtc but was exposed through Firefox via Ai scanning tools. https://bugzilla.mozilla.org/show_bug.cgi?id=2053579 Happy to cc someone from Google into this Mozilla bug report. There is more detail in the report available.
libwebrtc’s video PacketBuffer tracks received padding packets in received_padding_, a std::set<uint16_t, DescendingSeqNumComp<uint16_t>>. The comparator is only a valid strict weak ordering while all stored sequence numbers span at most half of the 16-bit sequence space (an explicitly documented precondition in sequence_number_util.h). A malicious remote WebRTC peer fully controls the media-stream sequence numbers of padding packets: RTX packets whose payload is just the 2-byte original sequence number (OSN) are de-encapsulated by RtxReceiveStream::OnRtpPacket into empty media packets, which RtpVideoStreamReceiver2 routes to PacketBuffer::InsertPadding. InsertPadding inserts the attacker-chosen value into received_padding_ without any range pruning, so the attacker can populate the set with values (e.g. 5, 32771, 46685, 32762) spanning more than half the sequence space, breaking the comparator’s transitivity and producing an inconsistently ordered rb-tree.
When a frame is subsequently completed across the 16-bit wrap (packets 65530..2), PacketBuffer::FindFrames executes received_padding_.erase(received_padding_.lower_bound(start), received_padding_.upper_bound(seq_num)). On the corrupted tree, lower_bound returns end() while upper_bound returns a live node, so std::set::erase is invoked with an inverted iterator range. libstdc++’s _M_erase_aux then walks from end(), passing the rb-tree header node — which is embedded inside the live VideoReceiveStream2 allocation — to free() (ASAN ‘attempting free on address which was not malloc()-ed’, 3648 bytes inside the 5232-byte VideoReceiveStream2 region) and frees tree nodes that remain linked into the structure, yielding use-after-free/heap corruption in the content process.
The bug is reachable by any web page that establishes an RTCPeerConnection with a malicious remote peer (recvonly video with RTX negotiated — the WebRTC default; no camera/microphone permission or other user interaction is required). It was reproduced on an unmodified mozilla-central ASAN build.
VERSION Chrome Version: n/a Operating System: Reproduced on Linux
REPRODUCTION CASE Test cases available.
CREDIT INFORMATION Reporter credit: Mozilla