Navigation

Execution backends

The core, novel work: run a repo's real commands against a RAM filesystem, isolated from the host. Three pluggable backends implement one ExecBackend interface; the orchestrator picks via BackendType (auto | native | vfs | os). auto resolves to native today — no non-native backend has beaten the gates by default.

How it works

flowchart TB
    cmd["exec(command)"] --> which{backend}

    which -->|native| spawn["spawn on host<br/>(the baseline + fallback)"]

    which -->|vfs| parse{"parseNodeInvocation<br/>recognised?"}
    parse -->|"node -e / --eval"| inproc["runNodeInProcess<br/>vm.runInThisContext"]
    parse -->|"node &lt;file&gt; (no args)"| inproc2["runNodeInProcess<br/>require(file)"]
    parse -->|"anything else — it falls back"| spawn
    inproc --> mount["real cwd, in-process<br/>streams/exit/require patched, restored after"]
    inproc2 --> mount

    which -->|os| host{"host?"}
    host -->|Linux| bwrap["bwrap --unshare-all<br/>RO lower + tmpfs upper over cwd"]
    host -->|win32| wsl["wsl.exe --exec bwrap …<br/>paths via wslpath, memoized"]
    host -->|"unsupported<br/>(no bwrap / no WSL node)"| throw["throw — never a silent<br/>un-isolated run"]
    wsl --> bwrap
    bwrap --> store["bind-mount .virrun/store/pnpm<br/>(shared dep store)"]

native backend — the baseline and the fallback

Runs the command on the host, unchanged. A command string goes through the shell; an argv array never does, so a command built from data cannot be reinterpreted as shell syntax. On win32 that argv still has to reach a .cmd shim — pnpm, npx, every node_modules/.bin entry — which a direct spawn cannot, so every argv spawn goes through cross-spawn: it resolves the file through PATHEXT and runs a shim under cmd.exe with each argument escaped. That escaping leaves a CR or LF bare, and cmd.exe ends the command at one even inside quotes, so spawnHidden refuses a win32 argv carrying a line break rather than let the rest run as a second command. Without it, virrun -- pnpm … could not run at all on a Windows host whose sandbox was unavailable, which is exactly when the fallback is needed.

vfs backend — in-process, pure npm

Runs JS workloads in-process so the virtual filesystem intercepts their fs calls and module loading. A shell-aware tokenizer parses the invocation; inline code (node -e/--eval) runs via vm.runInThisContext, a file (node <file>, a lone non-flag path, no script args) via require — both against the real working directory. Nothing is mounted over it: a vfs mount lives in a reserved namespace of its own and never shadows a real path (below), so until a layered provider can serve the cwd from inside a mount, the backend buys in-process speed, not a virtual filesystem. Process streams/exit and require are patched for the run and restored after; the require cache is cleared back to its pre-run state so each run re-executes like a fresh process.

Correctness is preserved by falling back to native for anything not run faithfully in-process — shell features, other flags, file runs with args, syntax errors, uncaught errors, async results, missing files — so the observable result always matches the baseline. Opt-in only: it runs code in the host process with no isolation, so Auto never selects it.

The virtual filesystem underneath

The FS layer is reused, not built. Node is standardizing a core node:vfs module (nodejs/node#61478); @platformatic/vfs (MIT) is the same work extracted to userland. virrun depends on platformatic behind a thin internal interface (FsProvider) — one module owns the import, so the swap to node:vfs once it needs no --experimental-vfs flag is a single-file change. It provides fs API compatibility (read/write/streams/promises/symlinks/watchers), mounting, and module loading from virtual files. Since 0.5 it mounts the way core does: mount() takes no path and returns a mount point inside a reserved namespace under os.devNull that cannot hold real entries, so every path is served by exactly one VFS or by the disk, never both. Core rejects an overlay at a real path as ambiguous, not as a security measure — its docs say VFS is no security boundary either. Core's open layered-mount work (nodejs/node#66235, a composable provider stacking a memory layer over a RealFSProvider with copy-up on write) is what would let the in-process runner serve a RAM overlay of the cwd from inside a mount.

Usage contract: the provider's own paths are rooted at /, and mount() returns where the process sees them — the provider's /a.js is <mount point>/a.js. Write before or after mounting; a real path of the same name is never touched. dispose() unmounts and is safe to call when already torn down.

Hard limit: in-process JS only. Child processes and native binaries bypass it with raw syscalls — the subprocess wall in architecture. Closing that gap is the os backend's job.

os backend — native, generic (Linux core, WSL2 bridge)

Makes every process, including spawned native binaries, see the RAM FS by moving the filesystem and isolation down to the OS:

  • RAM filesystem — bubblewrap supplies it directly: --overlay-src <cwd> is the read-only lower (the source) and --tmp-overlay <cwd> overmounts the working directory with an overlay whose upper is an invisible tmpfs. Reads hit the real source; all writes (node_modules, build output) stay in RAM.
  • Isolation — bubblewrap: one unprivileged tool collapses overlay + tmpfs + namespaces (--unshare-all, --die-with-parent). Unlike vfs, the os backend never falls back to native at run time: an unsupported host throws, because a silent un-isolated run would be a wrong answer disguised as success. (Backend selection may still degrade to native before a run starts — and says so on stderr, because the sandbox's own lines are the only other trace of it.)
  • Dep store — .virrun/store/pnpm is created lazily in the consuming repo, gitignored, bind-mounted writable into the sandbox, and exposed through pnpm env so deps download once; why imports copy rather than hardlink is cache.
  • Package-manager bootstrap — COREPACK_HOME points at .virrun/store/corepack, bound writable into every run, the install and the ordinary commands alike; the EROFS it prevents under a read-only / is cache's.
  • Guest toolchain (win32) — wsl.exe --exec skips the login + rc files, so a profile-bound version manager's node is off PATH. readWslLoginEnvironment captures the PATH a real interactive login shell sees — plus that PATH's node version, which is the node the sandbox actually runs and what the run banner reports. The capture is persisted host-side and checked against the distro before every reuse, so a bump that removes the captured install can't silently pin the sandbox to the old version; a switch that leaves it on disk is the one a stat can't see, and the six-hour age bound and VIRRUN_FORCE_PROBE are what cover that (cache).
  • Sandbox PATH (win32) — two parts, both stated rather than inherited. First the repo's own node_modules/.bin at the logical /mnt/<drive> path the overlay is mounted at — never the ext4 mirror's, which excludes node_modules by construction and so names a directory that does not exist. Then the captured login PATH with every Windows drive mount dropped: WSL interop appends the launching process's whole Windows PATH to a login shell, and none of it can serve a Linux sandbox — each entry holds win32 binaries or an sh shim that execs a Windows node, which is what turned a missing Linux toolchain into a cryptic exec: node: not found rather than a plain command-not-found. Dropping them also makes the capture launcher-independent: pnpm run puts the repo's bin on the Windows PATH and a bare terminal does not, and before the repo bin was named outright that leak was the only reason a bare oxlint resolved at all.
  • Windows bridge — on win32, createOsBackend invokes wsl.exe --exec bwrap ... against the same bwrap argv. Windows cwd and bind paths are translated once through wslpath (memoized), and pnpm store env is translated before entering Linux, so the public backend contract stays unchanged. Source reads come from an ext4 mirror, not /mnt/c.
  • macOS bridge — deferred; there is no WSL equivalent to target.
  • What a missing status block means — bwrap reports its own setup through --json-status-fd, so a child that closes without one never got that far. Only one of the reasons is bubblewrap. On win32 the child is the wsl.exe client and the script it runs has two preludes ahead of the sandbox — the source-mirror sync and the shared flock over the mirror lower — each of which prints its own marker line before exiting, and any run can simply be killed from outside, which prints nothing at all and arrives as node's signal or as a 128+n exit status. getNoStatusFailureHeadline reads those four cases apart. Naming them matters most for the kill: a run that another process TERMs reports a sandbox-setup failure with an empty stderr, so blaming bubblewrap for it throws away the one fact that explains it.

The acceptance test that proves the subprocess wall is broken: pnpm install on a repo with a native postinstall (sharp or esbuild) completes fully in RAM, isolated from the host, and the resulting node_modules is invisible to the real disk. It is parked as describe.todo and run on demand (correctness).

Key files

Paths relative to packages/virrun/src/.

FileRole
models/exec/ExecBackend.tsinterface every backend implements
services/exec/native/createNativeBackend.tsnative passthrough (the baseline + fallback)
services/exec/vfs/createVfsBackend.tsparse-and-delegate: in-process when recognised, else native
services/exec/vfs/parseNodeInvocation.tsrecognise node -e/--eval and node <file>
services/exec/vfs/runNodeInProcess.tsin-process runner against the real cwd
models/vfs/FsProvider.tsinternal FS interface the runtime codes against
services/vfs/createPlatformaticFsProvider.tsadapter over @platformatic/vfs; the lone import = the node:vfs swap shim
services/exec/os/createOsBackend.tschooses Linux bwrap or Windows/WSL bwrap
services/exec/bwrap/createLinuxOsBackend.tsspawns commands inside the Linux bwrap RAM overlay
services/exec/wsl/createWslOsBackend.tsspawns Linux bwrap through wsl.exe on Windows
services/exec/bwrap/buildBwrapArgs.tspure builder for the bwrap overlay argv
services/exec/bwrap/getNoStatusFailureHeadline.tsnames the failure behind a child that reported no bwrap status
services/exec/os/checkIsOsBackendSupported.tsLinux/WSL + bubblewrap availability check

Notes

  • Native-binary support across platforms is impossible in pure JS; the os backend is Linux-core and bridged elsewhere — accepted, see the platform table in architecture.
  • The shell layer (just-bash parser/builtins) is optional sugar for running shell scripts; it is not an exec engine and never spawns native binaries (pure-JS exec).
  • Do not use just-bash's FS abstraction — platformatic is node's fs, not a parallel one, so in-process tooling and the module loader see virtual files for free.

Details

Command palette

Keyboard shortcuts