anylinuxfs is a macOS CLI utility that mounts any Linux-supported filesystem (ext4, btrfs, xfs, NTFS, exFAT, ZFS, etc.) with full read/write support. It achieves this by running a lightweight libkrun microVM and exposing the mounted filesystem to the host via NFS.
Five components work together:
- anylinuxfs (Rust/macOS): Main CLI. Manages microVM lifecycle, disk arbitration, networking (gvproxy), and NFS mounting on the host.
- vmproxy (Rust/Linux/FreeBSD): Guest-side agent running inside the microVM. Mounts filesystems, exports via NFS, and handles the control socket. Compiled for
aarch64-unknown-linux-muslandaarch64-unknown-freebsd. - common-utils (Rust): Shared library used by both
anylinuxfsandvmproxy. Contains IPC protocol, logging, RAII utilities, and shared constants. - init-rootfs (Go/Linux): Bootstraps the Alpine Linux rootfs for the microVM (downloads OCI image, unpacks it). This is NOT the VM init process —
libkrunhas its own bundled init. - freebsd-bootstrap (Go/FreeBSD): Same as
init-rootfsbut for FreeBSD images.
The host orchestrates; the guest executes:
macOS host (anylinuxfs)
├── Starts network helper (gvproxy or vmnet-helper)
├── Launches krun microVM via libkrun FFI
│ └── vmproxy runs inside VM
│ ├── Configures network (192.168.127.2/30)
│ ├── Mounts disk (LUKS → RAID → LVM → filesystem)
│ ├── Writes /etc/exports, starts NFS daemon
│ └── Listens on control socket
└── Mounts NFS share: mount -t nfs 192.168.127.2:/mnt/disk /Volumes/...
Control socket (host ↔ vmproxy):
- Format:
[4-byte u32 BE length][RON-serialized payload], max 1 MiB per message - Transport: TCP on macOS/FreeBSD (
VM_CTRL_PORT = 7350), vsock inside Linux (CID_ANY:12700) - Messages:
Request::Quit,Request::SubscribeEvents→Response::Ack,Response::ReportEvent
API server (host → caller): Unix socket at /tmp/anylinuxfs-<id>.sock, returns RuntimeInfo (mount config, PIDs, device metadata, mount point).
NFS: Standard NFS3/NFS4 on the fixed 192.168.127.x/30 subnet. Ports 2049 (nfsd), 32767 (mountd), 32765 (statd) forwarded via gvproxy.
/dev/disk1s2 # single partition
lvm:vg_name:lv_name # LVM logical volume
raid:dev1:dev2:... # software RAID (mdadm)
- Use
anyhow::Result<T>throughout. Always chain.context("...")before?to preserve error context. - Never use
.unwrap()or.expect()in production paths. Reserve them for tests or provably-unreachable branches. - For child process failures, wrap with
StatusError(fromutils.rs) to capture exit codes.
// Correct
let content = fs::read_to_string(&path)
.context(format!("Failed to read {}", path.display()))?;
// Avoid
let content = fs::read_to_string(&path).unwrap();The codebase is purely synchronous. Do not introduce tokio, async_std, or async/await. Use std::thread::spawn and std::sync::mpsc channels for concurrency. This is intentional — simpler error handling, no executor overhead.
Use bstr::BString and ByteSlice for data that may or may not be valid Unicode but still needs normal string manipulation operations (searching, slicing, formatting). Typical use cases include environment variables and other byte sequences that are usually text but have no Unicode guarantee. This avoids the String vs Vec<u8> false dichotomy — you get string-like operations without requiring valid UTF-8.
Use Deferred::add(|| { ... }) from common-utils for cleanup that must run on scope exit (even on error). This is the project's RAII pattern — prefer it over manual cleanup in Drop impls or error branches.
let _cleanup = Deferred::add(|| { let _ = fs::remove_file(&socket_path); });ForkOutput<O, I, C> uses generic type parameters to track fd capabilities at compile time (PtyFd, PipeOutFds, CommFd, ()). When adding new fork-based code, match this pattern rather than storing raw fd integers.
- Always capture
SUDO_UID/SUDO_GIDenvironment variables at startup to record the original invoker. - Change Unix socket ownership to
(invoker_uid, invoker_gid)after creation.
- Run
cargo fmt/rustfmton any Rust changes before committing. Do not commit unformatted Rust code.
Requires util-linux for libblkid (via Homebrew) and must be built with the freebsd feature:
cargo build -F freebsd # from anylinuxfs/
cargo check -F freebsd # for type-checking onlyThe build-app.sh script at the repo root sets these automatically and also signs and copies the binary to bin/.
Cross-compiled (FreeBSD also needs sysroot to be built natively on macOS but this is handled by the build-app.sh script):
# Linux (musl):
cargo build --target aarch64-unknown-linux-musl -F freebsd
# FreeBSD:
cargo build --target aarch64-unknown-freebsd -F freebsd# init-rootfs
cd init-rootfs && go build -ldflags="-w -s" -tags containers_image_openpgp -o ../libexec/
# freebsd-bootstrap (cross-compile for FreeBSD)
cd freebsd-bootstrap && CGO_ENABLED=0 GOOS=freebsd GOARCH=arm64 \
go build -tags netgo -ldflags '-extldflags "-static" -w -s' -o ../libexec/| Module | Responsibility |
|---|---|
main.rs |
CLI parsing (clap), lifecycle orchestration, AppRunner |
api.rs |
Unix socket RPC server exposing RuntimeInfo |
settings.rs |
Config file loading/merging (Config, MountConfig, Preferences) |
diskutil.rs |
macOS DiskArbitration bindings for disk/partition discovery |
fsutil.rs |
Mount table queries, NFS mount option building |
vm_network.rs |
gvproxy / vmnet-helper startup and port forwarding |
vm_image.rs |
Alpine rootfs and FreeBSD image initialization |
pubsub.rs |
Generic PubSub<T> event hub (used for signal broadcasting) |
netutil.rs |
IP allocation (pick_available_network), DNS resolution |
devinfo.rs |
Device metadata probing via libblkid (UUID, label, fs type) |
rpcbind.rs |
RPC service registration via macOS oncrpc framework |
bindings.rs |
FFI bindings to libkrun C API (krun_create_ctx, krun_start_enter, etc.) |
utils.rs |
Process management, PTY, signal handling, ForkOutput, StatusError |
- VM isolation: Disk I/O is mediated inside the microVM. The host only sees an NFS share.
- Privilege dropping: Most commands require
sudofor/dev/disk*access, but the microVM runs with dropped privileges. - Passphrases: Supplied via
ALFS_PASSPHRASEenv var (orALFS_PASSPHRASE1,ALFS_PASSPHRASE2, etc. for multi-disk), or via interactive TTY prompt usingrpassword. Never log or print passphrases. - Socket security: API socket ownership is changed to the invoking user's UID/GID after creation.
- Host: macOS only — uses
DiskArbitration,CoreFoundation,objc2bindings. - Guest targets:
aarch64-unknown-linux-musl(default),aarch64-unknown-freebsd(optional). - Feature gate:
#[cfg(feature = "freebsd")]enablesImagesubcommands and ZFS support. - Platform guards:
#[cfg(target_os = "linux")]in vmproxy for vsock/procfs,#[cfg(any(target_os = "freebsd", target_os = "macos"))]for BSD paths.
./build-app.sh # Debug build (all components)
./build-app.sh --release # Release buildThe script handles cross-compilation for vmproxy and places binaries in bin/ and libexec/.
Prerequisites: Rust toolchain with aarch64-unknown-linux-musl and aarch64-unknown-freebsd targets, Go toolchain, and Homebrew packages: util-linux, libkrun, lld, llvm, pkgconf.
Rust unit tests (even for vmproxy which is normally cross-compiled for Linux/FreeBSD):
./run-rust-tests.shIntegration tests (BATS, end-to-end: ext4, btrfs, NTFS, LUKS, LVM, RAID, etc.):
./tests/run-tests.shPrerequisites for integration tests: brew install bats-core, project built, Alpine rootfs initialized (anylinuxfs init). Alpine packages are installed automatically on first run.
ZFS test note: ZFS always creates a partition table on the target device. Because of this, ZFS test images must be attached as a virtual disk via hdiutil_attach and mounted using the resulting partition device (${HDIUTIL_DEV}s1) rather than the raw image file. Remember to hdiutil_detach the device in teardown.
bats-core variable scope: Each test and teardown runs in its own subshell. Any variable that must be visible across these contexts (e.g. device nodes captured in a @test and detached in teardown) must be exported.
anylinuxfs list # List disks with filesystem detection
anylinuxfs mount <DISK_IDENT> # Mount filesystem (also the default command)
anylinuxfs unmount # Safely unmount and terminate VM
anylinuxfs status # Show current mount status
anylinuxfs init # Reinitialize microVM root filesystem
anylinuxfs shell # Open debug shell in VM
anylinuxfs log [-f] # Show/follow VM log
anylinuxfs dmesg # Show kernel messages
anylinuxfs apk {info|add|del} # Manage Alpine packages in VM
anylinuxfs image {list|install} # Manage VM images (FreeBSD feature)
anylinuxfs/src/— Core macOS CLI implementationvmproxy/src/— MicroVM agent implementationcommon-utils/src/— Shared IPC protocol, logging, RAII utilitiesinit-rootfs/— Alpine rootfs bootstrapping (Go, runs on host)freebsd-bootstrap/— FreeBSD image bootstrapping (Go, runs from within a special bootstrap VM)tests/— BATS integration testsetc/anylinuxfs.toml— Default configuration filelibexec/— Bundled helper binaries (gvproxy, vmproxy, init-rootfs, etc.)kernel-builder/— Unmaintained. Originally a Lua-based microVM launcher used for experiments during early development (beforeanylinuxfs shellwas usable) and for building custom Linux kernels (libkrun-based config with additional filesystem drivers enabled). Superseded byanylinuxfs shell, which now serves the same purpose on both Linux and FreeBSD. Do not modify or depend on this directory.
- New test cases: After writing a new BATS test case, always attempt to run it and report the result. Run individual test files directly with
bats tests/<file>.bats; use./tests/run-tests.shonly to run the full suite. - New features / non-trivial behavior changes: After implementing, offer to write a new test case. If the user declines, offer to run the existing test suite. If the user agrees to add tests, first check whether the change is already covered by existing tests (which may need updating); then either fix the existing test or create a new one.
- "Please remember" / "take a note": When the user asks you to remember something, add it as a rule or note in this instructions file (and mirror it in GEMINI.md).
git addnew files: After creating a new file, rungit add <file>on it. When creating multiple files, batch them into a singlegit addcommand.
Whenever the assistant performs repository-related work — including answering library questions, creating implementation plans, writing code, selecting APIs, or generating examples — it MUST resolve and fetch authoritative documentation via the Context7 MCP server before relying on its internal training knowledge. Treat these rules as mandatory workspace-level guidance for Copilot Chat and other assistants that read repository instructions.
- This is proactive: before producing code, examples, API choices, design decisions, or implementation steps, call
mcp_io_github_ups_resolve-library-idto locate the most appropriate Context7-compatible library ID (format:/org/projector/org/project/version). - Prefer exact-name matches first; if multiple candidate IDs are returned prefer higher Source Reputation, then greater Code Snippet coverage.
- For the selected ID, call
mcp_io_github_ups_get-library-docswithmode="code"to fetch API docs, usage examples, and representative code snippets. - Summarize concisely in responses and implementation notes: one-line description, the Context7 library ID used, and 1–3 short code examples (if available). Include a reference to the fetched docs or the Context7 ID.
- If the user requested a specific version and a
/org/project/versionID exists, use that versioned ID. - If Context7 fails or returns no match, clearly state the fallback (web search or canonical docs) and continue.
Adding this guidance at the repository level ensures Copilot Chat follows Context7-first behavior for library-related reasoning and code generation across devices that open this workspace.