Files
visor/runsc/boot/mount_hints.go
Shayon Mukherjee b5d24f0d4f runsc/boot: add per-mount directfs opt-out
The --directfs flag is global. When it is enabled, every gofer mount in
the sandbox includes the "directfs" option, which causes the sentry to
require a host FD for the mount root from the gofer's Mount RPC reply.
Custom gofer servers that back virtual or network-backed filesystems
cannot donate such an FD and currently have to disable --directfs
globally, which loses the directfs performance benefit on every other
mount.

Add a new "directfs" key to the dev.gvisor.spec.mount.<NAME>.* and
dev.gvisor.spec.rootfs.* annotations. When set to "off", the sentry
suppresses the directfs mount option for that mount only. When unset or
set to "default", the mount follows the global --directfs setting.

This is an opt-out rather than a full per-mount override. The annotation
does not enable directfs when --directfs is disabled because directfs
requires some sandbox-wide settings (like seccomp filters) that can not
be selectively enabled on containers.

When global directfs is enabled and a fsgofer-handled mount or bind
rootfs opts out, the stock gofer keeps LisaFS seccomp filters because
that mount can still issue LisaFS RPCs.

Fixes #13098
2026-05-12 06:15:02 -04:00

370 lines
11 KiB
Go

// Copyright 2022 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 boot
import (
"fmt"
"path/filepath"
"strings"
specs "github.com/opencontainers/runtime-spec/specs-go"
"gvisor.dev/gvisor/pkg/log"
"gvisor.dev/gvisor/pkg/sentry/fsimpl/erofs"
"gvisor.dev/gvisor/pkg/sentry/fsimpl/tmpfs"
"gvisor.dev/gvisor/runsc/config"
"gvisor.dev/gvisor/runsc/specutils"
)
const (
// MountPrefix is the annotation prefix for mount hints applied at the pod level.
MountPrefix = "dev.gvisor.spec.mount."
// RootfsPrefix is the annotation prefix for rootfs hint applied at the container level.
RootfsPrefix = "dev.gvisor.spec.rootfs."
)
// ShareType indicates who can access/mutate the volume contents.
type ShareType int
const (
invalid ShareType = iota
// container shareType indicates that the mount is used by a single
// container. There are no external observers.
container
// pod shareType indicates that the mount is used by more than one container
// inside the pod. There are no external observers.
pod
// shared shareType indicates that the mount can also be shared with a process
// outside the pod, e.g. NFS.
shared
)
func (s ShareType) String() string {
switch s {
case invalid:
return "invalid"
case container:
return "container"
case pod:
return "pod"
case shared:
return "shared"
default:
return fmt.Sprintf("invalid share value %d", s)
}
}
// PodMountHints contains a collection of mountHints for the pod.
type PodMountHints struct {
Mounts map[string]*MountHint `json:"mounts"`
}
// NewPodMountHints instantiates PodMountHints using spec.
func NewPodMountHints(spec *specs.Spec) (*PodMountHints, error) {
mnts := make(map[string]*MountHint)
for k, v := range spec.Annotations {
// Look for 'dev.gvisor.spec.mount' annotations and parse them.
if strings.HasPrefix(k, MountPrefix) {
// Remove the prefix and split the rest.
parts := strings.Split(k[len(MountPrefix):], ".")
if len(parts) != 2 {
return nil, fmt.Errorf("invalid mount annotation: %s=%s", k, v)
}
name := parts[0]
if len(name) == 0 {
return nil, fmt.Errorf("invalid mount name: %s", name)
}
mnt := mnts[name]
if mnt == nil {
mnt = &MountHint{Name: name}
mnts[name] = mnt
}
if err := mnt.setField(parts[1], v); err != nil {
log.Warningf("ignoring invalid mount annotation (name = %q, key = %q, value = %q): %v", name, parts[1], v, err)
}
}
}
// Validate all the parsed hints.
for name, m := range mnts {
log.Infof("Mount annotation found, name: %s, source: %q, type: %s, share: %v, suppress_directfs: %t", name, m.Mount.Source, m.Mount.Type, m.Share, m.SuppressDirectFS)
if m.Share == invalid || len(m.Mount.Source) == 0 || len(m.Mount.Type) == 0 {
log.Warningf("ignoring mount annotations for %q because of missing required field(s)", name)
delete(mnts, name)
continue
}
// Check for duplicate mount sources.
for name2, m2 := range mnts {
if name != name2 && m.Mount.Source == m2.Mount.Source {
return nil, fmt.Errorf("mounts %q and %q have the same mount source %q", m.Name, m2.Name, m.Mount.Source)
}
}
}
return &PodMountHints{Mounts: mnts}, nil
}
// MountHint represents extra information about mounts that are provided via
// annotations. They can override mount type, provide sharing information so
// that mounts can be correctly shared inside the pod, and tune gofer-specific
// behavior such as suppressing directfs.
// It is part of the sandbox.Sandbox struct, so it must be serializable.
type MountHint struct {
Name string `json:"name"`
Share ShareType `json:"share"`
Mount specs.Mount `json:"mount"`
// SuppressDirectFS suppresses the "directfs" gofer mount option for this
// mount even if --directfs is enabled globally. It does not enable directfs
// when --directfs is disabled because directfs requires some sandbox-wide
// settings (like seccomp filters) that can not be selectively enabled on
// containers.
SuppressDirectFS bool `json:"suppressDirectFS"`
}
func (m *MountHint) setField(key, val string) error {
switch key {
case "source":
if len(val) == 0 {
return fmt.Errorf("source cannot be empty")
}
m.Mount.Source = val
case "type":
return m.setType(val)
case "share":
return m.setShare(val)
case "options":
m.Mount.Options = specutils.FilterMountOptions(strings.Split(val, ","))
case "directfs":
return m.setDirectFS(val)
default:
return fmt.Errorf("invalid mount annotation: %s=%s", key, val)
}
return nil
}
func (m *MountHint) setType(val string) error {
switch val {
case tmpfs.Name, Bind:
m.Mount.Type = val
default:
return fmt.Errorf("invalid type %q", val)
}
return nil
}
func (m *MountHint) setShare(val string) error {
switch val {
case container.String():
m.Share = container
case pod.String():
m.Share = pod
case shared.String():
m.Share = shared
default:
return fmt.Errorf("invalid share value %q", val)
}
return nil
}
func (m *MountHint) setDirectFS(val string) error {
switch val {
case "default":
m.SuppressDirectFS = false
case "off":
m.SuppressDirectFS = true
default:
return fmt.Errorf("invalid directfs value %q, want \"default\" or \"off\"", val)
}
return nil
}
// ShouldShareMount returns true if this mount should be configured as a shared
// mount that is shared among multiple containers in a pod.
func (m *MountHint) ShouldShareMount() bool {
// Only support tmpfs for now. Bind mounts require a common gofer to mount
// all shared volumes.
return m.Mount.Type == tmpfs.Name &&
// A shared mount should be configured for share=container too so:
// 1. Restarting the container does not lose the tmpfs data.
// 2. Repeated mounts in the container reuse the same tmpfs instance.
(m.Share == container || m.Share == pod)
}
// IsSandboxLocal returns true if this mount is only used by the sandbox and
// has no external observers.
func (m *MountHint) IsSandboxLocal() bool {
return m.Share == container || m.Share == pod
}
// checkCompatible verifies that shared mount is compatible with master.
// Master options must be the same or less restrictive than the container mount,
// e.g. master can be 'rw' while container mounts as 'ro'.
func (m *MountHint) checkCompatible(replica *specs.Mount) error {
masterOpts := ParseMountOptions(m.Mount.Options)
replicaOpts := ParseMountOptions(replica.Options)
if masterOpts.ReadOnly && !replicaOpts.ReadOnly {
return fmt.Errorf("cannot mount read-write shared mount because master is read-only, mount: %+v", replica)
}
if masterOpts.Flags.NoExec && !replicaOpts.Flags.NoExec {
return fmt.Errorf("cannot mount exec enabled shared mount because master is noexec, mount: %+v", replica)
}
if masterOpts.Flags.NoATime && !replicaOpts.Flags.NoATime {
return fmt.Errorf("cannot mount atime enabled shared mount because master is noatime, mount: %+v", replica)
}
if masterOpts.Flags.NoSUID && !replicaOpts.Flags.NoSUID {
return fmt.Errorf("cannot mount suid enabled shared mount because master is nosuid, mount: %+v", replica)
}
return nil
}
func (m *MountHint) fileAccessType() config.FileAccessType {
if m.Share == shared {
return config.FileAccessShared
}
if m.ShouldShareMount() {
return config.FileAccessExclusive
}
if m.Share == container {
return config.FileAccessExclusive
}
return config.FileAccessShared
}
// FindMount finds the MountHint that applies to this mount.
func (p *PodMountHints) FindMount(mountSrc string) *MountHint {
for _, m := range p.Mounts {
if m.Mount.Source == mountSrc {
return m
}
}
return nil
}
// RootfsHint represents extra information about rootfs that are provided via
// annotations. They can provide mount source, mount type and overlay config.
type RootfsHint struct {
Mount specs.Mount
Overlay config.OverlayMedium
// Size of overlay tmpfs. Passed as `size={Size}` to tmpfs mount.
// Use default if unspecified.
Size string
SuppressDirectFS bool
}
func (r *RootfsHint) setSource(val string) error {
if !filepath.IsAbs(val) {
return fmt.Errorf("source should be an absolute path, got %q", val)
}
r.Mount.Source = val
return nil
}
func (r *RootfsHint) setType(val string) error {
switch val {
case erofs.Name, Bind:
r.Mount.Type = val
default:
return fmt.Errorf("invalid type %q", val)
}
return nil
}
func (r *RootfsHint) setOption(key, val string) error {
switch key {
case "size":
r.Size = val
default:
return fmt.Errorf("invalid rootfs option: %s=%s", key, val)
}
return nil
}
func (r *RootfsHint) setOptions(val string) error {
for _, option := range strings.Split(val, ",") {
parts := strings.SplitN(option, "=", 2)
if len(parts) != 2 {
return fmt.Errorf("rootfs options must be key=value: %s", option)
}
key, val := parts[0], parts[1]
if err := r.setOption(key, val); err != nil {
return err
}
}
return nil
}
func (r *RootfsHint) setDirectFS(val string) error {
switch val {
case "default":
r.SuppressDirectFS = false
case "off":
r.SuppressDirectFS = true
default:
return fmt.Errorf("invalid directfs value %q, want \"default\" or \"off\"", val)
}
return nil
}
func (r *RootfsHint) setField(key, val string) error {
switch key {
case "source":
return r.setSource(val)
case "type":
return r.setType(val)
case "overlay":
return r.Overlay.Set(val)
case "options":
return r.setOptions(val)
case "directfs":
return r.setDirectFS(val)
default:
return fmt.Errorf("invalid rootfs annotation: %s=%s", key, val)
}
}
// NewRootfsHint instantiates RootfsHint using spec.
func NewRootfsHint(spec *specs.Spec) (*RootfsHint, error) {
var hint *RootfsHint
for k, v := range spec.Annotations {
// Look for 'dev.gvisor.spec.rootfs' annotations and parse them.
if !strings.HasPrefix(k, RootfsPrefix) {
continue
}
// Remove the prefix.
k = k[len(RootfsPrefix):]
if hint == nil {
hint = &RootfsHint{}
}
if err := hint.setField(k, v); err != nil {
return nil, fmt.Errorf("invalid rootfs annotation (key = %q, value = %q): %v", k, v, err)
}
}
// Validate the parsed hint.
if hint != nil {
log.Infof("Rootfs annotations found, source: %q, type: %q, overlay: %q, suppress_directfs: %t", hint.Mount.Source, hint.Mount.Type, hint.Overlay, hint.SuppressDirectFS)
if len(hint.Mount.Source) == 0 || len(hint.Mount.Type) == 0 {
return nil, fmt.Errorf("rootfs annotations missing required field(s): %+v", hint)
}
}
return hint, nil
}