Skip to content

Memory Management & Resource Bounds Guide ​

xz.zig is engineered from the ground up for predictable memory consumption, zero-allocation caller buffers, and robust defense-in-depth against decompression bombs and malformed streams.


1. Ownership & Allocation Contracts ​

xz.zig strictly follows standard Zig memory ownership conventions:

  • Heap Allocations: Any function accepting a std.mem.Allocator allocates memory on behalf of the caller. The caller owns the returned buffer and is responsible for freeing it:

    zig
    const comp = try xz.compress(allocator, data, .{});
    defer allocator.free(comp);
    
    const orig = try xz.decompress(allocator, comp, .{});
    defer allocator.free(orig);
  • Zero-Allocation Caller Buffers: Functions named compressInto and decompressInto perform zero heap allocations. They operate directly on caller-provided memory buffers and return the number of bytes written:

    zig
    var out_buf: [4096]u8 = undefined;
    const written = try xz.compressInto(data, &out_buf, .{});
    const comp_slice = out_buf[0..written];

    If the destination slice is too small to contain the output, the function returns error.OutputTooSmall immediately without leaking or allocating memory.


2. Decompression Bomb Protection (CWE-409) ​

XZ containers encapsulate LZMA2 compressed blocks and optional filter stages. Highly repetitive or hostile payloads can achieve compression ratios exceeding 1,000:1 (and even millions-to-one on synthetic streams). An attacker could send a 10 KiB payload that attempts to decompress into hundreds of gigabytes, causing denial-of-service through memory exhaustion.

xz.zig defends against this with three layered safeguards:

Uncompressed Output Caps (max_output_size) ​

Set DecompressOptions.max_output_size to enforce an absolute uncompressed byte ceiling across multi-block streams:

zig
const decomp = try xz.decompress(allocator, untrusted_payload, .{
    .max_output_size = 32 * 1024 * 1024, // 32 MiB ceiling
}) catch |err| switch (err) {
    error.ExceededOutputLimit => {
        std.debug.print("Aborted: uncompressed output exceeds 32 MiB safety limit\n", .{});
        return;
    },
    else => return err,
};
defer allocator.free(decomp);

If the stream attempts to emit more bytes than permitted, the decoder halts decoding immediately and returns error.ExceededOutputLimit before additional memory is allocated.

Dictionary Memory Bounds (dict_size_limit / memlimit) ​

Block headers declare the dictionary size required by the LZMA2 filter (up to 1.5 GiB in the XZ specification). Rogue or forged headers could otherwise attempt to trigger excessive memory allocations during decoder setup.

zig
const decomp = try xz.decompress(allocator, untrusted_payload, .{
    .dict_size_limit = 16 * 1024 * 1024, // Allow up to 16 MiB dictionaries
}) catch |err| switch (err) {
    error.ExceededMemoryLimit => {
        std.debug.print("Aborted: block requires dictionary larger than allowed\n", .{});
        return;
    },
    else => return err,
};

If the decoded block header requests a dictionary exceeding this limit, the decoder rejects the stream with error.ExceededMemoryLimit before dictionary buffer allocation occurs.


3. Compression Presets & Memory Footprint ​

xz.zig provides standard compression presets (0–9) that scale dictionary sizes and memory usage predictably:

PresetDictionary SizeDescriptionRecommended Usage
04 KiBFastest / copy chunk store modeMicrocontrollers, extreme memory limits
164 KiBFast compression, tiny memory footprintSmall logs, cache entries
2256 KiBLightweight balanced modeEmbedded devices, fast backups
31 MiBModerate compression ratioNetwork transfer, quick archiving
42 MiBGood ratio, reasonable RAM usageGeneral-purpose archiving
54 MiBEnhanced ratioLarge log files, software distribution
68 MiBDefault preset, optimal balanceRelease tarballs, general archives
716 MiBHigh compression ratioHigh-ratio backups, release packages
832 MiBVery high compression ratioStatic distribution assets
964 MiBMaximum ratio modeCold storage, archival builds

You can override the dictionary size explicitly using .dict_size in CompressOptions:

zig
const comp = try xz.compress(allocator, data, .{
    .level = 6,
    .dict_size = 4 * 1024 * 1024, // Force 4 MiB dictionary
});

4. Bounded Streaming Pipelines ​

For files larger than system RAM or infinite network streams, use compressStream and decompressStream:

zig
var file = try std.fs.cwd().openFile("archive.xz", .{});
defer file.close();

var reader = file.reader();
var writer = std.io.getStdOut().writer();

try xz.decompressStream(allocator, &reader, &writer, .{
    .dict_size_limit = 32 * 1024 * 1024,
});

Streaming I/O operates on bounded internal ring buffers, ensuring memory consumption remains constant regardless of the total file size being processed.


5. Framing and VLI Integrity ​

xz.zig validates every variable-length integer (VLI) and header framing strictly per the XZ Specification (Section 1.0.4):

  • Multi-byte VLIs must not exceed 9 bytes (maximum representable value $2^{63}-1$).
  • Padding bytes in Block Headers and Indexes must be strictly 0x00. Any non-zero padding byte triggers error.InvalidPadding.
  • Header CRC32 and block check CRC32/CRC64/SHA-256 are validated on every block; corrupted data returns error.ChecksumMismatch or error.CorruptData.

Released under the MIT License.