EEF-CVE-2026-119434

Source
https://cna.erlef.org/osv/EEF-CVE-2026-119434.html
Import Source
https://cna.erlef.org/osv/EEF-CVE-2026-119434.json
JSON Data
https://api.osv.dev/v1/vulns/EEF-CVE-2026-119434
Aliases
  • CVE-2026-119434
Published
2026-11-05T20:15:56Z
Modified
2026-11-05T20:30:03Z
Severity
  • 6.9 (Medium) CVSS_V4 - CVSS:4.0/AV:L/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N CVSS Calculator
Summary
Decompression bomb in ezstd:decompress_using_ddict/2 allows BEAM memory exhaustion via unbounded output buffer
Details

Summary

Improper Handling of Highly Compressed Data (Data Amplification) vulnerability in silviucpp ezstd allows an attacker who supplies a zstd frame to ezstd:decompress_using_ddict/2 to exhaust memory and terminate the Erlang VM via a small frame that declares a very large content size.

zstd_nif_decompress_using_ddict in c_src/ezstd_nif.cc reads the frame's declared content size with ZSTD_getFrameContentSize and passes it directly to enif_make_new_binary, without the 1 GiB MAX_BUFFER_SIZE check that ezstd:decompress/1 applies before allocating. A valid 65,550-byte frame of RLE blocks declaring 2 GiB is rejected by ezstd:decompress/1 but is fully allocated and decompressed by the dictionary path. The allocation happens inside a NIF, so when it exceeds the memory available to the VM the operating system kills the whole node rather than the calling process.

This issue affects ezstd: from 1.0.5 before 1.2.5.

Details

1. Declared size is attacker controlled. A zstd frame header carries an optional Frame Content Size field. zstd_nif_decompress_using_ddict in c_src/ezstd_nif.cc reads it with ZSTD_getFrameContentSize and rejects only the two sentinel values ZSTD_CONTENTSIZE_UNKNOWN and ZSTD_CONTENTSIZE_ERROR.

2. Unbounded allocation. The size is passed directly to enif_make_new_binary, so a frame declaring 2 GiB makes the NIF request a 2 GiB binary. The sibling zstd_nif_decompress rejects any declared size above MAX_BUFFER_SIZE (1 GiB) before allocating; the dictionary path omitted that check from the commit that introduced it.

3. Amplification. A frame built from RLE blocks, each encoding 128 KiB of a repeated byte in four bytes, declares and genuinely decompresses to 2 GiB from 65,550 bytes of input, a ratio of about 32,760:1. ZSTD_decompress_usingDDict then fills the buffer. Because this happens inside a NIF, exceeding the memory available to the VM results in the operating system killing the whole BEAM process rather than an Erlang-level exception.

Proof of concept

  1. Build ezstd 1.2.4 and start a VM under an external memory limit, for example a container limited to 256 MiB.
  2. Create any decompression dictionary with ezstd:create_ddict/1; its content does not matter because the frame carries no dictionary ID.
  3. Construct a zstd frame: the magic number, a frame header descriptor selecting an 8-byte content size, a window descriptor, a little-endian content size of 2 GiB, then 16,383 RLE blocks of 128 KiB each (block header 0x02 0x00 0x10 followed by one payload byte) and a final RLE block with the last-block bit set. The frame is 65,550 bytes.
  4. Call ezstd:decompress/1 on the frame: it returns {error, <<"frame too large or invalid">>}.
  5. Call ezstd:decompress_using_ddict/2 on the same frame with the dictionary: the VM is killed by the operating system (exit status 137).
  6. Repeat on 1.2.5: the call returns {error, <<"frame too large or invalid">>} and the VM continues.

Impact

An attacker who can submit a zstd frame to a service that decompresses it with a fixed dictionary can crash the entire Erlang VM with a single request of about 65 KiB. Every application and process hosted in that VM is taken down, not only the one that called the decompressor.

Workarounds

Replace calls to ezstd:decompress_using_ddict/2 with the streaming API: create a context with ezstd:create_decompression_context/1 using a small output buffer, attach the dictionary with ezstd:select_ddict/2, and decompress with ezstd:decompress_streaming/2. The streaming path decompresses into the caller-sized buffer and gives up with {error, decompressor_stuck} after 1000 chunks, so a 64 KiB buffer caps a single call at 64 MiB of output. Treat that error as a rejected input. Legitimate frames whose output exceeds 1000 times the buffer size fail the same way, so size the buffer for the largest expected payload.

Database specific
{
    "capec_ids": [
        "CAPEC-130"
    ],
    "cpe_ids": [
        "cpe:2.3:a:silviucpp:ezstd:*:*:*:*:*:*:*:*"
    ],
    "cwe_ids": [
        "CWE-409"
    ]
}
References
Credits
    • Oz Kaiser - FINDER
    • Oz Kaiser - REPORTER
    • Silviu Caragea - REMEDIATION_DEVELOPER
    • Jonatan Männchen / EEF - COORDINATOR

Affected packages

Hex / ezstd

Package

Name
ezstd
Purl
pkg:hex/ezstd

Affected ranges

Type
SEMVER
Events
Introduced
1.0.5
Fixed
1.2.5

Affected versions

1.*
1.0.5
1.0.6
1.0.7
1.0.8
1.0.9
1.1.0
1.2.0
1.2.1
1.2.2
1.2.3
1.2.4

Database specific

source
"https://cna.erlef.org/osv/EEF-CVE-2026-119434.json"

Git / github.com/silviucpp/ezstd

Affected ranges

Type
GIT
Repo
https://github.com/silviucpp/ezstd
Events

Database specific

source
"https://cna.erlef.org/osv/EEF-CVE-2026-119434.json"