fix(apple): drain capture queue in destroy to avoid UAF on macOS 26#54
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
Walkthrough
Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Review rate limit: 0/1 reviews remaining, refill in 60 minutes.Comment |
There was a problem hiding this comment.
Pull request overview
This PR addresses a macOS 26 crash/UAF risk in the Apple camera backend teardown path by ensuring outstanding frame callbacks on the capture queue have completed before session/provider state is destroyed.
Changes:
- Detach the
AVCaptureVideoDataOutputsample buffer delegate and then synchronously drain the original_captureQueueduring-[CameraCaptureObjc destroy]. - Add detailed rationale comments documenting the macOS 26 capture pipeline behavior that can leave callbacks in-flight after delegate detachment.
If user code invokes close() from inside the new-frame callback, the existing dispatch_sync drain would deadlock on the capture queue. Tag the queue with dispatch_queue_set_specific and skip the drain when destroy is re-entered on it — the in-flight callback that called us is the only one that could touch the provider, and it is about to return.
There was a problem hiding this comment.
Pull request overview
This PR addresses a shutdown-time use-after-free on macOS 26 by ensuring no in-flight AVCaptureVideoDataOutput callbacks can continue running on the old capture queue while ProviderImp is being torn down.
Changes:
- Tag the
_captureQueuewith a queue-specific key to detect re-entrant teardown calls originating from the capture callback. - After detaching the sample buffer delegate, explicitly drain the original capture queue via
dispatch_sync(when safe) before continuing teardown.
|
@Auggie review |
|
Thanks for the fix~ |
Summary
On macOS 26 we observed reproducible crashes inside
ccap::ProviderImp::getFreeFrame()atstd::mutex::lock(), throwingstd::system_errorfrom anAVCaptureVideoDataOutput_Tundracallback onccap.queue:Root cause
In
-[CameraCaptureObjc destroy]we call:[_videoOutput setSampleBufferDelegate:nil queue:dispatch_get_main_queue()];setSampleBufferDelegate:queue: does not guarantee thatcaptureOutput: blocks already enqueued on the previous capturequeue have finished by the time it returns, especially when the new queue differs from the old one. On macOS 26's
reworked capture pipeline, a frame callback can still be in flight on the original
_captureQueuewhile we tear downProviderImp(and its mutexes), producing a use-after-free that surfaces as amutex::lock()system error.Fix
After detaching the delegate, explicitly drain the original capture queue with a
dispatch_syncbarrier beforedestroying session state. This guarantees no
captureOutput: callback is still running against a soon-to-be-freedProviderImp.Impact
Summary by CodeRabbit