CVE-2026-117973
Overview
Background
- TRR (Trusted Recursive Resolver)
netwerk/dns/TRR.cppimplements a single DNS-over-HTTPS query as aRunnable. It owns annsIChannel mChannelthat 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,
Cancelmust run on that thread and re-dispatch itself if called elsewhere. - Thread confinement
mChannel(and thensHttpChannel/nsIHttpChannelInternalit 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.
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
- 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.)
- Race a cancellation
Induce cancellation of the in-flight TRR (e.g. connection teardown, timeout, or navigation abort) so
TRR::Cancelis invoked from a thread other than the channel’s owning thread. - Off-thread channel access
Old
Cancelrunsdo_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. - Memory corruption With grooming of the freed channel’s heap slot, the off-thread dereference reads/acts on attacker-influenced memory.
- Primitive Use-after-free / cross-thread data race on an HTTP channel in the parent or socket process. (escalation inferred)
Impact Assessment
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
| Function | Change | Notes |
|---|---|---|
mOriginSuffixnetwerk/dns/TRR.cpp |
modified | |
mUseFreshConnectionnetwerk/dns/TRR.cpp |
modified | |
mTargetnetwerk/dns/TRR.cpp |
modified | |
ifnetwerk/dns/TRR.cpp |
modified | |
ifnetwerk/dns/TRRService.cpp |
modified | |
switchnetwerk/dns/TRRService.cpp |
modified |
Files Changed
netwerk/dns/TRR.cppnetwerk/dns/TRR.hnetwerk/dns/TRRService.cppnetwerk/dns/TRRService.h
Audit Directions
- Objects dereferenced to pick their own threadaudit other
Cancel/Shutdownpaths that inspect a non-thread-safe member to decide re-dispatch; the inspection itself may be off-thread. - Thread-affine networking membersreview other
nsIChannel/nsHttpChannelholders (proxy resolution, WebSocket, other resolvers) forQueryInterface/QueryObjectcalls made before confirming the current thread. - Lock-free reads of mutable membersconfirm any field read “without locking” from multiple threads is truly immutable (
const, set once at construction) asmTargetnow is. - Confirmation/blocklist TRR pathsverify all
new TRR(...)sites supply a correct target now that the dispatcher trustsTarget()rather than recomputing it.
Patch
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);