Medium CVSS 5.5 webkit OOB 🔧 Commit mapped

Overview

Medium
Severity
5.5
CVSS
No
Exploited ITW
Fixed
Fix Status
DescriptionProcessing maliciously crafted web content may lead to an unexpected process crash
ComponentWebCore WebAudio
Bug ClassOOB
Tracker275273
Fix commit64c9479d6f29 (WebKit/WebKit)
CWECWE-125 (Out-of-bounds read)
CVSS vectorCVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H
CISA KEVNot listed
CreditedHuang Xilin of Ant Group Light-Year Security Lab
Disclosed2024-07-29

Background

AudioBufferSourceNode
A Web Audio node that plays back the samples of an in-memory AudioBuffer, with controllable start offset, playbackRate and detune.
detune / playbackRate
AudioParams that jointly determine the effective sample-read rate; detune is combined exponentially (cents, 2^(detune/1200)) so extreme values can drive the effective rate to zero.
virtualReadIndex
The floating-point cursor tracking the current read position within the source buffer as renderFromBuffer produces output frames.
renderFromBuffer
The per-render-quantum routine on the audio thread that copies/resamples source buffer frames into the destination channels.
Out-of-bounds read
Accessing memory outside the intended bounds of a buffer, here reading a source sample at an index past the channel’s valid frame range.

Root Cause Analysis

The bug is an out-of-bounds read in AudioBufferSourceNode::renderFromBuffer in Source/WebCore/Modules/webaudio/AudioBufferSourceNode.cpp, specifically in the branch taken when the effective playback rate (pitchRate) evaluates to zero (the else if (!pitchRate) path). In that branch the code computes unsigned readIndex = static_cast<unsigned>(virtualReadIndex); and then, for every channel, does std::fill_n(destinationChannels[i] + writeIndex, framesToProcess, sourceChannels[i][readIndex]); — it repeatedly copies a single source sample located at readIndex. The implicit invariant is that readIndex must remain a valid index into the source buffer channel (i.e. readIndex < bufferLength/maxFrame). Before the patch nothing constrained readIndex in this branch: virtualReadIndex is advanced/positioned from the grain start offset and the accumulated playback state, and it can point past the valid frame range (or at maxFrame, the exclusive end), so sourceChannels[i][readIndex] reads out of bounds. The trigger, shown by the added layout test, is a very large negative detune value (detune.value = -0xffffff) combined with a start offset; the detune is applied as an exponential factor to the playback rate, so a large negative detune drives computedPlaybackRate toward zero, and once the rounded pitchRate is exactly 0 this vulnerable zero-rate branch is entered.

The fix reads the frame bounds into locals — int deltaFrames = static_cast<int>(virtualDeltaFrames); and maxFrame = static_cast<unsigned>(virtualMaxFrame); — and clamps: if (readIndex >= maxFrame) readIndex -= deltaFrames;, wrapping readIndex back into the buffer’s valid grain window before the fill, and finally writes the corrected value back with virtualReadIndex = readIndex; so the render loop’s state stays consistent. This restores the invariant that readIndex indexes a valid frame of the source buffer. The precise derivation of virtualReadIndex/virtualDeltaFrames/virtualMaxFrame is not fully shown in the diff (only their use in this branch is), so the exact numeric path by which readIndex exceeded maxFrame is inferred from the surrounding code and the added clamp rather than displayed line-by-line.

Key insight
A degenerate audio configuration — an extreme negative detune that rounds the effective playback rate to exactly zero — routes rendering into a special-case branch that copied a single source sample without validating that its index stayed within the buffer, an unchecked-index invariant that the surrounding non-zero-rate branches enforce but this one did not.

Attack Path

  1. Create an AudioContext and buffer source From attacker-controlled web content, construct new AudioContext(), create a small AudioBuffer (e.g. createBuffer(1, 256, 44100)) and an AudioBufferSourceNode, and assign the buffer to it.
  2. Schedule a grain with an offset Call src.start(undefined, 1) (or similar) so the playback begins at a nonzero offset into the buffer, positioning virtualReadIndex away from the buffer start.
  3. Force pitchRate to zero via detune Set src.detune.value = -0xffffff; because detune is applied as an exponential (2^(detune/1200)) factor to the computed playback rate, an extreme negative detune collapses the effective rate to zero, steering rendering into the !pitchRate branch.
  4. Connect into the graph to force rendering Connect the source into the audio graph (destination/panner) so the audio rendering thread invokes renderFromBuffer and executes the zero-rate fill loop.
  5. Trigger the OOB read With readIndex left unclamped at/beyond maxFrame, sourceChannels[i][readIndex] reads memory past the channel data. In practice this yields an out-of-bounds read of adjacent heap memory, most reliably producing a WebContent-process crash.

Impact Assessment

The primitive is an out-of-bounds read of heap memory (a single source float per channel copied across the output), executed on the WebContent process’s audio rendering path. Consistent with the CVE description and the test’s intent, the realistic outcome is an unexpected process crash rather than a reliable info-leak or memory-corruption chain, since the value read is only used as playback samples and the index is not directly attacker-controlled to an arbitrary offset. It is confined to the sandboxed WebContent process; escalation to code execution from this OOB read alone is not demonstrated and would require additional primitives.

Changed Functions

FunctionChangeNotes
AudioBufferSourceNode::renderFromBuffer
Source/WebCore/Modules/webaudio/AudioBufferSourceNode.cpp
modified In the zero playback-rate (`!pitchRate`) branch, introduces deltaFrames/maxFrame locals and clamps readIndex with `if (readIndex >= maxFrame) readIndex -= deltaFrames;` before the per-channel std::fill_n, and writes the corrected index back to virtualReadIndex to prevent the OOB source read.

Audit Directions

  • Other branches of renderFromBuffer
    Audit each else if branch in renderFromBuffer (the reverse branch, the interpolating resample branch) for the same missing bound: look for any sourceChannels[i][readIndex] / destinationChannels[...] indexing where readIndex/readIndex2 is not compared against maxFrame or bufferLength before use.
  • Degenerate-rate edge cases across Web Audio
    Grep the webaudio module for playback-rate and detune handling (computePlaybackRate, pitchRate, !pitchRate, virtualReadIndex) and check what happens when the rate collapses to 0 or overflows, since zero/near-zero rate is the trigger here.
  • std::fill_n / memcpy with derived indices
    Across WebCore audio and DSP code, grep for std::fill_n( and buffer copies whose source or offset comes from a float-to-unsigned cast of an accumulating cursor; verify a clamp precedes the cast/use.

Original Bug Report

The reporter's bug is still restricted on the tracker.