Fix off-by-one OOB table read in Sample::next()#331
Merged
Conversation
Sample::next() used a strict > comparison (phase_fractional > endpos_fractional) to decide when to wrap/finish playback, while isPlaying() already treats phase_fractional == endpos_fractional as finished (using <). When phase lands exactly on endpos_fractional (which happens deterministically for many freq/table-size ratios), next() would compute index = endpos_fractional >> SAMPLE_F_BITS == NUM_TABLE_CELLS, one past the last valid table entry, and read (and for INTERP_LINEAR, also read index+1) out of bounds before wrapping on the following call. Change the comparison to >= so next() treats phase == endpos as finished/wrap, consistent with isPlaying(), eliminating the out-of-bounds table read.
Collaborator
|
Looks good indeed, thanks for spotting that and for the detailed explanation of the root of the problem. Merging. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Sample::next()uses a strict>comparison to decide when playback hasreached the end of the table and should wrap (looping) or stop:
if (phase_fractional>endpos_fractional){isPlaying()already treatsphase_fractional == endpos_fractionalas"finished" (it uses
phase_fractional<endpos_fractionalto mean "stillplaying"), so the two are inconsistent at the exact boundary.
endpos_fractionalis set toNUM_TABLE_CELLS << SAMPLE_F_BITS(i.e. exactlyone past the last valid table index, in fixed-point). Because
phase_increment_fractionalis derived from an integer frequency step, for alot of common frequency/table-size combinations
phase_fractionallandsexactly on
endpos_fractionalafter some number of calls (not just "goespast" it). When that happens,
next()still takes theelsebranch thiscall, computes:
which reads one element past the end of
table(and, underINTERP_LINEAR, also readstable[index+1], two past the end) before thewrap/stop logic finally kicks in on the following call.
I confirmed this with a standalone harness (compiling
next()/incrementPhase()copied verbatim from this file against a real buffer witha canary byte placed one past the array): with a 100-cell table clocked to
land exactly on
endpos_fractional, the existing code reads the canary byte(confirmed out-of-bounds access) once per loop cycle; under
INTERP_LINEARit reads two canary bytes past the end.
examples/08.Samples/Sample_Loop_Pointsexercises exactly this full-range-plus-looping configuration against a
table (
ABOMB_DATA, 16384 cells, no padding) where the OOB read would landoutside the array.
Changing the comparison to
>=makesnext()treatphase == endposasfinished/wrap, matching
isPlaying(), removes the OOB read, and does notchange behavior for the (much more common) case where phase never lands
exactly on
endpos—>and>=are equivalent whenever equality neveroccurs.
Minimal one-character fix, no behavior change other than removing the OOB
read.