Containers and archives
Before disrobe can decompile anything, it often has to get inside a container. The disrobe-binfmt layer detects every format below. Most use the generic member-byte extractor; LUKS1 uses a dedicated raw-volume-key route and reports a typed key wall when no key is supplied. Auto-detection, recursive chaining through nested layers, and shared zip-slip and decompression-bomb guards remain in effect. A committed input drives 30 generic extractors to member bytes that match an independent extractor (bsdtar, 7-Zip or the format's own decompressor), and the tracked LUKS1 fixture separately proves byte-exact decryption into the VHD extractor.
At a glance
| Surface | Support |
|---|---|
| Formats detected | 103 archive, installer, filesystem, firmware, and encrypted-volume formats; 102 use the generic extraction entry point and LUKS1 uses its bounded raw-volume-key entry point |
| Formats exercised | 30 of them write member bytes from an input this repository commits that match the members bsdtar 3.8.8, 7-Zip 26.02 or the format's own decompressor extracts from the same input (crates/disrobe-cli/tests/golden/container_breadth_reference.tsv, written by scripts/container_breadth_reference.py), measured by crates/disrobe-cli/tests/container_breadth.rs and pinned in crates/disrobe-cli/tests/golden/container_breadth.txt. 12 more write members that no installed reference tool confirms, the committed PAR2 input reaches detection only, and 59 generic routes have no committed input. The declared roster is a capability list rather than a measurement |
| Carve engine | A recursive scan with the magic-signature table that detect_container uses. It carves sixteen formats: zip, 7z, RAR 4 and 5, CAB, xz, gzip, zstd, bzip2, tar, squashfs, minidump, ISO 9660, WIM, UnityFS, cpio and ar (including deb). Each hit ends where its decoder stops or where its headers place the end of the archive, so a stream followed by another container yields both; a zip is carved only when its end-of-central-directory record lies within the last 64 KiB of the input that follows it. InstallShield, RPM, xar, VHD, VHDX, lzip, lz4 and compress (.Z) signatures are detected but not carved, because the carver cannot compute where they end, and formats recognised by structure rather than a fixed signature (FAT, ext4 and MSI, for example) are not carved out of a larger blob. It recurses by depth and uses entropy to separate code from padding |
| Nesting | Container-in-container chaining, governed by --max-depth (default 8) |
| Directory input | Batch-processed recursively, bounded by --batch-max-depth |
| Guards | Per-entry and aggregate size caps, recursion-depth cap, zip-slip path sanitization, on every format |
Supported formats
| Category | Formats |
|---|---|
| Archives and installers | ZIP (incl. ZIP64; an archive with encrypted members is refused, since no password can be supplied), tar.gz / tar.bz2 / tar.xz / tar.zst, 7z, RAR4 and RAR5 (stored members from both; RAR5 LZ "normal" method decoded in-tree; RAR 2.9/3.x LZ, PPMd, mixed LZ and PPMd blocks, and canonical delta and x86 filter programs decoded in-tree, with every decoded member checked against the CRC-32 its header declares; an encrypted member, a member split across volumes, and a stored member whose packed and unpacked sizes differ are each refused by name while the archive's other members still extract), .cab, MSI, MSIX / APPX, NSIS (each File entry's bytes from stored, zlib, BZip2 and LZMA data, solid and non-solid, with ANSI or Unicode names; installers that makensis 3.11 builds from corpus/installers/nsis/tide_viewer.nsi extract with every file's SHA-256 equal to its input in solid LZMA, zlib and BZip2, non-solid LZMA, zlib and BZip2, and ANSI solid LZMA builds; a file is named as its File command names it, without the SetOutPath directory, and the install script's other instructions are not decompiled), Inno Setup 4.0.9 through setup-data profile 7.0.0.3 (stored, zlib, BZip2, LZMA1, and LZMA2 members, including solid groups), InstallShield legacy cabinets (ISc( majors 0 and 5: stored members, u16-length chunk-framed raw DEFLATE, and full-flush raw DEFLATE, with obfuscated members decoded before inflate), .deb, .rpm (metadata), AppImage Type 1 (ISO 9660, Rock Ridge, and zisofs) and Type 2 (SquashFS), Flatpak, Snap |
| Bare compression streams | gzip, bzip2, xz, zstd, lzma, lzip, lz4-frame, zlib, .Z (Unix compress); concatenated gzip members, bzip2 streams, xz streams, and lzip members all decode, in order, both bare and under tar.gz / tar.bz2 / tar.xz |
| Legacy archives | ar, arj (methods 0-4 decoded, with header and member CRC32 verified; split volumes refused by name), arc (methods 1-9 decoded; independent byte checks cover methods 2 and 5-9; methods 8-9 use grouped dynamic LZW with exact declared-size and CRC checks; methods 10-11 refused), LZH header levels 0-3 (-lh0- through -lh7-, -lhx-, -lz4-, -lz5-, -lzs-, and -pm0- decoded; -lhd- directories retained; -lhd- symbolic links refused; -pm1- and -pm2- decoded in tree and reaching extraction, each member checked against the CRC-16 its archiver stored; byte, code-page, and UTF-16 paths recovered; missing split volumes refused), lzop, FreeBSD uzip, Xamarin xalz, par2, ELF appended-overlay carve, StuffIt (classic stored and methods 2, 5 LZAH, 8 MW, and 13 decoded for data and resource forks, with record-header and fork CRC validation; StuffIt 5 containers parsed and their Arsenic and stored forks decoded in tree, though extraction still carves a StuffIt 5 archive rather than writing its forks; classic method 6 and StuffIt 5 method 14 are not implemented and refuse by name because no archive using them was found to grade a decoder against; encrypted forks refused), partclone (decoded) |
| Embedded-linux filesystems | squashfs, cramfs, ext4, romfs, minixfs, jffs2, UBI + UBIFS, yaffs, erofs (full and compact indexes with lz4, deflate, zstd, and microlzma decoded), NTFS, android-sparse, btrfs-send |
| Disk images and partitions | GPT and MBR (partition tables parsed; each partition carved and recursed in-tree), VHD (fixed and dynamic BAT), VHDX (region table + BAT; logical disk materialized from the block-allocation table, then partition-carved and FAT12 / 16 / 32 walked to pull individual stored files), WIM (header resources with XPRESS / LZX / LZMS chunk payloads decompressed in-tree), FAT12 / 16 / 32 (boot sector, FAT chain walk, root and subdirectory traversal) |
| Encrypted volumes | LUKS1 with aes-cbc-plain, a 128-, 192-, or 256-bit raw volume key, and a SHA-1, SHA-256, or SHA-512 PBKDF2 header digest. The raw key is read from a bounded file or standard input, verified against the header digest, and zeroized after use before the decrypted payload enters the normal container pipeline. Keyless extraction succeeds with a typed wall that names the cipher, mode, digest, iteration count, and missing raw volume key |
| Apple | .dmg (UDIF: koly trailer + blkx mish chunks; ADC / zlib / bzip2 / LZFSE / LZMA chunk decoders; then HFS+ catalog walk extracts individual files, all in-tree), .pkg (xar TOC + gzip / bzip2 heap, extracted in-tree) |
| Vendor firmware | D-Link (SHRS / encrypted-img AES / alpha / fpkg), EnGenius XOR, Autel ECC table, QNAP PC1, plus CRC-verified Netgear (chk / trx), Xiaomi, Tesla, HP, Moxa, INSTAR, and Airoha carves; OTP-AES Airoha firmware is an information-theoretic wall and is carved verbatim |
| Standalone executables | Bun --compile binaries for Windows, Linux and macOS (the embedded module graph and sourcemaps; each module's bundled source is recovered as Bun embedded it, and module names lose the /$bunfs/root/ or B:/~BUN/root/ prefix), Unity AssetBundle (UnityFS), .NET single-file bundles (majors 1, 2 and 6; embedded assemblies routed to the CIL decompiler, native entries to the native pass), Enigma Virtual Box 10.70 Build 20240522 x86 built-in stored-file bundles with literal .enigma1 and .enigma2 section names |
| App / runtime | Electron .asar, Docker image tarball, OCI image manifest + layers, ISO 9660 with Joliet fallback, Rock Ridge names, ordered multi-extent files, and zisofs v1 (extracted in-tree) |
Commands
Most extraction happens implicitly inside disrobe auto, which detects a container, extracts it, and recurses into the contents. Archive-shaped inputs are also available directly:
disrobe py extract package.whl --out extracted/
disrobe auto firmware-dir/ --out extracted/ --batch-max-depth 6
disrobe extract crash.dmp --out carved/
disrobe extract volume.luks --luks1-raw-volume-key-file volume.key --out decrypted/
disrobe extract volume.luks --luks1-raw-volume-key-file - --out decrypted/
Directory inputs are batch-processed recursively; --batch-max-depth limits directory descent. Container nesting inside a detected artifact is governed by --max-depth (default 8).
For ZIP, TAR, ARC, RAR, ARJ, LZH, StuffIt, Inno Setup, InstallShield, RPM, AppImage, .NET single-file, EROFS, and UEFI firmware-volume inputs, disrobe extract and disrobe auto apply the same default quota policy and retain the same ordered, byte-identical archive members. Extractor-generated sidecars are tagged separately and remain direct-only; an archive-controlled name such as .disrobe-user.bin remains an archive member. When an independently indexed member has an unsafe path, exceeds a quota, or cannot be decoded, extraction skips it and continues with trustworthy siblings. The direct result records the ordered reasons in integrity_violations; the automatic chain writes the ordered prefix that fits the 64 KiB container.refusals bound. container.refusals_total, container.refusals_omitted, and container.refusals_truncated make any truncation explicit. A malformed directory or table that cannot identify the next member safely still refuses the whole container.
Coverage and fidelity
Windows crash dumps
disrobe extract crash.dmp (or disrobe auto crash.dmp) carves the loaded PE modules out of a Windows minidump. It parses the stream directory, reads the module and memory lists, and for each module rebuilds an in-memory PE image by copying whatever memory the dump actually captured into a buffer at the correct RVAs, rewriting each section's file-offset field to match its virtual address so downstream PE parsers read the result as a well-formed image. Coverage is reported per page: the summary records how many bytes were recovered, which ranges the dump truncated, and which it never captured, with a reason for each gap, so a partially captured module is never presented as complete. The carve is graded by wrapping a real on-disk PE into a minidump and confirming the carved .text comes back byte-identical (minidump_real_pe.rs); each carved module and a .disrobe-minidump.json coverage summary land in the output directory.
Deno eszip archives
The disrobe-binfmt eszip reader (disrobe_binfmt::containers::eszip) parses a Deno eszip module-graph archive, versions 2 through 2.3, including one embedded inside a deno compile standalone executable, and reconstructs the module graph: each module's specifier, kind, and source bytes, plus redirects and npm specifiers, with per-module source-hash verification that drops any module whose stored hash does not match. It is exercised by a build-then-parse round-trip that also confirms a corrupted source hash is rejected.
Safety guards
Every extractor shares the quota machinery in crates/disrobe-binfmt/src/quota.rs:
- Per-entry size cap and aggregate size cap defuse decompression bombs.
- Recursion-depth cap defuses container-in-container bombs.
- Zip-slip path sanitization (
sanitize_entry_path): every entry path is sanitized so no extraction can escape the output directory, on every format.
Bypasses of any of these are treated as security issues; see the security policy.
Limits
Where a format's payload is not decoded in-tree, the table above names it per entry rather than implying full extraction:
- LUKS1 recovery is limited to
aes-cbc-plainwith a caller-supplied raw volume key. Passphrases and keyslot unlocking, XTS modes, and LUKS2 are not accepted by this route. A detected keyless LUKS1 header produces a successful typed wall. A detached LUKS1 header is refused until its separately stored encrypted payload is supplied. VeraCrypt and TrueCrypt may not be identifiable without a key, and headerless dm-crypt has no on-disk header to detect. - RAR 2.9/3.x members carry their transforms as RARVM programs. disrobe identifies a program by exact length and CRC-32 and runs a native transform for the canonical delta, x86 e8, x86 e8/e9, itanium, rgb and audio programs. It does not interpret RARVM bytecode, so a member carrying any other program is refused by name. The corpus grades the delta and x86 e8/e9 transforms against a real archive. The plain x86 e8, itanium, rgb and audio transforms have no real archive in the corpus yet, so their output rests on the CRC-32 check every decoded member passes and on known-answer tests rather than on a graded fixture. The crate publishes this gap as
RAR3_FILTERS_WITHOUT_REAL_ARTIFACTandRAR3_FILTER_COVERAGE_NOTEindisrobe_binfmt::containers::rar. Encrypted members (the RAR4LHD_PASSWORDflag or a RAR5 file-encryption record), members split across volumes (the RAR4LHD_SPLIT_BEFOREandLHD_SPLIT_AFTERflags or the RAR5 volume-continuation header flags), and solid state carried from an earlier entry are refused by name, and the other members still extract. A stored member whose packed size differs from its declared unpacked size, and any member whose recovered length differs from its header, is refused rather than written short. - Cabinet (
.caband MSI-embedded) folders are decoded once each, in member-offset order, for stored, MSZIP, LZX, and LZMS folders, and each data block's checksum is verified when the cabinet sets one. Quantum folders, members that continue into another cabinet of a set, and members whose name appears more than once in the cabinet are refused by name. - In a solid 7z block, a member refused for its path is still read past, so the members after it keep their own bytes.
- Inno Setup follows finite version profiles from the loader through both metadata blocks, file and data records, solid groups, filters, and checksums. Unsupported profiles and encrypted content without a secret refuse by name.
- ARC methods 10-11 are refused.
- StuffIt 5 containers are parsed and their forks decode in tree, but extraction still carves the archive verbatim rather than writing each fork, so
disrobe autorecovers a StuffIt 5 archive as one blob. Classic StuffIt forks are unaffected and extract per fork. - OTP-AES Airoha firmware is an information-theoretic wall and is carved verbatim.
- A minidump only contains the memory it captured. Truncated and never-captured ranges are reported with a reason per gap instead of being filled in.