{"containers":{"cna":{"affected":[{"collectionURL":"https://repo.hex.pm","cpes":["cpe:2.3:a:silviucpp:ezstd:*:*:*:*:*:*:*:*"],"defaultStatus":"unaffected","modules":["ezstd_nif","ezstd"],"packageName":"ezstd","packageURL":"pkg:hex/ezstd","product":"ezstd","programFiles":["c_src/ezstd_nif.cc","src/ezstd_nif.erl","src/ezstd.erl"],"programRoutines":[{"name":"zstd_nif_decompress_using_ddict"},{"name":"ezstd_nif:decompress_using_ddict/2"},{"name":"ezstd:decompress_using_ddict/2"}],"repo":"https://github.com/silviucpp/ezstd","vendor":"silviucpp","versions":[{"lessThan":"1.2.5","status":"affected","version":"1.0.5","versionType":"semver"}]},{"collectionURL":"https://github.com","cpes":["cpe:2.3:a:silviucpp:ezstd:*:*:*:*:*:*:*:*"],"defaultStatus":"unaffected","modules":["ezstd_nif","ezstd"],"packageName":"silviucpp/ezstd","packageURL":"pkg:github/silviucpp/ezstd","product":"ezstd","programFiles":["c_src/ezstd_nif.cc","src/ezstd_nif.erl","src/ezstd.erl"],"programRoutines":[{"name":"zstd_nif_decompress_using_ddict"},{"name":"ezstd_nif:decompress_using_ddict/2"},{"name":"ezstd:decompress_using_ddict/2"}],"repo":"https://github.com/silviucpp/ezstd","vendor":"silviucpp","versions":[{"lessThan":"e34f4a4f6dc18d990d1cece2d5fceef8e934a195","status":"affected","version":"6730128b1d35eca02a2028671421471ff651f423","versionType":"git"}]}],"cpeApplicability":[{"nodes":[{"cpeMatch":[{"criteria":"cpe:2.3:a:silviucpp:ezstd:*:*:*:*:*:*:*:*","versionEndExcluding":"1.2.5","versionStartIncluding":"1.0.5","vulnerable":true}],"negate":false,"operator":"OR"}],"operator":"AND"}],"credits":[{"lang":"en","type":"finder","value":"Oz Kaiser"},{"lang":"en","type":"reporter","value":"Oz Kaiser"},{"lang":"en","type":"remediation developer","value":"Silviu Caragea"},{"lang":"en","type":"coordinator","value":"Jonatan Männchen / EEF"}],"dateAssigned":"2026-11-04T17:02:50.000Z","descriptions":[{"lang":"en","supportingMedia":[{"base64":false,"type":"text/html","value":"<p>Improper Handling of Highly Compressed Data (Data Amplification) vulnerability in silviucpp ezstd allows an attacker who supplies a zstd frame to <code>ezstd:decompress_using_ddict/2</code> to exhaust memory and terminate the Erlang VM via a small frame that declares a very large content size.</p>\n<p><code>zstd_nif_decompress_using_ddict</code> in <code>c_src/ezstd_nif.cc</code> reads the frame's declared content size with <code>ZSTD_getFrameContentSize</code> and passes it directly to <code>enif_make_new_binary</code>, without the 1 GiB <code>MAX_BUFFER_SIZE</code> check that <code>ezstd:decompress/1</code> applies before allocating. A valid 65,550-byte frame of RLE blocks declaring 2 GiB is rejected by <code>ezstd:decompress/1</code> 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.</p>\n<p>This issue affects ezstd: from 1.0.5 before 1.2.5.</p>"},{"base64":false,"type":"text/markdown","value":"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.\n\n`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.\n\nThis issue affects ezstd: from 1.0.5 before 1.2.5."}],"value":"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.\n\nzstd_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.\n\nThis issue affects ezstd: from 1.0.5 before 1.2.5."}],"impacts":[{"capecId":"CAPEC-130","descriptions":[{"lang":"en","supportingMedia":[{"base64":false,"type":"text/html","value":"<p>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.</p>"},{"base64":false,"type":"text/markdown","value":"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."}],"value":"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."}]}],"metrics":[{"cvssV4_0":{"Automatable":"NOT_DEFINED","Recovery":"NOT_DEFINED","Safety":"NOT_DEFINED","attackComplexity":"LOW","attackRequirements":"NONE","attackVector":"LOCAL","baseScore":6.9,"baseSeverity":"MEDIUM","privilegesRequired":"NONE","providerUrgency":"NOT_DEFINED","subAvailabilityImpact":"NONE","subConfidentialityImpact":"NONE","subIntegrityImpact":"NONE","userInteraction":"NONE","valueDensity":"NOT_DEFINED","vectorString":"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","version":"4.0","vulnAvailabilityImpact":"HIGH","vulnConfidentialityImpact":"NONE","vulnIntegrityImpact":"NONE","vulnerabilityResponseEffort":"NOT_DEFINED"},"format":"CVSS","scenarios":[{"lang":"en","value":"GENERAL"}]}],"problemTypes":[{"descriptions":[{"cweId":"CWE-409","description":"CWE-409 Improper Handling of Highly Compressed Data (Data Amplification)","lang":"en","type":"CWE"}]}],"providerMetadata":{"dateUpdated":"2026-11-05T20:15:56.311Z","orgId":"6b3ad84c-e1a6-4bf7-a703-f496b71e49db","shortName":"EEF"},"references":[{"name":"ezstd v1.2.5 release notes","tags":["release-notes","related"],"url":"https://github.com/silviucpp/ezstd/releases/tag/v1.2.5"},{"name":"EEF CNA record for CVE-2026-119434","tags":["related","third-party-advisory"],"url":"https://cna.erlef.org/cves/CVE-2026-119434.html"},{"name":"OSV record EEF-CVE-2026-119434","tags":["related"],"url":"https://osv.dev/vulnerability/EEF-CVE-2026-119434"},{"name":"Introducing commit 6730128 in silviucpp/ezstd","tags":["related"],"url":"https://github.com/silviucpp/ezstd/commit/6730128b1d35eca02a2028671421471ff651f423"},{"name":"Fix commit e34f4a4 in silviucpp/ezstd","tags":["patch"],"url":"https://github.com/silviucpp/ezstd/commit/e34f4a4f6dc18d990d1cece2d5fceef8e934a195"}],"source":{"discovery":"EXTERNAL"},"title":"Decompression bomb in ezstd:decompress_using_ddict/2 allows BEAM memory exhaustion via unbounded output buffer","workarounds":[{"lang":"en","supportingMedia":[{"base64":false,"type":"text/html","value":"<p>Replace calls to <code>ezstd:decompress_using_ddict/2</code> with the streaming API: create a context with <code>ezstd:create_decompression_context/1</code> using a small output buffer, attach the dictionary with <code>ezstd:select_ddict/2</code>, and decompress with <code>ezstd:decompress_streaming/2</code>. The streaming path decompresses into the caller-sized buffer and gives up with <code>{error, decompressor_stuck}</code> 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.</p>"},{"base64":false,"type":"text/markdown","value":"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."}],"value":"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."}],"x_proofOfConcept":[{"lang":"en","supportingMedia":[{"base64":false,"type":"text/html","value":"<ol>\n<li>Build ezstd 1.2.4 and start a VM under an external memory limit, for example a container limited to 256 MiB.</li>\n<li>Create any decompression dictionary with <code>ezstd:create_ddict/1</code>; its content does not matter because the frame carries no dictionary ID.</li>\n<li>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 <code>0x02 0x00 0x10</code> followed by one payload byte) and a final RLE block with the last-block bit set. The frame is 65,550 bytes.</li>\n<li>Call <code>ezstd:decompress/1</code> on the frame: it returns <code>{error, &lt;&lt;\"frame too large or invalid\"&gt;&gt;}</code>.</li>\n<li>Call <code>ezstd:decompress_using_ddict/2</code> on the same frame with the dictionary: the VM is killed by the operating system (exit status 137).</li>\n<li>Repeat on 1.2.5: the call returns <code>{error, &lt;&lt;\"frame too large or invalid\"&gt;&gt;}</code> and the VM continues.</li>\n</ol>"},{"base64":false,"type":"text/markdown","value":"1. Build ezstd 1.2.4 and start a VM under an external memory limit, for example a container limited to 256 MiB.\n2. Create any decompression dictionary with `ezstd:create_ddict/1`; its content does not matter because the frame carries no dictionary ID.\n3. 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.\n4. Call `ezstd:decompress/1` on the frame: it returns `{error, <<\"frame too large or invalid\">>}`.\n5. Call `ezstd:decompress_using_ddict/2` on the same frame with the dictionary: the VM is killed by the operating system (exit status 137).\n6. Repeat on 1.2.5: the call returns `{error, <<\"frame too large or invalid\">>}` and the VM continues."}],"value":"* Build ezstd 1.2.4 and start a VM under an external memory limit, for example a container limited to 256 MiB.\n* Create any decompression dictionary with ezstd:create_ddict/1; its content does not matter because the frame carries no dictionary ID.\n* 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.\n* Call ezstd:decompress/1 on the frame: it returns {error, <<\"frame too large or invalid\">>}.\n* Call ezstd:decompress_using_ddict/2 on the same frame with the dictionary: the VM is killed by the operating system (exit status 137).\n* Repeat on 1.2.5: the call returns {error, <<\"frame too large or invalid\">>} and the VM continues."}],"x_technicalAnalysis":[{"lang":"en","supportingMedia":[{"base64":false,"type":"text/html","value":"<p><strong>1. Declared size is attacker controlled.</strong> A zstd frame header carries an optional Frame Content Size field. <code>zstd_nif_decompress_using_ddict</code> in <code>c_src/ezstd_nif.cc</code> reads it with <code>ZSTD_getFrameContentSize</code> and rejects only the two sentinel values <code>ZSTD_CONTENTSIZE_UNKNOWN</code> and <code>ZSTD_CONTENTSIZE_ERROR</code>.</p>\n<p><strong>2. Unbounded allocation.</strong> The size is passed directly to <code>enif_make_new_binary</code>, so a frame declaring 2 GiB makes the NIF request a 2 GiB binary. The sibling <code>zstd_nif_decompress</code> rejects any declared size above <code>MAX_BUFFER_SIZE</code> (1 GiB) before allocating; the dictionary path omitted that check from the commit that introduced it.</p>\n<p><strong>3. Amplification.</strong> 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. <code>ZSTD_decompress_usingDDict</code> 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.</p>"},{"base64":false,"type":"text/markdown","value":"**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`.\n\n**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.\n\n**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."}],"value":"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.\n\n2. 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.\n\n3. 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."}]}},"cveMetadata":{"assignerOrgId":"6b3ad84c-e1a6-4bf7-a703-f496b71e49db","assignerShortName":"EEF","cveId":"CVE-2026-119434","datePublished":"2026-11-05T20:15:56.311Z","dateReserved":"2026-10-30T10:30:01.015Z","dateUpdated":"2026-11-05T20:15:56.311Z","state":"PUBLISHED"},"dataType":"CVE_RECORD","dataVersion":"5.2"}