Skip to content

Fix unsigned underflow in AudioEntry.audioDataLengthBytes() - #140

Merged
dimitris-c merged 2 commits into
dimitris-c:mainfrom
StarGoApps:fix/audio-entry-length-underflow-upstream
Sep 30, 2026
Merged

dimitris-c merged 2 commits into
dimitris-c:mainfrom
StarGoApps:fix/audio-entry-length-underflow-upstream

Conversation

@romansorochak

@romansorochak romansorochak commented Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

Problem

AudioEntry.audioDataLengthBytes() subtracts the parsed data offset from the source length as UInt:

func audioDataLengthBytes() -> UInt {
    if let byteCount = audioStreamState.dataByteCount {
        return UInt(byteCount)
    }
    guard source.length > 0 else { return 0 }
    return UInt(source.length) - UInt(audioStreamState.dataOffset)
}

When dataOffset > source.length, the subtraction underflows and Swift traps (SIGTRAP). This happens on live streams whose reported length is smaller than the header offset the file-stream parser found. duration() reaches it through calculatedBitrate(), which becomes non-zero as soon as packets have been processed, so the crash hits during normal playback, not at the edges.

For context: in a radio app built on AudioStreaming, AudioEntry.duration() → audioDataLengthBytes() was the top fatal crash on iOS, 45 installs in a month across four app versions.

Fix

Compare before subtracting, and return 0 when dataOffset >= length. duration() already returns 0 for a stream of unknown length, so callers see the same value they get for any live stream. The explicit dataByteCount path is unchanged.

Tests

AudioEntryTests, a new file with five tests that use a fixed-length CoreAudioStreamSource stub:

  • dataOffset > length → 0. Against the current main this test does not fail, it takes the test runner down: xctest … exited with unexpected signal code 5.
  • dataOffset == length → 0.
  • the normal case, length - dataOffset.
  • an explicit dataByteCount still wins.
  • duration() is 0 when the offset exceeds the length, with a bitrate set.

With the fix, the full suite passes: swift test → Executed 59 tests, with 0 failures.

Notes

The PR also carries one small, separate test-only commit. The first CI run failed in DispatchTimerSourceTests.test_HandlerIsExecuted_On_The_Specified_Queue, which this change does not touch: API violation - multiple calls made to -[XCTestExpectation fulfill]. The timer repeats every 100 ms, and on a slow runner with --parallel it can fire again before wait returns. The test now sets assertForOverFulfill = false. Happy to drop that commit if you prefer it separately.

The change is confined to AudioEntry.audioDataLengthBytes(); there are no public API changes. Our next app release, now in App Review, carries this fix from our fork. Thanks for merging #138 and releasing it in 1.4.5 — this is the last patch we carry on top of upstream.

romansorochak and others added 2 commits September 27, 2026 00:49
A live stream can report a length smaller than the parsed header offset.
UInt(source.length) - UInt(dataOffset) then underflows and Swift traps
(SIGTRAP), reached through duration() once packets have arrived.
Return 0 in that case, which is what duration() already returns for a
stream of unknown length.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The timer source repeats every 100 ms. On a slow CI runner with --parallel
it can fire a second time before wait(for:) returns, and the second
fulfill() raises an API violation that fails the suite.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@dimitris-c

Copy link
Copy Markdown
Owner

Thank you for this fix, looks good

@dimitris-c
dimitris-c merged commit b484890 into dimitris-c:main Sep 30, 2026
1 check 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.

2 participants