Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

106 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

reversefs — a filesystem in reverse

Polska wersja: README.pl.md. Browsable guide: docs/index.html (GitHub Pages-ready).

Note

A normal filesystem driver reads a disk and shows you files.

reversefs does the opposite: it synthesizes the disk from your files, on the fly.

reversefs works retro disks in both directions:

  • Synthesis. It serves ordinary directories as complete, byte-exact block devices in 14 on-disk formats.
  • Mounting & real-disk tools. The same format knowledge, applied to existing disks for FUSE mounting, formatting, whole-disk initialization, inspection and flashing.

Table of contents


Synthesis: directories served as disks

How it works

The flow runs top-down: from your files down to the disk the consumer sees:

How it works: a host directory synthesized into a live device or sparse image, read and written by the consumer

The source directory can live on any host filesystem — ext4, btrfs, xfs, NFS, whatever holds your files.

reversefs uses only portable POSIX file APIs plus inotify for live sync.

Reads never copy your data into an image first:

  • metadata is generated from an in-memory model
  • file content is pread from your directory on demand

Writes from the consumer land in a copy-on-write overlay, then a reconcile pass parses the effective disk and converges your directory:

  • create / update / delete are all handled
  • only dirty clusters are written, so a 1-byte edit never rewrites a whole file
  • live bidirectional sync is on by default: consumer writes appear in your directory after they quiesce, and source-side changes rebuild the served disk
  • stable allocation means unchanged files keep their sectors, so a mounted consumer stays coherent

Data safety rests on three mechanisms:

  • deletion basis — a source file is deleted only if the consumer has actually read the directory entry that lists it
  • host placements — placements written by the consumer override the model on rebuilds
  • safety valve — a failed parse can never mass-delete your sources

Note

Every format was implemented against an oracle (a reference image made by the ecosystem's own tools) and is verified by independent judges: real drivers and fscks, listed per format below.

cargo test runs them all.

Common usage

cargo build --release                                                              # rustc >= 1.87
./target/release/reversefs dump  --<fs>-part/-disc ... --out disk.img              # image, works everywhere
sudo ./target/release/reversefs serve-ublk --<fs>-part/-disc ... --dev 0           # live /dev/ublkb0 (Linux >= 6.0)
./target/release/reversefs serve-nbd --<fs>-part/-disc ... --listen 0.0.0.0:10809  # live over TCP, no root
# teardown: unmount consumers first, then:
sudo ./target/release/reversefs clean-ublk --dev 0                                 # triggers the final reconcile

Tip

Not sure about a flag? reversefs examples prints the full per-format cookbook (every command below and more, each with a one-line comment; reversefs examples fat32 shows one section).

reversefs info --size 200G prints the FATX geometry a given size produces.

Real-disk tools (mount, mkfs, init, inspect, flash) are covered in Mounting & real-disk tools.

Warning

serve-ublk needs the ublk_drv kernel module (Linux ≥ 6.0).

On Ubuntu/Mint, 6.17.0-35 is broken. Boot 6.14.x instead (Launchpad #2154635).

End-to-end root test: sudo bash scripts/live-test.sh.

Background mode

Any serving subcommand (serve-ublk, serve-nbd, serve-udpbd, serve-iscsi, serve-aoe, serve-usb) takes --daemon to detach into the background once the device is up (Linux double-fork).

--pid-file FILE records the PID and --log-file FILE captures its output.

Stop with kill $(cat FILE). SIGTERM and SIGINT both trigger the final reconcile before exit.

Write-back stays safe while detached: live sync reconciles continuously (default 1 s).

reversefs serve-nbd --ntfs-part /data/share=64G --daemon --pid-file /run/reversefs.pid --log-file /var/log/reversefs.log

# ...later:
kill "$(cat /run/reversefs.pid)"

Read-only sources

Append :ro to any part spec (--ntfs-part /data/games=64G:ro, --fatx360-part /data/gry=x2:ro, --ps2-part /d/opl=+OPL=512M:ro) or pass --read-only to protect every source at once.

The consumer still mounts read-write and never sees I/O errors.

Its writes land in the session overlay (gone on restart) and the source directory is never modified.

Perfect for a game library served to a console.

Fleet mode (up)

One YAML file describes many virtual disks.

The supervisor spawns one child reversefs per disk, so the one-process-one-disk rule survives and a crash stays inside its own disk.

Each child gets its own restart policy (always / on-failure / never, capped backoff), and on SIGTERM every child is stopped through its final reconcile.

YAML keys are CLI flag names verbatim, and the children's own parsers validate them.

The config can never drift from the CLI.

Start from a commented template: reversefs up --example > fleet.yaml.

  • An optional defaults: section is merged under every disk (the disk's own keys win). Unknown top-level sections are rejected.
  • A mounts: section FUSE-mounts devices once the disks are up. It is a wrapper over reversefs mount that:
    • waits for each device: to appear (a synthesized /dev/ublkbN or a real console disk)
    • unmounts everything BEFORE the disks stop
  • up records its PID in the standard daemon location (/run/reversefs.pid as root, else $XDG_RUNTIME_DIR/reversefs.pid, else reversefs.pid next to the config).
  • Running several fleets on one machine? Give each config its own top-level pid-file: key (or pass --pid-file to both up and down).

On a headless box run up CONFIG --daemon and stop the whole fleet with reversefs down CONFIG. No terminal or Ctrl-C is needed.

# fleet.yaml                        # reversefs up fleet.yaml
defaults:                           # reversefs up fleet.yaml --check
  restart: on-failure               #   prints the child command lines
disks:
  - name: xbox360
    transport: serve-ublk
    dev: 0
    fatx360-disc: /data/x360
    size: 200G
  - name: cd
    transport: serve-http
    listen: 0.0.0.0:8080
    iso9660-disc: /data/cd

The formats

FATX360 — Xbox 360 (--fatx360-part / --fatx360-disc)

Big-endian XTAF, calibrated byte-for-byte against fusefatx v1.19 and a real console-formatted disk.

A physical RGH3 console recognizes flashed dumps.

Full retail disk layout with all partitions.

Unbacked partitions appear console-formatted and absorb writes into a session overlay (never an I/O error).

# just games (x2/Content), no flags needed on the fusefatx side:
reversefs serve-ublk --fatx360-part /data/x360=x2 --size 200G --dev 0

# pick partitions explicitly (games + Xbox1 saves, skip caches):
reversefs serve-ublk --fatx360-part /data/games=x2 --fatx360-part /data/x1=x1 --size 200G --dev 0

# console-ready image for a REAL console: capture the security-sector prefix
# once from the console's own disk, then bake it into the dump and flash it:
reversefs capture /dev/sdX --fatx360-template template.bin  # read-only, validates first
reversefs dump --fatx360-disc /data/x360 --size <disk-0x130EB0000> --fatx360-template template.bin --out disk.img
reversefs flash disk.img /dev/sdX                           # size-checked, verified;
reversefs flash disk.img /dev/sdX --incremental             # data extents only (7 GB, not 250 GB)

Caution

The security sector is RSA-signed per disk (serial number included), so the template must come from the very disk the console originally used. The template identifies your hardware.

Share it only on purpose. inspect shows what it found and refuses to capture from a non-FATX disk.

A stock console checks that signature; RGH/JTAG consoles are lenient.

Unsigned HDDSS (no template) — how init/dump/flash fill the identity

Without a template, the hd table gets an unsigned HDDSS at 0x2000. This is the same block a console writes when it formats a disk itself: sector count, the stock logo, no RSA signature (the fields are documented by free60).

  • init -t fatx360 fills in the target drive's real ATA serial/firmware/model (HDIO_GET_IDENTITY ioctl)
  • dumps and served virtual disks carry a blank identity
  • flash /dev/sdX --fatx360-hddss refreshes the block in place with the real drive's identity after flashing an image

RGH consoles skip the signature check.

A stock console still demands the MS-signed original.

  • Keys: sc gc se1 se2 x1 x2
  • Modes: dump, serve, --fatx360-part/--fatx360-disc, live sync + write-back, --fatx360-template
  • Cluster size: --cluster-size overrides the 16 KiB default for bare volumes (retail partitions keep their console-verified sizes)
  • Judges: unmodified fusefatx, fsck.fatx, a real Xbox 360 (RGH3)
Other 360 media (--fatx360-table: mu / usb / kit)

Default table is hd. Alternatives:

  • mu — memory unit (sc + x2, no security prefix)
  • usb — USB storage
  • kit — devkit disk (sector-0 partition header + xdv dashboard partition + x2, both backable)

The usb image is a single file: the console's Xbox360/Data0000..NNNN files joined back-to-back. The console splits its FATX filesystem into these numbered files because a USB stick's own filesystem cannot hold one huge file.

To write it onto a stick, cut the image back into those files: Data0000 is 512 MiB, the rest are 1 GiB chunks (Data0001, Data0002, …).

Only the x2 partition can be filled from a directory. The cache partitions cannot — their nominal extents overlap by design, so they are served as empty formatted volumes.

# 256 MiB memory-unit image, judged by unmodified fusefatx `-b mu`:
reversefs dump --fatx360-table mu --fatx360-disc /data/mu --size $((256*1024*1024 - 0x7FF000)) --out mu.img

# devkit disk with a dashboard partition:
reversefs dump --fatx360-table kit --fatx360-disc /data/kit --size <disk-0xC6600000> --out kit.img

FATX-OG — Xbox OG (--fatxog-part / --fatxog-disc)

Little-endian FATX, libfatx conventions.

Retail partitions are x y z c e f. The F and G partitions have a variable size (the others sit at fixed offsets), so their layout is written to sector 0 as an XBpartitioner table — what tools and LBA48-aware BIOSes read to see the custom F/G sizes.

reversefs serve-ublk --fatxog-disc /data/xboxog --size 40G --dev 0
  • Modes: dump, serve, --fatxog-part/--fatxog-disc, live sync + write-back
  • Judges: mborgerson fatxfs (via the partition table), reversefs mount as the in-tree consumer check

FAT32 / FAT16 / FAT12 — DOS & removable media (--fat32/16-part, --fat32/16/12-disc)

The classic FAT family with full VFAT long names (Unicode both ways).

  • FAT32/16: up to 4 MBR primary partitions (types 0x0C/0x06), one directory each
  • FAT12: a single bare superfloppy with canonical BPB geometry for 360K/720K/1200K/1440K/2880K (real DOS and Gotek-style hardware read those fields)
  • File-size ceiling: 4 GiB−1 (format limit; larger files are skipped with a warning)
reversefs serve-ublk --fat32-part /data/music=32G --fat32-part /data/docs=4G --dev 0
sudo partprobe /dev/ublkb0 && sudo mount -t vfat -o utf8 /dev/ublkb0p1 /mnt
reversefs dump  --fat16-part /data/dos=512M --out dos.img
reversefs dump  --fat12-disc /data/floppy=1440K --out floppy.img
# superfloppy: ONE bare volume covering the whole device, no MBR:
reversefs dump  --fat32-disc /data/stick=8G --out stick.img
reversefs serve-ublk --fat16-disc /data/dos=256M --dev 0  # mount /dev/ublkb0 directly

Tip

When mounting vfat, always pass -o utf8. Without it the kernel byte-encodes long names and Unicode comes back as mojibake.

--atari switches to Atari ST conventions:

  • the FAT12 floppy gets the TOS executable-boot checksum (0x1234 over big-endian words; pair it with --fat12-bootcode)
  • FAT16 disks get an AHDI root sector (GEM/BGM entries, big-endian) instead of a DOS MBR

That is what TOS drivers and Hatari parse.

PC tooling still reads the filesystems themselves.

  • Modes: dump, serve, live sync + write-back, boot options (below)
  • Judges: kernel vfat, dosfstools (fsck.fat), mtools, file(1)

exFAT — modern removable media (--exfat-part)

Modern removable-media format, MBR type 0x07, full entry-set metadata (boot-region & set checksums, allocation bitmap, compressed up-case table).

Native 64-bit sizes. Files over 4 GiB are verified (5 GiB E2E, md5-checked).

reversefs serve-ublk --exfat-part /data/video=64G --dev 0
sudo partprobe /dev/ublkb0 && sudo mount -t exfat /dev/ublkb0p1 /mnt
  • Modes: dump, serve, live sync + write-back
  • Judges: kernel exfat, fsck.exfat (exfatprogs), exfat-fuse

ISO9660 + Joliet + Rock Ridge — virtual CD-ROM (--iso9660-disc)

A directory served as a CD image:

  • PVD + Joliet (full Unicode names for Windows)
  • Rock Ridge (SUSP/RRIP: case-sensitive names and POSIX mode/uid/gid for Linux/macOS/retro Unixes)
  • path tables
  • optional El Torito boot for BIOS and/or UEFI (a hybrid disc boots both — QEMU/OVMF-verified)

Read-only by nature: no write-back.

Source-side changes refresh the disc live.

reversefs dump  --iso9660-disc /data/cd --iso9660-label MY_CD --out cd.iso
reversefs serve-ublk --iso9660-disc /data/cd --size 700M --dev 0  # mount -t iso9660 -o ro

# bootable (BIOS payload = no-emulation code; EFI payload = FAT image with BOOTX64.EFI):
reversefs dump --iso9660-disc /data/cd --iso9660-boot isolinux.bin --iso9660-boot-efi espfat.img --out boot.iso
  • Modes: dump, serve (RO), live source→ISO refresh, El Torito BIOS/UEFI
  • Judges: kernel isofs, isoinfo, 7z, QEMU (SeaBIOS + OVMF)

GameCube — Nintendo mini-DVD (--gc-disc)

A directory in wit's extract layout (sys/boot.bin, bi2.bin, apploader.img, main.dol + files/ tree) served as a GC mini-DVD: boot header layout words rewritten, big-endian FST rebuilt from the tree.

Consumers: Dolphin, GC Loader, Swiss.

Wii: existing ISOs (and .wbfs containers — parsed natively) serve byte-exact via --raw-disc.

reversefs dump      --gc-disc /data/gcgame --out game.gcm
reversefs serve-nbd --raw-disc game.wbfs --listen 0.0.0.0:10809
  • Judge: wit (Wiimms ISO Tool) — list + extraction round-trip, and a WBFS readback that equals wit's own conversion

XDVDFS / XISO — Xbox & Xbox 360 game disc (--xdvdfs-disc)

A directory served as an Xbox game-disc image (the format both the Xbox OG and the Xbox 360 use — not ISO9660: MICROSOFT*XBOX*MEDIA descriptor at sector 32, binary-search-tree directory tables, contiguous file extents).

Read-only by nature.

Source-side changes refresh the disc live.

Consumers: xemu/Cxbx-Reloaded, ODE boards (via the image), extract-xiso-compatible tools.

Note

For a retail 360 console an ODE (or a drive-emulation mod) is still required. The console won't accept a burned or plain disc without one.

reversefs dump  --xdvdfs-disc /data/game --out game.iso
reversefs serve-nbd --xdvdfs-disc /data/game --listen 0.0.0.0:10809
  • Modes: dump, serve (RO), live source→XISO refresh
  • Judge: extract-xiso (XboxDev) — list + extraction round-trip, byte-exact

Amiga FFS — AmigaDOS volumes (--affs-disc)

AmigaDOS Fast File System (DOS\1): ADF floppies (880K/1760K) and HDF hard files of any size (bitmap-extension chains implemented — no 49 MiB cap).

Boot block injectable with the checksum recomputed.

reversefs dump  --affs-disc /data/amiga --size 1760K --out disk.adf --affs-label WORKBENCH
reversefs serve-ublk --affs-disc /data/amiga --size 200M --dev 0  # mount -t affs

--affs-rdb wraps the volume in a Rigid Disk Block partition table (RDSK+PART in cylinder 0).

Real Amigas, WinUAE and partprobe see a bootable hard disk instead of a bare volume.

  • Modes: dump, serve, live sync + write-back (stable rebuilds), --affs-bootblock
  • Judges: xdftool (amitools), kernel affs, rdbtool (RDB parse + export)

Classic Mac HFS — System 6/7 Macintosh (--hfs-disc)

System 6/7-era HFS (not HFS+): MDB, volume bitmap, catalog B*-tree with a multi-level index (hundreds of entries verified), contiguous forks.

Names are ASCII ≤ 31 chars (period-correct).

Resource forks are empty.

reversefs dump  --hfs-disc /data/mac --size 32M --out mac.img --hfs-label MACDISK
reversefs serve-ublk --hfs-disc /data/mac --size 32M --dev 0  # mount -t hfs
  • Modes: dump, serve, live sync + write-back (stable rebuilds)
  • Judges: hfsutils (hmount/hls/hcopy), kernel hfs

PlayStation 2 HDD — APA/PFS + HDL games (--ps2-part / --ps2-hdl)

A full APA-partitioned PS2 disk: the standard __mbr/__net/__system/__sysconf set plus your PFS partitions (sizes round to power-of-two MiB, min 128M).

Serves files to OPL/uLaunchELF-style homebrew.

Booting the console itself additionally needs Sony __mbr content (FMCB setups).

Files scattered across many contiguous extents ("runs"; 1.5 GiB md5-verified) and multi-zone bitmaps are supported, so game-sized files just work (per-file ceiling ~28 GiB — the inode segment table holds up to 113 runs).

HDL game partitions (--ps2-hdl "IMAGE=TITLE", repeatable): a PS2 disc image becomes a type-0x1337 partition that OPL and HDLoader list and boot.

This is the layout HD Loader introduced (header at +0x1000/+0x101000, data 4 MiB in).

The boot file name is read from the disc's own SYSTEM.CNF; the image may be plain .iso or .zst/.gz/.cso/.zso (decompressed on the fly).

Combined with serve-udpbd, a real PS2 boots games straight over the LAN.

reversefs dump  --ps2-part /data/opl=+OPL=512M --size 40G --out hdd.img
reversefs serve-ublk --ps2-part /data/opl=+OPL=512M --size 40G --dev 0

# HDL games served straight to a real PS2 over the LAN:
reversefs serve-udpbd --ps2-hdl "or2006.iso.zst=OutRun 2006" --ps2-hdl "gt4.cso=GT4" --size 120G --listen 0.0.0.0:48573
  • Modes: dump, serve, live sync + write-back (PFS); HDL partitions are RO
  • Judges: pfsshell (APA/PFS), hdl_dump (HDL TOC/list/extraction round-trip)

NTFS — Windows & PS3 homebrew (--ntfs-part)

NTFS 3.1 built against an mkfs.ntfs oracle:

  • full MFT with fixups
  • $I30 directory B-trees — resident, INDX chains and multi-level (15 000 files in one directory verified)
  • the complete system-file roster: $MFT…$Extend, $Secure streams, $UpCase

Full write-back: the reconcile walks the effective MFT (the format's source of truth) rather than the index B-trees, so consumer-created files, directories and edits converge to your directory with the usual safety mechanisms.

Use cases: Windows readers/writers and PS3 homebrew (webMAN/multiMAN read NTFS USB disks).

reversefs dump  --ntfs-part /data/share=2G --out ntfs.img
reversefs serve-ublk --ntfs-part /data/share=2G --dev 0
sudo partprobe /dev/ublkb0 && sudo mount -t ntfs3 /dev/ublkb0p1 /mnt
  • Modes: dump, serve, live sync + write-back (stable rebuilds)
  • Judges: ntfs-3g (ntfsls/ntfscat, FUSE mount ro AND rw), kernel ntfs3

Bare FATX volume — memory cards & test images (--fatx360-volume / --fatxog-volume)

One FATX volume covering the whole device, no tables.

The console flavor sits in the flag name.

--fatxog-volume is the exact shape of an Xbox OG memory card (~8M; xemu mounts such images).

The 360 form is tool-facing (fusefatx --table file, test images).

Raw image passthrough — existing & compressed images (--raw-disc)

Serves an EXISTING disk/disc image byte-exact over any transport, with a session CoW overlay (the image is never modified).

The source may be a plain file or a compressed .zst/.gz/.cso/.zso (or a zip/tar holding one image) decompressed on the fly.

A game.iso.zst then plugs straight into NBD/iSCSI/USB-gadget.

dump decompresses to --out (doubles as a converter).

reversefs serve-nbd --raw-disc disk.img.zst --listen 0.0.0.0:10809
reversefs dump      --raw-disc game.cso --out game.iso

Write semantics are selectable:

Mode Flag Behavior
Session overlay (default) writes gone on restart, image pristine
Write into the image --raw-writable writes INTO the plain image file (qemu-nbd style — e.g. the target disk of an OS installation)
Persistent side overlay --raw-overlay FILE image stays pristine, writes persist in a side file (works over compressed bases too; per-consumer overlays over one shared base; deleting FILE+FILE.map = factory reset)

Both RW modes drop the NBD read-only flag automatically.

# diskless Windows install target served to an IDE head:
reversefs serve-nbd --raw-disc disk-c.img --raw-writable --listen :10811
# shared pristine base, per-console persistent state:
reversefs serve-nbd --raw-disc baza.img.zst --raw-overlay salon.ovl --listen :10812

Archive sources (.zip / .tar / .tar.zst)

Anywhere a source directory is accepted (any -disc/-volume path, --fat32-part, --ntfs-part, --fatx360-part …=x2, --ps2-part), you can pass an archive file instead.

It is indexed once and streamed on demand, never unpacked to disk.

The served disk is forced read-only for that source (consumer writes stay in the session overlay and vanish on restart, the archive is never modified).

reversefs serve-nbd --xdvdfs-disc game.zip --listen 0.0.0.0:10809
reversefs serve-ublk  --fat32-part game.zip=2G --dev 0
reversefs dump    --ntfs-part backup.tar.zst=8G --out disk.img
How each container is read (random-access strategy)
Container Random access
ZIP, stored direct pread at the member's byte range
ZIP, deflate live inflate cursor per entry + decompressor-state checkpoints (~44 KiB each, every 8 MiB) — backward seeks resume from the nearest checkpoint; RAM-bounded, no disk spill
TAR direct pread (members are contiguous)
tar.gz / .gz shared deflate cursor + checkpoints (single-member gzip)
CSO/ZSO (PSP/PS2 scene disc images) native block index: one block decompress per read + LRU
tar.zst / .zst, seekable format (t2sz, zstd seekable API) frame table → decompress one frame per read, small LRU cache
tar.zst / .zst, plain stream restartable sequential cursor; backward seeks re-decompress from the start (a warning suggests the seekable format)

Notes: encrypted zip entries and non-store/deflate methods are skipped with a warning.

FATX layouts apply their usual name mangling to archive entries.

Editing the archive while serving rebuilds the volume live (same watcher as directory sources).


Booting

Boot material is supplied as files and injected identically on every start.

Important

Bootloader installers that write sector blocklists from inside a mounted consumer persist only for the session by default.

Use --boot-state to make them survive restarts.

UEFI is immune by design.

--boot-mbr mbr440.bin                # BIOS code in sector 0 (our partition table kept)
--boot-gap core.img                  # post-MBR gap (GRUB core.img), sector 1..1MiB
--boot-active N                      # bootable flag (default 1 with --boot-mbr)
--boot-esp N                         # partition type 0xEF = EFI System Partition
--fat12-bootcode boot.bin            # floppy boot sector code (our BPB kept)
--affs-bootblock bb.bin              # Amiga bootblock (checksum + root ptr recomputed)
--iso9660-boot / --iso9660-boot-efi  # El Torito (see the ISO section)

All verified end-to-end in QEMU: BIOS and UEFI, from disk and from CD.

Persistent boot state (--boot-state DIR)

By default consumer writes outside the synthesized structures (MBR boot code written by an installer, the post-MBR gap, sector-blocklist placements) live only for the session. --boot-state makes them survive restarts:

sudo reversefs serve-ublk --fat32-part /data/pc=8G --boot-state /var/lib/reversefs/pc --dev 0
  • sectors written outside partitions are saved on every flush and reloaded at start
  • file→sector placements are saved at teardown and pinned on the next start, so blocklists written by syslinux/GRUB installed inside a mounted consumer stay valid across restarts
Foreign takeover (phase 2): a consumer reformats a partition to a foreign filesystem

If a consumer reformats a partition to a foreign filesystem (mkfs.ext4, a dd'd image, an OS installer), reversefs detects it at the next flush, stops synthesizing/reconciling that partition and persists its raw sectors to blob-<key>.img in the state directory.

The next start serves the foreign filesystem back byte-perfectly (fsck.ext4 + debugfs-verified).

Your source directory is never touched.

It simply stops being served for that partition.

Delete the takenover-<key> marker (and the blob) to re-synthesize from your directory again.

Other partitions of the same disk keep their live sync.

Example: a VM whose system partition is an installed OS blob while its data partition stays a live view of a host folder.

Detection triggers on boot-sector reformats and on Linux swap signatures (mkswap leaves sector 0 alone — an installer-made swap partition persists too, UUID included).

Stray writes inside a partition without any reformat remain session-only.

The device advertises DISCARD: fstrim or swapon --discard in the consumer punches sparse holes in blobs and frees overlay sectors, so a taken-over partition's disk usage shrinks back without a manual reset.


Transports (network, USB, edge heads)

The same virtual disk is served over many transports.

Pick by where the consumer lives.

Booting real hardware from the LIVE device (iSCSI + iPXE)

A physical machine cannot see /dev/ublkbN directly, but it can boot from it over the network.

One-time setup on the reversefs host (package tgt):

sudo reversefs serve-ublk --fat32-part /data/pc=8G --boot-mbr mbr440.bin --dev 0 &

sudo tgtadm --lld iscsi --op new --mode target --tid 1 -T iqn.2026-07.local:reversefs
sudo tgtadm --lld iscsi --op new --mode logicalunit --tid 1 --lun 1 -b /dev/ublkb0
sudo tgtadm --lld iscsi --op bind --mode target --tid 1 -I ALL

Boot the target machine with iPXE (USB stick from ipxe.org or PXE chain) and at its prompt:

dhcp
sanboot iscsi:<host-ip>::::iqn.2026-07.local:reversefs

The firmware attaches the iSCSI LUN as its boot disk.

BIOS or UEFI proceeds exactly as with local media, and every sector it touches is served (and reconciled) live by reversefs.

Serving over the network (NBD)

serve-nbd exports the same virtual disk directly over TCP: no root, no ublk, no kernel requirements on the server side.

It also runs where serve-ublk cannot (old kernels, ARM boards, containers):

reversefs serve-nbd --ntfs-part /data/share=64G --listen 0.0.0.0:10809
# on any Linux client:
sudo modprobe nbd max_part=8
sudo nbd-client <host> 10809 /dev/nbd0  # busybox: nbd-client -b 512 <host> 10809 /dev/nbd0
sudo mount /dev/nbd0p1 /mnt
# or rootless, from qemu tooling:
qemu-img info nbd://<host>:10809

The protocol is fixed-newstyle NBD with FLUSH (reconcile to ext4), TRIM (discard) and multi-connection support.

Both plain NBD_OPT_EXPORT_NAME clients (busybox) and NBD_OPT_GO clients (nbd-client, qemu, the kernel) negotiate.

Every clean disconnect also reconciles, so consumer writes are never lost on nbd-client -d or a guest shutdown.

Note

Throughput is link-bound: the server sustains ~2.8 GB/s reads and ~1 GB/s writes over loopback, an order of magnitude above 1GbE (117 MB/s) and 2.5GbE (295 MB/s) line rates.

  • Judge: scripts/nbd-qemu-judge.sh (rootless — qemu-img byte-exactness plus a real nbd.ko kernel client in a QEMU guest)

PS2 over the LAN (UDPBD)

serve-udpbd speaks the PS2 scene's UDP Block Device protocol (by Rick Gaiser) natively.

OPL/Neutrino's BDM: UDP device driver connects a real PlayStation 2 with a network adapter straight to the reversefs disk, no PC-side kernel pieces, no image files:

reversefs serve-udpbd --ps2-part /data/ps2/opl=+OPL=4G
# PS2 side: OPL/Neutrino with udpbd BDM driver, point it at this host's IP

Discovery (INFO broadcast), reads as RDMA packet streams (block size auto-tuned per request like the reference server) and writes with WRITE_DONE acks are implemented.

Every packet fits a standard 1500 MTU.

The protocol has no flush command, so reconcile rides the live-sync debounce (or completes after each write sequence when --sync-debounce-ms 0).

Serve read-only (:ro/--read-only) if the PS2 should never modify your library.

Network boot as an iSCSI target (iSCSI)

serve-iscsi is a built-in software iSCSI target (RFC 7143 subset: one LUN, no CHAP). It is:

  • the rootless replacement for the tgtadm-over-ublk recipe above
  • the transport iPXE can boot real hardware from (iPXE speaks iSCSI, not NBD)
reversefs serve-iscsi --ntfs-part /data/win=64G --listen 0.0.0.0:3260 --iqn iqn.2026-07.dev.reversefs:disk

# iPXE prompt on the target machine:
#   dhcp
#   sanboot iscsi:<host-ip>::::iqn.2026-07.dev.reversefs:disk

# Linux initiator:
sudo iscsiadm -m discovery -t st -p <host-ip>
sudo iscsiadm -m node -T iqn.2026-07.dev.reversefs:disk -p <host-ip> --login

# rootless check:
qemu-img info iscsi://<host-ip>:3260/iqn.2026-07.dev.reversefs:disk/0

SCSI SYNCHRONIZE CACHE reconciles to the source, and so does every logout/disconnect.

  • Judge: qemu's libiscsi initiator in cargo test (tests/iscsi_e2e.rs)

Raw Ethernet, no IP (AoE)

serve-aoe is an ATA-over-Ethernet target: pure layer-2 frames (ethertype 0x88A2), no IP/TCP anywhere.

That makes the CONSUMER side as simple as a block transport gets.

An FPGA or MCU head needs a MAC framer and a 36-byte header parser, not a network stack.

The Linux kernel ships a ready initiator (aoe.ko) and iPXE boots from aoe:eN.M.

Warning

Same L2 segment only (not routable).

The AF_PACKET socket needs root/CAP_NET_RAW on both ends.

sudo reversefs serve-aoe --fat32-part /data/files=8G --interface eth0     --aoe-shelf 0 --aoe-slot 0
# any Linux box on the same wire:
sudo modprobe aoe && sudo mount /dev/etherd/e0.0p1 /mnt
# iPXE: sanboot aoe:e0.0

Discovery (Query Config broadcast), IDENTIFY with LBA28+LBA48 geometry, sector reads/writes and FLUSH CACHE (→ reconcile) are implemented.

The per-request sector budget is derived from the interface MTU and advertised to the initiator (jumbo frames raise throughput).

Judged by protocol unit tests.

Kernel aoe.ko is the live judge.

USB drive mode and local NBD attach (USB + NBD)

On a board with a USB device controller, reversefs can BE the USB drive.

Suitable boards include the Raspberry Pi Zero / Zero 2 / 4 / 5, on the port that supports OTG/device mode.

Start it, plug the cable into a PC, console or TV, and the served disk is detected like ordinary removable media:

# a Pi Zero as a 64 GB exFAT USB stick backed by a directory:
sudo reversefs serve-usb --exfat-part /data/share=64G
# console game library, write-protected:
sudo reversefs serve-usb --fat32-part /data/games=32G --read-only
# a virtual USB DVD DRIVE (El Torito-bootable by any machine that boots
# from USB optical media - OS installs without burning a disc):
sudo reversefs serve-usb --raw-disc installer.iso --usb-cdrom

--usb-cdrom flips the LUN to the CD/DVD-ROM class (the kernel's f_mass_storage answers the MMC command set incl. READ TOC): hosts see an optical drive, BIOSes boot it via El Torito.

Implies read-only.

Pair with a bootable ISO (--raw-disc x.iso, also .zst-compressed, or a live --iso9660-disc ... --iso9660-boot ...).

Works with every backing: nbd/ublk use the kernel's MMC implementation, ffs speaks our own (READ TOC, GET CONFIGURATION, event polling).

Even kernels without nbd.ko/ublk can then be a bootable USB DVD drive.

--usb-backing selects the machinery feeding the kernel gadget:

backing path requirements
nbd (default) loopback NBD server + kernel client (/dev/nbdX) → f_mass_storage any kernel (nbd.ko is ancient), root
ublk /dev/ublkbNf_mass_storage kernel ≥ 6.0 with CONFIG_BLK_DEV_UBLK
ffs FunctionFS — our own SCSI/Bulk-Only Transport in userspace, no block device at all any kernel with configfs+FunctionFS

The kernel NBD client attach is also available standalone: serve-nbd --attach exposes the disk as a local /dev/nbdX on kernels too old for ublk.

It is a drop-in alternative to serve-ublk:

sudo reversefs serve-nbd --ntfs-part /data/share=64G --attach
sudo partprobe /dev/nbd0 && sudo mount /dev/nbd0p1 /mnt

Ctrl-C detaches/unbinds cleanly and runs a final reconcile.

Consumer writes are never lost, even if the host never ejected.

NAS and edge heads (NBD + HTTP)

The synthesis brain and the physical connector do not have to share a box.

Run reversefs on the NAS as the block server, and again on a small board near the console as a thin head that only moves sectors:

# NAS (holds the sources, does all the filesystem work):
reversefs serve-nbd --ps2-hdl "gt4.cso=GT4" --size 120G --listen 0.0.0.0:10809
# Pi Zero 2 W plugged into the console's USB port:
sudo reversefs serve-usb --connect nbd://nas:10809 --usb-backing ffs

--connect skips the local build entirely.

The head needs no sources, no layout flags and no storage.

  • With the default nbd backing the kernel NBD client attaches straight to the remote export (kernel readahead and request merging work against the network).
  • The ffs backing uses our userspace NBD client instead. It is the only option on kernels without nbd.ko, with no block device anywhere in the path.
    • That client reconnects by itself: a network blip or a NAS restart stalls the pending request, retries with a capped backoff and replays it on a fresh connection. The USB host never sees an I/O error.
    • An export whose size changed is refused, because that is a different disk, not a blip.

Inherently read-only media (ISO/XISO/GC/raw and archive sources) advertise the NBD read-only flag, so thin clients can drop their whole write path.

The server itself stays lenient (writes still land in the session overlay).

Edge cache (--cache-dir): stream from local storage after the first run

The head keeps fetched blocks in a local shadow file (SD/NVMe — or tmpfs for a RAM cache), so the game you are playing streams from local storage after the first run.

Freshness is journal-driven: the NAS side adds --status-listen :10810 (and optionally --status-state FILE to survive restarts).

Every content change bumps a generation with its byte ranges.

The head polls /dirty?since=G and invalidates only those ranges, so adding a game to a 16 TB library does not re-download the 2 TB you have cached.

Writes are chunk-aligned (1 MiB) and each unique chunk is written once until invalidated, so SD wear is minimal.

Eviction is LRU via hole-punching.

A foreign NBD server (no journal) gets no persistent cache unless you declare --cache-immutable for frozen content.

Needs the ffs/ublk backing (with nbd the data bypasses this process — the kernel page cache applies instead).

# NAS:
reversefs serve-nbd ... --listen 0.0.0.0:10809 --status-listen 0.0.0.0:10810

# head (2 TB NVMe cache over a 16 TB library):
sudo reversefs serve-usb --connect nbd://nas:10809 --usb-backing ffs --cache-dir /mnt/nvme/rfs-cache --cache-size 2T

Tip

Building an MCU head? The byte-exact NBD client blueprint (handshake, frames, readahead guidance, lwIP skeleton) lives in docs/esp32-nbd-client.md.

For microcontroller heads (ESP32-class) and anything else that speaks HTTP, serve-http exports the same device as one file with Range support.

It is the lowest-effort client protocol there is (read-only by nature):

reversefs serve-http --raw-disc game.iso.zst --listen 0.0.0.0:8080
curl -r 0-511 http://nas:8080/disk.img | xxd  # any sector, any client

Mounting & real-disk tools

The other direction: the same byte-exact format knowledge applied to EXISTING disks and images.

Examples: console drives on a USB adapter, dumps, memory units.

No external dependencies (no libfuse, no dd, no python).

Mounting real disks (reversefs mount)

A FUSE driver for existing volumes.

The mounting layer is format-agnostic (one MountableFs backend per format under fsops/).

The format currently mountable is FATX/XTAF, for both Xbox consoles:

  • big-endian XTAF = Xbox 360
  • little-endian FATX = Xbox OG

The variant is auto-detected from the superblock magic.

Requires the fuse cargo feature (default) and fusermount3.

reversefs mount --list /dev/sdX                               # what is mountable (key, type, offset, cluster)
mkdir /mnt/xbox
reversefs mount --all /dev/sdX /mnt/xbox                      # every partition: /mnt/xbox/<key>/
# Ctrl-C unmounts everything.

reversefs mount image.fatx /mnt/p                             # bare image (offset 0)
reversefs mount -t fatx360 /dev/sdX /mnt/p                    # 360 retail disk (-p x2 default)
reversefs mount -t fatx360 -p gc /dev/sdX /mnt/p              # other partitions: sc gc se1 se2 x1 x2
reversefs mount -t fatxog -p c /dev/sdX /mnt/p                # Xbox OG C: drive
reversefs mount -t mu /dev/mmcblk0 /mnt/p                     # 360 memory unit
reversefs mount -t kit -p xdv /dev/sdX /mnt/p                 # devkit dashboard partition
reversefs mount --offset $((0x130EB0000)) -r /dev/sdX /mnt/p  # any offset, read-only
fusermount -u /mnt/p                                          # unmount

-t/--type is the partition table type (like mount -t): file, fatx360, fatxog, mu, usb (the Data0000..NNNN concatenation), kit.

It is inferred from -p where unambiguous. -r/--read-only mounts read-only; --sector-size 4096 targets Xbox 360 "XL" disks.

Important

Consistency guarantees (designed after a real-world incident where an older driver accepted unlimited writes, df went negative and the console received corrupted data):

  • a single cluster-allocation path fails with ENOSPC before mutating anything;
  • free space is always derived from the FAT (used = total − free cannot go negative);
  • writes are write-through (a crash never loses acknowledged data);
  • freshly allocated clusters are zeroed.

The test suite proves it over real mounts: fill-to-capacity, write/delete cycles without leaks, remount md5 round-trips, cross-validation with the C++ fsck.fatx/mkfs.fatx.

Formatting and initializing (mkfs / init)

# format ONE partition (the mkfs.fatx role) — retail partitions get their
# console-verified cluster sizes (sc 32K, gc 64K, rest 16K):
reversefs mkfs -p x1 /dev/sdX                            # asks for confirmation
reversefs mkfs --variant fatxog image.img                # bare volume at offset 0

# initialize a WHOLE disk from zero — full retail layout, foreign partition
# tables erased (MBR + primary AND backup GPT, so nothing resurrects a
# stale GPT), fatxog gets an XBpartitioner table in sector 0 plus the RFRB
# refurb sector (fresh boot counters, like the retail formatter). fatx360 also
# gets an unsigned HDDSS with the drive's real ATA identity — what a console
# writes when it formats a disk itself (enough for RGH consoles):
reversefs init -t fatx360 /dev/sdX
reversefs init -t fatxog  /dev/sdX

# a fatx360 disk a STOCK console accepts without reformatting needs the
# MS-signed security sector captured from the console's own disk:
reversefs capture /dev/sdY --fatx360-template hddss.bin  # once, from the real disk
reversefs init -t fatx360 --fatx360-template hddss.bin /dev/sdX

Warning

Both operations are destructive and ask for confirmation (--yes when scripting).

Judged by the independent implementations: C++ fsck.fatx/fusefatx mount and write the initialized 360 disks, mborgerson fatxfs mounts and writes the OG ones.

Inspecting, capturing and flashing (inspect / capture / flash)

reversefs inspect /dev/sdX                                    # read-only: tables, unified partition list, hidden regions, verdict
reversefs capture /dev/sdX --fatx360-template t.bin           # extract the fatx360 template
#   --fatx360-cache-headers adds the sc/gc superblocks (sparse; rarely needed),
#   --force extracts fixed-extent artifacts even when detection says "not there"
reversefs capture /dev/sdX --boot-mbr m.bin --boot-gap g.bin  # GRUB from a real disk
reversefs flash disk.img /dev/sdX                             # size-checked image write + verify
reversefs flash disk.img /dev/sdX --incremental --previous old.img
reversefs flash /dev/sdX --boot-mbr m.bin                     # patch ONE region, touch nothing else

inspect — read everything on the disk

inspect reports everything it recognizes (a disk reformatted several times legitimately carries leftovers of more than one format), in one unified view:

  • every partition table (MBR, GPT, XBpartitioner, the fixed Xbox layouts) with a merged partition list showing each entry's origin and content
  • hidden regions (MBR boot code, post-MBR gap, the Xbox 360 security sector and sequential table)
  • an identity verdict and the list of fragments capture can extract

Filesystems reversefs does not synthesize (ext2/3/4, LUKS, LVM2, swap, btrfs, XFS, squashfs, md-RAID) are recognized as a foreign tier.

They are reported with label and size, never mounted or served.

capture — extract named fragments

capture extracts named fragments.

Its flags reuse the synthesis vocabulary, so the loop closes naturally: what capture --boot-mbr reads from a real disk is exactly what dump --boot-mbr bakes into a synthesized one and flash --boot-mbr patches back onto hardware.

Artifacts: --fatx360-template, --boot-mbr, --boot-gap, --fat12-bootcode, --affs-bootblock, --iso9660-boot, --iso9660-boot-efi (El Torito payloads).

Detection gates every extraction.

Artifacts the disk does not carry are refused.

flash — image write and fragment patching

flash refuses a size mismatch outright (FATX/FAT geometry is derived from the device size) and verifies the write afterwards (full mode: prefix plus sampled extents; incremental: the data extents).

One fragment sources from the disk instead of a file: flash /dev/sdX --fatx360-hddss rebuilds the unsigned security sector in place from the drive's own ATA identity and size.

Run it after flashing a dumped image so the HDDSS carries the real serial.

In fragment mode (flash DISK --boot-mbr FILE …, no image argument) only the named regions are written: bounded, read-verified, and merged where the format demands it.

Merging keeps whatever the format needs from the disk: FAT12 keeps the disk's BPB, AFFS keeps the disk's DOS type and root pointer and recomputes the checksum.

The partition table and all data stay untouched.

--incremental writes only the image's data extents.

It is safe even as the first write onto a used disk, as long as the image comes from reversefs dump.

dump materializes every byte a consumer can parse: filesystem structures, whole FATs, allocated clusters including their zero blocks.

The image's holes therefore lie only where no reader ever looks.

One 7 GB game on a 250 GB layout flashes in the time of 7 GB, not of 250 GB of zeros.

The stale bytes left in the holes are unreachable through the filesystem.

Caution

Two exceptions need a full flash: output of dump --raw-sparse and foreign sparse images of unknown provenance.

Their holes may sit inside meaningful data (covered by the dirty_flash_e2e test suite).

Where inspect answers "what is on this disk?", mount --list answers "what can I mount and how?".

Flashing onto a device served by reversefs itself (the overlay, imports, safety valve)

A device served by reversefs itself (/dev/ublkbN, /dev/nbdX via serve-nbd --attach) is a regular block device, so nothing stops you from pointing flash at it.

Know what that means:

  • The bytes land in the session write overlay (a sparse spill file), not on any real disk. The device stays a synthesis of the source directories, and the overlay evaporates at teardown.
  • If the flashed image matches the served layout and geometry (in practice: an image made by reversefs dump with the same partition specs), live sync parses it and reconciles the files into the source directory. This is an import, in which the image becomes the truth, so files absent from it are removed from the source.
  • A foreign or mismatched image (a different filesystem, or FAT32 formatted by another tool with different geometry — reconcile parses with the model's geometry, not the flashed BPB) reads as garbage and trips the safety valve: nothing is deleted, a warning is logged.
  • This also does not trigger the --boot-state takeover. That detector looks for unrecognizable content (no 55AA, or an ext4 superblock), not for a valid image of the wrong flavor.

Prefer --incremental here: a full flash writes every byte and grows the overlay spill to the full device size.

To extract files from a foreign image, mount it directly instead (kernel loop mount, or reversefs mount for FATX).

Under the hood

Architecture: consumers, transports, shared backend, block layer, format synthesizers, sources

Layer Module Role
Transport transport/backend transport-independent device construction (CLI specs → shared backend + live-sync threads)
Transport transport/ublk binds a built device to /dev/ublkbN (io_uring); Linux ≥ 6.0, cargo feature ublk (default)
Transport transport/nbd NBD server over std::net TCP (fixed-newstyle; FLUSH→reconcile, TRIM→discard, multi-conn, read-only flag for RO media) + userspace client (serve-usb --connect)
Transport transport/udpbd UDPBD server (PS2/OPL "UDP Block Device" v2, port 0xBDBD)
Transport transport/iscsi software iSCSI target (RFC 7143 subset, one LUN, no CHAP); iPXE-bootable
Transport transport/usb USB gadget: configfs + f_mass_storage (disk or --usb-cdrom optical) or pure-userspace FunctionFS with our own SCSI/BOT + MMC
Transport transport/aoe AoE target (ATA over Ethernet, raw L2 frames — FPGA/MCU-simple consumers; kernel initiator aoe.ko, iPXE sanboot aoe:eN.M)
Transport transport/http read-only HTTP/1.1 export with Range — the lowest-effort client protocol
Transport transport/changelog generation counter + dirty-range journal (--status-listen) driving edge-cache invalidation
Fleet supervisor up/down: one child process per disk from a YAML config, restart policies, ordered FUSE mounts: phase, pid-file lifecycle
Block blockdev overlay (CoW), multi (partition routing, gap segments), sync (debounce + SelfPaths), state (boot state), blockcache (edge cache), rawimg (passthrough/writable/overlay)
Formats formats/fatx(+360/og), formats/fat32(12/16/32+Atari), exfat, ntfs, iso9660, xdvdfs, gc, affs(+RDB), hfs, ps2(APA/PFS+HDL) one directory per format: volume model, serializers, parser, reconcile, device, sync
Source source directory walker (Unicode or FATX-mangled) + archive (zip/tar/gz/zst/cso/wbfs streamed without unpacking)
Tools tools/inspect, tools/capture, tools/flash read-only detection (tables, foreign tier, GPT, verdict); fragment extraction mirroring the synthesis flags; size-checked image flashing (full/incremental) and fragment patching, both read-verified
Mounting fsops/ operations on existing volumes, one dir per format, one file per operation: vfs (backend trait), fuse (protocol shell, the only fuser user — cargo feature fuse), list (mount probe), fatx/{state,mount,mkfs,init}

Adding a format follows a fixed pattern: a new src/formats/<fs>/ directory with volume/reconcile/device/sync, a LayoutArg variant and --<fs>-* flags.

FUSE mounting of a format is a MountableFs impl in fsops/<fs>/.

Write path and data-safety mechanisms

Live sync internals

One background thread per backed partition.

--sync-debounce-ms tunes the reconcile debounce (0 disables).

  • Consumer→source: overlay dirty + writes quiesced for the debounce ⇒ reconcile.
  • Source→device: inotify events quiesce ⇒ model rebuilt and swapped under the device mutex (dirty overlay reconciles first, same critical section). Events caused by our own reconcile are suppressed precisely by path.

Stable allocation + host placements keep a mounted consumer coherent across rebuilds.

Format internals in brief (pointers into the code; module doc comments carry full detail)
  • FATX (src/fatx/): FATX360 big-endian (XTAF), FATX-OG little-endian (FATX). Geometry replicates fusefatx partition::setup() exactly — the data offset is not stored on disk, both sides derive it (clus_num = par_size >> clus_pow; FAT16 below 0xFFF0 clusters; fat_size always rounds UP by a full 0x1000). Superblock pad bytes and Duverger timestamps match console-formatted disks byte-for-byte; the fatx360 layout injects the security-sector prefix from --fatx360-template, fatxog writes an XBpartitioner table in sector 0.
  • FAT12/16/32 (src/fat32/): one engine, FatKind selects width; full VFAT LFNs, 8.3 aliases ~N, FSInfo+backup boot, canonical floppy BPBs; --atari adds the TOS boot checksum (0x1234) / AHDI root sector.
  • exFAT (src/exfat/): boot-region checksums, allocation bitmap, compressed up-case table (a full 128 KiB table trips a 16-bit bug in exfatprogs and fsck rejects it), entry sets with SetChecksum/name_hash, native u64 sizes.
  • NTFS (src/ntfs/): MFT records with fixups, runlists (signed LCN deltas), multi-level $I30 B-trees; reconcile walks the effective MFT — the format's source of truth — not the index B-trees, and follows consumer-grown MFT extents from record 0's own runlist.
  • ISO9660 (src/iso9660/): PVD + Joliet + Rock Ridge (SUSP/RRIP: PX/TF/NM per entry, SP+ER in the root); El Torito catalog (the validation-entry checksum matters — OVMF rejects a bad sum, SeaBIOS shrugs).
  • XDVDFS (src/xdvdfs/): MICROSOFT*XBOX*MEDIA descriptor at sector 32; directories are balanced BSTs of 14-byte entries (bytewise collation with a–z uppercased), entries never cross a 2048-byte sector; empty dir = one 0xFF sector (extract-xiso convention).
  • GameCube (src/gc/): boot.bin layout words rewritten, big-endian FST (12-byte entries + string table, case-insensitive child order), file data 0x800-aligned. Wii stays passthrough-only (AES + hash trees on disc).
  • AFFS (src/affs/): 512-byte blocks as 128 big-endian words, sum-to-zero checksums, non-intl hash directories, bitmap extension blocks (no 49 MiB cap); --affs-rdb wraps the volume in an RDSK/PART table.
  • HFS (src/hfs/): MDB + alternate, catalog B*-tree with multi-level index, records addressed from the node end; trees built with free-node slack so libhfs can insert without growing the catalog.
  • PS2 (src/ps2/): APA headers in a doubly-linked list (__mbr must carry Sony's magic or the disk won't open); PFS 8 KiB zones, SEGD inodes with up to 113 runs; --ps2-hdl adds type-0x1337 HDL game partitions in hdl_dump's exact layout.

Known limitations

Important

A mounted consumer caches on-disk structures. Source-side changes hit the block device immediately (any raw read sees them), but a filesystem driver assumes the disk cannot change underneath it and keeps directory trees, FATs and file pages in its own cache.

A consumer mounted before the change may keep showing the old state until it remounts.

Stable allocation guarantees a half-stale consumer still reads coherent data.

There is no notification channel in the block protocol.

This is inherent to emulating a disk rather than a filesystem.

  • Format-inherent ceilings (documented per format above):
    • FAT12/16/32 and ISO9660: 4 GiB−1 files per file.
    • ISO9660 is read-only.
    • FATX names are mangled to 42 chars/charset on the FATX side (reconcile maps them back, so edits and deletes land in the original source files). FATX has no on-disk volume label.
    • HFS names are ASCII ≤ 31 with empty resource forks.
    • PS2 partitions are power-of-two sized (per-file ceiling ~28 GiB — the inode segment table).
  • NTFS MFT slack: the synthesized MFT reserves generous free records, but a consumer mass-creating files beyond the slack must grow the MFT itself (the reconcile parser follows MFT growth via record 0).
  • Transports favor correctness over throughput. ublk is single-queue with a synchronous handler. The NBD server handles each connection's requests sequentially (still ~2.8 GB/s on loopback — far above GbE line rate).
  • Stray in-partition writes without any recognizable reformat live in the session overlay only (reformats to ext4/NTFS/…/swap are detected and persisted with --boot-state).

Portability

The core (all formats, dump, serve-nbd/serve-udpbd/serve-iscsi) is pure Rust and builds for aarch64-unknown-linux-gnu and armv7-unknown-linux-gnueabihf.

  • The ublk cargo feature (default) adds the live serve-ublk transport. It needs Linux ≥ 6.0 with CONFIG_BLK_DEV_UBLK.
    • On targets without it, build with --no-default-features. Live serving then goes through the network transports (plain TCP/UDP, no root).
  • The fuse feature (default) is pure Rust (no libfuse — fuser talks to /dev/fuse via fusermount3) and ARM-clean.

Prebuilt binaries: every v* tag triggers the release pipeline (.github/workflows/release.yml), which cross-builds and publishes tarballs on the GitHub Releases page:

Tarball Target Contents
x86_64-linux PC full
aarch64-linux Raspberry Pi 3/4/5 on a 64-bit OS full
armv7-linux-noublk Odroid XU4 / 32-bit Pi OS network transports + dump

Built against a glibc 2.31 sysroot, so they run on Debian 11+ / Ubuntu 20.04+ era systems.

Release flow: bump version in Cargo.toml, commit, git tag vX.Y.Z, push the tag.

Standing on the shoulders of

The mounting side of this project would not exist without:

  • fusefatx by Christophe Duverger — the original C++ FUSE driver for the Xbox 360. Its v1.19 behaviour (geometry derivation, superblock conventions) is the oracle for the FATX360 variant, and its fsck.fatx/mkfs.fatx serve as independent judges in our test suite.
  • mborgerson/fatx — the reference implementation for the Xbox OG variant (libfatx/fatxfs). FATX-OG conventions and XBpartitioner table handling were cross-validated against it, and fatxfs judges our initialized OG disks.
  • fuser — pure-Rust FUSE bindings.

About

Synthesizes a disk from your files and exposes them as complete, byte-exact block devices.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages