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 <[email protected]> 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 <[email protected]> Commit-Queue: Danil Chapovalov <[email protected]> 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) {