CVE-2026-87630
Overview
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 thePacketizationFormat. - `SplitAboutEqually`
- Utility that divides a
payload_sizeinto near-equal packet lengths and casts it toint payload_lenfor 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.
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
- Establish a media session A peer negotiates a WebRTC connection where the local endpoint will encode and packetize outgoing video.
- Produce an oversized frame
A malformed or adversarial encoder/source yields an encoded video frame whose payload approaches or exceeds the
max_payload_len * 0x7000bound. - Reach the vulnerable factory
The frame is handed to
RtpPacketizer::Create, which (pre-fix) constructs a codec-specific packetizer before the size guard runs. - Trigger the overflow
Codec-specific length arithmetic and the
static_cast<int>(payload_size)overflow signed 32-bitint, yielding corrupt packet-length computations.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
EmptyRtpPacketizermodules/rtp_rtcp/source/rtp_format.cc |
modified | |
switchmodules/rtp_rtcp/source/rtp_format.cc |
modified | |
TESTmodules/rtp_rtcp/source/rtp_format_unittest.cc |
modified |
Files Changed
modules/rtp_rtcp/BUILD.gnmodules/rtp_rtcp/source/rtp_format.ccmodules/rtp_rtcp/source/rtp_format.hmodules/rtp_rtcp/source/rtp_format_unittest.cc
Audit Directions
- Validate-before-dispatchAudit other factory/
Createfunctions 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 sizesFlag
static_cast<int>(and similar narrowing) applied tosize_tpayload lengths in media pipelines, ensuring an upstreamSafeGt/range check precedes them. - Nullability contractsReview functions changed to
absl_nonnullfor callers that previously assumed a nullable result, and confirm rejection paths return a safe sentinel (likeEmptyRtpPacketizer) rather than null.
Patch
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/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",
]
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) {
Regression Test / PoC
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) {