High firefox UAF 🔧 Commit mapped

Overview

High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Impacthigh
DescriptionUse-after-free in the WebRTC: Audio/Video component
ComponentDOM
Bug ClassUAF
Tracker2043188
Fix commit766bdbb6fba0 (firefox) +8/-13
CISA KEVNot listed
Creditedcrixer
Disclosed2026-07-21

Background

`RTCEncodedFrameBase`
Gecko DOM object backing WebRTC Encoded Transform frames, exposing an encoded audio/video frame’s payload to script as an ArrayBuffer.
`JS::NewArrayBufferWithUserOwnedContents`
SpiderMonkey API that creates a JS ArrayBuffer aliasing caller-owned memory that the JS engine neither copies nor frees.
`JS::NewArrayBufferWithContents`
SpiderMonkey API that creates a JS ArrayBuffer taking ownership of an engine-arena allocation, tying the buffer’s lifetime to the JS object.
Use-after-free (UAF)
A memory-safety bug where memory is accessed after it has been freed, here through a dangling ArrayBuffer backing pointer.

Root Cause Analysis

The RTCEncodedFrameBase constructor built the script-visible mData ArrayBuffer with JS::NewArrayBufferWithUserOwnedContents, so the buffer aliased the raw bytes owned by the underlying frame (mState.mFrame->GetData().data()) rather than owning a copy. This violated the invariant that a JS ArrayBuffer handed to content must outlive, or independently own, its backing store: the frame’s data could be released while script still held a live reference to the ArrayBuffer, leaving a dangling backing pointer. The scattered DetachData() calls (in the cycle-collection unlink, in SetData, and elsewhere) attempted to manually keep the alias and the frame in sync, but could not cover every path by which script retained the buffer after the frame’s memory went away.

The fix copies the frame bytes into a JS-arena allocation (js_pod_arena_malloc + memcpy) and hands that copy to JS::NewArrayBufferWithContents, so the ArrayBuffer owns its own memory and its lifetime is governed entirely by the JS engine. Because mData is now self-owning, the fragile manual DetachData() bookkeeping is removed, and Size() reads the length from the buffer itself (GetArrayBufferByteLength(mData)) rather than dereferencing the possibly-gone frame.

Key insight
The core mistake was exposing a JS ArrayBuffer that merely aliased frame-owned memory via JS::NewArrayBufferWithUserOwnedContents, decoupling the buffer’s lifetime from the buffer’s backing store; the fix makes the ArrayBuffer own a private copy so its data can never outlive its owner.

Attack Path

  1. Obtain an encoded frame Via the WebRTC Encoded Transform API, script receives an RTCEncodedFrameBase whose mData ArrayBuffer aliases the frame’s internal payload.
  2. Retain the `ArrayBuffer` Script keeps a reference to the ArrayBuffer (or a typed-array view over it) beyond the point where the transform pipeline is done with the frame.
  3. Trigger backing release Cause the underlying frame data to be freed (frame consumption/TakeFrame, detach, or object teardown), leaving the ArrayBuffer’s backing pointer dangling.
  4. Access the dangling buffer Read or write the retained ArrayBuffer, dereferencing freed memory to disclose or corrupt heap contents.

Impact Assessment

An attacker running script in a page that uses WebRTC Encoded Transform can read from or write to freed heap memory in the content process, potentially disclosing adjacent heap data or corrupting objects toward controlled execution. The bug is reachable from ordinary web content with no special privileges, requiring only an active WebRTC encoded audio/video stream. It is rated high severity and manifests in the process hosting the DOM/WebRTC code.

Changed Functions

FunctionChangeNotes
if
dom/media/webrtc/jsapi/RTCEncodedFrameBase.cpp
modified

Files Changed

  • dom/media/webrtc/jsapi/RTCEncodedFrameBase.cpp

Audit Directions

  • User-owned `ArrayBuffer` contents
    Audit every use of JS::NewArrayBufferWithUserOwnedContents (and JS::NewExternalArrayBuffer) to confirm the aliased memory strictly outlives every reference script can hold to the buffer, or convert it to an owning copy.
  • Lifetime-coupled aliases
    Flag DOM objects that expose internal buffers to JS while retaining a separate owner (mFrame, backing spans), especially those relying on manual detach/sync calls across teardown paths to prevent dangling views.
  • Length read from a second source
    Review size/length accessors that read from a field other than the exposed buffer itself; a length derived from a separately-freed object is both a correctness and a UAF hazard.
diff --git a/dom/media/webrtc/jsapi/RTCEncodedFrameBase.cpp b/dom/media/webrtc/jsapi/RTCEncodedFrameBase.cpp
index cd8b8f64713..930d2b2788f 100644
--- a/dom/media/webrtc/jsapi/RTCEncodedFrameBase.cpp
+++ b/dom/media/webrtc/jsapi/RTCEncodedFrameBase.cpp
@@ -19,8 +19,6 @@ namespace mozilla::dom {
 
 NS_IMPL_CYCLE_COLLECTION_CLASS(RTCEncodedFrameBase)
 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(RTCEncodedFrameBase)
-  using ::ImplCycleCollectionUnlink;
-  tmp->DetachData();
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mOwner, mGlobal)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mData)
   NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
@@ -53,11 +51,13 @@ RTCEncodedFrameBase::RTCEncodedFrameBase(nsIGlobalObject* aGlobal,
 
   mozilla::HoldJSObjects(this);
 
-  if (mState.mFrame->GetData().data()) {
-    // Avoid a copy
-    mData = JS::NewArrayBufferWithUserOwnedContents(
-        jsapi.cx(), mState.mFrame->GetData().size(),
-        (void*)(mState.mFrame->GetData().data()));
+  const auto& frame = mState.mFrame->GetData();
+  if (frame.data()) {
+    UniquePtr<void, JS::FreePolicy> data(js_pod_arena_malloc<uint8_t>(
+        js::ArrayBufferContentsArena, frame.size()));
+    memcpy(data.get(), frame.data(), frame.size());
+    mData = JS::NewArrayBufferWithContents(jsapi.cx(), frame.size(),
+                                           std::move(data));
   } else {
     mData = JS::NewArrayBuffer(jsapi.cx(), 0);
   }
@@ -70,7 +70,6 @@ RTCEncodedFrameState::RTCEncodedFrameState(
 
 RTCEncodedFrameBase::~RTCEncodedFrameBase() {
   DetachData();
-  mData = nullptr;
   mozilla::DropJSObjects(this);
 }
 
@@ -98,7 +97,6 @@ unsigned long RTCEncodedFrameBase::Timestamp() const {
 }
 
 void RTCEncodedFrameBase::SetData(const ArrayBuffer& aData) {
-  DetachData();
   mData.set(aData.Obj());
   if (mState.mFrame) {
     aData.ProcessData([&](const Span<uint8_t>& aData, JS::AutoCheckCannotGC&&) {
@@ -122,10 +120,7 @@ RTCEncodedFrameBase::TakeFrame() {
 }
 
 size_t RTCEncodedFrameBase::Size() const {
-  if (!mState.mFrame) {
-    return 0;
-  }
-  return mState.mFrame->GetData().size();
+  return GetArrayBufferByteLength(mData);
 }
 
 RTCEncodedFrameState::~RTCEncodedFrameState() = default;
Loading diff…