CVE-2026-79027
Overview
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 |
Files Changed
api/video/rtp_video_frame_assembler.ccmodules/video_coding/packet_buffer.cc
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;
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