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
Sub-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
AudioVideoRTCRtpTransformer
LayoutTests/http/wpt/webrtc/audio-video-transform.js
modified
constructor
LayoutTests/http/wpt/webrtc/audio-video-transform.js
modified
if
LayoutTests/http/wpt/webrtc/audio-video-transform.js
modified

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/Source/WebCore/Modules/mediastream/RTCEncodedStreamProducer.cpp b/Source/WebCore/Modules/mediastream/RTCEncodedStreamProducer.cpp
index 54b8b1e78736..fd0867b4ab9f 100644
--- a/Source/WebCore/Modules/mediastream/RTCEncodedStreamProducer.cpp
+++ b/Source/WebCore/Modules/mediastream/RTCEncodedStreamProducer.cpp
@@ -38,6 +38,7 @@
 #include "JSRTCEncodedAudioFrame.h"
 #include "JSRTCEncodedVideoFrame.h"
 #include "Logging.h"
+#include "RTCRtpScriptTransformer.h"
 #include "ReadableStreamSource.h"
 #include "ScriptExecutionContextInlines.h"
 #include "Settings.h"
@@ -94,7 +95,7 @@ std::optional<Exception> RTCEncodedStreamProducer::initialize(JSDOMGlobalObject&
     return { };
 }
 
-void RTCEncodedStreamProducer::start(Ref<RTCRtpTransformBackend>&& transformBackend, bool isVideo)
+void RTCEncodedStreamProducer::start(Ref<RTCRtpTransformBackend>&& transformBackend, bool isVideo, RTCRtpScriptTransformer* transformer)
 {
     transformBackend->setTransformableFrameCallback([weakThis = WeakPtr { *this }, contextIdentifer = m_contextIdentifier](Ref<RTCRtpTransformableFrame>&& frame) mutable {
         ScriptExecutionContext::postTaskTo(contextIdentifer, [weakThis, frame = WTF::move(frame)](auto&) mutable {
@@ -104,6 +105,8 @@ void RTCEncodedStreamProducer::start(Ref<RTCRtpTransformBackend>&& transformBack
     });
     m_transformBackend = WTF::move(transformBackend);
     m_isVideo = isVideo;
+    m_hasTransformer = !!transformer;
+    m_transformer = transformer;
 }
 
 void RTCEncodedStreamProducer::enqueueFrame(Ref<RTCRtpTransformableFrame>&& frame)
@@ -136,6 +139,7 @@ void RTCEncodedStreamProducer::enqueueFrame(Ref<RTCRtpTransformableFrame>&& fram
     }
 #endif
 
+    frame->setTransformer(m_transformer);
     auto value = m_isVideo ? toJS(globalObject, globalObject, RTCEncodedVideoFrame::create(WTF::move(frame))) : toJS(globalObject, globalObject, RTCEncodedAudioFrame::create(WTF::move(frame)));
 
     m_readableSource->enqueue(value);
@@ -158,16 +162,21 @@ ExceptionOr<void> RTCEncodedStreamProducer::writeFrame(ScriptExecutionContext& c
     if (frameConversionResult.hasException(scope)) [[unlikely]]
         return Exception { ExceptionCode::ExistingExceptionError };
 
+    bool isVideo = false;
     auto frame = frameConversionResult.releaseReturnValue();
     auto rtcFrame = WTF::switchOn(frame,
         [&](Ref<RTCEncodedAudioFrame>& value) {
             return value->rtcFrame(vm);
         },
         [&](Ref<RTCEncodedVideoFrame>& value) {
+            isVideo = true;
             return value->rtcFrame(vm);
         }
     );
 
+    if (m_isVideo != isVideo || (m_hasTransformer && !rtcFrame->isFromTransformer(m_transformer.get())))
+        return { };
+
     // If no data, skip the frame since there is nothing to packetize or decode.
     if (rtcFrame->data().data())
         transformBackend->processTransformedFrame(rtcFrame.get());
diff --git a/Source/WebCore/Modules/mediastream/RTCEncodedStreamProducer.h b/Source/WebCore/Modules/mediastream/RTCEncodedStreamProducer.h
index 99bb1f0dbf25..934443fa6e00 100644
--- a/Source/WebCore/Modules/mediastream/RTCEncodedStreamProducer.h
+++ b/Source/WebCore/Modules/mediastream/RTCEncodedStreamProducer.h
@@ -53,7 +53,7 @@ public:
     static ExceptionOr<Ref<RTCEncodedStreamProducer>> create(ScriptExecutionContext&);
     ~RTCEncodedStreamProducer();
 
-    void start(Ref<RTCRtpTransformBackend>&&, bool isVideo);
+    void start(Ref<RTCRtpTransformBackend>&&, bool isVideo, RTCRtpScriptTransformer* = nullptr);
     void clear(bool shouldClearCallback);
 
     void generateKeyFrame(ScriptExecutionContext&, const String&, Ref<DeferredPromise>&&);
@@ -82,6 +82,8 @@ private:
     RefPtr<RTCRtpTransformBackend> m_transformBackend;
     Vector<Ref<DeferredPromise>> m_pendingKeyFramePromises;
     bool m_isVideo { false };
+    bool m_hasTransformer { false };
+    WeakPtr<RTCRtpScriptTransformer> m_transformer;
 
 #if !RELEASE_LOG_DISABLED
     bool m_enableAdditionalLogging { false };
diff --git a/Source/WebCore/Modules/mediastream/RTCRtpScriptTransformer.cpp b/Source/WebCore/Modules/mediastream/RTCRtpScriptTransformer.cpp
index cec98b2e3d89..9c4200d851e4 100644
--- a/Source/WebCore/Modules/mediastream/RTCRtpScriptTransformer.cpp
+++ b/Source/WebCore/Modules/mediastream/RTCRtpScriptTransformer.cpp
@@ -83,7 +83,7 @@ WritableStream& RTCRtpScriptTransformer::writable()
 void RTCRtpScriptTransformer::start(Ref<RTCRtpTransformBackend>&& backend)
 {
     m_isSender = backend->side() == RTCRtpTransformBackend::Side::Sender;
-    m_streamProducer->start(WTF::move(backend), backend->mediaType() == RTCRtpTransformBackend::MediaType::Video);
+    m_streamProducer->start(WTF::move(backend), backend->mediaType() == RTCRtpTransformBackend::MediaType::Video, this);
 }
 
 void RTCRtpScriptTransformer::clear(ClearCallback clearCallback)
diff --git a/Source/WebCore/Modules/mediastream/RTCRtpTransformableFrame.h b/Source/WebCore/Modules/mediastream/RTCRtpTransformableFrame.h
index ed24df2c9e30..a6edf51e1d68 100644
--- a/Source/WebCore/Modules/mediastream/RTCRtpTransformableFrame.h
+++ b/Source/WebCore/Modules/mediastream/RTCRtpTransformableFrame.h
@@ -76,8 +76,8 @@ public:
     virtual void setOptions(const RTCEncodedAudioFrameMetadata&) = 0;
     virtual void setOptions(const RTCEncodedVideoFrameMetadata&) = 0;
 
-    void setTransformer(RTCRtpScriptTransformer&);
-    bool isFromTransformer(RTCRtpScriptTransformer& transformer) const { return &transformer == m_transformer.get(); }
+    void setTransformer(WeakPtr<RTCRtpScriptTransformer>);
+    bool isFromTransformer(RTCRtpScriptTransformer* transformer) const { return transformer == m_transformer.get(); }
 
     virtual bool isLibWebRTCRtpTransformableFrame() const { return false; }
 
@@ -85,10 +85,10 @@ private:
     WeakPtr<RTCRtpScriptTransformer> m_transformer;
 };
 
-inline void RTCRtpTransformableFrame::setTransformer(RTCRtpScriptTransformer& transformer)
+inline void RTCRtpTransformableFrame::setTransformer(WeakPtr<RTCRtpScriptTransformer> transformer)
 {
     ASSERT(!m_transformer);
-    m_transformer = transformer;
+    m_transformer = WTF::move(transformer);
 };
 
 } // namespace WebCore
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;
+                }
+                if(audioReceiverTransformer === this) {
+                    audioChunk = chunk;
+                    while (audioSenderTransformer.tryWritingAudio)
+                        await new Promise(resolve => setTimeout(resolve, 50));
+                    audioReceiverTransformer.writer.write(audioChunk);
+                    audioChunk = undefined;
+                    this.process();
+                    return;
+                }
+            }
+
+            if (videoSenderTransformer && videoSenderTransformer.tryWritingVideo) {
+                if (videoSenderTransformer === this) {
+                    this.writer.write(chunk.value);
+                    if (videoChunk !== undefined) {
+                       this.writer.write(videoChunk.value);
+                       videoSenderTransformer.tryWritingVideo = false;
+                       this.context.options.port.postMessage("PASS");
+                    }
+                    this.process();
+                    return;
+                }
+                if (videoReceiverTransformer === this) {
+                    videoChunk = chunk;
+                    while (videoSenderTransformer.tryWritingVideo)
+                        await new Promise(resolve => setTimeout(resolve, 50));
+                    videoReceiverTransformer.writer.write(videoChunk);
+                    videoChunk = undefined;
+                    this.process();
+                    return;
+                }
+            }
+
             if (this.tryAccessingDataTwice) {
                 this.tryAccessingDataTwice = false;
                 const data1 = chunk.value.data;
diff --git a/LayoutTests/http/wpt/webrtc/audiovideo-script-transform-expected.txt b/LayoutTests/http/wpt/webrtc/audiovideo-script-transform-expected.txt
index 8c185caca2af..7c263d7fdbae 100644
--- a/LayoutTests/http/wpt/webrtc/audiovideo-script-transform-expected.txt
+++ b/LayoutTests/http/wpt/webrtc/audiovideo-script-transform-expected.txt
@@ -10,4 +10,8 @@ PASS Check data after write
 PASS Check modified data after write
 PASS Check data getter
 PASS Check metadata getter
+PASS Try writing receiver audio data on audio sender
+PASS Try writing receiver video data on video sender
+PASS Try writing video data on audio sender
+PASS Try writing audio data on video sender
 
diff --git a/LayoutTests/http/wpt/webrtc/audiovideo-script-transform.html b/LayoutTests/http/wpt/webrtc/audiovideo-script-transform.html
index a829632f4da3..6f8ab5537fd2 100644
--- a/LayoutTests/http/wpt/webrtc/audiovideo-script-transform.html
+++ b/LayoutTests/http/wpt/webrtc/audiovideo-script-transform.html
@@ -160,9 +160,33 @@ promise_test(async (test) => {
     videoSenderTransform.port.postMessage("tryAccessingMetadata");
     metadata =await waitForMessage(videoSenderTransform.port);
     assert_array_equals(Object.keys(metadata), ["contributingSources", "dependencies", "frameId", "height", "mimeType", "payloadType", "rtpTimestamp", "spatialIndex", "synchronizationSource", "temporalIndex", "width"]);
+}, "Check metadata getter");
+
+promise_test(async (test) => {
+    audioSenderTransform.port.postMessage("tryWritingAudio");
+    let result = await waitForMessage(audioSenderTransform.port);
+    assert_equals(result, "PASS");
+}, "Try writing receiver audio data on audio sender");
+
+promise_test(async (test) => {
+    videoSenderTransform.port.postMessage("tryWritingVideo");
+    let result = await waitForMessage(videoSenderTransform.port);
+    assert_equals(result, "PASS");
+}, "Try writing receiver video data on video sender");
+
+promise_test(async (test) => {
+    audioSenderTransform.port.postMessage("tryWritingVideo");
+    let result = await waitForMessage(audioSenderTransform.port);
+    assert_equals(result, "PASS");
+}, "Try writing video data on audio sender");
+
+promise_test(async (test) => {
+    videoSenderTransform.port.postMessage("tryWritingAudio");
+    result = await waitForMessage(videoSenderTransform.port);
+    assert_equals(result, "PASS");
 
     test.add_cleanup(() => localStream.getTracks().forEach(t => t.stop()));
-}, "Check metadata getter");
+}, "Try writing audio data on video sender");
         </script>
     </body>
 </html>
Loading diff…

Regression Test / PoC

shipped with the fix
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;
+                }
+                if(audioReceiverTransformer === this) {
+                    audioChunk = chunk;
+                    while (audioSenderTransformer.tryWritingAudio)
+                        await new Promise(resolve => setTimeout(resolve, 50));
+                    audioReceiverTransformer.writer.write(audioChunk);
+                    audioChunk = undefined;
+                    this.process();
+                    return;
+                }
+            }
+
+            if (videoSenderTransformer && videoSenderTransformer.tryWritingVideo) {
+                if (videoSenderTransformer === this) {
+                    this.writer.write(chunk.value);
+                    if (videoChunk !== undefined) {
+                       this.writer.write(videoChunk.value);
+                       videoSenderTransformer.tryWritingVideo = false;
+                       this.context.options.port.postMessage("PASS");
+                    }
+                    this.process();
+                    return;
+                }
+                if (videoReceiverTransformer === this) {
+                    videoChunk = chunk;
+                    while (videoSenderTransformer.tryWritingVideo)
+                        await new Promise(resolve => setTimeout(resolve, 50));
+                    videoReceiverTransformer.writer.write(videoChunk);
+                    videoChunk = undefined;
+                    this.process();
+                    return;
+                }
+            }
+
             if (this.tryAccessingDataTwice) {
                 this.tryAccessingDataTwice = false;
                 const data1 = chunk.value.data;
diff --git a/LayoutTests/http/wpt/webrtc/audiovideo-script-transform-expected.txt b/LayoutTests/http/wpt/webrtc/audiovideo-script-transform-expected.txt
index 8c185caca2af..7c263d7fdbae 100644
--- a/LayoutTests/http/wpt/webrtc/audiovideo-script-transform-expected.txt
+++ b/LayoutTests/http/wpt/webrtc/audiovideo-script-transform-expected.txt
@@ -10,4 +10,8 @@ PASS Check data after write
 PASS Check modified data after write
 PASS Check data getter
 PASS Check metadata getter
+PASS Try writing receiver audio data on audio sender
+PASS Try writing receiver video data on video sender
+PASS Try writing video data on audio sender
+PASS Try writing audio data on video sender
diff --git a/LayoutTests/http/wpt/webrtc/audiovideo-script-transform.html b/LayoutTests/http/wpt/webrtc/audiovideo-script-transform.html
index a829632f4da3..6f8ab5537fd2 100644
--- a/LayoutTests/http/wpt/webrtc/audiovideo-script-transform.html
+++ b/LayoutTests/http/wpt/webrtc/audiovideo-script-transform.html
@@ -160,9 +160,33 @@ promise_test(async (test) => {
     videoSenderTransform.port.postMessage("tryAccessingMetadata");
     metadata =await waitForMessage(videoSenderTransform.port);
     assert_array_equals(Object.keys(metadata), ["contributingSources", "dependencies", "frameId", "height", "mimeType", "payloadType", "rtpTimestamp", "spatialIndex", "synchronizationSource", "temporalIndex", "width"]);
+}, "Check metadata getter");
+
+promise_test(async (test) => {
+    audioSenderTransform.port.postMessage("tryWritingAudio");
+    let result = await waitForMessage(audioSenderTransform.port);
+    assert_equals(result, "PASS");
+}, "Try writing receiver audio data on audio sender");
+
+promise_test(async (test) => {
+    videoSenderTransform.port.postMessage("tryWritingVideo");
+    let result = await waitForMessage(videoSenderTransform.port);
+    assert_equals(result, "PASS");
+}, "Try writing receiver video data on video sender");
+
+promise_test(async (test) => {
+    audioSenderTransform.port.postMessage("tryWritingVideo");
+    let result = await waitForMessage(audioSenderTransform.port);
+    assert_equals(result, "PASS");
+}, "Try writing video data on audio sender");
+
+promise_test(async (test) => {
+    videoSenderTransform.port.postMessage("tryWritingAudio");
+    result = await waitForMessage(videoSenderTransform.port);
+    assert_equals(result, "PASS");
 
     test.add_cleanup(() => localStream.getTracks().forEach(t => t.stop()));
-}, "Check metadata getter");
+}, "Try writing audio data on video sender");
         </script>
     </body>
 </html>
Loading diff…

Original Bug Report

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