Closed Bug 2022733 Opened 3 months ago Closed 3 months ago

Heap-use-after-free in PWebTransport through [@ mozilla::net::WebTransportSessionProxy::GetServerCertificateHashes]

Categories

(Core :: Networking: WebSockets, defect, P1)

defect

Tracking

()

RESOLVED FIXED
150 Branch
Tracking Status
firefox-esr115 --- unaffected
firefox-esr140 149+ fixed
firefox148 --- wontfix
firefox149 + fixed
firefox150 + fixed

People

(Reporter: tsmith, Assigned: kershaw)

Details

(4 keywords, Whiteboard: [necko-triaged][necko-priority-queue][adv-main149+r][adv-ESR140.9+r])

Attachments

(6 files)

Attached file crash_stack.txt

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 RecvCloseCloseSession 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

  1. Compromised content process constructs new WebTransport(url, { serverCertificateHashes: [...10000 hashes...] }) and sends PBackground::CreateWebTransportParent with the hash array.
  2. Parent PBackground thread (T9) receives CreateWebTransportParent, allocates 10000 WebTransportHash objects (WebTransportParent.cpp:88, each a 40-byte heap allocation with a vtable at offset 0), and dispatches an InvokeAsync to the socket thread.
  3. Parent socket thread (T8) binds the PWebTransportParent endpoint and dispatches an AsyncConnect runnable to the main thread.
  4. Content process sends PWebTransport::Close(0, "race") immediately after the child endpoint binds. This IPC message is queued on the socket-thread IPC channel.
  5. Parent main thread (T0) runs AsyncConnectWithClient, which copies the 10000 RefPtr<WebTransportHash> into mServerCertHashes (line 143), transitions to NEGOTIATING (line 148), and calls mChannel->AsyncOpen().
  6. The HTTP channel on main thread begins connection setup asynchronously. When it reaches nsHttpChannel::ContinueOnBeforeConnect (nsHttpChannel.cpp:1249), it calls wtconSettings->GetServerCertificateHashes(aServerCertHashes), entering AppendElements(mServerCertHashes) — a loop over 10000 slots that copy-constructs each RefPtr, dereferencing each WebTransportHash vtable to call AddRef().
  7. Concurrently, socket thread (T8) dequeues the Close IPC message, runs RecvCloseCloseSession. CloseSession acquires mMutex and calls mServerCertHashes.Clear() at line 237. Clear() iterates the array, calling ~RefPtrRelease() on each hash. Since the main thread hasn't finished its AddRef sweep yet, many hashes still have refcount==1 → Release() drops to 0 → delete this → 40 bytes freed.
  8. Main thread (T0) reads the next array slot — a stale RefPtr::mRawPtr pointing into a freed 40-byte region. It dereferences offset 0 to fetch the vtable pointer, then dereferences the vtable to fetch the AddRef slot, then calls through it. ASAN catches the first read: heap-use-after-free, READ of size 8 at offset 0.
  9. 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 AddRef virtual 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 at WebTransportParent.cpp:123 inside InvokeAsync(mSocketThread, ...)
  • Message flow: Content DOM → WebTransportChild::SendClose → [IPC wire] → parent socket thread PWebTransportParent::OnMessageReceivedWebTransportParent::RecvClose (line 169) → mWebTransport->CloseSession()WebTransportSessionProxy::CloseSessionmServerCertHashes.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()).

Attached patch exploit.patchSplinter Review
Attached file test.html
Severity: -- → S2
Priority: -- → P1
Whiteboard: [necko-triaged][necko-priority-queue]
Assignee: nobody → kershaw
Attached file (secure)

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
Attachment #9552150 - Flags: sec-approval?

Comment on attachment 9552150 [details]
(secure)

sec-approval+ to land now and uplift

Attachment #9552150 - Flags: sec-approval? → sec-approval+
Pushed by kjang@mozilla.com: https://github.com/mozilla-firefox/firefox/commit/ddcc3a9be98f https://hg.mozilla.org/integration/autoland/rev/15e62f963525 WebTransportSessionProxy::mServerCertHashes cleanup, r=necko-reviewers,valentin

:kershaw can you add uplift requests here for beta, esr15, esr140 so that it makes it for 149?

Flags: needinfo?(kershaw)
Group: network-core-security → core-security-release
Status: NEW → RESOLVED
Closed: 3 months ago
Resolution: --- → FIXED
Target Milestone: --- → 150 Branch

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
Attachment #9553196 - Flags: approval-mozilla-beta?
Flags: qe-verify+
Attached file (secure)

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
Attachment #9553197 - Flags: approval-mozilla-esr140?
Attached file (secure)

(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.

Flags: needinfo?(kershaw)

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?”

Flags: needinfo?(kershaw)

(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.

Flags: needinfo?(kershaw)
Attachment #9553196 - Flags: approval-mozilla-beta? → approval-mozilla-beta+

(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.

Attachment #9553197 - Flags: approval-mozilla-esr140? → approval-mozilla-esr140+
QA Whiteboard: [qa-triage-done-c150/b149]
Whiteboard: [necko-triaged][necko-priority-queue] → [necko-triaged][necko-priority-queue][adv-main149+r][adv-ESR140.9+r]
QA Whiteboard: [qa-triage-done-c150/b149] → [sec] [uplift] [qa-triage-done-c150/b149]
Flags: qe-verify+
Group: core-security-release
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Created:
Updated:
Size: