Inspect qcow2 and raw cloud images without booting or mounting them — a zero-dependency Rust CLI from Virtainer. It reports which OS is inside a disk image and the facts that decide how a first-boot configuration has to be written for it. The image is parsed directly, read-only and in userspace: it never executes guest code, never asks the host kernel to parse guest filesystems, and has no dependencies beyond the Rust standard library.
- Inspect Windows images for edition/build, virtio driver services and files, sysprep state, EMS, RTC, power settings and dirty markers.
- Check which OS is inside a qcow2 or raw image without mounting it
(
/etc/os-release: ID, VERSION_ID, ID_LIKE, PRETTY_NAME, …). - Read os-release, cloud-init, sshd and sudo facts from a disk image: is
cloud-init installed and which version, its
datasource_list, whether sshd is a PAM build andUsePAMis on,sudoordoas. - Decide how to write first-boot config for an image: cloud-init, tiny-cloud or
Ignition; bash or only
/bin/sh; systemd, OpenRC, runit or busybox init; UEFI or BIOS boot. - List the installed packages and versions in an image from its apk, dpkg, pacman or RPM database.
- List, read or extract files from an image's ext4, XFS, btrfs, FAT or NTFS filesystem without libguestfs and without mounting it.
cargo install --locked --git https://github.com/virtainer/cloud-image-inspector
cloud-image-inspector disk.qcow2 # human-readable report
cloud-image-inspector --json disk.qcow2 # machine-readable report
A static x86_64 Linux binary is attached to each
release, with a
SHA256SUMS file to check it against.
All commands and options are under Usage; to build in a container, see Building.
Example report for the Alpine cloud image:
$ cloud-image-inspector images/generic_alpine-3.24.1-x86_64-uefi-cloudinit-r0.qcow2
== images/generic_alpine-3.24.1-x86_64-uefi-cloudinit-r0.qcow2
container qcow2 v3, virtual size 214.0 MiB, 64.0 KiB clusters, deflate compression
partitions gpt table
1 esp vfat 512.0 KiB name="EFI"
2 linux-data ext4 212.0 MiB label="/" name="/"
boot UEFI only
root partition 2 (ext4)
os Alpine Linux 3.24.1 (ID=alpine VERSION_ID=3.24.1)
shells bash: no
/bin/sh: /bin/sh -> /bin/busybox
privilege sudo: no doas: yes (/usr/bin/doas)
init openrc (/sbin/init: /sbin/init -> /bin/busybox)
sshd sshd: yes (/usr/sbin/sshd) PAM build: yes (/usr/sbin/sshd.pam)
UsePAM: (default: no) sshd_config.d included: true
first boot cloud-init: yes (/usr/bin/cloud-init), version 26.1-r3 (from apk database)
packages apk (201 installed, /lib/apk/db/installed)
...
| Fact | Source in the image |
|---|---|
| Container | qcow2 version, virtual size, cluster size, compression type, extended L2, snapshots |
| Partitions | GPT/MBR (incl. extended), type, name, filesystem by superblock magic, label |
| Boot method | UEFI (an EFI System Partition exists), BIOS (BIOS boot partition or MBR boot code) |
| Root filesystem | Linux: the partition with an os-release, btrfs subvolume (default first), OSTree deployment; Windows: an NTFS Windows/System32/config tree with a hive or kernel file |
| OS family | os_family: linux, windows or unknown, derived from image contents |
| OS | /etc/os-release or /usr/lib/os-release: ID, VERSION_ID, ID_LIKE, NAME, PRETTY_NAME, VERSION_CODENAME |
| Login shells | bash present, /bin/sh symlink chain (canonical paths), /etc/shells, useradd default SHELL (also /usr/etc) |
| Privilege tool | sudo, doas, /etc/sudoers.d, /etc/doas.d, /etc/doas.conf |
| Init system | /sbin/init chain; systemd, OpenRC, runit, busybox init |
| Serial console login | /etc/inittab, the /sbin/getty symlink chain (busybox or util-linux), agetty, login |
| sshd | sshd, the PAM build sshd.pam, effective UsePAM (first value wins, Include expanded; /usr/etc fallback) |
| First-boot agent | cloud-init and its version (package DB, else Python metadata), datasource_list; tiny-cloud; Ignition |
| Packages | apk (/lib/apk/db/installed), dpkg (Status: … installed only), pacman (local/*/desc, honouring DBPath), RPM (SQLite with WAL, and ndb): every installed package and version, including several of one name |
| Windows product | windows.product_name, edition_id, installation_type, integer build and ubr: SOFTWARE\Microsoft\Windows NT\CurrentVersion |
| Windows architecture | windows.arch: the ntoskrnl.exe PE machine field (amd64, x86, arm64, arm; unknown machines are null) |
| Windows drivers and agent | windows.drivers.{viostor,netkvm,viosock,virtainer_agent}: service-key present, integer start, executable file; the agent also has version from its PE fixed file-version resource |
| Windows sysprep | windows.sysprep.image_state: SOFTWARE\Microsoft\Windows\CurrentVersion\Setup\State\ImageState |
| Windows EMS | windows.ems.bootems, ems_enabled: the EFI System Partition's EFI/Microsoft/Boot/BCD hive, including inherited elements |
| Windows RTC and power | windows.rtc_is_universal, hibernation, fast_startup: selected SYSTEM ControlSet's RealTimeIsUniversal, HibernateEnabled, HiberbootEnabled |
| Windows dirty markers | windows.dirty.system_hive, software_hive: validated base-block sequence mismatches; ntfs_volume: $Volume dirty flag |
--json emits all of it; --all-packages adds the full package list.
| Layer | Supported |
|---|---|
| Container | qcow2 v2/v3: L1/L2, compressed clusters (deflate, zstd), zero clusters, extended L2 subclusters; raw |
| Partition tables | GPT (512 and 4096-byte sectors), MBR with extended/logical partitions, none (whole-disk filesystem) |
| ext2/3/4 | extent trees, ext2/3 block maps, 32/64-bit descriptors, meta_bg, inline data, htree dirs, 1 KiB–64 KiB blocks |
| XFS | v4 and v5, AG-encoded addressing, local/extent/B-tree forks, nrext64, all five directory formats, remote symlinks |
| btrfs | full chunk tree, B-trees of any height, subvolumes and default subvolume, inline/regular/prealloc extents, zlib/LZO/zstd |
| FAT12/16/32 | BPB geometry, bounded cluster chains, fixed and cluster-based root directories, checksum-validated long names, nested directories and file ranges |
| NTFS | boot sector, MFT bootstrap and fixups, attribute lists, resident/non-resident streams, signed data runs, sparse/uninitialized zeros, $INDEX_ROOT/$INDEX_ALLOCATION with bitmap and fixups, $UpCase |
| Windows registry | primary regf 1.3–1.6, base checksum/sequence numbers, hive bins and allocated cells, nk/vk/lf/lh/li/ri, big data; SZ/EXPAND_SZ/MULTI_SZ/DWORD/QWORD/BINARY |
| Decompressors | DEFLATE/zlib (RFC 1950/1951), zstd (RFC 8878, with XXH64), LZO1X and the btrfs LZO container |
| Package DBs | apk, dpkg, pacman (text); SQLite (RPM rpmdb.sqlite, WAL applied); RPM ndb (Packages.db) |
Not supported, and reported as such: LVM and LUKS volumes, multi-device btrfs, RAID0/10/5/6 btrfs profiles, qcow2 backing files and external data files (refused on purpose), encrypted qcow2, RPM Berkeley DB (EL7/EL8). Windows readers also refuse NTFS compression, EFS and reparse points (including WOF), inaccessible MFT bootstrap extensions, and FAT OEM short names or non-ASCII case folding. Unicode long names can be listed and matched exactly. These cases produce errors, never raw compressed bytes or a guessed absence.
--json adds top-level os_family and windows. A Windows report contains every
Windows field in the table above; Linux facts is null. A Linux report keeps its
existing facts and has windows: null. Unrecognized or ambiguous guest roots
have os_family: "unknown". Windows values that cannot be read or interpreted
are null, including missing registry settings: OS defaults are not assumed.
Confirmed missing service keys or files are false. Registry lookup errors
leave the affected facts unknown (None in the library, null in JSON and
unknown in text), while readable facts from other keys and hives remain
available. Warnings identify failed lookups.
Registry lookups follow persistent key paths without indexing every cell in SOFTWARE or SYSTEM. Stale volatile-subkey counts and pointers are ignored: volatile subkeys are memory-only. Per-lookup limits and their rationale are listed in VERIFICATION.md.
SYSTEM service and power settings use only Select\Current's ControlSet.
viostor, netkvm and viosock use their service ImagePath, or their explicit
Windows/System32/drivers/<name>.sys location when a readable service key has no
ImagePath set. Missing or unreadable service keys leave file evidence null. The agent
service is searched under virtainer-guest-agent, virtainer_agent and
virtainer_guest_agent; multiple aliases are ambiguous. Its executable is
located from the registered ImagePath. An absolute drive is mapped only when
SOFTWARE's SystemRoot identifies that drive. Without a usable agent ImagePath,
file and version are null: there is no assumed installation directory.
version is the PE fixed file version, formatted major.minor.build.revision.
EMS describes the boot manager's library bootems element (16000020) and the
boot manager's default Windows OS loader's ems element (260000b0), selected
by 23000003. GUID object elements and inheritance lists are decoded from
REG_SZ/REG_MULTI_SZ (or UTF-16 binary data). Inheritance is bounded and conflicts
are unknown. Multiple EFI System Partitions, a dirty BCD hive, or an unreadable
store leave EMS unknown. Missing elements stay null. These are stored settings;
they do not prove that a guest has booted or that its serial console works.
Hives and the NTFS journal are not replayed. A dirty hive's facts describe the stored snapshot and may omit recent changes; the separate dirty flag preserves that evidence. No guest executable is run. See VERIFICATION.md for synthetic coverage and the optional local Windows image check.
Images come from URLs and uploads, so every parser treats its input as hostile:
- all on-disk reads are bounds-checked (
bytes.rs), so malformed data is an error value, not a panic; - every length that sizes an allocation is capped (whole-file reads 256 MiB, single reads 512 MiB, compressed extents 1 MiB);
- every tree walk has a depth cap and a node-visit budget, since corrupt pointers can form a DAG that a depth cap alone does not stop;
- qcow2 backing files and external data files are refused, since following them would open a host path chosen by the image;
- symlinks resolve inside the image root (absolute targets restart at the image
root,
..never climbs above it, 40 hops at most).
cloud-image-inspector [--json] [--all-packages] IMAGE...
cloud-image-inspector ls [--partition N] [--subvol ID] IMAGE PATH
cloud-image-inspector cat [--partition N] [--subvol ID] IMAGE PATH
cloud-image-inspector stat [--partition N] [--subvol ID] IMAGE PATH
cloud-image-inspector export [--partition N] [--subvol ID] IMAGE PATH DEST
cloud-image-inspector --help | --version
File commands use the detected Linux or Windows root filesystem unless
--partition is given. Ambiguous roots require --partition N, including
images containing two Windows installations. Windows/FAT paths accept / or
\ separators; quote backslashes in the shell. Use --partition N to read an
EFI or seed volume.
An explicit --subvol ID can select a Btrfs data filesystem without an OS root.
If multiple Btrfs filesystems are present, also specify --partition N.
CII_STATS=1 prints how often each on-disk format path ran.
Run in containers (tools/Containerfile, tools/Containerfile.guestfs); see
VERIFICATION.md for the full record; tools/verify-all.sh reruns all of it.
Linux/container summary from the recorded 2026-10-05 run: 13 real cloud images (Alpine ×3, Debian, Ubuntu, Fedora, Fedora CoreOS,
AlmaLinux, Rocky, RHEL 9.7, RHEL 10.1, openSUSE, Arch) and 35 synthetic images,
every file identical to what the kernel or the source tree holds, every fact
confirmed, every package list identical to the distribution's own package manager
(rpm, dpkg, apk, pacman); 13,500 corrupted images without a panic or hang.
- Real images against the kernel (
tools/verify_real.py,tools/verify_config_facts.py): libguestfs boots a real Linux kernel (KVM) that mounts the same root filesystem; itstar-outof the whole tree is compared entry by entry (type, symlink target, SHA-256 of every file) with this tool'sexport. Every fact is re-derived from kernel-side lookups; packages are compared with the native package manager run on the kernel-extracted database (rpm --dbpath,dpkg-query --admindir,apk --rootin an Alpine container,pacman -Q --dbpathin an Arch container), and withvirt-inspector. - Synthetic fixtures (
tools/make_fixtures.py,tools/verify_fixtures.py): one edge-case tree built into 18 filesystem variants with the realmkfstools, plus 14 container and partition-layout variants (32 images); every export must equal the source tree, and every whole disk read through the tool must equalqemu-img convert -O raw. Three more are written by a running kernel (tools/kernel_fixtures.py: holes, unwritten extents, htree dirs, mount-time compression) and compared with the kernel's own view. - Decoders (
tools/make-vectors.py,tests/compress.rs,tests/pkgdb.rs): reference compressor output (zlib, zstd 1–22, python-lzo) and SQLite databases with overflow chains and a live WAL. - Robustness (
tests/fuzz_images.rs,tests/compress.rs): corrupted images (on the qcow2 file and on the guest disk, aimed at the blocks a clean run reads) and corrupted compressed streams must produce errors, never panics or hangs.
Windows and file-command coverage is self-contained in tests/windows.rs: 39
tests and 1,000 corrupted synthetic FAT/NTFS/regf/PE inputs. One additional Windows test requires
a local image and is ignored by default. No real Windows image was verified in
this checkout.
podman build -t cii-dev -f tools/Containerfile tools
podman run --rm -v "$PWD":/work -w /work cii-dev cargo build --release
Licensed under the Apache License, Version 2.0.
Virtainer is a self-hosted virtualization platform for hardware you own: it runs full Linux VMs and Docker images as hardware-isolated machines. Virtainer Free runs on a single host; Virtainer Pro, the multi-host edition for clusters, is in development. cloud-image-inspector is one of Virtainer's open-source components.