Skip to content

igzip: do not move next_in past the input in stateless inflate - #448

Open
ptweezy wants to merge 1 commit into
intel:masterfrom
ptweezy:igzip-inflate-stateless-end-input
Open

ptweezy wants to merge 1 commit into
intel:masterfrom
ptweezy:igzip-inflate-stateless-end-input

Conversation

@ptweezy

@ptweezy ptweezy commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

When the input of isal_inflate_stateless() ends inside a dynamic block header, it can return with next_in up to 7 bytes past the end of the input and avail_in wrapped to about 4 GB. This PR fixes that.

The bug

Before returning, isal_inflate_stateless() gives the unread whole bytes in its bit buffer back to the input: next_in -= read_in_length / 8 and avail_in += read_in_length / 8. setup_dynamic_header() checks that 14 bits of input are left, reads them, and then reads the code length code lengths (up to 57 more bits) before checking whether the input has ended. It then returns ISAL_END_INPUT with read_in_length as low as -57, so the give-back moves next_in forward instead of back, up to 7 bytes past the end of the input, and wraps avail_in, which is 0 at that point. The ISAL_INVALID_BLOCK returns of a truncated header can do the same.

The code is shared C, so every decoder has the bug. Stateful isal_inflate() restores the bit buffer itself when the input ends inside a header, so it is not affected.

Changes

Give back bytes only when read_in_length is positive: at 0 there is nothing to give back, and below 0 all of the input has been read and avail_in is already 0. The commit also adds a test to igzip_rand_test that inflates every prefix of a stream that starts with a dynamic block, stateless, and checks that next_in + avail_in stays at the end of the input.

Conflicts

This PR conflicts with #439 in igzip/igzip_rand_test.c.

Testing

On Linux x86-64:

  • make -f Makefile.unx check passes for the x86-64 build and arch=noarch, and tools/check_format.sh passes. Without the fix, the new test fails with both builds: for example, inflating the first 3 bytes of the stream returns ISAL_END_INPUT with next_in 6 bytes past the end of the input and avail_in 4294967290.
  • In a differential test of 100,000 valid and corrupted streams with the C decoder (arch=noarch), stateless and stateful with random buffer sizes, next_in + avail_in misses the end of the input in 41 cases on master, all stateless inflates whose input ends inside a dynamic block header, and in none with this PR.
  • On 40,000 valid and corrupted streams, this PR's x86-64 build gives master's results except in 19 stateless ISAL_END_INPUT returns, where master's avail_in wraps and this PR's is 0.

On Windows 11 (Ryzen 7 9800X3D), the mingw-w64 x86-64 build passes make check natively, including the new test.

On aarch64:

  • make check passes on macOS (Apple M4) with host_cpu=aarch64 arch=aarch64 and with arch=noarch, and on Linux in Docker (GCC 14.2) with Makefile.unx and autotools. Without the fix, the new test fails with both macOS builds, with the same numbers as on x86.
  • Against master's aarch64 build, a differential test inflated 120,000 valid and corrupted streams, stateless and stateful with random truncation, with the input ending at a PROT_NONE page. On master, next_in + avail_in misses the end of the input in 929 of them, all stateless inflates of truncated input (928 ISAL_END_INPUT returns and 1 ISAL_INVALID_BLOCK), with next_in 1 to 6 bytes past the end. With this PR it is at the end in all 120,000, and the return codes, total_out and output match master's.

When isal_inflate_stateless() returns, it gives back the whole bytes
left in its bit buffer, moving next_in back and avail_in up by
read_in_length / 8. When the input ends inside a dynamic block header,
setup_dynamic_header() can read up to 57 bits past the end of the input
before it checks for the end and returns ISAL_END_INPUT, which leaves
read_in_length negative. The give-back then moves next_in forward, up
to 7 bytes past the end of the input, and avail_in, which is 0 by
then, wraps to about 4 GB.

Give back bytes only when read_in_length is positive. At 0 there is
nothing to give back, and when it is negative, all of the input has
been read and avail_in is already 0. isal_inflate() restores its bit
buffer itself when the input ends inside a header, so it does not have
this problem.

Add a test to igzip_rand_test that inflates every prefix of a stream
that starts with a dynamic block, and checks that next_in + avail_in
stays at the end of the input.

Signed-off-by: Parker Loflin <parker@cronstable.dev>
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