Skip to content

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

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

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

Conversation

@austek

@austek austek commented Aug 23, 2026

Copy link
Copy Markdown

Fixes #66.

interpolate() accumulates 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 having already happened — undefined behavior in C, confirmed via a UBSan trace found while fuzzing: signed integer overflow at sonic.c:956.

Fix: accumulate in long instead (matching this file's existing convention — insertPitchPeriod already widens to long for the same reason), 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 had no other callers and is removed. The clamp 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, the same class of bug present elsewhere in this file (findSincCoefficient).

Adds tests/interpolate_overflow_test.c — a white-box test calling interpolate() directly (not static, for this reason). On a freshly created stream newRatePosition/oldRatePosition are both 0, which makes the internal ratio/width depend only on newSampleRate, so each tap's weight is known in advance; setting each sample to the maximum magnitude matching its tap's coefficient sign guarantees the true sum exceeds INT_MAX. Verified via UBSan during development: trips the old code, doesn't trip the fixed code, and both return the same correctly-clamped SHRT_MAX.

Deliberately not included: findSincCoefficient's separate left-shift bug sits on the exact same call path (interpolate calls it every invocation) but isn't fixed here, to keep this diff focused — it's fixed instead as a drive-by in the CI PR (#67), which needs it for the strict-and-sanitized job to pass -Werror+UBSan. Verified this fix is complete and correct in isolation by additionally patching that unrelated bug in a scratch copy and confirming the whole test suite is then completely clean under ASan+UBSan — once both this PR and #67 land, master will be fully clean.

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, confirmed via a
UBSan trace during fuzzing: signed integer overflow at sonic.c:956.

Replaced with accumulation in long (matching this file's existing
convention -- insertPitchPeriod already widens to long for the same
reason), 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 fixed elsewhere in this file (findSincCoefficient,
tracked separately as waywardgeek#50).

Fixes waywardgeek#66. 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 during
development to trip the old, unfixed code and to no longer do so after
the fix, while still returning the same correctly-clamped SHRT_MAX.

Note: findSincCoefficient's separate, already-tracked left-shift bug
(waywardgeek#50, PR waywardgeek#62) sits on the same call path (interpolate calls it every
invocation) and is NOT fixed here, kept deliberately out of scope to
avoid duplicating that PR's diff. Verified this fix is complete and
correct in isolation by additionally patching that unrelated bug in a
scratch copy and confirming the full test suite is then completely
clean under ASan+UBSan.
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.

Signed-integer-overflow UB in interpolate()'s overflow-detection idiom (sonic.c), and make fuzz has no ASan/UBSan

1 participant