Files
visor/runsc/container/gofer_to_host_rpc.go
zeekayandHanzo Dev 3da05638df State the ZAP wire of every urpc payload
urpc encodes with encoding/json, which walks the payload on every call: a
realistic control.ExecArgs costs 7084ns and 10 allocations to cross. ZAP
reads a field at a constant offset out of the buffer that arrived, so the
same payload is 740ns and one allocation -- but only if the offsets are
stated, and a stated offset that is wrong is silent corruption rather than
a compile error. So state them from the types themselves.

//tools/zap reflects over the payloads at build time and writes the wire
as Go source with the offsets as constants. It lives inside Bazel because
pkg/sentry/control cannot be imported from outside it: the *_go_proto
packages it depends on have no source in the tree.

Six payloads had no layout to state. A map, an interface and a type from
another module have no offsets, so each is answered in the type:

  SaveOpts.Metadata and StartContainerArgs.InitialCgroups/Limits cross as
  lists of pairs; boot.Route.Destination restates net.IPNet as Subnet;
  OpenMountArgs restates the OCI mount fields the call actually uses;
  StartArgs.Spec and SinkConfig.Config carry their documents as the bytes
  they arrived as; MetricsRegistrationResponse carries the protocol
  buffer the same way; and seccheck's session options become a type
  instead of a map of any.

ExecArgs and MetricsExportData still have none: ExecArgs holds live
kernel objects behind an interface, and MetricsExportData reaches a
prometheus snapshot whose labels are a map.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-22 23:10:33 -07:00

250 lines
6.7 KiB
Go

// Copyright 2024 The gVisor Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package container
import (
"fmt"
"os"
"runtime"
"slices"
"sync"
"syscall"
"golang.org/x/sys/unix"
"gvisor.dev/gvisor/pkg/urpc"
)
// OpenMountArgs is one mount source for the host to open on the gofer's
// behalf. It states the mount in this package because the RPC carries it: the
// OCI type it comes from belongs to another module, and a type from another
// module cannot state its own wire.
type OpenMountArgs struct {
// Source is the path to open.
Source string
// Options are the mount options the OCI spec gave.
Options []string
// UIDs and GIDs are the mount's id maps.
UIDs []Mapping
GIDs []Mapping
// Flags are the Linux mount flags.
Flags uint32
}
// Mapping is one id range: Size ids starting at Host stand for Size ids
// starting at Container.
type Mapping struct {
Container uint32
Host uint32
Size uint32
}
// mapped reports a source the host must open through an id-mapped user
// namespace, which is what carrying both maps means.
func (a *OpenMountArgs) mapped() bool {
return len(a.UIDs) > 0 && len(a.GIDs) > 0
}
type openMountRequest struct {
args *OpenMountArgs
result *OpenMountResult
done chan error
}
// goferToHostRPC is an rpc server that allows the gofer process to do RPC
// calls outside of its namespace container. It is used to configure sandbox
// mounts.
type goferToHostRPC struct {
mu sync.Mutex
openMountRequests chan *openMountRequest
goferPID int
}
// OpenMountResult is a result of the rpcp.OpenMount call.
type OpenMountResult struct {
urpc.FilePayload
}
func createIDMappedUserNS(uidMappings, gidMappings []Mapping) (*os.File, error) {
var sysUIDMaps []syscall.SysProcIDMap
for _, m := range uidMappings {
sysUIDMaps = append(sysUIDMaps, syscall.SysProcIDMap{
ContainerID: int(m.Container),
HostID: int(m.Host),
Size: int(m.Size),
})
}
var sysGIDMaps []syscall.SysProcIDMap
for _, m := range gidMappings {
sysGIDMaps = append(sysGIDMaps, syscall.SysProcIDMap{
ContainerID: int(m.Container),
HostID: int(m.Host),
Size: int(m.Size),
})
}
proc, err := os.StartProcess("/proc/self/exe", []string{"runsc[getUsernsFD]"}, &os.ProcAttr{
Sys: &syscall.SysProcAttr{
Cloneflags: unix.CLONE_NEWUSER,
UidMappings: sysUIDMaps,
GidMappings: sysGIDMaps,
Ptrace: true,
Pdeathsig: syscall.SIGKILL,
},
})
if err != nil {
return nil, fmt.Errorf("failed to start helper process for userns unshare: %w", err)
}
defer func() {
proc.Kill()
proc.Wait()
}()
// Ensure the Go runtime is using pidfds, which are required for the
// proc.Signal() call to correctly guarantee we opened the usernsfd
// for the right process.
//
// Technically, we could fetch the handle from the pidfd directly
// using the PIDFD_GET_USER_NAMESPACE ioctl, but this was only added
// in kernel 6.11.
pidfdUsed := false
proc.WithHandle(func(pidfd uintptr) {
pidfdUsed = true
})
if !pidfdUsed {
return nil, fmt.Errorf("failed to refer to userns helper process as pidfds are not supported")
}
usernsFD, err := os.Open(fmt.Sprintf("/proc/%d/ns/user", proc.Pid))
if err != nil {
return nil, fmt.Errorf("failed to open user namespace descriptor for child PID %d: %w", proc.Pid, err)
}
if err := proc.Signal(syscall.Signal(0)); err != nil {
usernsFD.Close()
return nil, fmt.Errorf("failed to verify userns helper process validity: %w", err)
}
return usernsFD, nil
}
func openIDMappedMount(req *OpenMountArgs) (*os.File, error) {
usernsFD, err := createIDMappedUserNS(req.UIDs, req.GIDs)
if err != nil {
return nil, err
}
defer usernsFD.Close()
openTreeFlags := uint(unix.OPEN_TREE_CLONE | unix.OPEN_TREE_CLOEXEC)
if req.Flags&unix.MS_REC != 0 {
openTreeFlags |= unix.AT_RECURSIVE
}
fd, err := unix.OpenTree(unix.AT_FDCWD, req.Source, openTreeFlags)
if err != nil {
return nil, fmt.Errorf("open_tree(%q) failed: %w", req.Source, err)
}
setattrFlags := uint(unix.AT_EMPTY_PATH)
if slices.Contains(req.Options, "ridmap") {
setattrFlags |= unix.AT_RECURSIVE
}
attr := &unix.MountAttr{
Attr_set: unix.MOUNT_ATTR_IDMAP,
Userns_fd: uint64(usernsFD.Fd()),
}
if err := unix.MountSetattr(fd, "", setattrFlags, attr); err != nil {
unix.Close(fd)
return nil, fmt.Errorf("mount_setattr(%q) failed: %w", req.Source, err)
}
return os.NewFile(uintptr(fd), req.Source), nil
}
func (rpc *goferToHostRPC) handleRequest(req *openMountRequest) {
defer close(req.done)
var fd *os.File
var err error
if req.args.mapped() {
fd, err = openIDMappedMount(req.args)
} else {
fd, err = os.OpenFile(req.args.Source, unix.O_PATH|unix.O_CLOEXEC, 0)
}
if err != nil {
req.done <- err
return
}
req.result.Files = []*os.File{fd}
}
func (rpc *goferToHostRPC) openMountThread() error {
if err := unix.Unshare(unix.CLONE_FS); err != nil {
return fmt.Errorf("open mount thread: unshare filesystem attributes: %w", err)
}
nsFd, err := os.Open(fmt.Sprintf("/proc/%d/ns/mnt", rpc.goferPID))
if err != nil {
return fmt.Errorf("open mount thread: open container mntns: %w", err)
}
defer nsFd.Close()
if err := unix.Setns(int(nsFd.Fd()), unix.CLONE_NEWNS); err != nil {
return fmt.Errorf("open mount thread: join container mntns: %w", err)
}
for req := range rpc.openMountRequests {
rpc.handleRequest(req)
}
return nil
}
// OpenMount opens a specified mount and returns a file descriptor to it. It is
// used when the mount isn't accessible from the gofer user namespace.
func (rpc *goferToHostRPC) OpenMount(reqArg *OpenMountArgs, res *OpenMountResult) error {
rpc.mu.Lock()
defer rpc.mu.Unlock()
if rpc.openMountRequests == nil {
rpc.openMountRequests = make(chan *openMountRequest)
go func() {
// This goroutine holds the current threads forever. It
// never exits, because child processes can set
// PDEATHSIG. It can't serve other go-routines, because
// it does unshare CLONE_FS.
runtime.LockOSThread()
if err := rpc.openMountThread(); err != nil {
for req := range rpc.openMountRequests {
req.done <- err
}
}
panic("unreachable")
}()
}
req := openMountRequest{
args: reqArg,
result: res,
done: make(chan error),
}
rpc.openMountRequests <- &req
err := <-req.done
return err
}