Linux and WSL2 support — status¶
foldyard has no platform branching: every "can this host do X?" is a capability probe, not a
sys.platform test. So a Linux host is not a port; it is a set of code paths that have or have
not been run there. This page is the index of which is which. The measurements live where they
were taken (linked per row); keep this page the summary.
Linux and WSL2 hosts are validated in CI on every run of the host tier (lima-host-e2e and
wsl2-host-e2e, see DEVELOPMENT.md).
macOS (lima with vz) is the platform most day-to-day use has seen
(isolation-layers.md).
Where a Linux host stands in the design: the boundary can be stronger than on macOS (nftables
can match the VM's own process on the host, which macOS's pf cannot — [machine] host_firewall)
and it is never weaker: there is no VM-less backend
(ADR-0027). WSL2's Hyper-V boundary protects
Windows, not the credentials, so the VM is required inside the distro too. Both are explained in
isolation-layers.md.
Where the results come from:
- The CI host tier — GitHub-hosted
ubuntu-24.04(x86,/dev/kvm, Lima 2.2.0 from the release tarball, QEMU 8.2, host podman CLI 4.9.3) booting a Fedora 44 / podman 5.8.4 guest; and the same inside WSL2 onwindows-2025. - The nested-KVM rig — a GCP VM (Fedora 44, Lima 2.2.0, podman 5.8.4, crun 1.28; nested-virt.md). It is one hypervisor level deeper than a laptop, so its timings are upper bounds; correctness results transfer as they are.
Validated on a Linux host¶
| area | result | when | where |
|---|---|---|---|
lima backend on Linux: machine ensure boots a QEMU VM |
✅ needs qemu-img (1) |
2026-09-11 | isolation-layers |
| repo-only mount set | ✅ over 9p (1) | 2026-09-11 | as above |
fy verify VM-boundary checks, incl. the home-mount negative |
✅ PASS, and FAILS on a leaky VM | 2026-09-11, CI | archive/verify-false-pass.md |
[machine] firewall (the VM firewall) |
✅ direct guest egress refused, DNS resolves | 2026-09-11, CI | lima-wall-machine-integration §2 |
| root in the guest only at boot; stale provisioning refused | ✅ checked by every machine ensure |
2026-09-12, CI | as above |
[machine] host_firewall (the host firewall) via fy up |
✅ (2) | 2026-09-12, CI | lima-wall-machine-integration §3 |
fy machine ensure / stop / recreate / rm, SIGKILLed QEMU recovered |
✅ | CI (test_machine_e2e.py) |
DEVELOPMENT.md |
fy up end to end: preflight, config-adopt gate, supervisor, proxy, overlay, compose |
✅ | CI (test_e2e.py, test_host_daemons_e2e.py) |
as above |
| the proxy on the host: CA, box routing, egress log | ✅ | 2026-09-13, CI | as above |
| config-adopt gate: first adoption, drift, revert | ✅ | 2026-09-13 | ADR-0022 |
fy box up / down + in-box fy verify |
✅ (3) | CI (test_box_e2e.py) |
archive/verify-false-pass.md |
fy worktree add / remove |
✅ | CI (test_worktree_e2e.py) |
DEVELOPMENT.md |
fy reclaim on a real store |
✅ | CI (test_reclaim_e2e.py) |
as above |
engine probes on host podman 4.9 (fy state, fy doctor) |
✅ after a fix (4) | CI (test_probes_e2e.py) |
as above |
gVisor as the box runtime ([machine] runtime = "gvisor") |
✅ works; overhead measured (5) | 2026-09-11 – 13, rig | ADR-0025 |
repo mounted under the host's home (/home/<user>/…) survives restarts |
✅ | CI (test_machine_e2e.py) |
6 |
the host's whole /home/<user> mounted at its own path, added after first boot |
❌ open (6) | 2026-09-17 | run 35231657712 |
ubuntu-24.04-arm runners |
❌ no KVM, so VM-backed CI is x86-only | 2026-09-17 | — |
| WSL2 as the host (Windows Server 2025 runner) | ✅ 30 passed, 6 skipped (7) | CI (wsl2-host-e2e) |
run 35261179135 |
[machine] host_firewall on WSL2 |
❌ kernel lacks CONFIG_NFT_SOCKET (7) |
2026-09-17 | run 35256429594 |
Details¶
1. Lima on Linux¶
vmtype resolves to qemu from limactl info, not from the OS. The host needs qemu-img,
which comes from qemu-utils and isn't implied by qemu-system-x86-core. The mounts use 9p:
virtiofs can't be used yet because Lima 2.2.0's rootless virtiofsd fails every inode create
(EINVAL).
2. The host firewall¶
Checked on the rig and in test_wall_e2e.py: the VM starts in its own cgroup scope; direct guest
egress is refused by name and IP; DNS resolves; the proxy's port is open and other ports and sshd
are refused; your own processes, limactl shell and the podman socket are unaffected; fy verify
passes; a hand-started VM is refused. The firewall needs [proxy] declared (preflight refuses it
otherwise). foldyard never runs sudo: fy machine host-firewall prints the install commands
and fy up checks enforcement (ADR-0028).
It needs a systemd --user manager (an SSH login has one; a bare su may not —
loginctl enable-linger gives one). When probing by hand, use curl --noproxy '*': Lima gives
the VM user the proxy environment, so a bare curl from limactl shell goes through the proxy.
3. The box¶
The first rig run found two fy verify bugs, both fixed: a false FAIL (a Fedora guest's btrfs
subvol=/root mount option matched the in-box home; the audit now reads only the mountpoint),
and the need for a private origin (a public one answered the then git ls-remote probe
without credentials; the probe is a git push --dry-run now, which a public origin refuses too).
4. podman 4.9 labels¶
Ubuntu 24.04's podman 4.9.3 has no {{.Label "k"}} in ps --format (it is a 5.x template
function), so every probe built on it read "engine unreachable" while fy ps worked. Now
{{json .Labels}} via devmode.ps_labels, pinned in tests/test_devmode.py.
5. gVisor¶
Measured on the rig and on macOS: fy verify passes inside a gVisor box; the test suite runs
1.6–2.0× slower on the rig (1.2× on macOS), fork/exec 11×, git walks about 1×. The overhead is
gVisor's syscall path, not file placement. The route is a second, runsc-default podman socket in
the VM, and the box mounts a filtered copy of it that strips runtime overrides. Details:
isolation-layers.md and
ADR-0025.
6. Open: a home mount added after first boot¶
After machine stop, adding a read-only 9p mount of the host's /home/<user> at the same path
(limactl edit), then machine ensure, the guest's rootless podman accepts connections but can't
create any container (crun: open …/merged/etc/resolv.conf: No such file,
mkdir /run/secrets: EPERM). Store ownership and the subuid map are intact; podman system
migrate doesn't help; removing the mount doesn't heal it; fy machine recreate does. Plain
restarts, a re-provision with the firewall, and SIGKILL → ensure are all fine, and a repo mounted
under the home at its own path survives restarts, so the trigger is a mount whose location is
the home, added after first boot. The rig ran the same recipe healthy on 2026-09-11. Because of
this, test_verify_e2e.py exposes the home at /mnt/c instead; the home-path branch of the audit
stays unit-tested.
7. WSL2¶
The distro has /dev/kvm once %USERPROFILE%\.wslconfig sets nestedVirtualization=true
(before the distro first starts). The unmodified machine ensure boots the guest to READY in
68 s (47 s on a restart), and the host tier runs as-is, about 3.5× slower than on Linux. Nothing
in the product differs; the job's plumbing (WSLENV, the user switch, the clone onto ext4) is
described in DEVELOPMENT.md.
The host firewall can't work on stock WSL2: it matches the VM by socket cgroupv2, and the WSL2
kernel (6.6 and 6.18 branches) has # CONFIG_NFT_SOCKET is not set, so nft refuses the rule and
the firewall fails closed. test_wall_e2e.py probes for this and skips. A custom WSL2 kernel
(.wslconfig kernel=) is the only route. The VM firewall runs in the guest and is unaffected.
Not yet validated on Linux¶
- WSL2 on a real Windows 11 machine. CI runs Windows Server 2025 with
nestedVirtualization=trueset explicitly. Windows 11 x86 defaults it on; Windows 10 silently overrides it. Still owed: the two-minute check on a real machine (wsl --shutdown; wsl; ls -l /dev/kvm). Keep the checkout on the distro's ext4, not under/mnt/c. Windows on ARM has no KVM in the distro. See isolation-layers.md. fy tui,fy code(the VS Code attach),fy open(browser) on Linux — untested.- The
podmanbackend (podman machine) on a Linux host — untried. tests/test_proxy_box_e2e.pyon a real Lima VM — it needs the test process inside a container beside the box, which thelive-e2ejob provides;test_box_e2e.pycovers the box on the VM.
Outstanding work before "supported"¶
- Wording that assumes macOS. The sweep from "Mac" to "host" is in progress. A few
user-facing strings remain, e.g. the Lima create failure in
machine.pythat blames Apple Virtualization. The rule for new text is in DEVELOPMENT.md: write host, say macOS only where the claim really is macOS-only. - 9p is slow on metadata (4.9 s vs 0.05 s for 3,000 small files). The fix is upstream
(
virtiofsdroot/file-handle support) or a newer Lima. Re-test when either moves. - The gVisor allowlist. gVisor is wired as
[machine] runtime = "gvisor"; the broader mount/endpoint allowlist is deferred (ADR-0025 §Decision 4). Not a Linux blocker: without gVisor the Linux claim is the same as macOS's. - What CI can't reach: arm64 (no KVM) and nested virtualisation inside the guest. For those the nested-KVM recipe in nested-virt.md remains.
Recording a new validation¶
Add a row with the date, versions if they differ from the ones above, and a link to where the measurement lives — isolation-layers.md for anything about the boundary, the relevant design page otherwise. Put anything longer than a line under Details. Move the item out of "Not yet validated" in the same commit. A run that failed is a row too.