Repository navigation
Conversation
ptweezy
force-pushed
the
igzip-stateful-fixes
branch
from
October 19, 2026 00:20
e1c6705 to
721d681
Compare
This was referenced Oct 19, 2026
ptweezy
force-pushed
the
igzip-stateful-fixes
branch
from
October 20, 2026 21:14
721d681 to
2d384cf
Compare
This was referenced Oct 21, 2026
ptweezy
force-pushed
the
igzip-stateful-fixes
branch
from
October 23, 2026 22:09
2d384cf to
3a36e65
Compare
When a call compresses all input from the caller's buffer and the flush type is FULL_FLUSH, get_hist_size() returns 0, so isal_deflate() keeps no history. The match history is reset when sync_flush() writes the sync marker: it sets has_hist to IGZIP_NO_HIST, and the next call resets the hash table. If the output buffer fills before the end of the block is written, the flush is still pending when isal_deflate() returns, and has_hist is not IGZIP_NO_HIST. If the caller supplies more input at that point, the next call compresses that input before it writes the sync marker, with the old hash table but without the history it refers to. At every level, match candidates can then point up to 32 KB before the start of the caller's input buffer, so the compressor reads memory outside the input. When those bytes happen to match, it emits a valid deflate stream that inflates to different data. When the call that leaves the flush pending compresses from the internal buffer, which holds the history of earlier input compressed with NO_FLUSH or SYNC_FLUSH, or of a dictionary, the history stays there. If the next call's input does not fit next to it, isal_deflate() clears the flush type while it compresses, so it writes a pending sync marker without resetting the match history, and the input after the marker is compressed against the data before it. A full flush is meant to make the following blocks independent of earlier data. When a call compresses all input while a full flush is pending, set has_hist to IGZIP_NO_HIST, so that the next call resets the match history before it compresses new input, as it does after the sync marker. Stored blocks are left alone: write_stored_block() resets the match history when it completes a full flush. Callers that keep the flush type until each flush completes, and supply more input only after it completes, get the same output as before. Add two tests to igzip_rand_test. One compresses with FULL_FLUSH and small output buffers at each level, and supplies more input while a flush is pending; without the fix, it fails for most seeds, including the default one. The other leaves the sync marker of a full flush pending at the start of a stream, after a SYNC_FLUSH, and after a dictionary, then supplies IGZIP_HIST_SIZE + 8 KB more input, and checks that the data after the marker decompresses on its own. Without the fix, it fails after the SYNC_FLUSH or the dictionary for most seeds. Signed-off-by: Parker Loflin <parker@cronstable.dev>
level_buf->icf_buf_avail_out holds the free space in the ICF buffer in bytes. init_new_icf_block() and compress_icf_map_g() store bytes, every reader treats the value as bytes, and the x86 level 1 and 2 functions subtract the bytes they write. update_state() in igzip_icf_base.c and the aarch64 level 1 and 2 body and finish functions store a count of deflate_icf entries instead, so every call that leaves the block open cuts the remaining space to a quarter. The block then ends early. In stateless compression this happens when the finish function runs after the body. In stateful compression it happens each time a call runs out of input, so smaller input buffers give more blocks. On dickens at level 1, 4 KB input buffers give 45.11% instead of the 45.01% of a single call. Store the space in bytes. Stateful level 1 and 2 compression of dickens in 4 KB, 16 KB, or 64 KB input buffers then gives the same size as a single call. Signed-off-by: Parker Loflin <parker@cronstable.dev>
print_perf_line() picks the label from the deflate strategy, so the isal stateless, isal stateful, and zlib inflate runs all print the label of the deflate run before them, for example zlib_inflate for every inflate run after a zlib level. Use info->inflate_mode, which the inflate runs already set. Signed-off-by: Parker Loflin <parker@cronstable.dev>
ptweezy
force-pushed
the
igzip-stateful-fixes
branch
from
October 24, 2026 01:49
3a36e65 to
c5d4955
Compare
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.
This PR fixes three igzip bugs, one per commit.
Reset the match history when a full flush is pending
Stateful
FULL_FLUSHcompression can read memory before the caller's input buffer and produce a valid deflate stream that inflates to the wrong data, at every level and on every architecture. This happens when the output buffer fills before the end of the block is written and the caller supplies more input before the flush completes.When a call compresses all of its input into a compressed block under
FULL_FLUSH,get_hist_size()drops the history, but the match history is reset only whensync_flush()writes the sync marker. The next call compresses its new input before writing the marker, with the old hash table but without the history it refers to, so match candidates point up to 32 KB before the new input buffer.To reproduce, call
isal_deflate()withFULL_FLUSHand 1 to 91 bytes of output space per call, add 1 to 117 bytes of input wheneveravail_inreaches 0, and check the result with zlib. On the first 512 KB ofdickens, master faults at levels 0 to 3 when each input chunk follows aPROT_NONEpage. When each chunk follows unrelated text, master's output is wrong in 1 to 4 of 20 runs at level 0 and in all 20 at levels 1 to 3, although zlib accepts every stream as valid. With this fix, no run faults or gives wrong output.If the call that leaves the flush pending compresses from the internal buffer instead, the history there (earlier
NO_FLUSHorSYNC_FLUSHinput, or a dictionary) stays. When the next call's input does not fit next to it,isal_deflate()compresses withflushset toNO_FLUSH, sosync_flush()writes the pending marker without resettinghas_hist, and the input after the marker is compressed against the data before it. The stream inflates correctly, butigzip_lib.hdocuments thatFULL_FLUSHmakes the following blocks independent. A test that leaves the marker pending after each chunk's first call (where the output size allows it) and then supplies 1 byte to 64 KB of new input (dickens, 400 streams per level) finds data that refers back across 293 of master's 2,620 sync markers. It also hits the first case: 3 of master's level 3 streams inflate to the wrong data.The fix sets
has_histtoIGZIP_NO_HISTwhen a call compresses all input into a compressed block while a full flush is pending, so the next call resets the match history before compressing new input. In the same test, the data after each of the 2,666 sync markers decompresses on its own. Output is byte-identical to master's for callers that keep the flush type until each flush completes and supply more input only after it does, as the existingigzip_rand_testcases do.The commit adds two tests to
igzip_rand_test.test_full_flush_pendingcompresses at each level withFULL_FLUSHand small output buffers, and supplies more input while a flush is pending.test_full_flush_pending_markerleaves the sync marker pending at the start of a stream, after aSYNC_FLUSHand after a dictionary, supplies 40 KB more input, and checks that the data after the marker decompresses on its own.Store the ICF buffer space in bytes
icf_buf_avail_outholds bytes everywhere except inupdate_state()(igzip_icf_base.c) and the aarch64 level 1 and 2 body and finish functions, which store an entry count. Each call that leaves the block open cuts the remaining space to a quarter, so blocks end early. Stateless level 1 and 2 compression is affected too: the finish function runs right after the body and sees a quarter of the space the body left, which withISAL_DEF_LVL1_MINorISAL_DEF_LVL2_MINlevel buffers is room for at most 217 ICF entries, so it can close the block early and put the rest of the input in another block. The x86 assembly already stores bytes, so x86 output does not change.With the fix, stateful level 1 and 2 compression of
dickensin 4 KB, 16 KB and 64 KB input buffers gives the same size as a single call. On master, aarch64 level 1 with 4 KB buffers gives 45.11% instead of 45.01%;arch=noarchis affected too.Label
igzip_perfinflate results by inflate modeigzip_perflabels the stateless, stateful and zlib inflate runs with the deflate mode, so all three print the same name.Conflicts
This PR conflicts with #448 in
igzip/igzip_rand_test.c.Testing
On x86-64 Linux (Ubuntu 24.04, GCC 13.3) and aarch64 Linux under qemu-user 8.2 (GCC 13.3):
tools/check_format.shpasses, andmake -f Makefile.unx checkpasses on x86-64, witharch=noarch, and on aarch64.igzip_rand_testpasses with seeds 1 to 20 on x86-64 and 1 to 10 on aarch64.test_full_flush_pendingfails with the default seed and 16 of seeds 1 to 20 (its buffer sizes come fromrand(), so the failing seeds depend on the platform), andtest_full_flush_pending_markerfails after theSYNC_FLUSHor the dictionary with the default seed and 19 of seeds 1 to 20.hist_bits, dictionaries, random buffer sizes, every input buffer ending at aPROT_NONEpage) matches master in all 30,000 x86-64 configurations. On aarch64, all 3,773 configurations at level 0 and 3,766 at level 3 match; at levels 1 and 2, the second fix changes 976 of 3,762 and 1,057 of 3,699 streams. zlib inflates all 7,775 finished raw deflate, gzip and zlib streams without a preset dictionary.