From e0849499cb2cb91ca4b601d6d45b7daddeecde6d Mon Sep 17 00:00:00 2001 From: m00dy Date: Wed, 28 Oct 2026 19:23:39 -0500 Subject: [PATCH] dictBuilder: validate buffer capacities in ZDICT_addEntropyTablesFromBuffer_advanced ### Summary This is defensive hardening of the deprecated `ZDICT_addEntropyTablesFromBuffer()` entry point and its private helper. The normal legacy trainer already bounds dictionary content to destination capacity. `ZDICT_addEntropyTablesFromBuffer_advanced()` in `lib/dictBuilder/zdict.c` (reachable via `ZDICT_addEntropyTablesFromBuffer()`) reserves an 8-byte dictionary header (`hSize = 8`) and computes: - `(char*)dictBuffer + hSize` with remaining capacity `dictBufferCapacity - hSize`, and - `(char*)dictBuffer + dictBufferCapacity - dictContentSize` for the pre-populated dictionary content at the end of `dictBuffer`. Before this change, `ZDICT_addEntropyTablesFromBuffer_advanced()` did not check these destination preconditions before performing unsigned subtraction and pointer arithmetic: 1. When `dictBufferCapacity < hSize` (for example `0` through `7`), `dictBufferCapacity - hSize` wraps unsigned `size_t` to `SIZE_MAX - 7 .. SIZE_MAX` and `(char*)dictBuffer + hSize` points past the destination allocation, allowing `ZDICT_analyzeEntropy()` to write Huffman and FSE tables out of bounds on the heap. 2. When `dictContentSize > dictBufferCapacity`, the left-associative expression `((char*)dictBuffer + dictBufferCapacity) - dictContentSize` attempts to move the pointer before `dictBuffer`, which is undefined behavior. 3. When `dictBuffer == NULL`, `(char*)dictBuffer + hSize` performs non-zero offset arithmetic on a null pointer before writing header fields. ### Fix Add an upfront precondition check in `ZDICT_addEntropyTablesFromBuffer_advanced()`, consistent with the existing capacity checks in `ZDICT_finalizeDictionary()`: - Return `ERROR(dstSize_tooSmall)` when `dictBuffer == NULL`, `dictBufferCapacity < dictContentSize`, or `dictBufferCapacity < hSize`. - Preserve existing truncation behavior when `dictBufferCapacity >= hSize` and `dictContentSize <= dictBufferCapacity` (including `ZDICT_trainFromBuffer_unsafe_legacy()` calls where `dictContentSize == dictBufferCapacity`). ### Testing - Added unit regression coverage in `tests/fuzzer.c` (`basicUnitTests`) verifying: - `ZSTD_error_dstSize_tooSmall` rejection for `dictBufferCapacity < 8` (`0` and `7`), `dictContentSize > dictBufferCapacity` (`17 > 16`), and `dictBuffer == NULL`; - successful dictionary header and entropy-table generation on a valid pre-populated buffer (`ZDICT_getDictID() != 0`). - Verified the full `basicUnitTests` suite in `tests/fuzzer.c` under AddressSanitizer and UndefinedBehaviorSanitizer as well as an optimized release build, plus strict C90 and C++98 compilation of `lib/dictBuilder/zdict.c`. Disclosure: Automated static analysis and AI coding assistance were used to draft and verify this defensive bounds check and regression test. --- lib/dictBuilder/zdict.c | 4 +++ tests/fuzzer.c | 67 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) diff --git a/lib/dictBuilder/zdict.c b/lib/dictBuilder/zdict.c index befa616b87f..3640995a92c 100644 --- a/lib/dictBuilder/zdict.c +++ b/lib/dictBuilder/zdict.c @@ -950,6 +950,10 @@ static size_t ZDICT_addEntropyTablesFromBuffer_advanced( U32 const notificationLevel = params.notificationLevel; size_t hSize = 8; + /* check conditions */ + if ((dictBuffer == NULL) || (dictBufferCapacity < dictContentSize) || (dictBufferCapacity < hSize)) + return ERROR(dstSize_tooSmall); + /* calculate entropy tables */ DISPLAYLEVEL(2, "\r%70s\r", ""); /* clean display line */ DISPLAYLEVEL(2, "statistics ... \n"); diff --git a/tests/fuzzer.c b/tests/fuzzer.c index 32360b9a58d..b6aa56f2d6f 100644 --- a/tests/fuzzer.c +++ b/tests/fuzzer.c @@ -34,6 +34,7 @@ #define ZSTD_DISABLE_DEPRECATE_WARNINGS /* No deprecation warnings, we still test some deprecated functions */ #include "zstd.h" /* ZSTD_VERSION_STRING */ #include "zstd_errors.h" /* ZSTD_getErrorCode */ +#define ZDICT_DISABLE_DEPRECATE_WARNINGS /* No deprecation warnings, we still test deprecated functions */ #define ZDICT_STATIC_LINKING_ONLY #include "zdict.h" /* ZDICT_trainFromBuffer */ #include "mem.h" @@ -3136,6 +3137,72 @@ static int basicUnitTests(U32 const seed, double compressibility) } DISPLAYLEVEL(3, "OK : %u \n", (unsigned)dictHeaderSize); + DISPLAYLEVEL(3, "test%3i : ZDICT_addEntropyTablesFromBuffer capacity bounds : ", testNb++); + { size_t const validCapacity = 1024; + size_t const validContentSize = 128; + BYTE* const smallBuf = (BYTE*)malloc(7); + BYTE* const contentOverflowBuf = (BYTE*)malloc(16); + BYTE* const validBuf = (BYTE*)malloc(validCapacity); + size_t res; + if (smallBuf == NULL || contentOverflowBuf == NULL || validBuf == NULL) { + free(smallBuf); + free(contentOverflowBuf); + free(validBuf); + goto _output_error; + } + memset(smallBuf, 0, 7); + memset(contentOverflowBuf, 0, 16); + { U32 u; for (u=0; u validCapacity || ZDICT_getDictID(validBuf, res) == 0) { + free(smallBuf); + free(contentOverflowBuf); + free(validBuf); + goto _output_error; + } + assert(ZDICT_getDictID(validBuf, res) != 0); + + free(smallBuf); + free(contentOverflowBuf); + free(validBuf); + } + DISPLAYLEVEL(3, "OK \n"); + DISPLAYLEVEL(3, "test%3i : compress with dictionary : ", testNb++); cSize = ZSTD_compress_usingDict(cctx, compressedBuffer, compressedBufferSize, CNBuffer, CNBuffSize,