Moderate firefox UAF 🔧 Commit mapped

Overview

Moderate
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Impactmoderate
DescriptionUse-after-free in the Networking: DNS component
ComponentNetworking
Bug ClassUAF
Tracker2076704
Fix commit0a663c251191 (firefox) +38/-44
CISA KEVNot listed
CreditedMozilla
Disclosed2026-10-27

Background

TRR (Trusted Recursive Resolver)
netwerk/dns/TRR.cpp implements a single DNS-over-HTTPS query as a Runnable. It owns an nsIChannel mChannel that performs the HTTPS request and is driven on a specific thread (main thread or the dedicated TRR thread).
`TRRService::MainThreadOrTRRTarget`
picks the event target (thread) a TRR request should run on, depending on configuration; it optionally takes a lock (aWithLock).
`TRR::Cancel`
aborts an in-flight TRR request; because the underlying channel can only be manipulated on its owning thread, Cancel must run on that thread and re-dispatch itself if called elsewhere.
Thread confinement
mChannel (and the nsHttpChannel/nsIHttpChannelInternal it may be) are not thread-safe; touching them off their owning thread is a use-after-free / data-race hazard.

Root Cause Analysis

The invariant is that mChannel must only be touched on the thread that owns the TRR request. The old TRR::Cancel violated this: to decide which thread to re-dispatch to, it first inspected mChannel on whatever thread Cancel happened to be called — do_QueryInterface(mChannel) to get nsIHttpChannelInternal, called GetIsTRRServiceChannel, and do_QueryObject(mChannel) to get an nsHttpChannel. Only after reading mChannel did it choose between the TRR thread and the main thread and re-dispatch. So on the wrong thread, Cancel already dereferenced and QueryInterfaced a channel object that may have been concurrently torn down or owned by another thread — a use-after-free / race on mChannel before the request ever reached its proper thread.

The patch pins the target thread at construction instead of deriving it from mChannel at cancel time. A new const nsCOMPtr<nsIEventTarget> mTarget is added to TRR and initialized in every constructor — the normal query constructors set it from the new static TRR::DefaultTarget() (which returns TRRService::Get()->MainThreadOrTRRTarget()), and the verification constructor takes an explicit nsIEventTarget* aTarget. Because mTarget is const and set at construction, it “can be read from any thread without locking.” TRR::Cancel now simply checks if (mTarget && !mTarget->IsOnCurrentThread()) and re-dispatches to mTarget before touching mChannel at all — the new comment states “mChannel is only accessed on mTarget, so it must not be touched before we get there.” Correspondingly, TRRService::DispatchTRRRequestInternal(aTrrRequest, aWithLock) is removed and DispatchTRRRequest now reads the already-chosen aTrrRequest->Target() instead of recomputing the target, and the callers in ConfirmationContext::HandleEvent and AddToBlocklist pass the target into the TRR constructor.

Key insight
TRR::Cancel dereferenced the non-thread-safe mChannel to decide which thread it belonged on — doing on the wrong thread exactly the thing that was only safe on the right thread. Fixing it required choosing the thread from an immutable, lock-free mTarget captured at construction, so mChannel is never touched off its owning thread.

Attack Path

  1. Trigger a TRR request Cause the parent/socket process to issue a DNS-over-HTTPS lookup (navigation or any name resolution when TRR is enabled). (web-reachability inferred; the diff shows only the networking internals.)
  2. Race a cancellation Induce cancellation of the in-flight TRR (e.g. connection teardown, timeout, or navigation abort) so TRR::Cancel is invoked from a thread other than the channel’s owning thread.
  3. Off-thread channel access Old Cancel runs do_QueryInterface(mChannel) / do_QueryObject(mChannel) on the wrong thread while the channel is being used or freed on its owning thread, creating a data race / use-after-free on the channel object.
  4. Memory corruption With grooming of the freed channel’s heap slot, the off-thread dereference reads/acts on attacker-influenced memory.
  5. Primitive Use-after-free / cross-thread data race on an HTTP channel in the parent or socket process. (escalation inferred)

Impact Assessment

The affected code runs in the parent or socket process (asserted by MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess() || XRE_IsSocketProcess())), so a successful exploit is a privileged-process memory-safety bug reachable from network-triggered DNS activity. The precondition is a race between TRR::Cancel on one thread and the channel’s use/teardown on its owning thread; it is timing-dependent and not deterministic. The concrete primitive shown by the diff is unsafe off-thread access (UAF/race) of mChannel; the advisory labels it a use-after-free, which the diff supports. Escalation beyond memory corruption is inferred.

Changed Functions

FunctionChangeNotes
mOriginSuffix
netwerk/dns/TRR.cpp
modified
mUseFreshConnection
netwerk/dns/TRR.cpp
modified
mTarget
netwerk/dns/TRR.cpp
modified
if
netwerk/dns/TRR.cpp
modified
if
netwerk/dns/TRRService.cpp
modified
switch
netwerk/dns/TRRService.cpp
modified

Files Changed

  • netwerk/dns/TRR.cpp
  • netwerk/dns/TRR.h
  • netwerk/dns/TRRService.cpp
  • netwerk/dns/TRRService.h

Audit Directions

  • Objects dereferenced to pick their own thread
    audit other Cancel/Shutdown paths that inspect a non-thread-safe member to decide re-dispatch; the inspection itself may be off-thread.
  • Thread-affine networking members
    review other nsIChannel/nsHttpChannel holders (proxy resolution, WebSocket, other resolvers) for QueryInterface/QueryObject calls made before confirming the current thread.
  • Lock-free reads of mutable members
    confirm any field read “without locking” from multiple threads is truly immutable (const, set once at construction) as mTarget now is.
  • Confirmation/blocklist TRR paths
    verify all new TRR(...) sites supply a correct target now that the dispatcher trusts Target() rather than recomputing it.
diff --git a/netwerk/dns/TRR.cpp b/netwerk/dns/TRR.cpp
index 212a2388e00..a960b375a00 100644
--- a/netwerk/dns/TRR.cpp
+++ b/netwerk/dns/TRR.cpp
@@ -60,13 +60,22 @@ TRR::GetPriority(uint32_t* aPriority) {
   return NS_OK;
 }
 
+// static
+already_AddRefed<nsIEventTarget> TRR::DefaultTarget() {
+  if (!TRRService::Get()) {
+    return nullptr;
+  }
+  return TRRService::Get()->MainThreadOrTRRTarget();
+}
+
 // when firing off a normal A or AAAA query
 TRR::TRR(AHostResolver* aResolver, nsHostRecord* aRec, enum TrrType aType)
     : mozilla::Runnable("TRR"),
       mRec(aRec),
       mHostResolver(aResolver),
       mType(aType),
-      mOriginSuffix(aRec->originSuffix) {
+      mOriginSuffix(aRec->originSuffix),
+      mTarget(DefaultTarget()) {
   mHost = aRec->host;
   mPB = aRec->pb;
   MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess() || XRE_IsSocketProcess(),
@@ -83,14 +92,16 @@ TRR::TRR(AHostResolver* aResolver, nsHostRecord* aRec, nsCString& aHost,
       mType(aType),
       mPB(aPB),
       mCnameLoop(aLoopCount),
-      mOriginSuffix(aRec ? aRec->originSuffix : ""_ns) {
+      mOriginSuffix(aRec ? aRec->originSuffix : ""_ns),
+      mTarget(DefaultTarget()) {
   MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess() || XRE_IsSocketProcess(),
                         "TRR must be in parent or socket process");
 }
 
 // to verify a domain
 TRR::TRR(AHostResolver* aResolver, nsACString& aHost, enum TrrType aType,
-         const nsACString& aOriginSuffix, bool aPB, bool aUseFreshConnection)
+         const nsACString& aOriginSuffix, bool aPB, bool aUseFreshConnection,
+         nsIEventTarget* aTarget)
     : mozilla::Runnable("TRR"),
       mHost(aHost),
       mRec(nullptr),
@@ -98,7 +109,8 @@ TRR::TRR(AHostResolver* aResolver, nsACString& aHost, enum TrrType aType,
       mType(aType),
       mPB(aPB),
       mOriginSuffix(aOriginSuffix),
-      mUseFreshConnection(aUseFreshConnection) {
+      mUseFreshConnection(aUseFreshConnection),
+      mTarget(aTarget) {
   MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess() || XRE_IsSocketProcess(),
                         "TRR must be in parent or socket process");
 }
@@ -971,35 +983,13 @@ TRR::OnDataAvailable(nsIRequest* aRequest, nsIInputStream* aInputStream,
 }
 
 void TRR::Cancel(nsresult aStatus) {
-  bool isTRRServiceChannel = false;
-  nsCOMPtr<nsIHttpChannelInternal> httpChannelInternal(
-      do_QueryInterface(mChannel));
-  if (httpChannelInternal) {
-    nsresult rv =
-        httpChannelInternal->GetIsTRRServiceChannel(&isTRRServiceChannel);
-    if (NS_FAILED(rv)) {
-      isTRRServiceChannel = false;
-    }
-  }
-  // nsHttpChannel can be only canceled on the main thread.
-  RefPtr<nsHttpChannel> httpChannel = do_QueryObject(mChannel);
-  if (isTRRServiceChannel && !XRE_IsSocketProcess() && !httpChannel) {
-    if (TRRService::Get()) {
-      nsCOMPtr<nsIThread> thread = TRRService::Get()->TRRThread();
-      if (thread && !thread->IsOnCurrentThread()) {
-        thread->Dispatch(NS_NewRunnableFunction(
-            "TRR::Cancel",
-            [self = RefPtr(this), aStatus]() { self->Cancel(aStatus); }));
-        return;
-      }
-    }
-  } else {
-    if (!NS_IsMainThread()) {
-      NS_DispatchToMainThread(NS_NewRunnableFunction(
-          "TRR::Cancel",
-          [self = RefPtr(this), aStatus]() { self->Cancel(aStatus); }));
-      return;
-    }
+  // mChannel is only accessed on mTarget, so it must not be touched before we
+  // get there.
+  if (mTarget && !mTarget->IsOnCurrentThread()) {
+    mTarget->Dispatch(NS_NewRunnableFunction(
+        "TRR::Cancel",
+        [self = RefPtr(this), aStatus]() { self->Cancel(aStatus); }));
+    return;
   }
 
   if (mCancelled) {
diff --git a/netwerk/dns/TRR.h b/netwerk/dns/TRR.h
index 65b87b892e9..a476afd9caf 100644
--- a/netwerk/dns/TRR.h
+++ b/netwerk/dns/TRR.h
@@ -47,7 +47,7 @@ class TRR : public Runnable,
   // to verify a domain
   explicit TRR(AHostResolver* aResolver, nsACString& aHost, enum TrrType aType,
                const nsACString& aOriginSuffix, bool aPB,
-               bool aUseFreshConnection);
+               bool aUseFreshConnection, nsIEventTarget* aTarget);
 
   NS_IMETHOD Run() override;
   void Cancel(nsresult aStatus);
@@ -69,6 +69,7 @@ class TRR : public Runnable,
   RequestPurpose Purpose() { return mPurpose; }
   void SetPurpose(RequestPurpose aPurpose) { mPurpose = aPurpose; }
   TRRSkippedReason SkipReason() const { return mTRRSkippedReason; }
+  nsIEventTarget* Target() const { return mTarget; }
 
  protected:
   virtual ~TRR() = default;
@@ -107,6 +108,8 @@ class TRR : public Runnable,
 
   void StoreIPHintAsDNSRecord(const struct SVCB& aSVCBRecord);
 
+  static already_AddRefed<nsIEventTarget> DefaultTarget();
+
   nsCOMPtr<nsIChannel> mChannel;
   enum TrrType mType { TRRTYPE_A };
   UniquePtr<DNSPacket> mPacket;
@@ -146,6 +149,10 @@ class TRR : public Runnable,
 
   // If true, we set LOAD_FRESH_CONNECTION on our channel's load flags.
   bool mUseFreshConnection = false;
+
+  // The thread this request runs on. Chosen at construction so it can be read
+  // from any thread without locking.
+  const nsCOMPtr<nsIEventTarget> mTarget;
 };
 
 }  // namespace net
diff --git a/netwerk/dns/TRRService.cpp b/netwerk/dns/TRRService.cpp
index c8589febc7a..8ec6a5667a5 100644
--- a/netwerk/dns/TRRService.cpp
+++ b/netwerk/dns/TRRService.cpp
@@ -527,14 +527,9 @@ TRRService::~TRRService() {
 }
 
 nsresult TRRService::DispatchTRRRequest(TRR* aTrrRequest) {
-  return DispatchTRRRequestInternal(aTrrRequest, true);
-}
-
-nsresult TRRService::DispatchTRRRequestInternal(TRR* aTrrRequest,
-                                                bool aWithLock) {
   NS_ENSURE_ARG_POINTER(aTrrRequest);
 
-  nsCOMPtr<nsIEventTarget> target = MainThreadOrTRRTarget(aWithLock);
+  nsCOMPtr<nsIEventTarget> target = aTrrRequest->Target();
   if (!target) {
     return NS_ERROR_FAILURE;
   }
@@ -874,9 +869,11 @@ bool TRRService::ConfirmationContext::HandleEvent(ConfirmationEvent aEvent,
                "Should only confirm in TRR first mode");
     // Set aUseFreshConnection if TRR lookups are retried
     // or if confirmation already failed.
+    nsCOMPtr<nsIEventTarget> target = service->MainThreadOrTRRTarget(false);
     mTask = new TRR(service, service->mConfirmationNS, TRRTYPE_NS, ""_ns, false,
                     mState == CONFIRM_TRYING_FAILED ||
-                        StaticPrefs::network_trr_retry_on_recoverable_errors());
+                        StaticPrefs::network_trr_retry_on_recoverable_errors(),
+                    target);
     mTask->SetTimeout(StaticPrefs::network_trr_confirmation_timeout_ms());
     mTask->SetPurpose(TRR::Confirmation);
 
@@ -892,7 +889,7 @@ bool TRRService::ConfirmationContext::HandleEvent(ConfirmationEvent aEvent,
     }
 
     LOG(("Dispatching confirmation task: %p", mTask.get()));
-    service->DispatchTRRRequestInternal(mTask, false);
+    service->DispatchTRRRequest(mTask);
   };
 
   switch (aEvent) {
@@ -1163,8 +1160,9 @@ void TRRService::AddToBlocklist(const nsACString& aHost,
       LOG(("TRR: verify if '%s' resolves as NS\n", check.get()));
 
       // check if there's an NS entry for this name
+      nsCOMPtr<nsIEventTarget> target = MainThreadOrTRRTarget();
       RefPtr<TRR> trr = new TRR(this, check, TRRTYPE_NS, aOriginSuffix,
-                                privateBrowsing, false);
+                                privateBrowsing, false, target);
       trr->SetPurpose(TRR::Blocklist);
       DispatchTRRRequest(trr);
     }
diff --git a/netwerk/dns/TRRService.h b/netwerk/dns/TRRService.h
index e6cd6479bcf..f17c4fcd4dd 100644
--- a/netwerk/dns/TRRService.h
+++ b/netwerk/dns/TRRService.h
@@ -132,7 +132,6 @@ class TRRService : public TRRServiceBase,
 
   void RebuildSuffixList(nsTArray<nsCString>&& aSuffixList);
 
-  nsresult DispatchTRRRequestInternal(TRR* aTrrRequest, bool aWithLock);
   already_AddRefed<nsIThread> TRRThread_locked();
   already_AddRefed<nsIEventTarget> MainThreadOrTRRTarget(bool aWithLock = true);
 
Loading diff…