Skip to content

Fix Int overflow in maxTotalSubFrames on 32-bit platforms (wasm32, watchOS) - #88

Open
mansbernhardt wants to merge 1 commit into
orchetect:mainfrom
mansbernhardt:fix/32bit-subframe-overflow
Open

Fix Int overflow in maxTotalSubFrames on 32-bit platforms (wasm32, watchOS)#88
mansbernhardt wants to merge 1 commit into
orchetect:mainfrom
mansbernhardt:fix/32bit-subframe-overflow

Conversation

@mansbernhardt

Copy link
Copy Markdown
Contributor

The bug

TimecodeFrameRate.maxTotalSubFrames(in:base:) computes its product directly in Int:

maxTotalFrames(in: extent) * base.rawValue

With extent == .max100Days that product exceeds Int32.max for every frame rate. The smallest case, 23.976 fps at 80 subframes, is already

2_073_600 × 100 × 80 = 16_588_800_000     vs Int.max = 2_147_483_647

so the multiplication traps on overflow on any 32-bit platform — wasm32, and watchOS armv7k / arm64_32.

Because the bound is recomputed inside every wrapping add (sfcNew.clamped(to: 0 ... maxSubFrameCountExpressible)), this makes all arithmetic on a .max100Days timecode trap on those platforms, no matter how small the operands are.

Repro (wasm32)

var lhs = try Timecode(.realTime(seconds: 1.0), at: .fps59_94)
var rhs = try Timecode(.realTime(seconds: 192.0), at: .fps59_94)
lhs.properties.upperLimit = .max100Days
rhs.properties.upperLimit = .max100Days
_ = try lhs.adding(rhs, by: .wrapping)      // ← unreachable

Observed in a browser, wasm32 debug build:

Int is 32-bit, max=2147483647
maxTotalFrames(24h)        = 5184000
maxTotalFrames(100d)       = 518400000
maxTotalSubFrames(24h,80)  = 414720000     ← fits
maxTotalSubFrames(100d,80) → TRAP
limit max24Hours — adding ok 00:03:12:48   ← same operands
limit max100Days — adding TRAP             ← same operands

Construction, comparison, max(by:) and .realTimeValue all work; only arithmetic under .max100Days traps.

Worth noting this is easy to hit without ever choosing .max100Days deliberately: our wrapper type sets it on every Timecode it constructs, so every timecode operation trapped once we started building for wasm32.

The fix

Compute in Int64, saturate on return:

let product = Int64(maxTotalFrames(in: extent)) * Int64(base.rawValue)
return Int(clamping: product)
  • No behaviour change on 64-bit. The product peaks at ~82.9e9 (120 fps, 100 days, 100 subframes), ~8 orders of magnitude below Int64.max, so the clamp never engages. The existing exact-value assertions in TimecodeFrameRate_Properties_Tests.properties() still hold.
  • Correct on 32-bit. This value is only ever used as an upper bound — a clamped(to:) range, or a > comparison against a subFrameCount. A subFrameCount that large is itself unrepresentable in a 32-bit Int, so saturating at Int.max still bounds the entire representable domain.
  • Signature unchanged, so it is not source-breaking.

Tests

Two regression tests in TimecodeFrameRate Properties Tests.swift:

  • maxTotalSubFramesDoesNotOverflowOn32Bit() — every frame rate × every subframe base at .max100Days; asserts the exact product on 64-bit and Int.max on 32-bit, and that maxSubFrameCountExpressible stays consistent.
  • max100DaysArithmeticDoesNotTrap() — the wrapping add above.

Full suite green locally: 506 tests in 55 suites passed.

One suggestion, happy to do it separately

The wasm CI jobs added in #87 (mine) run swift build only. This defect compiles perfectly and traps at runtime, so a build-only job structurally cannot catch it — and the tests above would have, had the suite run under wasm32. If you'd like, I can follow up with a PR that runs swift test on the wasm jobs via wasmtime or Node.

`maxTotalSubFrames(in:base:)` computed `maxTotalFrames(in: extent) *
base.rawValue` directly in `Int`. With `extent == .max100Days` that product
exceeds `Int32.max` for every frame rate — the smallest case, 23.976 fps at 80
subframes, is already 2_073_600 * 100 * 80 = 16_588_800_000 against an
`Int.max` of 2_147_483_647 — so the multiplication traps on any 32-bit
platform (wasm32, watchOS armv7k/arm64_32).

Because the bound is recomputed inside every wrapping add
(`sfcNew.clamped(to: 0 ... maxSubFrameCountExpressible)`), all arithmetic on a
`.max100Days` timecode trapped on those platforms however small the operands.

Compute the product in `Int64` and saturate on return. No behaviour change on
64-bit: the product peaks at ~82.9e9, far below `Int64.max`, so the clamp never
engages. Correct on 32-bit: the value is only ever used as an upper bound, and
a `subFrameCount` that large is itself unrepresentable there, so `Int.max`
still bounds the whole representable domain. Signature unchanged.

Adds two regression tests covering every frame rate and subframe base, and a
`.max100Days` wrapping add.
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