Overview

Medium
Severity
4.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInteger overflow in WebRTC
DescriptionInteger overflow in WebRTC
ComponentWebRTC
Bug ClassInteger Overflow
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable
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.
Tracker502783118
Fix commit424a6bd0b7f9 (src) +46/-14
CWECWE-190 (Integer overflow)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:N/A:N
CISA KEVNot listed
Creditedngrunbaum
Reported2026-04-15 (per vendor advisory)
Fix landed2026-08-06 (public git)
Disclosed2026-09-08
Patch gap33 days — fix public in source before the advisory

Background

`RtpPacketizer`
WebRTC helper that splits an encoded video frame payload into MTU-sized RTP packets for a given codec format.
`RtpPacketizer::Create`
Factory that dispatches to a codec-specific packetizer (kRaw, kH264, kVp8, kVp9, kAV1, kGeneric) based on the PacketizationFormat.
`SplitAboutEqually`
Utility that divides a payload_size into near-equal packet lengths and casts it to int payload_len for the arithmetic.
`SafeGt`
WebRTC safe-comparison primitive that compares two integers without triggering an overflow or signedness bug.

Root Cause Analysis

Before the fix, the oversized-frame guard (payload_size == 0 || SafeGt(payload_size, limits.max_payload_len * 0x7000)) lived inside SplitAboutEqually, so RtpPacketizer::Create first selected and constructed a codec-specific packetizer and only later reached the size check. This let an unrealistically large payload flow into codec-specific packetization logic that performs its own size arithmetic, where the static_cast<int>(payload_size) and per-packet length math could overflow a signed 32-bit int, violating the invariant that a frame never occupies close to half the RTP sequence-number space.

The fix hoists the identical check to the top of RtpPacketizer::Create, rejecting empty or oversized payloads (>~34MB at ~1.2KB MTU) before any codec path runs. Because callers require a non-null result, an oversized input now returns a benign EmptyRtpPacketizer (via the absl_nonnull return type) that produces zero packets. This protects all packetizer implementations, not just SplitAboutEqually, from the overflow-prone arithmetic.

Key insight
The single mistake was placing the oversized-payload bound check deep inside one helper (SplitAboutEqually) instead of at the RtpPacketizer::Create entry point, so other codec paths reached unchecked integer arithmetic; the fix moves the check earlier so no packetizer ever sees a frame large enough to overflow its length math.

Attack Path

  1. Establish a media session A peer negotiates a WebRTC connection where the local endpoint will encode and packetize outgoing video.
  2. Produce an oversized frame A malformed or adversarial encoder/source yields an encoded video frame whose payload approaches or exceeds the max_payload_len * 0x7000 bound.
  3. Reach the vulnerable factory The frame is handed to RtpPacketizer::Create, which (pre-fix) constructs a codec-specific packetizer before the size guard runs.
  4. Trigger the overflow Codec-specific length arithmetic and the static_cast<int>(payload_size) overflow signed 32-bit int, yielding corrupt packet-length computations.

Impact Assessment

An attacker who can drive an oversized video payload into the packetizer can cause an integer overflow in the RTP packetization arithmetic, leading to incorrect length computations and potential memory-safety consequences within the process handling WebRTC media (the renderer or a media/network utility process). Exploitation requires an active WebRTC session and the ability to supply a frame near the ~34MB bound, which the commit notes is impractical for legitimate use. Severity is rated medium, consistent with a hard-to-reach integer-overflow condition rather than a direct remote code-execution primitive.

Changed Functions

FunctionChangeNotes
EmptyRtpPacketizer
modules/rtp_rtcp/source/rtp_format.cc
modified
switch
modules/rtp_rtcp/source/rtp_format.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_format_unittest.cc
  • modules/rtp_rtcp/source/rtp_format.h
  • modules/rtp_rtcp/BUILD.gn

Audit Directions

  • Validate-before-dispatch
    Audit other factory/Create functions that construct codec- or format-specific objects before validating input size, and move bound checks to the earliest entry point.
  • Narrowing casts on attacker-influenced sizes
    Flag static_cast<int> (and similar narrowing) applied to size_t payload lengths in media pipelines, ensuring an upstream SafeGt/range check precedes them.
  • Nullability contracts
    Review functions changed to absl_nonnull for callers that previously assumed a nullable result, and confirm rejection paths return a safe sentinel (like EmptyRtpPacketizer) rather than null.
From 424a6bd0b7f93659204ecccff1d61d63c25937e2 Mon Sep 17 00:00:00 2001
From: Danil Chapovalov <danilchap@webrtc.org>
Date: Wed, 05 Aug 2026 18:47:37 +0200
Subject: [PATCH] Move oversized payload size check earlier in rtp packetizer

To protect more code from unrealistic video frames

Bug: chromium:502783118
Change-Id: I19cbbf84237e93346e515eb3670d561e1bdcefa9
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/494100
Reviewed-by: Åsa Persson <asapersson@webrtc.org>
Commit-Queue: Danil Chapovalov <danilchap@webrtc.org>
Cr-Commit-Position: refs/heads/main@{#48300}
---
diff --git a/modules/rtp_rtcp/source/rtp_format.cc b/modules/rtp_rtcp/source/rtp_format.cc
index 75d00b1..326a1b7 100644
--- a/modules/rtp_rtcp/source/rtp_format.cc
+++ b/modules/rtp_rtcp/source/rtp_format.cc
@@ -16,6 +16,7 @@
 #include <span>
 #include <vector>
 
+#include "absl/base/nullability.h"
 #include "modules/rtp_rtcp/source/rtp_format_h264.h"
 #include "modules/rtp_rtcp/source/rtp_format_video_generic.h"
 #include "modules/rtp_rtcp/source/rtp_format_vp8.h"
@@ -32,13 +33,28 @@
 #endif
 
 namespace webrtc {
+namespace {
+class EmptyRtpPacketizer : public RtpPacketizer {
+ public:
+  size_t NumPackets() const override { return 0; }
+  bool NextPacket(RtpPacketToSend* packet) override { return false; }
+};
+}  // namespace
 
-std::unique_ptr<RtpPacketizer> RtpPacketizer::Create(
+absl_nonnull std::unique_ptr<RtpPacketizer> RtpPacketizer::Create(
     PacketizationFormat format,
     std::span<const uint8_t> payload,
     PayloadSizeLimits limits,
     // Codec-specific details.
     const RTPVideoHeader& rtp_video_header) {
+  if (payload.empty() ||
+      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 std::make_unique<EmptyRtpPacketizer>();
+  }
+
   using enum PacketizationFormat;
   switch (format) {
     case kRaw: {
@@ -87,13 +103,6 @@
   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 >=
diff --git a/modules/rtp_rtcp/source/rtp_format.h b/modules/rtp_rtcp/source/rtp_format.h
index 214594b..193387d 100644
--- a/modules/rtp_rtcp/source/rtp_format.h
+++ b/modules/rtp_rtcp/source/rtp_format.h
@@ -18,6 +18,7 @@
 #include <span>
 #include <vector>
 
+#include "absl/base/nullability.h"
 #include "modules/rtp_rtcp/source/rtp_video_header.h"
 
 namespace webrtc {
@@ -43,7 +44,7 @@
     kVP9,
     kAV1,
   };
-  static std::unique_ptr<RtpPacketizer> Create(
+  static absl_nonnull std::unique_ptr<RtpPacketizer> Create(
       PacketizationFormat format,
       std::span<const uint8_t> payload,
       PayloadSizeLimits limits,
diff --git a/modules/rtp_rtcp/source/rtp_format_unittest.cc b/modules/rtp_rtcp/source/rtp_format_unittest.cc
index 4cb5153..e32ec1a 100644
--- a/modules/rtp_rtcp/source/rtp_format_unittest.cc
+++ b/modules/rtp_rtcp/source/rtp_format_unittest.cc
@@ -10,9 +10,13 @@
 
 #include "modules/rtp_rtcp/source/rtp_format.h"
 
+#include <cstdint>
+#include <memory>
 #include <vector>
 
 #include "absl/algorithm/container.h"
+#include "api/video/video_frame_type.h"
+#include "modules/rtp_rtcp/source/rtp_video_header.h"
 #include "test/gmock.h"
 #include "test/gtest.h"
 
@@ -25,6 +29,7 @@
 using ::testing::IsEmpty;
 using ::testing::Le;
 using ::testing::Not;
+using ::testing::NotNull;
 using ::testing::SizeIs;
 
 // Calculate difference between largest and smallest packets respecting sizes
@@ -231,19 +236,33 @@
   EXPECT_THAT(RtpPacketizer::SplitAboutEqually(20, limits), ElementsAre(9, 11));
 }
 
-TEST(RtpPacketizerSplitAboutEqually, RejectsZeroSize) {
+TEST(RtpPacketizerTest, RejectsZeroSize) {
   RtpPacketizer::PayloadSizeLimits limits;
   limits.max_payload_len = 1200;
+  RTPVideoHeader video_header;
+  video_header.frame_type = VideoFrameType::kVideoFrameKey;
 
-  EXPECT_THAT(RtpPacketizer::SplitAboutEqually(0, limits), IsEmpty());
+  std::unique_ptr<RtpPacketizer> packetizer =
+      RtpPacketizer::Create(RtpPacketizer::PacketizationFormat::kGeneric,
+                            /*payload=*/{}, limits, video_header);
+
+  ASSERT_THAT(packetizer, NotNull());
+  EXPECT_EQ(packetizer->NumPackets(), 0u);
 }
 
-TEST(RtpPacketizerSplitAboutEqually, RejectsHugeSize) {
+TEST(RtpPacketizerTest, RejectsHugeSize) {
   RtpPacketizer::PayloadSizeLimits limits;
   limits.max_payload_len = 1200;
+  RTPVideoHeader video_header;
+  video_header.frame_type = VideoFrameType::kVideoFrameKey;
+  const uint8_t kPayload[40'000'000] = {};
 
-  EXPECT_THAT(RtpPacketizer::SplitAboutEqually(0xFFFF'FFFF, limits), IsEmpty());
-  EXPECT_THAT(RtpPacketizer::SplitAboutEqually(40'000'000, limits), IsEmpty());
+  std::unique_ptr<RtpPacketizer> packetizer =
+      RtpPacketizer::Create(RtpPacketizer::PacketizationFormat::kGeneric,
+                            kPayload, limits, video_header);
+
+  ASSERT_THAT(packetizer, NotNull());
+  EXPECT_EQ(packetizer->NumPackets(), 0u);
 }
 
 TEST(RtpPacketizerSplitAboutEqually, RejectsZeroMaxPayloadLen) {
diff --git a/modules/rtp_rtcp/BUILD.gn b/modules/rtp_rtcp/BUILD.gn
index d5e1bb9..365cb37 100644
--- a/modules/rtp_rtcp/BUILD.gn
+++ b/modules/rtp_rtcp/BUILD.gn
@@ -355,6 +355,7 @@
     "../video_coding:codec_globals_headers",
     "//third_party/abseil-cpp/absl/algorithm:container",
     "//third_party/abseil-cpp/absl/base:core_headers",
+    "//third_party/abseil-cpp/absl/base:nullability",
     "//third_party/abseil-cpp/absl/container:inlined_vector",
     "//third_party/abseil-cpp/absl/functional:any_invocable",
     "//third_party/abseil-cpp/absl/memory",
@@ -1248,6 +1249,8 @@
     deps = [
       ":rtp_format_vp8_test_helper",
       ":rtp_rtcp",
+      ":rtp_video_header",
+      "../../api/video:video_frame_type",
       "../../test:test_support",
       "//third_party/abseil-cpp/absl/algorithm:container",
     ]
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 4cb5153..e32ec1a 100644
--- a/modules/rtp_rtcp/source/rtp_format_unittest.cc
+++ b/modules/rtp_rtcp/source/rtp_format_unittest.cc
@@ -10,9 +10,13 @@
 
 #include "modules/rtp_rtcp/source/rtp_format.h"
 
+#include <cstdint>
+#include <memory>
 #include <vector>
 
 #include "absl/algorithm/container.h"
+#include "api/video/video_frame_type.h"
+#include "modules/rtp_rtcp/source/rtp_video_header.h"
 #include "test/gmock.h"
 #include "test/gtest.h"
 
@@ -25,6 +29,7 @@
 using ::testing::IsEmpty;
 using ::testing::Le;
 using ::testing::Not;
+using ::testing::NotNull;
 using ::testing::SizeIs;
 
 // Calculate difference between largest and smallest packets respecting sizes
@@ -231,19 +236,33 @@
   EXPECT_THAT(RtpPacketizer::SplitAboutEqually(20, limits), ElementsAre(9, 11));
 }
 
-TEST(RtpPacketizerSplitAboutEqually, RejectsZeroSize) {
+TEST(RtpPacketizerTest, RejectsZeroSize) {
   RtpPacketizer::PayloadSizeLimits limits;
   limits.max_payload_len = 1200;
+  RTPVideoHeader video_header;
+  video_header.frame_type = VideoFrameType::kVideoFrameKey;
 
-  EXPECT_THAT(RtpPacketizer::SplitAboutEqually(0, limits), IsEmpty());
+  std::unique_ptr<RtpPacketizer> packetizer =
+      RtpPacketizer::Create(RtpPacketizer::PacketizationFormat::kGeneric,
+                            /*payload=*/{}, limits, video_header);
+
+  ASSERT_THAT(packetizer, NotNull());
+  EXPECT_EQ(packetizer->NumPackets(), 0u);
 }
 
-TEST(RtpPacketizerSplitAboutEqually, RejectsHugeSize) {
+TEST(RtpPacketizerTest, RejectsHugeSize) {
   RtpPacketizer::PayloadSizeLimits limits;
   limits.max_payload_len = 1200;
+  RTPVideoHeader video_header;
+  video_header.frame_type = VideoFrameType::kVideoFrameKey;
+  const uint8_t kPayload[40'000'000] = {};
 
-  EXPECT_THAT(RtpPacketizer::SplitAboutEqually(0xFFFF'FFFF, limits), IsEmpty());
-  EXPECT_THAT(RtpPacketizer::SplitAboutEqually(40'000'000, limits), IsEmpty());
+  std::unique_ptr<RtpPacketizer> packetizer =
+      RtpPacketizer::Create(RtpPacketizer::PacketizationFormat::kGeneric,
+                            kPayload, limits, video_header);
+
+  ASSERT_THAT(packetizer, NotNull());
+  EXPECT_EQ(packetizer->NumPackets(), 0u);
 }
 
 TEST(RtpPacketizerSplitAboutEqually, RejectsZeroMaxPayloadLen) {
Loading diff…

Original Bug Report

reported by ng...@mozilla.com

Heap-buffer-overflow read in [@ webrtc::RtpPacketizerAv1::NextPacket] via signed int overflow in PacketizeInternal

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=2029449 . I can CC someone to that bug if so desired.

RtpPacketizerAv1::PacketizeInternal contains a fragmentation loop whose condition obu_offset + limits.max_payload_len < obu.size is computed entirely in signed int. Both Obu::size and PayloadSizeLimits::max_payload_len are declared int, and obu.size is set to 1 + payload.size() with no upper bound check during ParseObus. When a single OBU’s payload approaches INT_MAX bytes, the loop’s obu_offset climbs until obu_offset + max_payload_len overflows to a negative value, which compares as less than obu.size, allowing the loop body to execute one extra iteration. This emplaces a Packet whose first_obu_offset points one byte before the end of the payload buffer but whose last_obu_size is the full ~1167-byte fragment size.

When RTPSenderVideo::SendVideo later calls NextPacket on this corrupt Packet, the memcpy at line 445 computes payload_offset = first_obu_offset - 1 (which is the last valid index) and copies last_obu_size bytes from there, reading 1166 bytes of adjacent heap memory past the EncodedImageBuffer. The DCHECK guards at lines 390-394 do not catch this because they only verify first_obu_offset < obu.size and last_obu_size <= obu.size, both of which hold individually.

This is reachable from unprivileged web content using only default-enabled features. The W3C RTCRtpScriptTransform API allows a worker to intercept encoded video frames and replace their data via the RTCEncodedVideoFrame.data setter, which accepts ArrayBuffers up to INT32_MAX bytes. The replaced data flows directly through RTPSenderVideoFrameTransformerDelegate::SendVideo into RtpPacketizer::Create (rtp_format.cc:60), which dispatches the AV1 codec to RtpPacketizerAv1 with no size validation. The OOB-read bytes are written into an RtpPacketToSend buffer that is queued for transmission to the remote WebRTC peer, turning this into a remote heap memory disclosure when the peer is attacker-controlled.

VERSION Chrome Version: https://issues.webrtc.org/issues/42221125 Operating System:

REPRODUCTION CASE See attached test.html

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