Skip to main content
Vulnerability Database/CVE-2026-87825

CVE-2026-87825: zstd-jni Use-After-Free Vulnerability

CVE-2026-87825 is a use-after-free vulnerability in zstd-jni that allows attackers to trigger memory corruption or JVM crashes by closing dictionaries while still referenced. This article covers technical details, affected versions, impact analysis, and mitigation strategies.

Published:

CVE-2026-87825 Overview

CVE-2026-87825 is a use-after-free vulnerability [CWE-416] in zstd-jni versions 1.3.8-4 through 1.5.7-13. The library provides Java Native Interface (JNI) bindings for the Zstandard compression library. Streams and contexts hold a dictionary's shared lock only during the load call, allowing the dictionary to be closed while still referenced by an active stream or context. Subsequent read or write operations then access freed native memory. The result is either silent data corruption or Java Virtual Machine (JVM) crashes. The maintainer fixed the flaw in release v1.5.7-14.

Critical Impact

Attackers with local access to an application using zstd-jni can trigger memory corruption or JVM termination, affecting data integrity and availability of Java services that rely on the library.

Affected Products

  • zstd-jni versions 1.3.8-4 through 1.5.7-13
  • Java applications embedding vulnerable zstd-jni builds
  • Downstream frameworks bundling zstd-jni for Zstandard compression support

Discovery Timeline

  • 2026-09-09 - CVE-2026-87825 published to the National Vulnerability Database (NVD)
  • 2026-09-09 - Last updated in NVD database

Technical Details for CVE-2026-87825

Vulnerability Analysis

The defect resides in the lifetime tracking of shared dictionary objects used with Zstandard compression and decompression contexts. When an application associates a dictionary with a ZstdInputStream, ZstdOutputStream, ZstdCompressCtx, or ZstdDecompressCtx, the stream or context acquires the dictionary's shared lock only for the duration of the load call. After the load returns, the caller can invoke close() on the dictionary while native code still references its underlying buffer.

When the compressor or decompressor next processes data, the native side dereferences memory that has already been freed by ZSTD_freeCDict or ZSTD_freeDDict. Depending on allocator state, this produces silent decompression corruption or a JVM crash through a segmentation fault in native code.

Root Cause

The root cause is incorrect cross-object lifetime tracking in AutoCloseBase and the dictionary-loading paths of ZstdInputStream, ZstdInputStreamNoFinalizer, ZstdCompressCtx, and related classes. The shared lock was released immediately after loadDict, so the dictionary's reference count no longer reflected active consumers. The close() method proceeded to doClose() without waiting for outstanding stream or context references to drain.

Attack Vector

Exploitation requires local access to a process that loads attacker-influenced Java code or accepts input controlling dictionary lifecycle calls. An attacker closes the dictionary between setDict() and the next read(), write(), compress(), or decompress() call. The subsequent JNI invocation reaches freed native memory, corrupting decompressed output or aborting the JVM.

java
// Patch excerpt: AutoCloseBase.java - reference-count based close
if (sharedLock == SHARED_LOCK_CLOSED) {
    return;
}
if (sharedLock > 0) {
    if (SHARED_LOCK_UPDATER.compareAndSet(this, sharedLock, sharedLock - 1)) {
        if (sharedLock == 0) {
            doClose();
        }
    }
}
// Source: https://github.com/luben/zstd-jni/commit/a560131d7834598afd9cea6b7c107bc88e915936
java
// Patch excerpt: ZstdInputStreamNoFinalizer.java - reject setDict on closed stream
public synchronized @NotNull ZstdInputStreamNoFinalizer setDict(byte @NotNull [] dict) throws IOException {
    if (isClosed) {
        throw new IOException("Stream closed");
    }
    int size = Zstd.loadDictDecompress(stream, dict, dict.length);
    if (Zstd.isError(size)) {
        throw new ZstdIOException(size);
    }
}
// Source: https://github.com/luben/zstd-jni/commit/393d7311766abbc285b149302c0fe1f94b16d555
java
// Patch excerpt: ZstdCompressCtx.java - release prior dict lock only after successful load
if (Zstd.isError(result)) {
    throw new ZstdException(result);
}
if (compression_dict != null) {
    compression_dict.releaseSharedLock();
}
// keep a reference to the dictionary so it's not garbage collected
compression_dict = dict;
// Source: https://github.com/luben/zstd-jni/commit/a560131d7834598afd9cea6b7c107bc88e915936

Detection Methods for CVE-2026-87825

Indicators of Compromise

  • Unexpected JVM crashes (hs_err_pid*.log) with stack frames in libzstd-jni-*.so or native ZSTD_decompressStream / ZSTD_compressStream2 calls
  • Silent decompression output mismatches or checksum failures in pipelines that use dictionary-based Zstandard streams
  • SIGSEGV signals in JVM processes shortly after ZstdDictDecompress.close() or ZstdDictCompress.close() invocations

Detection Strategies

  • Inventory Java build manifests (Maven, Gradle, SBOM) for com.github.luben:zstd-jni versions between 1.3.8-4 and 1.5.7-13
  • Enable JVM fatal error logging (-XX:ErrorFile=) and centralize hs_err_pid files for analysis of native-frame crashes
  • Add software composition analysis (SCA) rules that flag any transitive dependency resolving to a vulnerable zstd-jni coordinate

Monitoring Recommendations

  • Alert on repeated JVM restarts of services performing Zstandard compression or decompression
  • Correlate application logs showing ZstdException or ZstdIOException with subsequent process termination
  • Track dependency updates in CI pipelines to ensure vulnerable zstd-jni artifacts are not reintroduced

How to Mitigate CVE-2026-87825

Immediate Actions Required

  • Upgrade zstd-jni to 1.5.7-14 or later across all Java applications and container images
  • Rebuild and redeploy downstream libraries that shade or repackage zstd-jni
  • Review application code that shares ZstdDictCompress or ZstdDictDecompress instances across threads to ensure lifetime is bounded by owning streams and contexts

Patch Information

The fix ships in GitHub Release v1.5.7-14. Three commits address the flaw: commit 0827ed0 migrates ZstdInputStream to the advanced dictionary-load API, commit 393d731 hardens dictionary load and release paths, and commit a560131 reworks AutoCloseBase to use reference-counted shared locks. See the GitHub Security Advisory GHSA-947w-pxjj-c7m9 and the VulnCheck advisory for further detail.

Workarounds

  • Avoid calling close() on ZstdDictCompress or ZstdDictDecompress while any stream or context still holds a reference
  • Restrict dictionary instances to a single owning stream and close the stream before the dictionary
  • Where feasible, decompress without dictionaries until the patched release is deployed
bash
# Maven: enforce patched zstd-jni version
mvn versions:use-dep-version -Dincludes=com.github.luben:zstd-jni -DdepVersion=1.5.7-14 -DforceVersion=true

# Gradle: pin the patched version in build.gradle
# implementation 'com.github.luben:zstd-jni:1.5.7-14'

# Verify resolved version in a build
mvn dependency:tree -Dincludes=com.github.luben:zstd-jni

Disclaimer: This content was generated using AI. While we strive for accuracy, please verify critical information with official sources.

Experience the Most Advanced Cybersecurity Platform

See how the world’s most intelligent, autonomous cybersecurity platform can protect your organization today and into the future.