CVE-2026-87877 Overview
CVE-2026-87877 is a use-after-free vulnerability [CWE-416] in the zstd-jni Java binding library for the Zstandard compression algorithm. Affected versions between 1.3.8-4 and 1.5.7-13 fail to validate the closed state in the setDict, setLongMax, setLevel, and setRefMultipleDDicts methods of stream classes. An attacker with local access who can invoke these methods on a closed stream can write through freed native pointers. The result is corruption of unrelated Java objects or a Java Virtual Machine (JVM) crash.
Critical Impact
Calls to setter methods on closed zstd-jni streams dereference freed native memory, enabling process memory corruption and JVM termination.
Affected Products
- zstd-jni versions 1.3.8-4 through 1.5.7-13
- Java applications embedding vulnerable zstd-jni releases
- Downstream libraries and frameworks that expose zstd-jni stream classes to untrusted callers
Discovery Timeline
- 2026-09-09 - CVE-2026-87877 published to the National Vulnerability Database (NVD)
- 2026-09-09 - Last updated in NVD database
Technical Details for CVE-2026-87877
Vulnerability Analysis
The zstd-jni library wraps the native Zstandard C library through the Java Native Interface (JNI). Stream classes such as ZstdInputStreamNoFinalizer and BaseZstdBufferDecompressingStreamNoFinalizer hold a native pointer in the stream field. When close() executes, the native context is freed, but the Java object remains reachable.
Setter methods including setDict, setLongMax, setLevel, and setRefMultipleDDicts did not check the isClosed flag before forwarding the native pointer to JNI helpers such as Zstd.loadDictDecompress. The native code then writes to the freed allocation. Because the JVM heap and native allocator may reuse the region, adjacent objects can be silently corrupted or the process can terminate with a segmentation fault.
Root Cause
The root cause is missing state validation in setter methods on stream classes. The isClosed (or closed) boolean was maintained by the class but never consulted by the affected setters, producing a classic use-after-free pattern where a freed native resource is reused through a still-live Java wrapper.
Attack Vector
Exploitation requires local access to a JVM process that exposes zstd-jni stream instances to caller-supplied invocations. An attacker who can call setDict or a related setter on a stream that was already closed triggers the write-after-free primitive. Impact ranges from denial of service via JVM crash to corruption of unrelated Java objects that could be leveraged for further compromise.
// Patch: guard setDict against closed streams
// Source: https://github.com/luben/zstd-jni/commit/393d7311766abbc285b149302c0fe1f94b16d555
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);
}
}
// Patch: retain dict reference and add closed/null guards in the base class
// Source: https://github.com/luben/zstd-jni/commit/f38f9a1563113d96d0fc38baee543f7457dd8a8e
public abstract class BaseZstdBufferDecompressingStreamNoFinalizer implements Closeable {
protected long stream;
protected @Nullable ByteBuffer source;
protected boolean closed = false;
private boolean finishedFrame = false;
private boolean streamEnd = false;
// Keep reference to the active dict so it is not garbage collected
private @Nullable ZstdDictDecompress active_dict;
}
Detection Methods for CVE-2026-87877
Indicators of Compromise
- Unexpected JVM crashes with hs_err_pid*.log files referencing the libzstd-jni native library or Zstd_* frames.
- Segmentation faults or SIGSEGV signals originating from threads that recently closed a ZstdInputStream or decompressing stream.
- Application logs showing setter calls (setDict, setLevel, setLongMax, setRefMultipleDDicts) issued after a stream close().
Detection Strategies
- Inventory Java dependencies using Software Composition Analysis (SCA) tools to identify com.github.luben:zstd-jni versions between 1.3.8-4 and 1.5.7-13.
- Static analysis rules that flag invocations of the affected setters on stream instances after close() or outside try-with-resources scopes.
- Runtime monitoring for native crash dumps and correlate them with recent library upgrades or workload changes.
Monitoring Recommendations
- Alert on repeated JVM process terminations in workloads that ingest attacker-influenced compressed data.
- Track dependency drift for zstd-jni across build pipelines and container images.
- Forward hs_err crash reports and application stack traces to centralized logging for rapid triage.
How to Mitigate CVE-2026-87877
Immediate Actions Required
- Upgrade zstd-jni to version 1.5.7-14 or later across all Java applications and container images.
- Audit code paths that expose zstd-jni stream instances to ensure setters are never invoked after close().
- Rebuild and redeploy downstream artifacts that shade or embed zstd-jni.
Patch Information
The fixes are delivered in GitHub Release v1.5.7-14. The relevant commits are 0827ed0, 393d731, and f38f9a1. See GitHub Security Advisory GHSA-2jw3-mg7f-vw4q and the VulnCheck Advisory for zstd-jni for full details.
Workarounds
- Wrap all zstd-jni stream usage in try-with-resources blocks so setters cannot execute after close().
- Restrict access to APIs that forward untrusted input to zstd-jni setter methods until the patched version is deployed.
- Add application-level guards that null out or discard references to zstd-jni stream objects immediately after closing them.
# Maven: pin the fixed version
mvn versions:use-dep-version -Dincludes=com.github.luben:zstd-jni -DdepVersion=1.5.7-14 -DforceVersion=true
# Gradle: pin in build.gradle
# implementation 'com.github.luben:zstd-jni:1.5.7-14'
# Verify resolved version
mvn dependency:tree | grep zstd-jni
Disclaimer: This content was generated using AI. While we strive for accuracy, please verify critical information with official sources.