Medium watch ~ plausible CVSS 8.8 chrome OOB 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactBuffer overflow in WebRTC
DescriptionBuffer overflow in WebRTC
ComponentWebRTC
Bug ClassOOB
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable — upheld by independent review by AI
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Independent review by AIPlausible
Reviewer notes: 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.
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker503013378
Fix commit190a7606321b (src) +42/-5
CWECWE-122 (Heap buffer overflow)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
Creditedngrunbaum
Reported2026-04-15 (per vendor advisory)
Fix landed2026-07-23 (public git)
Disclosed2026-08-25
Patch gap33 days — fix public in source before the advisory

Background

RTP packetizer
WebRTC code (RtpPacketizer and its codec-specific subclasses) that splits one encoded video frame into a sequence of RTP packet payloads that each fit within PayloadSizeLimits.
`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_len plus first-, last- and single-packet reduction lengths, all int, 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

This analysis is consistent with the fixing commit but not fully provable from it: part of the vulnerable path lives in code the fix commit does not touch. Confirm the trigger and the sink against the source before relying on it for exploit development.
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.

Key insight
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

  1. 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).
  2. Packetize the frame The video sender creates a codec packetizer, which calls RtpPacketizer::SplitAboutEqually with the frame size. Before the patch, that size was implicitly narrowed to int with no release-mode check.
  3. 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).
  4. 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

The advisory reports a buffer overflow in WebRTC. In Chrome the WebRTC send pipeline runs in the renderer, so memory corruption there would be inside the sandboxed renderer process (inferred). The preconditions are an active WebRTC video send session plus a way to feed an abnormally large encoded frame into the packetizer. Whether ordinary web content can supply such a frame, for example through encoded-frame transforms, is not established by the patch (inferred). The vendor advisory rates this medium severity and does not report in-the-wild exploitation; NVD scores it CVSS 8.8. Any escalation to code execution is (inferred).

Changed Functions

FunctionChangeNotes
if
modules/rtp_rtcp/source/rtp_sender_video.cc
modified
TEST
modules/rtp_rtcp/source/rtp_format_unittest.cc
modified

Files Changed

  • modules/rtp_rtcp/source/rtp_format.cc
  • modules/rtp_rtcp/source/rtp_sender_video.h
  • modules/rtp_rtcp/source/rtp_format_unittest.cc
  • modules/rtp_rtcp/source/rtp_sender_video.cc
  • modules/rtp_rtcp/source/rtp_format.h
  • modules/rtp_rtcp/BUILD.gn

Audit Directions

  • `size_t` to `int` narrowing at length boundaries
    Look 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 as int or do int arithmetic on frame sizes without an explicit upper bound first.
  • Debug-only validation of untrusted sizes
    Search WebRTC for RTC_DCHECK that 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 assumptions
    Review 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 * 0x7000 product itself, if max_payload_len can ever be unexpectedly large.
From 190a7606321b53c861142f97fc39f679b295f31c Mon Sep 17 00:00:00 2001
From: Danil Chapovalov <danilchap@webrtc.org>
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 <tommi@webrtc.org>
Commit-Queue: Danil Chapovalov <danilchap@webrtc.org>
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",
Loading diff…

Regression Test / PoC

shipped with the fix
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;
Loading diff…

Original Bug Report

reported by ng...@mozilla.com

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]

View on issue tracker