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

617 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
98c203d9e2 Merge pull request #1931 from lz4/cli_rm_test_stdin
cli: --rm skips -t and stdin
2026-10-22 17:23:19 -07:00
Nexory
24e765a099 cli: --rm skips -t and stdin
The manual says that -t neither creates nor removes files, and that
--rm deletes the source after a successful compression or
decompression. The CLI did not follow it for two cases :

- `lz4 -t --rm file.lz4` tested the file, then deleted it.
- With stdin as source, the output was complete, but remove("stdin")
  failed and the command ended with "Remove error : stdin"
  (exit code 50 when compressing, 45 when decompressing).
  A file named `stdin` in the working directory would have been deleted.

One helper, LZ4IO_mustRemoveSrcFile(), now decides. It answers no in
test mode and for stdin, and both remove sites use it :
LZ4IO_finishCompressedFile() and LZ4IO_decompressSrcFile().
A regular source file is still removed.
2026-10-22 16:09:26 -07:00
Yann Collet
04c771f442 tests: add unittest, a unit test framework with autonomous tests
Each test is a function with its own context : resources are released at the end of the test,
even when it fails, and the random generator restarts from the same seed.
A failed assertion ends the test, the runner continues with the next one.

First suite : 18 lz4frame unit tests, moved from frametest.c
2026-10-22 15:52:14 -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
7947beddd6 frametest: incompressible test data no longer depends on FUZ_rand() cycles
FUZ_rand() cycles can be short : 31487 values for seed 2145. Bytes taken directly from it
then repeat every ~31 KB, and blocks expected to be stored uncompressed compress (to ~50%) :
testMixedUpdates() and testFavorDecSpeedAfterRawBlock() failed with seed 2145, also on dev.
make test picks a random seed : ~1 run in 240 failed here.
FUZ_fillNoise() generates incompressible bytes as a hash (XXH32) of their position, keyed by FUZ_rand(),
for the 8 buffers or segments filled with noise. Seeds 1 to 2400 pass.
2026-10-22 12:58:19 -07:00
Yann Collet
c0a1b631e6 review round 12: LZ4F_uncompressedUpdate() supports linked blocks ; CLI threads capped by file jobs and memory budget, with warnings ; Windows and Makefile fixes (checkpoint)
lz4frame :
- LZ4F_uncompressedUpdate() accepts linked blocks (dev and v1.10.0 could silently corrupt such frames) :
  single-threaded mode drops the LZ4 state's history when an uncompressed segment starts
  (the decoder's history contains the uncompressed data) ; multi-threaded mode needs nothing,
  jobs reload the 64 KB actually preceding them. LZ4F_compressBegin() now starts frames in compressed mode :
  the mode of the previous frame's end was kept, which skipped the history drop (frame corrupted with a dictionary).
- Windows : worker stacks reserved instead of committed (STACK_SIZE_PARAM_IS_A_RESERVATION) ;
  #error when <windows.h> was already included without the Vista API (amalgamated sources) ;
  doc : no multi-threaded context use nor release under the loader lock (DllMain, DLL global objects).
lib/Makefile, programs/Makefile : threads detection skipped by goals building nothing threaded (clean, uninstall, ...),
run once per command (sub-makes inherit it) : make install probes once instead of 2-4 times, make clean never.
lz4 : explicit -T# also limited to the jobs of each file (-T1000 on a 10 MB file : 3 workers, RSS 298 -> 27 MB at -9),
and by the memory budget on all systems (-T99999 : 1024 liblz4 maximum, 331 threads within the 4 GB default budget) ;
an explicit nb of threads reduced triggers a warning (automatic : only with -v).
tests : frametest testUncompressedLinked (linked blocks, dictionary made of input, frames ending uncompressed),
mixed updates and small-dst scripts and fuzzer with linked blocks ; frameMT uncompressed segments with linked blocks ;
ossfuzz stream and uncompressed round-trip fuzzers with linked blocks ; test-lz4-multithread budget warnings.
2026-10-22 12:58:19 -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
124e0d4da8 lz4frame: exact bounds, the job is the unit with nbWorkers >= 1 ; uncompressed segments are jobs ; CLI output buffer independent of nb of threads (checkpoint)
LZ4F_compressBound(), without autoFlush :
- single-threaded : (srcSize / blockSize + 1) blocks, stored uncompressed in worst case, plus the frame footer.
  It covers up to (blockSize-1) buffered bytes, a buffered block emitted separately (mode switch),
  or remaining from an incomplete LZ4F_flush(). srcSize == 0 : one block + footer, so that LZ4F_flush() and
  LZ4F_compressEnd() complete in a single invocation, strictly below the bound (v1.10.0 guarantee kept).
- nbWorkers >= 1 : the job replaces the block : ceil(srcSize / jobSize) jobs (1 for srcSize == 0) + footer,
  independently of nbWorkers. Updates defer completed jobs not fitting into dstBuffer : output is only required
  to start a job while all slots are busy. Flush and end emit jobs in flight in several invocations.
  nbWorkers is new : no existing user depends on a single invocation there.
Multi-threaded LZ4F_uncompressedUpdate() : uncompressed segments are jobs storing blocks uncompressed ;
a mode switch submits the job being filled (same output as single-threaded mode). A single LZ4F_mt_update()
serves both updates : uncTail, LZ4F_mt_emitUncTail() and LZ4F_uncompressedUpdate_MT() are removed.
lz4 : output buffer sized by LZ4F_compressBound(chunk) (~4 MB, instead of 72 MB with 8 threads),
end of frame in a loop ; more threads within a memory budget (--memory=300M : 13 -> 23 threads) ;
releaseLag = (2*nbWorkers+1) jobs + 64 KB.
tests : frameMT (bound structure, flush / end loops), stream fuzzer (reference loops with nbWorkers).
2026-10-22 12:58:19 -07:00
Yann Collet
fa086fd0d6 lz4frame: LZ4F_flush() and LZ4F_compressEnd() fill dstBuffer or complete ; CLI releaseLag from LZ4F_compressBound(0) (checkpoint)
LZ4F_flush() and LZ4F_compressEnd() : each invocation fills dstBuffer, or completes :
while it returns dstCapacity, output may remain, and the operation is invoked again, with any dstCapacity.
Output remains strictly below LZ4F_compressBound(0) (+1 for autoFlush), so that callers sizing
dstBuffer with the bound see no change : a single invocation completes.
- the first invocation sets the flush point : buffered input is compressed (all jobs waited for),
  so input is no longer read after it returns (LZ4F_srcStableUntilFlush lifetime unchanged) ;
- single-threaded : a block not fitting is compressed into stageBuff (allocated on first need), same output ;
  multi-threaded : the oldest job can be emitted partially (mt->emitted) ; frame footer staged (8 bytes) ;
- compressUpdate() / uncompressedUpdate() may follow an incomplete flush : remaining output comes first ;
  until an end completes, only LZ4F_compressEnd() continues it (LZ4F_compressBegin() abandons it) ;
- dstCapacity 0 is rejected when output remains, without modifying the frame ;
- contentSize mismatch is detected when the end starts, before writing anything ;
- LZ4F_compressFrame*() keep failing with dstCapacity_tooSmall when the frame doesn't fit.
lz4file : LZ4F_writeClose() loops on LZ4F_compressEnd().
lz4 : releaseLag = LZ4F_compressBound(0) + 64 KB (bound of input retained within cctx).
tests : frametest (#1817 streaming probes, fuzzer), frameMT (validatePartialFlush, small dst, strict bound),
stream fuzzer : flush and end in pieces.
2026-10-22 12:58:19 -07:00
Yann Collet
a88e3a7311 review round 11: allocations within workers documented, CLI nb of threads only grows across files, single-job files without workers, compressBound() with one job slot without multithreading (checkpoint)
lz4hc levels >= 10 allocate a temporary buffer (LZ4HC_HEAPMODE) within worker threads :
documented in lz4frame.h (nbWorkers), lz4frame.c (pool) and lz4.c (LZ4_USER_MEMORY_FUNCTIONS
must then be thread-safe).
lz4 : the automatic nb of workers no longer shrinks from one file to the next
(cRess_t.nbWorkers high-water mark) : worker threads and buffers are kept
(-m on 300 files of 64 KB - 8 MB : 1156 -> 21 thread creations).
lz4 : with independent blocks, a file fitting into a single job is compressed by the calling
thread (nbWorkers 0, same output). Not with linked blocks : single-threaded output then depends
on stableSrc (last partial block referencing history in place), so memory-mapped input would differ.
lz4frame : without LZ4F_MULTITHREAD, LZ4F_compressBound() covers a single job slot
(jobs processed serially) : 4 MB instead of 68 MB with linked blocks and nbWorkers 8.
tests : frameMT validateCompressBound ; test-lz4-multithread : single-job files and -m.
2026-10-22 12:58:19 -07:00
Yann Collet
88ed0b630e api: LZ4F_getMaxNbWorkers() reports multi-threading support ; CLI thread cap follows liblz4, lz4 -V reports multithreading (checkpoint)
LZ4F_getMaxNbWorkers() (static linking only, experimental like nbWorkers) :
0 when liblz4 is built without LZ4F_MULTITHREAD, otherwise the cap of nbWorkers,
now 1024 on 64-bit systems and 128 on 32-bit ones (was 200).
lz4 : build macro LZ4_NBWORKERS_MAX removed ; worker threads of frame and legacy
formats are capped by LZ4F_getMaxNbWorkers(), 1 without liblz4 multi-threading
(restores a cap for the legacy format on POSIX). lz4 -V reports "multithread"
only when both lz4 and liblz4 support it ; -T# warning and -v note accordingly.
VS2022 _test.bat checks the version line, catching a CLI built without LZ4F_MULTITHREAD
(same output, so no other observable difference).
frameMT : validateMaxNbWorkers ; test builds use LZ4F_NBWORKERS_MAX=128.
2026-10-22 12:58:19 -07:00
Yann Collet
d4a0782b47 review round 10: failed LZ4F_compressBegin() invalidates on allocation only, deferred emission of completed jobs, VS2022 x64 CLI multithreaded (checkpoint)
LZ4F_compressBegin*() validates its arguments first (dictSize included) :
rejecting them leaves a frame in progress untouched, jobs in flight included
(contract of #1817, tested by frametest). Past validation, the frame in progress
is abandoned before cctx is modified : an allocation failure invalidates it
(previously, cStage remained 1 with an inconsistent cctx, also with nbWorkers == 0).
lz4frame.h : LZ4F_srcStableUntilFlush lifetime and LZ4F_compressBegin() failures clarified.
Without autoFlush, LZ4F_compressUpdate() defers completed jobs not fitting into dst,
instead of failing : it only fails when output must be emitted (all job slots busy).
VS2022 : lz4.vcxproj x64 configurations define LZ4F_MULTITHREAD=1.
frameMT : validateDeferredEmission, validateBeginFailures.
2026-10-22 12:58:19 -07:00
Yann Collet
c6e5a8cf53 review round 9: destination too small invalidates MT frames (test), --rm keeps a renamed or replaced source, multithreaded CLI tests, MSan step (checkpoint) 2026-10-22 12:58:19 -07:00
Yann Collet
826f9bd6c8 cleanup: legacy helpers without frame leftovers, nbWorkers == 1 comments (checkpoint)
lz4io.c : the legacy compression pipeline no longer carries frame-only code
(ReadTracker.prefix and .xxh32, prefixSize parameter, CompressJobDesc.lastBlock).
xxhash.h remains included, for the content checksum of a memory-mapped source.
lz4frame.c, frameMT.c : nbWorkers == 1 means one worker thread, in addition to
the calling thread. Comments only, behavior unchanged.
2026-10-22 12:58:19 -07:00
Yann Collet
bfd8019c02 memory: CLI memory budget (--memory, LZ4_MEMORY), no fixed nb of workers, single-threaded path without threads, no tmpBuff for MT frames, uncompressed segments as single-threaded (checkpoint)
Prototype, not intended for a PR as is.

- lz4 CLI : the automatic nb of workers (no -T, or -T0) is limited by a memory budget
  (4 GB, 1 GB on 32-bit systems), estimated from liblz4's job slots, input buffers and
  LZ4 states, plus the CLI's buffers (within ~5% of actual allocations when all are used).
  It's also limited to the nb of jobs of each file, when its size is known
  (ex: 3 threads for a 10 MB file, instead of 145 on a 166-core machine).
  -9 on 844 MB, 166 cores : unchanged auto (145 workers, ~3 GB budget estimate).
- --memory=# (suffixes K, M, G) and environment variable LZ4_MEMORY set the budget ;
  an explicitly set budget also limits an explicit -T#, as does the default one on 32-bit
  systems. A budget too small for 1 worker triggers a warning.
- lz4 CLI : no more fixed maximum nb of workers for frames (LZ4_NBWORKERS_MAX now only
  bounds the legacy format's thread pool) ; liblz4 keeps a sanity limit, raised to 1024
  (200 on 32-bit systems, where it keeps LZ4F_compressBound() within address space),
  overridable at compile time (tests : 200, keeping the capped test case affordable).
- liblz4 without LZ4F_MULTITHREAD (ex: default static library) : independent blocks take
  the single-threaded path (same output, no job buffers, no extra copies) ; linked blocks
  keep jobs processed serially (same output as multi-threaded, ~3% slower at level 1).
- liblz4 : frames cut into jobs no longer allocate tmpBuff (-4.1 MB, LZ4F_cctx_size()).
- liblz4 : LZ4F_uncompressedUpdate() with workers now cuts blocks as single-threaded mode :
  an incomplete last block waits for next uncompressed input (it was written immediately,
  producing different blocks for consecutive uncompressed invocations). With independent
  blocks, output is now identical to nbWorkers == 0 in all cases, and doesn't depend on
  multi-threading support. tests/frameMT.c compares uncompressed segments with
  single-threaded mode too. Full digest reference : 899dd7e3d21cad9a.
- Documented in man (-T#, --memory, LZ4_MEMORY, LZ4_NBWORKERS), programs/README.md, lz4frame.h.

Validated : full tests MT / no-MT (same digest), ASAN+UBSAN, TSAN, make test, wine,
CLI == dev CLI, lz4-nomt == lz4, memory budget scenarios, modification scenarios,
build policies, ~88k libFuzzer executions.
2026-10-22 04:26:50 -07:00
Yann Collet
b54cc29c78 MT robustness: source modified during compression detected, pool retried, queued jobs cancelled, longer mmap release lag (checkpoint)
Prototype, not intended for a PR as is.

- lz4 CLI : a source file modified during its compression is detected, comparing its size
  and times (mtime, ctime) before and after, like tar ("file changed as we read it").
  The source is kept, even with --rm.
  - memory-mapped input is read several times (checksum, compression, history of next job) :
    the frame can be inconsistent : error 54, output removed.
  - otherwise (--no-mmap), input is read once : the frame holds the content read,
    ex: up to the end of a log file growing meanwhile : warning only.
  Documented in man (--[no-]mmap, --rm), -H help and lz4io.h.
- lz4frame.c : a missing or incomplete worker pool (ex: transient allocation failure) is
  created again at next frame, instead of staying serial for the context's lifetime.
- lz4frame.c : abandoning a frame (error, LZ4F_compressBegin(), LZ4F_freeCompressionContext())
  cancels jobs not started yet, instead of compressing them (and the calling thread helping) :
  only running jobs are waited for. Level 12, 4 MB jobs : CPU spent abandoning a frame
  1.58 s -> 0.80 s (2 workers), 4.03 s -> 3.50 s (8 workers).
- lz4 CLI : memory-mapped input released behind (2*nbWorkers+1) jobs of 4 MB + 64 KB of history,
  instead of nbWorkers+2 chunks (fewer refaults : ~5-8% fewer minor faults at -9 -BD).
- tests/frameMT.c : validatePoolRetry (each absorbed allocation failure, then next frame
  creates worker threads again ; fails with the previous lib).

Validated : full tests MT / no-MT, ASAN+UBSAN, TSAN, make test, wine, CLI == dev CLI,
in-place rewrite / growing log / truncation scenarios, ~88k libFuzzer executions.
2026-10-22 04:26:50 -07:00
Yann Collet
d9c9fe23a2 lz4frame: explicit reserved fields ; LZ4F_srcStableUntilFlush includes LZ4F_srcStable (checkpoint)
Prototype, not intended for a PR as is.

- LZ4F_preferences_t : reserved array replaced by explicit scalar fields
  (nbWorkers, reserved1, reserved0), and LZ4F_compressOptions_t likewise
  (stableSrc, reserved2, reserved1, reserved0), as LZ4F_decompressOptions_t.
  Same sizes and offsets (ABI unchanged). A future field takes the place of a
  reserved one, without changing positional initializers.
  One-time source change : the former positional form { ..., { 0, 0, 0 } }
  no longer compiles in C++ (warnings in C) ; examples/frameCompress.c updated.
  LZ4F_INIT_PREFERENCES : { LZ4F_INIT_FRAMEINFO, 0, 0u, 0u, 0u, 0u, 0u }.
- LZ4F_srcStableUntilFlush (2) is stronger than LZ4F_srcStable (1) : in addition
  to the last 64 KB of input (which remain, including after LZ4F_flush()),
  all of src remains until LZ4F_flush() / LZ4F_compressEnd() returns
  (with autoFlush : until the invocation returns). Docs only : the code already
  relied on this (history read in place after a flush, single-threaded path).
- tests/frameMT.c : validateFlushContract : one buffer, 2 invocations separated by
  a flush, everything but the last 64 KB scrambled after the flush with content
  resembling upcoming data ; nbWorkers 0 and 2, levels 1 and 9, with and without
  autoFlush. Scrambling the last 64 KB too is detected (contentChecksum_invalid).
  Streaming test : LZ4F_srcStableUntilFlush slices come from a working copy,
  scrambled the same way after each flush.

Output unchanged (same digests). Validated : full tests MT / no-MT, ASAN+UBSAN, TSAN,
make test, wine, examples (C, C++), cxxtest, CLI == dev CLI.
2026-10-22 04:26:50 -07:00
Yann Collet
c283199226 lz4frame: LZ4F_stableSrc_t, MT jobs read input in place only with LZ4F_srcStableUntilFlush (checkpoint)
Prototype, not intended for a PR as is.

Historically, stableSrc only guarantees that the last 64 KB of input remain
present (linked blocks reference them instead of copying them). The MT engine
was reading whole jobs in place after LZ4F_compressUpdate() returns, silently
extending this contract : code correct with nbWorkers == 0 (buffer reused with
independent blocks, alternating buffers with linked blocks) became racy once
nbWorkers is enabled.

- lz4frame.h : enum LZ4F_stableSrc_t, the field remains unsigned (C++ compatibility) :
  - LZ4F_srcUnstable (0) : default ;
  - LZ4F_srcStable (1) : historical meaning, last 64 KB of input ;
  - LZ4F_srcStableUntilFlush (2) : all of src remains until LZ4F_flush() / LZ4F_compressEnd()
    returns, or the frame is abandoned : MT jobs read it in place.
- single-threaded behavior is unchanged : any non-zero value means stable.
- MT : only exactly LZ4F_srcStableUntilFlush enables in-place reading after return ;
  otherwise job content is copied. autoFlush still reads in place (jobs complete within the call).
- one-shot compression and the CLI (memory-mapped input) employ LZ4F_srcStableUntilFlush.
- tests/frameMT.c : the three values ; with LZ4F_srcStable and workers, input is scrambled
  after each invocation (a mutant reading it in place is detected, also by TSan).
- compress_frame_stream_fuzzer : stableSrc in 0..2.

Output unchanged (same digests), CLI mmap speed unchanged.
Validated : full tests MT / no-MT, ASAN+UBSAN, TSAN, make test, wine, CLI == dev CLI,
~88k libFuzzer executions.
2026-10-22 04:26:50 -07:00
Yann Collet
ebbdb4e807 MT hardening : allocation failure injection, lz4file test, fuzzers with nbWorkers, CI sanitizer steps (checkpoint)
Prototype, not intended for a PR as is.

- FORWARD_IF_ERROR() used to evaluate its argument twice ; applied to LZ4F_mt_begin(),
  an allocation failure re-ran it, and LZ4F_compressBegin() reported success without
  starting the frame. Found by allocation failure injection.
  Fixed in dev by #1877 (single evaluation) ; FORWARD_IF_ERROR_MT() now follows the same pattern.
- tests/frameMT.c :
  - each allocation fails in turn (one-shot, streaming copy / stableSrc, levels 1 and 9) :
    clean allocation_failed or correct output, no leak, context remains usable ;
  - LZ4F_write() / LZ4F_read() with workers : same frame as one-shot ;
  - quick mode (-q), step labels, FRAMEMT_DUMP diagnostics ;
  - CDict now created after its dictionary is filled (found by ASAN).
- make test runs test-frameMT-quick ; CI sanitizers : ASan and TSan steps.
- ossfuzz : compress_frame_stream_fuzzer employs different nbWorkers for reference and test,
  comparing whole frames when output lags behind input ; round_trip_frame_fuzzer : random nbWorkers.
- LZ4F_MT_JOBSIZE_* can be overridden (testing only), to cross job boundaries with small inputs.
- lz4frame.h : per-invocation output depends on scheduling (without autoFlush).

Validated : full tests MT / no-MT (same digest), ASAN+UBSAN, TSAN, make test,
mingw-w64 build under wine (same digest as Linux), ~160k libFuzzer executions.
2026-10-22 04:26:50 -07:00
Yann Collet
0852fddadc lz4frame: streaming multi-threaded compression, persistent pool, CLI uses it (checkpoint)
Prototype, not intended for a PR as is.

- persistent thread pool within LZ4F_cctx (pthread / Win32), replaces the per-call fork-join;
  nbWorkers is now the nb of worker threads (the calling thread also runs jobs while waiting).
- streaming : LZ4F_compressUpdate() / LZ4F_flush() / LZ4F_compressEnd() cut input into jobs
  (4 MB linked, 1 MB independent), at fixed positions of the content; outputs emitted in order;
  input read in place with stableSrc or autoFlush, copied otherwise; autoFlush respected;
  LZ4F_compressBound() covers jobs in flight; LZ4F_uncompressedUpdate() drains pending jobs.
- one-shot compression goes through the same engine.
- CLI : frame compression goes through LZ4F streaming with nbWorkers (output identical to
  previous CLI, and lz4 == lz4-nomt); custom block sizes fall back to single-thread.
- build systems compile the CLI's liblz4 sources with LZ4F_MULTITHREAD.
- tests/frameMT.c : streaming validation (slicing, stableSrc, flushes, autoFlush,
  uncompressed segments, exact bounds, abandoned frames).
2026-10-22 04:26:50 -07:00
Yann Collet
02916432ee lz4frame: experimental multi-threaded one-shot compression (checkpoint)
Prototype, not intended for a PR as is.

- LZ4F_preferences_t.nbWorkers (replaces reserved[0], ABI preserved):
  LZ4F_compressFrame() and LZ4F_compressFrame_usingCDict() cut input into jobs,
  compressed by up to nbWorkers threads (inline fork-join, threads created per call).
  Output only depends on parameters, never on nbWorkers (>= 1) nor scheduling;
  identical to single-threaded with independent blocks, identical to the CLI -T output.
- LZ4F_MULTITHREAD build macro (default 0) : without it, jobs run serially, same output.
- build policy (zstd-like) : dynamic library multi-threaded by default, static library not
  (make lib-mt / lib-nomt / MULTITHREAD=, cmake LZ4_SHARED/STATIC_MULTITHREAD,
  meson shared-/static-multithread, VS2022 liblz4-dll).
- internal LZ4F_compressBlocks_internal() extracted from LZ4F_compressUpdateImpl().
- tests/frameMT.c (make -C tests test-frameMT) : validation + bench mode.

Known gaps : Win32 thread path never compiled; no persistent thread pool; no streaming.
2026-10-22 04:26:50 -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
ab5f917365 lz4frame: share state initialization, LZ4F_makeBlock() takes the context
LZ4F_compressBegin_internal() initialized the fast or HC state in 2
places, after an allocation and when changing its type : they now share
LZ4F_initLZ4State(). This also fixes a crash : when the allocation of a
larger state failed, the previous state was freed but still recorded as
allocated, and a following LZ4F_compressBegin() for its type used a NULL
state. frametest checks this case, with a failing custom allocator.

LZ4F_makeBlock() takes the compression context, instead of the same
5 of its fields at each of its 4 calls.
2026-10-21 14:22:18 -07:00
Yann Collet
f11eeb613c cli: legacy format honors --rm and keeps file attributes
Single and multi-threaded frame compression duplicated the opening of
files, the preparation of preferences, and the end of file processing :
closing files, copying owner, permissions and modification time, --rm,
final status. Legacy compression had none of this end : `lz4 -l` ignored
`--rm`, and the compressed file didn't keep the source's attributes.

The end of file processing is now LZ4IO_finishCompressedFile(), shared by
frame and legacy compression, and LZ4IO_compressFilename_extRess() opens
files and prepares preferences for both frame compressors, which only
compress.

Also, LZ4IO_compressFilename_extRess() now reports a processed size of 0
when a file can't be opened, instead of leaving it uninitialized : with
-m, a missing file added a stale or garbage size to the final speed
summary (found by scan-build).
2026-10-21 10:30:59 -07:00
Yann Collet
dcd73e8e4d Merge pull request #1915 from akashdeshmukh-git/cli-reject-malformed-long-args
cli: reject malformed --fast= and --threads arguments
2026-10-20 15:49:17 -07:00
akashdeshmukh-git
cebeba6e3e cli: reject malformed --fast= and --threads arguments
`--fast=5x` was accepted as `--fast=5`: characters following the
number were silently ignored, unlike `--threads=5x` which is rejected.

`--threadsX 2` was accepted as `--threads 2`, since the long command
was matched as a prefix only.

Both are now rejected with the usage message.
2026-10-20 08:32:54 +00:00
akashdeshmukh-git
26f121584a lz4io: detect truncated skippable frames in regular files
fseek() succeeds beyond end of file, so a skippable frame whose
declared size exceeds the remaining input was silently accepted
when reading from a file (`lz4 -t`, `lz4 -d` and `lz4 --list` all
reported success), while the same input from a pipe correctly failed
with "Error 43 : cannot skip skippable area".

fseek_u32() now seeks to the last byte of the area and reads it,
so truncation is detected the same way for files and pipes.
2026-10-20 08:32:54 +00:00
Yann Collet
6f6e186b3a headers: include dependencies outside of extern "C"
C++ forbids including a header within a linkage specification.
With clang C++ modules (-fmodules), including lz4.h or lz4hc.h failed :
<stdint.h> was imported within extern "C".

All public headers (lz4.h, lz4hc.h, lz4frame.h, lz4file.h) now include
their dependencies before opening extern "C".

New test : make -C tests test-cxx-modules (requires clang),
run in CI with clang on Linux, and on macOS.

Fixes #1600. Based on #1815 by @sergioperezcheco.
2026-10-19 21:34:16 -07:00
Yann Collet
ac2eba0f1a cli: accept the Linux kernel's legacy end marker
The kernel's legacy decoder also stops at a zero Block Compressed Size,
and initramfs builders (mkinitcpio) append these four zero bytes
to delimit concatenated archives.
lz4 passed it to LZ4_decompress_safe() as an empty block,
and failed with "Error 64 : Decoding Failed".

A zero block size now ends the legacy frame,
in both legacy decoders and in --list,
and decoding continues with the next frame, if any.
It was never a valid block : a compressed block is at least 1 byte.

The format spec (v1.6.5) documents it as an extension :
encoders should not produce it, since older decoders reject it.

Fixes #1889, based on #1890 by @agamal026
2026-10-19 19:05:22 -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
70860f1790 cli: decode Firefox's mozlz4 files
Firefox stores its .jsonlz4 / .mozlz4 files (session, bookmark backups,
search engines) as "mozLz40\0", the decompressed size (LE32),
then a single LZ4 block extending to the end of the file
(see MozLZ4 in Firefox's xpcom/ioutils/IOUtils.cpp).

`lz4 -d`, `lz4 -t` and `lz4cat` now recognize this format,
and decode it like Firefox's reader :
- the decompressed size is only used as decoding capacity
- empty content has no block (Firefox doesn't write one)

Allocations are bounded : sizes > LZ4_MAX_INPUT_SIZE are rejected,
as they can't be compressed into a single block,
and input is read up to LZ4_COMPRESSBOUND(size), the largest valid block.

Data starting with the first half of the magic only ("mozL") keeps
the previous behavior : `lz4cat` passes it through unchanged,
including the bytes read to check the second half.

Decoding only : lz4 doesn't produce this non-standard format.

Based on #1259 by @UnitedMarsupials, rewritten for current dev
(no mmap, bounded allocations, full magic check, tests).
2026-10-19 10:49:30 -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
779b355680 Merge pull request #1892 from arshsmith1/reject-negative-dictsize
lz4: reject negative dictSize in dictionary entry points
2026-10-18 12:22:34 -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
Yann Collet
ccfc64e7b9 legacy format: same level to acceleration mapping as the frame format
The legacy compressor used acceleration = -cLevel,
while lz4frame and bench use -cLevel + 1.
Legacy levels were shifted by one: `lz4 -l --fast=1` produced the same
output as `lz4 -l -1`, and `-l --fast=2` matched the frame `--fast=1`.

Use the same mapping, capped at LZ4_ACCELERATION_MAX
(avoids a signed overflow with --fast=2147483647).
Levels >= 0 are unchanged.

test-lz4-basic.sh: `-l --fast=1` must compress less than `-l -1`
(fails on dev), and `-l --fast=2147483647` runs.
2026-10-18 11:20:02 -07:00
Yann Collet
97c68c2cd9 cap acceleration at LZ4_ACCELERATION_MAX, now defined in lz4.h
Follow-up to "lz4frame: avoid signed overflow", per the #1824 review:
saturate at LZ4_ACCELERATION_MAX rather than INT_MAX,
since higher values make no difference.

LZ4_ACCELERATION_DEFAULT and LZ4_ACCELERATION_MAX move from lz4.c
to the LZ4_STATIC_LINKING_ONLY section of lz4.h, which lz4.c, lz4frame.c,
bench.c and frametest.c already include, so their values derive from it.
The LZ4_compress_fast() documentation already referred to both constants.

bench.c computed the acceleration the same way (-cLevel + 1),
so `lz4 -b --fast=2147483647` hit the same signed overflow:
apply the same cap there.

test-lz4-basic.sh: --fast=2147483647 must produce the same frame
as --fast=65536 (fails on dev, where acceleration wraps to the default),
and its benchmark runs (caught by the UBSan CI job before this fix).

lz4.1: --fast values above 65536 make no difference.
2026-10-18 11:20:02 -07:00
Akash Deshmukh
e77252c9db lz4frame: avoid signed overflow for very negative compression levels
LZ4F_compressBlock() and LZ4F_compressBlock_continue() compute the
acceleration as -level + 1. For compressionLevel == INT_MIN + 1 this
overflows (INT_MAX + 1), and for INT_MIN the negation itself overflows.
Both are undefined behavior (UBSan reports them, e.g. with
`lz4 --fast=2147483647`). In practice the result wraps to a negative
acceleration, which LZ4_compress_fast*() replaces with the default (1),
so the fastest requested levels silently compress at default speed.

Compute the acceleration in a helper that saturates instead of
overflowing. LZ4_compress_fast*() already caps acceleration at
LZ4_ACCELERATION_MAX, so the result is unchanged for every other level.

Add a frametest check that levels -65537, INT_MIN + 1 and INT_MIN
produce the same frame as level -65536.
2026-10-18 11:19:49 -07:00
Akash Deshmukh
3796322233 cli: reject numeric arguments that overflow
readU32FromChar() silently wrapped around on values larger than 32 bits,
and compression levels were then cast to int without a range check.
As a result, out-of-range arguments did something unrelated instead of
failing, for example:

  lz4 --fast=3000000000   compressed at maximum (HC) level
  lz4 -3000000000         used the fastest level (output larger than input)
  lz4 -4294967305         used level 9
  lz4 -B4294967300        used 64 KB blocks
  lz4 -B4194305K          used 1 KB blocks

readU32FromChar() now exits with an error when the value (including
K/M suffixes) overflows 32 bits, and compression level arguments
(-#, -e#, --fast=#) are rejected when larger than INT_MAX.
LZ4_CLEVEL and LZ4_NBWORKERS values that overflow are ignored with the
existing warning, like other invalid values.

Also fix the --fast=0 and --fast=-1 tests in test-lz4-basic.sh: they
used a misspelled file name (dg20K instead of dg20k), so lz4 failed
because the file was missing rather than because of the option.
2026-10-18 11:19:49 -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
74c02718d4 Merge pull request #1885 from lz4/decompress_nearEnd_copy
lz4: LZ4_decompress_generic() safe loop
2026-10-17 18:48:45 -07:00
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
7577ff1393 test --list on a truncated frame, add NEWS line
Removing the 4-byte content checksum from a valid frame must make
`lz4 --list` fail. Before the previous commit, the checksum was skipped
with fseek(), which succeeds past the end of file, so --list reported
the truncated frame as valid (exit 0) while `lz4 -t` rejected it.

The CLI is not covered by the fuzzers; this test runs in milliseconds.
2026-10-17 16:23:15 -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
7bf22d3373 tests: fix memory leak in decompress-partial-usingDict
main() allocated `buffer` and `dict` with malloc() and never freed them,
neither on success nor on any of the five error paths.
LeakSanitizer reports 133118 bytes leaked in 2 allocations.

Route all exits through a single `_end` label that frees both buffers.

Checked with gcc -fsanitize=address,undefined: no leak reported, on the
success path and on a forced error path.
2026-10-17 12:18:53 -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
arshiya tabasum
28cdacfb85 read full frame header for dictID frames in --list 2026-10-17 07:58:27 -07:00
Yann Collet
aab0413f98 Merge pull request #1868 from lz4/lz4file_truncated
lz4file: LZ4F_read() reports files ending within a frame
2026-10-17 07:14:45 -07:00