Skip to content

fix: Avoid abort in detachFromFigCaptureSession when reusing an output after configure([]) - #4215

Open
Luccacvb wants to merge 2 commits into
margelo:mainfrom
Luccacvb:fix/reused-output-detach-crash
Open

Luccacvb wants to merge 2 commits into
margelo:mainfrom
Luccacvb:fix/reused-output-detach-crash

Conversation

@Luccacvb

@Luccacvb Luccacvb commented Oct 10, 2026 •

Copy link
Copy Markdown

Fixes #4156. Related: #4209 (same assertion, its scenario isn't reproduced here).

What

When an output is reused in a new CameraSession after the previous one was torn down with stop() + configure([]) (what <Camera> does on unmount), the app aborts later, when Hermes frees the old session:

abort / __assert_rtn
-[AVCaptureOutput detachFromFigCaptureSession:]_block_invoke
-[AVCaptureOutput detachFromFigCaptureSession:]
-[AVCaptureSession _makeConfigurationLive:]
-[AVCaptureSession dealloc]
HybridCameraSession.deinit
...
facebook::hermes::deleteShared(...)
hermes::vm::HadesGC::OldGen::sweepNext(bool)
...
hermes::vm::gc(...)

An output removed from a stopped AVCaptureSession is only detached when that session deallocates (the dealloc -> _makeConfigurationLive: frames above), and that late detach asserts if another session took the output in the meantime. An output that outlives its <Camera> can hit this, e.g. a usePhotoOutput() in a screen that unmounts and remounts the <Camera>. #4156 hits it with a frame output and a barcode scanner output.

How

When configure([]) empties a session that is stopped and not interrupted, HybridCameraSession swaps in a new AVCaptureSession and releases the old one inside that call, on the session queue. All sessions share that serial queue, so the old session deallocates and detaches its outputs before another session can add them. Listeners are moved over to the new instance.

The assertion compares the session being detached with the one the output currently belongs to, so it only fires after a reuse. The deinit tried in #4209 also releases on the session queue, but at GC time, which can be after the output was reused. Releasing inside configure([]) happens before any reuse, and without reuse the release doesn't abort (5/5 on device, see below).

Preview layers that didn't get their connection because a later configure step threw are now detached as well, so they can't keep the old session alive.

#3800 moves configure([]) before stop() in the <Camera> teardown. Doing that by hand on main didn't abort (1 run), but it doesn't help when the session is already stopped (isActive={false}) or when stop() + configure([]) are called directly.

Test

  • visioncamera.multi-output.harness.ts: takes a photo in session A, tears A down with stop() + configure([]) like <Camera> does, uses the same photo output in session B, takes a photo, then calls gc() so Hermes frees A. On an iPhone 14 (iOS 26.1), main aborts inside gc() with the stack above (4/4 runs), with this PR it passes (3/3). The first commit only adds this test.
  • visioncamera.session.harness.ts: listeners keep firing after configure([]) + reconfigure, and remove() still works after the swap.

Also checked on the same device:

  • releasing a stopped session right after its configure([]) commits, without reuse: no abort, 5/5 on main and 5/5 with this PR
  • a <Camera> replaced by a new <Camera> with the same photo output, then gc(): aborts on main, passes with this PR (1 run each)
  • one photo output reused across 6 sessions with gc() in between: passes with this PR (1 run)
  • whole multi-output and session files: pass with this PR
  • same results as main: photo, controller, coordinates, nativepreviewview, hooks. These fail on both: locks metering modes after focusTo with locked adaptiveness, maps a square Frame region onto a square View region, starts a replacement preview and stops the removed preview while running, updates onUIRotationChanged when the interface orientation changes
  • pass with this PR, not run on main: camera-view, video, barcode-scanner, frame-converter, skia-camera

Android: both new tests pass on an emulator, no Android changes.

Not covered

  • A session torn down while interrupted keeps its AVCaptureSession, since swapping it would drop the system's auto-resume. stop() returns early while interrupted (isRunning is false), so that path may still hit this.
  • An output removed by a non-empty configure(...) on a stopped session and then added to another session can still hit the same late detach.

@vercel

vercel Bot commented Oct 10, 2026

Copy link
Copy Markdown

@Luccacvb is attempting to deploy a commit to the Margelo Team on Vercel.

A member of the Team first needs to authorize it.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

🐛 [iOS] Sharing one output between two CameraSession's aborts in [AVCaptureOutput detachFromFigCaptureSession:]

1 participant