Skip to content

Fix recurring EXC_BAD_ACCESS crashes on hub disconnect - #13

Draft
ygivenx with Copilot wants to merge 2 commits into
mainfrom
copilot/fix-multiple-crashes
Draft

Fix recurring EXC_BAD_ACCESS crashes on hub disconnect#13
ygivenx with Copilot wants to merge 2 commits into
mainfrom
copilot/fix-multiple-crashes

Conversation

Copilot AI commented Mar 27, 2026

Copy link
Copy Markdown
Contributor

Nine crashes in one day, all EXC_BAD_ACCESS / SIGSEGV / KERN_INVALID_ADDRESS in SwiftUI's AttributeGraph / ViewBodyAccessor.updateBody, consistently triggered by unplugging a hub that carries both microphone and monitors during a Teams call.

Critical: Dangling object references after deallocation

HotkeyManager — unretained C callback pointer
CGEvent.tapCreate stores Unmanaged.passUnretained(self) in the callback's userInfo. No deinit existed to invalidate the tap, so any key event after dealloc dereferences a freed pointer.

deinit {
    if let tap = eventTap {
        CGEvent.tapEnable(tap: tap, enable: false)
        CFMachPortInvalidate(tap)   // prevents callback firing on dead self
    }
}

AudioRecorder — orphaned AVAudioEngineConfigurationChange observer
prepareEngine() registers an @objc selector but no deinit removed it. Hardware-change notifications after dealloc invoke the selector on freed memory. Added deinit calling NotificationCenter.default.removeObserver(self) + engine/tap teardown.

High: RecordingIndicator stranded on disconnected screen

RecordingIndicator had no @MainActor, no cleanup, and no screen-change handling. When the external monitor disconnects the floating NSPanel is left at off-screen coordinates with no repositioning path. Fixed:

  • @MainActor annotation enforcing AppKit-on-main-thread
  • Registers for NSApplication.didChangeScreenParametersNotification; repositions panel to current main screen on each change
  • deinit dismisses the panel and removes the observer

Medium: Off-main @Published mutations causing SwiftUI attribute graph races

WhisperTranscriber.transcribe() is a non-isolated async function; isTranscribing = true/false and isModelLoaded were being set on the cooperative thread pool, racing with SwiftUI's objectWillChange subscription. All @Published mutations now use await MainActor.run {} / DispatchQueue.main.async.

Also added a reentrancy guard (alreadyTranscribing) to prevent two concurrent whisper.cpp inference sessions from corrupting the KV-cache context.

Medium: AppState state machine gaps during concurrent hardware events

When mic+monitor disconnect simultaneously, AVAudioEngineConfigurationChange can fire while whisper is mid-inference (main actor suspended at await). The resulting second transcribeAndInject task was clearing isTranscribing prematurely, collapsing the reentrancy protection. Also, startRecording() only guarded on !isRecording, not !isTranscribing, and switchModel() could call transcriber.unloadModel() mid-inference.

  • startRecording(): guard !isRecording, !isTranscribing
  • switchModel(): guard !isSwitchingModel, !isRecording, !isTranscribing
  • transcribeAndInject(): early bail if isTranscribing already true

Low: Force unwraps

  • ModelManager: FileManager.urls(...).first!guard let with temporaryDirectory fallback; URL(string:)!fatalError guard (programmer error, compile-time constant)
  • AudioRecorder: AVAudioFormat(...)!fatalError guard with descriptive message
  • UpdateChecker: apiURL changed to URL? with explicit nil guard before network call

💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

- HotkeyManager: add deinit to disable/invalidate CGEvent tap before
  object is released; prevents dangling unretained C callback pointer
  (KERN_INVALID_ADDRESS when hub-triggered key events fire after dealloc)

- AudioRecorder: add deinit to removeObserver/stop/removeTap; prevents
  @objc selector being called on deallocated object after hardware change;
  fix whisperFormat force-unwrap; guarantee onRecordingComplete on main

- RecordingIndicator: add @mainactor; add init/deinit for screen-change
  observer (NSApplication.didChangeScreenParametersNotification) so the
  floating panel is repositioned when monitors connect/disconnect rather
  than being stranded off-screen; add deinit to dismiss panel

- WhisperTranscriber: dispatch @published mutations to main thread;
  add alreadyTranscribing reentrancy guard and explicit main-thread
  flag cleanup (await MainActor.run) to prevent concurrent whisper.cpp
  inference and off-main @published data races

- AppState: guard startRecording against !isTranscribing; guard
  switchModel against !isRecording && !isTranscribing; add early
  reentrancy guard to transcribeAndInject to safely handle hardware
  disconnect mid-inference without corrupting isTranscribing state

- ModelManager: replace force-unwrap .first! with safe fallback;
  replace URL(string:)! with fatalError guard for programmer errors

- UpdateChecker: use optional URL? for apiURL with explicit nil guard

Agent-Logs-Url: https://github.com/ygivenx/freeWispr/sessions/8f0c2fd0-fc30-4bc7-80f4-510241df67b1

Co-authored-by: ygivenx <31292443+ygivenx@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix multiple crashes in FreeWispr application Fix recurring EXC_BAD_ACCESS crashes on hub disconnect Mar 27, 2026
Copilot AI requested a review from ygivenx March 27, 2026 00:49
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.

2 participants