CVE-2025-1931
Overview
Background
- WebTransport
- A bidirectional client/server transport API exposed to web content. In Firefox its
WebTransportDOM object lives in the content process and is driven over IPC by a socket-process/parent actor. - `WebTransportChild`
- The content-process IPDL child actor that receives
Recv*messages (remote closed, new streams, datagrams, stream reset) and forwards them to the DOMWebTransportobject via itsmTransportmember. - RefPtr / strong reference
- Gecko’s intrusively ref-counted smart pointer. Holding a local
RefPtrkeeps an object alive for the duration of a call even if other references are dropped mid-call. - Re-entrant destruction
- A callback that, as a side effect, can cause the object it was dispatched to (or the member holding it) to be released, so a bare member pointer can dangle before the call returns.
Root Cause Analysis
Each Recv* handler in dom/webtransport/child/WebTransportChild.cpp checked if (mTransport) and then immediately called a method on the raw member (mTransport->RemoteClosed(...), mTransport->NewBidirectionalStream(...), mTransport->NewUnidirectionalStream(...), mTransport->NewDatagramReceived(...), mTransport->OnStreamResetOrStopSending(...)). The invariant being violated is that mTransport must remain alive for the full duration of the forwarded call. These WebTransport methods can, as a side effect, tear down the connection — dispatching events to script, resolving/rejecting promises, or otherwise driving state that releases the last strong reference to the WebTransport object and clears/drops mTransport. When that happens re-entrantly, the in-flight mTransport->... call continues operating on freed memory: a use-after-free in the content process.
The patch takes a local strong reference before each dispatch — RefPtr<WebTransport> self(mTransport); self->RemoteClosed(...) (and the analogous change in the other four handlers). The local RefPtr guarantees the WebTransport instance stays alive until the handler returns, even if the message processing drops every other reference to it. This is the canonical Gecko lifetime fix: convert a transient raw-pointer dereference of a ref-counted member into a kept-alive RefPtr over the call.
mTransport member across a call that can re-entrantly destroy the WebTransport object, so a well-timed teardown freed the object mid-call; keeping a local RefPtr<WebTransport> alive for the duration removes the dangling dereference.Attack Path
- Attacker server Lures content into opening a WebTransport session to an attacker-controlled endpoint (web-reachable; WebTransport is script-exposed).
- Trigger teardown-in-callback
The server sends a sequence (e.g. remote-close, incoming stream, datagram, or stream reset) that causes the DOM
WebTransportmethod to run script/promise logic which drops the last reference and clearsmTransportduring the call (inferred: exact closing sequence not shown by the diff). - Dangling use
The still-executing
mTransport->...call continues on the just-freedWebTransportobject. - Primitive Use-after-free in the content process: a potentially exploitable crash, and with heap grooming a controllable freed-object reuse (memory-corruption primitive; exploitability beyond crash is inferred).
Impact Assessment
WebTransport object leading to a potentially exploitable crash; escalation to code execution would require shaping the freed allocation and is not demonstrated by the diff (inferred). Confidentiality/integrity are rated none in the vector, consistent with a crash-grade availability impact that may be upgradable with additional work.Changed Functions
| Function | Change | Notes |
|---|---|---|
ifdom/webtransport/child/WebTransportChild.cpp |
modified |
Files Changed
dom/webtransport/child/WebTransportChild.cpp
Audit Directions
- Other IPDL actors forwarding to ref-counted membersAudit
Recv*handlers that doif (mFoo) mFoo->Method(...)on a ref-counted DOM object; any that can re-entrantly releasemFooneed the same local-RefPtrguard. - WebTransport teardown pathsTrace which
WebTransportmethods can synchronously run script or resolve promises, and verify all of their callers hold the object alive. - `ActorDestroy` / `CloseAll` interplayCheck whether actor destruction can null
mTransportconcurrently with message dispatch and that the newRefPtrfully covers those windows.
Patch
diff --git a/dom/webtransport/child/WebTransportChild.cpp b/dom/webtransport/child/WebTransportChild.cpp
index 12714b003e9..e7c279398cf 100644
--- a/dom/webtransport/child/WebTransportChild.cpp
+++ b/dom/webtransport/child/WebTransportChild.cpp
@@ -34,7 +34,8 @@ void WebTransportChild::CloseAll() {
::mozilla::ipc::IPCResult WebTransportChild::RecvRemoteClosed(
const bool& aCleanly, const uint32_t& aCode, const nsACString& aReason) {
if (mTransport) {
- mTransport->RemoteClosed(aCleanly, aCode, aReason);
+ RefPtr<WebTransport> self(mTransport);
+ self->RemoteClosed(aCleanly, aCode, aReason);
}
return IPC_OK();
}
@@ -43,7 +44,8 @@ void WebTransportChild::CloseAll() {
const uint64_t& aStreamId, const RefPtr<DataPipeReceiver>& aIncoming,
const RefPtr<DataPipeSender>& aOutgoing) {
if (mTransport) {
- mTransport->NewBidirectionalStream(aStreamId, aIncoming, aOutgoing);
+ RefPtr<WebTransport> self(mTransport);
+ self->NewBidirectionalStream(aStreamId, aIncoming, aOutgoing);
}
return IPC_OK();
}
@@ -51,7 +53,8 @@ void WebTransportChild::CloseAll() {
::mozilla::ipc::IPCResult WebTransportChild::RecvIncomingUnidirectionalStream(
const uint64_t& aStreamId, const RefPtr<DataPipeReceiver>& aStream) {
if (mTransport) {
- mTransport->NewUnidirectionalStream(aStreamId, aStream);
+ RefPtr<WebTransport> self(mTransport);
+ self->NewUnidirectionalStream(aStreamId, aStream);
}
return IPC_OK();
}
@@ -59,7 +62,8 @@ void WebTransportChild::CloseAll() {
::mozilla::ipc::IPCResult WebTransportChild::RecvIncomingDatagram(
nsTArray<uint8_t>&& aData, const TimeStamp& aRecvTimeStamp) {
if (mTransport) {
- mTransport->NewDatagramReceived(std::move(aData), aRecvTimeStamp);
+ RefPtr<WebTransport> self(mTransport);
+ self->NewDatagramReceived(std::move(aData), aRecvTimeStamp);
}
return IPC_OK();
}
@@ -67,7 +71,8 @@ void WebTransportChild::CloseAll() {
::mozilla::ipc::IPCResult WebTransportChild::RecvOnStreamResetOrStopSending(
const uint64_t& aStreamId, const StreamResetOrStopSendingError& aError) {
if (mTransport) {
- mTransport->OnStreamResetOrStopSending(aStreamId, aError);
+ RefPtr<WebTransport> self(mTransport);
+ self->OnStreamResetOrStopSending(aStreamId, aError);
}
return IPC_OK();
}