mirror of
https://github.com/hanzoai/visor.git
synced 2026-09-24 17:04:14 +00:00
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
370 lines
11 KiB
Go
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
|
|
}
|