← WebKit — 2026-W35

c2c8b0cb3b0def5b71c991462d7b3714c46a8d62  AudioBufferSourceNode does not clamp out-of-range loopStart/loopEnd

severity medium class OOB-read confidence 0.65 signal score 10.38 Source/WebCore/Modules patch-now
Primitive: web-controlled AudioBuffer loop points produce an inverted range / out-of-range virtualReadIndex
Triage note: renderFromBuffer now clamps loopStart/loopEnd into the buffer before validating loopStart < loopEnd and sets m_virtualReadIndex = virtualMinFrame, replacing the old logic that could compute an inverted range from out-of-range m_loopStart/m_loopEnd set from JS. The comment states clamping-before-validating keeps an out-of-range endpoint from producing an inverted range, i.e. this fixes an OOB read of the audio buffer reachable via the Web Audio API.
Structural signals: null_guard (+1.2), clamp_saturate (+3.88), msg_strong (+3.0), msg_soft (+1.0), surgical (+1.3)

Commit message

AudioBufferSourceNode does not clamp out-of-range loopStart/loopEnd
https://bugs.webkit.org/show_bug.cgi?id=322372

Reviewed by Jean-Yves Avenard and Darin Adler.

renderFromBuffer() rejected out-of-range loop points and fell back to looping the
entire buffer, which is what the spec's playback algorithm says. But the
loopStart/loopEnd attribute definitions say those values are clamped into the
buffer instead, and the two readings give different output. The Audio WG resolved
the contradiction in favour of clamping in
https://github.com/WebAudio/web-audio-api/issues/2689: clamp both endpoints
first, then fall back to the entire buffer only if what is left is not a range of
positive length. The spec PR has not landed yet, so index.bs still shows the old
guard. Blink already does this in
AudioBufferSourceHandler::UpdateEffectiveLoopPoints(), and we now match it for a
negative loopStart, a loopStart past the buffer duration, loopStart >= loopEnd, a
negative loopEnd, a loopEnd past the duration, and the loopStart == loopEnd == 0
default.

Two configurations change. A negative loopStart looped the whole buffer and now
loops [0, loopEnd), the case the WPT test already asserted. A loopStart past the
buffer duration used to *pass* the old guard, since 0 <= loopStart < loopEnd
holds, leaving virtualMinFrame above virtualMaxFrame and a negative
virtualDeltaFrames; the playback-rate check then bailed out and rendered silence.
Clamping makes that range empty rather than inverted, so it falls back to the
whole buffer.

The forward wrap recomputed its target from the raw m_loopStart rather than the
effective loop points, so it wrapped to loopStart even when playback had fallen
back to the whole buffer. It now uses virtualMinFrame.

One difference from Blink remains, unchanged by this patch: Blink resolves a
loopEnd of exactly 0 to the buffer duration and loops [loopStart, duration),
while the resolution maps any loopEnd <= 0 to 0 and falls back to the entire
buffer.

* LayoutTests/imported/w3c/web-platform-tests/webaudio/the-audio-api/the-audiobuffersourcenode-interface/audiobuffersource-playbackrate-negative-expected.txt:
Rebaselined: "loops backwards using clamped loopStart when loopStart < 0" passes.
The two remaining failures are out-of-bounds start offsets, unrelated.

* LayoutTests/webaudio/AudioBufferSource/audiobuffersource-loop-comprehensive.html:
* LayoutTests/webaudio/AudioBufferSource/audiobuffersource-loop-comprehensive-expected.txt:
Case 14 expected loopStart = -8 with loopEnd = 3 to loop the whole buffer,
contradicting the WPT test. Updated to the clamped loop [0, 3), matching
Chromium's updated copy of this test.

* Source/WebCore/Modules/webaudio/AudioBufferSourceNode.cpp:
(WebCore::AudioBufferSourceNode::renderFromBuffer):

Canonical link: https://commits.webkit.org/319755@main

Before / after

Loading diff…