Skip to content

fix: prevent SIGSEGV crash when Stop Recording clicked after mic replug - #121

Merged
jatinkrmalik merged 1 commit into
mainfrom
fix/mainactor-button-tasks
Apr 23, 2026
Merged

fix: prevent SIGSEGV crash when Stop Recording clicked after mic replug#121
jatinkrmalik merged 1 commit into
mainfrom
fix/mainactor-button-tasks

Conversation

@jatinkrmalik

@jatinkrmalik jatinkrmalik commented Apr 23, 2026

Copy link
Copy Markdown
Member

Problem

@slatlasdev is hitting a daily crash:

  1. Start recording
  2. Replug/change microphone while stuck
  3. App auto-recovers (engine resets, appStatus → .idle)
  4. User clicks Stop Recording in the popover
  5. App crashesEXC_BAD_ACCESS (SIGSEGV), KERN_INVALID_ADDRESS

Root Cause

The crash trace from the IPS report (incident 48DBB805-253C-4F8B-9DCE-D6D389E10CE7, Sam Leatherdale, 2026-04-23):

SerialExecutor._isSameExecutor
SerialExecutor.isMainExecutor.getter
MainActor.assumeIsolated(_:file:line:)   ← faults here (EXC_BAD_ACCESS)
_ButtonGesture.internalBody.getter
PrimitiveButtonGestureCallbacks.dispatch

SwiftUI's _ButtonGesture internally wraps button action closures with MainActor.assumeIsolated { }. When a bare Task { } is used inside a Button action, the Swift 6 runtime synthesises an implicit assumeIsolated hop to validate the current executor. On macOS 26, if the view is being torn down or re-evaluated at the same moment the gesture fires — exactly what happens when onAudioDeviceChanged mutates @Published state and removes the Stop button while the user's click is mid-dispatch — the executor reference the runtime reads has already been freed, causing the SIGSEGV.

Why mic replug reliably triggers it:

  • AVAudioEngineConfigurationChange fires → handleAudioConfigurationChange tears down engine, dispatches onAudioDeviceChanged on main
  • onAudioDeviceChanged immediately sets isRecording = false + appStatus = .idle
  • SwiftUI re-renders statusSection, removing the Stop Recording button from the view tree
  • The user's in-flight click dispatches through the now-stale _ButtonGesture node → crash

Fixes

1. Task { @MainActor in } in all view button actions (primary fix)

Adding @MainActor to the Task closure binds it directly to the main actor's serial executor at the call site, eliminating the implicit assumeIsolated wrapper that SwiftUI synthesises. This is the correct Swift 6 pattern for all Button actions that call @MainActor-isolated methods.

Files changed:

  • MenuBarView.swift — Stop Recording button
  • UpdateView.swift — Download & Install, Retry buttons
  • OnboardingView.swiftonAppear startup, model select/download, test recording toggle
  • SettingsView.swift — Load, Download & Load, force-download alert, Check for Updates buttons

2. Reset stale callback flags in AudioEngine.handleAudioConfigurationChange (secondary fix)

When the audio device changes mid-recording, the handler was already stopping the engine and resetting isCurrentlyRecording, but left silenceCallbackFired and maxDurationCallbackFired set. On the next recording session after replug, those stale true flags meant neither the silence detector nor the max-duration limiter would ever fire — so recordings could run indefinitely with no auto-stop. Both flags are now cleared alongside isCurrentlyRecording.

Testing

  • ✅ All 163 existing unit tests pass (swift test)
  • Manual reproduction: replug mic during recording → click Stop → no crash

References

  • Crash: EXC_BAD_ACCESS / SIGSEGV, incident 48DBB805-253C-4F8B-9DCE-D6D389E10CE7
  • macOS 26.4.1 (25E253), VocaMac 0.6.0, Apple Silicon (Mac16,7)

… SIGSEGV crash

Bare Task { } closures inside SwiftUI Button actions caused an
EXC_BAD_ACCESS (SIGSEGV) crash on macOS 26 when the Swift concurrency
runtime called MainActor.assumeIsolated() on a stale executor reference.

This was reliably triggered by:
1. Replug a microphone while recording (causes AVAudioEngineConfigurationChange)
2. App auto-recovers via onAudioDeviceChanged → sets appStatus = .idle
3. User clicks Stop Recording button on the now-stale SwiftUI view node
4. _ButtonGesture dispatches via MainActor.assumeIsolated() → reads freed memory → crash

Fix: annotate every Task spawned from a Button/onAppear in views with
@mainactor, which removes the implicit assumeIsolated() call and binds
the task directly to the main actor instead.

Also reset silenceCallbackFired and maxDurationCallbackFired in
AudioEngine.handleAudioConfigurationChange so stale callback state
cannot fire spurious stop/timeout events after a device reconnect.
@jatinkrmalik
jatinkrmalik merged commit efa16a2 into main Apr 23, 2026
3 checks passed
jatinkrmalik added a commit that referenced this pull request Apr 24, 2026
Patch release covering all changes since v0.6.0:

- fix(#121): SIGSEGV crash when stopping recording after mic replug
- fix(#121): clear stale AudioEngine callback flags on device change
- docs(#120): explain GitHub 403 update-check error on shared networks
- ci(#122): bump GitHub Actions to Node.js 24-compatible versions

No new features and no breaking changes — PATCH bump per SemVer.
@jatinkrmalik
jatinkrmalik deleted the fix/mainactor-button-tasks branch August 22, 2026 05:32
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.

1 participant