Skip to content

Fix SIGABRT on shutdown: serialize encoder encode/flush/stop - #3

Merged
dsward2 merged 1 commit into
mainfrom
fix-encoder-shutdown-race
Sep 23, 2026
Merged

dsward2 merged 1 commit into
mainfrom
fix-encoder-shutdown-race

Conversation

@dsward2

@dsward2 dsward2 commented Sep 23, 2026

Copy link
Copy Markdown
Owner

Problem

LiveAudioServer crashed with SIGABRT nearly every time AntennaHead quit or restarted it (42 crash reports since 2026-09-18). There were two signatures:

  • AAC: AudioConverterReset → aacClose → DeleteLoopKernel → pointer being freed was not allocated
  • MP3: lame_encode_flush → format_bitstream assert

In every report, two threads were tearing down the same encoder at once: LiveAudioServer.stop() (Swift task) called encoder.stop() while the stdin-pcm-reader thread was still running. That thread sends an EOF encode(count: 0) → flush() and then calls stop() again in its epilogue. Nothing serialized them.

Fix

  • AACEncoder / MP3Encoder: an NSLock serializes encode, flush, and stop. stop() is idempotent, and encode() does nothing after stop.
  • AACEncoder.stop() no longer resets before disposing. Reset only discards buffered input.
  • MP3Encoder flushes at most once.
  • CLI: graceful shutdown is one-shot. The --exit-with-parent watchdog fires every 0.5 s and could start a second shutdown that called exit(0) mid-teardown.

Verification

  • swift build, and swift test passes all 110 tests.
  • SIGTERM stress with AntennaHead's args (--keep-alive --filler-mode silence, UDP input): old binary crashed 19/40 (mono) and 14/20 (stereo); new binary 0/120.
  • Parent-death (--exit-with-parent): old binary shut down cleanly 2/15 times, new binary 15/15.
  • AntennaHead built with this helper, launched and quit 6 times: no new LiveAudioServer .ips.

🤖 Generated with Claude Code

LiveAudioServer.stop() (a Swift task) stopped the MP3 and AAC encoders
while the stdin-pcm-reader thread was still encoding, sending its EOF
flush, and then calling stop() again in its own epilogue. Nothing
serialized these, so AudioConverterReset/Dispose and lame_encode_flush/
lame_close ran concurrently on the same handle: "pointer being freed was
not allocated" in aacClose, or a LAME assert in format_bitstream.

- AACEncoder / MP3Encoder: an NSLock serializes encode, flush and stop;
  stop() is idempotent and later encode() calls are no-ops.
- AACEncoder.stop() no longer resets before disposing (reset only
  discards buffered input and emits nothing).
- MP3Encoder flushes at most once (EOF flush and stop no longer both run).
- CLI: graceful shutdown is one-shot, so the --exit-with-parent watchdog
  (fires every 0.5 s) can't start a second shutdown that exit(0)s while
  the first is still tearing down.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@dsward2
dsward2 merged commit 1c9f471 into main Sep 23, 2026
0 of 2 checks passed
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