Skip to content

Fix signed-integer-overflow UB in interpolate()'s FIR accumulator - #3

Open
austek wants to merge 1 commit into
espeak-ng:masterfrom
ZirekHQ:fix/interpolate-overflow-espeak
Open

Fix signed-integer-overflow UB in interpolate()'s FIR accumulator#3
austek wants to merge 1 commit into
espeak-ng:masterfrom
ZirekHQ:fix/interpolate-overflow-espeak

Conversation

@austek

@austek austek commented Aug 23, 2026

Copy link
Copy Markdown

Summary

interpolate()'s 12-tap sinc-filter accumulator detected overflow with a sign-flip heuristic (oldSign != getSign(total)) that itself depends on the signed overflow already having occurred — undefined behavior in C.

Replaces the int total accumulator with long, wide enough to hold the true sum, so the final clamp compares against real bounds ((long)SHRT_MAX * 65536L) instead of inferring overflow from a sign flip. getSign is now unused and removed.

Same bug, same fix as waywardgeek/sonic#68 — this repo's interpolate()/getSign are byte-identical to what that file had upstream, so the fix ports directly.

Scope note

findSincCoefficient's separate left-shift-of-negative-value UB (same call path, still present in this file) is not touched here, to keep this diff focused — it's fixed instead as a drive-by in the CI PR (#6), which needs it for the strict-and-sanitized job to pass -Werror+UBSan. Verified via ASan+UBSan that this fix alone removes the accumulator overflow; the only remaining sanitizer finding is that separate issue.

Test plan

  • cd tests && make runtests && ./runtests — passes, new tests/interpolate_overflow_test.c included
  • Same build under -fsanitize=address,undefined — no accumulator-overflow finding; only the separate, out-of-scope findSincCoefficient finding remains

interpolate() accumulated 12 sinc-filter tap products into an int
total, detecting overflow via a sign-flip heuristic
(oldSign != getSign(total)) that itself relies on the signed overflow
already having happened -- undefined behavior in C.

Replaced with accumulation in long, wide enough to hold the true sum
without overflowing, so the final clamp compares against real bounds
instead of inferring overflow from a sign flip. getSign is now unused
and removed -- it had no other callers. The overflow bounds are
computed via multiplication ((long)SHRT_MIN * 65536L), not left-shift:
SHRT_MIN is negative, and left-shifting a negative value is undefined
behavior in C, the same class of bug already present elsewhere in this
file (findSincCoefficient) but not fixed here -- kept deliberately out
of scope to avoid overlapping that separate fix.

Adds tests/interpolate_overflow_test.c, a white-box test that calls
interpolate() directly (not static, for this reason) with a
deterministically constructed 12-sample input: on a freshly created
stream, newRatePosition/oldRatePosition are both 0, which makes the
internal ratio/width depend only on newSampleRate, so the weight at
each tap is known in advance. Setting each sample to the maximum
magnitude matching its tap's coefficient sign guarantees the
accumulator's true sum exceeds INT_MAX -- verified via UBSan to trip
the old, unfixed code and to no longer do so after the fix, while
still returning the same correctly-clamped SHRT_MAX.

Ported from waywardgeek/sonic#68, which fixes the same bug (also
present here, byte-identical interpolate()/getSign) upstream.
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