High CVSS 9.8 firefox Memory Corruption 🔧 Commit mapped

Overview

High
Severity
9.8
CVSS
No
Exploited ITW
Fixed
Fix Status
Impacthigh
DescriptionMemory safety bugs present in Firefox 130, Firefox ESR 115.15, Firefox ESR 128.2, and Thunderbird 128.2. 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.
ComponentNetworking
Bug ClassMemory Corruption
Tracker1872744
Fix commit62a6584cc47b (firefox) +17/-20
CWECWE-119 (Buffer bounds error)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedAndrew Osmond, Sebastian Hengst, Andrew McCreight
Disclosed2024-10-01

Background

`DNSRequestSender`
The nsICancelable DNS request object that brokers a DNS lookup over IPC, holding an mIPCActor pointer to the actual child or parent actor.
IPC actor lifecycle
An IPC actor such as DNSRequestChild or DNSRequestParent is only valid while its channel can send, which IProtocol::CanSend() reports; once the actor is torn down, sending on it is undefined.
`SchedulerGroup::Dispatch`
A helper that queues a runnable to execute later on the main thread, meaning the code inside the runnable runs at some future point after Cancel has returned.
Captured lambda closure
The NS_NewRunnableFunction lambda captured a raw copy of mIPCActor (as actor) plus request fields by value, so those captures had to remain valid until the deferred runnable actually ran.

Root Cause Analysis

The vulnerable path was DNSRequestSender::Cancel, which packaged the raw mIPCActor pointer and request parameters into a lambda dispatched via SchedulerGroup::Dispatch for later execution on the main thread. The captured actor was a raw pointer with no ownership guarantee, so the underlying DNSRequestChild/DNSRequestParent actor could be destroyed between the time Cancel returned and the time the runnable finally ran. Although the lambda checked actor->CanSend() before sending, that check itself dereferenced a pointer that may already have been freed, and CanSend() does not validate that the object memory is still live — it only reports channel state. The invariant violated is that an IPC actor must outlive any deferred operation that dereferences it, which the raw capture did not enforce.

The fix removes the deferred-closure design entirely: it re-checks mIPCActor && mIPCActor->CanSend() up front, and when off the main thread it re-dispatches Cancel on this via NewRunnableMethod, so the send is performed synchronously on the actor member with a fresh liveness check rather than on a stale captured copy.

Key insight
The core mistake was capturing the IPC actor as a raw pointer inside a deferred runnable, letting the actor be torn down before the closure dereferenced it (a use-after-free). The fix eliminates the stale capture by re-dispatching Cancel on the owning DNSRequestSender and re-validating mIPCActor->CanSend() at the point of use.

Attack Path

  1. Initiate a cancelable DNS request An attacker-influenced context starts a DNS lookup through DNSRequestSender, establishing the mIPCActor link to a DNSRequestChild or DNSRequestParent.
  2. Trigger cancellation off the main thread Cancellation is requested such that DNSRequestSender::Cancel runs off the main thread, causing it to build the lambda capturing a raw actor pointer and dispatch it via SchedulerGroup::Dispatch.
  3. Race the actor teardown Before the queued runnable executes, the IPC channel is closed or the actor is destroyed, freeing the DNSRequestChild/DNSRequestParent object while the captured raw pointer still references it.
  4. Deferred dereference of freed memory The main-thread runnable runs and dereferences the stale actor (via CanSend() and AsDNSRequestChild/AsDNSRequestParent), reading from freed memory.
  5. Corrupt and control With heap grooming to reoccupy the freed actor allocation, the use-after-free read/call can be steered toward controlled data, potentially leading to memory corruption and code execution.

Impact Assessment

An attacker gains a use-after-free on an IPC DNS actor, exercisable in whichever process performs the cancellation (content or parent process handling networking), which can be groomed into memory corruption and potentially arbitrary code execution. The precondition is a cancellation of an in-flight DNS request that races the teardown of the underlying DNSRequestChild/DNSRequestParent actor while the deferred runnable holds a stale raw pointer. Rated high impact consistent with a memory-corruption networking bug that presumes exploitability with sufficient effort.

Changed Functions

FunctionChangeNotes
Cancel
netwerk/dns/DNSRequestChild.cpp
modified
if
netwerk/dns/DNSRequestChild.cpp
modified

Files Changed

  • netwerk/dns/DNSRequestChild.cpp

Audit Directions

  • Raw actor captures in deferred runnables
    Search for NS_NewRunnableFunction/SchedulerGroup::Dispatch lambdas that capture raw IPC actor pointers by value and later call CanSend() or Send*, since the actor may be freed before the runnable runs.
  • CanSend as a liveness check
    Flag code that treats CanSend() as proof the object is alive rather than as channel state; the pointer must be independently guaranteed valid before any dereference.
  • Cross-thread cancelation paths
    Review other nsICancelable/Cancel implementations that hop threads to perform IPC, ensuring they re-dispatch on the owning object and re-check actor liveness instead of shipping captured raw pointers across threads.
diff --git a/netwerk/dns/DNSRequestChild.cpp b/netwerk/dns/DNSRequestChild.cpp
index 2d02bd5c8ba..923d6e677f5 100644
--- a/netwerk/dns/DNSRequestChild.cpp
+++ b/netwerk/dns/DNSRequestChild.cpp
@@ -391,29 +391,26 @@ void DNSRequestSender::OnRecvCancelDNSRequest(
 
 NS_IMETHODIMP
 DNSRequestSender::Cancel(nsresult reason) {
-  if (!mIPCActor) {
+  if (!mIPCActor || !mIPCActor->CanSend()) {
     return NS_ERROR_NOT_AVAILABLE;
   }
 
-  // We can only do IPC on the MainThread
-  nsCOMPtr<nsIRunnable> runnable = NS_NewRunnableFunction(
-      "net::CancelDNSRequestEvent",
-      [actor(mIPCActor), host(mHost), trrServer(mTrrServer), port(mPort),
-       type(mType), originAttributes(mOriginAttributes), flags(mFlags),
-       reason]() {
-        if (!actor->CanSend()) {
-          return;
-        }
-
-        if (DNSRequestChild* child = actor->AsDNSRequestChild()) {
-          Unused << child->SendCancelDNSRequest(
-              host, trrServer, port, type, originAttributes, flags, reason);
-        } else if (DNSRequestParent* parent = actor->AsDNSRequestParent()) {
-          Unused << parent->SendCancelDNSRequest(
-              host, trrServer, port, type, originAttributes, flags, reason);
-        }
-      });
-  SchedulerGroup::Dispatch(runnable.forget());
+  // we can only do IPC on the MainThread
+  if (!NS_IsMainThread()) {
+    SchedulerGroup::Dispatch(
+        NewRunnableMethod<nsresult>("net::DNSRequestSender::Cancel", this,
+                                    &DNSRequestSender::Cancel, reason));
+    return NS_OK;
+  }
+
+  if (DNSRequestChild* child = mIPCActor->AsDNSRequestChild()) {
+    Unused << child->SendCancelDNSRequest(mHost, mTrrServer, mPort, mType,
+                                          mOriginAttributes, mFlags, reason);
+  } else if (DNSRequestParent* parent = mIPCActor->AsDNSRequestParent()) {
+    Unused << parent->SendCancelDNSRequest(mHost, mTrrServer, mPort, mType,
+                                           mOriginAttributes, mFlags, reason);
+  }
+
   return NS_OK;
 }
 
Loading diff…