Heap-use-after-free in PWebTransport through [@ mozilla::net::WebTransportSessionProxy::GetServerCertificateHashes]
Categories
(Core :: Networking: WebSockets, defect, P1)
Tracking
()
People
(Reporter: tsmith, Assigned: kershaw)
Details
(4 keywords, Whiteboard: [necko-triaged][necko-priority-queue][adv-main149+r][adv-ESR140.9+r])
Attachments
(6 files)
|
10.92 KB,
text/plain
|
Details | |
|
3.18 KB,
patch
|
Details | Diff | Splinter Review | |
|
4.42 KB,
text/html
|
Details | |
|
48 bytes,
text/x-phabricator-request
|
dveditz
:
sec-approval+
|
Details | Review |
|
48 bytes,
text/x-phabricator-request
|
phab-bot
:
approval-mozilla-beta+
|
Details | Review |
|
48 bytes,
text/x-phabricator-request
|
phab-bot
:
approval-mozilla-esr140+
|
Details | Review |
Summary
Cross-thread heap-use-after-free in the Firefox parent process. WebTransportSessionProxy::mServerCertHashes is accessed without synchronization on the main thread while being destroyed on the socket thread. The PWebTransport::Close IPC message — sent by a compromised content process — is handled on the socket thread (where the IPC endpoint is bound) and calls CloseSession(), which clears mServerCertHashes. Concurrently, nsHttpChannel::ContinueOnBeforeConnect on the main thread reads the same array via GetServerCertificateHashes() without acquiring the proxy mutex. The main thread dereferences freed WebTransportHash objects, reading their vtable pointers to make virtual AddRef() calls.
Affected Code
File: /firefox/netwerk/protocol/webtransport/WebTransportSessionProxy.cpp, line 271–276
NS_IMETHODIMP WebTransportSessionProxy::GetServerCertificateHashes(
nsTArray<RefPtr<nsIWebTransportHash>>& aServerCertHashes) {
aServerCertHashes.Clear();
aServerCertHashes.AppendElements(mServerCertHashes); // line 274: NO MUTEX
return NS_OK;
}
File: /firefox/netwerk/protocol/webtransport/WebTransportSessionProxy.cpp, line 229–238
NS_IMETHODIMP
WebTransportSessionProxy::CloseSession(uint32_t status,
const nsACString& reason) {
MutexAutoLock lock(mMutex);
MOZ_ASSERT(mTarget->IsOnCurrentThread()); // debug-only; mTarget = main thread
mCloseStatus = status;
mReason = reason;
mListener = nullptr;
mPendingEvents.Clear();
mServerCertHashes.Clear(); // line 237: runs on SOCKET THREAD, holds mutex
File: /firefox/netwerk/protocol/webtransport/WebTransportSessionProxy.h, line 198
nsCOMPtr<nsIEventTarget> mTarget MOZ_GUARDED_BY(mMutex);
nsTArray<RefPtr<nsIWebTransportHash>> mServerCertHashes; // NOT GUARDED
Why it's vulnerable: Every surrounding member (lines 185–197) is annotated MOZ_GUARDED_BY(mMutex), but mServerCertHashes was omitted. The MOZ_ASSERT(mTarget->IsOnCurrentThread()) at line 232 proves the developer assumed CloseSession would run on the main thread — but WebTransportParent binds its IPC endpoint on mSocketThread (/firefox/dom/webtransport/parent/WebTransportParent.cpp:123), so RecvClose → CloseSession runs on the socket thread. Since MOZ_ASSERT is debug-only, release builds silently execute the cross-thread clear. The mutex held by CloseSession is useless because GetServerCertificateHashes never acquires it.
Correct Pattern
NS_IMETHODIMP WebTransportSessionProxy::GetServerCertificateHashes(
nsTArray<RefPtr<nsIWebTransportHash>>& aServerCertHashes) {
MutexAutoLock lock(mMutex);
aServerCertHashes.Clear();
aServerCertHashes.AppendElements(mServerCertHashes);
return NS_OK;
}
And in the header:
nsTArray<RefPtr<nsIWebTransportHash>> mServerCertHashes MOZ_GUARDED_BY(mMutex);
Exploit Chain
- Compromised content process constructs
new WebTransport(url, { serverCertificateHashes: [...10000 hashes...] })and sendsPBackground::CreateWebTransportParentwith the hash array. - Parent PBackground thread (T9) receives
CreateWebTransportParent, allocates 10000WebTransportHashobjects (WebTransportParent.cpp:88, each a 40-byte heap allocation with a vtable at offset 0), and dispatches anInvokeAsyncto the socket thread. - Parent socket thread (T8) binds the
PWebTransportParentendpoint and dispatches anAsyncConnectrunnable to the main thread. - Content process sends
PWebTransport::Close(0, "race")immediately after the child endpoint binds. This IPC message is queued on the socket-thread IPC channel. - Parent main thread (T0) runs
AsyncConnectWithClient, which copies the 10000RefPtr<WebTransportHash>intomServerCertHashes(line 143), transitions toNEGOTIATING(line 148), and callsmChannel->AsyncOpen(). - The HTTP channel on main thread begins connection setup asynchronously. When it reaches
nsHttpChannel::ContinueOnBeforeConnect(nsHttpChannel.cpp:1249), it callswtconSettings->GetServerCertificateHashes(aServerCertHashes), enteringAppendElements(mServerCertHashes)— a loop over 10000 slots that copy-constructs eachRefPtr, dereferencing eachWebTransportHashvtable to callAddRef(). - Concurrently, socket thread (T8) dequeues the
CloseIPC message, runsRecvClose→CloseSession.CloseSessionacquiresmMutexand callsmServerCertHashes.Clear()at line 237.Clear()iterates the array, calling~RefPtr→Release()on each hash. Since the main thread hasn't finished itsAddRefsweep yet, many hashes still have refcount==1 →Release()drops to 0 →delete this→ 40 bytes freed. - Main thread (T0) reads the next array slot — a stale
RefPtr::mRawPtrpointing into a freed 40-byte region. It dereferences offset 0 to fetch the vtable pointer, then dereferences the vtable to fetch theAddRefslot, then calls through it. ASAN catches the first read: heap-use-after-free, READ of size 8 at offset 0. - Without ASAN: if the attacker has sprayed the 40-byte size class with a controlled fake object (e.g. from another content-process-controlled IPC allocation), the vtable pointer is attacker-controlled, the
AddRefvirtual call jumps to attacker-controlled code, achieving RIP hijack in the parent process.
IPC Path
- IPDL protocol:
PWebTransport(/firefox/dom/webtransport/shared/PWebTransport.ipdl) - Message:
async Close(uint32_t code, nsCString reason);— child→parent - Parent actor:
WebTransportParent(/firefox/dom/webtransport/parent/WebTransportParent.cpp) - Child actor:
WebTransportChild(content process, DOM main thread) - Parent binding thread: Socket thread (
nsSocketTransportService) — bound atWebTransportParent.cpp:123insideInvokeAsync(mSocketThread, ...) - Message flow: Content DOM →
WebTransportChild::SendClose→ [IPC wire] → parent socket threadPWebTransportParent::OnMessageReceived→WebTransportParent::RecvClose(line 169) →mWebTransport->CloseSession()→WebTransportSessionProxy::CloseSession→mServerCertHashes.Clear()on socket thread - Concurrent reader: Parent main thread →
nsHttpChannel::ContinueOnBeforeConnect(nsHttpChannel.cpp:1249) →WebTransportSessionProxy::GetServerCertificateHashes(line 274)
Security Impact
Severity: Critical — sandbox escape to parent-process code execution.
Attacker capability: A compromised content process (achieved via any renderer bug) sends two fully-legitimate IPC messages — CreateWebTransportParent with a large serverCertificateHashes array, and PWebTransport::Close — with tight timing. The parent process then performs a virtual call through a freed vtable. The freed objects are in the 40-byte jemalloc size class; the attacker can groom this size class via other IPC-triggered allocations. A controlled vtable pointer at offset 0 of the reclaimed allocation yields arbitrary RIP control in the parent process.
Preconditions:
- Compromised content process (standard assumption for sandbox-escape analysis)
- WebTransport enabled (
network.webtransport.enabled— default-enabled in current Firefox) - No real WebTransport server required — the race occurs during connection setup before any network response
Reliability: Race is won consistently within 1–2 attempts with 10000 hashes. The 10000-element AppendElements loop on the main thread provides a millisecond-scale race window during which the socket thread only needs to execute a single IPC message receipt.
Suggested Fix
Add MOZ_GUARDED_BY(mMutex) to the member declaration and acquire the mutex in all accessors. Additionally, the MOZ_ASSERT at line 232 should be promoted to MOZ_RELEASE_ASSERT or the CloseSession call in RecvClose should be dispatched to the correct thread:
// WebTransportSessionProxy.h
nsTArray<RefPtr<nsIWebTransportHash>> mServerCertHashes MOZ_GUARDED_BY(mMutex);
// WebTransportSessionProxy.cpp — GetServerCertificateHashes
NS_IMETHODIMP WebTransportSessionProxy::GetServerCertificateHashes(
nsTArray<RefPtr<nsIWebTransportHash>>& aServerCertHashes) {
MutexAutoLock lock(mMutex);
aServerCertHashes.Clear();
aServerCertHashes.AppendElements(mServerCertHashes);
return NS_OK;
}
// WebTransportSessionProxy.cpp — AsyncConnectWithClient (lines 141-144) also needs the lock
if (!aServerCertHashes.IsEmpty()) {
MutexAutoLock lock(mMutex);
mServerCertHashes.Clear();
mServerCertHashes.AppendElements(aServerCertHashes);
}
Alternatively, WebTransportParent::RecvClose (WebTransportParent.cpp:169) should dispatch CloseSession to mTarget rather than invoking it directly on the socket thread — this would honor the original design intent expressed by the MOZ_ASSERT(mTarget->IsOnCurrentThread()).
| Reporter | ||
Comment 1•3 months ago
|
||
| Reporter | ||
Comment 2•3 months ago
|
||
Updated•3 months ago
|
| Assignee | ||
Updated•3 months ago
|
| Assignee | ||
Comment 3•3 months ago
|
||
| Assignee | ||
Comment 4•3 months ago
|
||
Comment on attachment 9552150 [details]
(secure)
Security Approval Request
- How easily could an exploit be constructed based on the patch?: This requires a compromised content process, since that’s the only way to trigger PWebTransport::Close at such an early stage.
- Do comments in the patch, the check-in comment, or tests included in the patch paint a bulls-eye on the security problem?: No
- Which branches (beta, release, and/or ESR) are affected by this flaw, and do the release status flags reflect this affected/unaffected state correctly?: all
- If not all supported branches, which bug introduced the flaw?: None
- Do you have backports for the affected branches?: Yes
- If not, how different, hard to create, and risky will they be?:
- How likely is this patch to cause regressions; how much testing does it need?: Low. This patch is straightforward.
- Is the patch ready to land after security approval is given?: Yes
- Is Android affected?: Yes
Updated•3 months ago
|
Comment 5•3 months ago
|
||
Comment on attachment 9552150 [details]
(secure)
sec-approval+ to land now and uplift
Comment 7•3 months ago
•
|
||
:kershaw can you add uplift requests here for beta, esr15, esr140 so that it makes it for 149?
Comment 8•3 months ago
|
||
Comment 9•3 months ago
|
||
firefox-beta Uplift Approval Request
- User impact if declined/Reason for urgency: Possible crash
- Code covered by automated testing?: no
- Fix verified in Nightly?: yes
- Needs manual QE testing?: yes
- Steps to reproduce for manual QE testing: N/A
- Risk associated with taking this patch: low
- Explanation of risk level: Low, since this patch is straightforward
- String changes made/needed?: N/A
- Is Android affected?: yes
| Assignee | ||
Comment 10•3 months ago
|
||
Original Revision: https://phabricator.services.mozilla.com/D287298
Comment 11•3 months ago
|
||
firefox-esr140 Uplift Approval Request
- User impact if declined/Reason for urgency: Possible crash
- Code covered by automated testing?: no
- Fix verified in Nightly?: yes
- Needs manual QE testing?: yes
- Steps to reproduce for manual QE testing: N/A
- Risk associated with taking this patch: low
- Explanation of risk level: Low, since this patch is straightforward
- String changes made/needed?: N/A
- Is Android affected?: yes
| Assignee | ||
Comment 12•3 months ago
|
||
Original Revision: https://phabricator.services.mozilla.com/D287298
| Assignee | ||
Comment 13•3 months ago
|
||
(In reply to Dianna Smith [:diannaS] from comment #7)
:kershaw can you add uplift requests here for beta, esr15, esr140 so that it makes it for 149?
We don’t need to uplift this to ESR115 because that code doesn’t exist in that branch.
Comment 14•3 months ago
|
||
We noticed that the "qe-verify+" label was added. Can this issue be verified manually? If so, could you please provide specific steps for manual verification?”
| Assignee | ||
Comment 15•3 months ago
|
||
(In reply to Brindusa Tot, DTE from comment #14)
We noticed that the "qe-verify+" label was added. Can this issue be verified manually? If so, could you please provide specific steps for manual verification?”
No, this can’t be verified manually. I must have done something incorrectly.
Sorry about that.
Updated•3 months ago
|
Updated•3 months ago
|
Comment 16•3 months ago
|
||
| uplift | ||
Comment 17•3 months ago
|
||
(In reply to Kershaw Chang [:kershaw] from comment #13)
(In reply to Dianna Smith [:diannaS] from comment #7)
:kershaw can you add uplift requests here for beta, esr15, esr140 so that it makes it for 149?
We don’t need to uplift this to ESR115 because that code doesn’t exist in that branch.
Updated esr115 flags to reflect this information.
Updated•3 months ago
|
Updated•3 months ago
|
Comment 18•3 months ago
|
||
| uplift | ||
Updated•3 months ago
|
| Reporter | ||
Updated•3 months ago
|
Updated•3 months ago
|
Updated•3 months ago
|
Updated•1 month ago
|
Description
•