Skip to content

Fix divide-by-zero trap in FFmpegAssetTrack audio nominalFrameRate - #928

Merged
kingslay merged 2 commits into
kingslay:mainfrom
YuriNachos:YuriNachos/w3-ksplayer-ffmpeg
Aug 15, 2026
Merged

kingslay merged 2 commits into
kingslay:mainfrom
YuriNachos:YuriNachos/w3-ksplayer-ffmpeg

Conversation

@YuriNachos

Copy link
Copy Markdown
Contributor

Single-file FFmpeg-interop divide-by-zero guard — the same family as #926 (NULL codecpar) and #908 (rotation NaN).

Root cause

FFmpegAssetTrack.init?(stream:) computed the audio nominalFrameRate with unguarded integer division (FFmpegAssetTrack.swift:100): max(Float(codecpar.sample_rate / frameSize), 48), where frameSize could be 0. Three FFmpeg facts combine into a deterministic trap on valid input:

  1. AVCodecParameters.frame_size is 0 pre-decode (audio-only; AAC/MP3/Opus/FLAC/Vorbis leave it at 0 until the first frame is decoded), so the timebase.den / timebase.num fallback branch is hot, not theoretical.
  2. That fallback integer-divides to 0 whenever den < num — a perfectly valid AVRational like {num:3,den:1} (exotic MPEG-TS audio, some MKV/AVI muxes, attached-pic pseudo-audio) yields 1 / 3 == 0.
  3. sample_rate can also be 0 pre-decode → 0 / 0 regardless of time_base.

Swift traps on integer divide-by-zero (Fatal error: Division by zero, SIGTRAP, exit 133) — the app dies. The codebase already guards sample_rate elsewhere (AudioDescriptor.init, Resample.swift:294, if sampleRate <= 0 { 48000 }); this site forgot the same defence.

Fix

Extract a pure audioNominalFrameRate(sampleRate:frameSize:timebase:) helper that guards frameSize > 0 && sampleRate > 0 (and timebase.num > 0) before dividing, falling back to the existing 48 floor otherwise. Byte-for-byte unchanged for well-formed streams; 48 was already the max(...) lower bound.

Test plan

  • swift build — clean, 0 new warnings.
  • swift test --filter FFmpegAssetTrackTest — 7 passed, 0 failed: div-to-zero fallback ({num:3,den:1} + frame_size=0), zero sample_rate (both den>=num and den<num), zero timebase.num, well-formed clamp-to-floor, above-floor (pins Int32 truncation 48000/700==68 so a Float-division refactor would fail), and the timebase-fallback path.
  • Red evidence: a standalone repro of the pre-fix arithmetic traps with Fatal error: Division by zero (exit 133).

AVCodecParameters.frame_size is audio-only and left at 0 by most demuxers
(AAC/MP3/Opus/FLAC/Vorbis) until the first frame is decoded. The
timebase.den / timebase.num fallback then integer-divides to 0 whenever
den < num (e.g. a valid AVRational{num:3,den:1}), and sample_rate can also
be 0 pre-decode, so the subsequent sample_rate / frameSize traps Swift's
integer division (Fatal error: Division by zero, SIGTRAP) on valid input.

Extract a pure audioNominalFrameRate(sampleRate:frameSize:timebase:) helper
that guards every divisor (frameSize, sampleRate, and the timebase.num
fallback) and falls back to the existing 48 floor, mirroring AudioDescriptor's
sampleRate <= 0 defence in Resample.swift. Integer division is preserved
verbatim, so well-formed streams are byte-for-byte unchanged; only the
previously-trapping inputs now return 48 instead of crashing.

Adds Tests/KSPlayerTests/FFmpegAssetTrackTest.swift with 7 focused cases.
@YuriNachos

Copy link
Copy Markdown
Contributor Author

I saw the related crash fixes (#926 and the subtitle parser loop fixes) land this week, so I thought I'd check in on this one while the area is still warm — the unguarded division traps on any audio stream whose frame_size is still 0 before the first decode. Is the guarded-division approach fine as-is, or would you rather see it handled differently, perhaps with a test fixture reproducing the trap? I'm glad to rework it whichever way makes it easiest to take.

@kingslay

Copy link
Copy Markdown
Owner

我对改动进行了评价。你可能没有看到。我建议的改动如下
let frameSize = codecpar.pointee.frame_size > 0 ? codecpar.pointee.frame_size : codecpar.pointee.codec_id == AV_CODEC_ID_TRUEHD ? 48 : 1000

@kingslay

Copy link
Copy Markdown
Owner

你最近提的这些bug,lgpl分支之前就已经修复了。如果你想要更稳定、功能更丰富的代码的话,那你可以申请lgpl授权。

Replace the timebase-derived frame-size fallback (den/num can
integer-divide to 0) with the constants suggested in review: TRUEHD
falls back to 48, every other codec to 1000. Same divide guard for
sample_rate; the 48 floor is unchanged. Tests updated to the new
signature, including TRUEHD- and generic-fallback cases.
@YuriNachos

Copy link
Copy Markdown
Contributor Author

谢谢!已按你的建议更新(4dec3e6):fallback 改为 codec-aware 常量 —— TRUEHD 用 48,其他 codec 用 1000,不再从 timebase 推导(den/num 整除可能为 0)。sample_rate <= 0 的除零守卫保留,48 下限不变。测试已更新到新签名(含 TRUEHD 和通用 fallback 两个新用例),swift test --filter FFmpegAssetTrackTest 7/7 通过。

(Thanks! Updated per your suggestion in 4dec3e6: the fallback is now codec-aware constants — TRUEHD uses 48, everything else 1000 — instead of deriving from the timebase (whose den/num integer division can be 0). The divide-by-zero guard on sample_rate stays, the 48 floor is unchanged. Tests updated to the new signature with TRUEHD and generic-fallback cases; swift test --filter FFmpegAssetTrackTest passes 7/7.)

Also noted your point that the LGPL branch already carries these fixes — understood; happy to close this if the guard lands upstream through that channel.

@kingslay
kingslay merged commit 25c923b into kingslay:main Aug 15, 2026
1 check failed
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