Use-After-Free in PCameras via [@ AggregateCapturer::OnFrame]
Categories
(Core :: WebRTC: Audio/Video, defect, P1)
Tracking
()
| 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)
|
37.53 KB,
text/plain
|
Details | |
|
130 bytes,
application/json
|
Details | |
|
620 bytes,
text/html
|
Details | |
|
13.56 KB,
patch
|
Details | Diff | Splinter Review | |
|
48 bytes,
text/x-phabricator-request
|
dveditz
:
sec-approval+
|
Details | Review |
|
48 bytes,
text/x-phabricator-request
|
Details | Review | |
|
48 bytes,
text/x-phabricator-request
|
phab-bot
:
approval-mozilla-beta+
|
Details | Review |
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
- Content process calls
navigator.mediaDevices.getUserMedia({video}), setting up a legitimatePCamerasactor. This createsCamerasParent#1 in the parent, which allocates the camera device → createsAggregateCapturer, stores it in staticsCapturers, and adds{parent=#1, streamId=0}tomStreams. Camera backend thread starts delivering frames viaOnFrame. - Compromised content creates a NEW thread, calls
BackgroundChild::GetOrCreateForCurrentThread()→ fresh PBackground channel. CallsSendPCamerasConstructor()→ parent createsCamerasParent#2 (anchor). - Content sends
AllocateCapture(CameraEngine, "fake-video-source-0", 0)from actor #2. Parent'sGetOrCreateCapturer(line 1329) iterates*mCapturers(the shared static), finds the existing capturer with matchingmUniqueId, and callscapturer->AddStream(this, ...)(line 1347). NowmStreamscontains entries for BOTH parents. - Content sends
StartCapturefrom actor #2 → setsmStarted=truesoOnFramecollects its pointer. - Content creates a THIRD thread (victim), repeats steps 2-4 →
CamerasParent#3 joins the same capturer. - Capture backend thread (T10) calls
OnFrame. UndermStreams.Lock(), it copies raw pointers{#1, #2, #3}into localparentsAndIds. Lock released. - Content closes victim thread's PBackground channel via
BackgroundChild::CloseForCurrentThread(). Parent IPC machinery invokesCamerasParent::ActorDestroyon #3. ActorDestroydispatchesCloseEnginestomVideoCaptureThreadviaNewRunnableMethod(holding a strong ref).- VideoCapture thread runs
CloseEngines→capturer->RemoveStreamsFor(this=#3). ReturnsmNumRemainingStreams=2. Capturer survives.~AggregateCapturerdoes NOT run.DeRegisterCaptureDataCallbackdoes NOT run. No synchronization with T10's in-flightOnFrame. CloseEnginesreturns. Runnable destructor releases its strong ref. Refcount hits zero. Destruction proxied to PBackground thread.- IPDL Background thread (T9) runs
ProxyDeleteVoidRunnable::Run()→ frees the 288-byteCamerasParent#3. - Capture backend thread (T10) reaches the iteration for
parent = #3. Callsparent->GetBuffer(...)→ readsmShmemPoolsDataMutex from freed memory → heap-use-after-free. - In a non-ASAN build with heap spray:
GetBufferproceeds →new DeliverFrameRunnable(parent, ...)callsAddRef()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 byPBackground - 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 (triggersActorDestroy) - 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 viamPBackgroundEventTargetload, and virtual call through attacker-controlled pointer viatarget->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):
InitFrameBufferPropertiescallsToI420()(full-frame format conversion, O(width×height)); per-iterationCopyVideoFrameBuffersperforms 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 ...
}
| Reporter | ||
Comment 1•6 months ago
|
||
| Reporter | ||
Comment 2•6 months ago
|
||
| Reporter | ||
Comment 3•6 months ago
|
||
Note that the patch modifies the parent process. Please review to ensure that this is not an introduced vulnerability.
Updated•6 months ago
|
| Assignee | ||
Comment 4•6 months ago
|
||
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 | ||
Comment 5•6 months ago
|
||
| Assignee | ||
Comment 6•6 months ago
|
||
Comment 7•6 months ago
|
||
Set release status flags based on info from the regressing bug 1771789
Updated•6 months ago
|
| Assignee | ||
Comment 8•6 months ago
|
||
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:
ReleaseStreamimmediately followed by__delete__. - Then
CamerasParentwill callAggregateCapturer::RemoveStreamafter a bounce to the video capture thread. Assuming__delete__has taken effect and released theCamerasParentinstance 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::RemoveStreamand 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.
- 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
TL;DR seems doable
Updated•6 months ago
|
Updated•6 months ago
|
| Assignee | ||
Comment 9•6 months ago
|
||
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
Comment 10•6 months ago
|
||
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
Updated•6 months ago
|
| Assignee | ||
Comment 11•6 months ago
|
||
Original Revision: https://phabricator.services.mozilla.com/D286868
Updated•6 months ago
|
Comment 12•6 months ago
|
||
Updated•6 months ago
|
Comment 13•6 months ago
|
||
Updated•6 months ago
|
Updated•6 months ago
|
Comment 14•6 months ago
|
||
| uplift | ||
Updated•6 months ago
|
Updated•6 months ago
|
Comment 16•4 months ago
|
||
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.
| Assignee | ||
Updated•4 months ago
|
Comment 17•4 months ago
|
||
Comment 18•4 months ago
|
||
Updated•4 months ago
|
Description
•