CVE-2024-9402
Overview
Background
- `DNSRequestSender`
- the networking helper that owns a DNS request and drives its lifecycle, including cancellation, across the parent/child IPC boundary.
- `mIPCActor`
- the raw pointer held by
DNSRequestSenderto the underlying IPC actor (either aDNSRequestChildor aDNSRequestParent) used to send messages such asSendCancelDNSRequest. - `CanSend`
- an IPC actor predicate that returns whether the actor is still connected and able to send messages, i.e. whether the underlying IPC channel has not been torn down.
- `SchedulerGroup::Dispatch`
- the mechanism used to marshal work onto the MainThread, since IPC sends are only permitted from that thread.
Root Cause Analysis
The vulnerable path was DNSRequestSender::Cancel, which needed to perform an IPC send but could only do so on the MainThread. The original code captured the actor pointer mIPCActor into an NS_NewRunnableFunction lambda and dispatched it via SchedulerGroup::Dispatch, deferring the CanSend liveness check until the runnable actually executed. Because the captured actor was a raw pointer to mIPCActor and the runnable ran asynchronously, the actor could be destroyed between the time Cancel queued the work and the time the runnable ran, so the deferred actor->CanSend() (and the subsequent AsDNSRequestChild/AsDNSRequestParent dereferences) operated on freed memory — a use-after-free.
The fix replaces the captured-pointer lambda with a NewRunnableMethod re-dispatch of Cancel itself onto the MainThread and checks mIPCActor->CanSend() synchronously up front, so the actor is validated and used within a single MainThread turn while the object is still alive. This works because the liveness check and the send now happen together against the live member, eliminating the asynchronous window in which the actor could be freed.
CanSend liveness check into an asynchronously dispatched lambda that captured a raw actor pointer, allowing the actor to be freed before the check and use; the fix moves the check and send into a synchronous MainThread execution of Cancel that references the live mIPCActor member directly.Attack Path
- Trigger a DNS request
The attacker causes content to issue a DNS resolution that creates a
DNSRequestSenderholding an IPC actor inmIPCActor. - Cancel off the MainThread
Cancellation is invoked so
DNSRequestSender::Cancelruns off the MainThread and queues a lambda viaSchedulerGroup::Dispatchcapturing the rawactorpointer. - Free the actor Before the queued runnable executes, the IPC channel is torn down and the actor object is destroyed, leaving the captured pointer dangling.
- Execute the stale runnable
The dispatched lambda runs and calls
actor->CanSend()andAsDNSRequestChild/AsDNSRequestParenton the freed actor, dereferencing reclaimed memory. - Corrupt memory With heap grooming the freed allocation is replaced with attacker-controlled data, turning the use-after-free into memory corruption and potential code execution.
Impact Assessment
Cancel against destruction of the underlying IPC actor so the deferred, captured-pointer access runs after the actor is freed. Rated high impact, consistent with the memory-safety cluster reported for Firefox 130 and ESR 128.2.Changed Functions
| Function | Change | Notes |
|---|---|---|
Cancelnetwerk/dns/DNSRequestChild.cpp |
modified | |
ifnetwerk/dns/DNSRequestChild.cpp |
modified |
Files Changed
netwerk/dns/DNSRequestChild.cpp
Audit Directions
- Captured raw actor/object pointers in dispatched runnablesLook for
NS_NewRunnableFunctionor lambda captures of raw IPC-actor or object pointers whose liveness is only checked inside the deferred body, where the object can be freed before execution. - Late liveness checksAudit code that defers
CanSendor other validity checks into asynchronous work instead of validating synchronously before queuing, especially across thread hops viaSchedulerGroup::Dispatch. - Cross-thread IPC send patternsReview other IPC senders that marshal onto the MainThread and confirm they re-dispatch the owning method (keeping strong references) rather than snapshotting raw pointers into a one-shot closure.
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;
}