Host share mounts¶
A workspace can mount host directory trees into its guest. The
host side is the operator's decision: the daemon's config names
the allowed roots a guest's mounts must sit under, and (on a
VM-hosted daemon) the trees its own VM carries. The guest side is
the requester's: each workspace names a host path, the absolute
guest path it lands at, and a mode — ro or rw, defaulting to
rw.
# the operator allows the roots (msksd.yaml; reloadable on SIGHUP)
mount_host_roots:
- /srv/projects
- /srv/refs
# a workspace mounts host paths (applies at its next start)
msks create ws1 --share /srv/projects:/mnt/projects
msks mount ws1 /srv/refs:/mnt/refs:ro
msks umount ws1 /srv/refs
Shares by name — a mount_shares config list and mounts that
referenced one — were removed: host paths are the one surface, and
a config that still names the key fails at load with the removal
message.
What a mount is¶
A workspace's mount names a host path, an absolute guest path, and
a mode. The guest path becomes a root-owned mount point inside the
VM: absolute, below the filesystem root, outside the
pseudo-filesystems (/proc, /sys, /dev, /run) and outside
/home (the home volume owns it — see the storage
chapter). Two mounts of one workspace never overlap,
and a host path appears once per workspace. Mounts follow
next-start semantics, like resize: setting them never touches a
running VM, and the next start attaches the current set — the seed
that boot carries bakes exactly these mounts, so a restart after a
change lands the new set and prunes the dropped one. Two mounts set
at once from two terminals race last-write-wins — the surface sets
the whole list; a workspace served by one operator at a time never
sees the race.
The two gates¶
Each host path runs two gates at set time, each refusing with its remedy:
- The mount gate (VM-hosted daemons,
mount_host_mode: prefix): the path, translated under/mnt/msks, must sit under a root the daemon's VM actually has mounted. A path whose root is not mounted is refused with the instruction to recreate the VM from a template that mounts it — vz mounts are fixed at VM start, so a config edit cannot add one. - The policy gate (every host): the path, resolved against
symlink escapes, must sit under one of the allowed roots
(
mount_host_roots). A path outside them is refused with the instruction to add it or a parent to the daemon's config. A symlink that resolves outside its allowed root is refused the same way — the share cannot walk out of what the operator allowed.
The spec grammar splits its fields on colons, so a host path
containing one is inexpressible on the wire (--share and the
TUI editor both refuse it, naming the field the colon broke).
A mount attaches read-write unless it says ro: the trust
decision is made before the mode matters (the root is allowed),
and a mount that wants less names ro. Read-write mounts hold no
exclusivity: any number of workspaces may attach the same tree,
and the coherence contract (below) is their concurrency story.
The skip behavior¶
A start proceeds when a mount cannot attach, and the skip is
state, not a log line. A root the reload removed from the allowed
list, a path whose root the daemon's VM no longer mounts, a
symlink that moved out from under its allowed root, or a path
that vanished since the set — each publishes an event
(mount.skipped) naming the mount and the reason, and the row
carries the standing marker (msks ls prints running, 1 mount
skipped) until the config and the row agree again. The
workspace's other mounts attach.
Coherence between guests¶
A host tree attaches read-write to any number of workspaces at
once, and every attach forces cache=never: data writes become
promptly coherent through the one host page cache, and lockfile
coordination (O_EXCL creates — git, cargo) works because the
host kernel serializes it. Two things do not coordinate between
guests on the same share: flock/fcntl locks (each attach is
its own virtiofsd), and mmap coherence.
Shares on a macOS host¶
On a Mac the daemon runs inside its own Linux VM (Lima over Apple's Virtualization.framework, on an M3-or-newer Mac), and a shared Mac folder crosses two hops to reach a workspace: vz virtiofs carries it from macOS into the daemon's VM under the reserved prefix and one virtiofsd per attached share carries it from there into the workspace guest.
The client speaks Mac paths throughout — --share ~/code:/mnt/code — and the
daemon's prefix host mode does the translation before the two
gates run. The Lima template mounts the invoking user's home
under the prefix at VM creation, writable, so the common case needs no edit; a
share root outside it (another user's home, a /Volumes tree) is a template
edit plus VM recreation, and the mount gate's refusal names that remedy.
The far end of the share is the Mac's own volume, and its behaviors cross with the tree. Four of them are contract, not configuration:
macOS: Watchers poll¶
File events do not cross virtiofs. Changes made on the Mac — an
editor's save, a build's output — reach every attached
workspace as data, promptly coherent the way coherence
between guests describes, but no
inotify event follows them: the share carries data, not
notifications. Watchers on shared trees run in polling mode —
Vite's server.watch.usePolling setting, jest's --watchAll,
or the tool's own poll option — and a tool that can watch
only through events cannot watch a share at all. Signaling
built on the same polling takes one care: a file's existence
becomes visible before its content does (a writer creates
empty, then fills), so a poller waits for a completion marker
the writer emits after the data, never for the file itself.
macOS: Case and normalization follow the Mac volume¶
The tree a workspace mounts is the Mac's volume, and its naming
rules cross with it: a Mac's default volume matches names
case-insensitively, storing them as typed (case-preserving),
and compares them
Unicode-normalization-insensitively — a name typed
composed (NFC) and the same name decomposed (NFD) are one file.
Tools inside the workspace that expect Linux naming can trip:
two names differing only in case cannot coexist in one
directory (a checkout holding both README and readme
collapses them), and a tool comparing filename bytes sees NFD
where the Mac accepted NFC — archive and package tooling that
hashes or compares names byte-for-byte can report a mismatch
against a Linux-built expectation. A tree that must keep Linux
naming rules exactly lives in the workspace's own storage (the
storage chapter) instead.
macOS: The rw single-writer contract¶
An rw share names the workspace its writer. Locks taken
inside the workspace hold there and stop there: flock and
fcntl locks end at the workspace's own virtiofsd, and the Mac
side honors none of them — Finder, an open editor, or a
Mac-side build writes past any lock the workspace took.
Between workspaces, coherence between
guests governs: lockfile creates
(O_EXCL — git, cargo) serialize through the host kernel and
work cross-workspace; flock/fcntl do not cross an attach.
Trees whose tools lock and treat the lock as the whole story —
a cargo or bazel cache, a SQLite database, anything with a
journal on disk — take one writer at a time: the workspace's,
or the Mac's. A Mac that only reads the share (browsing in
Finder, opening a file) rides the coherence contract; a
Mac-side writer into a lock-using tree races the workspace.
macOS: External volumes stay busy while shared¶
A file the stack holds open keeps its Mac volume mounted, so a
disk whose tree is shared into a running workspace refuses to
eject — the Finder reports the volume in use — until the
workspace stops and its files close. msks stop on the
workspace, eject the disk, and start again — with the volume
back on the Mac, the share reattaches at the next start the
way every mount set does; without it, the start skips the
mount and carries the marker (the skip
behavior). Sharing the volume at all was
the template edit above, since a /Volumes/... root sits
outside the default home mount.
The plumbing¶
Each attached share is one virtiofsd process (the platform's own
virtiofsd package; the default binary name resolves on PATH
first and then the package's install locations off PATH, so the
daemon finds it without configuration — MSKSD_VIRTIOFSD names
a different binary), spawned before the VM and stopped with it.
The mounts need virtiofsd 1.13.0 or newer: the identity
translation and the read-only enforcement both landed upstream
there, and an older binary refuses the daemon's arguments at
start (msksd doctor names the gap — Linux Mint 22's archive
ships 1.10.0, so its hosts build a newer virtiofsd and point
MSKSD_VIRTIOFSD at it). The resolution runs at settings load
and again on SIGHUP: a binary installed after the daemon started
is one reload away.
File ownership translates in both directions. Writes from the
guest land on the host as the daemon's own user, whatever guest
account made them — host-side ownership stays advisory, as with
a typical bind mount. Files read back inside the guest present
as the workspace's own user: the login user the workspace names,
or the image's built-in msks account when it names none — so
the share's tree reads as the logged-in user's files, and a
file the guest creates stays writable by the account that
created it. An edge sits outside that rule: a login user the
operator's user_data premade at another uid presents as the
first uid the seed provisions (1001 on a pristine image), not
the premade account's own — and the host's own permission bits
still govern every write: a file that presents as the workspace
user's still refuses what its host-side mode refuses. The
guest mounts
through systemd units the workspace's seed writes — one per
mount, named for the guest path, re-baked at each start — so a
reboot remounts without cloud-init running again.
Extended attributes remap under one host prefix (user.virtiofs.),
so a guest cannot write host security labels or root-owned
namespaces through the share. One name the map denies outright
and leaves unmapped: security.capability. Denied, the boundary
does not depend on the daemon's privilege — a daemon running as
root keeps the capability that would let it honor a guest's
file-capability writes, and the map is what says no. Left
unmapped, the host kernel clears file capabilities on write
itself — a remapped name would hand that job to virtiofsd, whose
clearing path re-checks the file's mode even through a file
descriptor opened writable at creation, so a program that
creates a file without write bits and keeps writing through the
open descriptor (git's loose-object files) would get EACCES on
every write (#657). File capabilities cannot ride on share files
at all: a name an earlier version's map stored as
user.virtiofs.security.capability still lists in the guest as
security.capability, reads as absent, and clears only from the
host side.