Released February 11, 2026

CVE-2026-20652

CVSS 7.5 severity high CWE-400CWE-120 vector CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

Overview

Impact:

Description: A remote attacker may be able to cause a denial-of-service

Researchers: Nathaniel Oh (@calysteon)

Key insight. A classic unsigned-underflow-to-huge-length bug: an unchecked ‘a - b’ on size_t buffer indices assumed a >= b, and violating that assumption produced a near-SIZE_MAX span length driving an out-of-bounds read. The fix is the canonical WTF::safeSub guard with an empty-result early return.

Patch Details

diff --git a/Source/WebCore/platform/audio/AudioResamplerKernel.cpp b/Source/WebCore/platform/audio/AudioResamplerKernel.cpp
index 4d64f8ff81c3..e5bcb5e1f613 100644
--- a/Source/WebCore/platform/audio/AudioResamplerKernel.cpp
+++ b/Source/WebCore/platform/audio/AudioResamplerKernel.cpp
@@ -31,6 +31,7 @@
 #include "AudioResampler.h"
 #include "AudioUtilities.h"
 #include <algorithm>
+#include <wtf/CheckedArithmetic.h>
 #include <wtf/TZoneMallocInlines.h>
 
 namespace WebCore {
@@ -58,7 +59,10 @@ std::span<float> AudioResamplerKernel::getSourceSpan(size_t framesToProcess, siz
 
     // Determine how many input frames we'll need.
     // We need to fill the buffer up to and including endIndex (so add 1) but we've already buffered m_fillIndex frames from last time.
-    size_t framesNeeded = 1 + endIndex - m_fillIndex;
+    size_t framesNeeded;
+    if (!WTF::safeSub(1 + endIndex, m_fillIndex, framesNeeded))
+        return { };
+
     if (numberOfSourceFramesNeededP)
         *numberOfSourceFramesNeededP = framesNeeded;
 

Background

Advisory: Apple Security Advisory

Attribute Value
CVE CVE-2026-20652
Bugzilla 303959
Component WebCore
Bug Class IntegerOverflow
Severity medium
Commit 7afdc436a98c9771…
Advisory Apple Advisory

Terminology

  • Audio resampler kernel — The per-channel core of WebCore’s sample-rate converter that pulls source frames and produces output frames at a different rate, tracking how much source data is buffered between calls.
  • m_fillIndex / endIndex — m_fillIndex is the count of source frames already buffered from a prior call; endIndex is the highest source frame index needed for the current output block, so 1 + endIndex - m_fillIndex is the additional frames to fetch.
  • Unsigned integer underflow — Subtracting a larger unsigned value from a smaller one wraps around to a huge value near the type maximum instead of producing a negative number, since size_t cannot represent negatives.
  • WTF::safeSub — A WebKit checked-arithmetic helper that performs a subtraction and returns false (writing nothing usable) if the operation overflows/underflows, letting callers bail out safely.
  • std::span — A non-owning (pointer, length) view over contiguous memory; an incorrectly large length lets consumers read past the backing buffer’s bounds.

Root Cause Analysis

AudioResamplerKernel::getSourceSpan() computes how many input (source) frames it must read to produce a requested block of output frames, then returns a std::span<float> over the source buffer describing that region. The frame count was computed as ‘size_t framesNeeded = 1 + endIndex - m_fillIndex;’, where endIndex is the highest source index needed for this output block and m_fillIndex is the number of source frames already buffered from the previous call (the ‘+1’ accounts for filling up to and including endIndex). This arithmetic silently assumes the invariant 1 + endIndex >= m_fillIndex. If that assumption is violated — i.e. m_fillIndex exceeds 1 + endIndex, which can arise from a crafted combination of resampling rate/scale factor, buffer state, and framesToProcess — the subtraction underflows. Because framesNeeded is an unsigned size_t, the underflow wraps to a near-SIZE_MAX value rather than going negative. That gigantic framesNeeded is then written through *numberOfSourceFramesNeededP and used to size/derive the returned source span, so downstream consumers read far past the end of the actual source buffer: an out-of-bounds read whose length is effectively unbounded, producing a crash (denial of service, as the CVE states). The fix adds <wtf/CheckedArithmetic.h> and replaces the raw subtraction with WTF::safeSub(1 + endIndex, m_fillIndex, framesNeeded); if the checked subtraction underflows it returns false and getSourceSpan() returns an empty span (‘return { };’) instead of propagating a wrapped length. This restores the invariant by refusing to produce a source span (and refusing to report a frame count) when the computed need would be negative, converting a would-be OOB access into a benign empty-span early return. The precise upstream conditions that let m_fillIndex exceed endIndex+1 are not in the shown diff (only getSourceSpan is changed), so the exact trigger state is an inference from the arithmetic and the resampler’s buffering model.

Attack Path

1. Reach the resampler from web content

Drive audio through a path that instantiates an AudioResampler — e.g. a Web Audio graph or media element whose source sample rate differs from the output, requiring sample-rate conversion — so AudioResamplerKernel::getSourceSpan() is invoked during rendering.

2. Control resampling parameters and buffering

Choose a playback/resampling ratio (scale factor) and feed frame counts such that the internally tracked m_fillIndex becomes larger than 1 + endIndex for a processing block, e.g. via an extreme or dynamically changed rate.

3. Trigger the underflow

On the next getSourceSpan() call the unsigned subtraction 1 + endIndex - m_fillIndex wraps to a near-SIZE_MAX framesNeeded.

4. Force the OOB read

The oversized framesNeeded is used to size the returned source span (and reported via numberOfSourceFramesNeededP), so the resampling loop reads far beyond the source buffer, dereferencing unmapped/adjacent memory and crashing the process.

Impact Assessment

The primitive is an out-of-bounds read with an attacker-influenced but essentially unbounded length (a wrapped size_t), which reliably faults on unmapped memory and yields a controlled crash — matching the CVE’s denial-of-service classification. It sits in the audio-rendering path within the sandboxed WebContent process (or the media/GPU audio path in configurations where resampling runs there), not the more privileged Network process. While large OOB reads can sometimes be shaped into information disclosure, the diff and the DoS description point to a crash rather than a reliable read primitive, and there is no write, so escalation to memory corruption / RCE from this bug alone is not indicated.

Audit Directions

Other index arithmetic in the same file/class

Review the rest of AudioResamplerKernel.cpp and AudioResampler for other size_t index/offset computations (m_fillIndex, endIndex, frame counts) that subtract or add without CheckedArithmetic, especially any that also feed span sizes or numberOfSourceFramesNeeded.

Unchecked subtraction feeding span/buffer lengths

Grep the platform/audio tree for patterns like ‘size_t <name> = … - …;’ whose result is used as a length, span size, or loop bound, and confirm each is either provably non-negative or wrapped in WTF::safeSub / checkedSum-style helpers.

Codebase-wide length-from-difference pattern

More broadly, search for span/buffer constructions where the length is computed as ’end - start’ or ‘1 + end - fill’ on unsigned types (std::span<…>, makeSpan, subspan, memcpy sizes) and audit for missing underflow checks, using #include <wtf/CheckedArithmetic.h> additions in recent commits as a lead for known-fragile call sites.

Changed Functions

Function File Change Note
AudioResamplerKernel::getSourceSpan Source/WebCore/platform/audio/AudioResamplerKernel.cpp modified Replaced ‘size_t framesNeeded = 1 + endIndex - m_fillIndex;’ with WTF::safeSub(1 + endIndex, m_fillIndex, framesNeeded) and an early ‘return { };’ on underflow, preventing an unsigned wraparound from producing an oversized source span.

Files Changed

Source Files

  • Source/WebCore/platform/audio/AudioResamplerKernel.cpp