CVE-2026-78891
Overview
Background
- RTP packetizer
- WebRTC code (
RtpPacketizerand its codec-specific subclasses) that splits one encoded video frame into a sequence of RTP packet payloads that each fit withinPayloadSizeLimits. - `RtpPacketizer::SplitAboutEqually`
- a static helper that takes a payload length and the per-packet limits and returns a
std::vector<int>of packet sizes summing to that length, or an empty vector on failure. - `PayloadSizeLimits`
- a struct holding
max_payload_lenplus first-, last- and single-packet reduction lengths, allint, which constrain each packet’s size. - RTP sequence number space
- a 16-bit counter. A frame needing tens of thousands of packets consumes a large share of it and is impractical to send.
Root Cause Analysis
Verify before building a PoC
What the commit does not establish: Says advisory gives no severity rating, but it lists medium / CVSS 8.8. Analysis text is truncated ('The patch adds'). Callers' size_t narrowing and the actual overrun site are not shown in the diff.
Before the patch, RtpPacketizer::SplitAboutEqually took the frame payload length as an int (payload_len). It relied only on a debug-only RTC_DCHECK_GT(payload_len, 0) to reject empty input and had no upper bound on size. Callers hold frame sizes as size_t, so an oversized encoded frame could be narrowed into int at the call boundary. A frame of 2 GB or more could become negative or wrap, and a large but in-range length could overflow the int arithmetic that follows (for example limits.single_packet_reduction_len + payload_len). In release builds, a zero-length or truncated value also reached the splitting logic, because the DCHECK is compiled out. The packet-size vector returned to the packetizer could then disagree with the real payload buffer, and the advisory classes the outcome as a buffer overflow in WebRTC. The exact copy that overruns is not shown in the diff (inferred: the codec packetizer reading or writing past the frame buffer based on the bad split).
The fix changes the parameter to size_t payload_size and returns an empty vector on two conditions: the size is 0, or it exceeds limits.max_payload_len * 0x7000 (checked with SafeGt, about 34 MB at a typical MTU). Only after that check is the value cast to int, so the remaining int arithmetic works on bounded values. An empty result makes the packetizer report zero packets, and the frame-sending code in rtp_sender_video.cc already returns false when num_packets == 0.
The patch adds a rate-limited warning on that path, so oversized frames are dropped instead of packetized.
SplitAboutEqually accepted an unbounded frame length through a narrowing int parameter and validated it only with a debug DCHECK. The fix takes size_t, rejects zero and anything above a sane per-frame packet budget before converting to int, and fails closed by returning an empty split.Attack Path
- Produce an oversized encoded frame
An encoded video frame far larger than normal (around 34 MB or more, up to sizes that do not fit in
int) reaches the RTP sending path. One plausible web-reachable source is page-controlled encoded frame data, such as an encoded-transform API that replaces frame payloads (inferred; the diff does not show the frame’s origin). - Packetize the frame
The video sender creates a codec packetizer, which calls
RtpPacketizer::SplitAboutEquallywith the frame size. Before the patch, that size was implicitly narrowed tointwith no release-mode check. - Mis-computed split
With a truncated, negative or overflowing
payload_len, the computed packet sizes no longer match the real payload length (inferred from the type change and the new bounds check). - Out-of-bounds access while building packets The packetizer copies payload bytes into RTP packets according to the bad sizes, overrunning the frame or packet buffer. That is the buffer overflow the advisory reports; the specific sink is not in the diff (inferred).
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifmodules/rtp_rtcp/source/rtp_sender_video.cc |
modified | |
TESTmodules/rtp_rtcp/source/rtp_format_unittest.cc |
modified |
Files Changed
modules/rtp_rtcp/source/rtp_format.ccmodules/rtp_rtcp/source/rtp_sender_video.hmodules/rtp_rtcp/source/rtp_format_unittest.ccmodules/rtp_rtcp/source/rtp_sender_video.ccmodules/rtp_rtcp/source/rtp_format.hmodules/rtp_rtcp/BUILD.gn
Audit Directions
- `size_t` to `int` narrowing at length boundariesLook for other packetizers and depacketizers in
modules/rtp_rtcp/source/(H.264, H.265, VP8, VP9, AV1, generic) and in codec code that take buffer lengths asintor dointarithmetic on frame sizes without an explicit upper bound first. - Debug-only validation of untrusted sizesSearch WebRTC for
RTC_DCHECKthat is the only guard on sizes derived from frame or packet data. In release builds these checks disappear, so the code needs a real early return. - Packet-count and sequence-number assumptionsReview code that computes the number of packets per frame, or reserves sequence numbers, for unchecked multiplication or overflow when frames are huge. This includes the new
limits.max_payload_len * 0x7000product itself, ifmax_payload_lencan ever be unexpectedly large.
Patch
From 190a7606321b53c861142f97fc39f679b295f31c Mon Sep 17 00:00:00 2001 From: Danil Chapovalov <[email protected]> Date: Thu, 23 Jul 2026 09:30:33 +0200 Subject: [PATCH] Limit size of a video frame when packetizing it To avoid various corner cases when frame is too large. Such large frames are impractical for RTP. Add a throttled log when a video frame is dropped by a packetizer. Bug: chromium:503013378 Change-Id: Ie8c55d7c3976f0cf7420163e30e4f95f21a8b3d2 Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/490600 Reviewed-by: Tomas Gunnarsson <[email protected]> Commit-Queue: Danil Chapovalov <[email protected]> Cr-Commit-Position: refs/heads/main@{#48214} --- diff --git a/modules/rtp_rtcp/source/rtp_format.cc b/modules/rtp_rtcp/source/rtp_format.cc index 5d3c880..75d00b1 100644 --- a/modules/rtp_rtcp/source/rtp_format.cc +++ b/modules/rtp_rtcp/source/rtp_format.cc @@ -10,6 +10,7 @@ #include "modules/rtp_rtcp/source/rtp_format.h" +#include <cstddef> #include <cstdint> #include <memory> #include <span> @@ -24,6 +25,8 @@ #include "modules/video_coding/codecs/vp8/include/vp8_globals.h" #include "modules/video_coding/codecs/vp9/include/vp9_globals.h" #include "rtc_base/checks.h" +#include "rtc_base/numerics/safe_compare.h" + #ifdef RTC_ENABLE_H265 #include "modules/rtp_rtcp/source/rtp_packetizer_h265.h" #endif @@ -77,14 +80,22 @@ } std::vector<int> RtpPacketizer::SplitAboutEqually( - int payload_len, + size_t payload_size, const PayloadSizeLimits& limits) { - RTC_DCHECK_GT(payload_len, 0); // First or last packet larger than normal are unsupported. RTC_DCHECK_GE(limits.first_packet_reduction_len, 0); RTC_DCHECK_GE(limits.last_packet_reduction_len, 0); std::vector<int> result; + if (payload_size == 0 || + SafeGt(payload_size, limits.max_payload_len * 0x7000)) { + // Do not support frames that are so large they need almost half of the RTP + // sequence number space. With MTU ~= 1.2KB that puts a limit of ~34MB on a + // single frame. Sending such large frames over RTP is likely impractical. + return result; + } + int payload_len = static_cast<int>(payload_size); + if (limits.max_payload_len >= limits.single_packet_reduction_len + payload_len) { result.push_back(payload_len); diff --git a/modules/rtp_rtcp/source/rtp_format.h b/modules/rtp_rtcp/source/rtp_format.h index e329109..214594b 100644 --- a/modules/rtp_rtcp/source/rtp_format.h +++ b/modules/rtp_rtcp/source/rtp_format.h @@ -60,9 +60,9 @@ // Returns true on success, false otherwise. virtual bool NextPacket(RtpPacketToSend* packet) = 0; - // Split payload_len into sum of integers with respect to `limits`. + // Splits `payload_size` into sum of integers with respect to `limits`. // Returns empty vector on failure. - static std::vector<int> SplitAboutEqually(int payload_len, + static std::vector<int> SplitAboutEqually(size_t payload_size, const PayloadSizeLimits& limits); }; } // namespace webrtc diff --git a/modules/rtp_rtcp/source/rtp_sender_video.cc b/modules/rtp_rtcp/source/rtp_sender_video.cc index d5d2fa0..1bb3c8a 100644 --- a/modules/rtp_rtcp/source/rtp_sender_video.cc +++ b/modules/rtp_rtcp/source/rtp_sender_video.cc @@ -710,8 +710,16 @@ const size_t num_packets = packetizer->NumPackets(); - if (num_packets == 0) + if (num_packets == 0) { + Timestamp now = clock_->CurrentTime(); + if (now >= last_fail_packetize_log_ + TimeDelta::Seconds(10)) { + RTC_LOG(LS_WARNING) << "Failed to packetize " << codec_type + << " video frame of size " << payload.size() + << ". Frame is dropped."; + last_fail_packetize_log_ = now; + } return false; + } bool first_frame = first_frame_sent_(); std::vector<std::unique_ptr<RtpPacketToSend>> rtp_packets; diff --git a/modules/rtp_rtcp/source/rtp_sender_video.h b/modules/rtp_rtcp/source/rtp_sender_video.h index 396591a..91d0360 100644 --- a/modules/rtp_rtcp/source/rtp_sender_video.h +++ b/modules/rtp_rtcp/source/rtp_sender_video.h @@ -240,6 +240,8 @@ RTC_GUARDED_BY(stats_mutex_); OneTimeEvent first_frame_sent_; + Timestamp last_fail_packetize_log_ RTC_GUARDED_BY(send_checker_) = + Timestamp::MinusInfinity(); // E2EE Custom Video Frame Encryptor (optional) FrameEncryptorInterface* const frame_encryptor_ = nullptr; diff --git a/modules/rtp_rtcp/source/rtp_format_unittest.cc b/modules/rtp_rtcp/source/rtp_format_unittest.cc index d67e17a..4cb5153 100644 --- a/modules/rtp_rtcp/source/rtp_format_unittest.cc +++ b/modules/rtp_rtcp/source/rtp_format_unittest.cc @@ -231,6 +231,21 @@ EXPECT_THAT(RtpPacketizer::SplitAboutEqually(20, limits), ElementsAre(9, 11)); } +TEST(RtpPacketizerSplitAboutEqually, RejectsZeroSize) { + RtpPacketizer::PayloadSizeLimits limits; + limits.max_payload_len = 1200; + + EXPECT_THAT(RtpPacketizer::SplitAboutEqually(0, limits), IsEmpty()); +} + +TEST(RtpPacketizerSplitAboutEqually, RejectsHugeSize) { + RtpPacketizer::PayloadSizeLimits limits; + limits.max_payload_len = 1200; + + EXPECT_THAT(RtpPacketizer::SplitAboutEqually(0xFFFF'FFFF, limits), IsEmpty()); + EXPECT_THAT(RtpPacketizer::SplitAboutEqually(40'000'000, limits), IsEmpty()); +} + TEST(RtpPacketizerSplitAboutEqually, RejectsZeroMaxPayloadLen) { RtpPacketizer::PayloadSizeLimits limits; limits.max_payload_len = 0; diff --git a/modules/rtp_rtcp/BUILD.gn b/modules/rtp_rtcp/BUILD.gn index c73c903..d496d99 100644 --- a/modules/rtp_rtcp/BUILD.gn +++ b/modules/rtp_rtcp/BUILD.gn @@ -340,6 +340,7 @@ "../../rtc_base:rate_limiter", "../../rtc_base:rtc_numerics", "../../rtc_base:rtp_to_ntp_estimator", + "../../rtc_base:safe_compare", "../../rtc_base:safe_conversions", "../../rtc_base:safe_minmax", "../../rtc_base:timeutils",
Regression Test / PoC
diff --git a/modules/rtp_rtcp/source/rtp_format_unittest.cc b/modules/rtp_rtcp/source/rtp_format_unittest.cc
index d67e17a..4cb5153 100644
--- a/modules/rtp_rtcp/source/rtp_format_unittest.cc
+++ b/modules/rtp_rtcp/source/rtp_format_unittest.cc
@@ -231,6 +231,21 @@
EXPECT_THAT(RtpPacketizer::SplitAboutEqually(20, limits), ElementsAre(9, 11));
}
+TEST(RtpPacketizerSplitAboutEqually, RejectsZeroSize) {
+ RtpPacketizer::PayloadSizeLimits limits;
+ limits.max_payload_len = 1200;
+
+ EXPECT_THAT(RtpPacketizer::SplitAboutEqually(0, limits), IsEmpty());
+}
+
+TEST(RtpPacketizerSplitAboutEqually, RejectsHugeSize) {
+ RtpPacketizer::PayloadSizeLimits limits;
+ limits.max_payload_len = 1200;
+
+ EXPECT_THAT(RtpPacketizer::SplitAboutEqually(0xFFFF'FFFF, limits), IsEmpty());
+ EXPECT_THAT(RtpPacketizer::SplitAboutEqually(40'000'000, limits), IsEmpty());
+}
+
TEST(RtpPacketizerSplitAboutEqually, RejectsZeroMaxPayloadLen) {
RtpPacketizer::PayloadSizeLimits limits;
limits.max_payload_len = 0;
Original Bug Report
Heap-buffer-overflow write in [@ webrtc::RtpPacketizerVp9::NextPacket] via size_t→int truncation in SplitAboutEqually
Security Bug
Important: Please do not change the component of this bug manually.
Please READ THIS FAQ before filing a bug: https://chromium.googlesource.com/chromium/src/+/HEAD/docs/security/faq.md
Please see the following link for instructions on filing security bugs: https://www.chromium.org/Home/chromium-security/reporting-security-bugs
Reports may be eligible for reward payments under the Chrome VRP: https://g.co/chrome/vrp
NOTE: Security bugs are normally made public once a fix has been widely deployed.
VULNERABILITY DETAILS NOTE At Mozilla we are tracking this at https://bugzilla.mozilla.org/show_bug.cgi?id=2029717 . I can CC someone to that bug if so desired.
The VP9 RTP packetizer in libwebrtc passes a size_t payload size to SplitAboutEqually(), which takes a signed int parameter. When a JavaScript-controlled WebRTC encoded frame has a payload of 0xFFFFFFFF bytes (4GB-1), the implicit cast truncates this to int -1. The release-build-only RTC_DCHECK_GT(payload_len, 0) does not catch this, and the subsequent check max_payload_len >= single_packet_reduction_len + (-1) passes trivially, returning a single-element vector containing -1. NextPacket() then reads packet_payload_len = -1, calls AllocatePayload(header_size + (-1)) = AllocatePayload(10) which succeeds with a tiny buffer, and finally calls memcpy(buffer + header_size, src, -1) where -1 implicitly converts to SIZE_MAX, causing a massive heap-buffer-overflow write past the ~1200-byte RTP packet buffer.
The trigger path is fully reachable from web content with default Firefox configuration. The WebRTC Encoded Transform API (media.peerconnection.scripttransform.enabled, default true) allows JavaScript to set frame.data to an arbitrary ArrayBuffer. Firefox supports ArrayBuffers up to 16GB on 64-bit platforms. The data path from RTCEncodedFrameBase::SetData through TransformableVideoSenderFrame, EncodedImageBuffer, RTPSenderVideo::SendVideo, and RtpPacketizer::Create uses size_t throughout — no truncation occurs until the final SplitAboutEqually call.
In production, exploitation requires ~8GB of free memory (4GB for the JS ArrayBuffer plus 4GB for the EncodedImageBuffer copy). The test patch substitutes a 16KB buffer with a faked 4GB size to make the bug observable under ASAN, but the truncation logic and the resulting memcpy(SIZE_MAX) are identical to production behavior.
VERSION Chrome Version: 147 stable Operating System:
REPRODUCTION CASE
FOR CRASHES, PLEASE INCLUDE THE FOLLOWING ADDITIONAL INFORMATION Type of crash: [tab, browser, etc.] Crash State: [see link above: stack trace with symbols, registers, exception record] Client ID (if relevant): [see link above]
CREDIT INFORMATION Externally reported security bugs may appear in Chrome release notes. If this bug is included, how would you like to be credited? Reporter credit: [goes here]