Skip to content

Race-condition crash in HybridCameraSession.start() — attach-side sibling of #3730 (iPhone 11, iOS 18.7.7, v5.0.4) #3773

@qutrek

Description

@qutrek

What's happening?

App crashes with EXC_CRASH (SIGABRT) on iPhone 11 / iOS 18.7.7 during normal camera usage. The crashed thread is always a CoreMedia FigCaptureSessionNotificationQueue worker processing _handleConfigurationCommittedNotificationWithPayload and asserting in [AVCaptureOutput attachToFigCaptureSession:]_block_invoke.cold.1 at AVCaptureOutput.m:364. A separate thread is in HybridCameraSession.start()AVCaptureSession.startRunning()_buildAndRunGraph at the moment of the assert.

This is the attach-side sibling of #3730. #3730 (closed as fixed in v5.0.0) was the detach-side of the same race; the attach side appears not to have been addressed and is reproducible in v5.0.4.

We have 6 byte-for-byte identical crash reports from a single user from real TestFlight usage. All triggered by ordinary actions: photo retake, zoom switching, video record start, and general camera usage with no specific repro action reported. Each triggered through a different user-visible action but produced the same stack frames in the crashed thread and the same parallel HybridCameraSession.start() thread.

Hypothesis

HybridCameraSession.start() calls session.startRunning() immediately after a previous configure()'s commitConfiguration() returned, with no barrier waiting for CoreMedia to finish propagating the asynchronous "configuration committed" notification on FigCaptureSessionNotificationQueue. On multi-physical-cam devices (iPhone 11 has wide + ultra-wide), the CoreMedia XPC commit takes longer to propagate than on single-cam devices, widening the race window. When the notification handler runs _makeConfigurationLive: and iterates outputs to call attachToFigCaptureSession:, an output's _outputInternal->figCaptureSession is non-NULL (already attached by the racing startRunning), tripping the internal assert.

The serial Self.queue in HybridCameraSession correctly serializes JS-driven calls (configure, start, stop), but does not synchronize with CoreMedia's async commit notification.

Reproduceable Code

Standard usage. The <Camera> is mounted with stable photo/video outputs; isActive is derived from focus + foreground + a pendingMedia flag (off during a per-shot review screen).

const photoOutput = usePhotoOutput(PHOTO_OUTPUT_OPTIONS); // stable options object
const videoOutput = useVideoOutput({
  targetResolution: VIDEO_RESOLUTION,
  targetBitRate: VIDEO_BIT_RATE,
  enableAudio: !isMuted,            // mute toggle recreates the output instance
});
const outputs = useMemo(() => [photoOutput, videoOutput], [photoOutput, videoOutput]);

const constraints = useMemo<Constraint[]>(() => {
  const out: Constraint[] = [
    { fps: 60 },
    { videoStabilizationMode: 'cinematic-extended' },
  ];
  if (enablePhotoHdr) {
    out.unshift({ photoHDR: true });
    if (deviceSupportsHdrVideo) {
      out.push({ videoDynamicRange: { bitDepth: 'hdr-10-bit', colorSpace: 'hlg-bt2020', colorRange: 'full' } });
    }
  }
  return out;
}, [enablePhotoHdr, device]);

// pendingMedia toggles isActive during photo/video review (free sensor for review, restart on retake)
const isActive = visible && isFocused && isForeground && hasPermission && pendingMedia == null;

<Camera
  ref={cameraRef}
  style={StyleSheet.absoluteFill}
  isActive={isActive}
  device={device}
  outputs={outputs}
  constraints={constraints}
  mirrorMode={cameraPosition === 'front' ? 'on' : 'off'}
  orientationSource="device"
  enableLowLightBoost={enableLowLightBoost}
  enableSmoothAutoFocus={enableSmoothAutoFocus}
  enableDistortionCorrection={enableDistortionCorrection}
  onStarted={onCameraStarted}
  onError={onError}
/>

User-visible actions that trigger reconfigure → start():

  • Photo capture → review → tap "Retake" (isActive flips off → on).
  • Toggle mute (isMuted) — useVideoOutput's memo invalidates on enableAudio change, recreating the AVCaptureMovieFileOutput instance, which forces removeOutput/addOutputWithNoConnections in updateOutputs.
  • Toggle HDR (enablePhotoHdr) — new constraints reference triggers configure().
  • Flip front/back — new device.
  • Zoom switching past the wide↔ultra-wide threshold (zoom itself is imperative via controller.setZoom, but iOS internally posts a configuration notification when the active sub-device changes on a multi-physical-cam logical device).

Relevant log output

Exception Type:  EXC_CRASH (SIGABRT)
Termination Reason: SIGNAL 6 Abort trap: 6

Crashed thread (CoreMedia FigCaptureSessionNotificationQueue):
0   libsystem_kernel.dylib    __pthread_kill + 8
1   libsystem_pthread.dylib   pthread_kill + 268
2   libsystem_c.dylib         __abort + 132
3   libsystem_c.dylib         abort + 136
4   libsystem_c.dylib         __assert_rtn + 284
5   AVFCapture                __45-[AVCaptureOutput attachToFigCaptureSession:]_block_invoke.cold.1 + 44 (AVCaptureOutput.m:364)
6   AVFCapture                __45-[AVCaptureOutput attachToFigCaptureSession:]_block_invoke + 100 (AVCaptureOutput.m:364)
7   libdispatch.dylib         _dispatch_client_callout + 16
8   libdispatch.dylib         _dispatch_lane_barrier_sync_invoke_and_complete + 56
9   AVFCapture                -[AVCaptureOutput attachToFigCaptureSession:] + 108 (AVCaptureOutput.m:363)
10  CoreFoundation            -[NSSet makeObjectsPerformSelector:withObject:] + 188
11  AVFCapture                -[AVCaptureSession _makeConfigurationLive:] + 344 (AVCaptureSession.m:4199)
12  AVFCapture                -[AVCaptureSession _handleConfigurationCommittedNotificationWithPayload:] + 600 (AVCaptureSession.m:5445)
13  AVFCapture                avcaptureFigCaptureSessionNotification + 176 (AVCaptureSession.m:6678)
14  CoreFoundation            __CFNOTIFICATIONCENTER_IS_CALLING_OUT_TO_AN_OBSERVER__ + 128
15  CoreFoundation            ___CFXRegistrationPost_block_invoke + 92
16  CoreFoundation            _CFXRegistrationPost + 436
17  CoreFoundation            _CFXNotificationPost + 736
18  CoreFoundation            CFNotificationCenterPostNotificationWithOptions + 140
19  CoreMedia                 CMNotificationCenterPostNotification + 96
20  CoreMedia                 __figXPCConnection_HandleNotificationMessage_block_invoke + 132

Parallel vision-camera thread (com.margelo.camera.session):
8   AVFCapture                -[AVCaptureSession _buildAndRunGraph:] + 1372 (AVCaptureSession.m:4709)
9   AVFCapture                -[AVCaptureSession _setRunning:] + 224 (AVCaptureSession.m:2674)
10  AVFCapture                -[AVCaptureSession startRunning] + 452 (AVCaptureSession.m:2536)
11  QutrekDev                 partial apply for closure #1 in HybridCameraSession.start() + 56
12  QutrekDev                 closure #1 in static Promise.parallel(_:_:) + 96 (Promise.swift:117)

Camera Device

{
  "id": "com.apple.avfoundation.avcapturedevice.built-in_video:0",
  "position": "back",
  "physicalDevices": ["wide-angle-camera", "ultra-wide-angle-camera"]
}

(iPhone 11 dual back camera; both back-facing sub-devices in a single logical multi-physical-cam device.)

Device

iPhone 11 (iPhone12,1), iOS 18.7.7

VisionCamera Version

5.0.4

Can you reproduce this issue in the VisionCamera Example app?

Have not tested in the example app yet. Reporting because of (a) volume of identical real-world crashes, (b) the obvious structural similarity to #3730 which suggests a known race-condition class.

Additional information

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions