1
0
mirror of https://github.com/lz4/lz4.git synced 2026-10-24 01:48:57 +03:00

152 Commits

Author SHA1 Message Date
Yann Collet
5d63c34ac0 tests: move the simple block unit tests from fuzzer.c to unittest
33 tests, in 2 new suites : lz4 (decoding of invalid input, one-shot and streaming compression)
and lz4hc (streaming, dictionaries, destSize). Tests depending on the HC level run on both levels
previously selected by fuzzer.

UT_sample(), KB, MB and ARRAY_SIZE() are now provided by unittest.h, for all suites.
2026-10-23 10:54:38 -07:00
Yann Collet
7fcff05478 Merge pull request #1928 from lz4/mid_lazy_short_match
level 2 : check ip+1 after any short match
2026-10-22 15:49:14 -07:00
Yann Collet
35d74f85f7 lz4hc: LZ4_saveDictHC() before any compression no longer corrupts the next block
On a new or reset stream (prefixStart == NULL), LZ4_saveDictHC() installed @safeBuffer as prefix,
which skipped the initialization done by the next compression (LZ4HC_init_internal()).
The stream then started at index 0, which the hash table uses for empty entries :
positions matched themselves (offset 0), and the block decoded into wrong data, at all HC levels,
also with an attached dictionary. Present in v1.10.0.
It now saves nothing, leaving the stream to be initialized by the next compression.
Found by lz4frame with uncompressed then compressed segments in linked blocks.
tests : fuzzer.c saveDictHC test #926 compresses after LZ4_saveDictHC(), without and with an attached dictionary.
2026-10-22 12:58:19 -07:00
Yann Collet
81ee260c4e level 2 : check ip+1 after any short match
LZ4MID_compress() checked ip+1 for a longer match only after a match
found by the 4-byte table within the prefix, never after a match found
by the 8-byte table. The check now follows any match shorter than 8 bytes,
also within an external dictionary, and skips the candidate ip itself:
inside runs, ip+1 would find ip at offset 1, and periodic data would
lock onto one literal and one offset 1 per run (4 KB to ~1.6 KB).
The previous check is removed.

Compressed size : linux-6.12 tar -1.3%, Silesia -0.6%, enwik9 -0.5%
(4 MB blocks), -0.25% (4 KB blocks), -0.9% (64 KB linked blocks),
-0.2% (4 KB pages with a 64 KB dictionary).
Speed, AMD EPYC Genoa (Zen 4), x86-64, gcc 11 / gcc 15 / clang 22 :
-0.5 to -1.3% (256 KB blocks), -1.3 to -2% (4 KB blocks),
-1.8 to -2.5% (64 KB linked blocks), -0.2 to -1.2% (4 KB pages with a dictionary).
x86 (gcc 12 -m32) : +1.4% (4 KB pages with a dictionary) to -4.9% (64 KB linked blocks).
2026-10-22 03:45:59 -07:00
Yann Collet
4b45bb5e32 fast levels : first test the position following the previous occurrence of ip-1
After each match, the fast parser only tested the most recent occurrence
of the sequence at ip. On periodic data with long runs, it is a short
match, and such matches can chain until the end of the input :
an 8-byte dictionary made a 4 KB page compress to 1543 bytes instead
of 49 (#1805). It also happens without dictionary.

The position following the previous occurrence of ip-1 is now tested
first, also within the dictionary, and taken if it matches 8+ bytes.
The table is filled with ip-1 instead of ip-2.
Both candidates share the same dictionary / current-block selection.

Silesia, compressed size : -0.6% (256 KB blocks), -0.3% (4 KB blocks),
-0.5% (4 KB pages with a 64 KB dictionary).
Speed, AMD EPYC Genoa (Zen 4), x86-64, gcc 11 / gcc 15 / clang 22 :
-0.8 to -1.2% (256 KB blocks), -1.7 to +1.5% (4 KB blocks),
-6 to +1% (4 KB pages with a dictionary).
x86 (gcc 12 -m32) : +1% (4 KB blocks) to -11% (dictionary).

Closes #1805
2026-10-21 20:54:37 -07:00
Yann Collet
f4f7a9882c lz4: decoder still supports rings of the former LZ4_DECODER_RING_BUFFER_SIZE()
A ring buffer of LZ4_DECODER_RING_BUFFER_SIZE(maxBlockSize) is allocated statically,
so its size is compiled into applications.
16d695b1 enlarged its margin from 14 to 64 bytes to fit the fast loop's wide copies,
leaving applications compiled with the former macro exposed to corrupted output.

The decoder now also supports the former margin, without advertising it.
When decoding restarts at the beginning of the ring, the oldest referenced history
lies in the external dictionary, ahead of the write position, at a constant distance.
When this distance is shorter than FASTLOOP_SAFE_DISTANCE, the block is decoded by the safe loop,
which writes at most 16 bytes beyond a copy, like the v1.8.x decoder the 14-byte margin was sized for.
This only concerns rings with less than 64 bytes of margin, hence never the current macro,
for the blocks following such a restart, until 64 KB have been decoded.
Other decoders are unaffected : the test is resolved at compile time outside of usingExtDict.

Applications compiled with older headers decode correctly with the new library, without recompiling.

The static assert now checks what correctness requires, i.e. the safe loop's margin,
which the former macro satisfies : on this point, lz4.c compiles again with an older lz4.h (#1863).

The fuzzer's worst-case ring test now also runs with the former ring size :
it fails without this change, and passes with it.
The other ring condition (decoder ring larger than the encoder's by maxBlockSize + 64 bytes, 2f449bb3)
is unchanged : the decoder can't know the encoder's ring size.
2026-10-19 16:24:45 -07:00
Yann Collet
023d031682 Merge pull request #1895 from lz4/saveDict_negative
Save dict negative
2026-10-19 07:59:05 -07:00
Yann Collet
354572d2d1 LZ4_saveDict(): a negative dictSize saves nothing
Follow-up to the previous commit: instead of returning early,
clamp a negative dictSize to 0, like LZ4_saveDictHC().
Returning early left the stream referencing the previous dictionary,
which the caller is about to overwrite, since it asked to save it:
the next block could then reference stale data.
With dictSize = 0, the stream continues without dictionary.

fuzzer: the next block copies the end of the dictionary,
and must decode without it (fails with the early return),
and saveBuffer is small enough for ASan to detect
the 64 KB copy (fails on dev).
2026-10-18 11:51:15 -07:00
arthuz-th
6b1d673e3c Reject negative dictionary sizes in LZ4_saveDict 2026-10-18 11:47:13 -07:00
arshiya tabasum
e829d4b3df lz4: reject negative dictSize in dictionary entry points
LZ4_decompress_safe_usingDict(), LZ4_decompress_safe_partial_usingDict(),
LZ4_decompress_fast_usingDict() and LZ4_setStreamDecode() only assert()
that dictSize is not negative, then cast it to size_t. In release builds
the dictionary becomes ~SIZE_MAX bytes long, so every match offset is
accepted and decoding reads before the dictionary buffer.

LZ4_loadDictHC() has the same assert(): on lz4mid levels the fill target
of LZ4MID_fillHTable() wraps and it reads up to 32 KB before the
dictionary.

The decoders now return -1, LZ4_setStreamDecode() returns 0 (its
documented error), and LZ4_loadDictHC() loads no dictionary, as
LZ4_loadDict() already does for a negative size.
2026-10-18 22:46:29 +05:30
Yann Collet
72b82c1720 lz4: LZ4_decompress_safe_partial() clears the match segment of an invalid offset 0
Near the end of a partial decoding, a match with offset 0 was copied onto
itself, leaving previous dst content in decoded data. doc/lz4_Block_format.md
warns against it (information disclosure), and other decoding paths
already produce 0 bytes. Clear the match segment instead.
2026-10-17 16:33:40 -07:00
Yann Collet
b19a8f7536 lz4: reject negative compressedSize in LZ4_decompress_safe_partial*()
Since v1.9.3 (c5d6f8a8), partial decoding clamps literals to the end of input.
With a negative srcSize, iend points before src: (size_t)(iend-ip) became huge,
and the literals memmove() ran far past both buffers.

Reject it in the existing srcSize==0 test of LZ4_decompress_generic(),
which becomes srcSize<=0: the generated code only changes je into jle.

The NEWS line also covers the LZ4_decompress_fast*() fix of #1875 (since v1.9.4).
2026-10-17 14:04:22 -07:00
Yann Collet
f110dbabd6 lz4: reject negative originalSize in LZ4_decompress_fast*()
A negative originalSize made oend point before the output buffer:
(size_t)(oend-op) then became huge, disabling every output bound check,
so decoding wrote and read past the buffers.

Reject it once, in LZ4_decompress_unsafe_generic(), which backs all
LZ4_decompress_fast*() entry points, as LZ4_decompress_generic()
already does for the safe variants.
2026-10-17 09:03:57 -07:00
Yann Collet
5584cff72b lz4: LZ4_compress_destSize_extState(): clamp acceleration, like other entry points
Values <= 0 produced corrupted output, and large negative values
read out of bounds. The clamp is now a single helper, LZ4_clampAcceleration(),
used by all 4 entry points.
2026-10-14 16:18:19 -07:00
Yann Collet
db8a56befe tests: LZ4_compressHC2_continue() round-trip
Fails on dev : LZ4_compressHC2_continue() always returns 0.
2026-10-13 16:27:56 -07:00
Yann Collet
a732f2a17e tests: LZ4_compress_fast_continue() after LZ4_loadDict() with a dictionary too small to be used
LZ4_loadDict() ignores dictionaries smaller than HASH_UNIT.
A large first block must then give the same output as a new stream,
directly or through LZ4_attach_dictionary().

Fails on dev : after LZ4_loadDict(), the output differs.
The fuzzer can detect it too, as a difference between
LZ4_compress_fast_continue() using extDict and using extDictCtx (seed 9016, cycle 8086).
2026-10-13 14:08:59 -07:00
Yann Collet
b4ee63fedc tests: decode into a ring larger than the encoder's by maxBlockSize + 64 bytes
Worst case, with dstCapacity = maxBlockSize : decoding restarts at the beginning of the ring,
a block ends with a wide copy writing beyond its end, a 1-byte block follows,
then a block references the oldest byte allowed by the encoder's ring.

With the formerly documented margin (maxBlockSize), the output is corrupted since v1.9.0.
2026-10-12 19:32:50 -07:00
Yann Collet
44ca30bbb4 tests: LZ4_compress_*_continue() with input overwriting the previous block
The new block overwrites the previous one (its dictionary) at the beginning, at the same place,
entirely, or at the end. Output decoded from a separate buffer must be correct.

Fails on dev : LZ4_compress_fast_continue() only handles an overwritten beginning.
2026-10-12 18:32:28 -07:00
Yann Collet
fff2a63d51 tests: decode any valid block into a ring of LZ4_decoderRingBufferSize()
Worst case : decoding restarts at the beginning of the ring with maxBlockSize-1 bytes left at its end,
then a wide copy (long literals or long match) is followed by a match at maximum distance.
The copy writes beyond its end, over the oldest history, still referenced by the match.

Fails on dev : on x86, the margin must be 29 bytes (LZ4_wildCopy32) instead of 14.
2026-10-12 16:55:03 -07:00
Yann Collet
e0f0df86c9 tests: LZ4_compress_fast_extState_fastReset() after LZ4_attach_dictionary() on a new state
Attaching a dictionary to a new state, then compressing with one-shot fast reset,
must give the same output as a new state.
It currently doesn't : the block finds no match, and debug builds hit an assert.
2026-10-12 15:29:51 -07:00
Yann Collet
3d8a138ab7 tests: LZ4_resetStream_fast() with a large first block gives the same output as a new stream
- unit test : a 64 KB block, compressed on a new state,
  then as a new stream on the same (now used) state, must be identical,
  including after dropping history with LZ4_saveDict(stream, buffer, 0).
  The input starts with a 4-byte sequence which reappears right after,
  followed by a different 5th byte.
- fuzzer : a state keeps a history across cycles;
  each block starts a new stream on it with LZ4_resetStream_fast(),
  and blocks >= 64 KB must match the output of a new stream.

64 KB is beyond the table clearing threshold, whatever LZ4_MEMORY_USAGE.

These tests fail on current dev :
LZ4_resetStream_fast() keeps the table, and a used table rejects its stale entries,
while a cleared table tests position 0 for unused hash slots.
2026-10-09 13:35:34 -07:00
Yann Collet
496d73578c tests: LZ4_compress_fast_extState_fastReset() output is independent of the state
Compare its output with LZ4_compress_fast_extState(), which fully initializes the state.
Whether the fast reset clears the table or reuses it,
the output must be the same :
below 64 KB, the table uses a 4-byte hash,
so reusing a table can't miss a match that a cleared table would find.
Beyond, the table is always cleared.

This guards a change of the threshold deciding to clear the table.
2026-10-09 11:45:33 -07:00
Yann Collet
3ad3a1a3f1 tests: favorDecSpeed survives a failed HC compression
A failed compression marks the HC state as dirty,
and LZ4_resetStreamHC_fast() then fully re-initializes it.

- fuzzer : after a failed compression, the next stream
  still favors decompression speed, as it would without the failure.
- frametest : with independent blocks at level 10 and favorDecSpeed,
  a block following an incompressible block is identical
  to the same block compressed alone.
  LZ4F compresses each block into srcSize-1 bytes,
  so any incompressible block is a failed compression.

These tests fail on current dev :
the re-initialization also resets favorDecSpeed,
so LZ4F ignores it after the first incompressible block of a frame.
2026-10-09 09:58:45 -07:00
Yann Collet
fe1d3925da fix ip reaching iend in safe decode loop shortcut path
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 14:34:25 -07:00
Yann Collet
a1eaa9342a fix ip reaching iend in fast decode loop 2026-06-01 14:34:25 -07:00
Yann Collet
0568154788 add unit test for all-0xFF input to LZ4_decompress_safe() 2026-06-01 14:34:25 -07:00
Yann Collet
b8af3e2f74 fix fuzzer test
even the _destSize*() variants need a targetDstSize >= 1 to be successful,
since it's the smallest valid payload allowed by LZ4.
2025-01-16 11:47:15 -08:00
Yann Collet
527431e832 fix minor scan-build warning 2024-12-23 13:30:37 -08:00
Yann Collet
de629c38e9 fix modulo 0, part 2 2024-12-23 13:08:39 -08:00
Yann Collet
eba7d05b8e fix modulo operation in tests 2024-12-23 11:31:28 -08:00
Yann Collet
710ed4f0d7 no good reason to FORCE_INLINE 2024-12-23 11:03:24 -08:00
Yann Collet
5c2068c606 fix lz4_opt behavior when srcSize == 0
reported by @rinsuki
2024-12-23 10:46:33 -08:00
Yann Collet
f458b2b1a5 LZ4_loadDictHC is compatible with LZ4MID level 2
But the state must have its compression level set _before_ loading the dictionary.
2024-07-17 23:01:08 -07:00
Yann Collet
7480aeed02 added a dictionary loader dedicated to level 2 (LZ4MID) 2024-07-17 12:44:52 -07:00
Yann Collet
ad204dc099 fix "source has 2 buffers" for level 2
by not searching into the 2nd buffer
which is an inefficiency (less compression opportunities)
but makes the code sensibly simpler to read
and is a use-case avoided by lz4frame.
2024-02-07 15:24:44 -08:00
Yann Collet
88e477db6b fix 1308
PR #1308 introduces a new symbol, `LZ4_compress_fast_extState_destSize()`,
which, upon inspection, doesn't seem to work as advertised.
In particular, it fails if `dstCapacity` is not large enough,
as opposed to adjusting input size to fit within `dstCapacity`.

Even if it had worked, it would have resulted in problems
for the existing `LZ4_compress_fast_extState()`,
that would have stopped failing when `dstCapacity` is not large enough,
resulting in silent input truncation.

Changed the approach.
It appears the wanted functionality already exists within `lz4` source code,
and is called `LZ4_compress_destSize_extState()`.
It wasn't exposed dues to a (non-verified) problem regarding lz4 state on exit:
it's claimed that since the `_destSize` doesn't completely consume the input,
the state may end up in an "unfinished" status,
making it dangerous to re-employ in later invocations.

Fixed that by enforcing a state initialization at the end of operation.

Also : added some tests,
ensure that `LZ4_compress_fast_extState()` fails as expected when `dstCapacity` is not large enough,
and ensure that `LZ4_compress_destSize_extState()` does succeed in the same circumstance,
by consume less input.

Finally, expose `LZ4_compress_destSize_extState()` as an advanced experimental symbol.
2023-12-27 20:56:27 -08:00
Yann Collet
dc474bd84a fix minor conversion warnings
appearing with clang-17
2023-12-19 23:11:29 -08:00
Dimitri Papadopoulos
bdfe4c0c98 Enclose by a do-while loop to avoid possible if/else logic defects 2023-06-25 17:43:51 +02:00
Takayuki Matsuoka
eeb33d10b0 fix: workaround for false positive analysis from MSVC v17.6 (part 2)
Same as d678159c64
2023-05-20 22:21:44 +09:00
Takayuki Matsuoka
46d12d661e Suppress false positive warning from MSVC (fuzzer.c)
Suppress the following false positive warnings from MSVC:
- Disable all arithmetic overflow (C26451)
- Suppress C6385: Reading invalid data from 'compressedBuffer'.
- Add ULL suffix to unsigned 64-bits constants.
2022-08-13 00:36:28 +09:00
Qi Wang
ec75db2294 tests: add tests for LZ4_decompress_safe_partial_usingDict
Signed-off-by: Qi Wang <wangqi@linux.alibaba.com>
2022-06-07 17:12:51 +08:00
Yann Collet
4b7a327068 fix strange printf formatting warning
so now, `%p` _requires_ a `void*` pointer ?
2020-12-01 08:05:19 -08:00
Yann Collet
87a80acbe7 updated license & header dates 2020-11-25 14:45:14 -08:00
Yann Collet
e0f85f2fc8 better visual conformance
only include <intrin.h> on vs2005+ (#947)
remove some useless #pragma
fix a few minor Visual warnings
2020-11-14 16:27:57 -08:00
Yann Collet
c76564b944 attempt at silencing cppcheck 2020-11-08 19:48:47 -08:00
Yann Collet
b16b9fcdbc added similar tests for LZ4_saveDict()
fast (non-HC) variant
2020-11-07 18:24:02 -08:00
Yann Collet
a2222a879f fix #926
fix incorrect behavior of LZ4_saveDictHC()
when invoked right after initialization.
2020-11-07 18:12:26 -08:00
Yann Collet
e968a24129 unified alignment test
across lz4.c and lz4hc.c
2020-11-06 14:46:48 -08:00
Yann Collet
ad552101dd fix minor explicit cast 2020-10-31 01:55:04 -07:00
Yann Collet
4b97866333 reduce new test stack usage
use heap instead
2020-10-02 18:03:12 -07:00