Skip to content

RFC: virtio-fs: host-enforced write policy for writable shares #977

Open

Description

@thejoeejoee

Problem

Tools that run untrusted workloads (e.g. coding agents) in libkrun VMs often share a user's working directories with the guest as writable virtio-fs shares. Some paths inside those shares are later consumed by the host outside the sandbox and must stay immutable from the guest. The motivating case is Git: .git/hooks/*, .git/config, .git/config.worktree and .git gitfiles are executed or interpreted by git on the host, so a guest that can rewrite them gets code execution on the host.

Protecting these paths inside the guest does not hold against guest root: bind mounts can be unmounted or shadowed, mount namespaces and containers inside the guest bring their own view, and guest LSMs leave gaps (usermode helpers, BPF, kernel bugs). The only enforcement point the guest cannot reach is the virtio-fs server in the VMM, but today it offers only read_only for a whole share (#623).

Proposal

An optional, per-share write policy enforced by the virtio-fs server. Guest requests are filtered; the host user is not affected.

Rule Effect for guest requests
protect:<path> no write open, truncate, setattr, xattr changes, fallocate, unlink, rename, link
seal:<path> protect for a directory and its whole subtree; no entries can be created inside
pin:<path> directory cannot be removed, renamed or replaced
reserve:<path> name that does not exist yet cannot be created

Paths are absolute host paths under the shared directory and are resolved once when the device is created.

Matching

  • Rules are keyed by (parent host st_dev/st_ino, name), and the host IDs of matching children are learned on lookup. Atomic replacement on the host (git config writes config.lock and renames it over config) gives the file a new inode; the next guest lookup classifies the new inode, so protection survives.
  • Sealed directories propagate to children on lookup, so the subtree does not need to be walked upfront.
  • Checks use host identity, so hard links or other names for a protected inode are covered too.

Implementation sketch

  • devices/src/virtio/fs/protected.rs: ProtectedFs<F: FileSystem> wrapper next to PassthroughFsRo, reusing read_only_open_flags. It guards open, create, mkdir, mknod, symlink, link, unlink, rmdir, rename (incl. RENAME_EXCHANGE), setattr, setxattr, removexattr, fallocate, setupmapping with write access, and ioctl, and forwards everything else. Denied requests fail with EROFS (writes into sealed or reserved locations) or EPERM (protected or pinned entries).

  • worker.rs: new FsServer::Protected(Server<AugmentFs<ProtectedFs<PassthroughFs>>>) variant.

  • FsDeviceConfig.protect: Option<ProtectPolicy>.

  • C API:

    int32_t krun_add_virtiofs_protected(uint32_t ctx_id, const char *c_tag,
                                        const char *c_path, uint64_t shm_size,
                                        const char *const c_policy[]);

    c_policy is a NULL-terminated array of "<rule>:<path>" strings.

PassthroughFs does not change, and the wrapper works the same with the Linux and macOS backends.

Non-goals

  • General ACLs, glob or pattern rules. Pattern rules do not help the Git case: git init/clone must be able to create .git/config and hooks, and gitfiles, --separate-git-dir and bare repositories avoid any name pattern.
  • Changing the policy while the VM runs.
  • Confining the VMM process itself (that is the job of a host sandbox such as Landlock).

Open questions

  1. Is a new krun_add_virtiofs* entry point acceptable, or would you prefer a config struct / builder for share options?
  2. Are all readdirplus entries guaranteed to go through FileSystem::lookup, so classification on lookup is complete?
  3. On macOS, a file the host replaces via rename() reads as ENOENT for up to entry_timeout (virtio-fs (macOS): a file the host replaces via rename() reads as ENOENT in the guest for up to entry_timeout (5s) #888). Protection is unaffected, but should protected names use a shorter entry timeout?
  4. Errno: EROFS vs EPERM vs EACCES for denied operations?

Context

Consumer: seznam/jailoc#274, where in-guest protection (read-only bind mounts plus no_new_privs) currently has to disable sudo and Docker in the guest. We are happy to send the PR (with unit tests for each guarded operation, Linux and macOS) if the direction is acceptable.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions