xgo-dev / xgo-dev/sandbox

Defer host FD migration; use fs.FS values for guest filesystem access

Open
#12 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Go
Stars
0
Forks
1
Avg merge
6h 36m
Merged PRs (30d)
18

Description

Proposal

Defer general host file-descriptor migration for now. For LLAR formula inputs, pass filesystem descriptions through ordinary Go values, configure the guest namespace with Sandbox.Mounts, and open files inside Sandbox.Run. The existing io/fs.FS interface is sufficient for this pattern; no new filesystem API or state adapter is needed for os.DirFS.

Keep the existing standard-stream and snapshot-channel handling. Automatic migration of arbitrary open files, directories, pipes, sockets and other kernel resources is out of scope for this step.

Background

A formula should access files through the filesystem configured for its guest, including read-only mounts and overlay behavior. Importing a host FD with gVisor's host.NewFD creates a vfs.FileDescription, but its implementation is the host backend. A guest write still passes through the context-switch interceptor and Sentry's syscall/VFS code, then the host backend writes to the already-open host resource. It does not pass through the corresponding bind, overlay or tmpfs object in the guest mount namespace.

Direct host-FD import:
  guest write(7) -> Switch / inspector -> Sentry FDTable[7]
                -> host file backend -> host file

Open through the guest filesystem:
  guest open("/tmp/input/input.txt") -> guest mount lookup
                                    -> configured filesystem creates the file object
                                    -> guest FDTable[7]
  guest write(7) -> Switch / inspector -> that filesystem's file object

Preserving descriptor numbers alone cannot select the correct guest filesystem object. Reconstructing open resources while preserving their behavior introduces file-position and flag restoration, dup relationships, renamed/deleted files, locks, pipe contents and socket state. That grows toward the resource-restoration part of CRIU. This issue records the decision to postpone that work rather than make it a prerequisite for formula execution.

Workarounds

Use an fs.FS implementation that contains transferable values and opens files when called in the guest. In the pinned Go 1.26.6 source, os.DirFS(dir) simply returns a value of the unexported named string type os.dirFS. Constructing it neither opens nor validates the directory. Open and ReadFile join the stored directory with the requested name and perform file operations at call time.

For example, this function captures an fs.FS on the host and reads through the guest's read-only input mount:

import (
    "io/fs"
    "os"

    "github.com/xgo-dev/sandbox"
)

func readInput(hostSource string) (string, error) {
    const guestSource = "/tmp/input"
    var inputs fs.FS = os.DirFS(guestSource)

    s := sandbox.Sandbox{
        Mounts: []sandbox.Mount{
            {Type: "bind", Source: "/", Target: "/", Options: []string{"ro"}},
            {Type: "tmpfs", Target: "/tmp", Options: []string{"mode=1777"}},
            {Type: "bind", Source: hostSource, Target: guestSource, Options: []string{"ro"}},
            {Type: "proc", Target: "/proc"},
        },
    }

    var result string
    err := s.Run(func() {
        data, err := fs.ReadFile(inputs, "input.txt")
        if err != nil {
            panic(err)
        }
        result = string(data)
    })
    return result, err
}

The normal Sentry library, toolchain and launch requirements still apply. hostSource must be an absolute host directory readable by the guest UID/GID. The read-only / keeps the executable and its loader/libraries visible in this example; it also exposes the host root for reads, so it is not a minimal filesystem policy. Keep the executable outside /tmp, which this example replaces with a fresh tmpfs.

Host:
  inputs = fs.FS(os.dirFS("/tmp/input"))
  No input-file FD is opened.

state:
  Encode the interface's concrete type and directory string.
  Restore the same typed value in the guest; no special DirFS conversion.

Guest:
  fs.ReadFile(inputs, "input.txt")
    -> os.ReadFile("/tmp/input/input.txt")
    -> guest open syscall / Switch / inspector
    -> Sentry mount lookup -> configured input filesystem
    -> read and close the guest-created FD

Return:
  state writes the captured result string back to the host.

Callers can also use inputs.Open(...) and close the returned file entirely inside the closure. Do not capture an already-open *os.File or return it through the transferred object graph. fs.FS is an interface, not a guarantee of transferability: implementations that retain open descriptors or other process-local state still require separate support. In particular, os.OpenRoot(...).FS() is not equivalent to the string-backed os.DirFS approach.

Use a guest path in os.DirFS; state does not translate host paths or create mounts. The same stored path is resolved in whichever process executes the filesystem method. io/fs is primarily a read interface; create output files inside the closure with os.OpenFile/os.WriteFile under a configured output mount. A writable bind persists writes in its host directory; an overlay with a tmpfs upper layer keeps changes in that upper layer.

os.DirFS itself is not a confinement boundary: symlinks may lead outside its directory. The Sentry namespace and mount configuration remain the boundary for filesystem access.

Verification

Verified in a temporary copy of upstream main at d215d421c10ed4df150f14163d92d10a32843890, without the experimental full-FD import changes: Go 1.26.6, Linux ARM64, real Systrap/Sentry execution. Both a captured fs.FS used with fs.ReadFile and one used with FS.Open plus io.ReadAll passed, including result writeback. The input path existed only in the guest mount namespace, while the host fixture lived at a different path. No serializer changes were needed. This specific example has not been run on AMD64.

Reference

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with sentry/fs_linux.go and the Sandbox.Mounts and Sandbox.Run entry points referenced in the proposal. Verify that a captured os.DirFS value can be used inside the guest with fs.ReadFile or FS.Open, while avoiding already-open descriptors. Done means guest filesystem access uses configured mounts and the existing standard-stream and snapshot-channel handling remains unchanged.

Written by the indexing model from the issue text.

Assessment

Tech stack
go
Domain
operating-systems
Issue type
Refactor
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.