High firefox Memory Corruption 🔧 Commit mapped

Overview

High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Impacthigh
DescriptionMemory safety bugs present in Firefox 152.0.3. Some of these bugs showed evidence of memory corruption and we presume that with enough effort some of these could have been exploited to run arbitrary code.
ComponentDOM
Bug ClassMemory Corruption
Tracker2043241
Fix commita52c881ebb3b (firefox) +42/-12
CISA KEVNot listed
CreditedChristian Holler, Gabriele Svelto, Greg Stoll and the Mozilla Fuzzing Team
Disclosed2026-06-30

Background

Web Serial API
a DOM feature exposing serial ports to web content via the SerialPort interface, backed by readable and writable streams.
`SerialPort::Shutdown`
the teardown path that runs when the SerialPort object is being destroyed or its global goes away, releasing OS resources through mChild->Shutdown().
Underlying source/sink algorithms
the author-supplied (or spec-defined) cancel/abort steps that a stream runs when it is gracefully cancelled or aborted, which can dispatch events and re-enter script.
`StreamCloseMode`
the new uint8_t enum (Graceful/Forced) added to choose whether CloseStreams runs author-observable algorithms or merely errors the streams.

Root Cause Analysis

During teardown, SerialPort::Shutdown called CloseStreams, which cancelled the readable stream and aborted the writable stream by running their underlying source/sink algorithms; these algorithms can dispatch events and re-enter author-visible JavaScript. Running script from within a destruction path violates the invariant that no author-observable, script-re-entrant algorithm may execute while the object is being torn down, because the re-entrant script can observe, mutate, or free the half-destroyed SerialPort and its streams. This creates a memory-corruption window (use-after-free / reentrancy) in the DOM.

The fix adds a StreamCloseMode parameter and routes the shutdown-time caller through StreamCloseMode::Forced, which calls ErrorNative on mReadable/mWritable and nulls them out without running any author-observable algorithm or dispatching events. Because the actual OS resources are released separately by mChild->Shutdown(), forcing an error rather than a graceful cancel/abort is safe and still frees the streams. Genuinely graceful callers (Close, Forget, MarkForgotten, NotifyDisconnected) are switched to StreamCloseMode::Graceful, preserving their intended behavior.

Key insight
The single core mistake was invoking script-re-entrant stream cancel/abort algorithms from the object’s own destruction path; the fix distinguishes a Forced mode that errors the streams via ErrorNative without running author-observable code, so shutdown can no longer re-enter script into a partially destroyed SerialPort.

Attack Path

  1. Open a serial port Author code obtains a SerialPort and opens it, creating live mReadable and mWritable streams with observable cancel/abort algorithms.
  2. Trigger shutdown The attacker forces the teardown path (e.g. navigating away or destroying the global) so SerialPort::Shutdown runs and calls CloseStreams.
  3. Re-enter via stream algorithms The graceful cancel/abort run their underlying algorithms, dispatching events and re-entering attacker-controlled JavaScript in the middle of destruction.
  4. Corrupt state The re-entrant script manipulates or frees the SerialPort/stream objects that shutdown is still operating on, producing a use-after-free or other memory corruption.
  5. Escalate With careful heap grooming the freed-then-reused memory is leveraged toward arbitrary code execution in the content process.

Impact Assessment

An attacker with the ability to run script and use the Web Serial API gains a memory-corruption primitive (use-after-free / reentrancy) inside the content process rendering the page, which Mozilla assesses as potentially exploitable for arbitrary code execution. Preconditions include reaching the Web Serial API (a granted or exercisable SerialPort) and being able to trigger the shutdown/teardown path while streams are still active. The corruption occurs in the DOM component within the content process’s JavaScript/DOM context.

Changed Functions

FunctionChangeNotes
if
dom/webserial/SerialPort.cpp
modified

Files Changed

  • dom/webserial/SerialPort.cpp
  • dom/webserial/SerialPort.h

Audit Directions

  • Script re-entrancy in destructors
    Audit any Shutdown/~Dtor/teardown path that invokes stream cancel/abort, event dispatch, or other MOZ_CAN_RUN_SCRIPT helpers, since re-entering author code mid-destruction risks use-after-free.
  • Graceful vs. forced teardown
    For every DOM API backed by streams (WebSerial, WebUSB, WebHID, etc.), verify shutdown paths error/null streams natively rather than running author-observable algorithms.
  • `MOZ_CAN_RUN_SCRIPT` on cleanup paths
    Flag callers of MOZ_CAN_RUN_SCRIPT-annotated functions like CloseStreams reached from object destruction, and confirm object lifetime is safe across the potential script call.
diff --git a/dom/webserial/SerialPort.cpp b/dom/webserial/SerialPort.cpp
index 818fb9acea0..d5dcee6705a 100644
--- a/dom/webserial/SerialPort.cpp
+++ b/dom/webserial/SerialPort.cpp
@@ -137,7 +137,7 @@ void SerialPort::Shutdown() {
   if (mState == State::Opened || mState == State::Closing) {
     mState = State::Closed;
     // Don't have to wait for this
-    RefPtr<Promise> ignoredPromise = CloseStreams();
+    RefPtr<Promise> ignoredPromise = CloseStreams(StreamCloseMode::Forced);
   }
 
   if (mOpenPromise) {
@@ -529,7 +529,7 @@ already_AddRefed<Promise> SerialPort::Close(ErrorResult& aRv) {
   // promise to wait for all (steps 5-8). Our pendingClosePromise is
   // effectively resolved once both streams are nulled (step 6), so the
   // combined promise just waits for the cancel and abort to finish.
-  RefPtr<Promise> combinedPromise = CloseStreams();
+  RefPtr<Promise> combinedPromise = CloseStreams(StreamCloseMode::Graceful);
   if (!combinedPromise) {
     combinedPromise = Promise::CreateResolvedWithUndefined(global, aRv);
     if (NS_WARN_IF(aRv.Failed())) {
@@ -598,7 +598,7 @@ already_AddRefed<Promise> SerialPort::Forget(ErrorResult& aRv) {
 
   if (wasActive) {
     // Don't have to wait for this
-    RefPtr<Promise> ignoredPromise = CloseStreams();
+    RefPtr<Promise> ignoredPromise = CloseStreams(StreamCloseMode::Graceful);
   }
 
   UpdateWorkerRef();
@@ -652,7 +652,7 @@ void SerialPort::MarkForgotten() {
 
   if (wasActive) {
     // Don't have to wait for this
-    RefPtr<Promise> ignoredPromise = CloseStreams();
+    RefPtr<Promise> ignoredPromise = CloseStreams(StreamCloseMode::Graceful);
   }
 
   UpdateWorkerRef();
@@ -767,7 +767,7 @@ void SerialPort::NotifyDisconnected() {
   }
   mPhysicallyPresent = false;
   // Don't have to wait for this
-  RefPtr<Promise> ignoredPromise = CloseStreams();
+  RefPtr<Promise> ignoredPromise = CloseStreams(StreamCloseMode::Graceful);
   UpdateWorkerRef();
   NotifySharingStateChanged(false);
 
@@ -951,7 +951,7 @@ void SerialPort::SettleClosePromise(nsresult aResult) {
   }
 }
 
-already_AddRefed<Promise> SerialPort::CloseStreams() {
+already_AddRefed<Promise> SerialPort::CloseStreams(StreamCloseMode aMode) {
   nsIGlobalObject* global = GetRelevantGlobal();
   if (!global) {
     return nullptr;
@@ -963,8 +963,9 @@ already_AddRefed<Promise> SerialPort::CloseStreams() {
 
   MOZ_LOG(gWebSerialLog, LogLevel::Info,
           ("SerialPort[%p]::CloseStreams closing streams "
-           "(readable=%p, writable=%p)",
-           this, mReadable.get(), mWritable.get()));
+           "(readable=%p, writable=%p, mode=%s)",
+           this, mReadable.get(), mWritable.get(),
+           aMode == StreamCloseMode::Forced ? "forced" : "graceful"));
 
   AutoJSAPI jsapi;
   if (!jsapi.Init(global)) {
@@ -973,14 +974,33 @@ already_AddRefed<Promise> SerialPort::CloseStreams() {
 
   JSContext* cx = jsapi.cx();
 
-  // Cancel the readable and abort the writable, collecting their promises.
-  nsTArray<RefPtr<Promise>> streamPromises;
-
   RefPtr<DOMException> exception =
       DOMException::Create(NS_ERROR_DOM_NETWORK_ERR, "Port has been closed"_ns);
   JS::Rooted<JS::Value> errorVal(cx);
   bool hasError = ToJSValue(cx, exception, &errorVal);
 
+  // Error the streams instead of aborting, so we don't dispatchevents
+  // or runauthor-visible algorithms. The OS resources are released by
+  // mChild->Shutdown() in Shutdown().
+  if (aMode == StreamCloseMode::Forced) {
+    if (mReadable && hasError) {
+      IgnoredErrorResult rv;
+      RefPtr readable = mReadable;
+      readable->ErrorNative(cx, errorVal, rv);
+    }
+    if (mWritable && hasError) {
+      IgnoredErrorResult rv;
+      RefPtr writable = mWritable;
+      writable->ErrorNative(cx, errorVal, rv);
+    }
+    mReadable = nullptr;
+    mWritable = nullptr;
+    return nullptr;
+  }
+
+  // Cancel the readable and abort the writable, collecting their promises.
+  nsTArray<RefPtr<Promise>> streamPromises;
+
   if (mReadable && hasError) {
     IgnoredErrorResult rv;
     RefPtr readable = mReadable;
diff --git a/dom/webserial/SerialPort.h b/dom/webserial/SerialPort.h
index fb319115eaa..3950affdafe 100644
--- a/dom/webserial/SerialPort.h
+++ b/dom/webserial/SerialPort.h
@@ -116,11 +116,21 @@ class SerialPort final : public DOMEventTargetHelper {
  private:
   ~SerialPort() override;
 
+  enum class StreamCloseMode : uint8_t {
+    // Cancel the readable and abort the writable, running their underlying
+    // source/sink algorithms.
+    Graceful,
+    // Error both streams without running author-observable algorithms or
+    // dispatching events.
+    Forced,
+  };
+
   MOZ_CAN_RUN_SCRIPT ReadableStream* CreateReadableStream();
   MOZ_CAN_RUN_SCRIPT WritableStream* CreateWritableStream();
   void CloseAfterStreamsClosed();
   void SettleClosePromise(nsresult aResult);
-  MOZ_CAN_RUN_SCRIPT already_AddRefed<Promise> CloseStreams();
+  MOZ_CAN_RUN_SCRIPT already_AddRefed<Promise> CloseStreams(
+      StreamCloseMode aMode);
   void NotifySharingStateChanged(bool aConnected);
   void UpdateWorkerRef();
 
Loading diff…