diff --git a/.github/workflows/all-workflows.yml b/.github/workflows/all-workflows.yml new file mode 100644 index 00000000000..e708c05566d --- /dev/null +++ b/.github/workflows/all-workflows.yml @@ -0,0 +1,48 @@ +name: all-workflows +# Waits for the other workflows run on the head commit of the pull request, +# and fails if any of them did not succeed. It finds them by itself: there is +# no list to maintain. It cannot see a workflow that does not run at all. + +concurrency: + group: all-workflows-${{ github.ref }} + cancel-in-progress: true + +on: + pull_request: + branches: [ dev ] + +permissions: + actions: read + +jobs: + check: + name: "all workflows passed" + runs-on: ubuntu-latest + timeout-minutes: 180 + steps: + - name: Wait for the other workflows + env: + GH_TOKEN: ${{ github.token }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + run: | + # Latest run of each workflow on the head commit, except this workflow + query="[.workflow_runs[] | select(.name != \"$GITHUB_WORKFLOW\")] + | group_by(.workflow_id) | map(max_by(.run_number))[] + | [.name, .status, .conclusion // \"-\"] | @tsv" + # Stop when they are all completed, and unchanged since the previous poll. + # A failed API call is retried at the next poll. + prev="" + while true; do + sleep 30 + runs=$(gh api "repos/$GITHUB_REPOSITORY/actions/runs?head_sha=$HEAD_SHA&per_page=100" --jq "$query") || continue + if [ -n "$runs" ] && [ "$runs" = "$prev" ] && printf '%s\n' "$runs" | awk -F'\t' '$2 != "completed" {n++} END {exit n > 0}'; then + break + fi + prev=$runs + done + printf '%s\n' "$runs" + failed=$(printf '%s\n' "$runs" | awk -F'\t' '$3 != "success" && $3 != "skipped" && $3 != "neutral"') + if [ -n "$failed" ]; then + printf 'Did not succeed:\n%s\n' "$failed" + exit 1 + fi diff --git a/.github/workflows/cmake-tests.yml b/.github/workflows/cmake-tests.yml index 4af2463569c..c8d4b5e5328 100644 --- a/.github/workflows/cmake-tests.yml +++ b/.github/workflows/cmake-tests.yml @@ -36,6 +36,21 @@ jobs: run: | cmake --build cmake-build --config Release + # Build without the tests, so C++ is not enabled, with CMAKE_CXX_COMPILER set as cross toolchain files do. + # The x86-64 assembly is only built when the compilation flags are applied. + cmake-c-only: + name: "CMake C only (No ZSTD_BUILD_TESTS)" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # tag=v7.0.1 + - name: Configure and build + run: | + cmake -S build/cmake -B cmake-build -DCMAKE_BUILD_TYPE=Release -DCMAKE_COMPILE_WARNING_AS_ERROR=ON -DCMAKE_CXX_COMPILER=g++ + cmake --build cmake-build --parallel + - name: Check that the x86-64 assembly is built + run: | + nm cmake-build/lib/libzstd.a | grep -q HUF_decompress4X1_usingDTable_internal_fast_asm_loop + # Ubuntu-based cmake build using make wrapper # This test uses the make-driven cmake build to ensure compatibility # with the existing build system integration @@ -125,6 +140,12 @@ jobs: name: "Clang-CL" runner: "windows-2022" cmake_extra_flags: "-DCMAKE_COMPILE_WARNING_AS_ERROR=ON -DZSTD_BUILD_TESTS=ON" + - generator: "Visual Studio 17 2022" + flags: "-T ClangCL" + name: "Clang-CL (No ZSTD_BUILD_TESTS)" + runner: "windows-2022" + # Intentionally omit ZSTD_BUILD_TESTS: C++ is not enabled, so only the C compiler id is set + cmake_extra_flags: "-DCMAKE_COMPILE_WARNING_AS_ERROR=ON" - generator: "Visual Studio 17 2022" flags: "-T ClangCL -A x64 -DCMAKE_C_FLAGS=/arch:AVX2" name: "Clang-CL AVX2" @@ -154,7 +175,7 @@ jobs: # Uses native ARM64 hardware for optimal performance and compatibility testing cmake-macos-arm64: name: "CMake macOS ARM64 (Apple Silicon)" - runs-on: macos-14 # ARM64 runner + runs-on: macos-latest # ARM64 runner steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # tag=v7.0.1 - name: "CMake build and test (ARM64)" diff --git a/.github/workflows/commit.yml b/.github/workflows/commit.yml index 9825a16b7b9..6b991c50565 100644 --- a/.github/workflows/commit.yml +++ b/.github/workflows/commit.yml @@ -44,7 +44,7 @@ jobs: - name: Install Dependencies run: | sudo apt-get update - sudo apt-get install gcc-powerpc-linux-gnu gcc-arm-linux-gnueabi gcc-aarch64-linux-gnu libc6-dev-ppc64-powerpc-cross libcurl4-gnutls-dev lib64gcc-13-dev-powerpc-cross + sudo apt-get install gcc-powerpc-linux-gnu gcc-arm-linux-gnueabi gcc-aarch64-linux-gnu libc6-dev-ppc64-powerpc-cross libcurl4-gnutls-dev gcc-multilib-powerpc-linux-gnu - name: gnu90 build run: make gnu90build && make clean - name: gnu99 build @@ -99,6 +99,6 @@ jobs: echo " are still good, copy it into the repo and commit it." echo "> diff tests/regression/results.csv $CIRCLE_ARTIFACTS/results.csv" diff tests/regression/results.csv $CIRCLE_ARTIFACTS/results.csv - - uses: actions/upload-artifact@v7.0.1 + - uses: actions/upload-artifact@v7.0.2 with: path: "/tmp/circleci-artifacts" diff --git a/.github/workflows/dev-long-tests.yml b/.github/workflows/dev-long-tests.yml index dcdb5674c00..550a9c2939a 100644 --- a/.github/workflows/dev-long-tests.yml +++ b/.github/workflows/dev-long-tests.yml @@ -115,9 +115,11 @@ jobs: - name: gcc-8 + ASan + UBSan + Test Zstd # See https://askubuntu.com/a/1428822 run: | + # the ASan runtime of gcc-8 loops on DEADLYSIGNAL with the 32-bit mmap ASLR of recent kernels + sudo sysctl -w vm.mmap_rnd_bits=28 echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu focal main universe" | sudo tee -a /etc/apt/sources.list sudo apt-get -qqq update - make gcc8install + sudo apt-get -yq --no-install-suggests --no-install-recommends install libc6-dev-i386 gcc-multilib gcc-8 gcc-8-multilib CC=gcc-8 make -j uasan-test-zstd /dev/null && + $XCC --version && $XEMU --version && + LDFLAGS="-static" CC=$XCC QEMU_SYS=$XEMU make clean check' - name: RISC-V if: ${{ matrix.name == 'RISC-V' }} run: | LDFLAGS="-static" CC=$XCC QEMU_SYS=$XEMU make clean check - CFLAGS="-march=rv64gcv -O3" LDFLAGS="-static -DMEM_FORCE_MEMORY_ACCESS=0" CC=$XCC QEMU_SYS="$XEMU -cpu rv64,v=true,vlen=128" make clean check - CFLAGS="-march=rv64gcv -O3" LDFLAGS="-static -DMEM_FORCE_MEMORY_ACCESS=0" CC=$XCC QEMU_SYS="$XEMU -cpu rv64,v=true,vlen=256" make clean check - CFLAGS="-march=rv64gcv -O3" LDFLAGS="-static -DMEM_FORCE_MEMORY_ACCESS=0" CC=$XCC QEMU_SYS="$XEMU -cpu rv64,v=true,vlen=512" make clean check + CFLAGS="-march=rv64gcv -O3" LDFLAGS="-static -DMEM_FORCE_MEMORY_ACCESS=0" CC=$XCC QEMU_SYS="$XEMU -cpu max,vlen=128" make clean check + CFLAGS="-march=rv64gcv -O3" LDFLAGS="-static -DMEM_FORCE_MEMORY_ACCESS=0" CC=$XCC QEMU_SYS="$XEMU -cpu max,vlen=256" make clean check + CFLAGS="-march=rv64gcv -O3" LDFLAGS="-static -DMEM_FORCE_MEMORY_ACCESS=0" CC=$XCC QEMU_SYS="$XEMU -cpu max,vlen=512" make clean check - name: M68K if: ${{ matrix.name == 'M68K' }} run: | diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index 5f99c754c22..afc40d0625f 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -51,7 +51,7 @@ jobs: # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF # format to the repository Actions tab. - name: "Upload artifact" - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # tag=v7.0.1 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # tag=v7.0.2 with: name: SARIF file path: results.sarif diff --git a/.github/workflows/windows-artifacts.yml b/.github/workflows/windows-artifacts.yml index 1b035966968..72b8a0797c6 100644 --- a/.github/workflows/windows-artifacts.yml +++ b/.github/workflows/windows-artifacts.yml @@ -93,7 +93,7 @@ jobs: mv bin/ zstd-${{ github.ref_name }}-${{matrix.ziparch}}/ - name: Publish zstd-$VERSION-${{matrix.ziparch}}.zip for manual inspection - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # tag=v7.0.1 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # tag=v7.0.2 with: compression-level: 9 # maximum compression if-no-files-found: error # defaults to `warn` diff --git a/CHANGELOG b/CHANGELOG index eb46d711872..42599054725 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,10 @@ v1.6.0 (Dec 2025) +fix: on a static DCtx, `ZSTD_DCtx_loadDictionary()` and `ZSTD_DCtx_refPrefix()` now return `ZSTD_error_memory_allocation` for nonempty dictionaries and prefixes instead of allocating and leaking an internal DDict api: legacy format support is now disabled by default +api: experimental `ZSTD_d_refMultipleDDicts` now requires an exact match for nonzero frame dictionary IDs in the registered set; missing nonzero IDs return `ZSTD_error_dictionary_wrong` instead of falling back to the current dictionary. Frames with dictionary ID 0 use a registered ID-0 DDict if present, or no dictionary otherwise. +api: set-based multi-DDict decoding no longer remembers a selected dictionary; legacy frames require single-dictionary or explicit-dictionary decoding +api: disabling experimental `ZSTD_d_refMultipleDDicts` releases every dictionary reference in its set; no selected dictionary is retained for subsequent decompression +api: removed the deprecated experimental functions `ZSTD_copyCCtx()` and `ZSTD_copyDCtx()` build: `ZSTD_LEGACY_SUPPORT` defaults to `0` in Makefile and CMake V1.5.7 (Feb 2025) diff --git a/Makefile b/Makefile index b726f7970ce..44ff3ad57ce 100644 --- a/Makefile +++ b/Makefile @@ -238,19 +238,22 @@ ppcbuild: clean ppc64build: clean CC=powerpc-linux-gnu-gcc CFLAGS="-m64 -Werror" $(MAKE) -j allzstd +# qemu-user names its binaries qemu-; qemu-user-static, before Ubuntu 26.04, names them qemu--static +QEMU_SUFFIX = $(if $(shell command -v qemu-arm 2>/dev/null),,-static) + .PHONY: armfuzz aarch64fuzz ppcfuzz ppc64fuzz armfuzz: clean - CC=arm-linux-gnueabi-gcc QEMU_SYS=qemu-arm-static MOREFLAGS="-static $(MOREFLAGS)" FUZZER_FLAGS="--no-big-tests $(FUZZER_FLAGS)" $(MAKE) -C $(TESTDIR) fuzztest + CC=arm-linux-gnueabi-gcc QEMU_SYS=qemu-arm$(QEMU_SUFFIX) MOREFLAGS="-static $(MOREFLAGS)" FUZZER_FLAGS="--no-big-tests $(FUZZER_FLAGS)" $(MAKE) -C $(TESTDIR) fuzztest aarch64fuzz: clean ld -v - CC=aarch64-linux-gnu-gcc QEMU_SYS=qemu-aarch64-static MOREFLAGS="-static $(MOREFLAGS)" FUZZER_FLAGS="--no-big-tests $(FUZZER_FLAGS)" $(MAKE) -C $(TESTDIR) fuzztest + CC=aarch64-linux-gnu-gcc QEMU_SYS=qemu-aarch64$(QEMU_SUFFIX) MOREFLAGS="-static $(MOREFLAGS)" FUZZER_FLAGS="--no-big-tests $(FUZZER_FLAGS)" $(MAKE) -C $(TESTDIR) fuzztest ppcfuzz: clean - CC=powerpc-linux-gnu-gcc QEMU_SYS=qemu-ppc-static MOREFLAGS="-static $(MOREFLAGS)" FUZZER_FLAGS="--no-big-tests $(FUZZER_FLAGS)" $(MAKE) -C $(TESTDIR) fuzztest + CC=powerpc-linux-gnu-gcc QEMU_SYS=qemu-ppc$(QEMU_SUFFIX) MOREFLAGS="-static $(MOREFLAGS)" FUZZER_FLAGS="--no-big-tests $(FUZZER_FLAGS)" $(MAKE) -C $(TESTDIR) fuzztest ppc64fuzz: clean - CC=powerpc-linux-gnu-gcc QEMU_SYS=qemu-ppc64-static MOREFLAGS="-m64 -static $(MOREFLAGS)" FUZZER_FLAGS="--no-big-tests $(FUZZER_FLAGS)" $(MAKE) -C $(TESTDIR) fuzztest + CC=powerpc-linux-gnu-gcc QEMU_SYS=qemu-ppc64$(QEMU_SUFFIX) MOREFLAGS="-m64 -static $(MOREFLAGS)" FUZZER_FLAGS="--no-big-tests $(FUZZER_FLAGS)" $(MAKE) -C $(TESTDIR) fuzztest .PHONY: cxxtest gcc5test gcc6test armtest aarch64test ppctest ppc64test cxxtest: CXXFLAGS += -Wall -Wextra -Wundef -Wshadow -Wcast-align -Werror @@ -267,26 +270,26 @@ gcc6test: clean armtest: clean $(MAKE) -C $(TESTDIR) datagen # use native, faster - $(MAKE) -C $(TESTDIR) test CC=arm-linux-gnueabi-gcc QEMU_SYS=qemu-arm-static ZSTDRTTEST= MOREFLAGS="-Werror -static $(MOREFLAGS)" FUZZER_FLAGS="--no-big-tests $(FUZZER_FLAGS)" + $(MAKE) -C $(TESTDIR) test CC=arm-linux-gnueabi-gcc QEMU_SYS=qemu-arm$(QEMU_SUFFIX) ZSTDRTTEST= MOREFLAGS="-Werror -static $(MOREFLAGS)" FUZZER_FLAGS="--no-big-tests $(FUZZER_FLAGS)" aarch64test: $(MAKE) -C $(TESTDIR) datagen # use native, faster - $(MAKE) -C $(TESTDIR) test CC=aarch64-linux-gnu-gcc QEMU_SYS=qemu-aarch64-static ZSTDRTTEST= MOREFLAGS="-Werror -static $(MOREFLAGS)" FUZZER_FLAGS="--no-big-tests $(FUZZER_FLAGS)" + $(MAKE) -C $(TESTDIR) test CC=aarch64-linux-gnu-gcc QEMU_SYS=qemu-aarch64$(QEMU_SUFFIX) ZSTDRTTEST= MOREFLAGS="-Werror -static $(MOREFLAGS)" FUZZER_FLAGS="--no-big-tests $(FUZZER_FLAGS)" ppctest: clean $(MAKE) -C $(TESTDIR) datagen # use native, faster - $(MAKE) -C $(TESTDIR) test CC=powerpc-linux-gnu-gcc QEMU_SYS=qemu-ppc-static ZSTDRTTEST= MOREFLAGS="-Werror -Wno-attributes -static $(MOREFLAGS)" FUZZER_FLAGS="--no-big-tests $(FUZZER_FLAGS)" + $(MAKE) -C $(TESTDIR) test CC=powerpc-linux-gnu-gcc QEMU_SYS=qemu-ppc$(QEMU_SUFFIX) ZSTDRTTEST= MOREFLAGS="-Werror -Wno-attributes -static $(MOREFLAGS)" FUZZER_FLAGS="--no-big-tests $(FUZZER_FLAGS)" ppc64test: clean $(MAKE) -C $(TESTDIR) datagen # use native, faster - $(MAKE) -C $(TESTDIR) test CC=powerpc-linux-gnu-gcc QEMU_SYS=qemu-ppc64-static ZSTDRTTEST= MOREFLAGS="-m64 -static $(MOREFLAGS)" FUZZER_FLAGS="--no-big-tests $(FUZZER_FLAGS)" + $(MAKE) -C $(TESTDIR) test CC=powerpc-linux-gnu-gcc QEMU_SYS=qemu-ppc64$(QEMU_SUFFIX) ZSTDRTTEST= MOREFLAGS="-m64 -static $(MOREFLAGS)" FUZZER_FLAGS="--no-big-tests $(FUZZER_FLAGS)" .PHONY: arm-ppc-compilation arm-ppc-compilation: - $(MAKE) -C $(PRGDIR) clean zstd CC=arm-linux-gnueabi-gcc QEMU_SYS=qemu-arm-static ZSTDRTTEST= MOREFLAGS="-Werror -static $(MOREFLAGS)" - $(MAKE) -C $(PRGDIR) clean zstd CC=aarch64-linux-gnu-gcc QEMU_SYS=qemu-aarch64-static ZSTDRTTEST= MOREFLAGS="-Werror -static $(MOREFLAGS)" - $(MAKE) -C $(PRGDIR) clean zstd CC=powerpc-linux-gnu-gcc QEMU_SYS=qemu-ppc-static ZSTDRTTEST= MOREFLAGS="-Werror -Wno-attributes -static $(MOREFLAGS)" - $(MAKE) -C $(PRGDIR) clean zstd CC=powerpc-linux-gnu-gcc QEMU_SYS=qemu-ppc64-static ZSTDRTTEST= MOREFLAGS="-m64 -static $(MOREFLAGS)" + $(MAKE) -C $(PRGDIR) clean zstd CC=arm-linux-gnueabi-gcc QEMU_SYS=qemu-arm$(QEMU_SUFFIX) ZSTDRTTEST= MOREFLAGS="-Werror -static $(MOREFLAGS)" + $(MAKE) -C $(PRGDIR) clean zstd CC=aarch64-linux-gnu-gcc QEMU_SYS=qemu-aarch64$(QEMU_SUFFIX) ZSTDRTTEST= MOREFLAGS="-Werror -static $(MOREFLAGS)" + $(MAKE) -C $(PRGDIR) clean zstd CC=powerpc-linux-gnu-gcc QEMU_SYS=qemu-ppc$(QEMU_SUFFIX) ZSTDRTTEST= MOREFLAGS="-Werror -Wno-attributes -static $(MOREFLAGS)" + $(MAKE) -C $(PRGDIR) clean zstd CC=powerpc-linux-gnu-gcc QEMU_SYS=qemu-ppc64$(QEMU_SUFFIX) ZSTDRTTEST= MOREFLAGS="-m64 -static $(MOREFLAGS)" regressiontest: $(MAKE) -C $(FUZZDIR) regressiontest @@ -322,14 +325,14 @@ asan: clean $(MAKE) test CC=clang MOREFLAGS="-g -fsanitize=address -Werror $(MOREFLAGS)" asan-%: clean - LDFLAGS=-fuse-ld=gold MOREFLAGS="-g -fno-sanitize-recover=all -fsanitize=address -Werror $(MOREFLAGS)" $(MAKE) -C $(TESTDIR) $* + MOREFLAGS="-g -fno-sanitize-recover=all -fsanitize=address -Werror $(MOREFLAGS)" $(MAKE) -C $(TESTDIR) $* msan: clean $(MAKE) test CC=clang MOREFLAGS="-g -fsanitize=memory -fno-omit-frame-pointer -Werror $(MOREFLAGS)" HAVE_LZMA=0 # datagen.c fails this test for no obvious reason msan-%: $(MAKE) clean - LDFLAGS=-fuse-ld=gold MOREFLAGS="-g -fno-sanitize-recover=all -fsanitize=memory -fno-omit-frame-pointer -Werror $(MOREFLAGS)" FUZZER_FLAGS="--no-big-tests $(FUZZER_FLAGS)" $(MAKE) -j -C $(TESTDIR) HAVE_LZMA=0 $* + MOREFLAGS="-g -fno-sanitize-recover=all -fsanitize=memory -fno-omit-frame-pointer -Werror $(MOREFLAGS)" FUZZER_FLAGS="--no-big-tests $(FUZZER_FLAGS)" $(MAKE) -j -C $(TESTDIR) HAVE_LZMA=0 $* asan32: clean $(MAKE) -C $(TESTDIR) test32 CC=clang MOREFLAGS="-g -fsanitize=address $(MOREFLAGS)" @@ -338,10 +341,10 @@ uasan: clean $(MAKE) test CC=clang MOREFLAGS="-g -fno-sanitize-recover=all -fsanitize=address,undefined -Werror $(MOREFLAGS)" uasan-%: clean - LDFLAGS=-fuse-ld=gold MOREFLAGS="-g -fno-sanitize-recover=all -fsanitize=address,undefined -Werror $(MOREFLAGS)" $(MAKE) -C $(TESTDIR) $* + MOREFLAGS="-g -fno-sanitize-recover=all -fsanitize=address,undefined -Werror $(MOREFLAGS)" $(MAKE) -C $(TESTDIR) $* tsan-%: clean - LDFLAGS=-fuse-ld=gold MOREFLAGS="-g -fno-sanitize-recover=all -fsanitize=thread -Werror $(MOREFLAGS)" $(MAKE) -C $(TESTDIR) $* FUZZER_FLAGS="--no-big-tests $(FUZZER_FLAGS)" + MOREFLAGS="-g -fno-sanitize-recover=all -fsanitize=thread -Werror $(MOREFLAGS)" $(MAKE) -C $(TESTDIR) $* FUZZER_FLAGS="--no-big-tests $(FUZZER_FLAGS)" .PHONY: apt-install apt-install: @@ -356,10 +359,10 @@ apt-add-repo: .PHONY: ppcinstall arminstall valgrindinstall libc6install gcc6install gcc7install gcc8install gpp6install clang38install ppcinstall: - APT_PACKAGES="qemu-system-ppc qemu-user-static gcc-powerpc-linux-gnu" $(MAKE) apt-install + APT_PACKAGES="qemu-system-ppc qemu-user gcc-powerpc-linux-gnu" $(MAKE) apt-install arminstall: - APT_PACKAGES="qemu-system-arm qemu-user-static gcc-arm-linux-gnueabi libc6-dev-armel-cross gcc-aarch64-linux-gnu libc6-dev-arm64-cross" $(MAKE) apt-install + APT_PACKAGES="qemu-system-arm qemu-user gcc-arm-linux-gnueabi libc6-dev-armel-cross gcc-aarch64-linux-gnu libc6-dev-arm64-cross" $(MAKE) apt-install valgrindinstall: APT_PACKAGES="valgrind" $(MAKE) apt-install diff --git a/build/cmake/CMakeModules/AddZstdCompilationFlags.cmake b/build/cmake/CMakeModules/AddZstdCompilationFlags.cmake index 3e7bcce5974..68a086908b0 100644 --- a/build/cmake/CMakeModules/AddZstdCompilationFlags.cmake +++ b/build/cmake/CMakeModules/AddZstdCompilationFlags.cmake @@ -23,7 +23,7 @@ function(EnableCompilerFlag _flag _C _CXX _LD) set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${_flag}" PARENT_SCOPE) endif () endif () - if (_CXX AND CMAKE_CXX_COMPILER) + if (_CXX AND CMAKE_CXX_COMPILER_LOADED) CHECK_CXX_COMPILER_FLAG(${_flag} CXX_FLAG_${varname}) if (CXX_FLAG_${varname}) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${_flag}" PARENT_SCOPE) @@ -56,42 +56,42 @@ macro(ADD_ZSTD_COMPILATION_FLAGS _C _CXX _LD) # compiler flags to mark the stack as non-executable. set(ZSTD_HAS_NOEXECSTACK false) - if (CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang" OR MINGW) #Not only UNIX but also WIN32 for MinGW + if (CMAKE_C_COMPILER_ID MATCHES "GNU|Clang" OR CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang" OR MINGW) #Not only UNIX but also WIN32 for MinGW # It's possible to select the exact standard used for compilation. # It's not necessary, but can be employed for specific purposes. # Note that zstd source code is compatible with both C++98 and above # and C-gnu90 (c90 + long long + variadic macros ) and above # EnableCompilerFlag("-std=c++11" false true) # Set C++ compilation to c++11 standard # EnableCompilerFlag("-std=c99" true false) # Set C compilation to c99 standard - if (CMAKE_CXX_COMPILER_ID MATCHES "Clang" AND MSVC) + if ((CMAKE_C_COMPILER_ID MATCHES "Clang" OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") AND MSVC) # clang-cl normally maps -Wall to -Weverything. - EnableCompilerFlag("/clang:-Wall" _C _CXX false) + EnableCompilerFlag("/clang:-Wall" ${_C} ${_CXX} false) else () - EnableCompilerFlag("-Wall" _C _CXX false) + EnableCompilerFlag("-Wall" ${_C} ${_CXX} false) endif () - EnableCompilerFlag("-Wextra" _C _CXX false) - EnableCompilerFlag("-Wundef" _C _CXX false) - EnableCompilerFlag("-Wshadow" _C _CXX false) - EnableCompilerFlag("-Wcast-align" _C _CXX false) - EnableCompilerFlag("-Wcast-qual" _C _CXX false) - EnableCompilerFlag("-Wstrict-prototypes" _C false false) + EnableCompilerFlag("-Wextra" ${_C} ${_CXX} false) + EnableCompilerFlag("-Wundef" ${_C} ${_CXX} false) + EnableCompilerFlag("-Wshadow" ${_C} ${_CXX} false) + EnableCompilerFlag("-Wcast-align" ${_C} ${_CXX} false) + EnableCompilerFlag("-Wcast-qual" ${_C} ${_CXX} false) + EnableCompilerFlag("-Wstrict-prototypes" ${_C} false false) # Enable asserts in Debug mode if (CMAKE_BUILD_TYPE MATCHES "Debug") - EnableCompilerFlag("-DDEBUGLEVEL=1" _C _CXX false) + EnableCompilerFlag("-DDEBUGLEVEL=1" ${_C} ${_CXX} false) endif () # Add noexecstack flags # LDFLAGS - EnableCompilerFlag("-Wl,-z,noexecstack" false false _LD) + EnableCompilerFlag("-Wl,-z,noexecstack" false false ${_LD}) # CFLAGS & CXXFLAGS - EnableCompilerFlag("-Qunused-arguments" _C _CXX false) - EnableCompilerFlag("-Wa,--noexecstack" _C _CXX false) + EnableCompilerFlag("-Qunused-arguments" ${_C} ${_CXX} false) + EnableCompilerFlag("-Wa,--noexecstack" ${_C} ${_CXX} false) # NOTE: Using 3 nested ifs because the variables are sometimes # empty if the condition is false, and sometimes equal to false. # This implicitly converts them to truthy values. There may be # a better way to do this, but this reliably works. - if (${LD_FLAG_WL_Z_NOEXECSTACK}) - if (${C_FLAG_WA_NOEXECSTACK}) - if (${CXX_FLAG_WA_NOEXECSTACK}) + if (LD_FLAG_WL_Z_NOEXECSTACK) + if (C_FLAG_WA_NOEXECSTACK) + if (NOT CMAKE_CXX_COMPILER_LOADED OR CXX_FLAG_WA_NOEXECSTACK) # We've succeeded in marking the stack as non-executable set(ZSTD_HAS_NOEXECSTACK true) endif() diff --git a/build/cmake/lib/CMakeLists.txt b/build/cmake/lib/CMakeLists.txt index 82f4f50869c..df567e2fbaa 100644 --- a/build/cmake/lib/CMakeLists.txt +++ b/build/cmake/lib/CMakeLists.txt @@ -98,7 +98,7 @@ if (ZSTD_LEGACY_SUPPORT) ${LIBRARY_LEGACY_DIR}/zstd_v07.h) endif () -if (MSVC AND NOT (CMAKE_CXX_COMPILER_ID STREQUAL "Clang")) +if (MSVC AND NOT (CMAKE_C_COMPILER_ID STREQUAL "Clang")) set(MSVC_RESOURCE_DIR ${ZSTD_SOURCE_DIR}/build/VS2010/libzstd-dll) set(PlatformDependResources ${MSVC_RESOURCE_DIR}/libzstd-dll.rc) else() @@ -189,7 +189,7 @@ if (MSVC) endif () # With MSVC static library needs to be renamed to avoid conflict with import library -if (MSVC OR (WIN32 AND CMAKE_CXX_COMPILER_ID STREQUAL "Clang" AND NOT MINGW)) +if (MSVC OR (WIN32 AND CMAKE_C_COMPILER_ID STREQUAL "Clang" AND NOT MINGW)) set(STATIC_LIBRARY_BASE_NAME zstd_static) else () set(STATIC_LIBRARY_BASE_NAME zstd) diff --git a/contrib/linux-kernel/linux_zstd.h b/contrib/linux-kernel/linux_zstd.h index 2f2a3c8b8a3..a3a1822a78f 100644 --- a/contrib/linux-kernel/linux_zstd.h +++ b/contrib/linux-kernel/linux_zstd.h @@ -677,7 +677,8 @@ typedef ZSTD_Sequence zstd_sequence; * @in_seqs_size: The number of sequences in in_seqs. * @literals: The literals associated to the sequences to be compressed. * @lit_size: The size of the literals in the literals buffer. - * @lit_capacity: The size of the literals buffer. + * @lit_capacity: The size of the literals buffer, which must be at least + * 8 bytes larger than lit_size. * @decompressed_size: The size of the input data * * Return: The compressed size or an error, which can be checked using diff --git a/contrib/seekable_format/tests/seekable_tests.c b/contrib/seekable_format/tests/seekable_tests.c index 809ea6425eb..6481c65106a 100644 --- a/contrib/seekable_format/tests/seekable_tests.c +++ b/contrib/seekable_format/tests/seekable_tests.c @@ -1,11 +1,14 @@ #include #include +#include #include // malloc #include #include #include #include "../zstd_seekable.h" +#include "zstd_errors.h" +#include "xxhash.h" /* ZSTD_seekable_customFile implementation that reads/seeks a buffer while keeping track of total bytes read */ @@ -53,6 +56,243 @@ static int seekBuffWithTotal(void* opaque, long long offset, int origin) return 0; } +static int testFrameSizeBounds(const ZSTD_seekable* stream, const ZSTD_seekTable* table) +{ + unsigned const nbFrames = ZSTD_seekTable_getNumFrames(table); + unsigned const indices[] = {nbFrames, nbFrames + 1, UINT_MAX}; + size_t i; + for (i = 0; i < sizeof(indices) / sizeof(indices[0]); ++i) { + unsigned const index = indices[i]; + size_t const results[] = { + ZSTD_seekable_getFrameCompressedSize(stream, index), + ZSTD_seekable_getFrameDecompressedSize(stream, index), + ZSTD_seekTable_getFrameCompressedSize(table, index), + ZSTD_seekTable_getFrameDecompressedSize(table, index), + }; + size_t j; + for (j = 0; j < sizeof(results) / sizeof(results[0]); ++j) { + if (ZSTD_getErrorCode(results[j]) != ZSTD_error_frameIndex_tooLarge) { + return 1; + } + } + } + return 0; +} + +/* Build a valid archive whose seek table can span multiple read buffers. */ +static void* createRepeatedFrameArchive(const char* data, unsigned numFrames, + int checksumFlag, size_t* archiveSize) +{ + unsigned char frame[128]; + size_t const dataSize = strlen(data); + size_t const frameSize = ZSTD_compress(frame, sizeof(frame), data, dataSize, 1); + size_t const entrySize = checksumFlag ? 12 : 8; + size_t capacity; + unsigned char* archive; + ZSTD_frameLog* frameLog; + unsigned i; + unsigned const checksum = (unsigned)XXH64(data, dataSize, 0); + if (ZSTD_isError(frameSize)) return NULL; + capacity = numFrames * (frameSize + entrySize) + 8 + ZSTD_seekTableFooterSize; + archive = malloc(capacity); + frameLog = ZSTD_seekable_createFrameLog(checksumFlag); + if (archive == NULL || frameLog == NULL) goto _error; + for (i = 0; i < numFrames; ++i) { + size_t const status = ZSTD_seekable_logFrame(frameLog, (unsigned)frameSize, + (unsigned)dataSize, checksum); + if (ZSTD_isError(status)) goto _error; + memcpy(archive + i * frameSize, frame, frameSize); + } + { ZSTD_outBuffer out = {archive, capacity, numFrames * frameSize}; + if (ZSTD_seekable_writeSeekTable(frameLog, &out) != 0) goto _error; + *archiveSize = out.pos; + } + ZSTD_seekable_freeFrameLog(frameLog); + return archive; +_error: + if (frameLog != NULL) ZSTD_seekable_freeFrameLog(frameLog); + free(archive); + return NULL; +} + +typedef struct { + buffWrapperWithTotal_t buff; + unsigned reads; + unsigned seeks; + unsigned failRead; + unsigned failSeek; +} faultyBuff_t; + +static int readFaultyBuff(void* opaque, void* buffer, size_t n) +{ + faultyBuff_t* const buff = (faultyBuff_t*)opaque; + if (++buff->reads == buff->failRead) return -1; + return readBuffWithTotal(&buff->buff, buffer, n); +} + +static int seekFaultyBuff(void* opaque, long long offset, int origin) +{ + faultyBuff_t* const buff = (faultyBuff_t*)opaque; + if (++buff->seeks == buff->failSeek) return -1; + return seekBuffWithTotal(&buff->buff, offset, origin); +} + +static int isUninitialized(ZSTD_seekable* stream) +{ + char output = '!'; + size_t const status = ZSTD_seekable_decompress(stream, &output, 1, 1); + size_t const frameStatus = ZSTD_seekable_decompressFrame(stream, &output, 1, 0); + ZSTD_seekTable* const table = ZSTD_seekTable_create_fromSeekable(stream); + int const result = ZSTD_getErrorCode(status) == ZSTD_error_init_missing + && ZSTD_getErrorCode(frameStatus) == ZSTD_error_init_missing + && output == '!' + && table == NULL + && ZSTD_seekable_getNumFrames(stream) == 0 + && ZSTD_seekable_offsetToFrameIndex(stream, 0) == 0 + && ZSTD_seekable_offsetToFrameIndex(stream, ~0ULL) == 0 + && ZSTD_seekable_getFrameCompressedOffset(stream, 0) == ZSTD_SEEKABLE_FRAMEINDEX_TOOLARGE + && ZSTD_seekable_getFrameDecompressedOffset(stream, 0) == ZSTD_SEEKABLE_FRAMEINDEX_TOOLARGE + && ZSTD_getErrorCode(ZSTD_seekable_getFrameCompressedSize(stream, 0)) == ZSTD_error_frameIndex_tooLarge + && ZSTD_getErrorCode(ZSTD_seekable_getFrameDecompressedSize(stream, 0)) == ZSTD_error_frameIndex_tooLarge; + ZSTD_seekTable_free(table); + return result; +} + +static int testRepeatedInit(void) +{ + const char* const data[2] = {"abcdefg", "ZYXWVUT"}; + unsigned const numFrames[2] = {1, 32768}; + void* archives[2] = {NULL, NULL}; + size_t sizes[2]; + FILE* files[2] = {NULL, NULL}; + buffWrapperWithTotal_t buffers[2]; + ZSTD_seekable* const stream = ZSTD_seekable_create(); + unsigned i; + int success = 0; + if (stream == NULL) goto _cleanup; + for (i = 0; i < 2; ++i) { + archives[i] = createRepeatedFrameArchive(data[i], numFrames[i], (int)i, &sizes[i]); + if (archives[i] == NULL) goto _cleanup; + files[i] = tmpfile(); + if (files[i] == NULL || fwrite(archives[i], 1, sizes[i], files[i]) != sizes[i] + || fflush(files[i]) != 0) goto _cleanup; + buffers[i] = (buffWrapperWithTotal_t){archives[i], sizes[i], 0, 0}; + } + /* Reuse one object across all three APIs and both checksum modes. + * LeakSanitizer/Valgrind also checks that only one table is retained. */ + for (i = 0; i < 30; ++i) { + unsigned const idx = i % 2; + char output[7]; + size_t status; + switch ((i / 2) % 3) { + case 0: + status = ZSTD_seekable_initBuff(stream, archives[idx], sizes[idx]); + break; + case 1: + status = ZSTD_seekable_initFile(stream, files[idx]); + break; + default: + { ZSTD_seekable_customFile const src = {&buffers[idx], &readBuffWithTotal, &seekBuffWithTotal}; + status = ZSTD_seekable_initAdvanced(stream, src); + } + break; + } + if (ZSTD_isError(status) || ZSTD_seekable_getNumFrames(stream) != numFrames[idx]) goto _cleanup; + status = ZSTD_seekable_decompressFrame(stream, output, sizeof(output), numFrames[idx] - 1); + if (status != sizeof(output) || memcmp(output, data[idx], sizeof(output)) != 0) goto _cleanup; + /* Leave buffered output behind for the next initialization. */ + status = ZSTD_seekable_decompress(stream, output, 1, 0); + if (status != 1 || output[0] != data[idx][0]) goto _cleanup; + } + success = 1; +_cleanup: + ZSTD_seekable_free(stream); + for (i = 0; i < 2; ++i) { + if (files[i] != NULL) fclose(files[i]); + free(archives[i]); + } + return success; +} + +static int testFailedInit(void) +{ + unsigned const numFrames = 32768; + const unsigned char badData[ZSTD_seekTableFooterSize] = {0}; + size_t originalSize; + size_t emptySize; + void* const original = createRepeatedFrameArchive("abcdefg", 1, 0, &originalSize); + void* const empty = createRepeatedFrameArchive("", 0, 0, &emptySize); + void* archive = NULL; + FILE* const badFile = tmpfile(); + ZSTD_seekable* const stream = ZSTD_seekable_create(); + unsigned checksumFlag; + int success = 0; + if (original == NULL || empty == NULL || badFile == NULL || stream == NULL) goto _cleanup; + if (fwrite(badData, 1, sizeof(badData), badFile) != sizeof(badData) + || fflush(badFile) != 0 || !isUninitialized(stream)) goto _cleanup; + + for (checksumFlag = 0; checksumFlag < 2; ++checksumFlag) { + size_t archiveSize; + unsigned failure; + archive = createRepeatedFrameArchive("ZYXWVUT", numFrames, (int)checksumFlag, &archiveSize); + if (archive == NULL) goto _cleanup; + for (failure = 0; failure < 8; ++failure) { + faultyBuff_t buff = {{archive, archiveSize, 0, 0}, 0, 0, 0, 0}; + ZSTD_seekable_customFile const src = {&buff, &readFaultyBuff, &seekFaultyBuff}; + ZSTD_ErrorCode const expected = failure < 2 ? ZSTD_error_prefix_unknown : ZSTD_error_seekableIO; + char output[7]; + size_t status = ZSTD_seekable_initBuff(stream, original, originalSize); + if (ZSTD_isError(status)) goto _cleanup; + status = ZSTD_seekable_decompress(stream, output, 1, 0); + if (status != 1 || output[0] != 'a') goto _cleanup; + + if (failure == 0) { + status = ZSTD_seekable_initBuff(stream, badData, sizeof(badData)); + } else if (failure == 1) { + status = ZSTD_seekable_initFile(stream, badFile); + } else { + /* Reads 1/2 happen before allocation; reads 3/4 after it. + * The loader also performs two seeks before allocation. */ + if (failure < 6) buff.failRead = failure - 1; + else buff.failSeek = failure - 5; + status = ZSTD_seekable_initAdvanced(stream, src); + } + if (ZSTD_getErrorCode(status) != expected || !isUninitialized(stream)) goto _cleanup; + + /* Consecutive failed initializations must also be harmless. */ + status = ZSTD_seekable_initBuff(stream, badData, sizeof(badData)); + if (ZSTD_getErrorCode(status) != ZSTD_error_prefix_unknown || !isUninitialized(stream)) goto _cleanup; + + /* A later successful initialization must use only the new input. */ + buff.reads = buff.seeks = buff.failRead = buff.failSeek = 0; + status = ZSTD_seekable_initAdvanced(stream, src); + if (ZSTD_isError(status) || ZSTD_seekable_getNumFrames(stream) != numFrames) goto _cleanup; + status = ZSTD_seekable_decompressFrame(stream, output, sizeof(output), numFrames - 1); + if (status != sizeof(output) || memcmp(output, "ZYXWVUT", sizeof(output)) != 0) goto _cleanup; + } + free(archive); + archive = NULL; + } + + /* A successfully initialized empty archive is not an uninitialized object. */ + if (ZSTD_isError(ZSTD_seekable_initBuff(stream, empty, emptySize))) goto _cleanup; + { char output; + if (ZSTD_seekable_getNumFrames(stream) != 0 + || ZSTD_seekable_offsetToFrameIndex(stream, 0) != 0 + || ZSTD_seekable_decompress(stream, &output, 1, 0) != 0 + || ZSTD_getErrorCode(ZSTD_seekable_decompressFrame(stream, &output, 1, 0)) != ZSTD_error_frameIndex_tooLarge) + goto _cleanup; + } + success = 1; +_cleanup: + ZSTD_seekable_free(stream); + if (badFile != NULL) fclose(badFile); + free(archive); + free(empty); + free(original); + return success; +} + /* Basic unit tests for zstd seekable format */ int main(int argc, const char** argv) { @@ -121,15 +361,21 @@ int main(int argc, const char** argv) size_t const origSize = ZSTD_seekTable_getFrameDecompressedSize(zst, 0); assert(origSize == inSize); + /* out-of-range frame index must be rejected, not read past the table */ + assert(ZSTD_isError(ZSTD_seekTable_getFrameCompressedSize(zst, nbFrames))); + assert(ZSTD_isError(ZSTD_seekTable_getFrameDecompressedSize(zst, nbFrames))); + unsigned const fo1idx = ZSTD_seekTable_offsetToFrameIndex(zst, 1); assert(fo1idx == 0); + int const boundsStatus = testFrameSizeBounds(stream, zst); free(inBuffer); free(seekBuffer); free(outBuffer); ZSTD_seekable_freeCStream(zscs); ZSTD_seekTable_free(zst); ZSTD_seekable_free(stream); + if (boundsStatus != 0) goto _test_error; } printf("Success!\n"); @@ -167,6 +413,17 @@ int main(int argc, const char** argv) goto _test_error; } } + /* This valid seek table has no frames, so even index 0 is invalid. */ + { ZSTD_seekTable* const table = ZSTD_seekTable_create_fromSeekable(stream); + assert(table != NULL); + assert(ZSTD_seekTable_getNumFrames(table) == 0); + int const boundsStatus = testFrameSizeBounds(stream, table); + ZSTD_seekTable_free(table); + if (boundsStatus != 0) { + ZSTD_seekable_free(stream); + goto _test_error; + } } + /* Should return an error, but not hang */ { const size_t offset = 2; size_t const status = ZSTD_seekable_decompress(stream, uncompressed_data, uncompressed_size, offset); @@ -365,6 +622,221 @@ int main(int argc, const char** argv) } printf("Success!\n"); + printf("Test %u - reject seek table with numFrames beyond format max: ", testNb++); + { /* A corrupt footer claiming numFrames > ZSTD_SEEKABLE_MAXFRAMES must be + * rejected up front: such a value overflows the seek table size math. */ + const uint8_t compressed_data[17] = { + 0x5e, 0x2a, 0x4d, 0x18, /* skippable frame magic (| 0xE) */ + 0x09, 0x00, 0x00, 0x00, /* skippable frame size = 9 */ + 0x01, 0x00, 0x00, 0x08, /* footer: numFrames = 0x08000001 */ + 0x00, /* footer: seek table descriptor */ + 0xb1, 0xea, 0x92, 0x8f, /* seekable magic number */ + }; + + ZSTD_seekable* const stream = ZSTD_seekable_create(); + assert(stream != NULL); + { size_t const status = ZSTD_seekable_initBuff(stream, compressed_data, sizeof(compressed_data)); + if (ZSTD_getErrorCode(status) != ZSTD_error_corruption_detected) { + ZSTD_seekable_free(stream); + goto _test_error; + } } + ZSTD_seekable_free(stream); + } + printf("Success!\n"); + + printf("Test %u - decompress at and past the end of the data: ", testNb++); + { char const inBuffer[] = "Lorem ipsum dolor sit amet"; + size_t const inSize = sizeof(inBuffer); + + size_t const seekCapacity = 5000; + void* const seekBuffer = malloc(seekCapacity); + assert(seekBuffer != NULL); + size_t seekSize; + + char outBuffer[16]; + + ZSTD_seekable_CStream* const zscs = ZSTD_seekable_createCStream(); + assert(zscs != NULL); + { size_t const initStatus = ZSTD_seekable_initCStream(zscs, 9, 0 /* checksumFlag */, 0 /* maxFrameSize */); + assert(!ZSTD_isError(initStatus)); + } + + { ZSTD_outBuffer outb = { .dst=seekBuffer, .pos=0, .size=seekCapacity }; + ZSTD_inBuffer inb = { .src=inBuffer, .pos=0, .size=inSize }; + + size_t const cStatus = ZSTD_seekable_compressStream(zscs, &outb, &inb); + assert(!ZSTD_isError(cStatus)); + assert(inb.pos == inb.size); + + size_t const endStatus = ZSTD_seekable_endStream(zscs, &outb); + assert(!ZSTD_isError(endStatus)); + seekSize = outb.pos; + } + + ZSTD_seekable* const stream = ZSTD_seekable_create(); + assert(stream != NULL); + { size_t const initStatus = ZSTD_seekable_initBuff(stream, seekBuffer, seekSize); + assert(!ZSTD_isError(initStatus)); } + + { size_t const atEnd = ZSTD_seekable_decompress(stream, outBuffer, sizeof(outBuffer), inSize); + size_t const pastEnd = ZSTD_seekable_decompress(stream, outBuffer, sizeof(outBuffer), inSize + 5000); + int failed = atEnd != 0 || !ZSTD_isError(pastEnd); + if (!failed) { + /* offset + len wraps around to 0 */ + size_t const wrapped = ZSTD_seekable_decompress(stream, outBuffer, sizeof(outBuffer), 0ULL - sizeof(outBuffer)); + failed = !ZSTD_isError(wrapped); + } + if (failed) { + free(seekBuffer); + ZSTD_seekable_freeCStream(zscs); + ZSTD_seekable_free(stream); + goto _test_error; + } + } + + free(seekBuffer); + ZSTD_seekable_freeCStream(zscs); + ZSTD_seekable_free(stream); + } + printf("Success!\n"); + + printf("Test %u - reject seek-table decompressed-size mismatches: ", testNb++); + { unsigned const actualSizes[] = {1, 0}; + unsigned checksumFlag; + for (checksumFlag = 0; checksumFlag < 2; ++checksumFlag) { + size_t frameCase; + for (frameCase = 0; frameCase < sizeof(actualSizes) / sizeof(actualSizes[0]); ++frameCase) { + unsigned const actualSize = actualSizes[frameCase]; + unsigned extraSize; + for (extraSize = 0; extraSize < 2; ++extraSize) { + char const data[] = "A"; + unsigned char archive[128]; + unsigned const declaredSize = actualSize + extraSize; + size_t const cSize = ZSTD_compress(archive, sizeof(archive), data, actualSize, 1); + ZSTD_frameLog* const log = ZSTD_seekable_createFrameLog((int)checksumFlag); + ZSTD_outBuffer seekTable = {archive, sizeof(archive), cSize}; + unsigned sourceMode; + assert(!ZSTD_isError(cSize)); + assert(log != NULL); + { size_t const status = ZSTD_seekable_logFrame(log, (unsigned)cSize, declaredSize, + (unsigned)XXH64(data, actualSize, 0)); + assert(!ZSTD_isError(status)); + } + { size_t const status = ZSTD_seekable_writeSeekTable(log, &seekTable); + assert(status == 0); + } + ZSTD_seekable_freeFrameLog(log); + + /* The frame and checksum are valid; only its advertised + * decompressed size is wrong when extraSize is 1. */ + for (sourceMode = 0; sourceMode < 3; ++sourceMode) { + ZSTD_seekable* const stream = ZSTD_seekable_create(); + buffWrapperWithTotal_t buff = {archive, seekTable.pos, 0, 0}; + FILE* file = NULL; + char out[2]; + size_t initStatus; + unsigned attempt; + int failed; + assert(stream != NULL); + if (sourceMode == 0) { + file = tmpfile(); + if (file == NULL || fwrite(archive, 1, seekTable.pos, file) != seekTable.pos || fflush(file) != 0) { + if (file != NULL) fclose(file); + ZSTD_seekable_free(stream); + goto _test_error; + } + initStatus = ZSTD_seekable_initFile(stream, file); + } else if (sourceMode == 1) { + initStatus = ZSTD_seekable_initBuff(stream, archive, seekTable.pos); + } else { + ZSTD_seekable_customFile const src = {&buff, &readBuffWithTotal, &seekBuffWithTotal}; + initStatus = ZSTD_seekable_initAdvanced(stream, src); + } + failed = ZSTD_isError(initStatus) != 0; + for (attempt = 0; !failed && attempt < 2; ++attempt) { + size_t const status = ZSTD_seekable_decompress(stream, out, declaredSize, 0); + if (extraSize) { + failed = ZSTD_getErrorCode(status) != ZSTD_error_corruption_detected; + } else { + failed = status != actualSize || memcmp(out, data, actualSize) != 0; + } + } + ZSTD_seekable_free(stream); + if (file != NULL) fclose(file); + if (failed) goto _test_error; + } + } + } + } + } + printf("Success!\n"); + + printf("Test %u - retry and recover after a failed frame seek: ", testNb++); + { /* A valid raw zstd frame containing "abcdefg", followed by a corrupt + * seek table. Frame 1 is claimed to start at compressed offset 4096, + * beyond this 65-byte input. Its decompressed range is [7, 10). */ + const uint8_t compressed_data[] = { + 0x28, 0xb5, 0x2f, 0xfd, 0x20, 0x07, 0x39, 0x00, 0x00, + 'a', 'b', 'c', 'd', 'e', 'f', 'g', + 0x5e, 0x2a, 0x4d, 0x18, 0x29, 0x00, 0x00, 0x00, + 0x00, 0x10, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, + 0x0a, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, + 0x0a, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x0a, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x04, 0x00, 0x00, 0x00, 0x00, 0xb1, 0xea, 0x92, 0x8f, + }; + struct { + unsigned long long offset; + size_t size; + const char* expected; + } const tests[] = { + {8, 1, NULL}, /* first seek fails before input is initialized */ + {8, 1, NULL}, /* retry the same request */ + {9, 1, NULL}, /* seek forward within the same frame */ + {7, 1, NULL}, /* seek to the start of the same frame */ + {0, 1, "a"}, /* recover by reading the valid first frame */ + {8, 1, NULL}, /* failed seek with an initialized decoder */ + {8, 1, NULL}, + {1, 6, "bcdefg"}, /* resume the previously valid frame */ + }; + unsigned useCustomSource; + for (useCustomSource = 0; useCustomSource < 2; ++useCustomSource) { + char outBuffer[7]; + buffWrapperWithTotal_t buff = {compressed_data, sizeof(compressed_data), 0, 0}; + ZSTD_seekable* const stream = ZSTD_seekable_create(); + size_t initStatus; + size_t idx; + int failed; + assert(stream != NULL); + if (useCustomSource) { + ZSTD_seekable_customFile const src = {&buff, &readBuffWithTotal, &seekBuffWithTotal}; + initStatus = ZSTD_seekable_initAdvanced(stream, src); + } else { + initStatus = ZSTD_seekable_initBuff(stream, compressed_data, sizeof(compressed_data)); + } + failed = ZSTD_isError(initStatus) || ZSTD_seekable_getNumFrames(stream) != 4; + for (idx = 0; !failed && idx < sizeof(tests) / sizeof(tests[0]); ++idx) { + size_t const status = ZSTD_seekable_decompress(stream, outBuffer, tests[idx].size, tests[idx].offset); + if (tests[idx].expected == NULL) { + failed = ZSTD_getErrorCode(status) != ZSTD_error_seekableIO; + } else { + failed = status != tests[idx].size || memcmp(outBuffer, tests[idx].expected, tests[idx].size) != 0; + } + } + ZSTD_seekable_free(stream); + if (failed) goto _test_error; + } + } + printf("Success!\n"); + + printf("Test %u - repeated initialization across source types: ", testNb++); + if (!testRepeatedInit()) goto _test_error; + printf("Success!\n"); + + printf("Test %u - failed initialization and recovery: ", testNb++); + if (!testFailedInit()) goto _test_error; + printf("Success!\n"); + /* TODO: Add more tests */ printf("Finished tests\n"); return 0; diff --git a/contrib/seekable_format/zstd_seekable.h b/contrib/seekable_format/zstd_seekable.h index b1f83d0e0a1..1c07fdb18a7 100644 --- a/contrib/seekable_format/zstd_seekable.h +++ b/contrib/seekable_format/zstd_seekable.h @@ -123,7 +123,12 @@ ZSTDLIB_API size_t ZSTD_seekable_writeSeekTable(ZSTD_frameLog* fl, ZSTD_outBuffe * A ZSTD_seekable object is required to tracking the seekTable. * * Call ZSTD_seekable_init* to initialize a ZSTD_seekable object with the -* the seek table provided in the input. +* seek table provided in the input. These functions may also be called again +* to reset the object for a new input. Resetting discards the previous input +* and seek table, even if initialization fails. Before the first successful +* initialization, or after a failed initialization, the object has no frames +* and decompression returns an initialization-missing error. The object may +* be initialized again or freed. * There are three modes for ZSTD_seekable_init: * - ZSTD_seekable_initBuff() : An in-memory API. The data contained in * `src` should be the entire seekable file, including the seek table. @@ -152,9 +157,12 @@ ZSTDLIB_API size_t ZSTD_seekable_writeSeekTable(ZSTD_frameLog* fl, ZSTD_outBuffe * consecutive range of data, it will efficiently retain the decompressor object * and avoid redecompressing frame prefixes. The return value is the number of * bytes decompressed, or an error code checkable with ZSTD_isError(). +* It is smaller than `dstSize` only when the data ends before +* `offset + dstSize`, and it is 0 when `offset` is the end of the data. +* An `offset` past the end of the data is an error. * * The seek table access functions can be used to obtain the data contained -* in the seek table. If frameIndex is larger than the value returned by +* in the seek table. If frameIndex is greater than or equal to * ZSTD_seekable_getNumFrames(), they will return error codes checkable with * ZSTD_isError(). Note that since the offset access functions return * unsigned long long instead of size_t, in this case they will instead return diff --git a/contrib/seekable_format/zstdseek_decompress.c b/contrib/seekable_format/zstdseek_decompress.c index ab9088a1061..7e10629b883 100644 --- a/contrib/seekable_format/zstdseek_decompress.c +++ b/contrib/seekable_format/zstdseek_decompress.c @@ -215,6 +215,10 @@ struct ZSTD_seekable_s { ZSTD_seekable_customFile src; U64 decompressedOffset; + /* (U32)-1 means no frame is ready for decompression. Otherwise this is + * the current frame index. Set to -1 before seeking, so a failed seek + * forces the next call to retry setup. Set to targetFrame only after + * the seek succeeds and the decompression state is initialized. */ U32 curFrame; BYTE inBuff[SEEKABLE_BUFF_SIZE]; /* need to do our own input buffering */ @@ -299,7 +303,7 @@ unsigned ZSTD_seekTable_offsetToFrameIndex(const ZSTD_seekTable* st, unsigned lo U32 hi = (U32)st->tableLen; assert(st->tableLen <= UINT_MAX); - if (pos >= st->entries[st->tableLen].dOffset) { + if (hi == 0 || pos >= st->entries[st->tableLen].dOffset) { return (unsigned)st->tableLen; } @@ -366,7 +370,7 @@ size_t ZSTD_seekable_getFrameDecompressedSize(const ZSTD_seekable* zs, unsigned size_t ZSTD_seekTable_getFrameDecompressedSize(const ZSTD_seekTable* st, unsigned frameIndex) { - if (frameIndex > st->tableLen) return ERROR(frameIndex_tooLarge); + if (frameIndex >= st->tableLen) return ERROR(frameIndex_tooLarge); return st->entries[frameIndex + 1].dOffset - st->entries[frameIndex].dOffset; } @@ -397,6 +401,13 @@ static size_t ZSTD_seekable_loadSeekTable(ZSTD_seekable* zs) U32 const frameSize = tableSize + ZSTD_seekTableFooterSize + ZSTD_SKIPPABLEHEADERSIZE; U32 remaining = frameSize - ZSTD_seekTableFooterSize; /* don't need to re-read footer */ + + /* numFrames is bounded by the format (the compressor never writes more + * than ZSTD_SEEKABLE_MAXFRAMES). A larger value can only come from a + * corrupt footer and overflows the tableSize/frameSize math above, so + * reject it before those values are used to seek or size the table. */ + if (numFrames > ZSTD_SEEKABLE_MAXFRAMES) return ERROR(corruption_detected); + { U32 const toRead = MIN(remaining, SEEKABLE_BUFF_SIZE); CHECK_IO(src.seek(src.opaque, -(S64)frameSize, SEEK_END)); CHECK_IO(src.read(src.opaque, zs->inBuff, toRead)); @@ -428,7 +439,10 @@ static size_t ZSTD_seekable_loadSeekTable(ZSTD_seekable* zs) U32 const offset = SEEKABLE_BUFF_SIZE - pos; U32 const toRead = MIN(remaining, SEEKABLE_BUFF_SIZE - offset); memmove(zs->inBuff, zs->inBuff + pos, offset); /* move any data we haven't read yet */ - CHECK_IO(src.read(src.opaque, zs->inBuff+offset, toRead)); + if (src.read(src.opaque, zs->inBuff+offset, toRead) < 0) { + free(entries); + return ERROR(seekableIO); + } remaining -= toRead; pos = 0; } @@ -473,24 +487,34 @@ size_t ZSTD_seekable_initFile(ZSTD_seekable* zs, FILE* src) size_t ZSTD_seekable_initAdvanced(ZSTD_seekable* zs, ZSTD_seekable_customFile src) { - zs->src = src; - - { const size_t seekTableInit = ZSTD_seekable_loadSeekTable(zs); - if (ZSTD_isError(seekTableInit)) return seekTableInit; } - + /* Reinitialization discards the previous archive, even on failure. + * A NULL entries pointer marks a context that is not initialized. */ + free(zs->seekTable.entries); + zs->seekTable = (ZSTD_seekTable){NULL, 0, 0}; zs->decompressedOffset = (U64)-1; zs->curFrame = (U32)-1; + /* Discard buffered input from the previous source, even on failure. */ + zs->in = (ZSTD_inBuffer){zs->inBuff, 0, 0}; + zs->src = src; + /* When not reading from a buffer via initBuff() clear the buffWrapper + * to avoid inheriting its state. */ + if (src.opaque != &zs->buffWrapper) { + zs->buffWrapper = (buffWrapper_t){NULL, 0, 0}; + } { const size_t dstreamInit = ZSTD_initDStream(zs->dstream); if (ZSTD_isError(dstreamInit)) return dstreamInit; } - return 0; + /* Publish the new seek table only after all initialization has succeeded. */ + return ZSTD_seekable_loadSeekTable(zs); } size_t ZSTD_seekable_decompress(ZSTD_seekable* zs, void* dst, size_t len, unsigned long long offset) { + if (zs->seekTable.entries == NULL) return ERROR(init_missing); unsigned long long const eos = zs->seekTable.entries[zs->seekTable.tableLen].dOffset; - if (offset + len > eos) { - len = eos - offset; + if (offset > eos) return ERROR(parameter_outOfBound); + if (len > eos - offset) { + len = (size_t)(eos - offset); } U32 targetFrame = ZSTD_seekable_offsetToFrameIndex(zs, offset); @@ -499,8 +523,9 @@ size_t ZSTD_seekable_decompress(ZSTD_seekable* zs, void* dst, size_t len, unsign do { /* check if we can continue from a previous decompress job */ if (targetFrame != zs->curFrame || offset < zs->decompressedOffset) { - zs->decompressedOffset = zs->seekTable.entries[targetFrame].dOffset; - zs->curFrame = targetFrame; + /* A failed seek must force the next call to reset the stream, + * rather than reuse uninitialized or stale input state. */ + zs->curFrame = (U32)-1; assert(zs->seekTable.entries[targetFrame].cOffset < LLONG_MAX); CHECK_IO(zs->src.seek(zs->src.opaque, @@ -512,6 +537,8 @@ size_t ZSTD_seekable_decompress(ZSTD_seekable* zs, void* dst, size_t len, unsign if (zs->buffWrapper.size && srcBytesRead > zs->buffWrapper.size) { return ERROR(seekableIO); } + zs->decompressedOffset = zs->seekTable.entries[targetFrame].dOffset; + zs->curFrame = targetFrame; } while (zs->decompressedOffset < offset + len) { @@ -552,6 +579,15 @@ size_t ZSTD_seekable_decompress(ZSTD_seekable* zs, void* dst, size_t len, unsign if (toRead == 0) { /* frame complete */ + /* A short frame must not cause the same seek-table entry to + * be selected and decompressed again. Check the declared + * boundary for every source, including FILE and custom I/O. */ + assert(targetFrame < zs->seekTable.tableLen); + if (zs->decompressedOffset != zs->seekTable.entries[targetFrame + 1].dOffset) { + zs->curFrame = (U32)-1; + return ERROR(corruption_detected); + } + /* verify checksum */ if (zs->seekTable.checksumFlag && (XXH64_digest(&zs->xxhState) & 0xFFFFFFFFU) != @@ -583,6 +619,7 @@ size_t ZSTD_seekable_decompress(ZSTD_seekable* zs, void* dst, size_t len, unsign size_t ZSTD_seekable_decompressFrame(ZSTD_seekable* zs, void* dst, size_t dstSize, unsigned frameIndex) { + if (zs->seekTable.entries == NULL) return ERROR(init_missing); if (frameIndex >= zs->seekTable.tableLen) { return ERROR(frameIndex_tooLarge); } diff --git a/doc/zstd_compression_format.md b/doc/zstd_compression_format.md index cdad14f5866..4827ce3e34a 100644 --- a/doc/zstd_compression_format.md +++ b/doc/zstd_compression_format.md @@ -489,7 +489,7 @@ __`Size_Format`__ - For `Raw_Literals_Block` and `RLE_Literals_Block`, it's only necessary to decode `Regenerated_Size`. There is no `Compressed_Size` field. -- For `Compressed_Block` and `Treeless_Literals_Block`, +- For `Compressed_Literals_Block` and `Treeless_Literals_Block`, it's required to decode both `Compressed_Size` and `Regenerated_Size` (the decompressed size). It's also necessary to decode the number of streams (1 or 4). diff --git a/doc/zstd_manual.html b/doc/zstd_manual.html index 89f0761f492..31a4d146df4 100644 --- a/doc/zstd_manual.html +++ b/doc/zstd_manual.html @@ -2114,8 +2114,6 @@

Buffer-less streaming compression functions

ZSTD_DEPR
 ZSTD_DEPRECATED("The buffer-less API is deprecated in favor of the normal streaming API. See docs.")
 ZSTDLIB_STATIC_API size_t ZSTD_compressBegin_usingCDict(ZSTD_CCtx* cctx, const ZSTD_CDict* cdict); /**< note: fails if cdict==NULL */
 

-
size_t ZSTD_copyCCtx(ZSTD_CCtx* cctx, const ZSTD_CCtx* preparedCCtx, unsigned long long pledgedSrcSize); /**<  note: if pledgedSrcSize is not known, use ZSTD_CONTENTSIZE_UNKNOWN */
-

size_t ZSTD_compressBegin_advanced(ZSTD_CCtx* cctx, const void* dict, size_t dictSize, ZSTD_parameters params, unsigned long long pledgedSrcSize); /**< pledgedSrcSize : If srcSize is not known at init time, use ZSTD_CONTENTSIZE_UNKNOWN */
 

Buffer-less streaming decompression (synchronous mode)

diff --git a/lib/compress/zstd_compress.c b/lib/compress/zstd_compress.c
index 38c3fc0d4cd..3cb6f21e603 100644
--- a/lib/compress/zstd_compress.c
+++ b/lib/compress/zstd_compress.c
@@ -184,10 +184,10 @@ static void ZSTD_freeCCtxContent(ZSTD_CCtx* cctx)
 {
     assert(cctx != NULL);
     assert(cctx->staticSize == 0);
-    ZSTD_clearAllDicts(cctx);
 #ifdef ZSTD_MULTITHREAD
-    ZSTDMT_freeCCtx(cctx->mtctx); cctx->mtctx = NULL;
+    ZSTDMT_freeCCtx(cctx->mtctx); cctx->mtctx = NULL;  /* pending jobs may use the dictionary */
 #endif
+    ZSTD_clearAllDicts(cctx);
     ZSTD_cwksp_free(&cctx->workspace, cctx->customMem);
 }
 
@@ -1264,6 +1264,13 @@ static int ZSTD_dedicatedDictSearch_isSupported(
         const ZSTD_compressionParameters* cParams);
 static void ZSTD_dedicatedDictSearch_revertCParams(
         ZSTD_compressionParameters* cParams);
+static size_t ZSTD_createCDict_advanced2_internal(
+        ZSTD_CDict** cdictPtr,
+        const void* dict, size_t dictSize,
+        ZSTD_dictLoadMethod_e dictLoadMethod,
+        ZSTD_dictContentType_e dictContentType,
+        const ZSTD_CCtx_params* originalCctxParams,
+        ZSTD_customMem customMem);
 
 /**
  * Initializes the local dictionary using requested parameters.
@@ -1289,14 +1296,14 @@ static size_t ZSTD_initLocalDict(ZSTD_CCtx* cctx)
     assert(cctx->cdict == NULL);
     assert(cctx->prefixDict.dict == NULL);
 
-    dl->cdict = ZSTD_createCDict_advanced2(
+    FORWARD_IF_ERROR(ZSTD_createCDict_advanced2_internal(
+            &dl->cdict,
             dl->dict,
             dl->dictSize,
             ZSTD_dlm_byRef,
             dl->dictContentType,
             &cctx->requestedParams,
-            cctx->customMem);
-    RETURN_ERROR_IF(!dl->cdict, memory_allocation, "ZSTD_createCDict_advanced failed");
+            cctx->customMem), "");
     cctx->cdict = dl->cdict;
     return 0;
 }
@@ -1360,6 +1367,15 @@ size_t ZSTD_CCtx_refThreadPool(ZSTD_CCtx* cctx, ZSTD_threadPool* pool)
 {
     RETURN_ERROR_IF(cctx->streamStage != zcss_init, stage_wrong,
                     "Can't ref a pool when ctx not in init stage.");
+#ifdef ZSTD_MULTITHREAD
+    if (pool != cctx->pool) {
+        /* The MT context, created by the first multi-threaded frame and then reused,
+         * runs its jobs on the pool it was created with : without this, the next frames
+         * would still use the previous pool, which the caller may free. No job is running :
+         * the stage is zcss_init, and ZSTDMT_freeCCtx() waits for pending jobs. */
+        ZSTDMT_freeCCtx(cctx->mtctx); cctx->mtctx = NULL;
+    }
+#endif
     cctx->pool = pool;
     return 0;
 }
@@ -1389,8 +1405,13 @@ size_t ZSTD_CCtx_reset(ZSTD_CCtx* cctx, ZSTD_ResetDirective reset)
 {
     if ( (reset == ZSTD_reset_session_only)
       || (reset == ZSTD_reset_session_and_parameters) ) {
+#ifdef ZSTD_MULTITHREAD
+        /* jobs of an abandoned frame may still reference the dictionary */
+        if (cctx->mtctx != NULL) ZSTDMT_waitForPendingJobs(cctx->mtctx);
+#endif
         cctx->streamStage = zcss_init;
         cctx->pledgedSrcSizePlusOne = 0;
+        cctx->stableIn_notConsumed = 0;  /* input pending in the abandoned frame */
     }
     if ( (reset == ZSTD_reset_parameters)
       || (reset == ZSTD_reset_session_and_parameters) ) {
@@ -2350,6 +2371,20 @@ static int ZSTD_shouldAttachDict(const ZSTD_CDict* cdict,
                                       * handled in _enforceMaxDist */
 }
 
+/* Check against the addressable content, which may be smaller than the full
+ * dictionary when only a suffix is loaded. */
+static size_t ZSTD_checkDictionaryRepCodes(const U32 rep[ZSTD_REP_NUM], size_t dictContentSize)
+{
+    U32 u;
+    for (u = 0; u < ZSTD_REP_NUM; ++u) {
+        RETURN_ERROR_IF(rep[u] == 0 || rep[u] > dictContentSize,
+                        dictionary_corrupted,
+                        "repcode %u exceeds dictionary content (%zu bytes)",
+                        rep[u], dictContentSize);
+    }
+    return 0;
+}
+
 static size_t
 ZSTD_resetCCtx_byAttachingCDict(ZSTD_CCtx* cctx,
                         const ZSTD_CDict* cdict,
@@ -2389,6 +2424,9 @@ ZSTD_resetCCtx_byAttachingCDict(ZSTD_CCtx* cctx,
             /* don't even attach dictionaries with no contents */
             DEBUGLOG(4, "skipping attaching empty dictionary");
         } else {
+            /* Enforce the dictMatchState block compressors' repcode bounds
+             * before allowing them to search this dictionary. */
+            FORWARD_IF_ERROR(ZSTD_checkDictionaryRepCodes(cdict->cBlockState.rep, cdictLen), "");
             DEBUGLOG(4, "attaching dictionary into context");
             cctx->blockState.matchState.dictMatchState = &cdict->matchState;
 
@@ -2532,105 +2570,6 @@ static size_t ZSTD_resetCCtx_usingCDict(ZSTD_CCtx* cctx,
     }
 }
 
-/*! ZSTD_copyCCtx_internal() :
- *  Duplicate an existing context `srcCCtx` into another one `dstCCtx`.
- *  Only works during stage ZSTDcs_init (i.e. after creation, but before first call to ZSTD_compressContinue()).
- *  The "context", in this case, refers to the hash and chain tables,
- *  entropy tables, and dictionary references.
- * `windowLog` value is enforced if != 0, otherwise value is copied from srcCCtx.
- * @return : 0, or an error code */
-static size_t ZSTD_copyCCtx_internal(ZSTD_CCtx* dstCCtx,
-                            const ZSTD_CCtx* srcCCtx,
-                            ZSTD_frameParameters fParams,
-                            U64 pledgedSrcSize,
-                            ZSTD_buffered_policy_e zbuff)
-{
-    RETURN_ERROR_IF(srcCCtx->stage!=ZSTDcs_init, stage_wrong,
-                    "Can't copy a ctx that's not in init stage.");
-    DEBUGLOG(5, "ZSTD_copyCCtx_internal");
-    ZSTD_memcpy(&dstCCtx->customMem, &srcCCtx->customMem, sizeof(ZSTD_customMem));
-    {   ZSTD_CCtx_params params = dstCCtx->requestedParams;
-        /* Copy only compression parameters related to tables. */
-        params.cParams = srcCCtx->appliedParams.cParams;
-        assert(srcCCtx->appliedParams.useRowMatchFinder != ZSTD_ps_auto);
-        assert(srcCCtx->appliedParams.postBlockSplitter != ZSTD_ps_auto);
-        assert(srcCCtx->appliedParams.ldmParams.enableLdm != ZSTD_ps_auto);
-        params.useRowMatchFinder = srcCCtx->appliedParams.useRowMatchFinder;
-        params.postBlockSplitter = srcCCtx->appliedParams.postBlockSplitter;
-        params.ldmParams = srcCCtx->appliedParams.ldmParams;
-        params.fParams = fParams;
-        params.maxBlockSize = srcCCtx->appliedParams.maxBlockSize;
-        ZSTD_resetCCtx_internal(dstCCtx, ¶ms, pledgedSrcSize,
-                                /* loadedDictSize */ 0,
-                                ZSTDcrp_leaveDirty, zbuff);
-        assert(dstCCtx->appliedParams.cParams.windowLog == srcCCtx->appliedParams.cParams.windowLog);
-        assert(dstCCtx->appliedParams.cParams.strategy == srcCCtx->appliedParams.cParams.strategy);
-        assert(dstCCtx->appliedParams.cParams.hashLog == srcCCtx->appliedParams.cParams.hashLog);
-        assert(dstCCtx->appliedParams.cParams.chainLog == srcCCtx->appliedParams.cParams.chainLog);
-        assert(dstCCtx->blockState.matchState.hashLog3 == srcCCtx->blockState.matchState.hashLog3);
-    }
-
-    ZSTD_cwksp_mark_tables_dirty(&dstCCtx->workspace);
-
-    /* copy tables */
-    {   size_t const chainSize = ZSTD_allocateChainTable(srcCCtx->appliedParams.cParams.strategy,
-                                                         srcCCtx->appliedParams.useRowMatchFinder,
-                                                         0 /* forDDSDict */)
-                                    ? ((size_t)1 << srcCCtx->appliedParams.cParams.chainLog)
-                                    : 0;
-        size_t const hSize =  (size_t)1 << srcCCtx->appliedParams.cParams.hashLog;
-        U32 const h3log = srcCCtx->blockState.matchState.hashLog3;
-        size_t const h3Size = h3log ? ((size_t)1 << h3log) : 0;
-
-        ZSTD_memcpy(dstCCtx->blockState.matchState.hashTable,
-               srcCCtx->blockState.matchState.hashTable,
-               hSize * sizeof(U32));
-        ZSTD_memcpy(dstCCtx->blockState.matchState.chainTable,
-               srcCCtx->blockState.matchState.chainTable,
-               chainSize * sizeof(U32));
-        ZSTD_memcpy(dstCCtx->blockState.matchState.hashTable3,
-               srcCCtx->blockState.matchState.hashTable3,
-               h3Size * sizeof(U32));
-    }
-
-    ZSTD_cwksp_mark_tables_clean(&dstCCtx->workspace);
-
-    /* copy dictionary offsets */
-    {
-        const ZSTD_MatchState_t* srcMatchState = &srcCCtx->blockState.matchState;
-        ZSTD_MatchState_t* dstMatchState = &dstCCtx->blockState.matchState;
-        dstMatchState->window       = srcMatchState->window;
-        dstMatchState->nextToUpdate = srcMatchState->nextToUpdate;
-        dstMatchState->loadedDictEnd= srcMatchState->loadedDictEnd;
-    }
-    dstCCtx->dictID = srcCCtx->dictID;
-    dstCCtx->dictContentSize = srcCCtx->dictContentSize;
-
-    /* copy block state */
-    ZSTD_memcpy(dstCCtx->blockState.prevCBlock, srcCCtx->blockState.prevCBlock, sizeof(*srcCCtx->blockState.prevCBlock));
-
-    return 0;
-}
-
-/*! ZSTD_copyCCtx() :
- *  Duplicate an existing context `srcCCtx` into another one `dstCCtx`.
- *  Only works during stage ZSTDcs_init (i.e. after creation, but before first call to ZSTD_compressContinue()).
- *  pledgedSrcSize==0 means "unknown".
-*   @return : 0, or an error code */
-size_t ZSTD_copyCCtx(ZSTD_CCtx* dstCCtx, const ZSTD_CCtx* srcCCtx, unsigned long long pledgedSrcSize)
-{
-    ZSTD_frameParameters fParams = { 1 /*content*/, 0 /*checksum*/, 0 /*noDictID*/ };
-    ZSTD_buffered_policy_e const zbuff = srcCCtx->bufferedPolicy;
-    ZSTD_STATIC_ASSERT((U32)ZSTDb_buffered==1);
-    if (pledgedSrcSize==0) pledgedSrcSize = ZSTD_CONTENTSIZE_UNKNOWN;
-    fParams.contentSizeFlag = (pledgedSrcSize != ZSTD_CONTENTSIZE_UNKNOWN);
-
-    return ZSTD_copyCCtx_internal(dstCCtx, srcCCtx,
-                                fParams, pledgedSrcSize,
-                                zbuff);
-}
-
-
 #define ZSTD_ROWSIZE 16
 /*! ZSTD_reduceTable() :
  *  reduce table indexes by `reducerValue`, or squash to zero.
@@ -4219,8 +4158,8 @@ typedef struct {
  * In practice, recursion depth usually doesn't go beyond 4.
  *
  * Furthermore, the number of splits is capped by ZSTD_MAX_NB_BLOCK_SPLITS.
- * At ZSTD_MAX_NB_BLOCK_SPLITS == 196 with the current existing blockSize
- * maximum of 128 KB, this value is actually impossible to reach.
+ * A 128 KiB block containing many 3-byte matches can have enough sequences
+ * for more splits than this limit allows.
  */
 static void
 ZSTD_deriveBlockSplitsHelper(seqStoreSplits* splits, size_t startIdx, size_t endIdx,
@@ -4254,6 +4193,8 @@ ZSTD_deriveBlockSplitsHelper(seqStoreSplits* splits, size_t startIdx, size_t end
     if (estimatedFirstHalfSize + estimatedSecondHalfSize < estimatedOriginalSize) {
         DEBUGLOG(5, "split decided at seqNb:%zu", midIdx);
         ZSTD_deriveBlockSplitsHelper(splits, startIdx, midIdx, zc, origSeqStore);
+        if (splits->idx >= ZSTD_MAX_NB_BLOCK_SPLITS)
+            return;
         splits->splitLocations[splits->idx] = (U32)midIdx;
         splits->idx++;
         ZSTD_deriveBlockSplitsHelper(splits, midIdx, endIdx, zc, origSeqStore);
@@ -4276,6 +4217,7 @@ static size_t ZSTD_deriveBlockSplits(ZSTD_CCtx* zc, U32 partitions[], U32 nbSeq)
         return 0;
     }
     ZSTD_deriveBlockSplitsHelper(&splits, 0, nbSeq, zc, &zc->seqStore);
+    assert(splits.idx <= ZSTD_MAX_NB_BLOCK_SPLITS);
     splits.splitLocations[splits.idx] = nbSeq;
     DEBUGLOG(5, "ZSTD_deriveBlockSplits: final nb partitions: %zu", splits.idx+1);
     return splits.idx;
@@ -4297,7 +4239,7 @@ ZSTD_compressBlock_splitBlock_internal(ZSTD_CCtx* zc,
     BYTE* op = (BYTE*)dst;
     size_t i = 0;
     size_t srcBytesTotal = 0;
-    U32* const partitions = zc->blockSplitCtx.partitions; /* size == ZSTD_MAX_NB_BLOCK_SPLITS */
+    U32* const partitions = zc->blockSplitCtx.partitions; /* includes the final block boundary */
     SeqStore_t* const nextSeqStore = &zc->blockSplitCtx.nextSeqStore;
     SeqStore_t* const currSeqStore = &zc->blockSplitCtx.currSeqStore;
     size_t const numSplits = ZSTD_deriveBlockSplits(zc, partitions, nbSeq);
@@ -5168,12 +5110,9 @@ size_t ZSTD_loadCEntropy(ZSTD_compressedBlockState_t* bs, void* workspace,
         /* All offset values <= dictContentSize + 128 KB must be representable for a valid table */
         bs->entropy.fse.offcode_repeatMode = ZSTD_dictNCountRepeat(offcodeNCount, offcodeMaxValue, MIN(offcodeMax, MaxOff));
 
-        /* All repCodes must be <= dictContentSize and != 0 */
-        {   U32 u;
-            for (u=0; u<3; u++) {
-                RETURN_ERROR_IF(bs->rep[u] == 0, dictionary_corrupted, "");
-                RETURN_ERROR_IF(bs->rep[u] > dictContentSize, dictionary_corrupted, "");
-    }   }   }
+        /* The format permits nonzero repcodes up to the full content size. */
+        FORWARD_IF_ERROR(ZSTD_checkDictionaryRepCodes(bs->rep, dictContentSize), "");
+    }
 
     return (size_t)(dictPtr - (const BYTE*)dict);
 }
@@ -5214,6 +5153,15 @@ static size_t ZSTD_loadZstdDictionary(ZSTD_compressedBlockState_t* bs,
         FORWARD_IF_ERROR(ZSTD_loadDictionaryContent(
             ms, NULL, ws, params, dictPtr, dictContentSize, dtlm, tfp), "");
     }
+    if (tfp == ZSTD_tfp_forCDict) {
+        /* The matchfinder may retain only a suffix of a large dictionary.
+         * In particular, short-cache CDicts retain at most (1 << 24) - 2
+         * bytes. Recheck the repcodes against the actual window so that
+         * dictMatchState searches cannot underflow their match indices. */
+        U32 const loadedDictSize = (U32)(ms->window.nextSrc - ms->window.base) - ms->window.lowLimit;
+        assert((U32)(ms->window.nextSrc - ms->window.base) >= ms->window.lowLimit);
+        FORWARD_IF_ERROR(ZSTD_checkDictionaryRepCodes(bs->rep, loadedDictSize), "");
+    }
     return dictID;
 }
 
@@ -5545,6 +5493,29 @@ size_t ZSTD_compress(void* dst, size_t dstCapacity,
 
 /* =====  Dictionary API  ===== */
 
+/* Returns the CDict workspace size, or an error if the dictionary copy would
+ * overflow while rounding, adding sanitizer redzones, or adding the workspace
+ * overhead. All by-copy CDict creation paths must use this calculation before
+ * reserving or copying the dictionary. */
+static size_t ZSTD_estimateCDictSize_internal(size_t dictSize,
+                                             size_t matchStateSize,
+                                             ZSTD_dictLoadMethod_e dictLoadMethod)
+{
+    size_t const baseSize = ZSTD_cwksp_alloc_size(sizeof(ZSTD_CDict))
+                          + ZSTD_cwksp_alloc_size(HUF_WORKSPACE_SIZE)
+                          + matchStateSize;
+    if (dictLoadMethod == ZSTD_dlm_byRef) return baseSize;
+    {
+        size_t const alignedDictSize = ZSTD_cwksp_align(dictSize, sizeof(void*));
+        size_t const dictAllocSize = ZSTD_cwksp_alloc_size(alignedDictSize);
+        RETURN_ERROR_IF(alignedDictSize < dictSize || dictAllocSize < alignedDictSize,
+                        memory_allocation, "CDict size overflow");
+        RETURN_ERROR_IF(dictAllocSize > (size_t)-1 - baseSize,
+                        memory_allocation, "CDict workspace size overflow");
+        return baseSize + dictAllocSize;
+    }
+}
+
 /*! ZSTD_estimateCDictSize_advanced() :
  *  Estimate amount of memory that will be needed to create a dictionary with following arguments */
 size_t ZSTD_estimateCDictSize_advanced(
@@ -5552,14 +5523,12 @@ size_t ZSTD_estimateCDictSize_advanced(
         ZSTD_dictLoadMethod_e dictLoadMethod)
 {
     DEBUGLOG(5, "sizeof(ZSTD_CDict) : %u", (unsigned)sizeof(ZSTD_CDict));
-    return ZSTD_cwksp_alloc_size(sizeof(ZSTD_CDict))
-         + ZSTD_cwksp_alloc_size(HUF_WORKSPACE_SIZE)
-         /* enableDedicatedDictSearch == 1 ensures that CDict estimation will not be too small
-          * in case we are using DDS with row-hash. */
-         + ZSTD_sizeof_matchState(&cParams, ZSTD_resolveRowMatchFinderMode(ZSTD_ps_auto, &cParams),
-                                  /* enableDedicatedDictSearch */ 1, /* forCCtx */ 0)
-         + (dictLoadMethod == ZSTD_dlm_byRef ? 0
-            : ZSTD_cwksp_alloc_size(ZSTD_cwksp_align(dictSize, sizeof(void *))));
+    /* enableDedicatedDictSearch == 1 ensures that CDict estimation will not be too small
+     * in case we are using DDS with row-hash. */
+    return ZSTD_estimateCDictSize_internal(dictSize,
+            ZSTD_sizeof_matchState(&cParams, ZSTD_resolveRowMatchFinderMode(ZSTD_ps_auto, &cParams),
+                                  /* enableDedicatedDictSearch */ 1, /* forCCtx */ 0),
+            dictLoadMethod);
 }
 
 size_t ZSTD_estimateCDictSize(size_t dictSize, int compressionLevel)
@@ -5641,16 +5610,15 @@ ZSTD_createCDict_advanced_internal(size_t dictSize,
     if ((!customMem.customAlloc) ^ (!customMem.customFree)) return NULL;
     DEBUGLOG(3, "ZSTD_createCDict_advanced_internal (dictSize=%u)", (unsigned)dictSize);
 
-    {   size_t const workspaceSize =
-            ZSTD_cwksp_alloc_size(sizeof(ZSTD_CDict)) +
-            ZSTD_cwksp_alloc_size(HUF_WORKSPACE_SIZE) +
-            ZSTD_sizeof_matchState(&cParams, useRowMatchFinder, enableDedicatedDictSearch, /* forCCtx */ 0) +
-            (dictLoadMethod == ZSTD_dlm_byRef ? 0
-             : ZSTD_cwksp_alloc_size(ZSTD_cwksp_align(dictSize, sizeof(void*))));
-        void* const workspace = ZSTD_customMalloc(workspaceSize, customMem);
+    {   size_t const workspaceSize = ZSTD_estimateCDictSize_internal(dictSize,
+            ZSTD_sizeof_matchState(&cParams, useRowMatchFinder, enableDedicatedDictSearch, /* forCCtx */ 0),
+            dictLoadMethod);
+        void* workspace;
         ZSTD_cwksp ws;
         ZSTD_CDict* cdict;
 
+        if (ZSTD_isError(workspaceSize)) return NULL;
+        workspace = ZSTD_customMalloc(workspaceSize, customMem);
         if (!workspace) {
             ZSTD_customFree(workspace, customMem);
             return NULL;
@@ -5686,7 +5654,10 @@ ZSTD_CDict* ZSTD_createCDict_advanced(const void* dictBuffer, size_t dictSize,
         &cctxParams, customMem);
 }
 
-ZSTD_CDict* ZSTD_createCDict_advanced2(
+/* Preserve initialization errors for internal callers. The public constructors
+ * retain their pointer-or-NULL API. On failure, *cdictPtr remains NULL. */
+static size_t ZSTD_createCDict_advanced2_internal(
+        ZSTD_CDict** cdictPtr,
         const void* dict, size_t dictSize,
         ZSTD_dictLoadMethod_e dictLoadMethod,
         ZSTD_dictContentType_e dictContentType,
@@ -5697,8 +5668,10 @@ ZSTD_CDict* ZSTD_createCDict_advanced2(
     ZSTD_compressionParameters cParams;
     ZSTD_CDict* cdict;
 
+    *cdictPtr = NULL;
     DEBUGLOG(3, "ZSTD_createCDict_advanced2, dictSize=%u, mode=%u", (unsigned)dictSize, (unsigned)dictContentType);
-    if (!customMem.customAlloc ^ !customMem.customFree) return NULL;
+    RETURN_ERROR_IF((!customMem.customAlloc) ^ (!customMem.customFree),
+                    parameter_combination_unsupported, "Invalid custom allocator");
 
     if (cctxParams.enableDedicatedDictSearch) {
         cParams = ZSTD_dedicatedDictSearch_getCParams(
@@ -5725,14 +5698,33 @@ ZSTD_CDict* ZSTD_createCDict_advanced2(
                         cctxParams.useRowMatchFinder, cctxParams.enableDedicatedDictSearch,
                         customMem);
 
-    if (!cdict || ZSTD_isError( ZSTD_initCDict_internal(cdict,
-                                    dict, dictSize,
-                                    dictLoadMethod, dictContentType,
-                                    cctxParams) )) {
-        ZSTD_freeCDict(cdict);
-        return NULL;
+    RETURN_ERROR_IF(!cdict, memory_allocation, "Could not allocate CDict");
+    {   size_t const err = ZSTD_initCDict_internal(cdict,
+                                dict, dictSize,
+                                dictLoadMethod, dictContentType,
+                                cctxParams);
+        if (ZSTD_isError(err)) {
+            ZSTD_freeCDict(cdict);
+            return err;
+        }
     }
 
+    *cdictPtr = cdict;
+    return 0;
+}
+
+ZSTD_CDict* ZSTD_createCDict_advanced2(
+        const void* dict, size_t dictSize,
+        ZSTD_dictLoadMethod_e dictLoadMethod,
+        ZSTD_dictContentType_e dictContentType,
+        const ZSTD_CCtx_params* originalCctxParams,
+        ZSTD_customMem customMem)
+{
+    ZSTD_CDict* cdict;
+    size_t const err = ZSTD_createCDict_advanced2_internal(
+            &cdict, dict, dictSize, dictLoadMethod, dictContentType,
+            originalCctxParams, customMem);
+    if (ZSTD_isError(err)) return NULL;
     return cdict;
 }
 
@@ -5794,16 +5786,13 @@ const ZSTD_CDict* ZSTD_initStaticCDict(
     ZSTD_ParamSwitch_e const useRowMatchFinder = ZSTD_resolveRowMatchFinderMode(ZSTD_ps_auto, &cParams);
     /* enableDedicatedDictSearch == 1 ensures matchstate is not too small in case this CDict will be used for DDS + row hash */
     size_t const matchStateSize = ZSTD_sizeof_matchState(&cParams, useRowMatchFinder, /* enableDedicatedDictSearch */ 1, /* forCCtx */ 0);
-    size_t const neededSize = ZSTD_cwksp_alloc_size(sizeof(ZSTD_CDict))
-                            + (dictLoadMethod == ZSTD_dlm_byRef ? 0
-                               : ZSTD_cwksp_alloc_size(ZSTD_cwksp_align(dictSize, sizeof(void*))))
-                            + ZSTD_cwksp_alloc_size(HUF_WORKSPACE_SIZE)
-                            + matchStateSize;
+    size_t const neededSize = ZSTD_estimateCDictSize_internal(dictSize, matchStateSize, dictLoadMethod);
     ZSTD_CDict* cdict;
     ZSTD_CCtx_params params;
 
     DEBUGLOG(4, "ZSTD_initStaticCDict (dictSize==%u)", (unsigned)dictSize);
     if ((size_t)workspace & 7) return NULL;  /* 8-aligned */
+    if (ZSTD_isError(neededSize) || workspaceSize < neededSize) return NULL;
 
     {
         ZSTD_cwksp ws;
@@ -5813,8 +5802,6 @@ const ZSTD_CDict* ZSTD_initStaticCDict(
         ZSTD_cwksp_move(&cdict->workspace, &ws);
     }
 
-    if (workspaceSize < neededSize) return NULL;
-
     ZSTD_CCtxParams_init(¶ms, 0);
     params.cParams = cParams;
     params.useRowMatchFinder = useRowMatchFinder;
@@ -6489,14 +6476,15 @@ size_t ZSTD_compressStream2( ZSTD_CCtx* cctx,
     if (cctx->streamStage == zcss_init) {
         size_t const inputSize = input->size - input->pos;  /* no obligation to start from pos==0 */
         size_t const totalInputSize = inputSize + cctx->stableIn_notConsumed;
+        if (cctx->stableIn_notConsumed) {  /* not the first time */
+            /* check stable source guarantees, including when this call starts compression */
+            RETURN_ERROR_IF(cctx->requestedParams.inBufferMode != ZSTD_bm_stable, stabilityCondition_notRespected, "stableInBuffer condition not respected: disabled while input is pending");
+            RETURN_ERROR_IF(input->src != cctx->expectedInBuffer.src, stabilityCondition_notRespected, "stableInBuffer condition not respected: wrong src pointer");
+            RETURN_ERROR_IF(input->pos != cctx->expectedInBuffer.size, stabilityCondition_notRespected, "stableInBuffer condition not respected: externally modified pos");
+        }
         if ( (cctx->requestedParams.inBufferMode == ZSTD_bm_stable) /* input is presumed stable, across invocations */
           && (endOp == ZSTD_e_continue)                             /* no flush requested, more input to come */
           && (totalInputSize < ZSTD_BLOCKSIZE_MAX) ) {              /* not even reached one block yet */
-            if (cctx->stableIn_notConsumed) {  /* not the first time */
-                /* check stable source guarantees */
-                RETURN_ERROR_IF(input->src != cctx->expectedInBuffer.src, stabilityCondition_notRespected, "stableInBuffer condition not respected: wrong src pointer");
-                RETURN_ERROR_IF(input->pos != cctx->expectedInBuffer.size, stabilityCondition_notRespected, "stableInBuffer condition not respected: externally modified pos");
-            }
             /* pretend input was consumed, to give a sense forward progress */
             input->pos = input->size;
             /* save stable inBuffer, for later control, and flush/end */
@@ -8038,6 +8026,7 @@ ZSTD_compressSequencesAndLiterals_internal(ZSTD_CCtx* cctx,
         FORWARD_IF_ERROR(block.nbSequences, "Error while trying to determine nb of sequences for a block");
         assert(block.nbSequences <= nbSequences);
         RETURN_ERROR_IF(block.litSize > litSize, externalSequences_invalid, "discrepancy: Sequences require more literals than present in buffer");
+        RETURN_ERROR_IF(block.blockSize > cctx->blockSizeMax, externalSequences_invalid, "sequences incorrectly define a too large block");
         ZSTD_resetSeqStore(&cctx->seqStore);
 
         conversionStatus = ZSTD_convertBlockSequences(cctx,
@@ -8126,7 +8115,7 @@ ZSTD_compressSequencesAndLiterals(ZSTD_CCtx* cctx,
     /* Transparent initialization stage, same as compressStream2() */
     DEBUGLOG(4, "ZSTD_compressSequencesAndLiterals (dstCapacity=%zu)", dstCapacity);
     assert(cctx != NULL);
-    if (litCapacity < litSize) {
+    if (litCapacity < litSize || litCapacity - litSize < 8) {
         RETURN_ERROR(workSpace_tooSmall, "literals buffer is not large enough: must be at least 8 bytes larger than litSize (risk of read out-of-bound)");
     }
     FORWARD_IF_ERROR(ZSTD_CCtx_init_compressStream2(cctx, ZSTD_e_end, decompressedSize), "CCtx initialization failed");
@@ -8170,7 +8159,8 @@ ZSTD_compressSequencesAndLiterals(ZSTD_CCtx* cctx,
 static ZSTD_inBuffer inBuffer_forEndFlush(const ZSTD_CStream* zcs)
 {
     const ZSTD_inBuffer nullInput = { NULL, 0, 0 };
-    const int stableInput = (zcs->appliedParams.inBufferMode == ZSTD_bm_stable);
+    const int stableInput = (zcs->appliedParams.inBufferMode == ZSTD_bm_stable)
+                         || (zcs->stableIn_notConsumed > 0);  /* pending input, parameters not applied yet */
     return stableInput ? zcs->expectedInBuffer : nullInput;
 }
 
diff --git a/lib/compress/zstd_compress_internal.h b/lib/compress/zstd_compress_internal.h
index c15a94da3b5..afebbdae33d 100644
--- a/lib/compress/zstd_compress_internal.h
+++ b/lib/compress/zstd_compress_internal.h
@@ -465,7 +465,7 @@ typedef struct {
     SeqStore_t currSeqStore;
     SeqStore_t nextSeqStore;
 
-    U32 partitions[ZSTD_MAX_NB_BLOCK_SPLITS];
+    U32 partitions[ZSTD_MAX_NB_BLOCK_SPLITS + 1]; /* splits, followed by the end of the block */
     ZSTD_entropyCTablesMetadata_t entropyMetadata;
 } ZSTD_blockSplitCtx;
 
diff --git a/lib/compress/zstd_double_fast.c b/lib/compress/zstd_double_fast.c
index 1a266e7d955..568df6f7204 100644
--- a/lib/compress/zstd_double_fast.c
+++ b/lib/compress/zstd_double_fast.c
@@ -375,8 +375,8 @@ size_t ZSTD_compressBlock_doubleFast_dictMatchState_generic(
     /* init */
     ip += (dictAndPrefixLength == 0);
 
-    /* dictMatchState repCode checks don't currently handle repCode == 0
-     * disabling. */
+    /* Dictionary repcodes are validated when the CDict is attached.
+     * dictMatchState repCode checks don't handle repCode == 0 disabling. */
     assert(offset_1 <= dictAndPrefixLength);
     assert(offset_2 <= dictAndPrefixLength);
 
diff --git a/lib/compress/zstd_fast.c b/lib/compress/zstd_fast.c
index ee25bcbac8d..8130fc961ff 100644
--- a/lib/compress/zstd_fast.c
+++ b/lib/compress/zstd_fast.c
@@ -532,8 +532,8 @@ size_t ZSTD_compressBlock_fast_dictMatchState_generic(
     /* init */
     DEBUGLOG(5, "ZSTD_compressBlock_fast_dictMatchState_generic");
     ip0 += (dictAndPrefixLength == 0);
-    /* dictMatchState repCode checks don't currently handle repCode == 0
-     * disabling. */
+    /* Dictionary repcodes are validated when the CDict is attached.
+     * dictMatchState repCode checks don't handle repCode == 0 disabling. */
     assert(offset_1 <= dictAndPrefixLength);
     assert(offset_2 <= dictAndPrefixLength);
 
diff --git a/lib/compress/zstd_lazy.c b/lib/compress/zstd_lazy.c
index 18b7b43948f..03c9992d0db 100644
--- a/lib/compress/zstd_lazy.c
+++ b/lib/compress/zstd_lazy.c
@@ -1601,8 +1601,8 @@ size_t ZSTD_compressBlock_lazy_generic(
         if (offset_1 > maxRep) offsetSaved1 = offset_1, offset_1 = 0;
     }
     if (isDxS) {
-        /* dictMatchState repCode checks don't currently handle repCode == 0
-         * disabling. */
+        /* Dictionary repcodes are validated when the CDict is attached.
+         * dictMatchState repCode checks don't handle repCode == 0 disabling. */
         assert(offset_1 <= dictAndPrefixLength);
         assert(offset_2 <= dictAndPrefixLength);
     }
diff --git a/lib/compress/zstd_preSplit.c b/lib/compress/zstd_preSplit.c
index d820c20ac24..67ab89f02ce 100644
--- a/lib/compress/zstd_preSplit.c
+++ b/lib/compress/zstd_preSplit.c
@@ -90,7 +90,7 @@ ZSTD_GEN_RECORD_FINGERPRINT(11, 9)
 ZSTD_GEN_RECORD_FINGERPRINT(43, 8)
 
 
-static U64 abs64(S64 s64) { return (U64)((s64 < 0) ? -s64 : s64); }
+static U64 ZSTD_abs64(S64 s64) { return (U64)((s64 < 0) ? -s64 : s64); }
 
 static U64 fpDistance(const Fingerprint* fp1, const Fingerprint* fp2, unsigned hashLog)
 {
@@ -99,7 +99,7 @@ static U64 fpDistance(const Fingerprint* fp1, const Fingerprint* fp2, unsigned h
     assert(hashLog <= HASHLOG_MAX);
     for (n = 0; n < ((size_t)1 << hashLog); n++) {
         distance +=
-            abs64((S64)fp1->events[n] * (S64)fp2->nbEvents - (S64)fp2->events[n] * (S64)fp1->nbEvents);
+            ZSTD_abs64((S64)fp1->events[n] * (S64)fp2->nbEvents - (S64)fp2->events[n] * (S64)fp1->nbEvents);
     }
     return distance;
 }
@@ -219,7 +219,7 @@ static size_t ZSTD_splitBlock_fromBorders(const void* blockStart, size_t blockSi
     {   U64 const distFromBegin = fpDistance(&fpstats->pastEvents, middleEvents, 8);
         U64 const distFromEnd = fpDistance(&fpstats->newEvents, middleEvents, 8);
         U64 const minDistance = SEGMENT_SIZE * SEGMENT_SIZE / 3;
-        if (abs64((S64)distFromBegin - (S64)distFromEnd) < minDistance)
+        if (ZSTD_abs64((S64)distFromBegin - (S64)distFromEnd) < minDistance)
             return 64 KB;
         return (distFromBegin > distFromEnd) ? 32 KB : 96 KB;
     }
diff --git a/lib/compress/zstdmt_compress.c b/lib/compress/zstdmt_compress.c
index cb710475c2a..5d94525dfeb 100644
--- a/lib/compress/zstdmt_compress.c
+++ b/lib/compress/zstdmt_compress.c
@@ -662,6 +662,7 @@ static const Range kNullRange = { NULL, 0 };
 typedef struct {
     size_t   consumed;                 /* SHARED - set0 by mtctx, then modified by worker AND read by mtctx */
     size_t   cSize;                    /* SHARED - set0 by mtctx, then modified by worker AND read by mtctx, then set0 by mtctx */
+    unsigned completed;                /* SHARED - set0 by mtctx, then set1 by worker when it no longer uses the job */
     ZSTD_pthread_mutex_t job_mutex;    /* Thread-safe - used by mtctx and worker */
     ZSTD_pthread_cond_t job_cond;      /* Thread-safe - used by mtctx and worker */
     ZSTDMT_CCtxPool* cctxPool;         /* Thread-safe - used by mtctx and (all) workers */
@@ -814,7 +815,8 @@ static void ZSTDMT_compressionJob(void* jobDescription)
     ZSTD_PTHREAD_MUTEX_LOCK(&job->job_mutex);
     if (ZSTD_isError(job->cSize)) assert(lastCBlockSize == 0);
     job->cSize += lastCBlockSize;
-    job->consumed = job->src.size;  /* when job->consumed == job->src.size , compression job is presumed completed */
+    job->consumed = job->src.size;
+    job->completed = 1;   /* also true for an empty job, unlike consumed == src.size */
     ZSTD_pthread_cond_signal(&job->job_cond);
     ZSTD_pthread_mutex_unlock(&job->job_mutex);
 }
@@ -1025,6 +1027,7 @@ static void ZSTDMT_releaseAllJobResources(ZSTDMT_CCtx* mtctx)
     }
     mtctx->inBuff.buffer = g_nullBuffer;
     mtctx->inBuff.filled = 0;
+    mtctx->jobReady = 0;   /* the prepared job was cleared above */
     mtctx->allJobsCompleted = 1;
 }
 
@@ -1034,7 +1037,7 @@ static void ZSTDMT_waitForAllJobsCompleted(ZSTDMT_CCtx* mtctx)
     while (mtctx->doneJobID < mtctx->nextJobID) {
         unsigned const jobID = mtctx->doneJobID & mtctx->jobIDMask;
         ZSTD_PTHREAD_MUTEX_LOCK(&mtctx->jobs[jobID].job_mutex);
-        while (mtctx->jobs[jobID].consumed < mtctx->jobs[jobID].src.size) {
+        while (!mtctx->jobs[jobID].completed) {
             DEBUGLOG(4, "waiting for jobCompleted signal from job %u", mtctx->doneJobID);   /* we want to block when waiting for data to flush */
             ZSTD_pthread_cond_wait(&mtctx->jobs[jobID].job_cond, &mtctx->jobs[jobID].job_mutex);
         }
@@ -1043,9 +1046,18 @@ static void ZSTDMT_waitForAllJobsCompleted(ZSTDMT_CCtx* mtctx)
     }
 }
 
+void ZSTDMT_waitForPendingJobs(ZSTDMT_CCtx* mtctx)
+{
+    if (mtctx->allJobsCompleted == 0) {   /* previous compression not correctly finished */
+        ZSTDMT_waitForAllJobsCompleted(mtctx);
+        ZSTDMT_releaseAllJobResources(mtctx); /* Will set allJobsCompleted to 1 */
+    }
+}
+
 size_t ZSTDMT_freeCCtx(ZSTDMT_CCtx* mtctx)
 {
     if (mtctx==NULL) return 0;   /* compatible with free on NULL */
+    ZSTDMT_waitForPendingJobs(mtctx);  /* a shared pool is not stopped below */
     if (!mtctx->providedFactory)
         POOL_free(mtctx->factory);   /* stop and free worker threads */
     ZSTDMT_releaseAllJobResources(mtctx);  /* release job resources into pools first */
@@ -1079,7 +1091,9 @@ size_t ZSTDMT_sizeof_CCtx(ZSTDMT_CCtx* mtctx)
  * @return : error code if fails, 0 on success */
 static size_t ZSTDMT_resize(ZSTDMT_CCtx* mtctx, unsigned nbWorkers)
 {
-    if (POOL_resize(mtctx->factory, nbWorkers)) return ERROR(memory_allocation);
+    if (!mtctx->providedFactory) {   /* a pool provided by the application keeps its size */
+        if (POOL_resize(mtctx->factory, nbWorkers)) return ERROR(memory_allocation);
+    }
     FORWARD_IF_ERROR( ZSTDMT_expandJobsTable(mtctx, nbWorkers) , "");
     mtctx->bufPool = ZSTDMT_expandBufferPool(mtctx->bufPool, BUF_POOL_MAX_NB_BUFFERS(nbWorkers));
     if (mtctx->bufPool == NULL) return ERROR(memory_allocation);
@@ -1262,17 +1276,13 @@ size_t ZSTDMT_initCStream_internal(
     assert(!((dict) && (cdict)));  /* either dict or cdict, not both */
 
     /* init */
+    ZSTDMT_waitForPendingJobs(mtctx);   /* before resizing the pools they use */
     if (params.nbWorkers != mtctx->params.nbWorkers)
         FORWARD_IF_ERROR( ZSTDMT_resize(mtctx, (unsigned)params.nbWorkers) , "");
 
     if (params.jobSize != 0 && params.jobSize < ZSTDMT_JOBSIZE_MIN) params.jobSize = ZSTDMT_JOBSIZE_MIN;
     if (params.jobSize > (size_t)ZSTDMT_JOBSIZE_MAX) params.jobSize = (size_t)ZSTDMT_JOBSIZE_MAX;
 
-    if (mtctx->allJobsCompleted == 0) {   /* previous compression not correctly finished */
-        ZSTDMT_waitForAllJobsCompleted(mtctx);
-        ZSTDMT_releaseAllJobResources(mtctx); /* Will set allJobsCompleted to 1 */
-    }
-
     mtctx->params = params;
     mtctx->frameContentSize = pledgedSrcSize;
     ZSTD_freeCDict(mtctx->cdictLocal);
@@ -1321,11 +1331,19 @@ size_t ZSTDMT_initCStream_internal(
          * with the LDM window.
          */
         size_t const nbSlackBuffers = 2 + (mtctx->targetPrefixSize > 0);
-        size_t const slackSize = mtctx->targetSectionSize * nbSlackBuffers;
+        U64 const slackSize = (U64)mtctx->targetSectionSize * nbSlackBuffers;
         /* Compute the total size, and always have enough slack */
-        size_t const nbWorkers = MAX(mtctx->params.nbWorkers, 1);
-        size_t const sectionsSize = mtctx->targetSectionSize * nbWorkers;
-        size_t const capacity = MAX(windowSize, sectionsSize) + slackSize;
+        U64 const nbWorkers = MAX(mtctx->params.nbWorkers, 1);
+        U64 const sectionsSize = (U64)mtctx->targetSectionSize * nbWorkers;
+        U64 const capacity64 = MAX((U64)windowSize, sectionsSize) + slackSize;
+        size_t capacity;
+        /* The totals are computed in 64 bits because on 32-bit builds they
+         * can exceed the range of size_t. They would wrap, and the round
+         * buffer would be too small (or not allocated at all), letting
+         * input be copied past its end. Reject such sizes instead. */
+        capacity = (size_t)capacity64;
+        RETURN_ERROR_IF((U64)capacity != capacity64,
+                        memory_allocation, "round buffer size overflow");
         if (mtctx->roundBuff.capacity < capacity) {
             if (mtctx->roundBuff.buffer)
                 ZSTD_customFree(mtctx->roundBuff.buffer, mtctx->cMem);
@@ -1389,6 +1407,7 @@ static void ZSTDMT_writeLastEmptyBlock(ZSTDMT_jobDescription* job)
     assert(job->src.size == 0);   /* last job is empty -> will be simplified into a last empty block */
     assert(job->firstJob == 0);   /* cannot be first job, as it also needs to create frame header */
     assert(job->dstBuff.start == NULL);   /* invoked from streaming variant only (otherwise, dstBuff might be user's output) */
+    job->completed = 1;   /* written here, never posted to a worker */
     job->dstBuff = ZSTDMT_getBuffer(job->bufPool);
     if (job->dstBuff.start == NULL) {
       job->cSize = ERROR(memory_allocation);
@@ -1422,6 +1441,7 @@ static size_t ZSTDMT_createCompressionJob(ZSTDMT_CCtx* mtctx, size_t srcSize, ZS
         mtctx->jobs[jobID].prefix = mtctx->inBuff.prefix;
         mtctx->jobs[jobID].consumed = 0;
         mtctx->jobs[jobID].cSize = 0;
+        mtctx->jobs[jobID].completed = 0;
         mtctx->jobs[jobID].params = mtctx->params;
         mtctx->jobs[jobID].cdict = mtctx->nextJobID==0 ? mtctx->cdict : NULL;
         mtctx->jobs[jobID].fullFrameSize = mtctx->frameContentSize;
diff --git a/lib/compress/zstdmt_compress.h b/lib/compress/zstdmt_compress.h
index 91b489b9cb4..eee96122f57 100644
--- a/lib/compress/zstdmt_compress.h
+++ b/lib/compress/zstdmt_compress.h
@@ -32,8 +32,8 @@
 #ifndef ZSTDMT_JOBSIZE_MIN   /* a different value can be selected at compile time */
 #  define ZSTDMT_JOBSIZE_MIN (512 KB)
 #endif
-#define ZSTDMT_JOBLOG_MAX   (MEM_32bits() ? 29 : 30)
-#define ZSTDMT_JOBSIZE_MAX  (MEM_32bits() ? (512 MB) : (1024 MB))
+#define ZSTDMT_JOBLOG_MAX   (MEM_32bits() ? 28 : 30)
+#define ZSTDMT_JOBSIZE_MAX  (MEM_32bits() ? (256 MB) : (1024 MB))
 
 
 /* ========================================================
@@ -49,6 +49,12 @@ ZSTDMT_CCtx* ZSTDMT_createCCtx_advanced(unsigned nbWorkers,
 					ZSTD_threadPool *pool);
 size_t ZSTDMT_freeCCtx(ZSTDMT_CCtx* mtctx);
 
+/*! ZSTDMT_waitForPendingJobs() :
+ *  Waits for the jobs of an unfinished frame to complete, then releases their resources.
+ *  Must be called before freeing anything these jobs reference, such as the dictionary.
+ *  Does nothing if the last frame was completed. */
+void ZSTDMT_waitForPendingJobs(ZSTDMT_CCtx* mtctx);
+
 size_t ZSTDMT_sizeof_CCtx(ZSTDMT_CCtx* mtctx);
 
 /* ===   Streaming functions   === */
diff --git a/lib/decompress/zstd_ddict.c b/lib/decompress/zstd_ddict.c
index 309ec0d0364..c7a49fe1716 100644
--- a/lib/decompress/zstd_ddict.c
+++ b/lib/decompress/zstd_ddict.c
@@ -63,7 +63,10 @@ void ZSTD_copyDDictParameters(ZSTD_DCtx* dctx, const ZSTD_DDict* ddict)
     dctx->dictID = ddict->dictID;
     dctx->prefixStart = ddict->dictContent;
     dctx->virtualStart = ddict->dictContent;
-    dctx->dictEnd = (const BYTE*)ddict->dictContent + ddict->dictSize;
+    /* An empty DDict may have NULL content. */
+    dctx->dictEnd = ddict->dictSize != 0
+            ? (const BYTE*)ddict->dictContent + ddict->dictSize
+            : ddict->dictContent;
     dctx->previousDstEnd = dctx->dictEnd;
 #ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION
     dctx->dictContentBeginForFuzzing = dctx->prefixStart;
@@ -142,25 +145,41 @@ static size_t ZSTD_initDDict_internal(ZSTD_DDict* ddict,
     return 0;
 }
 
+size_t ZSTD_createDDict_advanced_internal(ZSTD_DDict** ddictPtr,
+                                          const void* dict, size_t dictSize,
+                                          ZSTD_dictLoadMethod_e dictLoadMethod,
+                                          ZSTD_dictContentType_e dictContentType,
+                                          ZSTD_customMem customMem)
+{
+    ZSTD_DDict* ddict;
+    *ddictPtr = NULL;
+    RETURN_ERROR_IF((!customMem.customAlloc) ^ (!customMem.customFree),
+                    parameter_combination_unsupported, "Invalid custom allocator");
+
+    ddict = (ZSTD_DDict*) ZSTD_customMalloc(sizeof(ZSTD_DDict), customMem);
+    RETURN_ERROR_IF(ddict == NULL, memory_allocation, "");
+    ddict->cMem = customMem;
+    {   size_t const initResult = ZSTD_initDDict_internal(ddict,
+                                        dict, dictSize,
+                                        dictLoadMethod, dictContentType);
+        if (ZSTD_isError(initResult)) {
+            ZSTD_freeDDict(ddict);
+            return initResult;
+    }   }
+    *ddictPtr = ddict;
+    return 0;
+}
+
 ZSTD_DDict* ZSTD_createDDict_advanced(const void* dict, size_t dictSize,
                                       ZSTD_dictLoadMethod_e dictLoadMethod,
                                       ZSTD_dictContentType_e dictContentType,
                                       ZSTD_customMem customMem)
 {
-    if ((!customMem.customAlloc) ^ (!customMem.customFree)) return NULL;
-
-    {   ZSTD_DDict* const ddict = (ZSTD_DDict*) ZSTD_customMalloc(sizeof(ZSTD_DDict), customMem);
-        if (ddict == NULL) return NULL;
-        ddict->cMem = customMem;
-        {   size_t const initResult = ZSTD_initDDict_internal(ddict,
-                                            dict, dictSize,
-                                            dictLoadMethod, dictContentType);
-            if (ZSTD_isError(initResult)) {
-                ZSTD_freeDDict(ddict);
-                return NULL;
-        }   }
-        return ddict;
-    }
+    ZSTD_DDict* ddict;
+    size_t const err = ZSTD_createDDict_advanced_internal(&ddict,
+                            dict, dictSize, dictLoadMethod, dictContentType, customMem);
+    if (ZSTD_isError(err)) return NULL;
+    return ddict;
 }
 
 /*! ZSTD_createDDict() :
@@ -190,13 +209,12 @@ const ZSTD_DDict* ZSTD_initStaticDDict(
                                 ZSTD_dictLoadMethod_e dictLoadMethod,
                                 ZSTD_dictContentType_e dictContentType)
 {
-    size_t const neededSpace = sizeof(ZSTD_DDict)
-                             + (dictLoadMethod == ZSTD_dlm_byRef ? 0 : dictSize);
+    size_t const neededSpace = ZSTD_estimateDDictSize(dictSize, dictLoadMethod);
     ZSTD_DDict* const ddict = (ZSTD_DDict*)sBuffer;
     assert(sBuffer != NULL);
     assert(dict != NULL);
     if ((size_t)sBuffer & 7) return NULL;   /* 8-aligned */
-    if (sBufferSize < neededSpace) return NULL;
+    if (ZSTD_isError(neededSpace) || sBufferSize < neededSpace) return NULL;
     if (dictLoadMethod == ZSTD_dlm_byCopy) {
         ZSTD_memcpy(ddict+1, dict, dictSize);  /* local copy */
         dict = ddict+1;
@@ -224,7 +242,10 @@ size_t ZSTD_freeDDict(ZSTD_DDict* ddict)
  *  Note : dictionary created by reference using ZSTD_dlm_byRef are smaller */
 size_t ZSTD_estimateDDictSize(size_t dictSize, ZSTD_dictLoadMethod_e dictLoadMethod)
 {
-    return sizeof(ZSTD_DDict) + (dictLoadMethod == ZSTD_dlm_byRef ? 0 : dictSize);
+    if (dictLoadMethod == ZSTD_dlm_byRef) return sizeof(ZSTD_DDict);
+    RETURN_ERROR_IF(dictSize > (size_t)-1 - sizeof(ZSTD_DDict),
+                    memory_allocation, "DDict size overflow");
+    return sizeof(ZSTD_DDict) + dictSize;
 }
 
 size_t ZSTD_sizeof_DDict(const ZSTD_DDict* ddict)
diff --git a/lib/decompress/zstd_ddict.h b/lib/decompress/zstd_ddict.h
index c4ca8877a07..c9295ec1051 100644
--- a/lib/decompress/zstd_ddict.h
+++ b/lib/decompress/zstd_ddict.h
@@ -39,6 +39,14 @@ size_t ZSTD_DDict_dictSize(const ZSTD_DDict* ddict);
 
 void ZSTD_copyDDictParameters(ZSTD_DCtx* dctx, const ZSTD_DDict* ddict);
 
+/* Same as ZSTD_createDDict_advanced(), but returns the error code on failure,
+ * in which case *ddictPtr is set to NULL. */
+size_t ZSTD_createDDict_advanced_internal(ZSTD_DDict** ddictPtr,
+                                          const void* dict, size_t dictSize,
+                                          ZSTD_dictLoadMethod_e dictLoadMethod,
+                                          ZSTD_dictContentType_e dictContentType,
+                                          ZSTD_customMem customMem);
+
 
 
 #endif /* ZSTD_DDICT_H */
diff --git a/lib/decompress/zstd_decompress.c b/lib/decompress/zstd_decompress.c
index 325f0b54dfc..2a69ef11cb2 100644
--- a/lib/decompress/zstd_decompress.c
+++ b/lib/decompress/zstd_decompress.c
@@ -83,7 +83,7 @@
 #define DDICT_HASHSET_MAX_LOAD_FACTOR_COUNT_MULT 4
 #define DDICT_HASHSET_MAX_LOAD_FACTOR_SIZE_MULT 3  /* These two constants represent SIZE_MULT/COUNT_MULT load factor without using a float.
                                                     * Currently, that means a 0.75 load factor.
-                                                    * So, if count * COUNT_MULT / size * SIZE_MULT != 0, then we've exceeded
+                                                    * So, if count * COUNT_MULT >= size * SIZE_MULT, then we've reached
                                                     * the load factor of the ddict hash set.
                                                     */
 
@@ -153,18 +153,15 @@ static size_t ZSTD_DDictHashSet_expand(ZSTD_DDictHashSet* hashSet, ZSTD_customMe
 /* Fetches a DDict with the given dictID
  * Returns the ZSTD_DDict* with the requested dictID. If it doesn't exist, then returns NULL.
  */
-static const ZSTD_DDict* ZSTD_DDictHashSet_getDDict(ZSTD_DDictHashSet* hashSet, U32 dictID) {
+static const ZSTD_DDict* ZSTD_DDictHashSet_getDDict(const ZSTD_DDictHashSet* hashSet, U32 dictID) {
     size_t idx = ZSTD_DDictHashSet_getIndex(hashSet, dictID);
     const size_t idxRangeMask = hashSet->ddictPtrTableSize - 1;
     DEBUGLOG(4, "Hashed index: for dictID: %u is %zu", dictID, idx);
-    for (;;) {
-        size_t currDictID = ZSTD_getDictID_fromDDict(hashSet->ddictPtrTable[idx]);
-        if (currDictID == dictID || currDictID == 0) {
-            /* currDictID == 0 implies a NULL ddict entry */
+    while (hashSet->ddictPtrTable[idx] != NULL) {
+        if (ZSTD_getDictID_fromDDict(hashSet->ddictPtrTable[idx]) == dictID)
             break;
-        } else {
-            idx = (idx + 1) & idxRangeMask;    /* Goes to start of table when we reach the end */
-        }
+        /* ID 0 is valid for raw-content dictionaries, not an empty slot. */
+        idx = (idx + 1) & idxRangeMask;
     }
     DEBUGLOG(4, "Final idx after probing for dictID %u is: %zu", dictID, idx);
     return hashSet->ddictPtrTable[idx];
@@ -207,7 +204,7 @@ static void ZSTD_freeDDictHashSet(ZSTD_DDictHashSet* hashSet, ZSTD_customMem cus
  */
 static size_t ZSTD_DDictHashSet_addDDict(ZSTD_DDictHashSet* hashSet, const ZSTD_DDict* ddict, ZSTD_customMem customMem) {
     DEBUGLOG(4, "Adding dict ID: %u to hashset with - Count: %zu Tablesize: %zu", ZSTD_getDictID_fromDDict(ddict), hashSet->ddictPtrCount, hashSet->ddictPtrTableSize);
-    if (hashSet->ddictPtrCount * DDICT_HASHSET_MAX_LOAD_FACTOR_COUNT_MULT / hashSet->ddictPtrTableSize * DDICT_HASHSET_MAX_LOAD_FACTOR_SIZE_MULT != 0) {
+    if (hashSet->ddictPtrCount * DDICT_HASHSET_MAX_LOAD_FACTOR_COUNT_MULT >= hashSet->ddictPtrTableSize * DDICT_HASHSET_MAX_LOAD_FACTOR_SIZE_MULT) {
         FORWARD_IF_ERROR(ZSTD_DDictHashSet_expand(hashSet, customMem), "");
     }
     FORWARD_IF_ERROR(ZSTD_DDictHashSet_emplaceDDict(hashSet, ddict), "");
@@ -248,8 +245,9 @@ static void ZSTD_DCtx_resetParameters(ZSTD_DCtx* dctx)
     dctx->maxBlockSizeParam = 0;
 }
 
-static void ZSTD_initDCtx_internal(ZSTD_DCtx* dctx)
+static void ZSTD_initDCtx_internal(ZSTD_DCtx* dctx, ZSTD_customMem customMem)
 {
+    dctx->customMem   = customMem;
     dctx->staticSize  = 0;
     dctx->ddict       = NULL;
     dctx->ddictLocal  = NULL;
@@ -260,6 +258,8 @@ static void ZSTD_initDCtx_internal(ZSTD_DCtx* dctx)
     dctx->inBuffSize  = 0;
     dctx->outBuffSize = 0;
     dctx->streamStage = zdss_init;
+    dctx->stage       = ZSTDds_getFrameHeaderSize;
+    dctx->expected    = 0;   /* ZSTD_decompressContinue() requires ZSTD_decompressBegin() first */
 #if defined(ZSTD_LEGACY_SUPPORT) && (ZSTD_LEGACY_SUPPORT>=1)
     dctx->legacyContext = NULL;
     dctx->previousLegacyVersion = 0;
@@ -284,7 +284,7 @@ ZSTD_DCtx* ZSTD_initStaticDCtx(void *workspace, size_t workspaceSize)
     if ((size_t)workspace & 7) return NULL;  /* 8-aligned */
     if (workspaceSize < sizeof(ZSTD_DCtx)) return NULL;  /* minimum size */
 
-    ZSTD_initDCtx_internal(dctx);
+    ZSTD_initDCtx_internal(dctx, ZSTD_defaultCMem);
     dctx->staticSize = workspaceSize;
     dctx->inBuff = (char*)(dctx+1);
     return dctx;
@@ -295,8 +295,7 @@ static ZSTD_DCtx* ZSTD_createDCtx_internal(ZSTD_customMem customMem) {
 
     {   ZSTD_DCtx* const dctx = (ZSTD_DCtx*)ZSTD_customMalloc(sizeof(*dctx), customMem);
         if (!dctx) return NULL;
-        dctx->customMem = customMem;
-        ZSTD_initDCtx_internal(dctx);
+        ZSTD_initDCtx_internal(dctx, customMem);
         return dctx;
     }
 }
@@ -320,6 +319,16 @@ static void ZSTD_clearDict(ZSTD_DCtx* dctx)
     dctx->dictUses = ZSTD_dont_use;
 }
 
+/* Discard borrowed dictionary references without freeing the DDicts. */
+static void ZSTD_clearDDictSet(ZSTD_DCtx* dctx)
+{
+    if (dctx->ddictSet != NULL) {
+        assert(!dctx->staticSize);
+        ZSTD_freeDDictHashSet(dctx->ddictSet, dctx->customMem);
+        dctx->ddictSet = NULL;
+    }
+}
+
 size_t ZSTD_freeDCtx(ZSTD_DCtx* dctx)
 {
     if (dctx==NULL) return 0;   /* support free on NULL */
@@ -332,43 +341,34 @@ size_t ZSTD_freeDCtx(ZSTD_DCtx* dctx)
         if (dctx->legacyContext)
             ZSTD_freeLegacyStreamContext(dctx->legacyContext, dctx->previousLegacyVersion);
 #endif
-        if (dctx->ddictSet) {
-            ZSTD_freeDDictHashSet(dctx->ddictSet, cMem);
-            dctx->ddictSet = NULL;
-        }
+        ZSTD_clearDDictSet(dctx);
         ZSTD_customFree(dctx, cMem);
         return 0;
     }
 }
 
-/* no longer useful */
-void ZSTD_copyDCtx(ZSTD_DCtx* dstDCtx, const ZSTD_DCtx* srcDCtx)
-{
-    size_t const toCopy = (size_t)((char*)(&dstDCtx->inBuff) - (char*)dstDCtx);
-    ZSTD_memcpy(dstDCtx, srcDCtx, toCopy);  /* no need to copy workspace */
-}
-
-/* Given a dctx with a digested frame params, re-selects the correct ZSTD_DDict based on
- * the requested dict ID from the frame. If there exists a reference to the correct ZSTD_DDict, then
- * accordingly sets the ddict to be used to decompress the frame.
+/* Look up the DDict for a complete, valid Zstandard frame header.
+ * The result is for this frame only; it does not change the single-DDict state.
  *
- * If no DDict is found, then no action is taken, and the ZSTD_DCtx::ddict remains as-is.
+ * Returns dictionary_wrong if the set does not contain a requested nonzero ID.
+ * For ID 0, returns the registered DDict, or NULL to use no dictionary.
  *
+ * Must be called before installing the dictionary's decoding state.
  * ZSTD_d_refMultipleDDicts must be enabled for this function to be called.
  */
-static void ZSTD_DCtx_selectFrameDDict(ZSTD_DCtx* dctx) {
+static size_t ZSTD_DCtx_getFrameDDict(const ZSTD_DCtx* dctx, U32 dictID,
+                                    const ZSTD_DDict** ddict) {
+    const ZSTD_DDict* frameDDict;
     assert(dctx->refMultipleDDicts && dctx->ddictSet);
-    DEBUGLOG(4, "Adjusting DDict based on requested dict ID from frame");
-    if (dctx->ddict) {
-        const ZSTD_DDict* frameDDict = ZSTD_DDictHashSet_getDDict(dctx->ddictSet, dctx->fParams.dictID);
-        if (frameDDict) {
-            DEBUGLOG(4, "DDict found!");
-            ZSTD_clearDict(dctx);
-            dctx->dictID = dctx->fParams.dictID;
-            dctx->ddict = frameDDict;
-            dctx->dictUses = ZSTD_use_indefinitely;
-        }
-    }
+    assert(dctx->ddictLocal == NULL && dctx->ddict == NULL);
+    assert(dctx->dictUses == ZSTD_dont_use);
+    DEBUGLOG(4, "Looking up requested dict ID from frame");
+    frameDDict = ZSTD_DDictHashSet_getDDict(dctx->ddictSet, dictID);
+    RETURN_ERROR_IF(frameDDict == NULL && dictID != 0, dictionary_wrong, "No matching DDict in the set");
+    DEBUGLOG(4, "DDict lookup complete");
+    assert(frameDDict == NULL || ZSTD_getDictID_fromDDict(frameDDict) == dictID);
+    *ddict = frameDDict;
+    return 0;
 }
 
 
@@ -696,7 +696,7 @@ unsigned long long ZSTD_getDecompressedSize(const void* src, size_t srcSize)
 
 /** ZSTD_decodeFrameHeader() :
  * `headerSize` must be the size provided by ZSTD_frameHeaderSize().
- * If multiple DDict references are enabled, also will choose the correct DDict to use.
+ * The dictionary's decoding state must already be installed.
  * @return : 0 if success, or an error code, which can be tested using ZSTD_isError() */
 static size_t ZSTD_decodeFrameHeader(ZSTD_DCtx* dctx, const void* src, size_t headerSize)
 {
@@ -704,11 +704,6 @@ static size_t ZSTD_decodeFrameHeader(ZSTD_DCtx* dctx, const void* src, size_t he
     if (ZSTD_isError(result)) return result;    /* invalid header */
     RETURN_ERROR_IF(result>0, srcSize_wrong, "headerSize too small");
 
-    /* Reference DDict requested by frame if dctx references multiple ddicts */
-    if (dctx->refMultipleDDicts == ZSTD_rmd_refMultipleDDicts && dctx->ddictSet) {
-        ZSTD_DCtx_selectFrameDDict(dctx);
-    }
-
 #ifndef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION
     /* Skip the dictID check in fuzzing mode, because it makes the search
      * harder.
@@ -926,9 +921,9 @@ static void ZSTD_DCtx_trace_end(ZSTD_DCtx const* dctx, U64 uncompressedSize, U64
         ZSTD_memset(&trace, 0, sizeof(trace));
         trace.version = ZSTD_VERSION_NUMBER;
         trace.streaming = streaming;
-        if (dctx->ddict) {
-            trace.dictionaryID = ZSTD_getDictID_fromDDict(dctx->ddict);
-            trace.dictionarySize = ZSTD_DDict_dictSize(dctx->ddict);
+        if (dctx->traceDictSize != 0) {
+            trace.dictionaryID = dctx->dictID;
+            trace.dictionarySize = dctx->traceDictSize;
             trace.dictionaryIsCold = dctx->ddictIsCold;
         }
         trace.uncompressedSize = (size_t)uncompressedSize;
@@ -1028,6 +1023,7 @@ static size_t ZSTD_decompressFrame(ZSTD_DCtx* dctx,
             RETURN_ERROR(corruption_detected, "invalid block type");
         }
         FORWARD_IF_ERROR(decodedSize, "Block decompression failure");
+        RETURN_ERROR_IF(decodedSize > dctx->fParams.blockSizeMax, corruption_detected, "Decompressed Block Size Exceeds Maximum");
         DEBUGLOG(5, "Decompressed block of dSize = %u", (unsigned)decodedSize);
         if (dctx->validateChecksum) {
             XXH64_update(&dctx->xxhState, op, decodedSize);
@@ -1070,7 +1066,8 @@ size_t ZSTD_decompressMultiFrame(ZSTD_DCtx* dctx,
                                         void* dst, size_t dstCapacity,
                                   const void* src, size_t srcSize,
                                   const void* dict, size_t dictSize,
-                                  const ZSTD_DDict* ddict)
+                                  const ZSTD_DDict* ddict,
+                                  int useDDictSet)
 {
     void* const dststart = dst;
     int moreThan1Frame = 0;
@@ -1078,22 +1075,27 @@ size_t ZSTD_decompressMultiFrame(ZSTD_DCtx* dctx,
     DEBUGLOG(5, "ZSTD_decompressMultiFrame");
     assert(dict==NULL || ddict==NULL);  /* either dict or ddict set, not both */
 
-    if (ddict) {
-        dict = ZSTD_DDict_dictContent(ddict);
-        dictSize = ZSTD_DDict_dictSize(ddict);
-    }
+    assert(!useDDictSet || (dctx->refMultipleDDicts && dctx->ddictSet
+                           && dctx->ddictLocal == NULL && dctx->ddict == NULL
+                           && dict == NULL && ddict == NULL));
 
     while (srcSize >= ZSTD_startingInputLength(dctx->format)) {
+        const ZSTD_DDict* frameDDict = ddict;
 
 #if defined(ZSTD_LEGACY_SUPPORT) && (ZSTD_LEGACY_SUPPORT >= 1)
         if (dctx->format == ZSTD_f_zstd1 && ZSTD_isLegacy(src, srcSize)) {
             size_t decodedSize;
-            size_t const frameSize = ZSTD_findFrameCompressedSizeLegacy(src, srcSize);
+            const void* const legacyDict = ddict ? ZSTD_DDict_dictContent(ddict) : dict;
+            size_t const legacyDictSize = ddict ? ZSTD_DDict_dictSize(ddict) : dictSize;
+            size_t frameSize;
+            RETURN_ERROR_IF(useDDictSet, parameter_unsupported,
+                            "DDict-set selection is not supported for legacy frames");
+            frameSize = ZSTD_findFrameCompressedSizeLegacy(src, srcSize);
             if (ZSTD_isError(frameSize)) return frameSize;
             RETURN_ERROR_IF(dctx->staticSize, memory_allocation,
                 "legacy support is not compatible with static dctx");
 
-            decodedSize = ZSTD_decompressLegacy(dst, dstCapacity, src, frameSize, dict, dictSize);
+            decodedSize = ZSTD_decompressLegacy(dst, dstCapacity, src, frameSize, legacyDict, legacyDictSize);
             if (ZSTD_isError(decodedSize)) return decodedSize;
 
             {
@@ -1130,9 +1132,17 @@ size_t ZSTD_decompressMultiFrame(ZSTD_DCtx* dctx,
                 continue; /* check next frame */
         }   }
 
-        if (ddict) {
-            /* we were called from ZSTD_decompress_usingDDict */
-            FORWARD_IF_ERROR(ZSTD_decompressBegin_usingDDict(dctx, ddict), "");
+        if (useDDictSet) {
+            ZSTD_FrameHeader zfh;
+            size_t const hSize = ZSTD_getFrameHeader_advanced(&zfh, src, srcSize, dctx->format);
+            /* Invalid/incomplete headers must not change dictionary selection.
+             * ZSTD_decompressFrame() will report the header error. */
+            if (hSize == 0 && zfh.frameType == ZSTD_frame)
+                FORWARD_IF_ERROR(ZSTD_DCtx_getFrameDDict(dctx, zfh.dictID, &frameDDict), "");
+        }
+        if (frameDDict) {
+            /* Use either the explicit DDict or the one selected from the set. */
+            FORWARD_IF_ERROR(ZSTD_decompressBegin_usingDDict(dctx, frameDDict), "");
         } else {
             /* this will initialize correctly with no dict if dict == NULL, so
              * use this in all cases but ddict */
@@ -1172,12 +1182,13 @@ size_t ZSTD_decompress_usingDict(ZSTD_DCtx* dctx,
                            const void* src, size_t srcSize,
                            const void* dict, size_t dictSize)
 {
-    return ZSTD_decompressMultiFrame(dctx, dst, dstCapacity, src, srcSize, dict, dictSize, NULL);
+    return ZSTD_decompressMultiFrame(dctx, dst, dstCapacity, src, srcSize, dict, dictSize, NULL, 0);
 }
 
 
 static ZSTD_DDict const* ZSTD_getDDict(ZSTD_DCtx* dctx)
 {
+    assert(dctx->ddictSet == NULL);
     switch (dctx->dictUses) {
     default:
         assert(0 /* Impossible */);
@@ -1195,6 +1206,11 @@ static ZSTD_DDict const* ZSTD_getDDict(ZSTD_DCtx* dctx)
 
 size_t ZSTD_decompressDCtx(ZSTD_DCtx* dctx, void* dst, size_t dstCapacity, const void* src, size_t srcSize)
 {
+    if (dctx->refMultipleDDicts && dctx->ddictSet)
+        return ZSTD_decompressMultiFrame(dctx, dst, dstCapacity, src, srcSize,
+                                         NULL, 0, NULL, 1);
+    /* Preserve the single-dictionary behavior: resolve once for the entire
+     * call, including concatenated frames and single-use prefixes. */
     return ZSTD_decompress_usingDDict(dctx, dst, dstCapacity, src, srcSize, ZSTD_getDDict(dctx));
 }
 
@@ -1210,7 +1226,7 @@ size_t ZSTD_decompress(void* dst, size_t dstCapacity, const void* src, size_t sr
     return regenSize;
 #else   /* stack mode */
     ZSTD_DCtx dctx;
-    ZSTD_initDCtx_internal(&dctx);
+    ZSTD_initDCtx_internal(&dctx, ZSTD_defaultCMem);
     return ZSTD_decompressDCtx(&dctx, dst, dstCapacity, src, srcSize);
 #endif
 }
@@ -1268,12 +1284,17 @@ ZSTD_nextInputType_e ZSTD_nextInputType(ZSTD_DCtx* dctx) {
 static int ZSTD_isSkipFrame(ZSTD_DCtx* dctx) { return dctx->stage == ZSTDds_skipFrame; }
 
 /** ZSTD_decompressContinue() :
+ *  The frame must first be initialized with ZSTD_decompressBegin() or one of its dictionary-taking variants.
+ *  ZSTD_DCtx_reset() does not replace this initialization.
  *  srcSize : must be the exact nb of bytes expected (see ZSTD_nextSrcSizeToDecompress())
  *  @return : nb of bytes generated into `dst` (necessarily <= `dstCapacity)
  *            or an error code, which can be tested using ZSTD_isError() */
 size_t ZSTD_decompressContinue(ZSTD_DCtx* dctx, void* dst, size_t dstCapacity, const void* src, size_t srcSize)
 {
     DEBUGLOG(5, "ZSTD_decompressContinue (srcSize:%u)", (unsigned)srcSize);
+    /* The previous frame is finished, or the context was never initialized */
+    RETURN_ERROR_IF(dctx->stage == ZSTDds_getFrameHeaderSize && dctx->expected == 0,
+                    stage_wrong, "missing init (ZSTD_decompressBegin)");
     /* Sanity check */
     RETURN_ERROR_IF(srcSize != ZSTD_nextSrcSizeToDecompressWithInputSize(dctx, srcSize), srcSize_wrong, "not allowed");
     ZSTD_checkContinuity(dctx, dst, dstCapacity);
@@ -1561,6 +1582,7 @@ size_t ZSTD_decompressBegin(ZSTD_DCtx* dctx)
     assert(dctx != NULL);
 #if ZSTD_TRACE
     dctx->traceCtx = (ZSTD_trace_decompress_begin != NULL) ? ZSTD_trace_decompress_begin(dctx) : 0;
+    dctx->traceDictSize = 0;
 #endif
     dctx->expected = ZSTD_startingInputLength(dctx->format);  /* dctx->format must be properly set */
     dctx->stage = ZSTDds_getFrameHeaderSize;
@@ -1591,6 +1613,9 @@ size_t ZSTD_decompressBegin_usingDict(ZSTD_DCtx* dctx, const void* dict, size_t
         RETURN_ERROR_IF(
             ZSTD_isError(ZSTD_decompress_insertDictionary(dctx, dict, dictSize)),
             dictionary_corrupted, "");
+#if ZSTD_TRACE
+    dctx->traceDictSize = dict ? dictSize : 0;
+#endif
     return 0;
 }
 
@@ -1604,7 +1629,8 @@ size_t ZSTD_decompressBegin_usingDDict(ZSTD_DCtx* dctx, const ZSTD_DDict* ddict)
     if (ddict) {
         const char* const dictStart = (const char*)ZSTD_DDict_dictContent(ddict);
         size_t const dictSize = ZSTD_DDict_dictSize(ddict);
-        const void* const dictEnd = dictStart + dictSize;
+        /* An empty DDict may have NULL content. */
+        const void* const dictEnd = dictSize != 0 ? dictStart + dictSize : dictStart;
         dctx->ddictIsCold = (dctx->dictEnd != dictEnd);
         DEBUGLOG(4, "DDict is %s",
                     dctx->ddictIsCold ? "~cold~" : "hot!");
@@ -1612,6 +1638,9 @@ size_t ZSTD_decompressBegin_usingDDict(ZSTD_DCtx* dctx, const ZSTD_DDict* ddict)
     FORWARD_IF_ERROR( ZSTD_decompressBegin(dctx) , "");
     if (ddict) {   /* NULL ddict is equivalent to no dictionary */
         ZSTD_copyDDictParameters(dctx, ddict);
+#if ZSTD_TRACE
+        dctx->traceDictSize = ZSTD_DDict_dictSize(ddict);
+#endif
     }
     return 0;
 }
@@ -1657,10 +1686,9 @@ size_t ZSTD_decompress_usingDDict(ZSTD_DCtx* dctx,
                             const void* src, size_t srcSize,
                             const ZSTD_DDict* ddict)
 {
-    /* pass content and size in case legacy frames are encountered */
     return ZSTD_decompressMultiFrame(dctx, dst, dstCapacity, src, srcSize,
                                      NULL, 0,
-                                     ddict);
+                                     ddict, 0);
 }
 
 
@@ -1702,9 +1730,12 @@ size_t ZSTD_DCtx_loadDictionary_advanced(ZSTD_DCtx* dctx,
 {
     RETURN_ERROR_IF(dctx->streamStage != zdss_init, stage_wrong, "");
     ZSTD_clearDict(dctx);
+    ZSTD_clearDDictSet(dctx);
     if (dict && dictSize != 0) {
-        dctx->ddictLocal = ZSTD_createDDict_advanced(dict, dictSize, dictLoadMethod, dictContentType, dctx->customMem);
-        RETURN_ERROR_IF(dctx->ddictLocal == NULL, memory_allocation, "NULL pointer!");
+        RETURN_ERROR_IF(dctx->staticSize, memory_allocation,
+                        "static DCtx can't allocate an internal DDict");
+        FORWARD_IF_ERROR(ZSTD_createDDict_advanced_internal(&dctx->ddictLocal,
+                            dict, dictSize, dictLoadMethod, dictContentType, dctx->customMem), "");
         dctx->ddict = dctx->ddictLocal;
         dctx->dictUses = ZSTD_use_indefinitely;
     }
@@ -1781,8 +1812,6 @@ size_t ZSTD_DCtx_refDDict(ZSTD_DCtx* dctx, const ZSTD_DDict* ddict)
     RETURN_ERROR_IF(dctx->streamStage != zdss_init, stage_wrong, "");
     ZSTD_clearDict(dctx);
     if (ddict) {
-        dctx->ddict = ddict;
-        dctx->dictUses = ZSTD_use_indefinitely;
         if (dctx->refMultipleDDicts == ZSTD_rmd_refMultipleDDicts) {
             if (dctx->ddictSet == NULL) {
                 dctx->ddictSet = ZSTD_createDDictHashSet(dctx->customMem);
@@ -1792,7 +1821,13 @@ size_t ZSTD_DCtx_refDDict(ZSTD_DCtx* dctx, const ZSTD_DDict* ddict)
             }
             assert(!dctx->staticSize);  /* Impossible: ddictSet cannot have been allocated if static dctx */
             FORWARD_IF_ERROR(ZSTD_DDictHashSet_addDDict(dctx->ddictSet, ddict, dctx->customMem), "");
+        } else {
+            assert(dctx->ddictSet == NULL);
+            dctx->ddict = ddict;
+            dctx->dictUses = ZSTD_use_indefinitely;
         }
+    } else {
+        ZSTD_clearDDictSet(dctx);
     }
     return 0;
 }
@@ -1928,6 +1963,12 @@ size_t ZSTD_DCtx_setParameter(ZSTD_DCtx* dctx, ZSTD_dParameter dParam, int value
             if (dctx->staticSize != 0) {
                 RETURN_ERROR(parameter_unsupported, "Static dctx does not support multiple DDicts!");
             }
+            if (value == ZSTD_rmd_refSingleDDict && dctx->ddictSet != NULL) {
+                /* No dictionary from the set is inherited by single-DDict mode. */
+                assert(dctx->ddict == NULL && dctx->ddictLocal == NULL);
+                assert(dctx->dictUses == ZSTD_dont_use);
+                ZSTD_clearDDictSet(dctx);
+            }
             dctx->refMultipleDDicts = (ZSTD_refMultipleDDicts_e)value;
             return 0;
         case ZSTD_d_disableHuffmanAssembly:
@@ -1947,6 +1988,10 @@ size_t ZSTD_DCtx_reset(ZSTD_DCtx* dctx, ZSTD_ResetDirective reset)
 {
     if ( (reset == ZSTD_reset_session_only)
       || (reset == ZSTD_reset_session_and_parameters) ) {
+        /* A prefix is only used once : if a frame was started, it is abandoned now,
+         * so the prefix must not leak into the next frame */
+        if (dctx->streamStage != zdss_init && dctx->dictUses == ZSTD_use_once)
+            ZSTD_clearDict(dctx);
         dctx->streamStage = zdss_init;
         dctx->noForwardProgress = 0;
         dctx->isFrameDecompression = 1;
@@ -1955,6 +2000,7 @@ size_t ZSTD_DCtx_reset(ZSTD_DCtx* dctx, ZSTD_ResetDirective reset)
       || (reset == ZSTD_reset_session_and_parameters) ) {
         RETURN_ERROR_IF(dctx->streamStage != zdss_init, stage_wrong, "");
         ZSTD_clearDict(dctx);
+        ZSTD_clearDDictSet(dctx);
         ZSTD_DCtx_resetParameters(dctx);
     }
     return 0;
@@ -2084,6 +2130,8 @@ static size_t ZSTD_decompressContinueStream(
 
 size_t ZSTD_decompressStream(ZSTD_DStream* zds, ZSTD_outBuffer* output, ZSTD_inBuffer* input)
 {
+    const ZSTD_DDict* frameDDict = NULL;
+    int const useDDictSet = zds->refMultipleDDicts && zds->ddictSet != NULL;
     const char* const src = (const char*)input->src;
     const char* const istart = input->pos != 0 ? src + input->pos : src;
     const char* const iend = input->size != 0 ? src + input->size : src;
@@ -2135,16 +2183,15 @@ size_t ZSTD_decompressStream(ZSTD_DStream* zds, ZSTD_outBuffer* output, ZSTD_inB
             }   }
 #endif
             {   size_t const hSize = ZSTD_getFrameHeader_advanced(&zds->fParams, zds->headerBuffer, zds->lhSize, zds->format);
-                if (zds->refMultipleDDicts && zds->ddictSet) {
-                    ZSTD_DCtx_selectFrameDDict(zds);
-                }
                 if (ZSTD_isError(hSize)) {
 #if defined(ZSTD_LEGACY_SUPPORT) && (ZSTD_LEGACY_SUPPORT>=1)
                     U32 const legacyVersion = ZSTD_isLegacy(istart, iend-istart);
                     if (legacyVersion) {
-                        ZSTD_DDict const* const ddict = ZSTD_getDDict(zds);
+                        ZSTD_DDict const* const ddict = useDDictSet ? NULL : ZSTD_getDDict(zds);
                         const void* const dict = ddict ? ZSTD_DDict_dictContent(ddict) : NULL;
                         size_t const dictSize = ddict ? ZSTD_DDict_dictSize(ddict) : 0;
+                        RETURN_ERROR_IF(useDDictSet, parameter_unsupported,
+                                        "DDict-set selection is not supported for legacy frames");
                         DEBUGLOG(5, "ZSTD_decompressStream: detected legacy version v0.%u", legacyVersion);
                         RETURN_ERROR_IF(zds->staticSize, memory_allocation,
                             "legacy support is incompatible with static dctx");
@@ -2181,6 +2228,13 @@ size_t ZSTD_decompressStream(ZSTD_DStream* zds, ZSTD_outBuffer* output, ZSTD_inB
                     break;
             }   }
 
+            /* Select only after a complete ordinary frame header is available,
+             * before any dictionary-backed decoding state is installed. */
+            frameDDict = NULL;
+            if (useDDictSet && zds->fParams.frameType == ZSTD_frame) {
+                FORWARD_IF_ERROR(ZSTD_DCtx_getFrameDDict(zds, zds->fParams.dictID, &frameDDict), "");
+            }
+
             /* check for single-pass mode opportunity */
             if (zds->fParams.frameContentSize != ZSTD_CONTENTSIZE_UNKNOWN
                 && zds->fParams.frameType != ZSTD_skippableFrame
@@ -2188,7 +2242,8 @@ size_t ZSTD_decompressStream(ZSTD_DStream* zds, ZSTD_outBuffer* output, ZSTD_inB
                 size_t const cSize = ZSTD_findFrameCompressedSize_advanced(istart, (size_t)(iend-istart), zds->format);
                 if (cSize <= (size_t)(iend-istart)) {
                     /* shortcut : using single-pass mode */
-                    size_t const decompressedSize = ZSTD_decompress_usingDDict(zds, op, (size_t)(oend-op), istart, cSize, ZSTD_getDDict(zds));
+                    size_t const decompressedSize = ZSTD_decompress_usingDDict(zds, op, (size_t)(oend-op), istart, cSize,
+                                                    useDDictSet ? frameDDict : ZSTD_getDDict(zds));
                     if (ZSTD_isError(decompressedSize)) return decompressedSize;
                     DEBUGLOG(4, "shortcut to single-pass ZSTD_decompress_usingDDict()");
                     assert(istart != NULL);
@@ -2210,7 +2265,8 @@ size_t ZSTD_decompressStream(ZSTD_DStream* zds, ZSTD_outBuffer* output, ZSTD_inB
 
             /* Consume header (see ZSTDds_decodeFrameHeader) */
             DEBUGLOG(4, "Consume header");
-            FORWARD_IF_ERROR(ZSTD_decompressBegin_usingDDict(zds, ZSTD_getDDict(zds)), "");
+            FORWARD_IF_ERROR(ZSTD_decompressBegin_usingDDict(zds,
+                             useDDictSet ? frameDDict : ZSTD_getDDict(zds)), "");
 
             if (zds->format == ZSTD_f_zstd1
                 && (MEM_readLE32(zds->headerBuffer) & ZSTD_MAGIC_SKIPPABLE_MASK) == ZSTD_MAGIC_SKIPPABLE_START) {  /* skippable frame */
diff --git a/lib/decompress/zstd_decompress_block.c b/lib/decompress/zstd_decompress_block.c
index 8d108c72ece..f5b05e5a1ea 100644
--- a/lib/decompress/zstd_decompress_block.c
+++ b/lib/decompress/zstd_decompress_block.c
@@ -892,7 +892,10 @@ static void ZSTD_safecopyDstBeforeSrc(BYTE* op, const BYTE* ip, size_t length)
         return;
     }
 
-    if (op <= oend - WILDCOPY_OVERLENGTH && diff < -WILDCOPY_VECLEN) {
+    /* ZSTD_wildcopy() may write up to WILDCOPY_OVERLENGTH-1 bytes past its length.
+     * Those bytes must not land on source bytes the leftover loop has yet to read,
+     * so src must be at least WILDCOPY_OVERLENGTH bytes ahead of dst. */
+    if (op <= oend - WILDCOPY_OVERLENGTH && diff <= -WILDCOPY_OVERLENGTH) {
         ZSTD_wildcopy(op, ip, (size_t)(oend - WILDCOPY_OVERLENGTH - op), ZSTD_no_overlap);
         ip += oend - WILDCOPY_OVERLENGTH - op;
         op += oend - WILDCOPY_OVERLENGTH - op;
@@ -977,8 +980,11 @@ size_t ZSTD_execSequenceEndSplitLitBuffer(BYTE* op,
     assert(op < op + sequenceLength);
     assert(oLitEnd < op + sequenceLength);
 
+    /* The literals still to be read live in dst at [*litPtr, litLimit).
+     * The output of this sequence must not run past the literals it consumes. */
+    RETURN_ERROR_IF(op + sequenceLength > iLitEnd, dstSize_tooSmall, "output should not catch up to and overwrite literal buffer");
+
     /* copy literals */
-    RETURN_ERROR_IF(op > *litPtr && op < *litPtr + sequence.litLength, dstSize_tooSmall, "output should not catch up to and overwrite literal buffer");
     ZSTD_safecopyDstBeforeSrc(op, *litPtr, sequence.litLength);
     op = oLitEnd;
     *litPtr = iLitEnd;
@@ -1281,10 +1287,6 @@ ZSTD_decodeSequence(seqState_t* seqState, const ZSTD_longOffset_e longOffsets, c
         assert(llBits <= MaxLLBits);
         assert(mlBits <= MaxMLBits);
         assert(ofBits <= MaxOff);
-        /* As GCC has better branch and block analyzers, sometimes it is only
-         * valuable to mark likeliness for Clang.
-         */
-
         /* sequence */
         {   size_t offset;
             if (ofBits > 1) {
@@ -1364,10 +1366,6 @@ ZSTD_decodeSequence(seqState_t* seqState, const ZSTD_longOffset_e longOffsets, c
         assert(llBits <= MaxLLBits);
         assert(mlBits <= MaxMLBits);
         assert(ofBits <= MaxOff);
-        /* As GCC has better branch and block analyzers, sometimes it is only
-         * valuable to mark likeliness for Clang.
-         */
-
         /* sequence */
         {   size_t offset;
             if (ofBits > 1) {
diff --git a/lib/decompress/zstd_decompress_internal.h b/lib/decompress/zstd_decompress_internal.h
index 58a5bc8cc8f..a7d51529816 100644
--- a/lib/decompress/zstd_decompress_internal.h
+++ b/lib/decompress/zstd_decompress_internal.h
@@ -163,7 +163,7 @@ struct ZSTD_DCtx_s
 
     /* dictionary */
     ZSTD_DDict* ddictLocal;
-    const ZSTD_DDict* ddict;     /* set by ZSTD_initDStream_usingDDict(), or ZSTD_DCtx_refDDict() */
+    const ZSTD_DDict* ddict;     /* single-DDict state; NULL while ddictSet exists */
     U32 dictID;
     int ddictIsCold;             /* if == 1 : dictionary is "new" for working context, and presumed "cold" (not in cpu cache) */
     ZSTD_dictUses_e dictUses;
@@ -210,6 +210,7 @@ struct ZSTD_DCtx_s
     /* Tracing */
 #if ZSTD_TRACE
     ZSTD_TraceCtx traceCtx;
+    size_t traceDictSize;       /* size of the dictionary installed for this frame */
 #endif
 };  /* typedef'd to ZSTD_DCtx within "zstd.h" */
 
diff --git a/lib/dictBuilder/cover.c b/lib/dictBuilder/cover.c
index 06d1cb93a5c..921052657ee 100644
--- a/lib/dictBuilder/cover.c
+++ b/lib/dictBuilder/cover.c
@@ -310,7 +310,7 @@ static int COVER_cmp8(COVER_ctx_t *ctx, const void *lp, const void *rp) {
 }
 
 /**
- * Same as COVER_cmp() except ties are broken by pointer value
+ * Same as COVER_cmp() except ties are broken by the position each element holds
  */
 #if (ZDICT_QSORT == ZDICT_QSORT_MSVC) || (ZDICT_QSORT == ZDICT_QSORT_APPLE)
 static int WIN_CDECL COVER_strict_cmp(void* g_coverCtx, const void* lp, const void* rp) {
@@ -321,7 +321,9 @@ static int COVER_strict_cmp(const void *lp, const void *rp) {
 #endif
   int result = COVER_cmp((COVER_ctx_t*)g_coverCtx, lp, rp);
   if (result == 0) {
-    result = lp < rp ? -1 : 1;
+    U32 const lpos = *(const U32 *)lp;
+    U32 const rpos = *(const U32 *)rp;
+    result = (lpos < rpos) ? -1 : (lpos > rpos) ? 1 : 0;
   }
   return result;
 }
@@ -337,7 +339,9 @@ static int COVER_strict_cmp8(const void *lp, const void *rp) {
 #endif
   int result = COVER_cmp8((COVER_ctx_t*)g_coverCtx, lp, rp);
   if (result == 0) {
-    result = lp < rp ? -1 : 1;
+    U32 const lpos = *(const U32 *)lp;
+    U32 const rpos = *(const U32 *)rp;
+    result = (lpos < rpos) ? -1 : (lpos > rpos) ? 1 : 0;
   }
   return result;
 }
@@ -636,14 +640,25 @@ static size_t COVER_ctx_init(COVER_ctx_t *ctx, const void *samplesBuffer,
   const unsigned nbTestSamples = splitPoint < 1.0 ? nbSamples - nbTrainSamples : nbSamples;
   const size_t trainingSamplesSize = splitPoint < 1.0 ? COVER_sum(samplesSizes, nbTrainSamples) : totalSamplesSize;
   const size_t testSamplesSize = splitPoint < 1.0 ? COVER_sum(samplesSizes + nbTrainSamples, nbTestSamples) : totalSamplesSize;
+  const size_t readLength = MAX(d, sizeof(U64));
   ctx->displayLevel = displayLevel;
   /* Checks */
-  if (totalSamplesSize < MAX(d, sizeof(U64)) ||
+  if (totalSamplesSize < readLength ||
       totalSamplesSize >= (size_t)COVER_MAX_SAMPLES_SIZE) {
     DISPLAYLEVEL(1, "Total samples size is too large (%u MB), maximum size is %u MB\n",
                  (unsigned)(totalSamplesSize>>20), (COVER_MAX_SAMPLES_SIZE >> 20));
     return ERROR(srcSize_wrong);
   }
+  /* The training set must be large enough for the partial suffix array, whose
+   * size is `trainingSamplesSize - MAX(d, sizeof(U64)) + 1`. When splitPoint
+   * < 1.0 the training set is a subset of the corpus, so it can be smaller
+   * than the total even after the check above. Guard against the size_t
+   * subtraction underflowing into a huge allocation. */
+  if (trainingSamplesSize < MAX(d, sizeof(U64))) {
+    DISPLAYLEVEL(1, "Training samples size is too small (%u), minimum size is %u\n",
+                 (unsigned)trainingSamplesSize, (unsigned)MAX(d, sizeof(U64)));
+    return ERROR(srcSize_wrong);
+  }
   /* Check if there are at least 5 training samples */
   if (nbTrainSamples < 5) {
     DISPLAYLEVEL(1, "Total number of training samples is %u and is invalid.", nbTrainSamples);
@@ -666,7 +681,7 @@ static size_t COVER_ctx_init(COVER_ctx_t *ctx, const void *samplesBuffer,
   ctx->nbTrainSamples = nbTrainSamples;
   ctx->nbTestSamples = nbTestSamples;
   /* Partial suffix array */
-  ctx->suffixSize = trainingSamplesSize - MAX(d, sizeof(U64)) + 1;
+  ctx->suffixSize = trainingSamplesSize - readLength + 1;
   ctx->suffix = (U32 *)malloc(ctx->suffixSize * sizeof(U32));
   /* Maps index to the dmerID */
   ctx->dmerAt = (U32 *)malloc(ctx->suffixSize * sizeof(U32));
@@ -692,7 +707,7 @@ static size_t COVER_ctx_init(COVER_ctx_t *ctx, const void *samplesBuffer,
   {
     /* suffix is a partial suffix array.
      * It only sorts suffixes by their first parameters.d bytes.
-     * The sort is stable, so each dmer group is sorted by position in input.
+     * Ties are broken by position, so each group is sorted by input position.
      */
     U32 i;
     for (i = 0; i < ctx->suffixSize; ++i) {
@@ -1052,6 +1067,8 @@ COVER_dictSelection_t COVER_selectDict(BYTE* customDictContent, size_t dictBuffe
 
   size_t largestDict = 0;
   size_t largestCompressed = 0;
+  size_t candidateContentSize = 0;
+  size_t candidateDict = 0;
   BYTE* customDictContentEnd = customDictContent + dictContentSize;
 
   BYTE* largestDictbuffer = (BYTE*)malloc(dictBufferCapacity);
@@ -1066,20 +1083,20 @@ COVER_dictSelection_t COVER_selectDict(BYTE* customDictContent, size_t dictBuffe
 
   /* Initial dictionary size and compressed size */
   memcpy(largestDictbuffer, customDictContent, dictContentSize);
-  dictContentSize = ZDICT_finalizeDictionary(
+  largestDict = ZDICT_finalizeDictionary(
     largestDictbuffer, dictBufferCapacity, customDictContent, dictContentSize,
     samplesBuffer, samplesSizes, nbFinalizeSamples, params.zParams);
 
-  if (ZDICT_isError(dictContentSize)) {
+  if (ZDICT_isError(largestDict)) {
     free(largestDictbuffer);
     free(candidateDictBuffer);
-    return COVER_dictSelectionError(dictContentSize);
+    return COVER_dictSelectionError(largestDict);
   }
 
   totalCompressedSize = COVER_checkTotalCompressedSize(params, samplesSizes,
                                                        samplesBuffer, offsets,
                                                        nbCheckSamples, nbSamples,
-                                                       largestDictbuffer, dictContentSize);
+                                                       largestDictbuffer, largestDict);
 
   if (ZSTD_isError(totalCompressedSize)) {
     free(largestDictbuffer);
@@ -1089,31 +1106,29 @@ COVER_dictSelection_t COVER_selectDict(BYTE* customDictContent, size_t dictBuffe
 
   if (params.shrinkDict == 0) {
     free(candidateDictBuffer);
-    return setDictSelection(largestDictbuffer, dictContentSize, totalCompressedSize);
+    return setDictSelection(largestDictbuffer, largestDict, totalCompressedSize);
   }
 
-  largestDict = dictContentSize;
   largestCompressed = totalCompressedSize;
-  dictContentSize = ZDICT_DICTSIZE_MIN;
+  candidateContentSize = ZDICT_DICTSIZE_MIN;
 
-  /* Largest dict is initially at least ZDICT_DICTSIZE_MIN */
-  while (dictContentSize < largestDict) {
+  while (candidateContentSize < dictContentSize) {
     memcpy(candidateDictBuffer, largestDictbuffer, largestDict);
-    dictContentSize = ZDICT_finalizeDictionary(
-      candidateDictBuffer, dictBufferCapacity, customDictContentEnd - dictContentSize, dictContentSize,
+    candidateDict = ZDICT_finalizeDictionary(
+      candidateDictBuffer, dictBufferCapacity, customDictContentEnd - candidateContentSize, candidateContentSize,
       samplesBuffer, samplesSizes, nbFinalizeSamples, params.zParams);
 
-    if (ZDICT_isError(dictContentSize)) {
+    if (ZDICT_isError(candidateDict)) {
       free(largestDictbuffer);
       free(candidateDictBuffer);
-      return COVER_dictSelectionError(dictContentSize);
+      return COVER_dictSelectionError(candidateDict);
 
     }
 
     totalCompressedSize = COVER_checkTotalCompressedSize(params, samplesSizes,
                                                          samplesBuffer, offsets,
                                                          nbCheckSamples, nbSamples,
-                                                         candidateDictBuffer, dictContentSize);
+                                                         candidateDictBuffer, candidateDict);
 
     if (ZSTD_isError(totalCompressedSize)) {
       free(largestDictbuffer);
@@ -1123,14 +1138,13 @@ COVER_dictSelection_t COVER_selectDict(BYTE* customDictContent, size_t dictBuffe
 
     if ((double)totalCompressedSize <= (double)largestCompressed * regressionTolerance) {
       free(largestDictbuffer);
-      return setDictSelection( candidateDictBuffer, dictContentSize, totalCompressedSize );
+      return setDictSelection( candidateDictBuffer, candidateDict, totalCompressedSize );
     }
-    dictContentSize *= 2;
+    candidateContentSize *= 2;
   }
-  dictContentSize = largestDict;
   totalCompressedSize = largestCompressed;
   free(candidateDictBuffer);
-  return setDictSelection( largestDictbuffer, dictContentSize, totalCompressedSize );
+  return setDictSelection( largestDictbuffer, largestDict, totalCompressedSize );
 }
 
 /**
diff --git a/lib/dictBuilder/fastcover.c b/lib/dictBuilder/fastcover.c
index 56a08138520..f59170ece60 100644
--- a/lib/dictBuilder/fastcover.c
+++ b/lib/dictBuilder/fastcover.c
@@ -317,10 +317,11 @@ FASTCOVER_ctx_init(FASTCOVER_ctx_t* ctx,
     const unsigned nbTestSamples = splitPoint < 1.0 ? nbSamples - nbTrainSamples : nbSamples;
     const size_t trainingSamplesSize = splitPoint < 1.0 ? COVER_sum(samplesSizes, nbTrainSamples) : totalSamplesSize;
     const size_t testSamplesSize = splitPoint < 1.0 ? COVER_sum(samplesSizes + nbTrainSamples, nbTestSamples) : totalSamplesSize;
+    const size_t readLength = MAX(d, sizeof(U64));
     ctx->displayLevel = displayLevel;
 
     /* Checks */
-    if (totalSamplesSize < MAX(d, sizeof(U64)) ||
+    if (totalSamplesSize < readLength ||
         totalSamplesSize >= (size_t)FASTCOVER_MAX_SAMPLES_SIZE) {
         DISPLAYLEVEL(1, "Total samples size is too large (%u MB), maximum size is %u MB\n",
                     (unsigned)(totalSamplesSize >> 20), (FASTCOVER_MAX_SAMPLES_SIZE >> 20));
@@ -339,6 +340,12 @@ FASTCOVER_ctx_init(FASTCOVER_ctx_t* ctx,
         return ERROR(srcSize_wrong);
     }
 
+    /* Check the training set can hold at least one dmer, otherwise nbDmers underflows below */
+    if (trainingSamplesSize < readLength) {
+        DISPLAYLEVEL(1, "Total training samples size is %u and is invalid.\n", (unsigned)trainingSamplesSize);
+        return ERROR(srcSize_wrong);
+    }
+
     /* Zero the context */
     memset(ctx, 0, sizeof(*ctx));
     DISPLAYLEVEL(2, "Training on %u samples of total size %u\n", nbTrainSamples,
@@ -351,7 +358,7 @@ FASTCOVER_ctx_init(FASTCOVER_ctx_t* ctx,
     ctx->nbSamples = nbSamples;
     ctx->nbTrainSamples = nbTrainSamples;
     ctx->nbTestSamples = nbTestSamples;
-    ctx->nbDmers = trainingSamplesSize - MAX(d, sizeof(U64)) + 1;
+    ctx->nbDmers = trainingSamplesSize - readLength + 1;
     ctx->d = d;
     ctx->f = f;
     ctx->accelParams = accelParams;
diff --git a/lib/legacy/zstd_v05.c b/lib/legacy/zstd_v05.c
index ee180487e94..9389963e85f 100644
--- a/lib/legacy/zstd_v05.c
+++ b/lib/legacy/zstd_v05.c
@@ -3650,8 +3650,8 @@ static size_t ZSTDv05_loadEntropy(ZSTDv05_DCtx* dctx, const void* dict, size_t d
     dictSize -= matchlengthHeaderSize;
 
     litlengthHeaderSize = FSEv05_readNCount(litlengthNCount, &litlengthMaxValue, &litlengthLog, dict, dictSize);
-    if (litlengthLog > LLFSEv05Log) return ERROR(dictionary_corrupted);
     if (FSEv05_isError(litlengthHeaderSize)) return ERROR(dictionary_corrupted);
+    if (litlengthLog > LLFSEv05Log) return ERROR(dictionary_corrupted);
     errorCode = FSEv05_buildDTable(dctx->LLTable, litlengthNCount, litlengthMaxValue, litlengthLog);
     if (FSEv05_isError(errorCode)) return ERROR(dictionary_corrupted);
 
@@ -3662,8 +3662,7 @@ static size_t ZSTDv05_loadEntropy(ZSTDv05_DCtx* dctx, const void* dict, size_t d
 static size_t ZSTDv05_decompress_insertDictionary(ZSTDv05_DCtx* dctx, const void* dict, size_t dictSize)
 {
     size_t eSize;
-    U32 magic = MEM_readLE32(dict);
-    if (magic != ZSTDv05_DICT_MAGIC) {
+    if (dictSize < 4 || MEM_readLE32(dict) != ZSTDv05_DICT_MAGIC) {
         /* pure content mode */
         ZSTDv05_refDictContent(dctx, dict, dictSize);
         return 0;
diff --git a/lib/legacy/zstd_v06.c b/lib/legacy/zstd_v06.c
index fb8c14df914..a4f8b9dc7d7 100644
--- a/lib/legacy/zstd_v06.c
+++ b/lib/legacy/zstd_v06.c
@@ -3802,8 +3802,7 @@ static size_t ZSTDv06_loadEntropy(ZSTDv06_DCtx* dctx, const void* dict, size_t d
 static size_t ZSTDv06_decompress_insertDictionary(ZSTDv06_DCtx* dctx, const void* dict, size_t dictSize)
 {
     size_t eSize;
-    U32 const magic = MEM_readLE32(dict);
-    if (magic != ZSTDv06_DICT_MAGIC) {
+    if (dictSize < 4 || MEM_readLE32(dict) != ZSTDv06_DICT_MAGIC) {
         /* pure content mode */
         ZSTDv06_refDictContent(dctx, dict, dictSize);
         return 0;
diff --git a/lib/libzstd.mk b/lib/libzstd.mk
index 60928b0ee95..835ef99f313 100644
--- a/lib/libzstd.mk
+++ b/lib/libzstd.mk
@@ -134,6 +134,8 @@ endif
 ifeq ($(shell echo "int main(int argc, char* argv[]) { (void)argc; (void)argv; return 0; }" | $(CC) $(FLAGS) -z cet-report=error -x c -Werror - -o $(VOID) 2>$(VOID) && echo 1 || echo 0),1)
 LDFLAGS += -z cet-report=error
 endif
+# 32-bit system objects usually have no CET properties: 32-bit targets link with LDFLAGS_32
+LDFLAGS_32 = $(subst -z cet-report=error,,$(LDFLAGS))
 
 HAVE_COLORNEVER = $(shell echo a | grep --color=never a > /dev/null 2> /dev/null && echo 1 || echo 0)
 GREP_OPTIONS ?=
diff --git a/lib/zstd.h b/lib/zstd.h
index 97fef316fd6..4bfff05c1f3 100644
--- a/lib/zstd.h
+++ b/lib/zstd.h
@@ -597,6 +597,7 @@ typedef enum {
  *  - The session : will stop compressing current frame, and make CCtx ready to start a new one.
  *                  Useful after an error, or to interrupt any ongoing compression.
  *                  Any internal data not yet flushed is cancelled.
+ *                  With nbWorkers >= 1, it waits for the jobs already started to complete.
  *                  Compression parameters and dictionary remain unchanged.
  *                  They will be used to compress next frame.
  *                  Resetting session never fails.
@@ -1029,7 +1030,8 @@ ZSTDLIB_API size_t      ZSTD_freeDDict(ZSTD_DDict* ddict);
 
 /*! ZSTD_decompress_usingDDict() :
  *  Decompression using a digested Dictionary.
- *  Recommended when same dictionary is used multiple times. */
+ *  Recommended when same dictionary is used multiple times.
+ *  Uses the supplied DDict, regardless of dictionaries referenced in dctx. */
 ZSTDLIB_API size_t ZSTD_decompress_usingDDict(ZSTD_DCtx* dctx,
                                               void* dst, size_t dstCapacity,
                                         const void* src, size_t srcSize,
@@ -1150,6 +1152,8 @@ ZSTDLIB_API size_t ZSTD_CCtx_refPrefix(ZSTD_CCtx* cctx,
  * @result : 0, or an error code (which can be tested with ZSTD_isError()).
  *  Special : Adding a NULL (or 0-size) dictionary invalidates any previous dictionary,
  *            meaning "return to no-dictionary mode".
+ *  Loading any dictionary also discards the multiple-DDict reference table,
+ *  without disabling ZSTD_d_refMultipleDDicts.
  *  Note 1 : Loading a dictionary involves building tables,
  *           which has a non-negligible impact on CPU usage and latency.
  *           It's recommended to "load once, use many times", to amortize the cost
@@ -1162,13 +1166,20 @@ ZSTDLIB_API size_t ZSTD_DCtx_loadDictionary(ZSTD_DCtx* dctx, const void* dict, s
 
 /*! ZSTD_DCtx_refDDict() : Requires v1.4.0+
  *  Reference a prepared dictionary, to be used to decompress next frames.
- *  The dictionary remains active for decompression of future frames using same DCtx.
+ *  In single-DDict mode, the dictionary remains active for decompression of
+ *  future frames using the same DCtx.
  *
  *  If called with ZSTD_d_refMultipleDDicts enabled, repeated calls of this function
  *  will store the DDict references in a table, and the DDict used for decompression
  *  will be determined at decompression time, as per the dict ID in the frame.
- *  The memory for the table is allocated on the first call to refDDict, and can be
- *  freed with ZSTD_freeDCtx().
+ *  The memory for the table is allocated on the first call to refDDict.
+ *  The table is freed by ZSTD_DCtx_refDDict(dctx, NULL), by ZSTD_DCtx_reset()
+ *  with ZSTD_reset_parameters or ZSTD_reset_session_and_parameters, by any
+ *  ZSTD_DCtx_loadDictionary() or ZSTD_DCtx_refPrefix() call, by disabling
+ *  multiple-DDict mode, and by ZSTD_freeDCtx().
+ *  Disabling multiple-DDict mode releases all dictionary references from the set;
+ *  no dictionary from the set is retained for subsequent decompression.
+ *  Referencing a DDict invalidates any previously loaded dictionary or prefix.
  *
  *  If called with ZSTD_d_refMultipleDDicts disabled (the default), only one dictionary
  *  will be managed, and referencing a dictionary effectively "discards" any previous one.
@@ -1186,7 +1197,8 @@ ZSTDLIB_API size_t ZSTD_DCtx_refDDict(ZSTD_DCtx* dctx, const ZSTD_DDict* ddict);
  *  Prefix is **only used once**. Reference is discarded at end of frame.
  *  End of frame is reached when ZSTD_decompressStream() returns 0.
  * @result : 0, or an error code (which can be tested with ZSTD_isError()).
- *  Note 1 : Adding any prefix (including NULL) invalidates any previously set prefix or dictionary
+ *  Note 1 : Adding any prefix (including NULL) invalidates any previously set prefix or dictionary,
+ *           including the multiple-DDict reference table.
  *  Note 2 : Prefix buffer is referenced. It **must** outlive decompression.
  *           Prefix buffer must remain unmodified up to the end of frame,
  *           reached when ZSTD_decompressStream() returns 0.
@@ -1699,6 +1711,8 @@ ZSTD_compressSequences(ZSTD_CCtx* cctx,
  * - @litBufCapacity is the size of the underlying buffer into which literals are written, starting at address @literals.
  *   @litBufCapacity must be at least 8 bytes larger than @litSize.
  * - @decompressedSize must be correct, and correspond to the sum of all Sequences. Any discrepancy will generate an error.
+ * - Each block must regenerate at most ZSTD_BLOCKSIZE_MAX bytes, or less with a smaller window or ZSTD_c_maxBlockSize.
+ *   A larger block will generate an error.
  * @return : final compressed size, or a ZSTD error code.
  */
 ZSTDLIB_STATIC_API size_t
@@ -1813,6 +1827,8 @@ ZSTDLIB_STATIC_API size_t ZSTD_estimateDStreamSize_fromFrame(const void* src, si
 /*! ZSTD_estimate?DictSize() :
  *  ZSTD_estimateCDictSize() will bet that src size is relatively "small", and content is copied, like ZSTD_createCDict().
  *  ZSTD_estimateCDictSize_advanced() makes it possible to control compression parameters precisely, like ZSTD_createCDict_advanced().
+ *  These estimates return an error code if dictSize is too large.
+ *  Check the result with ZSTD_isError().
  *  Note : dictionaries created by reference (`ZSTD_dlm_byRef`) are logically smaller.
  */
 ZSTDLIB_STATIC_API size_t ZSTD_estimateCDictSize(size_t dictSize, int compressionLevel);
@@ -1836,7 +1852,10 @@ ZSTDLIB_STATIC_API size_t ZSTD_estimateDDictSize(size_t dictSize, ZSTD_dictLoadM
  *  Note 3 : cParams : use ZSTD_getCParams() to convert a compression level
  *           into its associated cParams.
  *  Limitation 1 : currently not compatible with internal dictionary creation, triggered by
- *                 ZSTD_CCtx_loadDictionary(), ZSTD_initCStream_usingDict() or ZSTD_initDStream_usingDict().
+ *                 ZSTD_CCtx_loadDictionary(), ZSTD_DCtx_loadDictionary(), ZSTD_DCtx_refPrefix(),
+ *                 ZSTD_initCStream_usingDict() or ZSTD_initDStream_usingDict().
+ *                 For one-shot decompression, ZSTD_decompress_usingDict() needs no allocation.
+ *                 Otherwise, use ZSTD_initStaticDDict() + ZSTD_DCtx_refDDict().
  *  Limitation 2 : static cctx currently not compatible with multi-threading.
  *  Limitation 3 : static dctx is incompatible with legacy support.
  */
@@ -1897,9 +1916,10 @@ ZSTDLIB_STATIC_API ZSTD_CDict* ZSTD_createCDict_advanced(const void* dict, size_
  *  This can limit resources for applications with multiple threads where each one uses
  *  a threaded compression mode (via ZSTD_c_nbWorkers parameter).
  *  ZSTD_createThreadPool creates a new thread pool with a given number of threads.
+ *  ZSTD_c_nbWorkers does not change the number of threads of such a pool.
  *  Note that the lifetime of such pool must exist while being used.
  *  ZSTD_CCtx_refThreadPool assigns a thread pool to a context (use NULL argument value
- *  to use an internal thread pool).
+ *  to use an internal thread pool). The context then no longer uses its previous pool.
  *  ZSTD_freeThreadPool frees a thread pool, accepts NULL pointer.
  */
 typedef struct POOL_ctx_s ZSTD_threadPool;
@@ -2549,7 +2569,22 @@ ZSTDLIB_STATIC_API size_t ZSTD_DCtx_getParameter(ZSTD_DCtx* dctx, ZSTD_dParamete
  * to store references to multiple ZSTD_DDict. That is, multiple calls of ZSTD_refDDict()
  * using a given ZSTD_DCtx, rather than overwriting the previous DDict reference, will instead
  * store all references. At decompression time, the appropriate dictID is selected
- * from the set of DDicts based on the dictID in the frame.
+ * from the set of DDicts based on the dictID in each frame. This applies to
+ * ZSTD_decompressDCtx() and ZSTD_decompressStream(); entry points taking an
+ * explicit dictionary use that dictionary instead.
+ * When a set exists, an ordinary Zstandard frame with a nonzero dictionary ID must
+ * have an exact match in the set; otherwise decompression returns
+ * ZSTD_error_dictionary_wrong. A frame with dictionary ID 0 (including an omitted
+ * ID) uses the registered ID-0 DDict if present, or no dictionary otherwise.
+ * There is no default dictionary in the set, and a lookup does not change the
+ * dictionary used by another frame.
+ * Skippable frames do not select a dictionary. Legacy formats are not supported
+ * by set-based decoding; use a single or explicitly supplied dictionary instead.
+ * Loading a dictionary or referencing a prefix discards the referenced set,
+ * without disabling this mode. Referencing a DDict discards any previously
+ * loaded dictionary or prefix. The two kinds of dictionary storage cannot coexist.
+ * Disabling this mode discards the entire set. No dictionary from the set is
+ * retained for subsequent decompression.
  *
  * Usage is simply calling ZSTD_refDDict() on multiple dict buffers.
  *
@@ -3029,10 +3064,6 @@ ZSTDLIB_STATIC_API size_t ZSTD_compressBegin_usingDict(ZSTD_CCtx* cctx, const vo
 ZSTD_DEPRECATED("The buffer-less API is deprecated in favor of the normal streaming API. See docs.")
 ZSTDLIB_STATIC_API size_t ZSTD_compressBegin_usingCDict(ZSTD_CCtx* cctx, const ZSTD_CDict* cdict); /**< note: fails if cdict==NULL */
 
-ZSTD_DEPRECATED("This function will likely be removed in a future release. It is misleading and has very limited utility.")
-ZSTDLIB_STATIC_API
-size_t ZSTD_copyCCtx(ZSTD_CCtx* cctx, const ZSTD_CCtx* preparedCCtx, unsigned long long pledgedSrcSize); /**<  note: if pledgedSrcSize is not known, use ZSTD_CONTENTSIZE_UNKNOWN */
-
 ZSTD_DEPRECATED("The buffer-less API is deprecated in favor of the normal streaming API. See docs.")
 ZSTDLIB_STATIC_API size_t ZSTD_compressContinue(ZSTD_CCtx* cctx, void* dst, size_t dstCapacity, const void* src, size_t srcSize);
 ZSTD_DEPRECATED("The buffer-less API is deprecated in favor of the normal streaming API. See docs.")
@@ -3089,8 +3120,12 @@ size_t ZSTD_compressBegin_usingCDict_advanced(ZSTD_CCtx* const cctx, const ZSTD_
   as long as the encoder and decoder progress in "lock-step",
   aka use exactly the same buffer sizes, break contiguity at the same place, etc.
 
-  Once buffers are setup, start decompression, with ZSTD_decompressBegin().
+  Once buffers are set up and decompression parameters are configured, initialize the decoder with ZSTD_decompressBegin().
   If decompression requires a dictionary, use ZSTD_decompressBegin_usingDict() or ZSTD_decompressBegin_usingDDict().
+  One of these initialization functions must be called before the first ZSTD_decompressContinue() call for every frame,
+  including when reusing a context or after calling ZSTD_DCtx_reset().
+  ZSTD_DCtx_reset() does not replace this initialization, and ZSTD_decompressContinue() does not initialize the decoder itself.
+  After a session reset, an interrupted frame cannot be resumed : initialize the decoder again and restart from the frame's beginning.
 
   Then use ZSTD_nextSrcSizeToDecompress() and ZSTD_decompressContinue() alternatively.
   ZSTD_nextSrcSizeToDecompress() tells how many bytes to provide as 'srcSize' to ZSTD_decompressContinue().
@@ -3101,7 +3136,7 @@ size_t ZSTD_compressBegin_usingCDict_advanced(ZSTD_CCtx* const cctx, const ZSTD_
   It can also be an error code, which can be tested with ZSTD_isError().
 
   A frame is fully decoded when ZSTD_nextSrcSizeToDecompress() returns zero.
-  Context can then be reset to start a new decompression.
+  Before decoding another frame, call the appropriate ZSTD_decompressBegin*() function again.
 
   Note : it's possible to know if next input to present is a header or a block, using ZSTD_nextInputType().
   This information is not required to properly decode a frame.
@@ -3122,16 +3157,28 @@ size_t ZSTD_compressBegin_usingCDict_advanced(ZSTD_CCtx* const cctx, const ZSTD_
 
 ZSTDLIB_STATIC_API size_t ZSTD_decodingBufferSize_min(unsigned long long windowSize, unsigned long long frameContentSize);  /**< when frame content size is not known, pass in frameContentSize == ZSTD_CONTENTSIZE_UNKNOWN */
 
+/*! ZSTD_decompressBegin() :
+ *  Initialize the bufferless decoder to start a new frame without a dictionary.
+ *  Decompression parameters must be configured before calling this function.
+ *  Call this function, ZSTD_decompressBegin_usingDict(), or ZSTD_decompressBegin_usingDDict()
+ *  before the first ZSTD_decompressContinue() call for each frame, including after ZSTD_DCtx_reset().
+ *  Use the dictionary-taking variants when the frame requires a dictionary.
+ */
 ZSTDLIB_STATIC_API size_t ZSTD_decompressBegin(ZSTD_DCtx* dctx);
 ZSTDLIB_STATIC_API size_t ZSTD_decompressBegin_usingDict(ZSTD_DCtx* dctx, const void* dict, size_t dictSize);
 ZSTDLIB_STATIC_API size_t ZSTD_decompressBegin_usingDDict(ZSTD_DCtx* dctx, const ZSTD_DDict* ddict);
 
 ZSTDLIB_STATIC_API size_t ZSTD_nextSrcSizeToDecompress(ZSTD_DCtx* dctx);
+
+/*! ZSTD_decompressContinue() :
+ *  Decode the next part of a frame initialized with ZSTD_decompressBegin()
+ *  or one of its dictionary-taking variants. This function does not initialize the decoder.
+ *  After finishing a frame or resetting the session, initialize the decoder again
+ *  before calling this function for a new frame.
+ */
 ZSTDLIB_STATIC_API size_t ZSTD_decompressContinue(ZSTD_DCtx* dctx, void* dst, size_t dstCapacity, const void* src, size_t srcSize);
 
 /* misc */
-ZSTD_DEPRECATED("This function will likely be removed in the next minor release. It is misleading and has very limited utility.")
-ZSTDLIB_STATIC_API void   ZSTD_copyDCtx(ZSTD_DCtx* dctx, const ZSTD_DCtx* preparedDCtx);
 typedef enum { ZSTDnit_frameHeader, ZSTDnit_blockHeader, ZSTDnit_block, ZSTDnit_lastBlock, ZSTDnit_checksum, ZSTDnit_skippableFrame } ZSTD_nextInputType_e;
 ZSTDLIB_STATIC_API ZSTD_nextInputType_e ZSTD_nextInputType(ZSTD_DCtx* dctx);
 
diff --git a/programs/Makefile b/programs/Makefile
index 94f2179d028..11dd93b7572 100644
--- a/programs/Makefile
+++ b/programs/Makefile
@@ -186,6 +186,7 @@ zstd-release: DEBUGFLAGS_LD :=
 zstd-release: zstd
 
 CLEAN += zstd32
+zstd32 : LDFLAGS := $(LDFLAGS_32)
 zstd32 : CPPFLAGS += $(THREAD_CPP)
 zstd32 : LDFLAGS  += $(THREAD_LD)
 zstd32 : CPPFLAGS += -DZSTD_LEGACY_SUPPORT=$(ZSTD_LEGACY_SUPPORT)
diff --git a/programs/README.md b/programs/README.md
index 268bdc52357..49a9db70641 100644
--- a/programs/README.md
+++ b/programs/README.md
@@ -202,12 +202,13 @@ Advanced options:
 
 Advanced compression options:
   --ultra                       Enable levels beyond 19, up to 22; requires more memory.
+  --max                         Enable the maximum compression settings.
   --fast[=#]                    Use to very fast compression levels. [Default: 1]
   --adapt                       Dynamically adapt compression level to I/O conditions.
   --long[=#]                    Enable long distance matching with window log #. [Default: 27]
   --patch-from=REF              Use REF as the reference point for Zstandard's diff engine.
 
-  -T#                           Spawn # compression threads. [Default: 1; pass 0 for core count.]
+  -T#                           Spawn # compression threads. [Default: 1-4, depending on logical cores; pass 0 for core count.]
   --single-thread               Share a single thread for I/O and compression (slightly different than `-T1`).
   --auto-threads={physical|logical}
                                 Use physical/logical cores when using `-T0`. [Default: Physical]
diff --git a/programs/benchfn.c b/programs/benchfn.c
index 3e042cf38f8..f31993c0a43 100644
--- a/programs/benchfn.c
+++ b/programs/benchfn.c
@@ -113,7 +113,8 @@ BMK_runOutcome_t BMK_benchFunction(BMK_benchParams_t p,
     /* init */
     {   size_t i;
         for(i = 0; i < p.blockCount; i++) {
-            memset(p.dstBuffers[i], 0xE5, p.dstCapacities[i]);  /* warm up and erase result buffer */
+            if (p.dstCapacities[i] > 0)   /* dstBuffers[i] may be NULL when dstCapacities[i] == 0 */
+                memset(p.dstBuffers[i], 0xE5, p.dstCapacities[i]);  /* warm up and erase result buffer */
     }   }
 
     /* benchmark */
diff --git a/programs/benchzstd.c b/programs/benchzstd.c
index f55c8697504..4bc300b81b6 100644
--- a/programs/benchzstd.c
+++ b/programs/benchzstd.c
@@ -517,7 +517,7 @@ static BMK_benchOutcome_t BMK_benchMemAdvancedNoAlloc(
                                 : chunkSize;
                 srcPtr += chunkSize;
                 cPtr += cCapacities[chunkID];
-                resPtr += chunkSize;
+                resPtr += resSizes[chunkID];
                 remaining -= chunkSize;
                 if (adv->mode == BMK_decodeOnly) {
                     cSizes[chunkID]  = chunkSize;
diff --git a/programs/dibio.c b/programs/dibio.c
index dc629103ba2..245fb80e072 100644
--- a/programs/dibio.c
+++ b/programs/dibio.c
@@ -27,6 +27,7 @@
 #include          /* memset */
 #include           /* fprintf, fopen, ftello64 */
 #include           /* errno */
+#include          /* INT_MAX */
 
 #include "timefn.h"         /* UTIL_time_t, UTIL_clockSpanMicro, UTIL_getTime */
 #include "../lib/common/debug.h" /* assert */
@@ -270,6 +271,7 @@ typedef struct {
  */
 static fileStats DiB_fileStats(const char** fileNamesTable, int nbFiles, size_t chunkSize, int displayLevel)
 {
+    size_t const maxNbSamples = MIN((size_t)INT_MAX, (size_t)-1 / sizeof(size_t));
     fileStats fs;
     int n;
     memset(&fs, 0, sizeof(fs));
@@ -279,6 +281,7 @@ static fileStats DiB_fileStats(const char** fileNamesTable, int nbFiles, size_t
 
     for (n=0; n maxNbSamples - (size_t)fs.nbSamples)
+        EXM_THROW(14, "too many samples for dictionary training");
+      fs.nbSamples += (int)fileNbSamples;
+
       /* the case where we are breaking up files in sample chunks */
       if (chunkSize > 0) {
         /* TODO: is there a minimum sample size? Can we have a 1-byte sample? */
-        fs.nbSamples += (int)((fileSize + chunkSize-1) / chunkSize);
         fs.totalSizeToLoad += fileSize;
       }
       else {
@@ -308,7 +317,6 @@ static fileStats DiB_fileStats(const char** fileNamesTable, int nbFiles, size_t
           DISPLAYLEVEL(3, "Sample file '%s' is too large, limiting to %d KB\n",
               fileNamesTable[n], SAMPLESIZE_MAX / (1 KB));
         }
-        fs.nbSamples += 1;
         fs.totalSizeToLoad += MIN(fileSize, SAMPLESIZE_MAX);
       }
     }
@@ -357,7 +365,7 @@ int DiB_trainFromFiles(const char* dictFileName, size_t maxDictSize,
             loadedSize = (size_t)MIN(loadedSize, memLimit);
         }
         srcBuffer = malloc(loadedSize+NOISELENGTH);
-        sampleSizes = (size_t*)malloc(fs.nbSamples * sizeof(size_t));
+        sampleSizes = (size_t*)malloc((size_t)fs.nbSamples * sizeof(*sampleSizes));
     }
 
     /* Checks */
diff --git a/programs/fileio.c b/programs/fileio.c
index 78a906d533b..b22d1b49f0c 100644
--- a/programs/fileio.c
+++ b/programs/fileio.c
@@ -850,6 +850,8 @@ FIO_openDstFile(FIO_ctx_t* fCtx, FIO_prefs_t* const prefs,
                 const char* srcFileName, const char* dstFileName,
                 const int mode)
 {
+    stat_t dstStat;
+    int dstExists;
     if (prefs->testMode) return NULL;  /* do not open file in test mode */
 
     assert(dstFileName != NULL);
@@ -869,7 +871,11 @@ FIO_openDstFile(FIO_ctx_t* fCtx, FIO_prefs_t* const prefs,
         return NULL;
     }
 
-    if (UTIL_isRegularFile(dstFileName)) {
+    dstExists = UTIL_stat(dstFileName, &dstStat);
+    /* A dangling symbolic link counts as an existing file :
+     * writing through it would create its target. */
+    if ( (dstExists && UTIL_isRegularFileStat(&dstStat))
+      || (!dstExists && UTIL_isLink(dstFileName)) ) {
         /* Check if destination file already exists */
 #if !defined(_WIN32)
         /* this test does not work on Windows :
@@ -891,7 +897,12 @@ FIO_openDstFile(FIO_ctx_t* fCtx, FIO_prefs_t* const prefs,
                 return NULL;
         }
         /* need to unlink */
-        FIO_removeFile(dstFileName);
+        if (dstExists) {
+            /* still exists only if remove() failed */
+            dstExists = FIO_removeFile(dstFileName) != 0;
+        } else {
+            (void)remove(dstFileName);  /* dangling link */
+        }
     }
 
     {
@@ -906,9 +917,22 @@ FIO_openDstFile(FIO_ctx_t* fCtx, FIO_prefs_t* const prefs,
             f = _fdopen(fd, "wb");
         }
 #else
-        const int openflags = O_WRONLY|O_CREAT|O_TRUNC;
-        const int fd = open(dstFileName, openflags, mode);
+        /* A new file is created with O_EXCL, which fails on any symbolic link,
+         * even a dangling one : output never goes to the target of a link.
+         * An existing file is either a regular file which could not be removed,
+         * or not a regular file (/dev/null, pipe) : it is opened as it is. */
+        int const dstIsSpecial = dstExists && !UTIL_isRegularFileStat(&dstStat);
+        const int openflags = !dstExists ? O_WRONLY|O_CREAT|O_EXCL
+                            : dstIsSpecial ? O_WRONLY
+                            : O_WRONLY|O_TRUNC;
+        int fd = open(dstFileName, openflags, mode);
         FILE* f = NULL;
+        if (fd != -1 && dstIsSpecial && UTIL_isFdRegularFile(fd)) {
+            /* replaced by a regular file since UTIL_stat() : don't write into it */
+            close(fd);
+            fd = -1;
+            errno = EEXIST;
+        }
         if (fd != -1) {
             f = fdopen(fd, "wb");
         }
@@ -2829,7 +2853,7 @@ FIO_decompressGzFrame(dRess_t* ress, const char* srcFileName)
 
 #ifdef ZSTD_LZMADECOMPRESS
 static unsigned long long
-FIO_decompressLzmaFrame(dRess_t* ress,
+FIO_decompressLzmaFrame(dRess_t* ress, const FIO_prefs_t* const prefs,
                         const char* srcFileName, int plain_lzma)
 {
     unsigned long long outFileSize = 0;
@@ -2841,10 +2865,12 @@ FIO_decompressLzmaFrame(dRess_t* ress,
 
     strm.next_in = 0;
     strm.avail_in = 0;
+    /* --memory also limits xz/lzma decoding : liblzma refuses to allocate more */
+    assert(prefs->memLimit > 0);
     if (plain_lzma) {
-        initRet = lzma_alone_decoder(&strm, UINT64_MAX); /* LZMA */
+        initRet = lzma_alone_decoder(&strm, prefs->memLimit); /* LZMA */
     } else {
-        initRet = lzma_stream_decoder(&strm, UINT64_MAX, 0); /* XZ */
+        initRet = lzma_stream_decoder(&strm, prefs->memLimit, 0); /* XZ */
     }
 
     if (initRet != LZMA_OK) {
@@ -2874,6 +2900,15 @@ FIO_decompressLzmaFrame(dRess_t* ress,
             DISPLAYLEVEL(1, "zstd: %s: premature lzma end \n", srcFileName);
             decodingError = 1; break;
         }
+        if (ret == LZMA_MEMLIMIT_ERROR) {
+            U64 const memNeeded = lzma_memusage(&strm);
+            U64 const memMB = (memNeeded >> 20) + ((memNeeded & ((1 MB) - 1)) != 0);
+            DISPLAYLEVEL(1, "%s : Memory needed by xz/lzma decoder larger than maximum : %llu > %u \n",
+                            srcFileName, (unsigned long long)memNeeded, prefs->memLimit);
+            if (memMB <= (UINT_MAX >> 20))
+                DISPLAYLEVEL(1, "%s : Use --memory=%uMB \n", srcFileName, (unsigned)memMB);
+            decodingError = 1; break;
+        }
         if (ret != LZMA_OK && ret != LZMA_STREAM_END) {
             DISPLAYLEVEL(1, "zstd: %s: lzma_code decoding error %d \n",
                             srcFileName, ret);
@@ -3031,7 +3066,7 @@ static int FIO_decompressFrames(FIO_ctx_t* const fCtx,
         } else if ((buf[0] == 0xFD && buf[1] == 0x37)  /* xz magic number */
                 || (buf[0] == 0x5D && buf[1] == 0x00)) { /* lzma header (no magic number) */
 #ifdef ZSTD_LZMADECOMPRESS
-            unsigned long long const frameSize = FIO_decompressLzmaFrame(&ress, srcFileName, buf[0] != 0xFD);
+            unsigned long long const frameSize = FIO_decompressLzmaFrame(&ress, prefs, srcFileName, buf[0] != 0xFD);
             if (frameSize == FIO_ERROR_FRAME_DECODING) return 1;
             filesize += frameSize;
 #else
@@ -3514,10 +3549,14 @@ FIO_analyzeFrames(fileInfo_t* info, FILE* const srcFile)
             }
             /* Skippable frame */
             else if ((magicNumber & ZSTD_MAGIC_SKIPPABLE_MASK) == ZSTD_MAGIC_SKIPPABLE_START) {
-                U32 const frameSize = MEM_readLE32(headerBuffer + 4);
-                long const seek = (long)(8 + frameSize - numBytesRead);
-                ERROR_IF(LONG_SEEK(srcFile, seek, SEEK_CUR) != 0,
-                        info_frame_error, "Error: could not find end of skippable frame");
+                /* The generic minimum-header check permits six or seven bytes. */
+                ERROR_IF(numBytesRead < ZSTD_SKIPPABLEHEADERSIZE, info_truncated_input,
+                        "Error: reached end of file with incomplete skippable frame");
+                {   U32 const frameSize = MEM_readLE32(headerBuffer + 4);
+                    S64 const seek = (S64)ZSTD_SKIPPABLEHEADERSIZE + frameSize - (S64)numBytesRead;
+                    ERROR_IF(LONG_SEEK(srcFile, seek, SEEK_CUR) != 0,
+                            info_frame_error, "Error: could not find end of skippable frame");
+                }
                 info->numSkippableFrames++;
             }
             /* unknown content */
diff --git a/programs/fileio_asyncio.c b/programs/fileio_asyncio.c
index 42a47201656..c9d8ea1718e 100644
--- a/programs/fileio_asyncio.c
+++ b/programs/fileio_asyncio.c
@@ -614,7 +614,8 @@ size_t AIO_ReadPool_fillBuffer(ReadPoolCtx_t* ctx, size_t n) {
      * and coalesce the remaining bytes with the next job's buffer */
     if (ctx->srcBufferLoaded > 0) {
         useCoalesce = 1;
-        memcpy(ctx->coalesceBuffer, ctx->srcBuffer, ctx->srcBufferLoaded);
+        /* Remaining bytes may already be in coalesceBuffer. */
+        memmove(ctx->coalesceBuffer, ctx->srcBuffer, ctx->srcBufferLoaded);
         ctx->srcBuffer = ctx->coalesceBuffer;
     }
 
diff --git a/programs/util.c b/programs/util.c
index 652530b1223..f9fce9f5b63 100644
--- a/programs/util.c
+++ b/programs/util.c
@@ -490,6 +490,17 @@ int UTIL_isBlockDevStat(const stat_t* statbuf)
     return 0;
 }
 
+#if defined(_WIN32)
+/* Symbolic links and junctions are "name surrogate" reparse points.
+ * Other reparse points (OneDrive placeholders, deduplicated files, ...)
+ * are regular files and directories : they must not be treated as links. */
+static int UTIL_isNameSurrogate(DWORD attributes, DWORD reparseTag)
+{
+    return (attributes & FILE_ATTRIBUTE_REPARSE_POINT)
+        && IsReparseTagNameSurrogate(reparseTag);
+}
+#endif
+
 int UTIL_isLink(const char* infilename)
 {
     UTIL_TRACE_CALL("UTIL_isLink(%s)", infilename);
@@ -503,6 +514,24 @@ int UTIL_isLink(const char* infilename)
             return 1;
         }
     }
+#elif defined(_WIN32)
+    {
+        DWORD const attributes = GetFileAttributesA(infilename);
+        if ( attributes != INVALID_FILE_ATTRIBUTES
+          && (attributes & FILE_ATTRIBUTE_REPARSE_POINT) ) {
+            /* GetFileAttributes() does not report the reparse tag, FindFirstFile() does */
+            WIN32_FIND_DATAA findData;
+            HANDLE const hFind = FindFirstFileA(infilename, &findData);
+            if (hFind != INVALID_HANDLE_VALUE) {
+                int const isLink = UTIL_isNameSurrogate(findData.dwFileAttributes, findData.dwReserved0);
+                FindClose(hFind);
+                if (isLink) {
+                    UTIL_TRACE_RET(1);
+                    return 1;
+                }
+            }
+        }
+    }
 #endif
     (void)infilename;
     UTIL_TRACE_RET(0);
@@ -730,6 +759,13 @@ static const char** UTIL_createLinePointers(char* buffer, size_t numLines, size_
             len++;
         }
 
+#if defined(_WIN32)
+        /* note: read in binary mode, so the CRT no longer folds CRLF; CR is legal in a filename elsewhere */
+        if (len > 0 && buffer[pos + len - 1] == '\r') {
+            buffer[pos + len - 1] = '\0';
+        }
+#endif
+
         /* Move past this string and its null terminator */
         pos += len;
         if (pos < bufferSize) pos++;  /* Skip the null terminator if we're not at buffer end */
@@ -942,6 +978,11 @@ static int UTIL_prepareFileList(const char* dirName,
         memcpy(path+dirLength+1, cFile.cFileName, fnameLength);
         pathLength = dirLength+1+fnameLength;
         path[pathLength] = 0;
+        if (!followLinks && UTIL_isNameSurrogate(cFile.dwFileAttributes, cFile.dwReserved0)) {
+            UTIL_DISPLAYLEVEL(2, "Warning : %s is a symbolic link, ignoring\n", path);
+            free(path);
+            continue;
+        }
         if (cFile.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
             if ( strcmp (cFile.cFileName, "..") == 0
               || strcmp (cFile.cFileName, ".") == 0 )
diff --git a/programs/zstd.1 b/programs/zstd.1
index e49a182d506..cdaee14ac83 100644
--- a/programs/zstd.1
+++ b/programs/zstd.1
@@ -108,7 +108,7 @@ Display information related to a zstd compressed file, such as size, ratio, and
 \fB\-\-fast[=#]\fR: switch to ultra\-fast compression levels\. If \fB=#\fR is not present, it defaults to \fB1\fR\. The higher the value, the faster the compression speed, at the cost of some compression ratio\. This setting overwrites compression level if one was set previously\. Similarly, if a compression level is set after \fB\-\-fast\fR, it overrides it\.
 .
 .IP "\(bu" 4
-\fB\-T#\fR, \fB\-\-threads=#\fR: Compress using \fB#\fR working threads (default: between 1 and 4 depending on physical CPU cores; see \fBZSTD_NBTHREADS\fR below)\. If \fB#\fR is 0, attempt to detect and use the number of physical CPU cores\. In all cases, the nb of threads is capped to \fBZSTDMT_NBWORKERS_MAX\fR, which is either 64 in 32\-bit mode, or 256 for 64\-bit environments\. This modifier does nothing if \fBzstd\fR is compiled without multithread support\.
+\fB\-T#\fR, \fB\-\-threads=#\fR: Compress using \fB#\fR working threads (default: between 1 and 4 depending on the number of logical CPU cores; see \fBZSTD_NBTHREADS\fR below)\. If \fB#\fR is 0, attempt to detect and use the number of physical CPU cores\. In all cases, the nb of threads is capped to \fBZSTDMT_NBWORKERS_MAX\fR, which is either 64 in 32\-bit mode, or 256 for 64\-bit environments\. This modifier does nothing if \fBzstd\fR is compiled without multithread support\.
 .
 .IP "\(bu" 4
 \fB\-\-single\-thread\fR: Use a single thread for both I/O and compression\. As compression is serialized with I/O, this can be slightly slower\. Single\-thread mode features significantly lower memory usage, which can be useful for systems with limited amount of memory, such as 32\-bit systems\.
@@ -132,7 +132,7 @@ Note 2: this mode is different from \fB\-T1\fR, which spawns 1 compression threa
 \fB\-\-long[=#]\fR: enables long distance matching with \fB#\fR \fBwindowLog\fR, if \fB#\fR is not present it defaults to \fB27\fR\. This increases the window size (\fBwindowLog\fR) and memory usage for both the compressor and decompressor\. This setting is designed to improve the compression ratio for files with long matches at a large distance\.
 .
 .IP
-Note: If \fBwindowLog\fR is set to larger than 27, \fB\-\-long=windowLog\fR or \fB\-\-memory=windowSize\fR needs to be passed to the decompressor\.
+Note: If \fBwindowLog\fR is set to larger than 27, \fB\-\-long=windowLog\fR, \fB\-\-max\fR, or \fB\-\-memory=windowSize\fR needs to be passed to the decompressor\.
 .
 .IP "\(bu" 4
 \fB\-\-max\fR: set advanced parameters to maximum compression\. warning: this setting is very slow and uses a lot of resources\. It\'s inappropriate for 32\-bit mode and therefore disabled in this mode\.
@@ -268,7 +268,7 @@ Employing environment variables to set parameters has security implications\. Th
 \fBZSTD_CLEVEL\fR can be used to set the level between 1 and 19 (the "normal" range)\. If the value of \fBZSTD_CLEVEL\fR is not a valid integer, it will be ignored with a warning message\. \fBZSTD_CLEVEL\fR just replaces the default compression level (\fB3\fR)\.
 .
 .P
-\fBZSTD_NBTHREADS\fR can be used to set the number of threads \fBzstd\fR will attempt to use during compression\. If the value of \fBZSTD_NBTHREADS\fR is not a valid unsigned integer, it will be ignored with a warning message\. \fBZSTD_NBTHREADS\fR has a default value of \fBmax(1, min(4, nbCores/4))\fR, and is capped at ZSTDMT_NBWORKERS_MAX==200\. \fBzstd\fR must be compiled with multithread support for this variable to have any effect\.
+\fBZSTD_NBTHREADS\fR can be used to set the number of threads \fBzstd\fR will attempt to use during compression\. If the value of \fBZSTD_NBTHREADS\fR is not a valid unsigned integer, it will be ignored with a warning message\. \fBZSTD_NBTHREADS\fR has a default value of \fBmax(1, min(4, nbLogicalCores/4))\fR, and is capped at \fBZSTDMT_NBWORKERS_MAX\fR, which is either 64 in 32\-bit mode, or 256 for 64\-bit environments\. \fBzstd\fR must be compiled with multithread support for this variable to have any effect\.
 .
 .P
 They can both be overridden by corresponding command line arguments: \fB\-#\fR for compression level and \fB\-T#\fR for number of compression threads\.
@@ -294,7 +294,7 @@ Specify the maximum number of bits for a match distance\.
 The higher number of increases the chance to find a match which usually improves compression ratio\. It also increases memory requirements for the compressor and decompressor\. The minimum \fIwlog\fR is 10 (1 KiB) and the maximum is 30 (1 GiB) on 32\-bit platforms and 31 (2 GiB) on 64\-bit platforms\.
 .
 .IP
-Note: If \fBwindowLog\fR is set to larger than 27, \fB\-\-long=windowLog\fR or \fB\-\-memory=windowSize\fR needs to be passed to the decompressor\.
+Note: If \fBwindowLog\fR is set to larger than 27, \fB\-\-long=windowLog\fR, \fB\-\-max\fR, or \fB\-\-memory=windowSize\fR needs to be passed to the decompressor\.
 .
 .TP
 \fBhashLog\fR=\fIhlog\fR, \fBhlog\fR=\fIhlog\fR
@@ -530,7 +530,7 @@ The \fBzstd\fR CLI provides a benchmarking mode that can be used to easily find
 It\'s possible to pass multiple files to the benchmark, and even a directory with \fB\-r DIRECTORY\fR\. When no \fBFILE\fR is provided, the benchmark will use a procedurally generated \fBlorem ipsum\fR text\.
 .
 .P
-Benchmarking will employ \fBmax(1, min(4, nbCores/4))\fR worker threads by default in order to match the behavior of the normal CLI I/O\.
+Benchmarking will employ 1 worker thread by default\. Use \fB\-T#\fR to benchmark with more worker threads\.
 .
 .IP "\(bu" 4
 \fB\-b#\fR: benchmark file(s) using compression level #
diff --git a/programs/zstd.1.md b/programs/zstd.1.md
index 1721441dfbb..d46f395dc3f 100644
--- a/programs/zstd.1.md
+++ b/programs/zstd.1.md
@@ -126,7 +126,7 @@ the last one takes effect.
     This setting overwrites compression level if one was set previously.
     Similarly, if a compression level is set after `--fast`, it overrides it.
 * `-T#`, `--threads=#`:
-    Compress using `#` working threads (default: between 1 and 4 depending on physical CPU cores; see `ZSTD_NBTHREADS` below).
+    Compress using `#` working threads (default: between 1 and 4 depending on the number of logical CPU cores; see `ZSTD_NBTHREADS` below).
     If `#` is 0, attempt to detect and use the number of physical CPU cores.
     In all cases, the nb of threads is capped to `ZSTDMT_NBWORKERS_MAX`,
     which is either 64 in 32-bit mode, or 256 for 64-bit environments.
@@ -164,8 +164,8 @@ the last one takes effect.
     This setting is designed to improve the compression ratio for files with
     long matches at a large distance.
 
-    Note: If `windowLog` is set to larger than 27, `--long=windowLog` or
-    `--memory=windowSize` needs to be passed to the decompressor.
+    Note: If `windowLog` is set to larger than 27, `--long=windowLog`,
+    `--max`, or `--memory=windowSize` needs to be passed to the decompressor.
 * `-D DICT`:
     use `DICT` as Dictionary to compress or decompress FILE(s)
 * `--patch-from FILE`:
@@ -210,7 +210,7 @@ the last one takes effect.
     Set a memory usage limit. By default, `zstd` uses 128 MiB for decompression
     as the maximum amount of memory the decompressor is allowed to use, but you can
     override this manually if need be in either direction (i.e. you can increase or
-    decrease it).
+    decrease it). This limit also applies when decompressing `.xz` and `.lzma` files.
 
     This is also used during compression when using with `--patch-from=`. In this case,
     this parameter overrides that maximum size allowed for a dictionary. (128 MiB).
@@ -348,7 +348,8 @@ If the value of `ZSTD_CLEVEL` is not a valid integer, it will be ignored with a
 
 `ZSTD_NBTHREADS` can be used to set the number of threads `zstd` will attempt to use during compression.
 If the value of `ZSTD_NBTHREADS` is not a valid unsigned integer, it will be ignored with a warning message.
-`ZSTD_NBTHREADS` has a default value of `max(1, min(4, nbCores/4))`, and is capped at ZSTDMT_NBWORKERS_MAX==200.
+`ZSTD_NBTHREADS` has a default value of `max(1, min(4, nbLogicalCores/4))`, and is capped at `ZSTDMT_NBWORKERS_MAX`,
+which is either 64 in 32-bit mode, or 256 for 64-bit environments.
 `zstd` must be compiled with multithread support for this variable to have any effect.
 
 They can both be overridden by corresponding command line arguments:
@@ -385,8 +386,8 @@ The list of available _options_:
     The minimum _wlog_ is 10 (1 KiB) and the maximum is 30 (1 GiB) on 32-bit
     platforms and 31 (2 GiB) on 64-bit platforms.
 
-    Note: If `windowLog` is set to larger than 27, `--long=windowLog` or
-    `--memory=windowSize` needs to be passed to the decompressor.
+    Note: If `windowLog` is set to larger than 27, `--long=windowLog`,
+    `--max`, or `--memory=windowSize` needs to be passed to the decompressor.
 
 - `hashLog`=_hlog_, `hlog`=_hlog_:
     Specify the maximum number of bits for a hash table.
@@ -674,7 +675,7 @@ Note that results are very dependent on the content being compressed.
 It's possible to pass multiple files to the benchmark, and even a directory with `-r DIRECTORY`.
 When no `FILE` is provided, the benchmark will use a procedurally generated `lorem ipsum` text.
 
-Benchmarking will employ `max(1, min(4, nbCores/4))` worker threads by default in order to match the behavior of the normal CLI I/O.
+Benchmarking will employ 1 worker thread by default. Use `-T#` to benchmark with more worker threads.
 
 * `-b#`:
     benchmark file(s) using compression level #
diff --git a/programs/zstdcli.c b/programs/zstdcli.c
index d0821db9741..eef861722c2 100644
--- a/programs/zstdcli.c
+++ b/programs/zstdcli.c
@@ -225,6 +225,7 @@ static void usageAdvanced(const char* programName)
     DISPLAYOUT("\n");
     DISPLAYOUT("Advanced compression options:\n");
     DISPLAYOUT("  --ultra                       Enable levels beyond %i, up to %i; requires more memory.\n", ZSTDCLI_CLEVEL_MAX, ZSTD_maxCLevel());
+    DISPLAYOUT("  --max                         Enable the maximum compression settings.\n");
     DISPLAYOUT("  --fast[=#]                    Use to very fast compression levels. [Default: %u]\n", 1);
 #ifdef ZSTD_GZCOMPRESS
     if (exeNameMatch(programName, ZSTD_GZ)) {     /* behave like gzip */
diff --git a/tests/Makefile b/tests/Makefile
index e268ddeb9e4..d12e42f3613 100644
--- a/tests/Makefile
+++ b/tests/Makefile
@@ -149,6 +149,7 @@ zstdmt_l_%.o : $(LIB_SRCDIR)/legacy/%.c
 FULLBENCHS := fullbench fullbench32
 CLEAN += $(FULLBENCHS)
 fullbench32: CPPFLAGS += -m32
+fullbench32: LDFLAGS := $(LDFLAGS_32)
 $(FULLBENCHS) : CPPFLAGS += $(MULTITHREAD_CPP) -Wno-deprecated-declarations
 $(FULLBENCHS) : LDFLAGS += $(MULTITHREAD_LD)
 $(FULLBENCHS) : DEBUGFLAGS = -DNDEBUG  # turn off assert() for speed measurements
@@ -174,6 +175,7 @@ fuzzer : $(ZSTDMT_OBJECTS)
 fuzzer fuzzer32 : $(ZDICT_FILES) $(PRGDIR)/util.c $(PRGDIR)/timefn.c $(PRGDIR)/datagen.c fuzzer.c
 
 fuzzer32 : CFLAGS += -m32 $(MULTITHREAD)
+fuzzer32 : LDFLAGS := $(LDFLAGS_32)
 fuzzer32 : $(ZSTD_FILES)
 	$(LINK.c) $^ -o $@$(EXT)
 
@@ -186,6 +188,7 @@ ZSTREAM_LOCAL_FILES := $(PRGDIR)/datagen.c $(PRGDIR)/util.c $(PRGDIR)/timefn.c s
 ZSTREAM_PROPER_FILES := $(ZDICT_FILES) $(ZSTREAM_LOCAL_FILES)
 ZSTREAMFILES := $(ZSTD_FILES) $(ZSTREAM_PROPER_FILES)
 zstreamtest32 : CFLAGS += -m32
+zstreamtest32 : LDFLAGS := $(LDFLAGS_32)
 zstreamtest zstreamtest32 : CPPFLAGS += $(MULTITHREAD_CPP)
 zstreamtest zstreamtest32 : LDFLAGS += $(MULTITHREAD_LD)
 zstreamtest : $(ZSTDMT_OBJECTS) $(ZSTREAM_PROPER_FILES)
diff --git a/tests/check_size.py b/tests/check_size.py
index 028b0a9c07c..9bfdeb8c99b 100755
--- a/tests/check_size.py
+++ b/tests/check_size.py
@@ -25,6 +25,7 @@
 	sys.exit(1)
 
 size = os.path.getsize(file)
+print(f"{file} is {size} bytes, {100 * size / limit:.0f}% of the limit of {limit} bytes")
 
 if size > limit:
 	print(f"file {file} is {size} bytes, which is greater than the limit of {limit} bytes")
diff --git a/tests/cli-tests/basic/list-short-skippable.sh b/tests/cli-tests/basic/list-short-skippable.sh
new file mode 100755
index 00000000000..629887d6ab3
--- /dev/null
+++ b/tests/cli-tests/basic/list-short-skippable.sh
@@ -0,0 +1,65 @@
+#!/bin/sh
+
+set -eu
+
+# A complete frame whose dictionary-ID bytes leave known values in the list
+# parser's header buffer. Listing does not need the referenced dictionary.
+# Layout: magic, single-segment descriptor, dictID 11 22 de ad, FCS 4,
+# and a final raw block containing four bytes.
+printf '\050\265\057\375\043\021\042\336\255\004\041\000\000AAAA' > prefix.zst
+
+check_incomplete_header() {
+    if zstd -l "$1" > list.stdout 2> list.stderr; then
+        die "list accepted an incomplete skippable header: $1"
+    fi
+    grep -Fq 'incomplete' list.stderr
+    if grep -Fq 'seeked to position' list.stderr; then
+        die "list used a length from an incomplete header: $1"
+    fi
+    if grep -Eq 'AddressSanitizer|UndefinedBehaviorSanitizer|runtime error:' list.stderr; then
+        die "sanitizer failure while listing $1"
+    fi
+}
+
+# An empty file is incomplete, but no next-frame bytes after a complete frame
+# is a valid boundary.
+printf '' > short.zst
+check_incomplete_header short.zst
+zstd -l prefix.zst > /dev/null
+
+# Exercise the lowest and highest skippable magics and every nonempty
+# truncated header, both at the start of a file and after a frame that primes
+# the header buffer. The loop values are octal escapes for the low magic byte,
+# 0x50 and 0x5F.
+for magic in 120 137; do
+    printf "\\$magic\052\115\030\000\000\000\000" > header.zst
+    for length in 1 2 3 4 5 6 7; do
+        dd if=header.zst of=short.zst bs=1 count="$length" 2>/dev/null
+        check_incomplete_header short.zst
+        cat prefix.zst short.zst > after.zst
+        check_incomplete_header after.zst
+    done
+
+    # A complete eight-byte, zero-payload skippable frame is still valid.
+    zstd -l header.zst > /dev/null
+    cat prefix.zst header.zst > after.zst
+    zstd -l after.zst > /dev/null
+done
+
+# Complete nonempty payloads still skip to the next frame. The short case
+# requires seeking backward after read-ahead; the long case seeks forward.
+printf '\120\052\115\030\004\000\000\000data' > short-payload.zst
+printf '\137\052\115\030\040\000\000\0000123456789abcdef0123456789abcdef' > long-payload.zst
+for payload in short-payload.zst long-payload.zst; do
+    zstd -l "$payload" > /dev/null
+    cat "$payload" prefix.zst > after.zst
+    zstd -l after.zst > /dev/null
+done
+
+# Preserve the separate signed-64-bit seek fix: this complete header declares
+# a payload extending to position 2^32, and must fail without seeking backward.
+printf '\120\052\115\030\370\377\377\377' > large.zst
+if zstd -l large.zst > list.stdout 2> list.stderr; then
+    die 'list accepted an incomplete large skippable frame'
+fi
+grep -Fq 'seeked to position 4294967296' list.stderr
diff --git a/tests/cli-tests/decompression/skippable-overlap.sh b/tests/cli-tests/decompression/skippable-overlap.sh
new file mode 100755
index 00000000000..d72d26cc599
--- /dev/null
+++ b/tests/cli-tests/decompression/skippable-overlap.sh
@@ -0,0 +1,17 @@
+#!/bin/sh
+
+set -eu
+
+# An 8-byte empty skippable frame followed by a 9-byte empty zstd frame.
+# The initial 18-byte header refill moves this 17-byte stream into the
+# coalesce buffer. After consuming the skippable frame, the next refill
+# moves 9 bytes from offset 8 back to offset 0. Run under ASan to detect
+# an overlapping memcpy.
+printf '\120\052\115\030\000\000\000\000\050\265\057\375\040\000\001\000\000' > skippable-overlap.zst
+
+# This is a valid stream and must decode successfully to empty output.
+for io in --asyncio --no-asyncio; do
+    for operation in -t -dc; do
+        zstd -q "$io" "$operation" skippable-overlap.zst
+    done
+done
diff --git a/tests/cli-tests/decompression/skippable-overlap.sh.stderr.exact b/tests/cli-tests/decompression/skippable-overlap.sh.stderr.exact
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/tests/cli-tests/decompression/skippable-overlap.sh.stdout.exact b/tests/cli-tests/decompression/skippable-overlap.sh.stdout.exact
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/tests/cli-tests/dict-builder/sample-count-overflow.sh b/tests/cli-tests/dict-builder/sample-count-overflow.sh
new file mode 100755
index 00000000000..6fda5c077f0
--- /dev/null
+++ b/tests/cli-tests/dict-builder/sample-count-overflow.sh
@@ -0,0 +1,45 @@
+#!/bin/sh
+
+set -eu
+
+# The count must fit both an int and a size_t array allocation. Query the
+# tested binary, since the test runner may be executing a 32-bit build.
+case "$(zstd -V)" in
+    *32-bit*) max_samples=1073741823 ;;
+    *)        max_samples=2147483647 ;;
+esac
+
+printf '%s\n' 'Error 14 : too many samples for dictionary training' > expected.stderr
+
+make_sparse_file() {
+    dd if=/dev/zero of="$1" bs=1 count=0 seek="$2" 2>/dev/null
+}
+
+check_too_many_samples() {
+    status=0
+    zstd -q --train --memory=1MB "$@" -o dictionary > actual.stdout 2> actual.stderr || status=$?
+    test "$status" -eq 14 || die "expected sample-count rejection, got exit $status: $*"
+    test ! -s actual.stdout || die 'unexpected dictionary-training output'
+    cmp expected.stderr actual.stderr
+    test ! -e dictionary || die 'oversized sample set produced a dictionary'
+}
+
+# The first count above the limit. On 32-bit builds this is the original
+# 1 GiB / -B1 reproducer, whose sample-table allocation wrapped to zero.
+make_sparse_file sample "$((max_samples + 1))"
+for trainer in --train --train-cover --train-fastcover --train-legacy; do
+    check_too_many_samples "$trainer" -B1 sample
+done
+
+# A nonzero wrapped allocation is unsafe too.
+make_sparse_file sample "$((max_samples + 2))"
+check_too_many_samples --split=1 sample
+
+# Include the last partial chunk when checking the limit.
+make_sparse_file sample "$((max_samples * 2 + 1))"
+check_too_many_samples --split=2 sample
+
+# Individually valid counts must also be checked when they are added.
+make_sparse_file sample "$max_samples"
+make_sparse_file extra 1
+check_too_many_samples --split=1 sample extra
diff --git a/tests/cli-tests/file-stat/compress-file-to-dir-without-write-perm.sh.stderr.exact b/tests/cli-tests/file-stat/compress-file-to-dir-without-write-perm.sh.stderr.exact
index 05d91ffc9b7..390c792f3e6 100644
--- a/tests/cli-tests/file-stat/compress-file-to-dir-without-write-perm.sh.stderr.exact
+++ b/tests/cli-tests/file-stat/compress-file-to-dir-without-write-perm.sh.stderr.exact
@@ -18,9 +18,9 @@ Trace:FileStat:  < 1
 Trace:FileStat:  > UTIL_stat(-1, out/file.zst)
 Trace:FileStat:  < 0
 Trace:FileStat: < 0
-Trace:FileStat: > UTIL_isRegularFile(out/file.zst)
-Trace:FileStat:  > UTIL_stat(-1, out/file.zst)
-Trace:FileStat:  < 0
+Trace:FileStat: > UTIL_stat(-1, out/file.zst)
+Trace:FileStat: < 0
+Trace:FileStat: > UTIL_isLink(out/file.zst)
 Trace:FileStat: < 0
 Trace:FileStat: > UTIL_isFdRegularFile(-1)
 Trace:FileStat: < 0
diff --git a/tests/cli-tests/file-stat/compress-file-to-file.sh.stderr.glob b/tests/cli-tests/file-stat/compress-file-to-file.sh.stderr.glob
index 17a677deba4..3500d7b529b 100644
--- a/tests/cli-tests/file-stat/compress-file-to-file.sh.stderr.glob
+++ b/tests/cli-tests/file-stat/compress-file-to-file.sh.stderr.glob
@@ -18,9 +18,9 @@ Trace:FileStat:  < 1
 Trace:FileStat:  > UTIL_stat(-1, file.zst)
 Trace:FileStat:  < 0
 Trace:FileStat: < 0
-Trace:FileStat: > UTIL_isRegularFile(file.zst)
-Trace:FileStat:  > UTIL_stat(-1, file.zst)
-Trace:FileStat:  < 0
+Trace:FileStat: > UTIL_stat(-1, file.zst)
+Trace:FileStat: < 0
+Trace:FileStat: > UTIL_isLink(file.zst)
 Trace:FileStat: < 0
 Trace:FileStat: > UTIL_isFdRegularFile(*)
 Trace:FileStat:  > UTIL_stat(*, )
diff --git a/tests/cli-tests/file-stat/compress-stdin-to-file.sh.stderr.glob b/tests/cli-tests/file-stat/compress-stdin-to-file.sh.stderr.glob
index 38732cbe995..b315792dc3a 100644
--- a/tests/cli-tests/file-stat/compress-stdin-to-file.sh.stderr.glob
+++ b/tests/cli-tests/file-stat/compress-stdin-to-file.sh.stderr.glob
@@ -10,9 +10,9 @@ Trace:FileStat: > UTIL_isSameFile(/*stdin*\, file.zst)
 Trace:FileStat:  > UTIL_stat(-1, /*stdin*\)
 Trace:FileStat:  < 0
 Trace:FileStat: < 0
-Trace:FileStat: > UTIL_isRegularFile(file.zst)
-Trace:FileStat:  > UTIL_stat(-1, file.zst)
-Trace:FileStat:  < 0
+Trace:FileStat: > UTIL_stat(-1, file.zst)
+Trace:FileStat: < 0
+Trace:FileStat: > UTIL_isLink(file.zst)
 Trace:FileStat: < 0
 Trace:FileStat: > UTIL_isFdRegularFile(*)
 Trace:FileStat:  > UTIL_stat(*, )
diff --git a/tests/cli-tests/file-stat/decompress-file-to-file.sh.stderr.glob b/tests/cli-tests/file-stat/decompress-file-to-file.sh.stderr.glob
index ce194dfb376..2f5267451ac 100644
--- a/tests/cli-tests/file-stat/decompress-file-to-file.sh.stderr.glob
+++ b/tests/cli-tests/file-stat/decompress-file-to-file.sh.stderr.glob
@@ -18,9 +18,9 @@ Trace:FileStat:  < 1
 Trace:FileStat:  > UTIL_stat(-1, file)
 Trace:FileStat:  < 0
 Trace:FileStat: < 0
-Trace:FileStat: > UTIL_isRegularFile(file)
-Trace:FileStat:  > UTIL_stat(-1, file)
-Trace:FileStat:  < 0
+Trace:FileStat: > UTIL_stat(-1, file)
+Trace:FileStat: < 0
+Trace:FileStat: > UTIL_isLink(file)
 Trace:FileStat: < 0
 Trace:FileStat: > UTIL_isFdRegularFile(*)
 Trace:FileStat:  > UTIL_stat(*, )
diff --git a/tests/cli-tests/file-stat/decompress-stdin-to-file.sh.stderr.glob b/tests/cli-tests/file-stat/decompress-stdin-to-file.sh.stderr.glob
index 9eb9208832f..88fd1af2f92 100644
--- a/tests/cli-tests/file-stat/decompress-stdin-to-file.sh.stderr.glob
+++ b/tests/cli-tests/file-stat/decompress-stdin-to-file.sh.stderr.glob
@@ -10,9 +10,9 @@ Trace:FileStat: > UTIL_isSameFile(/*stdin*\, file)
 Trace:FileStat:  > UTIL_stat(-1, /*stdin*\)
 Trace:FileStat:  < 0
 Trace:FileStat: < 0
-Trace:FileStat: > UTIL_isRegularFile(file)
-Trace:FileStat:  > UTIL_stat(-1, file)
-Trace:FileStat:  < 0
+Trace:FileStat: > UTIL_stat(-1, file)
+Trace:FileStat: < 0
+Trace:FileStat: > UTIL_isLink(file)
 Trace:FileStat: < 0
 Trace:FileStat: > UTIL_isFdRegularFile(*)
 Trace:FileStat:  > UTIL_stat(*, )
diff --git a/tests/fuzzer.c b/tests/fuzzer.c
index 32360b9a58d..55ad99d0ccf 100644
--- a/tests/fuzzer.c
+++ b/tests/fuzzer.c
@@ -44,8 +44,11 @@
 /* must be included after util.h, due to ERROR macro redefinition issue on Visual Studio */
 #include "zstd_internal.h" /* ZSTD_WORKSPACETOOLARGE_MAXDURATION, ZSTD_WORKSPACETOOLARGE_FACTOR, KB, MB */
 #include "threading.h"    /* ZSTD_pthread_create, ZSTD_pthread_join */
+#include "pool.h"         /* POOL_sizeof */
 #include "compress/hist.h" /* HIST_count_wksp */
 #include "compress/zstd_compress_internal.h" /* ZSTD_get1BlockSummary */
+#include "decompress/zstd_decompress_internal.h" /* ZSTD_DCtx */
+#include "dictBuilder/cover.h" /* COVER_selectDict */
 
 
 /*-************************************
@@ -483,6 +486,31 @@ static int threadPoolTests(void) {
     }
     DISPLAYLEVEL(3, "OK \n");
 
+    DISPLAYLEVEL(3, "thread pool test : change the pool and nbWorkers of a context: ");
+    {   ZSTD_CCtx* const cctx = ZSTD_createCCtx();
+        ZSTD_threadPool* const pool1 = ZSTD_createThreadPool(1);
+        ZSTD_threadPool* const pool2 = ZSTD_createThreadPool(1);
+        CHECK(cctx != NULL && pool1 != NULL && pool2 != NULL);
+        CHECK(!ZSTD_isError( ZSTD_CCtx_setParameter(cctx, ZSTD_c_nbWorkers, 2) ));
+        CHECK(!ZSTD_isError( ZSTD_CCtx_refThreadPool(cctx, pool1) ));
+        CHECK(!ZSTD_isError( ZSTD_compress2(cctx, compressedBuffer, compressedBufferSize, CNBuffer, CNBuffSize) ));
+        /* the next frames must use pool2, then an internal pool */
+        CHECK(!ZSTD_isError( ZSTD_CCtx_refThreadPool(cctx, pool2) ));
+        ZSTD_freeThreadPool(pool1);
+        CHECK(!ZSTD_isError( ZSTD_compress2(cctx, compressedBuffer, compressedBufferSize, CNBuffer, CNBuffSize) ));
+        {   size_t const pool2Size = POOL_sizeof(pool2);
+            /* the context must not resize a pool it shares */
+            CHECK(!ZSTD_isError( ZSTD_CCtx_setParameter(cctx, ZSTD_c_nbWorkers, 4) ));
+            CHECK(!ZSTD_isError( ZSTD_compress2(cctx, compressedBuffer, compressedBufferSize, CNBuffer, CNBuffSize) ));
+            CHECK(POOL_sizeof(pool2) == pool2Size);
+        }
+        CHECK(!ZSTD_isError( ZSTD_CCtx_refThreadPool(cctx, NULL) ));
+        ZSTD_freeThreadPool(pool2);
+        CHECK(!ZSTD_isError( ZSTD_compress2(cctx, compressedBuffer, compressedBufferSize, CNBuffer, CNBuffSize) ));
+        ZSTD_freeCCtx(cctx);
+    }
+    DISPLAYLEVEL(3, "OK \n");
+
 _end:
     free(CNBuffer);
     free(compressedBuffer);
@@ -575,6 +603,159 @@ static void test_decompressBound(unsigned tnb)
     DISPLAYLEVEL(3, "OK \n");
 }
 
+/* Size of the literals of the block built by FUZ_writeSplitLitBlock().
+ * It is larger than the decoder's 64 KB extra literal buffer, so the first
+ * kSplitLitInDst literals are stored at the end of the block's area in dst. */
+#define kSplitLitSize (64 KB + 112)
+#define kSplitLitInDst (kSplitLitSize - 64 KB)
+#define kSplitLitBlockMaxSize (kSplitLitSize + 3 + 9)
+
+/* Writes a compressed block (without block header) with raw literals and 2 sequences,
+ * which decodes to 48 literals, a match of 65539+seq1MLExtra bytes at offset 1,
+ * 49 literals, a match of 3 bytes at offset 1, and the remaining literals.
+ * The block decodes to more than ZSTD_BLOCKSIZE_MAX bytes, so the first match runs
+ * up to (or past) the literals that are stored in dst.
+ * Writes the expected output to expected, which must hold
+ * FUZ_splitLitBlockDecodedSize(seq1MLExtra) bytes.
+ * @return : size of the block */
+static size_t FUZ_writeSplitLitBlock(BYTE* block, BYTE* expected, U32 seq1MLExtra)
+{
+    U32 const seq1ML = 65539 + seq1MLExtra;
+    BYTE* op = block;
+    size_t i;
+
+    /* Literals section header: raw literals, 3 byte header */
+    MEM_writeLE24(op, (U32)(kSplitLitSize << 4) | (3 << 2));
+    op += 3;
+    for (i = 0; i < kSplitLitSize; i++) {
+        *op++ = i < kSplitLitInDst ? (BYTE)(i + 1) : 0xCC;
+    }
+
+    /* Sequences section header: 2 sequences, LL & OF RLE, ML predefined.
+     * LL code 24 (base 48, 4 extra bits), OF code 0 (repcode 1 = offset 1). */
+    *op++ = 2;
+    *op++ = 0x50;
+    *op++ = 24;
+    *op++ = 0;
+
+    /* Bitstream, read from the top down: ML initial state 57 (ML code 52: base 65539,
+     * 16 extra bits), sequence 1 ML extra bits, LL extra bits 0 (LL 48),
+     * ML state update to state 0 (ML 3), sequence 2 LL extra bits 1 (LL 49). */
+    {   U64 const bitStream = (1ULL << 36) | (57ULL << 30) | ((U64)seq1MLExtra << 14) | 1;
+        MEM_writeLE32(op, (U32)bitStream);
+        op[4] = (BYTE)(bitStream >> 32);
+        op += 5;
+    }
+
+    {   BYTE* eop = expected;
+        memcpy(eop, block + 3, 48);
+        eop += 48;
+        memset(eop, 48, seq1ML);
+        eop += seq1ML;
+        memcpy(eop, block + 3 + 48, 49);
+        eop += 49;
+        memset(eop, 97, 3);
+        eop += 3;
+        memcpy(eop, block + 3 + 97, kSplitLitSize - 97);
+    }
+
+    assert((size_t)(op - block) <= kSplitLitBlockMaxSize);
+    return (size_t)(op - block);
+}
+
+static size_t FUZ_splitLitBlockDecodedSize(U32 seq1MLExtra)
+{
+    return kSplitLitSize + 65539 + seq1MLExtra + 3;
+}
+
+static void test_decompressSplitLitBuffer(unsigned tnb)
+{
+    /* The literals are split, rather than stored after the block's output, because
+     * dstCapacity <= ZSTD_BLOCKSIZE_MAX + litSize + 2 * WILDCOPY_OVERLENGTH. */
+    size_t const dstCapacity = ZSTD_BLOCKSIZE_MAX + kSplitLitSize + 64;
+    size_t const srcCapacity = ZSTD_FRAMEHEADERSIZE_MAX + ZSTD_blockHeaderSize + kSplitLitBlockMaxSize;
+    /* seq1MLExtra = 65371 : The first match ends 18 bytes before the next literals,
+     *                       so the literals are copied to an overlapping position.
+     * seq1MLExtra = 65460 : The first match overwrites the next literals. */
+    U32 const seq1MLExtras[] = { 65371, 65460 };
+    BYTE* const src = (BYTE*)malloc(srcCapacity);
+    BYTE* const dst = (BYTE*)malloc(dstCapacity);
+    BYTE* const expected = (BYTE*)malloc(dstCapacity);
+    ZSTD_DCtx* const dctx = ZSTD_createDCtx();
+    size_t i;
+    int anySucceeded = 0;
+    CHECK(src != NULL && dst != NULL && expected != NULL && dctx != NULL);
+
+    DISPLAYLEVEL(3, "test%3u : decompress block with split literals overrun by output : ", tnb);
+    for (i = 0; i < COUNTOF(seq1MLExtras); i++) {
+        size_t const decodedSize = FUZ_splitLitBlockDecodedSize(seq1MLExtras[i]);
+        size_t blockSize;
+        size_t r;
+        CHECK(decodedSize <= dstCapacity);
+
+        /* The block API has no ZSTD_BLOCKSIZE_MAX limit on the decoded size,
+         * so the decoder must either reject the block or decode it correctly. */
+        blockSize = FUZ_writeSplitLitBlock(src, expected, seq1MLExtras[i]);
+        CHECK_Z( ZSTD_decompressBegin(dctx) );
+        r = ZSTD_decompressBlock(dctx, dst, dstCapacity, src, blockSize);
+        if (!ZSTD_isError(r)) {
+            CHECK_EQ(r, decodedSize);
+            CHECK(!memcmp(dst, expected, decodedSize));
+            anySucceeded = 1;
+        }
+
+        /* Wrap the block in a frame with windowLog 17, so blockSizeMax = ZSTD_BLOCKSIZE_MAX.
+         * The frame must be rejected, because the block decodes to more than blockSizeMax. */
+        MEM_writeLE32(src, ZSTD_MAGICNUMBER);
+        src[4] = 0x00; /* Frame header descriptor: no content size, no checksum, no dictID */
+        src[5] = 0x38; /* Window descriptor: windowLog 17 */
+        blockSize = FUZ_writeSplitLitBlock(src + 9, expected, seq1MLExtras[i]);
+        MEM_writeLE24(src + 6, 1 /* last block */ | ((U32)bt_compressed << 1) | (U32)(blockSize << 3));
+        r = ZSTD_decompress(dst, dstCapacity, src, 9 + blockSize);
+        CHECK(ZSTD_isError(r));
+    }
+    CHECK(anySucceeded); /* Must not reject all blocks */
+    DISPLAYLEVEL(3, "OK \n");
+
+    ZSTD_freeDCtx(dctx);
+    free(expected);
+    free(dst);
+    free(src);
+}
+
+static void test_decompressContinueWithoutBegin(unsigned tnb)
+{
+    char src[1000];
+    char compressed[ZSTD_COMPRESSBOUND(sizeof(src))];
+    char decoded[sizeof(src)];
+    ZSTD_DCtx* const dctx = ZSTD_createDCtx();
+    size_t cSize, cPos = 0, dPos = 0;
+    CHECK(dctx != NULL);
+
+    DISPLAYLEVEL(3, "test%3u : ZSTD_decompressContinue() without ZSTD_decompressBegin() : ", tnb);
+    memset(src, 'a', sizeof(src));
+    CHECK_VAR(cSize, ZSTD_compress(compressed, sizeof(compressed), src, sizeof(src), 1));
+
+    /* new context */
+    CHECK_EQ(ZSTD_nextSrcSizeToDecompress(dctx), 0);
+    CHECK_EQ(ZSTD_getErrorCode(ZSTD_decompressContinue(dctx, decoded, sizeof(decoded), NULL, 0)), ZSTD_error_stage_wrong);
+
+    /* finished frame */
+    CHECK_Z(ZSTD_decompressBegin(dctx));
+    while (ZSTD_nextSrcSizeToDecompress(dctx) != 0) {
+        size_t const inSize = ZSTD_nextSrcSizeToDecompress(dctx);
+        CHECK_NEWV(outSize, ZSTD_decompressContinue(dctx, decoded + dPos, sizeof(decoded) - dPos, compressed + cPos, inSize));
+        cPos += inSize;
+        dPos += outSize;
+    }
+    CHECK_EQ(cPos, cSize);
+    CHECK_EQ(dPos, sizeof(src));
+    CHECK_EQ(ZSTD_getErrorCode(ZSTD_decompressContinue(dctx, decoded, sizeof(decoded), NULL, 0)), ZSTD_error_stage_wrong);
+    DISPLAYLEVEL(3, "OK \n");
+
+    ZSTD_freeDCtx(dctx);
+}
+
 static unsigned test_histCountWksp(unsigned seed, unsigned testNb)
 {
     static const unsigned symLowLimits[] =  {   0,  27,   0,   0,  27,  42,   0,   0, 27, 42, 27, 42 };
@@ -894,6 +1075,84 @@ static unsigned test_convertSequences_noRepcodes(unsigned seed, unsigned testNb)
     return testNb;
 }
 
+static U32 FUZ_blockSplitHash(U32 x)
+{
+    x ^= x >> 16;
+    x *= 0x7feb352dU;
+    x ^= x >> 15;
+    x *= 0x846ca68bU;
+    return x ^ (x >> 16);
+}
+
+/* The input is all zeroes, so all these offsets are valid matches.
+ * Groups of 150 sequences have distinct offset-code distributions, making
+ * enough dyadic splits beneficial to exhaust the block splitter's budget. */
+static size_t FUZ_blockSplitSequenceProducer(void* state,
+        ZSTD_Sequence* out, size_t capacity,
+        const void* src, size_t srcSize, const void* dict, size_t dictSize,
+        int compressionLevel, size_t windowSize)
+{
+    size_t i;
+    (void)state; (void)src; (void)dict; (void)dictSize;
+    (void)compressionLevel; (void)windowSize;
+    CHECK_EQ(srcSize, 128000);
+    CHECK(capacity >= 38401);
+    for (i = 0; i < 38400; ++i) {
+        U32 const code = 3 + FUZ_blockSplitHash((U32)(i / 150)) % 10;
+        out[i].offset = (1U << code) + (U32)(i % 4);
+        out[i].litLength = i == 0 ? 12800 : 0;
+        out[i].matchLength = 3;
+        out[i].rep = 0;
+    }
+    memset(out + i, 0, sizeof(*out));
+    return i + 1;
+}
+
+static unsigned test_blockSplitLimit(unsigned testNb)
+{
+    size_t const srcSize = 128000;
+    size_t const dstCapacity = ZSTD_compressBound(srcSize);
+    BYTE* const src = (BYTE*)calloc(1, srcSize);
+    BYTE* const dst = (BYTE*)malloc(dstCapacity);
+    BYTE* const decoded = (BYTE*)malloc(srcSize);
+    ZSTD_CCtx* const cctx = ZSTD_createCCtx();
+    ZSTD_FrameHeader header;
+    size_t cSize, pos, nbBlocks = 0;
+
+    DISPLAYLEVEL(3, "test%3u : external sequences exhaust the block split budget : ", testNb++);
+    CHECK(src != NULL && dst != NULL && decoded != NULL && cctx != NULL);
+    CHECK_Z(ZSTD_CCtx_setParameter(cctx, ZSTD_c_compressionLevel, 19));
+    CHECK_Z(ZSTD_CCtx_setParameter(cctx, ZSTD_c_minMatch, 3));
+    CHECK_Z(ZSTD_CCtx_setParameter(cctx, ZSTD_c_splitAfterSequences, ZSTD_ps_enable));
+    CHECK_Z(ZSTD_CCtx_setParameter(cctx, ZSTD_c_validateSequences, 1));
+    ZSTD_registerSequenceProducer(cctx, NULL, FUZ_blockSplitSequenceProducer);
+    cSize = ZSTD_compress2(cctx, dst, dstCapacity, src, srcSize);
+    CHECK_Z(cSize);
+    CHECK_EQ(ZSTD_getFrameHeader(&header, dst, cSize), 0);
+    pos = header.headerSize;
+    for (;;) {
+        U32 blockHeader;
+        CHECK(pos + ZSTD_blockHeaderSize <= cSize);
+        blockHeader = MEM_readLE24(dst + pos);
+        pos += ZSTD_blockHeaderSize;
+        pos += ((blockHeader >> 1) & 3) == bt_rle ? 1 : blockHeader >> 3;
+        CHECK(pos <= cSize);
+        ++nbBlocks;
+        if (blockHeader & 1) break;
+    }
+    CHECK_EQ(pos, cSize);
+    CHECK_EQ(nbBlocks, ZSTD_MAX_NB_BLOCK_SPLITS + 1);
+    CHECK_EQ(cctx->blockSplitCtx.partitions[ZSTD_MAX_NB_BLOCK_SPLITS], 38400);
+    CHECK_EQ(ZSTD_decompress(decoded, srcSize, dst, cSize), srcSize);
+    CHECK_EQ(memcmp(src, decoded, srcSize), 0);
+    ZSTD_freeCCtx(cctx);
+    free(decoded);
+    free(dst);
+    free(src);
+    DISPLAYLEVEL(3, "OK \n");
+    return testNb;
+}
+
 static unsigned test_get1BlockSummary(unsigned testNb)
 {
     static const ZSTD_Sequence nseqs[] = {
@@ -976,6 +1235,1044 @@ static unsigned test_get1BlockSummary(unsigned testNb)
 
 /* ============================================================= */
 
+typedef struct {
+    int fail;
+    size_t outstanding;
+} FUZ_dictAllocState;
+
+static void* FUZ_dictMalloc(void* opaque, size_t size)
+{
+    FUZ_dictAllocState* const state = (FUZ_dictAllocState*)opaque;
+    void* allocation;
+    if (state->fail) return NULL;
+    allocation = malloc(size);
+    if (allocation != NULL) ++state->outstanding;
+    return allocation;
+}
+
+static void FUZ_dictFree(void* opaque, void* allocation)
+{
+    FUZ_dictAllocState* const state = (FUZ_dictAllocState*)opaque;
+    if (allocation != NULL) {
+        CHECK(state->outstanding != 0);
+        --state->outstanding;
+    }
+    free(allocation);
+}
+
+static void test_dictionaryLoadErrors(const void* dict, size_t dictSize)
+{
+    FUZ_dictAllocState state = { 0, 0 };
+    ZSTD_customMem const customMem = { FUZ_dictMalloc, FUZ_dictFree, &state };
+    ZSTD_CCtx* const cctx = ZSTD_createCCtx_advanced(customMem);
+    ZSTD_CCtx_params cdictParams;
+    BYTE const src[32] = { 0 };
+    BYTE compressed[ZSTD_COMPRESSBOUND(sizeof(src))];
+    size_t const baseline = state.outstanding;
+    size_t cSize;
+
+    CHECK(cctx != NULL);
+    CHECK_Z(ZSTD_CCtxParams_init(&cdictParams, 1));
+    CHECK_Z(ZSTD_CCtx_setParameter(cctx, ZSTD_c_compressionLevel, 1));
+    CHECK_Z(ZSTD_CCtx_loadDictionary_byReference(cctx, dict, dictSize));
+
+    /* Actual allocation failures must keep their distinct error code. */
+    state.fail = 1;
+    CHECK(ZSTD_createCDict_advanced2(dict, dictSize, ZSTD_dlm_byRef,
+            ZSTD_dct_fullDict, &cdictParams, customMem) == NULL);
+    CHECK_EQ(ZSTD_getErrorCode(ZSTD_compress2(cctx, compressed, sizeof(compressed),
+            src, sizeof(src))), ZSTD_error_memory_allocation);
+    CHECK_EQ(state.outstanding, baseline);
+    state.fail = 0;
+
+    /* A failed CDict initialization must free its partial allocation and
+     * leave the CCtx able to retry. The first eight bytes retain the magic
+     * number and dictionary ID, but omit the required entropy tables. */
+    CHECK(dictSize > 8);
+    CHECK_Z(ZSTD_CCtx_loadDictionary_byReference(cctx, dict, 8));
+    CHECK(ZSTD_createCDict_advanced2(dict, 8, ZSTD_dlm_byRef,
+            ZSTD_dct_fullDict, &cdictParams, customMem) == NULL);
+    CHECK_EQ(state.outstanding, baseline);
+    CHECK_EQ(ZSTD_getErrorCode(ZSTD_compress2(cctx, compressed, sizeof(compressed),
+            src, sizeof(src))), ZSTD_error_dictionary_corrupted);
+    CHECK_EQ(state.outstanding, baseline);
+    CHECK_EQ(ZSTD_getErrorCode(ZSTD_compress2(cctx, compressed, sizeof(compressed),
+            src, sizeof(src))), ZSTD_error_dictionary_corrupted);
+    CHECK_EQ(state.outstanding, baseline);
+
+    CHECK_Z(ZSTD_CCtx_loadDictionary_byReference(cctx, dict, dictSize));
+    CHECK_VAR(cSize, ZSTD_compress2(cctx, compressed, sizeof(compressed), src, sizeof(src)));
+    {   ZSTD_DCtx* const dctx = ZSTD_createDCtx();
+        BYTE decoded[sizeof(src)];
+        CHECK(dctx != NULL);
+        {   CHECK_NEWV(dSize, ZSTD_decompress_usingDict(dctx, decoded, sizeof(decoded),
+                    compressed, cSize, dict, dictSize));
+            CHECK(dSize == sizeof(src) && memcmp(src, decoded, sizeof(src)) == 0);
+        }
+        ZSTD_freeDCtx(dctx);
+    }
+    ZSTD_freeCCtx(cctx);
+    CHECK_EQ(state.outstanding, 0);
+}
+
+#if ZSTD_WINDOW_OVERFLOW_CORRECT_FREQUENTLY
+/* dict has a valid full-dictionary header followed by at least 3 MB + 64 bytes of content. */
+static void test_dictionaryRepcodesAfterOverflow(BYTE* dict, size_t headerSize)
+{
+    /* Reaching the normal overflow threshold requires compressing a few GB.
+     * Only run this regression in frequent-correction mode, where a smaller
+     * dictionary and forceMaxWindow reproduce the same state cheaply. Loading
+     * the dictionary into the reused CCtx retains only 3 MB of its content. */
+    size_t const contentSize = 3 MB + 64;
+    size_t const preloadSize = 256 KB - 64;
+    size_t const fullDictSize = headerSize + contentSize;
+    U32 const rep = (U32)contentSize - 16;
+    size_t const compressedCapacity = ZSTD_compressBound(128 KB);
+    void* const compressed = malloc(compressedCapacity);
+    ZSTD_CCtx* const fresh = ZSTD_createCCtx();
+    ZSTD_CCtx* const reused = ZSTD_createCCtx();
+    ZSTD_DCtx* const dctx = ZSTD_createDCtx();
+    ZSTD_parameters params = ZSTD_getParams(1, ZSTD_CONTENTSIZE_UNKNOWN, 0);
+    ZSTD_CCtx_params cctxParams;
+    BYTE src[4 KB];
+    BYTE decoded[sizeof(src)];
+    size_t remaining = preloadSize;
+    U32 u;
+
+    CHECK(compressed != NULL && fresh != NULL && reused != NULL && dctx != NULL);
+    for (u = 0; u < ZSTD_REP_NUM; ++u)
+        MEM_writeLE32(dict + headerSize - 12 + 4 * u, repStartValue[u]);
+    MEM_writeLE32(dict + headerSize - 12, rep);
+    memcpy(src, dict + fullDictSize - rep, sizeof(src));
+
+    params.cParams.windowLog = 20;
+    params.cParams.hashLog = 18;
+    params.cParams.chainLog = 17;
+    CHECK_Z(ZSTD_CCtxParams_init_advanced(&cctxParams, params));
+    CHECK_Z(ZSTD_CCtxParams_setParameter(&cctxParams, ZSTD_c_forceMaxWindow, 1));
+
+    /* First prove that exactly this dictionary and these parameters work. */
+    CHECK_Z(ZSTD_compressBegin_advanced_internal(fresh, dict, fullDictSize,
+            ZSTD_dct_fullDict, ZSTD_dtlm_fast, NULL, &cctxParams, ZSTD_CONTENTSIZE_UNKNOWN));
+    CHECK_EQ(fresh->blockState.matchState.window.nbOverflowCorrections, 0);
+    {   CHECK_NEWV(cSize, ZSTD_compressEnd(fresh, compressed, compressedCapacity, src, sizeof(src)));
+        {   CHECK_NEWV(dSize, ZSTD_decompress_usingDict(dctx, decoded, sizeof(decoded),
+                    compressed, cSize, dict, fullDictSize));
+            CHECK(dSize == sizeof(src) && memcmp(src, decoded, sizeof(src)) == 0);
+        }
+    }
+
+    /* Advance the indices using actual compression, without replacing the
+     * workspace or manufacturing internal match-state indices. */
+    CHECK_Z(ZSTD_compressBegin_advanced_internal(reused, NULL, 0,
+            ZSTD_dct_auto, ZSTD_dtlm_fast, NULL, &cctxParams, ZSTD_CONTENTSIZE_UNKNOWN));
+    while (remaining != 0) {
+        size_t const chunkSize = MIN(remaining, 128 KB);
+        CHECK_Z(ZSTD_compressContinue(reused, compressed, compressedCapacity,
+                                     dict + headerSize, chunkSize));
+        remaining -= chunkSize;
+    }
+    CHECK_Z(ZSTD_compressEnd(reused, compressed, compressedCapacity, NULL, 0));
+    CHECK_EQ(reused->blockState.matchState.window.nbOverflowCorrections, 0);
+    CHECK_EQ((size_t)(reused->blockState.matchState.window.nextSrc
+                   - reused->blockState.matchState.window.base),
+             preloadSize + ZSTD_WINDOW_START_INDEX);
+
+    {   size_t const result = ZSTD_compressBegin_advanced_internal(reused, dict, fullDictSize,
+                ZSTD_dct_fullDict, ZSTD_dtlm_fast, NULL, &cctxParams, ZSTD_CONTENTSIZE_UNKNOWN);
+        ZSTD_window_t const* const window = &reused->blockState.matchState.window;
+        U32 const loadedDictSize = (U32)(window->nextSrc - window->base) - window->lowLimit;
+        CHECK_EQ(window->nbOverflowCorrections, 1);
+        CHECK_EQ(loadedDictSize, 3 MB);
+        CHECK_LT(loadedDictSize, rep);
+        CHECK_Z(result);
+    }
+    {   CHECK_NEWV(cSize, ZSTD_compressEnd(reused, compressed, compressedCapacity, src, sizeof(src)));
+        {   CHECK_NEWV(dSize, ZSTD_decompress_usingDict(dctx, decoded, sizeof(decoded),
+                    compressed, cSize, dict, fullDictSize));
+            CHECK(dSize == sizeof(src) && memcmp(src, decoded, sizeof(src)) == 0);
+        }
+    }
+
+    ZSTD_freeDCtx(dctx);
+    ZSTD_freeCCtx(reused);
+    ZSTD_freeCCtx(fresh);
+    free(compressed);
+}
+#endif
+
+static void test_largeDictionaryRepcodes(const void* dict, size_t dictSize)
+{
+    U32 const shortCacheMaxDictSize = (1U << (32 - ZSTD_SHORT_CACHE_TAG_BITS)) - ZSTD_WINDOW_START_INDEX;
+    struct DictCase { size_t contentSize; U32 rep; };
+    struct DictCase const cases[] = {
+        { 20 MB, shortCacheMaxDictSize + 1 },
+        { 20 MB, 20 MB - 16 },
+        { 20 MB, shortCacheMaxDictSize },
+        { 10 MB, 10 MB - 16 },
+    };
+    int const levels[] = { -5, 1, 2, 3, 4, 5 };
+    size_t const headerSize = ZDICT_getDictHeaderSize(dict, dictSize);
+    BYTE* largeDict;
+    ZSTD_CCtx* const cctx = ZSTD_createCCtx();
+    ZSTD_DCtx* const dctx = ZSTD_createDCtx();
+    BYTE src[4 KB];
+    BYTE compressed[8 KB];
+    BYTE decoded[sizeof(src)];
+    size_t ci, li;
+    U32 r;
+
+    CHECK_Z(headerSize);
+    CHECK(headerSize >= 12);
+    largeDict = (BYTE*)malloc(headerSize + 20 MB);
+    CHECK(largeDict != NULL && cctx != NULL && dctx != NULL);
+    memcpy(largeDict, dict, headerSize);
+    RDG_genBuffer(largeDict + headerSize, 20 MB, 0.5, 0., 0);
+
+#if ZSTD_WINDOW_OVERFLOW_CORRECT_FREQUENTLY
+    test_dictionaryRepcodesAfterOverflow(largeDict, headerSize);
+#endif
+
+    for (ci = 0; ci < COUNTOF(cases); ++ci) {
+        size_t const largeDictSize = headerSize + cases[ci].contentSize;
+        /* Make the large repcode a possible match in the full dictionary. */
+        memcpy(src, largeDict + largeDictSize - cases[ci].rep, sizeof(src));
+        for (li = 0; li < COUNTOF(levels); ++li) {
+            int const level = levels[li];
+            ZSTD_CCtx_params cdictParams;
+            ZSTD_compressionParameters cParams;
+            int reject;
+            CHECK_Z(ZSTD_CCtxParams_init(&cdictParams, level));
+            cParams = ZSTD_getCParamsFromCCtxParams(&cdictParams,
+                    ZSTD_CONTENTSIZE_UNKNOWN, largeDictSize, ZSTD_cpm_createCDict);
+            reject = (cParams.strategy == ZSTD_fast || cParams.strategy == ZSTD_dfast)
+                  && cases[ci].rep > shortCacheMaxDictSize;
+            for (r = 0; r < ZSTD_REP_NUM; ++r) {
+                ZSTD_CDict* cdict;
+                U32 u;
+                size_t cSize;
+                ZSTD_inBuffer in = { src, sizeof(src), 0 };
+                ZSTD_outBuffer out = { compressed, sizeof(compressed), 0 };
+                for (u = 0; u < ZSTD_REP_NUM; ++u)
+                    MEM_writeLE32(largeDict + headerSize - 12 + 4 * u, repStartValue[u]);
+                MEM_writeLE32(largeDict + headerSize - 12 + 4 * r, cases[ci].rep);
+
+                cdict = ZSTD_createCDict_byReference(largeDict, largeDictSize, level);
+                if (reject) {
+                    CHECK(cdict == NULL);
+                    /* The copy constructor and advanced constructor share the check. */
+                    CHECK(ZSTD_createCDict(largeDict, largeDictSize, level) == NULL);
+                    CHECK(ZSTD_createCDict_advanced(largeDict, largeDictSize,
+                            ZSTD_dlm_byRef, ZSTD_dct_fullDict,
+                            cParams, ZSTD_defaultCMem) == NULL);
+
+                    /* Local CDict creation is deferred until compression starts. */
+                    CHECK_Z(ZSTD_CCtx_reset(cctx, ZSTD_reset_session_and_parameters));
+                    CHECK_Z(ZSTD_CCtx_setParameter(cctx, ZSTD_c_compressionLevel, level));
+                    CHECK_Z(ZSTD_CCtx_loadDictionary(cctx, largeDict, largeDictSize));
+                    CHECK_EQ(ZSTD_getErrorCode(ZSTD_compress2(cctx, compressed,
+                            sizeof(compressed), src, sizeof(src))), ZSTD_error_dictionary_corrupted);
+                    CHECK_Z(ZSTD_CCtx_reset(cctx, ZSTD_reset_session_and_parameters));
+                    CHECK_Z(ZSTD_CCtx_setParameter(cctx, ZSTD_c_compressionLevel, level));
+                    CHECK_Z(ZSTD_CCtx_loadDictionary_byReference(cctx, largeDict, largeDictSize));
+                    CHECK_EQ(ZSTD_getErrorCode(ZSTD_compressStream2(cctx, &out, &in,
+                            ZSTD_e_continue)), ZSTD_error_dictionary_corrupted);
+
+                    /* The dictionary is format-valid. The non-CDict path retains
+                     * all of its content, so it must still round-trip. */
+                    CHECK_VAR(cSize, ZSTD_compress_usingDict(cctx, compressed, sizeof(compressed),
+                            src, sizeof(src), largeDict, largeDictSize, level));
+                } else {
+                    CHECK(cdict != NULL);
+                    CHECK_VAR(cSize, ZSTD_compress_usingCDict(cctx, compressed, sizeof(compressed),
+                            src, sizeof(src), cdict));
+                    {   CHECK_NEWV(dSize, ZSTD_decompress_usingDict(dctx, decoded, sizeof(decoded),
+                                compressed, cSize, largeDict, largeDictSize));
+                        CHECK(dSize == sizeof(src) && memcmp(src, decoded, sizeof(src)) == 0);
+                    }
+
+                    /* Start with unknown input size to exercise attached streaming. */
+                    CHECK_Z(ZSTD_CCtx_reset(cctx, ZSTD_reset_session_and_parameters));
+                    CHECK_Z(ZSTD_CCtx_refCDict(cctx, cdict));
+                    CHECK_Z(ZSTD_compressStream2(cctx, &out, &in, ZSTD_e_continue));
+                    CHECK(in.pos == in.size);
+                    CHECK(ZSTD_compressStream2(cctx, &out, &in, ZSTD_e_end) == 0);
+                    cSize = out.pos;
+                    ZSTD_freeCDict(cdict);
+                }
+                {   CHECK_NEWV(dSize, ZSTD_decompress_usingDict(dctx, decoded, sizeof(decoded),
+                            compressed, cSize, largeDict, largeDictSize));
+                    CHECK(dSize == sizeof(src) && memcmp(src, decoded, sizeof(src)) == 0);
+                }
+            }
+        }
+    }
+
+    ZSTD_freeDCtx(dctx);
+    ZSTD_freeCCtx(cctx);
+    free(largeDict);
+}
+
+/* Multi-DDict fixtures deliberately share entropy tables but have different
+ * content. The handcrafted frame always copies eight dictionary bytes: unlike
+ * compressor-generated data, it cannot turn into a literals-only block and
+ * accidentally pass when the decoder selects the wrong dictionary. */
+enum {
+    FUZ_mdict_knownSize = 1,
+    FUZ_mdict_checksum = 2
+};
+
+static void FUZ_mdict_makeDict(BYTE dst[44], U32 id, int fill)
+{
+    static const BYTE header[28] = {
+        0x37,0xa4,0x30,0xec, 0,0,0,0,  /* magic and dictionary ID */
+        128,0x10,                     /* Huffman table */
+        0xf0,0x03, 0xf0,0x03, 0xf0,0x03, /* FSE tables */
+        4,0,0,0, 4,0,0,0, 4,0,0,0    /* repeat offsets */
+    };
+    memcpy(dst, header, sizeof(header));
+    MEM_writeLE32(dst + 4, id);
+    memset(dst + sizeof(header), fill, 16);
+}
+
+/* ID 0 creates a raw-content DDict; other IDs create formatted DDicts. */
+static ZSTD_DDict* FUZ_mdict_createDict(U32 id, int fill)
+{
+    BYTE bytes[44];
+    ZSTD_DDict* ddict;
+    FUZ_mdict_makeDict(bytes, id, fill);
+    ddict = id ? ZSTD_createDDict(bytes, sizeof(bytes))
+               : ZSTD_createDDict(bytes + 28, 16);
+    CHECK(ddict != NULL && ZSTD_getDictID_fromDDict(ddict) == id);
+    return ddict;
+}
+
+/* One compressed block, no literals, one match of length 8 at offset 4.
+ * ID 0 omits the frame's dictID. A known content size permits the streaming
+ * shortcut; the optional checksum is for the expected output "AAAAAAAA". */
+static size_t FUZ_mdict_makeFrame(BYTE* dst, U32 id, unsigned flags)
+{
+    static const BYTE block[] = {
+        0x3d,0,0,       /* last compressed block, seven-byte payload */
+        0,1,0x54,       /* no literals; one sequence; RLE sequence tables */
+        0,0,5,1         /* LL=0, repeat offset, ML=8, bitstream */
+    };
+    int const knownSize = (flags & FUZ_mdict_knownSize) != 0;
+    size_t n = 4;
+    MEM_writeLE32(dst, ZSTD_MAGICNUMBER);
+    dst[n++] = (BYTE)((id ? 3 : 0) | (knownSize ? 32 : 0)
+                      | ((flags & FUZ_mdict_checksum) ? 4 : 0));
+    if (!knownSize) dst[n++] = 0; /* 1 KiB window */
+    if (id) { MEM_writeLE32(dst + n, id); n += 4; }
+    if (knownSize) dst[n++] = 8;
+    memcpy(dst + n, block, sizeof(block)); n += sizeof(block);
+    if (flags & FUZ_mdict_checksum) {
+        MEM_writeLE32(dst + n, (U32)XXH64("AAAAAAAA", 8, 0)); n += 4;
+    }
+    return n;
+}
+
+static size_t FUZ_mdict_makeSkip(BYTE* dst, unsigned variant)
+{
+    MEM_writeLE32(dst, ZSTD_MAGIC_SKIPPABLE_START + variant);
+    MEM_writeLE32(dst + 4, 0); /* empty skippable frame */
+    return 8;
+}
+
+/* Register one or two DDicts in a new multiple-DDict context. */
+static ZSTD_DCtx* FUZ_mdict_createContext(const ZSTD_DDict* first,
+                                        const ZSTD_DDict* second)
+{
+    ZSTD_DCtx* const dctx = ZSTD_createDCtx();
+    CHECK(dctx != NULL);
+    CHECK_Z(ZSTD_DCtx_setParameter(dctx, ZSTD_d_refMultipleDDicts, 1));
+    if (first) CHECK_Z(ZSTD_DCtx_refDDict(dctx, first));
+    if (second) CHECK_Z(ZSTD_DCtx_refDDict(dctx, second));
+    return dctx;
+}
+
+/* Feed complete frames in chunks, returning the first error or total output.
+ * These tests provide enough output space, so every call must make progress. */
+static size_t FUZ_mdict_decompressStream(ZSTD_DCtx* dctx, void* dst, size_t capacity,
+                                        const BYTE* src, size_t size, size_t chunk)
+{
+    ZSTD_outBuffer out = {dst, capacity, 0};
+    size_t pos = 0, ret = 1;
+    CHECK(chunk != 0);
+    while (pos < size) {
+        size_t const n = MIN(size - pos, chunk);
+        ZSTD_inBuffer in = {src + pos, n, 0};
+        while (in.pos < in.size) {
+            size_t const oldIn = in.pos, oldOut = out.pos;
+            ret = ZSTD_decompressStream(dctx, &out, &in);
+            if (ZSTD_isError(ret)) return ret;
+            CHECK(in.pos > oldIn || out.pos > oldOut);
+        }
+        pos += n;
+    }
+    CHECK_EQ(ret, 0);
+    return out.pos;
+}
+
+/* The bufferless API requires exactly the input size requested by the decoder. */
+static size_t FUZ_mdict_decompressContinue(ZSTD_DCtx* dctx, BYTE* dst, size_t capacity,
+                                          const BYTE* src, size_t size)
+{
+    size_t inPos = 0, outPos = 0;
+    for (;;) {
+        size_t const n = ZSTD_nextSrcSizeToDecompress(dctx);
+        size_t r;
+        if (n == 0) break;
+        CHECK(inPos <= size && n <= size - inPos);
+        CHECK(outPos <= capacity);
+        r = ZSTD_decompressContinue(dctx, outPos != 0 ? dst + outPos : dst, capacity - outPos,
+                                    src + inPos, n);
+        if (ZSTD_isError(r)) return r;
+        inPos += n;
+        outPos += r;
+    }
+    CHECK_EQ(inPos, size);
+    return outPos;
+}
+
+static void FUZ_mdict_checkOutput(size_t r, const void* out, const char* expected)
+{
+    CHECK_Z(r);
+    CHECK_EQ(r, strlen(expected));
+    CHECK_EQ(memcmp(out, expected, r), 0);
+}
+
+static void FUZ_mdict_testOneShot(void)
+{
+    ZSTD_DDict* const a = FUZ_mdict_createDict(1111, 'A');
+    ZSTD_DDict* const b = FUZ_mdict_createDict(2222, 'B');
+    ZSTD_DDict* const raw = FUZ_mdict_createDict(0, 'R');
+    BYTE frame[96], out[32], tmp[32];
+    size_t n, r;
+    int magicless, invalid;
+
+    /* B is registered last. Every frame must nevertheless install its own
+     * exact match, including after reset and within concatenated input. */
+    for (magicless = 0; magicless <= 1; ++magicless) {
+        ZSTD_DCtx* const dctx = FUZ_mdict_createContext(raw, a);
+        size_t const skip = magicless ? 4 : 0;
+        static const U32 ids[] = {1111, 2222, 0};
+        size_t i;
+        CHECK_Z(ZSTD_DCtx_refDDict(dctx, b));
+        if (magicless) CHECK_Z(ZSTD_DCtx_setParameter(dctx, ZSTD_d_format, ZSTD_f_zstd1_magicless));
+        n = FUZ_mdict_makeFrame(frame, 1111, 0);
+        r = ZSTD_decompressDCtx(dctx, out, sizeof(out), frame + skip, n - skip);
+        FUZ_mdict_checkOutput(r, out, "AAAAAAAA");
+        CHECK_Z(ZSTD_DCtx_reset(dctx, ZSTD_reset_session_only));
+        n = FUZ_mdict_makeFrame(frame, 2222, 0);
+        r = ZSTD_decompressDCtx(dctx, out, sizeof(out), frame + skip, n - skip);
+        FUZ_mdict_checkOutput(r, out, "BBBBBBBB");
+
+        n = magicless ? 0 : FUZ_mdict_makeSkip(frame, 7);
+        for (i = 0; i < COUNTOF(ids); ++i) {
+            size_t const fSize = FUZ_mdict_makeFrame(tmp, ids[i], i == 1 ? FUZ_mdict_knownSize : 0);
+            memcpy(frame + n, tmp + skip, fSize - skip);
+            n += fSize - skip;
+        }
+        r = ZSTD_decompressDCtx(dctx, out, sizeof(out), frame, n);
+        FUZ_mdict_checkOutput(r, out, "AAAAAAAABBBBBBBBRRRRRRRR");
+        CHECK_Z(ZSTD_freeDCtx(dctx));
+    }
+
+    /* Failed headers must not prevent the next frame from finding its match. */
+    for (invalid = 0; invalid < 2; ++invalid) {
+        ZSTD_DCtx* const dctx = FUZ_mdict_createContext(raw, a);
+        FUZ_mdict_makeFrame(frame, 1111, 0);
+        if (invalid) memset(frame, 0xff, 5);
+        r = ZSTD_decompressDCtx(dctx, out, sizeof(out), frame, 5);
+        CHECK(ZSTD_isError(r));
+        n = FUZ_mdict_makeFrame(frame, 1111, 0);
+        r = ZSTD_decompressDCtx(dctx, out, sizeof(out), frame, n);
+        FUZ_mdict_checkOutput(r, out, "AAAAAAAA");
+        CHECK_Z(ZSTD_freeDCtx(dctx));
+    }
+    CHECK_Z(ZSTD_freeDDict(a)); CHECK_Z(ZSTD_freeDDict(b));
+    CHECK_Z(ZSTD_freeDDict(raw));
+}
+
+static void FUZ_mdict_testExplicitDictionary(void)
+{
+    ZSTD_DDict* const a = FUZ_mdict_createDict(1111, 'A');
+    ZSTD_DDict* const b = FUZ_mdict_createDict(2222, 'B');
+    BYTE frame[32], out[32], dict[44];
+    size_t n, r;
+    int local;
+    for (local = 0; local < 2; ++local) {
+        ZSTD_DCtx* const dctx = FUZ_mdict_createContext(b, NULL);
+        FUZ_mdict_makeDict(dict, 2222, 'B');
+        if (local) CHECK_Z(ZSTD_DCtx_loadDictionary(dctx, dict, sizeof(dict)));
+        n = FUZ_mdict_makeFrame(frame, 1111, 0);
+
+        /* The set must not override the explicit argument, even though A is
+         * absent from the set and strict lookup would reject this frame. */
+        r = ZSTD_decompress_usingDDict(dctx, out, sizeof(out), frame, n, b);
+        CHECK_EQ(ZSTD_getErrorCode(r), ZSTD_error_dictionary_wrong);
+        r = ZSTD_decompress_usingDDict(dctx, out, sizeof(out), frame, n, a);
+        FUZ_mdict_checkOutput(r, out, "AAAAAAAA");
+        r = ZSTD_decompress_usingDict(dctx, out, sizeof(out), frame, n, dict, sizeof(dict));
+        CHECK_EQ(ZSTD_getErrorCode(r), ZSTD_error_dictionary_wrong);
+        FUZ_mdict_makeDict(dict, 1111, 'A');
+        r = ZSTD_decompress_usingDict(dctx, out, sizeof(out), frame, n, dict, sizeof(dict));
+        FUZ_mdict_checkOutput(r, out, "AAAAAAAA");
+
+        /* Header validation must likewise honor the bufferless API's DDict. */
+        CHECK_Z(ZSTD_decompressBegin_usingDDict(dctx, b));
+        r = FUZ_mdict_decompressContinue(dctx, out, sizeof(out), frame, n);
+        CHECK_EQ(ZSTD_getErrorCode(r), ZSTD_error_dictionary_wrong);
+        CHECK_Z(ZSTD_decompressBegin_usingDDict(dctx, a));
+        r = FUZ_mdict_decompressContinue(dctx, out, sizeof(out), frame, n);
+        FUZ_mdict_checkOutput(r, out, "AAAAAAAA");
+
+        /* Explicit dictionaries must not replace the configured set or the
+         * context-owned B. Disabling the set must not retain borrowed B. */
+        n = FUZ_mdict_makeFrame(frame, 2222, 0);
+        r = ZSTD_decompressDCtx(dctx, out, sizeof(out), frame, n);
+        FUZ_mdict_checkOutput(r, out, "BBBBBBBB");
+        if (!local) { CHECK_Z(ZSTD_DCtx_setParameter(dctx, ZSTD_d_refMultipleDDicts, 0)); }
+        n = FUZ_mdict_makeFrame(frame, 0, 0);
+        r = ZSTD_decompressDCtx(dctx, out, sizeof(out), frame, n);
+        if (local) { FUZ_mdict_checkOutput(r, out, "BBBBBBBB"); }
+        else { CHECK_EQ(ZSTD_getErrorCode(r), ZSTD_error_corruption_detected); }
+        CHECK_Z(ZSTD_freeDCtx(dctx));
+    }
+    CHECK_Z(ZSTD_freeDDict(a)); CHECK_Z(ZSTD_freeDDict(b));
+}
+
+static void FUZ_mdict_testPrefixCompatibility(void)
+{
+    ZSTD_DDict* const b = FUZ_mdict_createDict(2222, 'B');
+    BYTE prefix[16], frame[64], out[32];
+    size_t n, first, r;
+    int multiple;
+    memset(prefix, 'A', sizeof(prefix));
+    for (multiple = 0; multiple < 2; ++multiple) {
+        ZSTD_DCtx* const dctx = multiple ? FUZ_mdict_createContext(b, NULL) : ZSTD_createDCtx();
+        CHECK(dctx != NULL);
+        CHECK_Z(ZSTD_DCtx_refPrefix(dctx, prefix, sizeof(prefix)));
+
+        /* Preserve the existing single-DDict one-shot behavior: a prefix is
+         * resolved once per call, even across concatenated frames. This also
+         * applies when the multi-DDict flag is enabled but the set is empty. */
+        first = FUZ_mdict_makeFrame(frame, 0, 0);
+        n = first + FUZ_mdict_makeFrame(frame + first, 0, 0);
+        r = ZSTD_decompressDCtx(dctx, out, sizeof(out), frame, n);
+        FUZ_mdict_checkOutput(r, out, "AAAAAAAAAAAAAAAA");
+        r = ZSTD_decompressDCtx(dctx, out, sizeof(out), frame, first);
+        CHECK_EQ(ZSTD_getErrorCode(r), ZSTD_error_corruption_detected);
+
+        /* Streaming still consumes the prefix on its first frame. */
+        CHECK_Z(ZSTD_DCtx_refPrefix(dctx, prefix, sizeof(prefix)));
+        r = FUZ_mdict_decompressStream(dctx, out, sizeof(out), frame, first, 1);
+        FUZ_mdict_checkOutput(r, out, "AAAAAAAA");
+        r = FUZ_mdict_decompressStream(dctx, out, sizeof(out), frame, first, 1);
+        CHECK_EQ(ZSTD_getErrorCode(r), ZSTD_error_corruption_detected);
+        CHECK_Z(ZSTD_DCtx_reset(dctx, ZSTD_reset_session_only));
+
+        /* Do not change the established skippable-frame prefix behavior in
+         * this multi-DDict fix. One-shot keeps its dictionary for the call;
+         * streaming consumes the prefix even on a skippable frame. */
+        CHECK_Z(ZSTD_DCtx_refPrefix(dctx, prefix, sizeof(prefix)));
+        first = FUZ_mdict_makeSkip(frame, 7);
+        n = first + FUZ_mdict_makeFrame(frame + first, 0, 0);
+        r = ZSTD_decompressDCtx(dctx, out, sizeof(out), frame, n);
+        FUZ_mdict_checkOutput(r, out, "AAAAAAAA");
+        CHECK_Z(ZSTD_DCtx_refPrefix(dctx, prefix, sizeof(prefix)));
+        r = ZSTD_decompressDCtx(dctx, out, sizeof(out), frame, first);
+        CHECK_EQ(r, 0);
+        r = ZSTD_decompressDCtx(dctx, out, sizeof(out), frame + first, n - first);
+        CHECK_EQ(ZSTD_getErrorCode(r), ZSTD_error_corruption_detected);
+        CHECK_Z(ZSTD_DCtx_refPrefix(dctx, prefix, sizeof(prefix)));
+        r = FUZ_mdict_decompressStream(dctx, out, sizeof(out), frame, n, 1);
+        CHECK_EQ(ZSTD_getErrorCode(r), ZSTD_error_corruption_detected);
+        CHECK_Z(ZSTD_freeDCtx(dctx));
+    }
+    CHECK_Z(ZSTD_freeDDict(b));
+}
+
+static void FUZ_mdict_testZeroID(void)
+{
+    U32 const zero = 0;
+    U32 id = 1;
+    ZSTD_DDict* const raw = FUZ_mdict_createDict(0, 'R');
+    ZSTD_DDict* full;
+    ZSTD_DCtx* const dctx = FUZ_mdict_createContext(raw, NULL);
+    BYTE frame[32], out[32];
+    size_t n, r;
+    /* Find a nonzero ID in raw dictionary 0's initial 64-slot hash bucket. */
+    while ((XXH64(&id, sizeof(id), 0) & 63) != (XXH64(&zero, sizeof(zero), 0) & 63))
+        ++id;
+    full = FUZ_mdict_createDict(id, 'A');
+    n = FUZ_mdict_makeFrame(frame, id, 0);
+
+    /* An occupied ID-0 slot is not a match for the missing nonzero ID. */
+    r = ZSTD_decompressDCtx(dctx, out, sizeof(out), frame, n);
+    CHECK_EQ(ZSTD_getErrorCode(r), ZSTD_error_dictionary_wrong);
+    CHECK_Z(ZSTD_DCtx_reset(dctx, ZSTD_reset_session_only));
+    r = FUZ_mdict_decompressStream(dctx, out, sizeof(out), frame, n, 1);
+    CHECK_EQ(ZSTD_getErrorCode(r), ZSTD_error_dictionary_wrong);
+    CHECK_Z(ZSTD_DCtx_reset(dctx, ZSTD_reset_session_only));
+
+    /* After insertion, lookup must probe past raw dictionary 0 to find A.
+     * An actual ID-less frame must still find the raw dictionary itself. */
+    CHECK_Z(ZSTD_DCtx_refDDict(dctx, full));
+    CHECK_Z(ZSTD_DCtx_refDDict(dctx, raw));
+    r = ZSTD_decompressDCtx(dctx, out, sizeof(out), frame, n);
+    FUZ_mdict_checkOutput(r, out, "AAAAAAAA");
+    r = FUZ_mdict_decompressStream(dctx, out, sizeof(out), frame, n, 1);
+    FUZ_mdict_checkOutput(r, out, "AAAAAAAA");
+    n = FUZ_mdict_makeFrame(frame, 0, 0);
+    r = ZSTD_decompressDCtx(dctx, out, sizeof(out), frame, n);
+    FUZ_mdict_checkOutput(r, out, "RRRRRRRR");
+    r = FUZ_mdict_decompressStream(dctx, out, sizeof(out), frame, n, 1);
+    FUZ_mdict_checkOutput(r, out, "RRRRRRRR");
+    CHECK_Z(ZSTD_freeDCtx(dctx));
+    CHECK_Z(ZSTD_freeDDict(raw)); CHECK_Z(ZSTD_freeDDict(full));
+}
+
+static void FUZ_mdict_testClearReferences(void)
+{
+    int mode;
+    /* Register A, clear its references, free A, then register B and request A.
+     * The result must be dictionary_wrong, without consulting freed A. */
+    for (mode = 0; mode < 5; ++mode) {
+        ZSTD_DDict* const a = FUZ_mdict_createDict(1111, 'A');
+        ZSTD_DDict* const b = FUZ_mdict_createDict(2222, 'B');
+        ZSTD_DCtx* const dctx = FUZ_mdict_createContext(a, NULL);
+        BYTE frame[32], out[32], dummy = 0;
+        size_t const n = FUZ_mdict_makeFrame(frame, 1111, 0);
+        size_t r;
+        switch (mode) {
+        case 0: CHECK_Z(ZSTD_DCtx_reset(dctx, ZSTD_reset_parameters)); break;
+        case 1: CHECK_Z(ZSTD_DCtx_loadDictionary(dctx, NULL, 0)); break;
+        case 2: CHECK_Z(ZSTD_DCtx_loadDictionary(dctx, &dummy, 0)); break;
+        case 3: CHECK_Z(ZSTD_DCtx_refDDict(dctx, NULL)); break;
+        default:
+            /* Even a dictionary already selected for decoding is released. */
+            r = ZSTD_decompressDCtx(dctx, out, sizeof(out), frame, n);
+            FUZ_mdict_checkOutput(r, out, "AAAAAAAA");
+            CHECK_Z(ZSTD_DCtx_setParameter(dctx, ZSTD_d_refMultipleDDicts, 0));
+            break;
+        }
+        CHECK_Z(ZSTD_freeDDict(a));
+        r = ZSTD_decompressDCtx(dctx, out, sizeof(out), frame, n);
+        CHECK_EQ(ZSTD_getErrorCode(r), ZSTD_error_dictionary_wrong);
+        CHECK_Z(ZSTD_DCtx_setParameter(dctx, ZSTD_d_refMultipleDDicts, 1));
+        CHECK_Z(ZSTD_DCtx_refDDict(dctx, b));
+        r = ZSTD_decompressDCtx(dctx, out, sizeof(out), frame, n);
+        CHECK_EQ(ZSTD_getErrorCode(r), ZSTD_error_dictionary_wrong);
+        CHECK_Z(ZSTD_freeDCtx(dctx)); CHECK_Z(ZSTD_freeDDict(b));
+    }
+}
+
+static void FUZ_mdict_testDisableSet(void)
+{
+    int mode;
+    for (mode = 0; mode < 4; ++mode) {
+        ZSTD_DDict* const a = FUZ_mdict_createDict(1111, 'A');
+        ZSTD_DDict* const b = FUZ_mdict_createDict(2222, 'B');
+        ZSTD_DDict* const raw = FUZ_mdict_createDict(0, 'R');
+        ZSTD_DDict* const d = FUZ_mdict_createDict(3333, 'D');
+        ZSTD_DCtx* const dctx = FUZ_mdict_createContext(a, b);
+        BYTE frame[32], out[32];
+        size_t n, r;
+        CHECK_Z(ZSTD_DCtx_refDDict(dctx, raw));
+        n = FUZ_mdict_makeFrame(frame, 1111, mode == 3 ? FUZ_mdict_knownSize : 0);
+        if (mode != 0) {
+            r = mode == 1 ? ZSTD_decompressDCtx(dctx, out, sizeof(out), frame, n)
+                          : FUZ_mdict_decompressStream(dctx, out, sizeof(out), frame, n,
+                                                       mode == 2 ? 1 : sizeof(frame));
+            FUZ_mdict_checkOutput(r, out, "AAAAAAAA");
+        }
+        /* All registered DDicts can be freed immediately, including the most
+         * recently registered one and any DDict already used for decoding. */
+        CHECK_Z(ZSTD_DCtx_setParameter(dctx, ZSTD_d_refMultipleDDicts, 0));
+        CHECK_Z(ZSTD_freeDDict(a)); CHECK_Z(ZSTD_freeDDict(b));
+        CHECK_Z(ZSTD_freeDDict(raw));
+        n = FUZ_mdict_makeFrame(frame, 0, 0);
+        r = ZSTD_decompressDCtx(dctx, out, sizeof(out), frame, n);
+        CHECK_EQ(ZSTD_getErrorCode(r), ZSTD_error_corruption_detected);
+        CHECK_Z(ZSTD_DCtx_setParameter(dctx, ZSTD_d_refMultipleDDicts, 1));
+        n = FUZ_mdict_makeFrame(frame, 1111, 0);
+        r = ZSTD_decompressDCtx(dctx, out, sizeof(out), frame, n);
+        CHECK_EQ(ZSTD_getErrorCode(r), ZSTD_error_dictionary_wrong);
+        CHECK_Z(ZSTD_DCtx_refDDict(dctx, d));
+        r = ZSTD_decompressDCtx(dctx, out, sizeof(out), frame, n);
+        CHECK_EQ(ZSTD_getErrorCode(r), ZSTD_error_dictionary_wrong);
+        n = FUZ_mdict_makeFrame(frame, 3333, 0);
+        r = ZSTD_decompressDCtx(dctx, out, sizeof(out), frame, n);
+        FUZ_mdict_checkOutput(r, out, "DDDDDDDD");
+        CHECK_Z(ZSTD_freeDCtx(dctx)); CHECK_Z(ZSTD_freeDDict(d));
+    }
+
+    /* With no set, toggling the opt-in parameter does not discard a separately
+     * configured local dictionary, prefix, or single borrowed DDict. */
+    for (mode = 0; mode < 3; ++mode) {
+        ZSTD_DDict* const a = FUZ_mdict_createDict(1111, 'A');
+        ZSTD_DCtx* const dctx = ZSTD_createDCtx();
+        BYTE dict[44], frame[32], out[32];
+        size_t n, r;
+        CHECK(dctx != NULL);
+        FUZ_mdict_makeDict(dict, 1111, 'A');
+        if (mode == 0) { CHECK_Z(ZSTD_DCtx_loadDictionary(dctx, dict, sizeof(dict))); }
+        if (mode == 1) { CHECK_Z(ZSTD_DCtx_refPrefix_advanced(dctx, dict, sizeof(dict), ZSTD_dct_fullDict)); }
+        if (mode == 2) { CHECK_Z(ZSTD_DCtx_refDDict(dctx, a)); }
+        CHECK_Z(ZSTD_DCtx_setParameter(dctx, ZSTD_d_refMultipleDDicts, 1));
+        CHECK_Z(ZSTD_DCtx_setParameter(dctx, ZSTD_d_refMultipleDDicts, 0));
+        n = FUZ_mdict_makeFrame(frame, 1111, 0);
+        r = ZSTD_decompressDCtx(dctx, out, sizeof(out), frame, n);
+        FUZ_mdict_checkOutput(r, out, "AAAAAAAA");
+        CHECK_Z(ZSTD_freeDCtx(dctx)); CHECK_Z(ZSTD_freeDDict(a));
+    }
+}
+
+static void FUZ_mdict_testDictionaryReplacement(void)
+{
+    int mode, used;
+    for (mode = 0; mode < 5; ++mode) {
+        for (used = 0; used < 2; ++used) {
+            ZSTD_DDict* const a = FUZ_mdict_createDict(1111, 'A');
+            ZSTD_DDict* const b = FUZ_mdict_createDict(2222, 'B');
+            ZSTD_DDict* const d = FUZ_mdict_createDict(4444, 'D');
+            ZSTD_DCtx* const dctx = FUZ_mdict_createContext(a, b);
+            BYTE* const dict = (BYTE*)malloc(44);
+            BYTE frame[32], out[32];
+            size_t n, r;
+            CHECK(dict != NULL);
+            FUZ_mdict_makeDict(dict, 3333, 'C');
+            switch (mode) {
+            case 0: CHECK_Z(ZSTD_DCtx_loadDictionary(dctx, dict, 44)); break;
+            case 1: CHECK_Z(ZSTD_DCtx_loadDictionary_byReference(dctx, dict, 44)); break;
+            case 2: CHECK_Z(ZSTD_DCtx_loadDictionary_advanced(dctx, dict + 28, 16,
+                                                   ZSTD_dlm_byCopy, ZSTD_dct_rawContent)); break;
+            case 3: CHECK_Z(ZSTD_DCtx_refPrefix(dctx, dict + 28, 16)); break;
+            default: CHECK_Z(ZSTD_DCtx_refPrefix_advanced(dctx, dict, 44, ZSTD_dct_fullDict)); break;
+            }
+            /* Every local dictionary/prefix replacement releases all borrowed
+             * DDicts, even though multiple-DDict mode remains enabled. */
+            CHECK_Z(ZSTD_freeDDict(a)); CHECK_Z(ZSTD_freeDDict(b));
+            if (used) {
+                n = FUZ_mdict_makeFrame(frame, 0, 0);
+                r = ZSTD_decompressDCtx(dctx, out, sizeof(out), frame, n);
+                FUZ_mdict_checkOutput(r, out, "CCCCCCCC");
+            }
+            /* Going back to the set must discard both an unused and an already
+             * consumed prefix, as well as a sticky local dictionary. */
+            CHECK_Z(ZSTD_DCtx_refDDict(dctx, d));
+            free(dict);
+            n = FUZ_mdict_makeFrame(frame, 1111, 0);
+            r = ZSTD_decompressDCtx(dctx, out, sizeof(out), frame, n);
+            CHECK_EQ(ZSTD_getErrorCode(r), ZSTD_error_dictionary_wrong);
+            n = FUZ_mdict_makeFrame(frame, 4444, 0);
+            r = ZSTD_decompressDCtx(dctx, out, sizeof(out), frame, n);
+            FUZ_mdict_checkOutput(r, out, "DDDDDDDD");
+            CHECK_Z(ZSTD_freeDCtx(dctx)); CHECK_Z(ZSTD_freeDDict(d));
+        }
+    }
+}
+
+static void FUZ_mdict_testStreaming(void)
+{
+    ZSTD_DDict* const a = FUZ_mdict_createDict(1111, 'A');
+    ZSTD_DDict* const b = FUZ_mdict_createDict(7, 'B');
+    BYTE data[64], out[32];
+    static const struct { unsigned flags; size_t chunk; } cases[] = {
+        {0, 1},                                      /* fragmented/buffered */
+        {FUZ_mdict_knownSize, sizeof(data)},          /* single-pass shortcut */
+        {FUZ_mdict_knownSize | FUZ_mdict_checksum, sizeof(data)}
+    };
+    size_t i, n, r;
+
+    /* Skippable frames do not require a dictionary, including an ID-0 entry.
+     * The following ordinary frame must still select its exact match. */
+    for (i = 0; i < COUNTOF(cases); ++i) {
+        ZSTD_DCtx* const dctx = FUZ_mdict_createContext(b, a);
+        n = FUZ_mdict_makeSkip(data, 7);
+        r = FUZ_mdict_decompressStream(dctx, out, sizeof(out), data, n, cases[i].chunk);
+        CHECK_EQ(r, 0);
+        n = FUZ_mdict_makeFrame(data, 1111, cases[i].flags);
+        r = FUZ_mdict_decompressStream(dctx, out, sizeof(out), data, n, cases[i].chunk);
+        FUZ_mdict_checkOutput(r, out, "AAAAAAAA");
+        CHECK_Z(ZSTD_freeDCtx(dctx));
+    }
+
+    /* Ordinary frames select an exact match on both buffered and shortcut paths. */
+    for (i = 0; i < COUNTOF(cases); ++i) {
+        ZSTD_DCtx* const dctx = FUZ_mdict_createContext(a, b);
+        n = FUZ_mdict_makeFrame(data, 1111, cases[i].flags);
+        r = FUZ_mdict_decompressStream(dctx, out, sizeof(out), data, n, cases[i].chunk);
+        FUZ_mdict_checkOutput(r, out, "AAAAAAAA");
+        CHECK_Z(ZSTD_freeDCtx(dctx));
+    }
+
+    /* Incomplete and rejected headers must not trigger an ID-0 lookup.
+     * This set deliberately has no ID-0 entry. */
+    for (i = 0; i < 2; ++i) {
+        ZSTD_DCtx* const dctx = FUZ_mdict_createContext(a, b);
+        ZSTD_inBuffer in = {data, 5, 0};
+        ZSTD_outBuffer ob = {out, sizeof(out), 0};
+        FUZ_mdict_makeFrame(data, 1111, 0);
+        if (i) memset(data, 0xff, 5);
+        r = ZSTD_decompressStream(dctx, &ob, &in);
+        if (i) { CHECK_EQ(ZSTD_getErrorCode(r), ZSTD_error_prefix_unknown); }
+        else { CHECK_Z(r); CHECK(r != 0); CHECK_EQ(in.pos, in.size); }
+        CHECK_Z(ZSTD_DCtx_reset(dctx, ZSTD_reset_session_only));
+        n = FUZ_mdict_makeFrame(data, 1111, 0);
+        r = FUZ_mdict_decompressStream(dctx, out, sizeof(out), data, n, 1);
+        FUZ_mdict_checkOutput(r, out, "AAAAAAAA");
+        CHECK_Z(ZSTD_freeDCtx(dctx));
+    }
+    CHECK_Z(ZSTD_freeDDict(a)); CHECK_Z(ZSTD_freeDDict(b));
+}
+
+static void FUZ_mdict_testStrictMatching(void)
+{
+    ZSTD_DDict* const a = FUZ_mdict_createDict(1111, 'A');
+    ZSTD_DDict* const b = FUZ_mdict_createDict(2222, 'B');
+    ZSTD_DDict* const raw = FUZ_mdict_createDict(0, 'R');
+    static const U32 missingIDs[] = {0, 3333};
+    BYTE frame[32], out[32];
+    size_t i, n, r;
+    int mode;
+
+    for (mode = 0; mode < 4; ++mode) {
+        int const magicless = mode >> 1;
+        int const shortcut = mode & 1;
+        ZSTD_DCtx* const dctx = FUZ_mdict_createContext(a, b);
+        unsigned const flags = shortcut ? FUZ_mdict_knownSize : 0;
+        size_t const chunk = shortcut ? sizeof(frame) : 1;
+        size_t const skip = magicless ? 4 : 0;
+        if (magicless) { CHECK_Z(ZSTD_DCtx_setParameter(dctx, ZSTD_d_format, ZSTD_f_zstd1_magicless)); }
+        /* Neither a missing nonzero ID nor an omitted ID may fall back to B. */
+        for (i = 0; i < COUNTOF(missingIDs); ++i) {
+            ZSTD_ErrorCode const expected = missingIDs[i] == 0
+                    ? ZSTD_error_corruption_detected : ZSTD_error_dictionary_wrong;
+            n = FUZ_mdict_makeFrame(frame, 2222, flags);
+            r = ZSTD_decompressDCtx(dctx, out, sizeof(out), frame + skip, n - skip);
+            FUZ_mdict_checkOutput(r, out, "BBBBBBBB");
+            n = FUZ_mdict_makeFrame(frame, missingIDs[i], flags);
+            r = ZSTD_decompressDCtx(dctx, out, sizeof(out), frame + skip, n - skip);
+            CHECK_EQ(ZSTD_getErrorCode(r), expected);
+            CHECK_Z(ZSTD_DCtx_reset(dctx, ZSTD_reset_session_only));
+            n = FUZ_mdict_makeFrame(frame, 2222, flags);
+            r = FUZ_mdict_decompressStream(dctx, out, sizeof(out), frame + skip, n - skip, chunk);
+            FUZ_mdict_checkOutput(r, out, "BBBBBBBB");
+            n = FUZ_mdict_makeFrame(frame, missingIDs[i], flags);
+            r = FUZ_mdict_decompressStream(dctx, out, sizeof(out), frame + skip, n - skip, chunk);
+            CHECK_EQ(ZSTD_getErrorCode(r), expected);
+            CHECK_Z(ZSTD_DCtx_reset(dctx, ZSTD_reset_session_only));
+        }
+        /* ID 0 is a real key: registering raw R makes an ID-less frame use R,
+         * even when a different DDict was most recently referenced or used. */
+        CHECK_Z(ZSTD_DCtx_refDDict(dctx, raw));
+        CHECK_Z(ZSTD_DCtx_refDDict(dctx, a));
+        n = FUZ_mdict_makeFrame(frame, 0, flags);
+        r = ZSTD_decompressDCtx(dctx, out, sizeof(out), frame + skip, n - skip);
+        FUZ_mdict_checkOutput(r, out, "RRRRRRRR");
+        CHECK_Z(ZSTD_DCtx_refDDict(dctx, a));
+        r = FUZ_mdict_decompressStream(dctx, out, sizeof(out), frame + skip, n - skip, chunk);
+        FUZ_mdict_checkOutput(r, out, "RRRRRRRR");
+        n = FUZ_mdict_makeFrame(frame, 1111, flags);
+        r = FUZ_mdict_decompressStream(dctx, out, sizeof(out), frame + skip, n - skip, chunk);
+        FUZ_mdict_checkOutput(r, out, "AAAAAAAA");
+        n = FUZ_mdict_makeFrame(frame, 0, flags);
+        r = FUZ_mdict_decompressStream(dctx, out, sizeof(out), frame + skip, n - skip, chunk);
+        FUZ_mdict_checkOutput(r, out, "RRRRRRRR");
+        CHECK_Z(ZSTD_freeDCtx(dctx));
+    }
+
+    /* Ordinary frames compressed without a dictionary work with or without
+     * an explicitly registered empty ID-0 DDict. */
+    {   ZSTD_DDict* const empty = ZSTD_createDDict(NULL, 0);
+        ZSTD_DCtx* const dctx = FUZ_mdict_createContext(a, NULL);
+        BYTE plain[128];
+        CHECK(empty != NULL && ZSTD_getDictID_fromDDict(empty) == 0);
+        n = ZSTD_compress(plain, sizeof(plain), "no dictionary", 13, 1);
+        CHECK_Z(n);
+        r = ZSTD_decompressDCtx(dctx, out, sizeof(out), plain, n);
+        FUZ_mdict_checkOutput(r, out, "no dictionary");
+        CHECK_Z(ZSTD_DCtx_reset(dctx, ZSTD_reset_session_only));
+        r = FUZ_mdict_decompressStream(dctx, out, sizeof(out), plain, n, 1);
+        FUZ_mdict_checkOutput(r, out, "no dictionary");
+        CHECK_Z(ZSTD_DCtx_reset(dctx, ZSTD_reset_session_only));
+        CHECK_Z(ZSTD_DCtx_refDDict(dctx, empty));
+        CHECK_Z(ZSTD_DCtx_refDDict(dctx, a));
+        r = ZSTD_decompressDCtx(dctx, out, sizeof(out), plain, n);
+        FUZ_mdict_checkOutput(r, out, "no dictionary");
+        CHECK_Z(ZSTD_DCtx_refDDict(dctx, a));
+        r = FUZ_mdict_decompressStream(dctx, out, sizeof(out), plain, n, 1);
+        FUZ_mdict_checkOutput(r, out, "no dictionary");
+        CHECK_Z(ZSTD_freeDCtx(dctx)); CHECK_Z(ZSTD_freeDDict(empty));
+    }
+    CHECK_Z(ZSTD_freeDDict(a)); CHECK_Z(ZSTD_freeDDict(b));
+    CHECK_Z(ZSTD_freeDDict(raw));
+}
+
+static void FUZ_mdict_testEmptyDDict(void)
+{
+    ZSTD_DDict* const a = FUZ_mdict_createDict(1111, 'A');
+    BYTE dummy = 0, plain[128], emptyFrame[32], frame[32], out[32];
+    ZSTD_DDict* const empty[] = {
+        ZSTD_createDDict(NULL, 0),
+        ZSTD_createDDict_byReference(NULL, 0),
+        ZSTD_createDDict(&dummy, 0),
+        ZSTD_createDDict_byReference(&dummy, 0)
+    };
+    size_t const plainSize = ZSTD_compress(plain, sizeof(plain), "no dictionary", 13, 1);
+    size_t const emptySize = ZSTD_compress(emptyFrame, sizeof(emptyFrame), NULL, 0, 1);
+    size_t i;
+    CHECK_Z(plainSize);
+    CHECK_Z(emptySize);
+
+    for (i = 0; i < COUNTOF(empty); ++i) {
+        ZSTD_DCtx* dctx;
+        size_t n, r;
+        CHECK(empty[i] != NULL && ZSTD_getDictID_fromDDict(empty[i]) == 0);
+        dctx = FUZ_mdict_createContext(empty[i], a);
+
+        /* Installing an empty DDict must replace the previous frame's history,
+         * without doing arithmetic on a possibly NULL dictionary pointer. */
+        n = FUZ_mdict_makeFrame(frame, 1111, 0);
+        r = ZSTD_decompressDCtx(dctx, out, sizeof(out), frame, n);
+        FUZ_mdict_checkOutput(r, out, "AAAAAAAA");
+        r = ZSTD_decompressDCtx(dctx, out, sizeof(out), plain, plainSize);
+        FUZ_mdict_checkOutput(r, out, "no dictionary");
+        r = FUZ_mdict_decompressStream(dctx, out, sizeof(out), plain, plainSize, 1);
+        FUZ_mdict_checkOutput(r, out, "no dictionary");
+        r = FUZ_mdict_decompressStream(dctx, out, sizeof(out), plain, plainSize, plainSize);
+        FUZ_mdict_checkOutput(r, out, "no dictionary");
+
+        /* The explicit and bufferless APIs must accept the same DDict. */
+        r = ZSTD_decompress_usingDDict(dctx, out, sizeof(out), plain, plainSize, empty[i]);
+        FUZ_mdict_checkOutput(r, out, "no dictionary");
+        CHECK_Z(ZSTD_decompressBegin_usingDDict(dctx, empty[i]));
+        r = FUZ_mdict_decompressContinue(dctx, out, sizeof(out), plain, plainSize);
+        FUZ_mdict_checkOutput(r, out, "no dictionary");
+
+        /* Empty frames also permit a NULL, zero-capacity output buffer. */
+        CHECK_EQ(ZSTD_decompressDCtx(dctx, NULL, 0, emptyFrame, emptySize), 0);
+        CHECK_EQ(FUZ_mdict_decompressStream(dctx, NULL, 0, emptyFrame, emptySize, 1), 0);
+        CHECK_EQ(FUZ_mdict_decompressStream(dctx, NULL, 0, emptyFrame, emptySize, emptySize), 0);
+        CHECK_EQ(ZSTD_decompress_usingDDict(dctx, NULL, 0, emptyFrame, emptySize, empty[i]), 0);
+        CHECK_Z(ZSTD_decompressBegin_usingDDict(dctx, empty[i]));
+        CHECK_EQ(FUZ_mdict_decompressContinue(dctx, NULL, 0, emptyFrame, emptySize), 0);
+
+        CHECK_Z(ZSTD_DCtx_setParameter(dctx, ZSTD_d_refMultipleDDicts, 0));
+        CHECK_Z(ZSTD_DCtx_refDDict(dctx, empty[i]));
+        r = ZSTD_decompressDCtx(dctx, out, sizeof(out), plain, plainSize);
+        FUZ_mdict_checkOutput(r, out, "no dictionary");
+        CHECK_Z(ZSTD_freeDCtx(dctx));
+        CHECK_Z(ZSTD_freeDDict(empty[i]));
+    }
+    CHECK_Z(ZSTD_freeDDict(a));
+}
+
+static void FUZ_mdict_testNoDictionary(void)
+{
+    ZSTD_DDict* const a = FUZ_mdict_createDict(1111, 'A');
+    ZSTD_DDict* const b = FUZ_mdict_createDict(2222, 'B');
+    BYTE plain[128], frame[32], data[256], out[128];
+    size_t const plainSize = ZSTD_compress(plain, sizeof(plain), "no dictionary", 13, 1);
+    int mode;
+    CHECK_Z(plainSize);
+    CHECK_EQ(ZSTD_getDictID_fromFrame(plain, plainSize), 0);
+    CHECK_EQ(ZSTD_getFrameContentSize(plain, plainSize), 13);
+
+    for (mode = 0; mode < 4; ++mode) {
+        int const magicless = mode >> 1;
+        int const shortcut = mode & 1;
+        ZSTD_DCtx* const dctx = FUZ_mdict_createContext(a, b);
+        unsigned const flags = shortcut ? FUZ_mdict_knownSize : 0;
+        size_t const chunk = shortcut ? sizeof(data) : 1;
+        size_t const skip = magicless ? 4 : 0;
+        static const U32 ids[] = {2222, 1111};
+        size_t i, n = 0, r;
+        if (magicless) { CHECK_Z(ZSTD_DCtx_setParameter(dctx, ZSTD_d_format, ZSTD_f_zstd1_magicless)); }
+
+        /* Dictionary-free frames neither inherit the preceding dictionary nor
+         * prevent the following frame from selecting its own exact match. */
+        for (i = 0; i < COUNTOF(ids); ++i) {
+            size_t const frameSize = FUZ_mdict_makeFrame(frame, ids[i], flags);
+            memcpy(data + n, frame + skip, frameSize - skip); n += frameSize - skip;
+            memcpy(data + n, plain + skip, plainSize - skip); n += plainSize - skip;
+        }
+        r = ZSTD_decompressDCtx(dctx, out, sizeof(out), data, n);
+        FUZ_mdict_checkOutput(r, out, "BBBBBBBBno dictionaryAAAAAAAAno dictionary");
+        r = FUZ_mdict_decompressStream(dctx, out, sizeof(out), data, n, chunk);
+        FUZ_mdict_checkOutput(r, out, "BBBBBBBBno dictionaryAAAAAAAAno dictionary");
+        CHECK_Z(ZSTD_freeDCtx(dctx));
+    }
+    CHECK_Z(ZSTD_freeDDict(a)); CHECK_Z(ZSTD_freeDDict(b));
+}
+
+/* The DDicts use the default allocator. Counting the DCtx's first 128-slot
+ * table lets the test observe growth without inspecting private structs. */
+static void* FUZ_mdict_alloc(void* opaque, size_t size)
+{
+    if (size == 128 * sizeof(ZSTD_DDict*)) ++*(unsigned*)opaque;
+    return malloc(size);
+}
+
+static void FUZ_mdict_free(void* opaque, void* ptr)
+{
+    (void)opaque;
+    free(ptr);
+}
+
+static void FUZ_mdict_testGrowth(void)
+{
+    unsigned expansions = 0;
+    ZSTD_customMem const mem = {FUZ_mdict_alloc, FUZ_mdict_free, &expansions};
+    ZSTD_DCtx* const dctx = ZSTD_createDCtx_advanced(mem);
+    ZSTD_DDict* ddicts[49];
+    BYTE frame[32], out[32];
+    size_t i, n, r;
+    CHECK(dctx != NULL);
+    expansions = 0; /* Do not count allocation of the DCtx itself. */
+    CHECK_Z(ZSTD_DCtx_setParameter(dctx, ZSTD_d_refMultipleDDicts, 1));
+    for (i = 0; i < COUNTOF(ddicts); ++i) {
+        ddicts[i] = FUZ_mdict_createDict(10000 + (U32)i, 'A' + (int)(i % 26));
+        CHECK_Z(ZSTD_DCtx_refDDict(dctx, ddicts[i]));
+        /* A 64-slot table grows when inserting entry 49, after reaching 75%.
+         * The old expression incorrectly grew it when inserting entry 17. */
+        CHECK_EQ(expansions, (unsigned)(i == 48));
+    }
+    n = FUZ_mdict_makeFrame(frame, 10000, 0);
+    r = FUZ_mdict_decompressStream(dctx, out, sizeof(out), frame, n, 1);
+    FUZ_mdict_checkOutput(r, out, "AAAAAAAA"); /* Rehashing retained entry 1. */
+    CHECK_Z(ZSTD_freeDCtx(dctx));
+    for (i = 0; i < COUNTOF(ddicts); ++i) CHECK_Z(ZSTD_freeDDict(ddicts[i]));
+}
+
+static unsigned test_multipleDDicts(unsigned testNb)
+{
+    static const struct { const char* name; void (*run)(void); } tests[] = {
+        {"one-shot frame selection", FUZ_mdict_testOneShot},
+        {"explicit dictionary APIs", FUZ_mdict_testExplicitDictionary},
+        {"single-DDict prefix compatibility", FUZ_mdict_testPrefixCompatibility},
+        {"raw dictionary ID zero", FUZ_mdict_testZeroID},
+        {"clearing dictionary references", FUZ_mdict_testClearReferences},
+        {"disabling the dictionary set", FUZ_mdict_testDisableSet},
+        {"local dictionary replacement", FUZ_mdict_testDictionaryReplacement},
+        {"streaming frame selection", FUZ_mdict_testStreaming},
+        {"strict dictionary-ID matching", FUZ_mdict_testStrictMatching},
+        {"empty dictionaries", FUZ_mdict_testEmptyDDict},
+        {"dictionary-free frames", FUZ_mdict_testNoDictionary},
+        {"hash-table growth", FUZ_mdict_testGrowth},
+    };
+    size_t i;
+    for (i = 0; i < COUNTOF(tests); ++i) {
+        DISPLAYLEVEL(3, "test%3u : multi-DDict %s : ", testNb++, tests[i].name);
+        tests[i].run();
+        DISPLAYLEVEL(3, "OK \n");
+    }
+    return testNb;
+}
+
 static int basicUnitTests(U32 const seed, double compressibility)
 {
     size_t const CNBuffSize = 5 MB;
@@ -1026,6 +2323,10 @@ static int basicUnitTests(U32 const seed, double compressibility)
 
     test_decompressBound(testNb++);
 
+    test_decompressSplitLitBuffer(testNb++);
+
+    test_decompressContinueWithoutBegin(testNb++);
+
     test_setCParams(testNb++);
 
     DISPLAYLEVEL(3, "test%3u : ZSTD_adjustCParams : ", testNb++);
@@ -1157,13 +2458,10 @@ static int basicUnitTests(U32 const seed, double compressibility)
             ZSTD_getDictID_fromDDict;
         ZSTD_DStream* (* const funcptr_createDStream)(
             ZSTD_customMem customMem) = ZSTD_createDStream_advanced;
-        void (* const funcptr_copyDCtx)(
-            ZSTD_DCtx* dctx, const ZSTD_DCtx* preparedDCtx) = ZSTD_copyDCtx;
         ZSTD_nextInputType_e (* const funcptr_nextInputType)(ZSTD_DCtx* dctx) =
             ZSTD_nextInputType;
         const void *voidptr_getDictID;
         const void *voidptr_createDStream;
-        const void *voidptr_copyDCtx;
         const void *voidptr_nextInputType;
         DEBUG_STATIC_ASSERT(sizeof(funcptr_getDictID) == sizeof(voidptr_getDictID));
         memcpy(
@@ -1174,17 +2472,12 @@ static int basicUnitTests(U32 const seed, double compressibility)
             (void*)&voidptr_createDStream,
             (const void*)&funcptr_createDStream,
             sizeof(void*));
-        memcpy(
-            (void*)&voidptr_copyDCtx,
-            (const void*)&funcptr_copyDCtx,
-            sizeof(void*));
         memcpy(
             (void*)&voidptr_nextInputType,
             (const void*)&funcptr_nextInputType,
             sizeof(void*));
         DISPLAYLEVEL(3, "%p ", voidptr_getDictID);
         DISPLAYLEVEL(3, "%p ", voidptr_createDStream);
-        DISPLAYLEVEL(3, "%p ", voidptr_copyDCtx);
         DISPLAYLEVEL(3, "%p ", voidptr_nextInputType);
     }
     DISPLAYLEVEL(3, ": OK \n");
@@ -1372,7 +2665,7 @@ static int basicUnitTests(U32 const seed, double compressibility)
         ZSTD_freeCCtx(cctx);
     }
 
-    DISPLAYLEVEL(3, "test%3i : maxBlockSize = 2K", testNb++);
+    DISPLAYLEVEL(3, "test%3i : maxBlockSize = 2K : ", testNb++);
     {
         ZSTD_CCtx* cctx = ZSTD_createCCtx();
         ZSTD_DCtx* dctx = ZSTD_createDCtx();
@@ -1390,6 +2683,37 @@ static int basicUnitTests(U32 const seed, double compressibility)
         ZSTD_freeDCtx(dctx);
         ZSTD_freeCCtx(cctx);
     }
+    DISPLAYLEVEL(3, "OK \n");
+
+    DISPLAYLEVEL(3, "test%3i : maxBlockSize rejects raw & RLE blocks : ", testNb++);
+    {
+        ZSTD_CCtx* cctx = ZSTD_createCCtx();
+        ZSTD_DCtx* dctx = ZSTD_createDCtx();
+        size_t const srcSize = 8 KB;
+        BYTE* const src = (BYTE*)malloc(srcSize);
+        int i;
+        CHECK(src != NULL);
+        CHECK_Z(ZSTD_CCtx_setParameter(cctx, ZSTD_c_maxBlockSize, 2048));
+        CHECK_Z(ZSTD_DCtx_setParameter(dctx, ZSTD_d_maxBlockSize, 1024));
+
+        /* i = 0 : incompressible data, which is stored in raw blocks
+         * i = 1 : a single repeated byte, which is stored in RLE blocks */
+        for (i = 0; i < 2; i++) {
+            if (i == 0) {
+                RDG_genBuffer(src, srcSize, 0.0, 0.0, seed);
+            } else {
+                memset(src, 'a', srcSize);
+            }
+            cSize = ZSTD_compress2(cctx, compressedBuffer, compressedBufferSize, src, srcSize);
+            CHECK_Z(cSize);
+            CHECK(ZSTD_isError(ZSTD_decompressDCtx(dctx, decodedBuffer, CNBuffSize, compressedBuffer, cSize)));
+        }
+
+        free(src);
+        ZSTD_freeDCtx(dctx);
+        ZSTD_freeCCtx(cctx);
+    }
+    DISPLAYLEVEL(3, "OK \n");
 
     DISPLAYLEVEL(3, "test%3i : ldm fill dict out-of-bounds check", testNb++);
     {
@@ -2321,6 +3645,82 @@ static int basicUnitTests(U32 const seed, double compressibility)
     }
     DISPLAYLEVEL(3, "OK \n");
 
+    DISPLAYLEVEL(3, "test%3i : static DCtx dictionary allocation : ", testNb++);
+    {
+        static const char dict[] = "static DCtx dictionary regression test";
+        size_t const dictSize = sizeof(dict);
+        size_t const dctxSize = ZSTD_estimateDCtxSize();
+        size_t const dstreamSize = ZSTD_estimateDStreamSize(1 KB);
+        size_t const ddictSize = ZSTD_estimateDDictSize(dictSize, ZSTD_dlm_byRef);
+        void* const workspace = malloc(dstreamSize);
+        void* const ddictWorkspace = malloc(ddictSize);
+        ZSTD_CCtx* const cctx = ZSTD_createCCtx();
+        const ZSTD_DDict* ddict;
+        char compressed[ZSTD_COMPRESSBOUND(sizeof(dict))];
+        char decoded[sizeof(dict)];
+        size_t compressedSize;
+        int fill;
+        int stream;
+        CHECK(workspace != NULL && ddictWorkspace != NULL && cctx != NULL);
+        ddict = ZSTD_initStaticDDict(ddictWorkspace, ddictSize, dict, dictSize,
+                                     ZSTD_dlm_byRef, ZSTD_dct_rawContent);
+        CHECK(ddict != NULL);
+        CHECK_VAR(compressedSize, ZSTD_compress_usingDict(cctx, compressed, sizeof(compressed),
+                                                         dict, dictSize, dict, dictSize, 1));
+        CHECK_Z(ZSTD_freeCCtx(cctx));
+
+        /* Both zeroed and reused workspaces must reject internal DDict
+         * creation without invoking an allocator. */
+        for (fill = 0; fill < 2; ++fill) {
+            for (stream = 0; stream < 2; ++stream) {
+                ZSTD_DCtx* dctx;
+                memset(workspace, fill ? 0xA5 : 0, dstreamSize);
+                dctx = stream ? ZSTD_initStaticDStream(workspace, dstreamSize)
+                              : ZSTD_initStaticDCtx(workspace, dctxSize);
+                CHECK(dctx != NULL);
+                CHECK_EQ(ZSTD_DCtx_loadDictionary(dctx, dict, dictSize), ERROR(memory_allocation));
+                CHECK_EQ(ZSTD_DCtx_loadDictionary_byReference(dctx, dict, dictSize), ERROR(memory_allocation));
+                CHECK_EQ(ZSTD_DCtx_loadDictionary_advanced(dctx, dict, dictSize, ZSTD_dlm_byCopy, ZSTD_dct_rawContent), ERROR(memory_allocation));
+                CHECK_EQ(ZSTD_DCtx_loadDictionary_advanced(dctx, dict, dictSize, ZSTD_dlm_byRef, ZSTD_dct_rawContent), ERROR(memory_allocation));
+                CHECK_EQ(ZSTD_DCtx_refPrefix(dctx, dict, dictSize), ERROR(memory_allocation));
+                CHECK_EQ(ZSTD_DCtx_refPrefix_advanced(dctx, dict, dictSize, ZSTD_dct_auto), ERROR(memory_allocation));
+                CHECK_EQ(ZSTD_initDStream_usingDict(dctx, dict, dictSize), ERROR(memory_allocation));
+                CHECK_EQ(dctx->customMem.customAlloc, NULL);
+                CHECK_EQ(dctx->customMem.customFree, NULL);
+                CHECK_EQ(dctx->customMem.opaque, NULL);
+
+                /* Clearing a dictionary does not allocate. */
+                CHECK_Z(ZSTD_DCtx_loadDictionary(dctx, NULL, dictSize));
+                CHECK_Z(ZSTD_DCtx_loadDictionary(dctx, dict, 0));
+                CHECK_Z(ZSTD_DCtx_loadDictionary_byReference(dctx, NULL, dictSize));
+                CHECK_Z(ZSTD_DCtx_loadDictionary_byReference(dctx, dict, 0));
+                CHECK_Z(ZSTD_DCtx_refPrefix(dctx, NULL, dictSize));
+                CHECK_Z(ZSTD_DCtx_refPrefix(dctx, dict, 0));
+                CHECK_Z(ZSTD_initDStream_usingDict(dctx, NULL, dictSize));
+                CHECK_Z(ZSTD_initDStream_usingDict(dctx, dict, 0));
+
+                /* A caller-owned DDict remains usable without allocation. */
+                CHECK_Z(ZSTD_DCtx_refDDict(dctx, ddict));
+                CHECK_EQ(ZSTD_decompressDCtx(dctx, decoded, sizeof(decoded), compressed, compressedSize), dictSize);
+                CHECK_EQ(memcmp(decoded, dict, dictSize), 0);
+                CHECK_Z(ZSTD_DCtx_reset(dctx, ZSTD_reset_session_and_parameters));
+                if (stream) {
+                    ZSTD_inBuffer input = { compressed, compressedSize, 0 };
+                    ZSTD_outBuffer output = { decoded, sizeof(decoded), 0 };
+                    CHECK_Z(ZSTD_initDStream_usingDDict(dctx, ddict));
+                    CHECK_EQ(ZSTD_decompressStream(dctx, &output, &input), 0);
+                    CHECK_EQ(input.pos, compressedSize);
+                    CHECK_EQ(output.pos, dictSize);
+                    CHECK_EQ(memcmp(decoded, dict, dictSize), 0);
+                }
+                CHECK_Z(ZSTD_DCtx_reset(dctx, ZSTD_reset_session_and_parameters));
+            }
+        }
+        free(ddictWorkspace);
+        free(workspace);
+    }
+    DISPLAYLEVEL(3, "OK \n");
+
     /* Static CCtx tests */
 #define STATIC_CCTX_LEVEL 4
     DISPLAYLEVEL(3, "test%3i : create static CCtx for level %u : ", testNb++, STATIC_CCTX_LEVEL);
@@ -2816,19 +4216,12 @@ static int basicUnitTests(U32 const seed, double compressibility)
 
     /* Dictionary and CCtx Duplication tests */
     {   ZSTD_CCtx* const ctxOrig = ZSTD_createCCtx();
-        ZSTD_CCtx* const ctxDuplicated = ZSTD_createCCtx();
         ZSTD_DCtx* const dctx = ZSTD_createDCtx();
         static const size_t dictSize = 551;
-        assert(dctx != NULL); assert(ctxOrig != NULL); assert(ctxDuplicated != NULL);
-
-        DISPLAYLEVEL(3, "test%3i : copy context too soon : ", testNb++);
-        { size_t const copyResult = ZSTD_copyCCtx(ctxDuplicated, ctxOrig, 0);
-          if (!ZSTD_isError(copyResult)) goto _output_error; }   /* error must be detected */
-        DISPLAYLEVEL(3, "OK \n");
+        assert(dctx != NULL); assert(ctxOrig != NULL);
 
         DISPLAYLEVEL(3, "test%3i : load dictionary into context : ", testNb++);
         CHECK_Z( ZSTD_compressBegin_usingDict(ctxOrig, CNBuffer, dictSize, 2) );
-        CHECK_Z( ZSTD_copyCCtx(ctxDuplicated, ctxOrig, 0) ); /* Begin_usingDict implies unknown srcSize, so match that */
         DISPLAYLEVEL(3, "OK \n");
 
         DISPLAYLEVEL(3, "test%3i : compress with flat dictionary : ", testNb++);
@@ -2847,25 +4240,6 @@ static int basicUnitTests(U32 const seed, double compressibility)
                   if (r != CNBuffSize - dictSize) goto _output_error);
         DISPLAYLEVEL(3, "OK \n");
 
-        DISPLAYLEVEL(3, "test%3i : compress with duplicated context : ", testNb++);
-        {   size_t const cSizeOrig = cSize;
-            cSize = 0;
-            CHECKPLUS(r, ZSTD_compressEnd(ctxDuplicated,
-                                    compressedBuffer, compressedBufferSize,
-                       (const char*)CNBuffer + dictSize, CNBuffSize - dictSize),
-                      cSize += r);
-            if (cSize != cSizeOrig) goto _output_error;   /* should be identical ==> same size */
-        }
-        DISPLAYLEVEL(3, "OK (%u bytes : %.2f%%)\n", (unsigned)cSize, (double)cSize/CNBuffSize*100);
-
-        DISPLAYLEVEL(3, "test%3i : frame built with duplicated context should be decompressible : ", testNb++);
-        CHECKPLUS(r, ZSTD_decompress_usingDict(dctx,
-                                           decodedBuffer, CNBuffSize,
-                                           compressedBuffer, cSize,
-                                           CNBuffer, dictSize),
-                  if (r != CNBuffSize - dictSize) goto _output_error);
-        DISPLAYLEVEL(3, "OK \n");
-
         DISPLAYLEVEL(3, "test%3i : decompress with DDict : ", testNb++);
         {   ZSTD_DDict* const ddict = ZSTD_createDDict(CNBuffer, dictSize);
             size_t const r = ZSTD_decompress_usingDDict(dctx, decodedBuffer, CNBuffSize, compressedBuffer, cSize, ddict);
@@ -2886,19 +4260,6 @@ static int basicUnitTests(U32 const seed, double compressibility)
             DISPLAYLEVEL(3, "OK (size of static DDict : %u) \n", (unsigned)ddictBufferSize);
         }
 
-        DISPLAYLEVEL(3, "test%3i : check content size on duplicated context : ", testNb++);
-        {   size_t const testSize = CNBuffSize / 3;
-            CHECK_Z( ZSTD_compressBegin(ctxOrig, ZSTD_defaultCLevel()) );
-            CHECK_Z( ZSTD_copyCCtx(ctxDuplicated, ctxOrig, testSize) );
-
-            CHECK_VAR(cSize, ZSTD_compressEnd(ctxDuplicated, compressedBuffer, ZSTD_compressBound(testSize),
-                                          (const char*)CNBuffer + dictSize, testSize) );
-            {   ZSTD_FrameHeader zfh;
-                if (ZSTD_getFrameHeader(&zfh, compressedBuffer, cSize)) goto _output_error;
-                if ((zfh.frameContentSize != testSize) && (zfh.frameContentSize != 0)) goto _output_error;
-        }   }
-        DISPLAYLEVEL(3, "OK \n");
-
 #if !defined(ZSTD_EXCLUDE_DFAST_BLOCK_COMPRESSOR) \
  && !defined(ZSTD_EXCLUDE_GREEDY_BLOCK_COMPRESSOR) \
  && !defined(ZSTD_EXCLUDE_DFAST_BLOCK_COMPRESSOR) \
@@ -3008,7 +4369,6 @@ static int basicUnitTests(U32 const seed, double compressibility)
 #endif
 
         ZSTD_freeCCtx(ctxOrig);
-        ZSTD_freeCCtx(ctxDuplicated);
         ZSTD_freeDCtx(dctx);
     }
 
@@ -3060,6 +4420,16 @@ static int basicUnitTests(U32 const seed, double compressibility)
         if (ZDICT_isError(dictSize)) goto _output_error;
         DISPLAYLEVEL(3, "OK, created dictionary of size %u \n", (unsigned)dictSize);
 
+        DISPLAYLEVEL(3, "test%3i : FASTCOVER dictBuilder samples smaller than a dmer (issue #4499) : ", testNb++);
+        {   const char smallData[8] = { 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H' };
+            const size_t smallSizes[8] = { 1, 1, 1, 1, 1, 1, 1, 1 };
+            size_t const r = ZDICT_trainFromBuffer(dictBuffer, dictBufferCapacity,
+                                                   smallData, smallSizes, 8);
+            if (!ZDICT_isError(r)) goto _output_error;
+            if (ZSTD_getErrorCode(r) != ZSTD_error_srcSize_wrong) goto _output_error;
+        }
+        DISPLAYLEVEL(3, "OK \n");
+
         DISPLAYLEVEL(3, "test%3i : Multithreaded COVER dictBuilder : ", testNb++);
         { U32 u; for (u=0; u> 32), (unsigned)hash,
+                                    (unsigned)(expectedHash[dIdx] >> 32), (unsigned)expectedHash[dIdx]);
+                    goto _output_error;
+                }
+            }
+            free(coverDict);
+        }
+        DISPLAYLEVEL(3, "OK \n");
+
+        DISPLAYLEVEL(3, "test%3i : COVER_selectDict shrinkDict with content < ZDICT_DICTSIZE_MIN : ", testNb++);
+        /* The shrink loop tries suffixes of the content, starting at ZDICT_DICTSIZE_MIN.
+         * With smaller content no suffix fits, so the full dictionary must be returned.
+         * The content is allocated at its exact size, so under ASan any read before it
+         * is reported as a heap-buffer-overflow. */
+        {   size_t offsets[8];
+            unsigned const nbSelectSamples = (unsigned)(sizeof(offsets) / sizeof(offsets[0]));
+            size_t const contentSize = ZDICT_DICTSIZE_MIN - 16;
+            size_t const selectCapacity = 4 KB;
+            BYTE* const content = (BYTE*)malloc(contentSize);
+            BYTE* const expected = (BYTE*)malloc(selectCapacity);
+            ZDICT_cover_params_t coverParams;
+            COVER_dictSelection_t selection;
+            size_t expectedSize;
+            unsigned u;
+            if (content == NULL || expected == NULL) {
+                free(content);
+                free(expected);
+                goto _output_error;
+            }
+            memcpy(content, CNBuffer, contentSize);
+            for (u = 0; u < nbSelectSamples; u++) offsets[u] = u * sampleUnitSize;
+            memset(&coverParams, 0, sizeof(coverParams));
+            coverParams.splitPoint = 1.0;
+            coverParams.shrinkDict = 1;
+            coverParams.shrinkDictMaxRegression = 100000;  /* accept any candidate */
+            expectedSize = ZDICT_finalizeDictionary(expected, selectCapacity,
+                                                    content, contentSize,
+                                                    CNBuffer, samplesSizes, nbSelectSamples,
+                                                    coverParams.zParams);
+            selection = COVER_selectDict(content, selectCapacity, contentSize,
+                                         (const BYTE*)CNBuffer, samplesSizes, nbSelectSamples,
+                                         nbSelectSamples, nbSelectSamples, coverParams, offsets, 0);
+            {   int const ok = !ZDICT_isError(expectedSize)
+                            && !COVER_dictSelectionIsError(selection)
+                            && selection.dictSize == expectedSize
+                            && !memcmp(selection.dictContent, expected, expectedSize);
+                COVER_dictSelectionFree(selection);
+                free(content);
+                free(expected);
+                if (!ok) goto _output_error;
+            }
+        }
+        DISPLAYLEVEL(3, "OK \n");
+
         DISPLAYLEVEL(3, "test%3i : check dictID : ", testNb++);
         dictID = ZDICT_getDictID(dictBuffer, dictSize);
         if (dictID==0) goto _output_error;
@@ -3136,6 +4590,14 @@ static int basicUnitTests(U32 const seed, double compressibility)
         }
         DISPLAYLEVEL(3, "OK : %u \n", (unsigned)dictHeaderSize);
 
+        DISPLAYLEVEL(3, "test%3i : Dictionary creation errors : ", testNb++);
+        test_dictionaryLoadErrors(dictBuffer, dictSize);
+        DISPLAYLEVEL(3, "OK \n");
+
+        DISPLAYLEVEL(3, "test%3i : Large dictionary repcodes : ", testNb++);
+        test_largeDictionaryRepcodes(dictBuffer, dictSize);
+        DISPLAYLEVEL(3, "OK \n");
+
         DISPLAYLEVEL(3, "test%3i : compress with dictionary : ", testNb++);
         cSize = ZSTD_compress_usingDict(cctx, compressedBuffer, compressedBufferSize,
                                         CNBuffer, CNBuffSize,
@@ -3298,6 +4760,19 @@ static int basicUnitTests(U32 const seed, double compressibility)
         }
         DISPLAYLEVEL(3, "OK \n");
 
+        DISPLAYLEVEL(3, "test%3i : loading a dictionary containing only header should return dictionary_corrupted : ", testNb++);
+        {   ZSTD_DCtx* const dctx = ZSTD_createDCtx();
+            const char* const headerOnly = "\x37\xa4\x30\xec\x11\x22\x33\x44";
+            assert(dctx != NULL);
+            CHECK_EQ(ZSTD_getErrorCode(ZSTD_DCtx_loadDictionary(dctx, headerOnly, 8)), ZSTD_error_dictionary_corrupted);
+            CHECK_EQ(ZSTD_getErrorCode(ZSTD_DCtx_loadDictionary_byReference(dctx, headerOnly, 8)), ZSTD_error_dictionary_corrupted);
+            /* the context remains usable */
+            CHECK_Z(ZSTD_DCtx_loadDictionary(dctx, dictBuffer, dictSize));
+            CHECK_EQ(ZSTD_decompressDCtx(dctx, decodedBuffer, CNBuffSize, compressedBuffer, cSize), CNBuffSize);
+            ZSTD_freeDCtx(dctx);
+        }
+        DISPLAYLEVEL(3, "OK \n");
+
         DISPLAYLEVEL(3, "test%3d : bufferless api with cdict : ", testNb++);
         {   ZSTD_CDict* const cdict = ZSTD_createCDict(dictBuffer, dictSize, 1);
             ZSTD_DCtx* const dctx = ZSTD_createDCtx();
@@ -3690,6 +5165,8 @@ static int basicUnitTests(U32 const seed, double compressibility)
         }
         DISPLAYLEVEL(3, "OK \n");
 
+        testNb = test_multipleDDicts(testNb);
+
         DISPLAYLEVEL(3, "test%3i : ZSTD_decompressDCtx() with multiple ddicts : ", testNb++);
         {
             const size_t numDicts = 128;
@@ -3941,6 +5418,43 @@ static int basicUnitTests(U32 const seed, double compressibility)
         }
         DISPLAYLEVEL(3, "OK (ASAN would report OOB write if vulnerable)\n");
 
+        DISPLAYLEVEL(3, "test%3i : DDict hashset is dropped by reset and refDDict(NULL) : ", testNb++);
+        {   /* After a parameter reset or refDDict(NULL), a frame must not select
+             * a DDict referenced before, which the caller may have freed since */
+            char* const dictBufA = (char*)malloc(dictBufferFixedSize);
+            ZSTD_DDict* const ddictB = ZSTD_createDDict(dictBufferFixed, dictBufferFixedSize);
+            ZSTD_DCtx* const dctx = ZSTD_createDCtx();
+            size_t frameSize;
+            int n;
+            if (!dictBufA || !ddictB || !dctx) goto _output_error;
+            ZSTD_memcpy(dictBufA, dictBufferFixed, dictBufferFixedSize);
+            MEM_writeLE32(dictBufA + ZSTD_FRAMEIDSIZE, ZSTD_getDictID_fromDDict(ddictB) + 1);
+            frameSize = ZSTD_compress_usingDict(cctx, compressedBuffer, compressedBufferSize,
+                                                CNBuffer, 100, dictBufA, dictBufferFixedSize, 1);
+            CHECK_Z(frameSize);
+            for (n = 0; n < 2; n++) {
+                ZSTD_DDict* const ddictA = ZSTD_createDDict(dictBufA, dictBufferFixedSize);
+                if (!ddictA) goto _output_error;
+                CHECK_Z( ZSTD_DCtx_setParameter(dctx, ZSTD_d_refMultipleDDicts, ZSTD_rmd_refMultipleDDicts) );
+                CHECK_Z( ZSTD_DCtx_refDDict(dctx, ddictA) );
+                if (n == 0) {
+                    CHECK_Z( ZSTD_DCtx_reset(dctx, ZSTD_reset_session_and_parameters) );
+                } else {
+                    CHECK_Z( ZSTD_DCtx_refDDict(dctx, NULL) );
+                }
+                ZSTD_freeDDict(ddictA);
+                /* only ddictB is referenced now : the frame needs ddictA, so it must fail */
+                CHECK_Z( ZSTD_DCtx_setParameter(dctx, ZSTD_d_refMultipleDDicts, ZSTD_rmd_refMultipleDDicts) );
+                CHECK_Z( ZSTD_DCtx_refDDict(dctx, ddictB) );
+                if (!ZSTD_isError(ZSTD_decompressDCtx(dctx, decodedBuffer, CNBuffSize, compressedBuffer, frameSize)))
+                    goto _output_error;
+            }
+            ZSTD_freeDCtx(dctx);
+            ZSTD_freeDDict(ddictB);
+            free(dictBufA);
+        }
+        DISPLAYLEVEL(3, "OK \n");
+
         ZSTD_freeCCtx(cctx);
         free(dictBuffer);
         free(samplesSizes);
@@ -3995,6 +5509,25 @@ static int basicUnitTests(U32 const seed, double compressibility)
         if (dictID==0) goto _output_error;
         DISPLAYLEVEL(3, "OK : %u \n", (unsigned)dictID);
 
+        DISPLAYLEVEL(3, "test%3i : ZDICT_optimizeTrainFromBuffer_cover with tiny training set : ", testNb++);
+        {   /* With splitPoint < 1.0 the training set is a subset of the corpus.
+             * If that subset is smaller than MAX(d, sizeof(U64)) the partial
+             * suffix array size used to underflow size_t (issue #4682). The
+             * builder must reject it cleanly instead. The first few samples are
+             * tiny so the 0.75 training split stays below the threshold, while
+             * the total corpus is comfortably large. */
+            size_t tinySizes[8] = { 1, 1, 1, 1, 1, 1, 100, 100 };
+            ZDICT_cover_params_t tinyParams;
+            size_t tinyResult;
+            memset(&tinyParams, 0, sizeof(tinyParams));
+            tinyParams.steps = 4;
+            tinyParams.splitPoint = 0.75;
+            tinyResult = ZDICT_optimizeTrainFromBuffer_cover(
+                dictBuffer, optDictSize, CNBuffer, tinySizes, 8, &tinyParams);
+            if (!ZDICT_isError(tinyResult)) goto _output_error;
+        }
+        DISPLAYLEVEL(3, "OK \n");
+
         ZSTD_freeCCtx(cctx);
         free(dictBuffer);
         free(samplesSizes);
@@ -4090,6 +5623,46 @@ static int basicUnitTests(U32 const seed, double compressibility)
         }
     }
 
+    DISPLAYLEVEL(3, "test%3i : oversized round buffer must fail cleanly on 32-bit : ", testNb++);
+    /* On 32-bit builds the round buffer size can exceed 4 GiB, which
+     * wraps in 32-bit size_t. Without the overflow check in
+     * ZSTDMT_initCStream_internal(), init succeeds with a buffer that
+     * is too small for the requested configuration (and if it wraps
+     * to 0, the first streaming copy overruns it). With the check,
+     * the first ZSTD_compressStream2() call fails cleanly with
+     * memory_allocation and nothing is copied. Both cases are
+     * unreachable on 64-bit builds, where they get a real buffer.
+     * - case 0: windowLog 30 with btultra2 (level 22) and LDM disabled
+     *   makes the overlap prefix 1 GiB, which bumps the job size to
+     *   1 GiB; 1 worker needs 4 GiB, which wraps to 0.
+     * - case 1: 16 workers with 256 MB jobs need 4.75 GiB, which
+     *   wraps to 768 MB. */
+    if (MEM_32bits()) {
+#ifdef ZSTD_MULTITHREAD
+        struct { int nbWorkers; int windowLog; int jobSize; } const cases[2] = {
+            { 1, 30, 0 },
+            { 16, 27, 256 MB },
+        };
+        size_t caseNb;
+        for (caseNb = 0; caseNb < 2; caseNb++) {
+            ZSTD_CCtx* const cctx = ZSTD_createCCtx();
+            ZSTD_outBuffer out = { compressedBuffer, compressedBufferSize, 0 };
+            ZSTD_inBuffer in = { CNBuffer, CNBuffSize, 0 };
+            size_t r;
+            CHECK(cctx != NULL);
+            CHECK_Z( ZSTD_CCtx_setParameter(cctx, ZSTD_c_nbWorkers, cases[caseNb].nbWorkers) );
+            CHECK_Z( ZSTD_CCtx_setParameter(cctx, ZSTD_c_compressionLevel, ZSTD_maxCLevel()) );
+            CHECK_Z( ZSTD_CCtx_setParameter(cctx, ZSTD_c_windowLog, cases[caseNb].windowLog) );
+            CHECK_Z( ZSTD_CCtx_setParameter(cctx, ZSTD_c_jobSize, cases[caseNb].jobSize) );
+            CHECK_Z( ZSTD_CCtx_setParameter(cctx, ZSTD_c_enableLongDistanceMatching, ZSTD_ps_disable) );
+            r = ZSTD_compressStream2(cctx, &out, &in, ZSTD_e_continue);
+            ZSTD_freeCCtx(cctx);
+            if (ZSTD_getErrorCode(r) != ZSTD_error_memory_allocation) goto _output_error;
+        }
+#endif
+    }
+    DISPLAYLEVEL(3, "OK \n");
+
     /* advanced parameters for decompression */
     {   ZSTD_DCtx* const dctx = ZSTD_createDCtx();
         assert(dctx != NULL);
@@ -4445,6 +6018,7 @@ static int basicUnitTests(U32 const seed, double compressibility)
     testNb = test_convertSequences_noRepcodes(seed, testNb);
 
     testNb = test_get1BlockSummary(testNb);
+    testNb = test_blockSplitLimit(testNb);
 
     DISPLAYLEVEL(3, "test%3i : ZSTD_compressSequencesAndLiterals : ", testNb++);
     {
@@ -4476,6 +6050,13 @@ static int basicUnitTests(U32 const seed, double compressibility)
         {   size_t const litSize = FUZ_getLitSize(seqs, nbSeqs);
             FUZ_transferLiterals(litBuffer, decompressSize, CNBuffer, srcSize, seqs, nbSeqs);
 
+            /* literal buffer capacity without 8 bytes of margin: must fail */
+            compressedSize = ZSTD_compressSequencesAndLiterals(cctx, dst, dstCapacity, seqs, nbSeqs, litBuffer, litSize, litSize+7, srcSize);
+            if (ZSTD_getErrorCode(compressedSize) != ZSTD_error_workSpace_tooSmall) {
+                DISPLAY("ZSTD_compressSequencesAndLiterals() should have failed: literals buffer capacity is too small\n");
+                goto _output_error;
+            }
+
             /* not enough literals: must fail */
             compressedSize = ZSTD_compressSequencesAndLiterals(cctx, dst, dstCapacity, seqs, nbSeqs, src, litSize-1, decompressSize, srcSize);
             if (!ZSTD_isError(compressedSize)) {
@@ -4505,7 +6086,7 @@ static int basicUnitTests(U32 const seed, double compressibility)
             }
 
             /* correct amount of literals: should compress successfully */
-            compressedSize = ZSTD_compressSequencesAndLiterals(cctx, dst, dstCapacity, seqs, nbSeqs, litBuffer, litSize, decompressSize, srcSize);
+            compressedSize = ZSTD_compressSequencesAndLiterals(cctx, dst, dstCapacity, seqs, nbSeqs, litBuffer, litSize, litSize+8, srcSize);
             if (ZSTD_isError(compressedSize)) {
                 DISPLAY("Error in ZSTD_compressSequencesAndLiterals()\n");
                 goto _output_error;
@@ -4548,6 +6129,18 @@ static int basicUnitTests(U32 const seed, double compressibility)
     }
     DISPLAYLEVEL(3, "OK \n");
 
+    DISPLAYLEVEL(3, "test%3i : ZSTD_compressSequencesAndLiterals : reject a too large block : ", testNb++);
+    {   /* one block of ZSTD_BLOCKSIZE_MAX + 1 bytes : 16 literals and a match, then the block delimiter */
+        ZSTD_Sequence const seqs[2] = { { 1, 16, ZSTD_BLOCKSIZE_MAX + 1 - 16, 0 }, { 0, 0, 0, 0 } };
+        ZSTD_CCtx* const cctx = ZSTD_createCCtx();
+        CHECK(cctx != NULL);
+        CHECK_Z( ZSTD_CCtx_setParameter(cctx, ZSTD_c_blockDelimiters, ZSTD_sf_explicitBlockDelimiters) );
+        CHECK( ZSTD_isError(ZSTD_compressSequencesAndLiterals(cctx, compressedBuffer, compressedBufferSize,
+                                seqs, 2, CNBuffer, 16, CNBuffSize, ZSTD_BLOCKSIZE_MAX + 1)) );
+        ZSTD_freeCCtx(cctx);
+    }
+    DISPLAYLEVEL(3, "OK \n");
+
     /* Multiple blocks of zeros test */
     #define LONGZEROSLENGTH 1000000 /* 1MB of zeros */
     DISPLAYLEVEL(3, "test%3i : compress %u zeroes : ", testNb++, LONGZEROSLENGTH);
@@ -5241,7 +6834,6 @@ static int fuzzerTests(U32 seed, unsigned nbTests, unsigned startTest, U32 const
     BYTE* const cBuffer = (BYTE*) malloc (cBufferSize);
     BYTE* const dstBuffer = (BYTE*) malloc (dstBufferSize);
     BYTE* const mirrorBuffer = (BYTE*) malloc (dstBufferSize);
-    ZSTD_CCtx* const refCtx = ZSTD_createCCtx();
     ZSTD_CCtx* const ctx = ZSTD_createCCtx();
     ZSTD_DCtx* const dctx = ZSTD_createDCtx();
     U32 result = 0;
@@ -5258,7 +6850,7 @@ static int fuzzerTests(U32 seed, unsigned nbTests, unsigned startTest, U32 const
     cNoiseBuffer[3] = (BYTE*)malloc (srcBufferSize);
     cNoiseBuffer[4] = (BYTE*)malloc (srcBufferSize);
     CHECK (!cNoiseBuffer[0] || !cNoiseBuffer[1] || !cNoiseBuffer[2] || !cNoiseBuffer[3] || !cNoiseBuffer[4]
-           || !dstBuffer || !mirrorBuffer || !cBuffer || !refCtx || !ctx || !dctx,
+           || !dstBuffer || !mirrorBuffer || !cBuffer || !ctx || !dctx,
            "Not enough memory, fuzzer tests cancelled");
 
     /* Create initial samples */
@@ -5441,16 +7033,15 @@ static int fuzzerTests(U32 seed, unsigned nbTests, unsigned startTest, U32 const
                             testNb, (unsigned)maxTestSize, cLevel, (unsigned)dictSize);
 
             if (FUZ_rand(&lseed) & 0xF) {
-                CHECK_Z ( ZSTD_compressBegin_usingDict(refCtx, dict, dictSize, cLevel) );
+                CHECK_Z ( ZSTD_compressBegin_usingDict(ctx, dict, dictSize, cLevel) );
             } else {
                 ZSTD_compressionParameters const cPar = ZSTD_getCParams(cLevel, ZSTD_CONTENTSIZE_UNKNOWN, dictSize);
                 ZSTD_frameParameters const fPar = { FUZ_rand(&lseed)&1 /* contentSizeFlag */,
                                                     !(FUZ_rand(&lseed)&3) /* contentChecksumFlag*/,
                                                     0 /*NodictID*/ };   /* note : since dictionary is fake, dictIDflag has no impact */
                 ZSTD_parameters const p = FUZ_makeParams(cPar, fPar);
-                CHECK_Z ( ZSTD_compressBegin_advanced(refCtx, dict, dictSize, p, 0) );
+                CHECK_Z ( ZSTD_compressBegin_advanced(ctx, dict, dictSize, p, ZSTD_CONTENTSIZE_UNKNOWN) );
             }
-            CHECK_Z( ZSTD_copyCCtx(ctx, refCtx, 0) );
         }
 
         {   U32 const nbChunks = (FUZ_rand(&lseed) & 127) + 2;
@@ -5514,7 +7105,6 @@ static int fuzzerTests(U32 seed, unsigned nbTests, unsigned startTest, U32 const
     DISPLAY("\r%u fuzzer tests completed   \n", testNb-1);
 
 _cleanup:
-    ZSTD_freeCCtx(refCtx);
     ZSTD_freeCCtx(ctx);
     ZSTD_freeDCtx(dctx);
     free(cNoiseBuffer[0]);
diff --git a/tests/invalidDictionaries.c b/tests/invalidDictionaries.c
index 66caa9e99cb..70e42a0808a 100644
--- a/tests/invalidDictionaries.c
+++ b/tests/invalidDictionaries.c
@@ -9,6 +9,9 @@
  */
 
 #include 
+#include 
+#include 
+#define ZSTD_STATIC_LINKING_ONLY
 #include "zstd.h"
 
 static const char invalidRepCode[] = {
@@ -38,8 +41,189 @@ static const dictionary dictionaries[] = {
   {NULL, 0},
 };
 
+static void* countAllocations(void* opaque, size_t size) {
+  size_t* const count = (size_t*)opaque;
+  ++*count;
+  (void)size;
+  return NULL;
+}
+
+static void ignoreFree(void* opaque, void* address) {
+  (void)opaque;
+  (void)address;
+}
+
+#define CHECK(condition)                                                   \
+  do {                                                                     \
+    if (!(condition)) {                                                    \
+      fprintf(stderr, "Dictionary size test failed at line %d: %s\n",     \
+              __LINE__, #condition);                                       \
+      goto cleanup;                                                        \
+    }                                                                      \
+  } while (0)
+
+static int testCDictSizeOverflow(void) {
+  static const char dict[] = "a small raw content dictionary";
+  ZSTD_compressionParameters cParams = ZSTD_getCParams(1, 0, 0);
+  ZSTD_CCtx_params* const params = ZSTD_createCCtxParams();
+  ZSTD_CDict* cdict = NULL;
+  void* workspace = NULL;
+  size_t allocCount = 0;
+  ZSTD_customMem const customMem = {countAllocations, ignoreFree, &allocCount};
+  size_t baseSize;
+  size_t workspaceSize;
+  int result = 1;
+
+  CHECK(params != NULL);
+  CHECK(!ZSTD_isError(ZSTD_CCtxParams_init(params, 1)));
+  cParams.windowLog = ZSTD_WINDOWLOG_MIN;
+  cParams.hashLog = ZSTD_HASHLOG_MIN;
+  cParams.chainLog = ZSTD_CHAINLOG_MIN;
+  baseSize = ZSTD_estimateCDictSize_advanced(0, cParams, ZSTD_dlm_byCopy);
+  CHECK(!ZSTD_isError(baseSize));
+  workspaceSize = ZSTD_estimateCDictSize_advanced(sizeof(dict), cParams, ZSTD_dlm_byCopy);
+  CHECK(!ZSTD_isError(workspaceSize));
+  workspace = malloc(workspaceSize);
+  CHECK(workspace != NULL);
+
+  {
+    size_t const maxSize = (size_t)-1;
+    size_t const badSizes[] = {
+      maxSize,
+      maxSize - 1,
+      maxSize - (sizeof(void*) - 1),
+      /* The estimator includes a conservative match-state allowance, so stay
+       * well inside the overflow range for both static and heap workspaces. */
+      maxSize - baseSize / 2,
+    };
+    size_t i;
+    for (i = 0; i < sizeof(badSizes) / sizeof(badSizes[0]); ++i) {
+      size_t const dictSize = badSizes[i];
+      /* Exercise the public reproducer as well as each workspace-sizing path.
+       * The dictionary is deliberately much smaller than the claimed size:
+       * overflow must be rejected before the input is read or copied. */
+      cdict = ZSTD_createCDict(dict, dictSize, 1);
+      CHECK(cdict == NULL);
+      CHECK(ZSTD_isError(ZSTD_estimateCDictSize(dictSize, 1)));
+      CHECK(ZSTD_isError(ZSTD_estimateCDictSize_advanced(dictSize, cParams, ZSTD_dlm_byCopy)));
+      cdict = ZSTD_createCDict_advanced(dict, dictSize, ZSTD_dlm_byCopy,
+                                      ZSTD_dct_auto, cParams, customMem);
+      CHECK(cdict == NULL);
+      cdict = ZSTD_createCDict_advanced2(dict, dictSize, ZSTD_dlm_byCopy,
+                                       ZSTD_dct_auto, params, customMem);
+      CHECK(cdict == NULL);
+      CHECK(allocCount == 0);
+      CHECK(ZSTD_initStaticCDict(workspace, workspaceSize, dict, dictSize,
+                                ZSTD_dlm_byCopy, ZSTD_dct_auto, cParams) == NULL);
+      /* By-reference estimates don't include a dictionary allocation. */
+      CHECK(ZSTD_estimateCDictSize_advanced(dictSize, cParams, ZSTD_dlm_byRef) == baseSize);
+    }
+  }
+
+  {
+    size_t const goodSizes[] = {0, 1, sizeof(void*) - 1, sizeof(void*), sizeof(dict)};
+    size_t i;
+    for (i = 0; i < sizeof(goodSizes) / sizeof(goodSizes[0]); ++i) {
+      size_t const dictSize = goodSizes[i];
+      size_t const neededSize = ZSTD_estimateCDictSize_advanced(dictSize, cParams, ZSTD_dlm_byCopy);
+      CHECK(!ZSTD_isError(neededSize));
+      CHECK(neededSize <= workspaceSize);
+      cdict = ZSTD_createCDict(dict, dictSize, 1);
+      CHECK(cdict != NULL);
+      ZSTD_freeCDict(cdict);
+      cdict = NULL;
+      CHECK(ZSTD_initStaticCDict(workspace, neededSize, dict, dictSize,
+                                ZSTD_dlm_byCopy, ZSTD_dct_auto, cParams) != NULL);
+      CHECK(ZSTD_initStaticCDict(workspace, baseSize, dict, dictSize,
+                                ZSTD_dlm_byRef, ZSTD_dct_auto, cParams) != NULL);
+    }
+  }
+  result = 0;
+
+cleanup:
+  ZSTD_freeCDict(cdict);
+  ZSTD_freeCCtxParams(params);
+  free(workspace);
+  return result;
+}
+
+static int testDDictSizeOverflow(void) {
+  static const char dict[] = "a small raw content dictionary";
+  size_t const maxSize = (size_t)-1;
+  size_t const baseSize = ZSTD_estimateDDictSize(0, ZSTD_dlm_byCopy);
+  size_t const workspaceSize = ZSTD_estimateDDictSize(sizeof(dict), ZSTD_dlm_byCopy);
+  ZSTD_DDict* ddict = NULL;
+  void* workspace = NULL;
+  int result = 1;
+
+  CHECK(!ZSTD_isError(baseSize));
+  CHECK(!ZSTD_isError(workspaceSize));
+  CHECK(workspaceSize == baseSize + sizeof(dict));
+  workspace = malloc(workspaceSize);
+  CHECK(workspace != NULL);
+
+  {
+    size_t const badSizes[] = {
+      maxSize,
+      maxSize - 1,
+      /* The smallest dictSize that overflows the sum. */
+      maxSize - baseSize + 1,
+    };
+    size_t i;
+    for (i = 0; i < sizeof(badSizes) / sizeof(badSizes[0]); ++i) {
+      size_t const dictSize = badSizes[i];
+      /* Reject overflow before accessing the deliberately short dictionary.
+       * An error code must not be mistaken for a valid workspace size. */
+      CHECK(ZSTD_initStaticDDict(workspace, workspaceSize, dict, dictSize,
+                                ZSTD_dlm_byCopy, ZSTD_dct_auto) == NULL);
+      CHECK(ZSTD_initStaticDDict(workspace, maxSize, dict, dictSize,
+                                ZSTD_dlm_byCopy, ZSTD_dct_auto) == NULL);
+      CHECK(ZSTD_isError(ZSTD_estimateDDictSize(dictSize, ZSTD_dlm_byCopy)));
+      CHECK(ZSTD_estimateDDictSize(dictSize, ZSTD_dlm_byRef) == baseSize);
+    }
+    /* Large representable estimates are unchanged. Stay below the values
+     * reserved for error codes without allocating the advertised size. */
+    CHECK(ZSTD_estimateDDictSize(maxSize - 2 * baseSize, ZSTD_dlm_byCopy)
+          == maxSize - baseSize);
+  }
+
+  {
+    size_t const goodSizes[] = {0, 1, sizeof(void*) - 1, sizeof(void*), sizeof(dict)};
+    size_t i;
+    for (i = 0; i < sizeof(goodSizes) / sizeof(goodSizes[0]); ++i) {
+      size_t const dictSize = goodSizes[i];
+      size_t const neededSize = ZSTD_estimateDDictSize(dictSize, ZSTD_dlm_byCopy);
+      CHECK(neededSize == baseSize + dictSize);
+      CHECK(ZSTD_estimateDDictSize(dictSize, ZSTD_dlm_byRef) == baseSize);
+      CHECK(ZSTD_initStaticDDict(workspace, neededSize - 1, dict, dictSize,
+                                ZSTD_dlm_byCopy, ZSTD_dct_auto) == NULL);
+      CHECK(ZSTD_initStaticDDict(workspace, neededSize, dict, dictSize,
+                                ZSTD_dlm_byCopy, ZSTD_dct_auto) != NULL);
+      CHECK(ZSTD_initStaticDDict(workspace, baseSize - 1, dict, dictSize,
+                                ZSTD_dlm_byRef, ZSTD_dct_auto) == NULL);
+      CHECK(ZSTD_initStaticDDict(workspace, baseSize, dict, dictSize,
+                                ZSTD_dlm_byRef, ZSTD_dct_auto) != NULL);
+      ddict = ZSTD_createDDict(dict, dictSize);
+      CHECK(ddict != NULL);
+      CHECK(ZSTD_sizeof_DDict(ddict) == neededSize);
+      ZSTD_freeDDict(ddict);
+      ddict = NULL;
+    }
+  }
+  result = 0;
+
+cleanup:
+  ZSTD_freeDDict(ddict);
+  free(workspace);
+  return result;
+}
+
+#undef CHECK
+
 int main(int argc, const char** argv) {
   const dictionary *dict;
+  if (testCDictSizeOverflow()) return 3;
+  if (testDDictSizeOverflow()) return 4;
   for (dict = dictionaries; dict->data != NULL; ++dict) {
     ZSTD_CDict *cdict;
     ZSTD_DDict *ddict;
diff --git a/tests/legacy.c b/tests/legacy.c
index 18a590f1d51..a4a8da33402 100644
--- a/tests/legacy.c
+++ b/tests/legacy.c
@@ -256,6 +256,196 @@ static int testFrameDecoding(const char* compressed, size_t compressedSize,
     return 0;
 }
 
+static int testDictionaryAPI(unsigned version,
+                             const char* compressed, size_t compressedSize,
+                             const void* dict, size_t dictSize)
+{
+    size_t const expectedSize = strlen(BLOCK);
+    char* const decoded = malloc(expectedSize);
+    ZSTD_DCtx* const dctx = ZSTD_createDCtx();
+    ZSTD_inBuffer input = { compressed, compressedSize, 0 };
+    ZSTD_outBuffer output = { decoded, expectedSize, 0 };
+    size_t ret;
+    int result = 1;
+
+    if (decoded == NULL || dctx == NULL) {
+        DISPLAY("ERROR: Not enough memory!\n");
+        goto cleanup;
+    }
+
+    ret = ZSTD_decompress_usingDict(dctx, decoded, expectedSize,
+                                    compressed, compressedSize, dict, dictSize);
+    if (ZSTD_isError(ret)) {
+        DISPLAY("ERROR: ZSTD_decompress_usingDict: %s\n", ZSTD_getErrorName(ret));
+        goto cleanup;
+    }
+    if (ret != expectedSize || memcmp(decoded, BLOCK, expectedSize) != 0) {
+        DISPLAY("ERROR: Wrong decoded output with dictionary\n");
+        goto cleanup;
+    }
+
+    /* Formats older than v0.4 have no streaming decoder. */
+    if (version < OLDEST_STREAMABLE_VERSION) {
+        result = 0;
+        goto cleanup;
+    }
+
+    ret = ZSTD_DCtx_reset(dctx, ZSTD_reset_session_and_parameters);
+    if (ZSTD_isError(ret)) goto error;
+    /* Keep the exact-sized allocation visible to ASan. */
+    ret = ZSTD_DCtx_loadDictionary_byReference(dctx, dict, dictSize);
+    if (ZSTD_isError(ret)) goto error;
+    do {
+        size_t const oldInputPos = input.pos;
+        size_t const oldOutputPos = output.pos;
+        ret = ZSTD_decompressStream(dctx, &output, &input);
+        if (ZSTD_isError(ret)) goto error;
+        if (ret != 0 && input.pos == oldInputPos && output.pos == oldOutputPos) {
+            DISPLAY("ERROR: Dictionary stream made no progress\n");
+            goto cleanup;
+        }
+    } while (ret != 0);
+    if (input.pos != input.size || output.pos != expectedSize
+        || memcmp(decoded, BLOCK, expectedSize) != 0) {
+        DISPLAY("ERROR: Wrong streaming output with dictionary\n");
+        goto cleanup;
+    }
+    result = 0;
+    goto cleanup;
+
+error:
+    DISPLAY("ERROR: Dictionary streaming API: %s\n", ZSTD_getErrorName(ret));
+cleanup:
+    ZSTD_freeDCtx(dctx);
+    free(decoded);
+    return result;
+}
+
+static int testLegacyDictionaries(void)
+{
+    struct {
+        unsigned version;
+        const char* data;
+        size_t size;
+    } const frames[] = {
+#if (ZSTD_LEGACY_SUPPORT <= 1)
+        { 1, FRAME_V01, FRAME_V01_SIZE },
+#endif
+#if (ZSTD_LEGACY_SUPPORT <= 2)
+        { 2, FRAME_V02, FRAME_V02_SIZE },
+#endif
+#if (ZSTD_LEGACY_SUPPORT <= 3)
+        { 3, FRAME_V03, FRAME_V03_SIZE },
+#endif
+#if (ZSTD_LEGACY_SUPPORT <= 4)
+        { 4, FRAME_V04, FRAME_V04_SIZE },
+#endif
+#if (ZSTD_LEGACY_SUPPORT <= 5)
+        { 5, FRAME_V05, FRAME_V05_SIZE },
+#endif
+#if (ZSTD_LEGACY_SUPPORT <= 6)
+        { 6, FRAME_V06, FRAME_V06_SIZE },
+#endif
+#if (ZSTD_LEGACY_SUPPORT <= 7)
+        { 7, FRAME_V07, FRAME_V07_SIZE },
+#endif
+        { 8, FRAME_V08, FRAME_V08_SIZE }
+    };
+    size_t f;
+
+    for (f = 0; f < sizeof(frames) / sizeof(frames[0]); ++f) {
+        unsigned pattern;
+        for (pattern = 0; pattern < 2; ++pattern) {
+            unsigned char dictContent[8] = { 0x37, 0xA4, 0x30, 0xEC, 0, 0, 0, 0 };
+            size_t maxSize = sizeof(dictContent);
+            size_t dictSize;
+            if (pattern == 0) {
+                memset(dictContent, 'A', sizeof(dictContent));
+            } else {
+                /* v0.5 and v0.6 have a four-byte magic, while v0.7
+                 * also has a four-byte dictionary ID. Every truncated
+                 * header must be treated as raw dictionary content.
+                 * For v0.5 and v0.6, sizes 4 to 8 keep the magic but
+                 * truncate the entropy tables, which must not be read
+                 * past their end. v0.7 stops at 7: from 8 bytes, its
+                 * magic is parsed as a zstd dictionary. */
+                if (frames[f].version < 5 || frames[f].version > 7) continue;
+                dictContent[0] = (unsigned char)(0x30 + frames[f].version);
+                maxSize = frames[f].version == 7 ? 7 : 8;
+            }
+            for (dictSize = 0; dictSize <= maxSize; ++dictSize) {
+                void* const dict = malloc(dictSize == 0 ? 1 : dictSize);
+                int failed;
+                if (dict == NULL) {
+                    DISPLAY("ERROR: Not enough memory!\n");
+                    return 1;
+                }
+                memcpy(dict, dictContent, dictSize);
+                failed = testDictionaryAPI(frames[f].version,
+                                           frames[f].data, frames[f].size,
+                                           dict, dictSize);
+                free(dict);
+                if (failed) {
+                    DISPLAY("ERROR: v0.%u dictionary of size %u, pattern %u\n",
+                            frames[f].version, (unsigned)dictSize, pattern);
+                    return 1;
+                }
+            }
+        }
+    }
+    DISPLAY("Legacy dictionary API OK\n");
+    return 0;
+}
+
+#if (ZSTD_LEGACY_SUPPORT <= 7)
+static int testMultipleDDicts(void)
+{
+    size_t const blockSize = strlen(BLOCK);
+    char* const output = malloc(2 * blockSize);
+    ZSTD_DCtx* const dctx = ZSTD_createDCtx();
+    ZSTD_DDict* const empty = ZSTD_createDDict(NULL, 0);
+    char frames[FRAME_V08_SIZE + FRAME_V07_SIZE];
+    int result = 1;
+    size_t ret;
+
+    do {
+        ZSTD_inBuffer input = {FRAME_V07, FRAME_V07_SIZE, 0};
+        ZSTD_outBuffer out = {output, 2 * blockSize, 0};
+        if (output == NULL || dctx == NULL || empty == NULL) break;
+        if (ZSTD_isError(ZSTD_DCtx_setParameter(dctx, ZSTD_d_refMultipleDDicts, 1))) break;
+        if (ZSTD_isError(ZSTD_DCtx_refDDict(dctx, empty))) break;
+
+        ret = ZSTD_decompressDCtx(dctx, output, 2 * blockSize, FRAME_V07, FRAME_V07_SIZE);
+        if (ZSTD_getErrorCode(ret) != ZSTD_error_parameter_unsupported) break;
+        if (ZSTD_isError(ZSTD_DCtx_reset(dctx, ZSTD_reset_session_only))) break;
+        ret = ZSTD_decompressStream(dctx, &out, &input);
+        if (ZSTD_getErrorCode(ret) != ZSTD_error_parameter_unsupported) break;
+        if (ZSTD_isError(ZSTD_DCtx_reset(dctx, ZSTD_reset_session_only))) break;
+
+        /* A preceding modern frame must not supply an implicit dictionary to
+         * the legacy frame. Explicit dictionaries remain supported. */
+        memcpy(frames, FRAME_V08, FRAME_V08_SIZE);
+        memcpy(frames + FRAME_V08_SIZE, FRAME_V07, FRAME_V07_SIZE);
+        ret = ZSTD_decompressDCtx(dctx, output, 2 * blockSize, frames, sizeof(frames));
+        if (ZSTD_getErrorCode(ret) != ZSTD_error_parameter_unsupported) break;
+        ret = ZSTD_decompress_usingDDict(dctx, output, 2 * blockSize,
+                                         frames, sizeof(frames), empty);
+        if (ret != 2 * blockSize || memcmp(output, BLOCK, blockSize) != 0
+            || memcmp(output + blockSize, BLOCK, blockSize) != 0) break;
+        ret = ZSTD_decompress_usingDict(dctx, output, 2 * blockSize,
+                                        FRAME_V07, FRAME_V07_SIZE, NULL, 0);
+        if (ret != blockSize || memcmp(output, BLOCK, blockSize) != 0) break;
+        result = 0;
+    } while (0);
+
+    free(output);
+    ZSTD_freeDCtx(dctx);
+    ZSTD_freeDDict(empty);
+    DISPLAY("Multi-DDict legacy boundary %s\n", result ? "FAILED" : "OK");
+    return result;
+}
+#endif
+
 int main(void)
 {
     size_t const blockSize = strlen(BLOCK);
@@ -298,6 +488,10 @@ int main(void)
          * are what the streaming subset must decode to */
         if (testStreamingAPI(streamable, streamableSize,
                              expected, blockSize * nbStreamFrames)) break;
+        if (testLegacyDictionaries()) break;
+#if (ZSTD_LEGACY_SUPPORT <= 7)
+        if (testMultipleDDicts()) break;
+#endif
         result = 0;
     } while (0);
 
diff --git a/tests/playTests.sh b/tests/playTests.sh
index 44bc82b5610..3ce5849b703 100755
--- a/tests/playTests.sh
+++ b/tests/playTests.sh
@@ -112,6 +112,15 @@ case "$OS" in
     ;;
 esac
 
+# Native Windows builds (MSVC, MinGW) define _WIN32; Cygwin builds don't
+isNativeWindows=false
+if [ "$isWindows" = true ]; then
+  case "$UNAME" in
+    CYGWIN*) ;;
+    *) isNativeWindows=true ;;
+  esac
+fi
+
 case "$UNAME" in
   Darwin) MD5SUM="md5 -r" ;;
   NetBSD) MD5SUM="md5 -n" ;;
@@ -443,6 +452,34 @@ mkdir -p tmpEmptyDir
 zstd -r tmpEmptyDir
 rm -rf tmpEmptyDir
 
+println "test : directory links are not followed by -r"
+# remove leftovers of a previous failed run
+if [ "$isWindows" = true ]; then cmd //c "rmdir tmpLinkDir\tmpLink" 2> /dev/null || :; fi
+rm -rf tmpLinkDir tmpOutside tmpStdErr
+mkdir -p tmpLinkDir tmpOutside
+echo "inner" > tmpLinkDir/tmpInner
+echo "victim" > tmpOutside/tmpVictim
+if [ "$isWindows" = true ]; then
+  # junctions don't require the privileges that symlinks do
+  (cd tmpLinkDir && cmd //c "mklink /J tmpLink ..\tmpOutside")
+else
+  ln -s ../tmpOutside tmpLinkDir/tmpLink
+fi
+zstd -r --rm tmpLinkDir 2> tmpStdErr
+# remove the link before asserting, so a failing test never leaves a live
+# link behind for the later "rm -rf tmp*" cleanups to encounter
+if [ "$isWindows" = true ]; then
+  cmd //c "rmdir tmpLinkDir\tmpLink"
+else
+  rm -f tmpLinkDir/tmpLink
+fi
+$GREP "symbolic link" tmpStdErr
+test -f tmpLinkDir/tmpInner.zst # file inside the tree was compressed
+test ! -f tmpLinkDir/tmpInner # ... and removed (--rm)
+test -f tmpOutside/tmpVictim # file outside the tree was left untouched
+test ! -f tmpOutside/tmpVictim.zst # ... not compressed through the link
+rm -rf tmpLinkDir tmpOutside tmpStdErr
+
 
 println "\n===>  file removal"
 zstd -f --rm tmp
@@ -800,6 +837,25 @@ zstd -f --filelist=tmp_symLink
 test -f tmp2.zst
 test -f tmp1.zst
 
+println "test : file list with CR at the end of lines, --filelist=FILE"
+# The list is read in binary mode. Native Windows builds drop the CR of CRLF line endings;
+# other builds keep it, since CR is a valid filename character there.
+rm -f *.zst
+if [ "$isNativeWindows" = true ]; then
+    printf 'tmp1\r\ntmp2\r\n' > tmp_crList
+    zstd -f --filelist=tmp_crList
+    test -f tmp1.zst
+    test -f tmp2.zst
+else
+    CR=$(printf '\r')
+    echo "CR" > "tmpCR$CR"
+    printf 'tmpCR\r\n' > tmp_crList
+    zstd -f --filelist=tmp_crList
+    test -f "tmpCR$CR.zst"
+    rm -f "tmpCR$CR" "tmpCR$CR.zst"
+fi
+rm -f tmp_crList
+
 println "test : compress multiple files reading them from multiple files, --filelist=FILE"
 rm -f *.zst
 println "Hello world!, file3" > tmp3
@@ -984,6 +1040,20 @@ zstd world.tmp hello.tmp -f
 test -f world.tmp.zst  # symbolic link should have been compressed with --force
 rm -f hello.tmp world.tmp world2.tmp hello.tmp.zst world.tmp.zst
 
+println "test : output is not written through a dangling symbolic link"
+rm -f hello.tmp hello.tmp.zst tmpLinkTarget
+println "hello world" > hello.tmp
+zstd -q --rm hello.tmp
+ln -s tmpLinkTarget hello.tmp
+zstd -q -d hello.tmp.zst && die "output should not be written through a dangling link"
+test ! -e tmpLinkTarget  # link target should not have been created
+zstd -q -d -f hello.tmp.zst  # --force replaces the link
+test ! -L hello.tmp
+test -f hello.tmp
+test ! -e tmpLinkTarget
+zstd -q -d hello.tmp.zst -o /dev/null  # existing non-regular files are still opened
+rm -f hello.tmp hello.tmp.zst tmpLinkTarget
+
 fi
 
 
@@ -1308,6 +1378,11 @@ zstd -f tmp1
 zstd -b -d -i0 tmp1.zst
 println "benchmark can fail - decompression on invalid data"
 zstd -b -d -i0 tmp1 && die "invalid .zst data => benchmark should have failed"
+println "benchmark decompression of several frames whose content is smaller than the frame"
+printf 'a' > tmpbd1 ; printf 'b' > tmpbd2 ; printf 'c' > tmpbd3
+zstd -qf tmpbd1 tmpbd2 tmpbd3
+zstd -b -d -i0 tmpbd1.zst tmpbd2.zst tmpbd3.zst
+rm -f tmpbd1 tmpbd2 tmpbd3 tmpbd1.zst tmpbd2.zst tmpbd3.zst
 
 GZIPMODE=1
 zstd --format=gzip -V || GZIPMODE=0
@@ -1423,6 +1498,14 @@ if [ $LZMAMODE -eq 1 ]; then
     cat tmp.xz tmp.lzma tmp.zst tmp.lzma tmp.xz tmp.zst | zstd -d -f -o tmp
     truncateLastByte tmp.xz | zstd -t > $INTOVOID && die "incomplete frame not detected !"
     truncateLastByte tmp.lzma | zstd -t > $INTOVOID && die "incomplete frame not detected !"
+    # --memory also limits xz/lzma decoding (level 3 : 4 MiB dictionary, needs 5 MB)
+    zstd -f -3 --format=xz tmp -o tmp3.xz
+    zstd -f -3 --format=lzma tmp -o tmp3.lzma
+    zstd -t --memory=1MB tmp3.xz 2>tmplog && die "--memory not enforced for xz"
+    $GREP "Use --memory=5MB" tmplog
+    zstd -t --memory=1MB tmp3.lzma 2>tmplog && die "--memory not enforced for lzma"
+    $GREP "Use --memory=5MB" tmplog
+    zstd -t --memory=5MB tmp3.xz tmp3.lzma
     rm -f tmp*
 else
     println "xz mode not supported"
@@ -1637,6 +1720,11 @@ rm -f $TEST_DATA_FILE
 rm -f $FULL_COMPRESSED_FILE
 rm -f $TRUNCATED_COMPRESSED_FILE
 
+println "test : detect skippable frame larger than file "
+printf '\120\052\115\030\370\377\377\377payload' > tmp_skippable.zst  # skippable magic, Frame_Size 0xFFFFFFF8
+zstd --list tmp_skippable.zst && die "-l must fail on truncated skippable frame"
+rm -f tmp_skippable.zst
+
 println "\n===>  zstd --list/-l errors when presented with stdin / no files"
 zstd -l && die "-l must fail on empty list of files"
 zstd -l - && die "-l does not work on stdin"
diff --git a/tests/test-zstd-versions.py b/tests/test-zstd-versions.py
index 57131c24349..9af75a7c0d4 100755
--- a/tests/test-zstd-versions.py
+++ b/tests/test-zstd-versions.py
@@ -15,6 +15,7 @@
 import glob
 import hashlib
 import os
+import platform
 import shutil
 import sys
 import subprocess
@@ -23,7 +24,12 @@
 repo_url = 'https://github.com/facebook/zstd.git'
 tmp_dir_name = 'tests/versionsTest'
 make_cmd = 'make'
-make_args = ['-j','CFLAGS=-O0']
+# Some old versions do not compile when the default target enables BMI2,
+# as GCC does on ubuntu-26.04 runners (x86-64-v3).
+cflags = '-O0'
+if platform.machine() in ('x86_64', 'AMD64'):
+    cflags += ' -march=x86-64'
+make_args = ['-j', 'CFLAGS=' + cflags]
 git_cmd = 'git'
 test_dat_src = 'README.md'
 test_dat = 'test_dat'
diff --git a/tests/zstreamtest.c b/tests/zstreamtest.c
index 88a20a3e270..bc486574721 100644
--- a/tests/zstreamtest.c
+++ b/tests/zstreamtest.c
@@ -1043,6 +1043,107 @@ static int basicUnitTests(U32 seed, double compressibility, int bigTests)
         }
         DISPLAYLEVEL(3, "OK \n");
 
+        DISPLAYLEVEL(3, "test%3i : ZSTD_c_stableInBuffer : a new session ignores input pending in the abandoned frame : ", testNb++);
+        {   ZSTD_CCtx* const cctx2 = ZSTD_createCCtx();
+            const char* const src2 = (const char*)CNBuffer + 1 MB;
+            size_t const srcSize2 = 50 KB;
+            int pendingCase;
+            assert(cctx2 != NULL);
+            /* 0: pending before initialization, 1: pending within a frame,
+             * 2: multi-threaded, 3: new session started by ZSTD_compress2() */
+            for (pendingCase = 0; pendingCase < 4; pendingCase++) {
+                ZSTD_inBuffer inBuf;
+                ZSTD_outBuffer outBuf;
+                size_t frameSize;
+                inBuf.src = CNBuffer;
+                inBuf.size = (pendingCase == 1) ? 200 KB : 1 KB;
+                inBuf.pos = 0;
+                outBuf.dst = compressedBuffer;
+                outBuf.size = compressedBufferSize;
+                outBuf.pos = 0;
+                CHECK_Z( ZSTD_CCtx_reset(cctx2, ZSTD_reset_session_and_parameters) );
+                CHECK_Z( ZSTD_CCtx_setParameter(cctx2, ZSTD_c_stableInBuffer, 1) );
+                if (pendingCase == 2) CHECK_Z( ZSTD_CCtx_setParameter(cctx2, ZSTD_c_nbWorkers, 2) );
+                CHECK_Z( ZSTD_compressStream2(cctx2, &outBuf, &inBuf, ZSTD_e_continue) );
+                if (pendingCase == 3) {
+                    frameSize = ZSTD_compress2(cctx2, compressedBuffer, compressedBufferSize, src2, srcSize2);
+                    CHECK_Z(frameSize);
+                } else {
+                    CHECK_Z( ZSTD_CCtx_reset(cctx2, ZSTD_reset_session_only) );
+                    inBuf.src = src2;
+                    inBuf.size = srcSize2;
+                    inBuf.pos = 0;
+                    outBuf.pos = 0;
+                    /* unknown size : keeps multi-threading enabled */
+                    if (pendingCase == 2) CHECK_Z( ZSTD_compressStream2(cctx2, &outBuf, &inBuf, ZSTD_e_flush) );
+                    CHECK(ZSTD_compressStream2(cctx2, &outBuf, &inBuf, ZSTD_e_end) != 0, "compression should be successful and fully flushed");
+                    frameSize = outBuf.pos;
+                }
+                {   size_t const decSize = ZSTD_decompress(decodedBuffer, CNBufferSize, compressedBuffer, frameSize);
+                    CHECK_Z(decSize);
+                    CHECK(decSize != srcSize2, "case %i : regenerated %u bytes, instead of %u", pendingCase, (unsigned)decSize, (unsigned)srcSize2);
+                    CHECK(memcmp(src2, decodedBuffer, srcSize2) != 0, "case %i : regenerated data different from original", pendingCase);
+            }   }
+            ZSTD_freeCCtx(cctx2);
+        }
+        DISPLAYLEVEL(3, "OK \n");
+
+        DISPLAYLEVEL(3, "test%3i : ZSTD_c_stableInBuffer : pending input is checked when compression starts : ", testNb++);
+        {   ZSTD_CCtx* const cctx2 = ZSTD_createCCtx();
+            int violation;
+            assert(cctx2 != NULL);
+            /* 0: different src pointer, 1: ZSTD_c_stableInBuffer disabled */
+            for (violation = 0; violation < 2; violation++) {
+                ZSTD_inBuffer inBuf;
+                ZSTD_outBuffer outBuf;
+                inBuf.src = CNBuffer;
+                inBuf.size = 1 KB;
+                inBuf.pos = 0;
+                outBuf.dst = compressedBuffer;
+                outBuf.size = compressedBufferSize;
+                outBuf.pos = 0;
+                CHECK_Z( ZSTD_CCtx_reset(cctx2, ZSTD_reset_session_and_parameters) );
+                CHECK_Z( ZSTD_CCtx_setParameter(cctx2, ZSTD_c_stableInBuffer, 1) );
+                CHECK_Z( ZSTD_compressStream2(cctx2, &outBuf, &inBuf, ZSTD_e_continue) );
+                if (violation == 0) {
+                    inBuf.src = (const char*)CNBuffer + 1 MB;
+                    inBuf.pos = 0;
+                } else {
+                    CHECK_Z( ZSTD_CCtx_setParameter(cctx2, ZSTD_c_stableInBuffer, 0) );
+                    inBuf.size = 2 KB;
+                }
+                {   size_t const ret = ZSTD_compressStream2(cctx2, &outBuf, &inBuf, ZSTD_e_end);
+                    CHECK(!ZSTD_isError(ret), "case %i : must error", violation);
+                    CHECK(!(ZSTD_getErrorCode(ret) == ZSTD_error_stabilityCondition_notRespected), "case %i : must be this error", violation);
+            }   }
+            ZSTD_freeCCtx(cctx2);
+        }
+        DISPLAYLEVEL(3, "OK \n");
+
+        DISPLAYLEVEL(3, "test%3i : ZSTD_c_stableInBuffer : ZSTD_endStream() on pending input, new context : ", testNb++);
+        {   ZSTD_CCtx* const cctx2 = ZSTD_createCCtx();
+            ZSTD_inBuffer inBuf;
+            ZSTD_outBuffer outBuf;
+            size_t const srcSize2 = 1 KB;
+            assert(cctx2 != NULL);
+            inBuf.src = CNBuffer;
+            inBuf.size = srcSize2;
+            inBuf.pos = 0;
+            outBuf.dst = compressedBuffer;
+            outBuf.size = compressedBufferSize;
+            outBuf.pos = 0;
+            CHECK_Z( ZSTD_CCtx_setParameter(cctx2, ZSTD_c_stableInBuffer, 1) );
+            CHECK_Z( ZSTD_compressStream2(cctx2, &outBuf, &inBuf, ZSTD_e_continue) );
+            CHECK(ZSTD_endStream(cctx2, &outBuf) != 0, "compression should be successful and fully flushed");
+            {   size_t const decSize = ZSTD_decompress(decodedBuffer, CNBufferSize, compressedBuffer, outBuf.pos);
+                CHECK_Z(decSize);
+                CHECK(decSize != srcSize2, "regenerated %u bytes, instead of %u", (unsigned)decSize, (unsigned)srcSize2);
+                CHECK(memcmp(CNBuffer, decodedBuffer, srcSize2) != 0, "regenerated data different from original");
+            }
+            ZSTD_freeCCtx(cctx2);
+        }
+        DISPLAYLEVEL(3, "OK \n");
+
         DISPLAYLEVEL(3, "test%3i : ZSTD_compressStream2() with ZSTD_c_stableOutBuffer only : ", testNb++);
         CHECK_Z(ZSTD_CCtx_reset(cctx, ZSTD_reset_session_and_parameters));
         CHECK_Z(ZSTD_CCtx_setParameter(cctx, ZSTD_c_checksumFlag, 1));
@@ -1406,6 +1507,39 @@ static int basicUnitTests(U32 seed, double compressibility, int bigTests)
         DISPLAYLEVEL(3, "OK (%s)\n", ZSTD_getErrorName(r));
     }
 
+    DISPLAYLEVEL(3, "test%3i : prefix is dropped by session reset after a rejected frame header : ", testNb++);
+    {   BYTE const badHeader[6] = { 0x28, 0xB5, 0x2F, 0xFD, 0x08, 0x00 };  /* reserved bit set */
+        CHECK_Z( ZSTD_DCtx_refPrefix(zd, dictionary.start, dictionary.filled) );
+        outBuff.dst = decodedBuffer;
+        outBuff.size = CNBufferSize;
+        outBuff.pos = 0;
+        inBuff.src = badHeader;
+        inBuff.size = sizeof(badHeader);
+        inBuff.pos = 0;
+        if (!ZSTD_isError(ZSTD_decompressStream(zd, &outBuff, &inBuff))) goto _output_error;
+        CHECK_Z( ZSTD_DCtx_reset(zd, ZSTD_reset_session_only) );
+        outBuff.pos = 0;
+        inBuff.src = compressedBuffer;
+        inBuff.size = cSize;
+        inBuff.pos = 0;
+        if (!ZSTD_isError(ZSTD_decompressStream(zd, &outBuff, &inBuff))) goto _output_error;  /* prefix must be gone */
+        CHECK_Z( ZSTD_DCtx_reset(zd, ZSTD_reset_session_only) );
+    }
+    DISPLAYLEVEL(3, "OK \n");
+
+    DISPLAYLEVEL(3, "test%3i : prefix survives session reset before any input : ", testNb++);
+    CHECK_Z( ZSTD_DCtx_refPrefix(zd, dictionary.start, dictionary.filled) );
+    CHECK_Z( ZSTD_DCtx_reset(zd, ZSTD_reset_session_only) );
+    outBuff.dst = decodedBuffer;
+    outBuff.size = CNBufferSize;
+    outBuff.pos = 0;
+    inBuff.src = compressedBuffer;
+    inBuff.size = cSize;
+    inBuff.pos = 0;
+    CHECK_Z( ZSTD_decompressStream(zd, &outBuff, &inBuff) );
+    if (outBuff.pos != CNBufferSize) goto _output_error;  /* must regenerate whole input */
+    DISPLAYLEVEL(3, "OK \n");
+
     DISPLAYLEVEL(3, "test%3i : compress again with ZSTD_compressStream2 : ", testNb++);
     outBuff.dst = compressedBuffer;
     outBuff.size = compressedBufferSize;
@@ -2512,6 +2646,49 @@ static int basicUnitTests(U32 seed, double compressibility, int bigTests)
     }
     DISPLAYLEVEL(3, "OK \n");
 
+#ifdef ZSTD_MULTITHREAD
+    /* Freeing or resetting the context must wait for the jobs still running,
+     * since they use its dictionary, pools and jobs table */
+    DISPLAYLEVEL(3, "test%3i : abandon a multi-threaded frame with a job in progress : ", testNb++);
+    {   size_t const jobSize = 1 MB;
+        const void* const dict = (const char*)CNBuffer + 2 * jobSize;
+        int scenario;
+        for (scenario = 0; scenario < 5; scenario++) {
+            ZSTD_CCtx* const cctx = ZSTD_createCCtx();
+            ZSTD_threadPool* const pool = (scenario == 1) ? ZSTD_createThreadPool(1) : NULL;
+            ZSTD_inBuffer in = { CNBuffer, (scenario == 2) ? 0 : jobSize, 0 };
+            ZSTD_outBuffer out = { compressedBuffer, compressedBufferSize, 0 };
+            CHECK(cctx == NULL, "ZSTD_createCCtx failed");
+            CHECK_Z( ZSTD_CCtx_setParameter(cctx, ZSTD_c_compressionLevel, 10) );
+            CHECK_Z( ZSTD_CCtx_setParameter(cctx, ZSTD_c_nbWorkers, 1) );
+            CHECK_Z( ZSTD_CCtx_setParameter(cctx, ZSTD_c_jobSize, (int)jobSize) );
+            if (pool != NULL) CHECK_Z( ZSTD_CCtx_refThreadPool(cctx, pool) );
+            CHECK_Z( ZSTD_CCtx_loadDictionary(cctx, dict, 1 MB) );   /* makes the first job slow */
+            CHECK_Z( ZSTD_compressStream2(cctx, &out, &in, ZSTD_e_continue) );   /* starts the first job, if any */
+            if (scenario == 2) {
+                /* empty frame : its only job is empty, and still runs when
+                 * ZSTD_e_end returns because the output buffer is full */
+                out.size = out.pos;
+                CHECK_Z( ZSTD_compressStream2(cctx, &out, &in, ZSTD_e_end) );
+            }
+            if (scenario == 4) {
+                /* the second job is prepared, and waits until the worker is free */
+                in.size = 2 * jobSize;
+                CHECK_Z( ZSTD_compressStream2(cctx, &out, &in, ZSTD_e_continue) );
+            }
+            if (scenario >= 3) {
+                /* abandon the frame, change the dictionary, and compress a new frame */
+                CHECK_Z( ZSTD_CCtx_reset(cctx, ZSTD_reset_session_only) );
+                CHECK_Z( ZSTD_CCtx_loadDictionary(cctx, dict, 512 KB) );
+                CHECK_Z( ZSTD_compress2(cctx, compressedBuffer, compressedBufferSize, CNBuffer, jobSize) );
+            }
+            ZSTD_freeCCtx(cctx);   /* scenarios 0 to 2 free it with a job in progress */
+            ZSTD_freeThreadPool(pool);
+        }
+    }
+    DISPLAYLEVEL(3, "OK \n");
+#endif
+
 _end:
     FUZ_freeDictionary(dictionary);
     ZSTD_freeCStream(zc);