Closed Bug 2021925 Opened 6 months ago Closed 6 months ago

Use-After-Free in PCameras via [@ AggregateCapturer::OnFrame]

Categories

(Core :: WebRTC: Audio/Video, defect, P1)

defect

Tracking

()

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

People

(Reporter: truber, Assigned: pehrsons)

References

(Regression)

Details

(6 keywords, Whiteboard: [adv-main149+r])

Attachments

(7 files)

Attached file crash_stack.txt —

Summary

Heap use-after-free in the Firefox parent process. AggregateCapturer::OnFrame() at /firefox/dom/media/systemservices/CamerasParent.cpp line 605 copies raw CamerasParent* pointers out of the mStreams DataMutex-guarded container, releases the lock, then dereferences the pointers. When multiple CamerasParent actors (from the same or different content processes) share the same AggregateCapturer via the process-wide sCapturers singleton, and one actor is torn down while OnFrame is in-flight, CloseEngines removes that parent's streams but leaves the capturer alive — bypassing the only synchronization barrier (DeRegisterCaptureDataCallback, which is only invoked from ~AggregateCapturer). The CamerasParent is then freed on the PBackground thread while OnFrame on the capture-backend thread still holds the stale raw pointer.

Affected Code

File: /firefox/dom/media/systemservices/CamerasParent.cpp, lines 605-692

void AggregateCapturer::OnFrame(const webrtc::VideoFrame& aVideoFrame) {
  std::multimap<CamerasParent*, int> parentsAndIds;
  {
    auto streamsGuard = mStreams.Lock();        // <-- lock acquired
    for (auto& stream : *streamsGuard) {
      // ... frame rate throttling ...
      parentsAndIds.insert({stream->mParent, stream->mId});  // line 634: RAW POINTER COPIED OUT
    }
  }                                             // <-- line 636: LOCK RELEASED

  // ... InitFrameBufferProperties (includes ToI420 conversion — O(w*h)) ...

  for (auto it = parentsAndIds.begin(); it != parentsAndIds.end();) {
    const auto& parent = it->first;             // <-- STALE RAW POINTER
    // ...
    ShmemBuffer shMemBuffer =
        parent->GetBuffer(mCaptureId, properties.bufferSize());   // line 666: UAF read (mShmemPools.Lock())
    // ...
    runnable = new DeliverFrameRunnable(parent, ...);             // line 681/686: UAF write (RefPtr ctor → AddRef())
    // ...
    nsIEventTarget* target = parent->GetBackgroundEventTarget();  // line 690: UAF read (mPBackgroundEventTarget)
    target->Dispatch(runnable, NS_DISPATCH_NORMAL);               // line 691: virtual call through attacker-controllable pointer
  }
}

Why vulnerable: stream->mParent is declared as CamerasParent* const (CamerasParent.h line 93) — a raw pointer with no ownership. The mStreams mutex protects the nsTArray<std::unique_ptr<Stream>> container, but once the raw pointer is copied into the local parentsAndIds multimap, the mutex provides zero protection for the pointee's lifetime.

The teardown path is:

// CamerasParent.cpp:712
void CamerasParent::CloseEngines() {
  for (const auto& capturer : Reversed(*mCapturers)) {
    auto removed = capturer->RemoveStreamsFor(this);    // acquires mStreams lock, removes entries
    if (removed.mNumRemainingStreams == 0) {            // FALSE when another parent has streams
      // ... would destroy capturer → ~AggregateCapturer → DeRegisterCaptureDataCallback ...
      // ... but THIS BRANCH IS SKIPPED ...
    }
  }
}

And RemoveStreamsFor (line 480) acquires mStreams.Lock(), removes entries, and returns — but OnFrame already copied the pointer out before RemoveStreamsFor acquired the lock. Classic TOCTOU.

Correct Pattern

void AggregateCapturer::OnFrame(const webrtc::VideoFrame& aVideoFrame) {
  std::multimap<RefPtr<CamerasParent>, int> parentsAndIds;  // <-- RefPtr, not raw
  {
    auto streamsGuard = mStreams.Lock();
    for (auto& stream : *streamsGuard) {
      // ...
      parentsAndIds.insert({RefPtr(stream->mParent), stream->mId});  // AddRef under lock
    }
  }
  // Now parentsAndIds owns strong refs; CamerasParent cannot be freed
  // until this function returns.
  // ...
}

This is safe because CamerasParent uses NS_INLINE_DECL_THREADSAFE_REFCOUNTING_WITH_DELETE_ON_EVENT_TARGET (CamerasParent.h line 148) — AddRef/Release are atomic. Taking the ref while mStreams is locked guarantees the pointee is still alive (the pointer could only be removed from mStreams by RemoveStreamsFor/RemoveStream, which take the same lock; and the CamerasParent cannot be freed until after RemoveStreamsFor returns, because the NewRunnableMethod runnable that called CloseEngines holds its own strong ref).

Exploit Chain

  1. Content process calls navigator.mediaDevices.getUserMedia({video}), setting up a legitimate PCameras actor. This creates CamerasParent #1 in the parent, which allocates the camera device → creates AggregateCapturer, stores it in static sCapturers, and adds {parent=#1, streamId=0} to mStreams. Camera backend thread starts delivering frames via OnFrame.
  2. Compromised content creates a NEW thread, calls BackgroundChild::GetOrCreateForCurrentThread() → fresh PBackground channel. Calls SendPCamerasConstructor() → parent creates CamerasParent #2 (anchor).
  3. Content sends AllocateCapture(CameraEngine, "fake-video-source-0", 0) from actor #2. Parent's GetOrCreateCapturer (line 1329) iterates *mCapturers (the shared static), finds the existing capturer with matching mUniqueId, and calls capturer->AddStream(this, ...) (line 1347). Now mStreams contains entries for BOTH parents.
  4. Content sends StartCapture from actor #2 → sets mStarted=true so OnFrame collects its pointer.
  5. Content creates a THIRD thread (victim), repeats steps 2-4 → CamerasParent #3 joins the same capturer.
  6. Capture backend thread (T10) calls OnFrame. Under mStreams.Lock(), it copies raw pointers {#1, #2, #3} into local parentsAndIds. Lock released.
  7. Content closes victim thread's PBackground channel via BackgroundChild::CloseForCurrentThread(). Parent IPC machinery invokes CamerasParent::ActorDestroy on #3.
  8. ActorDestroy dispatches CloseEngines to mVideoCaptureThread via NewRunnableMethod (holding a strong ref).
  9. VideoCapture thread runs CloseEngines → capturer->RemoveStreamsFor(this=#3). Returns mNumRemainingStreams=2. Capturer survives. ~AggregateCapturer does NOT run. DeRegisterCaptureDataCallback does NOT run. No synchronization with T10's in-flight OnFrame.
  10. CloseEngines returns. Runnable destructor releases its strong ref. Refcount hits zero. Destruction proxied to PBackground thread.
  11. IPDL Background thread (T9) runs ProxyDeleteVoidRunnable::Run() → frees the 288-byte CamerasParent #3.
  12. Capture backend thread (T10) reaches the iteration for parent = #3. Calls parent->GetBuffer(...) → reads mShmemPools DataMutex from freed memory → heap-use-after-free.
  13. In a non-ASAN build with heap spray: GetBuffer proceeds → new DeliverFrameRunnable(parent, ...) calls AddRef() on attacker-controlled memory → parent->GetBackgroundEventTarget() reads attacker-controlled pointer → target->Dispatch() performs virtual call through attacker-controlled vtable → arbitrary code execution in parent process.

IPC Path

  • IPDL protocol: PCameras (/firefox/dom/media/systemservices/PCameras.ipdl), managed by PBackground
  • Parent actor: mozilla::camera::CamerasParent (/firefox/dom/media/systemservices/CamerasParent.cpp). Allocated in /firefox/ipc/glue/BackgroundParentImpl.cpp:632 (AllocPCamerasParent → AssertIsInMainProcess())
  • Child actor: mozilla::camera::CamerasChild (/firefox/dom/media/systemservices/CamerasChild.cpp)
  • Messages used (child→parent): PBackground::PCamerasConstructor, PCameras::AllocateCapture, PCameras::StartCapture, PBackground channel close (triggers ActorDestroy)
  • Process boundary: Content process sends all of the above. Parent process runs CamerasParent, AggregateCapturer, and the capture backend.

Security Impact

  • Severity: High (sec-high). Heap use-after-free in the parent (chrome) process, triggerable from a compromised content process via IPC. Sandbox escape.
  • Attacker capability: With heap grooming to reclaim the freed 288-byte slot: write primitive via AddRef(), read primitive via mPBackgroundEventTarget load, and virtual call through attacker-controlled pointer via target->Dispatch().
  • Preconditions: Camera permission granted; compromised content process. Works with any capture backend (OnFrame always runs on a thread distinct from mVideoCaptureThread).
  • Race window (without widener): InitFrameBufferProperties calls ToI420() (full-frame format conversion, O(width×height)); per-iteration CopyVideoFrameBuffers performs full-frame memcpy. Natural window is microseconds-to-milliseconds scale with high-resolution frames.

ASAN Report

==355990==ERROR: AddressSanitizer: heap-use-after-free on address 0x747beb022940 at pc 0x735bc96b6320 bp 0x735b95ccb590 sp 0x735b95ccb588
READ of size 1 at 0x747beb022940 thread T10
    #0 0x735bc96b631f in mozilla::camera::AggregateCapturer::OnFrame(webrtc::VideoFrame const&) /firefox/dom/media/systemservices/CamerasParent.cpp:684:7
    #1 0x735bca4f2d4e in webrtc::videocapturemodule::VideoCaptureImpl::DeliverCapturedFrame(webrtc::VideoFrame&) /firefox/third_party/libwebrtc/modules/video_capture/video_capture_impl.cc:151:19
    #2 0x735bc96d0b1f in webrtc::videocapturemodule::VideoCaptureFake::OnGeneratedImage(RefPtr<mozilla::layers::Image> const&, mozilla::TimeStamp) /firefox/dom/media/systemservices/fake_video_capture/video_capture_fake.cc:78:3
    #19 0x735bbfc1d642 in mozilla::TaskQueue::Runner::Run() /firefox/xpcom/threads/TaskQueue.cpp:311:20

0x747beb022940 is located 0 bytes inside of 288-byte region [0x747beb022940,0x747beb022a60)
freed by thread T9 here:
    #0 0x559b5f6f96a6 in __interceptor_free _asan_rtl_:3
    #1 0x735bbfad2dc1 in mozilla::detail::ProxyDeleteVoidRunnable::Run() /firefox/xpcom/base/nsISupportsImpl.cpp:95:7

previously allocated by thread T9 here:
    #0 0x559b5f6f9944 in __interceptor_malloc _asan_rtl_:3
    #1 0x559b5f74f355 in moz_xmalloc /firefox/memory/mozalloc/mozalloc.cpp:51:15
    #4 0x735bc96c6147 in mozilla::camera::CamerasParent::Create() /firefox/dom/media/systemservices/CamerasParent.cpp:1697:10
    #5 0x735bc185e024 in mozilla::ipc::PBackgroundParent::OnMessageReceived(IPC::Message const&) /firefox/obj-x86_64-pc-linux-gnu/ipc/ipdl/PBackgroundParent.cpp:3387:45

SUMMARY: AddressSanitizer: heap-use-after-free (/firefox/obj-x86_64-pc-linux-gnu/dist/bin/libxul.so+0x1c8b631f)
Shadow bytes around the buggy address:
=>0x747beb022900: fa fa fa fa fa fa fa fa[fd]fd fd fd fd fd fd fd

Stderr timeline:

[EXPLOIT-PARENT] OnFrame: collected 3 parents, entering race window
[EXPLOIT-PARENT] CloseEngines: this=0x747beb022dc0 RemoveStreamsFor -> remaining=2
[EXPLOIT-PARENT] CloseEngines: this=0x747beb022940 RemoveStreamsFor -> remaining=1
[EXPLOIT-PARENT] ~CamerasParent: this=0x747beb022dc0 FREED
[EXPLOIT-PARENT] ~CamerasParent: this=0x747beb022940 FREED
[EXPLOIT-PARENT] OnFrame: race window done, about to deref raw CamerasParent* pointers
==355990==ERROR: AddressSanitizer: heap-use-after-free on address 0x747beb022940

Suggested Fix

In /firefox/dom/media/systemservices/CamerasParent.cpp, change OnFrame to take strong references under the lock:

void AggregateCapturer::OnFrame(const webrtc::VideoFrame& aVideoFrame) {
  std::multimap<RefPtr<CamerasParent>, int> parentsAndIds;
  {
    auto streamsGuard = mStreams.Lock();
    for (auto& stream : *streamsGuard) {
      // ... existing frame-rate throttling ...
      parentsAndIds.insert({RefPtr(stream->mParent), stream->mId});
    }
  }
  // parentsAndIds now holds strong refs; safe to dereference without the lock.
  // ... rest of function unchanged ...
}
Attached file prefs.json —
Attached file test.html —
Attached patch exploit.patch — — Splinter Review

Note that the patch modifies the parent process. Please review to ensure that this is not an introduced vulnerability.

Group: dom-core-security → media-core-security
Component: DOM: Device Interfaces → Audio/Video
Keywords: csectype-uaf

The exploit modifies the parent process (AggregateCapturer::OnFrame) to make it (a lot) easier to flip the race and trigger the UAF. I wrote a gtest without that bit and it's easy to trigger the race and UAF. I am spinning on AggregateCapturer::OnFrame though, which Firefox wouldn't be doing. There are probably other techniques to increase likelihood of hitting the trigger with a low-rate OnFrame, like setting up and tearing down PCameras channels at a high rate.

I haven't checked whether it's possible for a content process to have an unbound number of background channels to the parent. It seems to me if one has exploited the content process, one can create any numer of PCameras channels for a single PBackground channel like so.

Assignee: nobody → apehrson
Severity: -- → S2
Status: NEW → ASSIGNED
Component: Audio/Video → WebRTC: Audio/Video
Keywords: regression
Priority: -- → P1
Regressed by: 1771789
Depends on: 2022178
Attached file (secure) —
Attached file (secure) —

Set release status flags based on info from the regressing bug 1771789

My assessment on how hard it would be to trigger this UAF in practice from an exploited content process is:

  • The content process would have to send over PCameras: ReleaseStream immediately followed by __delete__.
  • Then CamerasParent will call AggregateCapturer::RemoveStream after a bounce to the video capture thread. Assuming __delete__ has taken effect and released the CamerasParent instance from the IPC channel with the right timing, there's still the strong ref on the video capture thread that needs a bounce over to the PBackground thread before the dtor can run.
  • To hit the UAF, AggregateCapturer::RemoveStream and the CamerasParent dtor have to both run while the capture thread is here.
    • There's a frame-buffer-copy path in there that is not trivially short. It is also multiplied by the number of streams registered on the AggregateCapturer. Seems like something an attacker can use to increase exposure to the race.

TL;DR seems doable

Comment on attachment 9551373 [details]
(secure)

Security Approval Request

  • How easily could an exploit be constructed based on the patch?: Not so easy. Needs an exploited content process. Also see comment 8 for triggering the UAF.
  • 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?: beta, release
  • If not all supported branches, which bug introduced the flaw?: Bug 1771789
  • Do you have backports for the affected branches?: No
  • If not, how different, hard to create, and risky will they be?: Simple
  • How likely is this patch to cause regressions; how much testing does it need?: Not likely
  • Is the patch ready to land after security approval is given?: Yes
  • Is Android affected?: Yes
Attachment #9551373 - Flags: sec-approval?

Comment on attachment 9551373 [details]
(secure)

sec-approval+ to land now and uplift to beta
please hold off on landing tests until 2026-05-05 or later

Attachment #9551373 - Flags: sec-approval?
Attachment #9551373 - Flags: sec-approval+
Attachment #9551373 - Flags: approval-mozilla-beta+
Whiteboard: [reminder-test 2026-05-05]
Attachment #9552132 - Flags: approval-mozilla-beta?
Attachment #9551373 - Flags: approval-mozilla-beta+
Group: media-core-security → core-security-release
Status: ASSIGNED → RESOLVED
Closed: 6 months ago
Resolution: --- → FIXED
Target Milestone: --- → 150 Branch
Attachment #9552132 - Flags: approval-mozilla-beta? → approval-mozilla-beta+
QA Whiteboard: [sec] [uplift] [qa-triage-done-c150/b149]
Whiteboard: [reminder-test 2026-05-05] → [reminder-test 2026-05-05][adv-main149+r]
Duplicate of this bug: 2024235

2 months ago, dveditz placed a reminder on the bug using the whiteboard tag [reminder-test 2026-05-05] .

pehrsons, please refer to the original comment to better understand the reason for the reminder.

Flags: needinfo?(apehrson)
Whiteboard: [reminder-test 2026-05-05][adv-main149+r] → [adv-main149+r]
Flags: needinfo?(apehrson)
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: