CVE-2024-9401
Overview
Background
- `DNSRequestSender`
- The
nsICancelableDNS request object that brokers a DNS lookup over IPC, holding anmIPCActorpointer to the actual child or parent actor. - IPC actor lifecycle
- An IPC actor such as
DNSRequestChildorDNSRequestParentis only valid while its channel can send, whichIProtocol::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
Cancelhas returned. - Captured lambda closure
- The
NS_NewRunnableFunctionlambda captured a raw copy ofmIPCActor(asactor) 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.
Cancel on the owning DNSRequestSender and re-validating mIPCActor->CanSend() at the point of use.Attack Path
- Initiate a cancelable DNS request
An attacker-influenced context starts a DNS lookup through
DNSRequestSender, establishing themIPCActorlink to aDNSRequestChildorDNSRequestParent. - Trigger cancellation off the main thread
Cancellation is requested such that
DNSRequestSender::Cancelruns off the main thread, causing it to build the lambda capturing a rawactorpointer and dispatch it viaSchedulerGroup::Dispatch. - Race the actor teardown
Before the queued runnable executes, the IPC channel is closed or the actor is destroyed, freeing the
DNSRequestChild/DNSRequestParentobject while the captured raw pointer still references it. - Deferred dereference of freed memory
The main-thread runnable runs and dereferences the stale
actor(viaCanSend()andAsDNSRequestChild/AsDNSRequestParent), reading from freed memory. - 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
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
| Function | Change | Notes |
|---|---|---|
Cancelnetwerk/dns/DNSRequestChild.cpp |
modified | |
ifnetwerk/dns/DNSRequestChild.cpp |
modified |
Files Changed
netwerk/dns/DNSRequestChild.cpp
Audit Directions
- Raw actor captures in deferred runnablesSearch for
NS_NewRunnableFunction/SchedulerGroup::Dispatchlambdas that capture raw IPC actor pointers by value and later callCanSend()orSend*, since the actor may be freed before the runnable runs. - CanSend as a liveness checkFlag 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 pathsReview other
nsICancelable/Cancelimplementations 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.
Patch
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;
}