This project provides Windows-first userspace tooling for ext4 volumes, built on the fs-ext4 Rust library.
A Raspberry Pi root-filesystem SD card plugged into a Windows 11 ARM64
host, auto-mounted on F: by the ExtFsWatcher service and browsable in
Explorer the moment it appeared.
Scope:
- Auto-mount service -- plug an ext4 SD card or USB stick into Windows
and the
ExtFsWatcherservice mounts it on a free drive letter in the active console session. Detach and it unmounts cleanly. - Right-click verb -- "Mount as ext4" on
.imgfiles in Explorer for offline disk images. - CLI browser -- open an ext4 image or raw device; list/stat/read files without mounting. Cross-platform (macOS/Linux work too -- handy for testing).
- Setup.exe -- bundles WinFsp via a Burn bootstrapper, so end users only run one installer.
The fs-ext4 library lives at ../rust-fs-ext4/
(a sibling checkout of antimatter-studios/rust-fs-ext4,
pinned in chores.yml) and is path-depended; this crate is the distribution unit.
The Windows-driver scaffolding (SCM service, disk-arrival watcher,
WinFsp.Launcher integration, partition-table walker, raw-device I/O,
installer + CI templates) lives in
winfsp-fs-skeleton,
checked out as a sibling at ../winfsp-fs-skeleton/.
ext4-win-driver is the first consumer of that skeleton -- it was
extracted from this project once the auto-mount path was working
end-to-end. See The skeleton split below for the
boundary.
Shipped in v0.1.0:
- CLI:
info,ls,stat,cat,tree,parts,audit - MBR/GPT partition parsing (5 unit tests)
-
--part Nmounts a partition slice via the C ABI's callback mount - Win32 raw-device support (
\\.\X:,\\.\PhysicalDriveN,\\?\STORAGE#Disk#...) with sector-aligned reads for 4Kn / advanced-format drives - WinFsp read-only mount -- verified on Windows 11 ARM64 with both VHD smoke tests and a real Raspberry Pi SD card
- WinFsp read-write mount (default;
--rofor read-only) -- create/pwrite/truncate/rename/unlink/rmdir/mkdir/utimens wired through the C ABI. Streaming positional writes land in fs-ext4 viapwrite, so large-file workloads no longer round-trip the whole file. -
ExtFsWatcherSCM service -- subscribes to disk-class arrivals (GUID_DEVINTERFACE_DISK), walks the partition table directly, probes each partition for the ext4 superblock magic, and asks WinFsp.Launcher to spawn a per-partition mount in the active console session - Right-click .img -> Mount as ext4 verb registered under
HKCR\SystemFileAssociations\.img - MSI + Burn bundle that auto-installs WinFsp if missing (chained via the official WinFsp redistributable MSI)
- x64 + arm64 Setup.exe built per release tag by GH Actions
(
.github/workflows/release.yml) and attached to the release.
Not yet shipped:
- winget submission (manifests staged at
winget/v0.2.0/)
Out of scope (intentionally):
- Code-signing certificate -- the per-year cost isn't justified for an unsponsored side project. Setup.exe will continue to trip SmartScreen on first download until that changes.
Download ext4-win-driver-<ver>-<arch>-Setup.exe (arch is x64 or
arm64) from
Releases,
run it, accept the GPL-3 prompt. The installer:
- chains the WinFsp MSI if WinFsp isn't already installed,
- drops
ext4.exeandMount-Ext4.ps1intoC:\Program Files\ext4-win-driver\, - registers
ExtFsWatcheras an automatic Windows service and starts it, - registers the
ext4-mountservice-class with WinFsp.Launcher, - registers the "Mount as ext4" right-click verb on
.imgfiles.
Setup.exe /quiet works for unattended installs. SCCM/Intune deploys
should push the bare MSI separately, after pushing WinFsp.
Plug in an ext4 SD card or USB stick: nothing else needed. The service picks it up, finds the ext4 partition(s), and mounts each on a free drive letter (E: upward).
CLI (works on any host):
ext4 info <image> # volume label, sizes, features
ext4 ls <image> [path] # directory listing
ext4 stat <image> <path>
ext4 cat <image> <path>
ext4 tree <image> [--canonical] # --canonical: no inode numbers, sorted
ext4 parts <image> # MBR/GPT partition table
ext4 audit <image> # link-count + dirent integrity scan
ext4 ls <whole-disk.img> --part 1 / # browse partition 1
Manual WinFsp mount (Windows + mount feature):
ext4 mount <image> --drive X: # read-write (default)
ext4 mount <image> --drive X: --ro # read-only
ext4 mount <whole-disk.img> --drive X: --part 1 # mount partition 1
Then browse X: in Explorer, or Get-ChildItem X:\, etc. Ctrl-C to unmount.
The default for every mount path is read-write. Pass --ro per-mount,
or use one of the system-wide knobs below:
- Auto-mount service. The WinFsp.Launcher CommandLine for the
ext4-mountservice-class lives atHKLM\SOFTWARE\WOW6432Node\WinFsp\Services\ext4-mount\CommandLine(32-bit registry view). Default:mount %2 --drive %1 --part %3. Append--rothere to force every auto-mount read-only. - Right-click verb. The shortcut runs
Mount-Ext4.ps1which takes a-ReadOnlyswitch. EditHKCR\SystemFileAssociations\.img\shell\MountAsExt4\commandto add-ReadOnlyto the script invocation if you want the verb itself to default to RO. - Per-mount via PowerShell. Run
Mount-Ext4.ps1 -ImagePath disk.img -ReadOnlyfor one-off RO mounts.
The legacy --rw flag is still accepted (silently) so existing scripts
and harness.toml configs that pre-date the flip don't break.
Watch mode (foreground variant of the service, useful for dev / debugging):
ext4 watch # foreground; logs each arrival
After v0.1.0 was working end-to-end, the platform plumbing was extracted into its own crate (winfsp-fs-skeleton) so the same scaffolding can host future filesystem drivers (qcow2, ntfs, ...) without copy-paste. The boundary:
| Lives in skeleton (reusable) | Lives here (ext4-specific) |
|---|---|
service::run<B> -- SCM dispatcher + WinFsp.Launcher |
src/main.rs -- Ext4Backend impl (4 const + 1 fn), CLI dispatch |
watch::run<B> -- foreground variant |
src/cmd.rs -- info/ls/stat/cat/tree/parts/audit subcommands |
partition -- MBR/GPT parsing |
src/mount.rs -- WinFsp FileSystemContext impl, fs-ext4 callbacks |
device -- BlockSource + sector-aligned FileSource |
src/probe.rs -- is_ext4 magic-byte predicate |
probe -- drive-letter selection, GUID_DEVINTERFACE_DISK, DEV_BROADCAST_DEVICEINTERFACE_W parsing |
|
templates/installer/ -- WiX MSI + Burn shapes |
installer/ -- ext4-customised copies of the templates |
templates/release.yml -- GH Actions x64 + arm64 build matrix |
.github/workflows/release.yml |
templates/winget/ -- manifest skeleton |
winget/v0.1.0/ -- the actual submission |
The public seam is one trait + four constants:
use winfsp_fs_skeleton::FsBackend;
struct Ext4Backend;
impl FsBackend for Ext4Backend {
const FS_NAME: &'static str = "ext4";
const SERVICE_NAME: &'static str = "ExtFsWatcher";
const LAUNCHER_SERVICE_CLASS: &'static str = "ext4-mount";
const FILE_EXTENSION: &'static str = "img";
fn detect(bytes: &[u8]) -> bool { probe::is_ext4(bytes) }
}Cmd::Watch and Cmd::Service in src/main.rs just
hand off:
Cmd::Watch => winfsp_fs_skeleton::watch::run::<Ext4Backend>(),
Cmd::Service => winfsp_fs_skeleton::service::run::<Ext4Backend>(),CLI only (any platform):
cargo build --release
WinFsp mount + auto-mount service (Windows only):
cargo build --release --features mount,service
The release profile statically links the C runtime via
.cargo/config.toml so the binary has no
libunwind.dll import -- required for the SCM session-0 launch path
where the LocalSystem PATH doesn't reach the LLVM-MinGW runtime dir.
panic = "abort" is set in Cargo.toml for the same release profile.
- WinFsp 2.1+ installed on the build/run machine
(winfsp.dev -> MSI, or
winget install WinFsp.WinFsp). - A forked
winfsp-rs is checked
out as a sibling at
../winfsp-rs/, pinned to a commit from thefix/guard-strategy-constant-typebranch (path-depended; the upstream PR is pending). The fork also requires:LLVMforlibclang.dll(winget install LLVM.LLVM)- LLVM-MinGW (
winget install MartinStorsjo.LLVM-MinGW.UCRT)
LIBCLANG_PATH=C:\Program Files\LLVM\binso bindgen can findlibclang.dll.- winfsp-fs-skeleton
is checked out as a sibling at
../winfsp-fs-skeleton/; pure Rust, no extra toolchain requirements.
After cargo build --release --features mount,service finishes:
installer\build.ps1 -ExePath target\release\ext4.exe -Arch arm64Produces dist\ext4-win-driver-<ver>-arm64.msi and
dist\ext4-win-driver-<ver>-arm64-Setup.exe. The script PE-sniffs
-ExePath to catch arch mismatches before WiX does. See
installer/README.md for the longer walkthrough
(WiX 7 install, WinFsp redist pin, etc).
Scenarios live in test-matrix.json and the per-project
adapter config in fs-windows-test-harness.toml. Both are
consumed by the shared
fs-windows-test-harness,
checked out as a sibling repo at
../fs-windows-test-harness/ -- not a submodule.
Its URL and pinned tag (HARNESS_URL / HARNESS_REF) live in
chores.yml, next to the other family pins.
After cloning, check out the siblings at their pinned refs:
chore siblings # clone or move each sibling to its pin; refuses to touch a dirty one
chore siblings:check # report where each sibling is, changing nothingCI does not run chore (there is no Windows build of it); the workflows read
the same *_URL / *_REF values out of chores.yml and clone the siblings
directly.
Run the test matrix (Mac -> SSH -> Windows VM -> diag pull):
chore matrix # full matrix, quietly
chore matrix -- basic-ro-list # one scenario (substring filter)
chore matrix -- rw- --verbose # stream the run as wellchore matrix builds the host CLI the verifiers use, then runs
scripts/run-matrix.sh, which starts the Alpine VM that builds each
scenario's ext4 image and hands off to the harness's run-tests.sh. It is
quiet: the whole run (every SSH session, echoed PowerShell command line and
verifier) goes to tmp/logs/matrix.log, and what is printed is one verdict
line and the executed-scenario count against its floor. A run that passed
but printed more than the tier's measured budget fails with status 65; the
budgets are the table in scripts/tier-budgets.txt, and the runner and
wrapper are rust-fs-core's scripts/tier.sh and scripts/output-budget.sh,
run in place from ../rust-fs-core, not copies.
CI runs the same matrix on every pull request, in the matrix job on
windows-latest: the runner is its own Windows VM over SSH to localhost,
and the images are built by the same _vm-builder.sh on an Ubuntu runner
first (EXT4_PREBUILT_IMAGES), because a Windows runner cannot start the
builder VM.
The first run needs the VM's details. A quiet run has no terminal for the
harness to prompt on, so pass them once as flags and the harness writes
.test-env (gitignored) for every later run:
chore matrix -- --vm-host=USER@IP --ssh-key=/path/to/key --vm-workdir=C:/path/on/vmAfter a VM IP change or key rotation, pass the new value the same way; any
flag given is written back to .test-env.
Diagnostics land under test-diagnostics/run-<UTC>/. See the harness's
docs/triage-protocol.md
for how to read a failure, and
docs/multi-agent-protocol.md
for running multiple agents against the same matrix.
To move to a new harness release, edit HARNESS_REF in chores.yml to the
new tag, then re-run chore siblings to check it out locally. That one line
is the whole bump: CI picks the new ref up from the same file.
GPL-3.0-or-later -- inherited from the WinFsp Rust bindings. The CLI alone (without
the mount feature) doesn't link winfsp and could be relicensed if split
out, but the single-license declaration keeps the distribution unit simple.
