# Apex Found a 15-Year-Old, High-Severity Bug in XZ Utils > A failed memory allocation left XZ Utils remembering a buffer that no longer existed. Reusing the decoder could crash the application. Author: Cantina Security Research Published: October 5, 2026 Topics: XZ Utils, liblzma, memory safety, decoder reuse, C, Apex Canonical URL: https://www.cantina.security/blog/xz-utils-crash-bug [Apex](https://www.cantina.security/apex), Cantina's autonomous OffSec agent, discovered a **High-severity memory safety vulnerability** in the `liblzma` compression library within XZ Utils that could cause an invalid write and crash an application when it reused a decoder after a memory allocation failure under the conditions described below. Cantina reported the finding to the XZ Utils maintainers, who classified it as High in their [upstream advisory](https://github.com/tukaani-project/xz/security/advisories/GHSA-5qpq-xqfv-j9pg). The vulnerability was **fixed in XZ Utils 5.8.4**, released on September 9, 2026, and affects selected decoding paths for `.lzma`, `.lz` and MicroLZMA formats while leaving `.xz` decoding and the raw decoder APIs unaffected. Upgrade to version 5.8.4 or install a distribution package that includes the upstream fix described in the [release notes](https://github.com/tukaani-project/xz/releases/tag/v5.8.4) and the [affected interfaces](https://github.com/tukaani-project/xz/security/advisories/GHSA-5qpq-xqfv-j9pg). ## At a glance - **One finding:** a failed allocation could leave a reusable decoder in an inconsistent state and cause an invalid write on a later attempt. - **Required sequence:** use the same stream and decoder type through a dictionary-allocation failure and then reuse them with the previous dictionary size. - **Format differences:** `.lzma` supplies its dictionary size in the input header while MicroLZMA obtains it from the application through the [MicroLZMA API](https://github.com/tukaani-project/xz/blob/d3e650e63c110e830fd5391e7f8b45df0b91d3da/src/liblzma/api/lzma/container.h#L952-L996). - **Demonstrated impact:** the recorded crash establishes an invalid write while the possibility of denial of service depends on how the application handles errors and reuses the decoder, with no remote code execution established by the public evidence. ## Affected decoder interfaces and patch availability The problem affects stable versions 5.0.0 through 5.8.3 and dates back to the release of 5.0.0 on October 23, 2010, almost 16 years before the fix. The affected interfaces depend on the version because the newer lzip and MicroLZMA APIs were not all included in 5.0.0. [Release history](https://tukaani.org/xz/old.html#_5_0_x), [5.4.0 API additions](https://github.com/tukaani-project/xz/blob/d3e650e63c110e830fd5391e7f8b45df0b91d3da/NEWS#L1481-L1567). | Public initializer or decoding path | Format | Status for this finding | | --- | --- | --- | | `lzma_alone_decoder()` | `.lzma` | Affected | | `lzma_lzip_decoder()` | `.lz` | Affected | | `lzma_auto_decoder()` | `.lzma` or `.lz` | Affected | | `lzma_microlzma_decoder()` | MicroLZMA | Affected | | `.xz` format decoders | `.xz` | Unaffected | | Raw decoder APIs | Raw decoding | Unaffected | **5.8.4 is the fixed release** and the v5.8, v5.6, v5.4 and v5.2 Git branches contain the fix, but upstream will not produce new 5.6.x, 5.4.x or 5.2.x releases. Check with the maintainer of an older distribution package to see whether it includes a backport because the base version alone does not determine its patch status. [Release and backport status](https://github.com/tukaani-project/xz/releases/tag/v5.8.4). The advisory is [GHSA-5qpq-xqfv-j9pg](https://github.com/tukaani-project/xz/security/advisories/GHSA-5qpq-xqfv-j9pg) and lists no CVE identifier or numerical CVSS score as of October 5, 2026, while the release notes state that a CVE identifier is pending. The following technical walkthrough describes how the fixes enable safe decoder initialization. ## What the decoder needs to remember Applications use XZ Utils' `liblzma` library by passing compressed input and output buffers through an `lzma_stream`, initializing a decoder and calling `lzma_code()` to process the data before calling `lzma_end()` to free its memory. The public API allows the stream to be reinitialized while the cleanup requirement discussed here concerns `liblzma`'s internal filter initialization. [Stream lifecycle](https://github.com/tukaani-project/xz/blob/d3e650e63c110e830fd5391e7f8b45df0b91d3da/src/liblzma/api/lzma/base.h#L486-L520) and [internal cleanup contract](https://github.com/tukaani-project/xz/commit/e5e63d50eac1b4357a5a7a0abd3c5f99a5c47881). LZ decompression uses a history buffer called a **dictionary** to keep bytes available for reuse when compressed data refers to output the decoder has produced. The shared LZ decoder tracks both the buffer's address and its capacity in the [dictionary implementation](https://github.com/tukaani-project/xz/blob/9fc6f5cd8774ebef8d4e030f7081fb6984c0dc3f/src/liblzma/lz/lz_decoder.h#L81-L101). ```c // Dictionary fields: src/liblzma/lz/lz_decoder.h:81-101 coder->dict.buf // Address of the dictionary buffer. coder->dict.size // Internal dictionary size; excludes trailing LZ_DICT_EXTRA bytes. ``` The size must describe an available allocation and excludes the trailing `LZ_DICT_EXTRA` bytes reserved for extra copying in the [dictionary layout](https://github.com/tukaani-project/xz/blob/d3e650e63c110e830fd5391e7f8b45df0b91d3da/src/liblzma/lz/lz_decoder.h#L63-L66). ## How a `.lzma` file reaches the allocation code We follow the `.lzma` path because the maintainer's regression tests include it, starting with `lzma_alone_decoder()` setting up a stream to read that format. Header parsing happens later when the application calls `lzma_code()` and execution reaches `alone_decode()`. The function `alone_decode()` reads the dictionary size from the file header and checks the configured memory limit before initializing the internal LZMA decoder through this path: ```text lzma_alone_decoder() prepares the stream lzma_code() processes input -> alone_decode() reads the .lzma header -> lzma_next_filter_init() -> lzma_lzma_decoder_init() -> lzma_lz_decoder_init() -> lz_decoder_reset() ``` The function `lzma_next_filter_init()` selects and initializes the next internal decoding stage (called a filter) while `lzma_lzma_decoder_init()` connects the LZMA decoder to the shared LZ implementation. That leads to `lzma_lz_decoder_init()`, which allocates the dictionary and is where the size becomes stale. [Header parsing and initialization](https://github.com/tukaani-project/xz/blob/9fc6f5cd8774ebef8d4e030f7081fb6984c0dc3f/src/liblzma/common/alone_decoder.c#L63-L165), [filter initializer](https://github.com/tukaani-project/xz/blob/e5e63d50eac1b4357a5a7a0abd3c5f99a5c47881/src/liblzma/common/common.c#L122-L130), [LZMA wrapper](https://github.com/tukaani-project/xz/blob/9fc6f5cd8774ebef8d4e030f7081fb6984c0dc3f/src/liblzma/lzma/lzma_decoder.c#L1185-L1195). ## The buffer was gone but its size remained This allocation logic comes from `src/liblzma/lz/lz_decoder.c` at lines 279 to 298 in the commit immediately before the fix, with shortened comments and added annotations: ```c // src/liblzma/lz/lz_decoder.c:279-298, before the fix if (coder->dict.size != alloc_size) { lzma_free(coder->dict.buf, allocator); // Release the old buffer. coder->dict.buf = lzma_alloc( alloc_size + LZ_DICT_EXTRA, allocator); if (coder->dict.buf == NULL) return LZMA_MEM_ERROR; // Old dict.size survives. coder->dict.size = alloc_size; // Reached only on success. } lz_decoder_reset(next->coder); ``` The function `lzma_free()` frees the current dictionary before `lzma_alloc()` requests a replacement, with a successful allocation recording the new capacity and resetting the dictionary for use. If the request fails the assignment sets `dict.buf` to `NULL` and the function returns without updating `dict.size`. [Vulnerable allocation logic](https://github.com/tukaani-project/xz/blob/9fc6f5cd8774ebef8d4e030f7081fb6984c0dc3f/src/liblzma/lz/lz_decoder.c#L279-L298). The resulting state is: ```text dict.buf = NULL dict.size = capacity recorded before the failed replacement ``` When the application reinitializes the stream using the same decoder function and provides a file requesting the original dictionary size, the comparison at the top of the allocation block finds a match and skips allocation before calling `lz_decoder_reset()`. The function `lz_decoder_reset()` resets both the dictionary's position and its history counters and at the same time writes a zero byte into the buffer. ```c // src/liblzma/lz/lz_decoder.c:53-62, before the fix static void lz_decoder_reset(lzma_coder *coder) { coder->dict.pos = LZ_DICT_INIT_POS; coder->dict.full = 0; coder->dict.buf[LZ_DICT_INIT_POS - 1] = '\0'; // Invalid write if dict.buf is NULL. coder->dict.has_wrapped = false; coder->dict.need_reset = false; return; } ``` The write uses a null buffer pointer when the decoder is reused even though the allocation failure was returned to the application during the previous attempt. [Reset function](https://github.com/tukaani-project/xz/blob/9fc6f5cd8774ebef8d4e030f7081fb6984c0dc3f/src/liblzma/lz/lz_decoder.c#L53-L62). ## Why the failed decoder was still available The caller must release the failed filter chain when internal filter initialization fails, but the affected format decoders returned the error without carrying out that cleanup. In `alone_decode()`, the original call was: ```c // src/liblzma/common/alone_decoder.c:155-156, before the fix return_if_error(lzma_next_filter_init(&coder->next, allocator, filters)); ``` The `return_if_error()` macro passes the error to the caller without destroying the internal decoder, so reinitializing with the same decoder function can reach the same object with its null dictionary pointer and outdated size. [Original caller](https://github.com/tukaani-project/xz/blob/9fc6f5cd8774ebef8d4e030f7081fb6984c0dc3f/src/liblzma/common/alone_decoder.c#L141-L159), [reuse check](https://github.com/tukaani-project/xz/blob/e5e63d50eac1b4357a5a7a0abd3c5f99a5c47881/src/liblzma/common/common.h#L389-L394). The maintainer's cleanup commit explains that the affected paths initialized the LZMA filter directly to avoid including unrelated filters in statically linked programs, while the raw filter initialization path cleaned up on failure. The bug also affected `lzma_auto_decoder()` indirectly when it selected `.lzma` or `.lz` decoding. [Cleanup patch and explanation](https://github.com/tukaani-project/xz/commit/e5e63d50eac1b4357a5a7a0abd3c5f99a5c47881). ## What the fix changes The maintainers addressed both the dictionary bookkeeping and the lifetime of a failed filter chain. ### Clear the size before replacing the dictionary The change in `lzma_lz_decoder_init()` is one line: ```diff --- a/src/liblzma/lz/lz_decoder.c +++ b/src/liblzma/lz/lz_decoder.c @@ -279,3 +279,4 @@ // Allocate and initialize the dictionary. if (coder->dict.size != alloc_size) { + coder->dict.size = 0; lzma_free(coder->dict.buf, allocator); ``` The fix clears the recorded size before freeing the old buffer and sets the new size after a successful allocation as before. If allocation fails the size stays at zero, so a later initialization cannot mistake it for the previous dictionary's capacity and will try to allocate again. [Dictionary fix](https://github.com/tukaani-project/xz/commit/fe4d763d566a38ad61d4c5022520c25578a3a464). ### Destroy the filter chain when initialization fails The caller now cleans up before returning an error, as shown in the `.lzma` change: ```diff --- a/src/liblzma/common/alone_decoder.c +++ b/src/liblzma/common/alone_decoder.c @@ -155,2 +155,6 @@ -return_if_error(lzma_next_filter_init(&coder->next, - allocator, filters)); +const lzma_ret ret = lzma_next_filter_init(&coder->next, + allocator, filters); +if (ret != LZMA_OK) { + lzma_next_end(&coder->next, allocator); + return ret; +} ``` The function `lzma_next_end()` calls the decoder's cleanup routine and resets its bookkeeping to the initial state, with the same cleanup applied to `lzip_decode()` and `microlzma_decode()`. The `lzma_auto_decoder()` path receives the fix through the format decoders it selects. [Caller fixes](https://github.com/tukaani-project/xz/commit/e5e63d50eac1b4357a5a7a0abd3c5f99a5c47881) and [cleanup function](https://github.com/tukaani-project/xz/blob/e5e63d50eac1b4357a5a7a0abd3c5f99a5c47881/src/liblzma/common/common.c#L151-L169). ## A regression test with three decode attempts The maintainer's function `test_reuse_after_failure()` tests the `.lzma` path by reusing a single `lzma_stream`, with `lzma_alone_decoder()` being called before each attempt. A custom allocator refuses requests of 1 MiB or more, whereas the decoder has a separate memory limit of 16 MiB. [Maintainer test](https://github.com/tukaani-project/xz/commit/ff834f25ae4f9b3e6270d41b4c843b8b1183346c). | Attempt | Dictionary requested by the input | Result checked by the test | | --- | --- | --- | | 1 | 4 KiB | Decoding completes with `LZMA_STREAM_END`. | | 2 | 8 MiB | Replacement allocation fails with `LZMA_MEM_ERROR`. | | 3 | 4 KiB | After reinitialization, decoding must complete with `LZMA_STREAM_END`. | The maintainer reports that this test crashes when both fix commits are reverted, demonstrating one decoder path under a controlled allocation failure without establishing production exploitability across every affected interface. The `LZMA_MEM_ERROR` result indicates an allocation failure on this path while `LZMA_MEMLIMIT_ERROR` means the decoder's configured limit rejected the memory requirement before dictionary initialization, so that limit rejection does not produce the faulty state. [Limit check](https://github.com/tukaani-project/xz/blob/9fc6f5cd8774ebef8d4e030f7081fb6984c0dc3f/src/liblzma/common/alone_decoder.c#L141-L156), [error definitions](https://github.com/tukaani-project/xz/blob/d3e650e63c110e830fd5391e7f8b45df0b91d3da/src/liblzma/api/lzma/base.h#L126-L152). ## Fix and release timeline 1. **On September 9, 2026,** the maintainers carried out the [dictionary size fix](https://github.com/tukaani-project/xz/commit/fe4d763d566a38ad61d4c5022520c25578a3a464), the [filter cleanup fix](https://github.com/tukaani-project/xz/commit/e5e63d50eac1b4357a5a7a0abd3c5f99a5c47881), and the [regression test](https://github.com/tukaani-project/xz/commit/ff834f25ae4f9b3e6270d41b4c843b8b1183346c). 2. **On September 9, 2026,** the project released [XZ Utils 5.8.4](https://github.com/tukaani-project/xz/releases/tag/v5.8.4) and the [security advisory](https://github.com/tukaani-project/xz/security/advisories/GHSA-5qpq-xqfv-j9pg) for the bug discovered by Apex and reported by Cantina. ## What to take into your next review When a decoder, parser or connection object can be reused its error handling paths need to be examined alongside successful initialization to establish which fields still refer to the previous allocation, which objects remain attached to the caller and what the next initialization will rely on. A useful test sequence is to allocate the object successfully and then make a replacement allocation fail before reusing the object with its original parameters, checking both the intermediate error code and whether the final operation is safe. Returning an error ends one attempt but the object left behind determines whether the next attempt is safe. Book an Apex demo ## Sources - [XZ Utils security advisory: GHSA-5qpq-xqfv-j9pg](https://github.com/tukaani-project/xz/security/advisories/GHSA-5qpq-xqfv-j9pg) - [XZ Utils 5.8.4 release notes](https://github.com/tukaani-project/xz/releases/tag/v5.8.4) - [XZ Utils release history, including 5.0.0 in October 2010](https://tukaani.org/xz/old.html#_5_0_x) - [Dictionary allocation fix](https://github.com/tukaani-project/xz/commit/fe4d763d566a38ad61d4c5022520c25578a3a464) - [Cleanup after filter initialization failure](https://github.com/tukaani-project/xz/commit/e5e63d50eac1b4357a5a7a0abd3c5f99a5c47881) - [Maintainer regression test](https://github.com/tukaani-project/xz/commit/ff834f25ae4f9b3e6270d41b4c843b8b1183346c) - [liblzma stream lifecycle documentation](https://github.com/tukaani-project/xz/blob/d3e650e63c110e830fd5391e7f8b45df0b91d3da/src/liblzma/api/lzma/base.h#L486-L520) - [MicroLZMA dictionary-size API](https://github.com/tukaani-project/xz/blob/d3e650e63c110e830fd5391e7f8b45df0b91d3da/src/liblzma/api/lzma/container.h#L952-L996) - [Dictionary layout and trailing allocation bytes](https://github.com/tukaani-project/xz/blob/d3e650e63c110e830fd5391e7f8b45df0b91d3da/src/liblzma/lz/lz_decoder.h#L63-L66)