Validate seek table bounds in WaveBankReader - #668
Open
Roland Shum (ShumWengSang) wants to merge 1 commit into
Open
Validate seek table bounds in WaveBankReader#668Roland Shum (ShumWengSang) wants to merge 1 commit into
Roland Shum (ShumWengSang) wants to merge 1 commit into
Conversation
FindSeekTable returned a pointer into the seek segment without validating the subtable it points at, and its own bounds checks were incomplete: - The per-entry offset check tested only the start of the element, so an index whose last byte fell outside the segment was accepted. - The running offset was accumulated into a uint32_t from a size_t expression, so it truncated and a wrapped value passed the segment bound check. - The bound check permitted offset == seekSize, yielding a pointer one past the end whose leading count word was itself outside the allocation. - The count word at the head of each subtable was never validated, so callers indexing [0, count] could read far past the segment. GetDuration indexes it directly, and GetSeekTable exports it to callers. Validate centrally in FindSeekTable so all three call sites inherit the result, and accumulate in 64-bit. Callers already handle a nullptr return. Also step the big-endian conversion loop over whole uint32_t elements only: the seek segment length comes from the file and is not guaranteed to be a multiple of four, while the buffer is allocated at exactly that length, so a trailing partial element was read and written out of bounds.
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.
Validate seek table bounds in
WaveBankReaderSummary
FindSeekTablereturns a pointer into the wave bank's seek-table segment. It validated the offset it was handed but never the subtable that offset points at, and its own bounds checks were incomplete in three ways. The net effect is that values taken straight from the file can steer a read outside the seek segment allocation.The segment is laid out as
dwEntryCountuint32_toffsets, followed by per-entry subtables. Each subtable begins with its own element count.m_seekDatais allocated at exactlyseekLenbytes (WaveBankReader.cpp:745), andseekSizeinFindSeekTableis that same header field, so "inside the segment" and "inside the allocation" are the same condition.Defects
The per-entry offset check tested only the start of the element.
At
index * 4 == seekSize - 1this passes, andtable[index]then reads past the end. The whole element has to be inside the segment, not just its first byte.The running offset truncated.
sizeof(uint32_t) * data.dwEntryCountissize_t, so on 64-bit the right-hand side is computed in 64 bits and then narrowed into auint32_t. Both terms come from the file. A sum aboveUINT32_MAXwraps to a small value that then passes the segment bound check below.The bound check permitted
offset == seekSize.if (offset > seekSize)At equality the returned pointer is exactly one past the end of the allocation, so the leading count word — which every consumer reads first — is itself outside the buffer.
The subtable's count was never validated. The count word is attacker-controlled and consumers index
[0, count]against it.ENTRYCOMPACT::GetDurationdoes so directly (seekTable[seekCount], L379 for xWMA and L390 for XMA), and the publicGetSeekTableAPI exports the value outright:so a caller iterating the returned range reads out of bounds without
GetDurationbeing involved at all.Three call sites consume
FindSeekTable: the XMA2 format path (L957/L987),GetMetadata(L1155/L1158), andGetSeekTable(L1127). PatchingGetDurationalone would leave the exported count inGetSeekTableunfixed, so validation belongs inFindSeekTablewhere all three inherit it. All three already handle anullptrreturn.Change
FindSeekTablenow requires the whole offset element to be in the segment, accumulates in 64-bit, requires the leading count word to be fully in the segment before reading it, and validates the declared count against the bytes actually remaining. No signatures change and no new failure mode is introduced — callers already treatnullptras "no seek table."Separately, in
Open(), the big-endian conversion loop:steps four bytes at a time to
seekLenover a buffer allocated at exactlyseekLenbytes, with nothing requiringseekLento be a multiple of four. When it is not, the final iteration reads and writes up to three bytes past the allocation. The loop now steps over whole elements only. For a well-formed segment the behaviour is identical; the byte-swap path is selected by the file, sinceOpen()accepts either signature and derivesbefrom it (L562/L567).Validation
Built x64 Release via CMake — no errors, no new warnings.
Both the original and patched
FindSeekTablewere compiled verbatim into a differential harness (real C++ integer semantics, so theuint32_ttruncation behaves exactly as shipped) and swept over 47,784 combinations of segment size, entry count, index, offset value, and subtable count — including the boundary cases atseekSize,offset == seekSize, and offsets chosen to wrap the 32-bit accumulation.The oracle is the property consumers depend on: for a returned pointer, the count read plus the full
[0, count]window must lie inside[0, seekSize).The patch removes every unsafe result, never returns a different pointer where both versions accept, and never accepts anything the original rejected — the change is purely additive rejection.
Note for reviewers
The harness also counts misaligned returns: 1,011 for the original, 154 for the patched version.
offsetoriginates in the file and nothing requires it to be four-byte aligned, so*seekTableon aconst uint32_t*is undefined behaviour — pre-existing in both versions and not introduced here. Addingif (offset & 3) return nullptr;would eliminate it, but those 154 cases are in-bounds and safe, so rejecting them would restrict content beyond what the fix requires. Left out deliberately; happy to add it as a separate change if you'd prefer the stricter contract.