Skip to content

seekable: reject seek tables exceeding ZSTD_SEEKABLE_MAXFRAMES#4685

Open
evilgensec wants to merge 1 commit into
facebook:devfrom
evilgensec:fix/seekable-numframes-maxframes
Open

seekable: reject seek tables exceeding ZSTD_SEEKABLE_MAXFRAMES#4685
evilgensec wants to merge 1 commit into
facebook:devfrom
evilgensec:fix/seekable-numframes-maxframes

Conversation

@evilgensec
Copy link
Copy Markdown

What

ZSTD_seekable_loadSeekTable() reads Number_Of_Frames from the seek table footer and uses it, unbounded, to size:

  • the seek-table arithmetic — tableSize = sizePerEntry * numFrames (U32), and
  • the entry array — malloc(sizeof(seekEntry_t) * (numFrames + 1)).

It never checks the value against ZSTD_SEEKABLE_MAXFRAMES, the limit the writer enforces in ZSTD_seekable_logFrame() (zstdseek_compress.c). The reader therefore accepts a frame count the writer would never emit.

Impact

A crafted seek table can set Number_Of_Frames close to 2^32.

  • On platforms where size_t is 32-bit, sizeof(seekEntry_t) * (numFrames + 1) overflows. The entry array is under-allocated, but the cumulative-position loop still writes numFrames entries — a heap out-of-bounds write.
  • On 64-bit platforms the multiplication is done in a 64-bit size_t, so it does not overflow; the allocation simply becomes huge and fails cleanly, so there is no out-of-bounds write. The U32 tableSize / frameSize arithmetic still wraps on these platforms.

The footer field is part of the trailer of an otherwise valid seekable archive, so a reader handed an attacker-influenced file reaches this path.

Fix

Reject Number_Of_Frames > ZSTD_SEEKABLE_MAXFRAMES before the size computations, mirroring the bound applied on write. With the limit in place neither sizePerEntry * numFrames (≤ 12 * 0x8000000) nor sizeof(seekEntry_t) * (numFrames + 1) can overflow a 32-bit size_t, and an over-large value is rejected with corruption_detected.

Testing

  • contrib/seekable_format compiles clean under -Wall -Wextra -Werror.
  • make -C contrib/seekable_format/tests test passes (round-trip, the two no-hang cases, empty-string magic, multi-decompress) — valid archives are unaffected, since the bound (0x8000000) is far above any frame count a writer produces.

ZSTD_seekable_loadSeekTable() read Number_Of_Frames from the seek table footer and used it to size the entry array and the table-size arithmetic without bounding it by ZSTD_SEEKABLE_MAXFRAMES, the limit the writer enforces in ZSTD_seekable_logFrame().

A crafted seek table could set Number_Of_Frames close to 2^32. On platforms with a 32-bit size_t this overflows the malloc(sizeof(seekEntry_t) * (numFrames + 1)) size computation, under-allocating the array while the parse loop still writes numFrames entries, a heap out-of-bounds write. 64-bit platforms are unaffected: the oversized allocation does not overflow and fails cleanly. Reject the value before these computations, matching the limit applied on write.
Copilot AI review requested due to automatic review settings June 3, 2026 11:49
@meta-cla meta-cla Bot added the CLA Signed label Jun 3, 2026
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds defensive validation when loading a seek table to prevent oversized numFrames values from causing unsafe allocations or arithmetic issues during decompression.

Changes:

  • Introduces an early rejection when numFrames exceeds ZSTD_SEEKABLE_MAXFRAMES.
  • Refactors tableSize / frameSize computation into separate variables after validation.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +396 to +405
U32 tableSize;
U32 frameSize;

/* numFrames is bounded by ZSTD_SEEKABLE_MAXFRAMES on write; reject larger
* values to avoid overflowing the entry-array allocation below. */
if (numFrames > ZSTD_SEEKABLE_MAXFRAMES) {
return ERROR(corruption_detected);
}
tableSize = sizePerEntry * numFrames;
frameSize = tableSize + ZSTD_seekTableFooterSize + ZSTD_SKIPPABLEHEADERSIZE;
@evilgensec
Copy link
Copy Markdown
Author

Closing this. I'll route the change through the project's preferred reporting process instead. Apologies for the noise.

@evilgensec evilgensec closed this Jun 3, 2026
@evilgensec
Copy link
Copy Markdown
Author

Reopening to keep the fix available for review.

@evilgensec evilgensec reopened this Jun 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants