Medium CVSS 7.5 webkit Logic Error 🔧 Commit mapped

Overview

Medium
Severity
7.5
CVSS
No
Exploited ITW
Fixed
Fix Status
DescriptionProcessing maliciously crafted web content may lead to an unexpected process crash
ComponentWebCore MediaStream
Bug ClassLogic Error
Tracker311131
Fix commit8384c8455e7b (WebKit/WebKit) +76/-1
CWECWE-119 (Buffer bounds error)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
CISA KEVNot listed
CreditedKenneth Hsu of Palo Alto Networks, Jérôme DJOUDER, dr3dd
Disclosed2026-05-11

Background

RTCRtpScriptTransform / Encoded Transform
A WebRTC API letting a page process encoded audio/video RTP frames in a worker via a ReadableStream/WritableStream pair before send or after receive.
RTCEncodedStreamProducer
The WebCore glue that exposes incoming encoded frames as a ReadableStream and takes frames written back via writeFrame to hand to the native transform backend.
RTCRtpTransformBackend
The platform-side object (libwebrtc) that packetizes/decodes transformed frames; it expects frames of the media type it was created for (audio vs video).
Media-type confusion
Supplying a frame of the wrong media type (audio frame to a video pipeline) so native code misinterprets buffer layout/size, a form of type confusion leading to a crash.
WeakPtr
A non-owning smart pointer that automatically becomes null when its target is destroyed, used here so a frame’s remembered transformer can be checked without dangling.

Root Cause Analysis

The bug is in the WebRTC Encoded Transform (RTCRtpScriptTransform) write path in Source/WebCore/Modules/mediastream/RTCEncodedStreamProducer.cpp. A script transformer exposes a WritableStream; frames written into it are converted in RTCEncodedStreamProducer::writeFrame and then handed to the transform backend via transformBackend->processTransformedFrame(rtcFrame.get()). The intended invariant is that a frame written back into a given transformer’s writable must belong to that same transformer and match its media type (audio backend gets audio frames, video backend gets video frames). Before the patch nothing enforced this: script could obtain an RTCEncodedAudioFrame/RTCEncodedVideoFrame object produced by one transformer (e.g. the audio sender) and write() it into a different transformer’s writable (e.g. the video sender), or write a frame that originated from an unrelated transformer. writeFrame would still call processTransformedFrame on a backend whose media type or origin did not match the frame, causing the native backend to interpret an audio frame as video data (or use a frame tied to a foreign/torn-down transformer) and crash.

The fix computes the actual frame type (bool isVideo, set true in the video switchOn branch) and adds the guard ‘if (m_isVideo != isVideo || (m_hasTransformer && !rtcFrame->isFromTransformer(m_transformer.get()))) return { };’, silently dropping any frame whose media type does not match the producer or that did not originate from this producer’s transformer. To support that check the producer now records its owning transformer: start() gains an RTCRtpScriptTransformer* parameter (passed as ’this’ from RTCRtpScriptTransformer::start), stored as m_hasTransformer/m_transformer, and enqueueFrame stamps each outgoing frame via frame->setTransformer(m_transformer). RTCRtpTransformableFrame is hardened in parallel: setTransformer now takes a WeakPtr<RTCRtpScriptTransformer> (rather than a reference) and isFromTransformer takes a raw pointer, so a frame’s remembered transformer is a weak reference that safely reads null if the transformer was destroyed, preventing a stale comparison. The added WPT test writes audio frames on the video sender and vice-versa and expects the operation to be safely ignored (PASS) rather than crashing.

Key insight
The write-back path of an encoded-media transform trusted script to return only well-typed frames belonging to that transformer; the fix enforces that trust boundary by tagging each frame with its originating transformer and rejecting any written frame whose media type or origin does not match.

Attack Path

  1. Install two script transforms Malicious page sets up RTCRtpScriptTransform on two RTCRtpSenders (or a sender and receiver) of different media types — e.g. an audio track and a video track — so it holds two transformers with distinct backends.
  2. Capture frames from both readables In each transformer’s transform worker, read() a chunk (an RTCEncodedAudioFrame from the audio side, an RTCEncodedVideoFrame from the video side) and stash the frame objects in shared variables.
  3. Cross-write mismatched frames Write the captured video frame into the audio transformer’s writable (or the audio frame into the video transformer’s writable), or write a frame belonging to a different transformer, coordinating timing between the workers as the test harness does.
  4. Reach the unchecked backend call Pre-patch writeFrame performs no type/origin check and calls transformBackend->processTransformedFrame with the mismatched frame.
  5. Trigger the crash The native RTCRtpTransformBackend processes an audio payload as video (or a frame tied to a foreign/destroyed transformer), leading to an unexpected process crash (denial of service).

Impact Assessment

The commit establishes a missing validation that allowed a cross-type/cross-origin frame to reach the native transform backend; the CVE describes the realistic result as an unexpected process crash (denial of service), consistent with a controlled crash rather than a demonstrated read/write primitive. The processing happens in the WebContent process’s WebRTC stack, so any corruption or crash is confined to the sandboxed renderer. The WeakPtr change additionally forecloses a stale/dangling transformer comparison; whether the underlying media-type mismatch could be pushed beyond a crash toward memory corruption is not shown by the diff and would be inference, but the guarded call site (processTransformedFrame on a mistyped buffer) is the kind of primitive that can sometimes be escalated. Severity medium, no sandbox escape indicated.

Changed Functions

FunctionChangeNotes
RTCEncodedStreamProducer::start
Source/WebCore/Modules/mediastream/RTCEncodedStreamProducer.cpp
modified Adds an RTCRtpScriptTransformer* parameter and records m_hasTransformer/m_transformer so written frames can be validated against the owning transformer.
RTCEncodedStreamProducer::enqueueFrame
Source/WebCore/Modules/mediastream/RTCEncodedStreamProducer.cpp
modified Stamps each outgoing frame with frame->setTransformer(m_transformer) so a later write-back can be recognized as originating from this transformer.
RTCEncodedStreamProducer::writeFrame
Source/WebCore/Modules/mediastream/RTCEncodedStreamProducer.cpp
modified Computes the frame's real media type and adds the guard rejecting frames whose isVideo mismatches or that did not come from this transformer, returning early before processTransformedFrame.
RTCRtpScriptTransformer::start
Source/WebCore/Modules/mediastream/RTCRtpScriptTransformer.cpp
modified Passes 'this' to m_streamProducer->start so the producer knows its owning transformer.
RTCRtpTransformableFrame::setTransformer
Source/WebCore/Modules/mediastream/RTCRtpTransformableFrame.h
modified Signature changed to take WeakPtr<RTCRtpScriptTransformer> (was a reference), storing a weak reference to the originating transformer.
RTCRtpTransformableFrame::isFromTransformer
Source/WebCore/Modules/mediastream/RTCRtpTransformableFrame.h
modified Signature changed to take a raw pointer and compares against the weak m_transformer, safely handling a destroyed transformer.

Files Changed

  • LayoutTests/http/wpt/webrtc/audio-video-transform.js
  • LayoutTests/http/wpt/webrtc/audiovideo-script-transform-expected.txt
  • LayoutTests/http/wpt/webrtc/audiovideo-script-transform.html
  • Source/WebCore/Modules/mediastream/RTCEncodedStreamProducer.cpp
  • Source/WebCore/Modules/mediastream/RTCEncodedStreamProducer.h
  • Source/WebCore/Modules/mediastream/RTCRtpScriptTransformer.cpp
  • Source/WebCore/Modules/mediastream/RTCRtpTransformableFrame.h

Audit Directions

  • Other frame entry points in RTCEncodedStreamProducer
    Verify every path that reaches processTransformedFrame or enqueue validates media type and transformer origin; grep the file for ‘processTransformedFrame’, ’m_isVideo’, and ‘isFromTransformer’.
  • Frame provenance stamping completeness
    Confirm all producers/backends set a transformer before frames become script-visible so m_hasTransformer is never falsely false; grep for ‘setTransformer(’ and its callers, and check RTCRtpTransformBackend subclasses.
  • Reference-to-WeakPtr hardening variants
    Look for other cross-object ‘setX(Foo&)’ patterns in the mediastream/webrtc modules that store a reference to a script-controllable object and could dangle; grep for ‘WeakPtr<’ members initialized from references and ASSERT(!m_…) reset patterns.
  • Type-tag validation on other transform pipelines
    Audit analogous ReadableStream/WritableStream transform bridges (e.g. WebCodecs, MediaStreamTrack processors) for missing type/origin checks when script writes objects back into a native sink; grep for ‘writeFrame’, ‘switchOn’, and ‘MediaType::Video’.
diff --git a/LayoutTests/http/wpt/webrtc/audio-video-transform.js b/LayoutTests/http/wpt/webrtc/audio-video-transform.js
index 46f520b50530..565591b39f12 100644
--- a/LayoutTests/http/wpt/webrtc/audio-video-transform.js
+++ b/LayoutTests/http/wpt/webrtc/audio-video-transform.js
@@ -1,3 +1,7 @@
+var audioSenderTransformer, videoSenderTransformer;
+var audioReceiverTransformer, videoReceiverTransformer;
+var audioChunk, videoChunk;
+
 class AudioVideoRTCRtpTransformer {
     constructor(transformer) {
         this.askKeyFrame = false;
@@ -17,7 +21,24 @@ class AudioVideoRTCRtpTransformer {
                 this.tryAccessingDataTwice = true;
             else if (event.data === "tryAccessingMetadata")
                 this.tryAccessingMetadata = true;
+            else if (event.data === "tryWritingAudio")
+                this.tryWritingAudio = true;
+            else if (event.data === "tryWritingVideo")
+                this.tryWritingVideo = true;
         };
+
+        if (this.context.options.side === "sender") {
+            if (this.context.options.mediaType === "audio")
+                audioSenderTransformer = this;
+            else if (this.context.options.mediaType === "video")
+                videoSenderTransformer = this;
+        } else {
+            if (this.context.options.mediaType === "audio")
+                audioReceiverTransformer = this;
+            else if (this.context.options.mediaType === "video")
+                videoReceiverTransformer = this;
+        }
+
         this.start();
     }
     start()
@@ -29,10 +50,98 @@ class AudioVideoRTCRtpTransformer {
 
     process()
     {
-        this.reader.read().then(chunk => {
+        this.reader.read().then(async chunk => {
             if (chunk.done)
                 return;
 
+            if (audioSenderTransformer && audioSenderTransformer.tryWritingVideo) {
+                if (audioSenderTransformer === this) {
+                    this.writer.write(chunk.value);
+                    if (videoChunk !== undefined) {
+                       this.writer.write(videoChunk.value);
+                       audioSenderTransformer.tryWritingVideo = false;
+                       this.context.options.port.postMessage("PASS");
+                    }
+                    this.process();
+                    return;
+                }
+                if(videoSenderTransformer === this) {
+                    videoChunk = chunk;
+                    while (audioSenderTransformer.tryWritingVideo)
+                        await new Promise(resolve => setTimeout(resolve, 50));
+                    videoSenderTransformer.writer.write(videoChunk);
+                    videoChunk = undefined;
+                    this.process();
+                    return;
+                }
+            }
+
+            if (videoSenderTransformer && videoSenderTransformer.tryWritingAudio) {
+                if (videoSenderTransformer === this) {
+                    this.writer.write(chunk.value);
+                    if (audioChunk !== undefined) {
+                       this.writer.write(audioChunk.value);
+                       videoSenderTransformer.tryWritingAudio = false;
+                       this.context.options.port.postMessage("PASS");
+                    }
+                    this.process();
+                    return;
+                }
+                if(audioSenderTransformer === this) {
+                    audioChunk = chunk;
+                    while (videoSenderTransformer.tryWritingAudio)
+                        await new Promise(resolve => setTimeout(resolve, 50));
+                    audioSenderTransformer.writer.write(audioChunk);
+                    audioChunk = undefined;
+                    this.process();
+                    return;
+                }
+            }
+
+            if (audioSenderTransformer && audioSenderTransformer.tryWritingAudio) {
+                if (audioSenderTransformer === this) {
+                    this.writer.write(chunk.value);
+                    if (audioChunk !== undefined) {
+                       this.writer.write(audioChunk.value);
+                       audioSenderTransformer.tryWritingAudio = false;
+                       this.context.options.port.postMessage("PASS");
+                    }
+                    this.process();
+                    return;
Loading diff…

Original Bug Report

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