Files
visor/runsc/boot/mount_hints_test.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

468 lines
13 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 (
"slices"
"strings"
"testing"
specs "github.com/opencontainers/runtime-spec/specs-go"
"gvisor.dev/gvisor/pkg/sentry/fsimpl/erofs"
"gvisor.dev/gvisor/runsc/config"
)
func TestPodMountHintsHappy(t *testing.T) {
spec := &specs.Spec{
Annotations: map[string]string{
MountPrefix + "mount1.source": "foo",
MountPrefix + "mount1.type": "tmpfs",
MountPrefix + "mount1.share": "pod",
MountPrefix + "mount2.source": "bar",
MountPrefix + "mount2.type": "bind",
MountPrefix + "mount2.share": "container",
MountPrefix + "mount2.options": "rw,private",
},
}
podHints, err := NewPodMountHints(spec)
if err != nil {
t.Fatalf("newPodMountHints failed: %v", err)
}
// Check that fields were set correctly.
mount1 := podHints.Mounts["mount1"]
if want := "mount1"; want != mount1.Name {
t.Errorf("mount1 name, want: %q, got: %q", want, mount1.Name)
}
if want := "foo"; want != mount1.Mount.Source {
t.Errorf("mount1 source, want: %q, got: %q", want, mount1.Mount.Source)
}
if want := "tmpfs"; want != mount1.Mount.Type {
t.Errorf("mount1 type, want: %q, got: %q", want, mount1.Mount.Type)
}
if want := pod; want != mount1.Share {
t.Errorf("mount1 type, want: %q, got: %q", want, mount1.Share)
}
if want := []string(nil); !slices.Equal(want, mount1.Mount.Options) {
t.Errorf("mount1 type, want: %q, got: %q", want, mount1.Mount.Options)
}
if mount1.SuppressDirectFS {
t.Errorf("mount1 SuppressDirectFS, want: false, got: true")
}
mount2 := podHints.Mounts["mount2"]
if want := "mount2"; want != mount2.Name {
t.Errorf("mount2 name, want: %q, got: %q", want, mount2.Name)
}
if want := "bar"; want != mount2.Mount.Source {
t.Errorf("mount2 source, want: %q, got: %q", want, mount2.Mount.Source)
}
if want := "bind"; want != mount2.Mount.Type {
t.Errorf("mount2 type, want: %q, got: %q", want, mount2.Mount.Type)
}
if want := container; want != mount2.Share {
t.Errorf("mount2 type, want: %q, got: %q", want, mount2.Share)
}
if want := []string{"rw", "private"}; !slices.Equal(want, mount2.Mount.Options) {
t.Errorf("mount2 type, want: %q, got: %q", want, mount2.Mount.Options)
}
}
func TestPodMountHintsErrors(t *testing.T) {
for _, tst := range []struct {
name string
annotations map[string]string
error string
}{
{
name: "too short",
annotations: map[string]string{
MountPrefix + "mount1": "foo",
},
error: "invalid mount annotation",
},
{
name: "no name",
annotations: map[string]string{
MountPrefix + ".source": "foo",
},
error: "invalid mount name",
},
{
name: "duplicate source",
annotations: map[string]string{
MountPrefix + "mount1.source": "foo",
MountPrefix + "mount1.type": "tmpfs",
MountPrefix + "mount1.share": "pod",
MountPrefix + "mount2.source": "foo",
MountPrefix + "mount2.type": "bind",
MountPrefix + "mount2.share": "container",
},
error: "have the same mount source",
},
} {
t.Run(tst.name, func(t *testing.T) {
spec := &specs.Spec{Annotations: tst.annotations}
podHints, err := NewPodMountHints(spec)
if err == nil || !strings.Contains(err.Error(), tst.error) {
t.Errorf("newPodMountHints invalid error, want: .*%s.*, got: %v", tst.error, err)
}
if podHints != nil {
t.Errorf("newPodMountHints must return nil on failure: %+v", podHints)
}
})
}
}
// Tests that when a required mount annotation is missing, the entire mount
// hint is omitted and ignored.
func TestPodMountHintsIgnore(t *testing.T) {
for _, tst := range []struct {
name string
annotations map[string]string
}{
{
name: "invalid source",
annotations: map[string]string{
MountPrefix + "mount1.source": "",
MountPrefix + "mount1.type": "tmpfs",
MountPrefix + "mount1.share": "pod",
},
},
{
name: "invalid type",
annotations: map[string]string{
MountPrefix + "mount1.source": "foo",
MountPrefix + "mount1.type": "invalid",
MountPrefix + "mount1.share": "pod",
},
},
{
name: "invalid share",
annotations: map[string]string{
MountPrefix + "mount1.source": "foo",
MountPrefix + "mount1.type": "tmpfs",
MountPrefix + "mount1.share": "invalid",
},
},
} {
t.Run(tst.name, func(t *testing.T) {
spec := &specs.Spec{Annotations: tst.annotations}
podHints, err := NewPodMountHints(spec)
if err != nil {
t.Errorf("newPodMountHints() failed: %v", err)
} else if podHints != nil {
if hint, ok := podHints.Mounts["mount1"]; ok {
t.Errorf("hint was provided when it should have been omitted: %+v", hint)
}
}
})
}
}
func TestPodMountHintsDirectFSInvalid(t *testing.T) {
spec := &specs.Spec{
Annotations: map[string]string{
MountPrefix + "mount1.source": "foo",
MountPrefix + "mount1.type": "bind",
MountPrefix + "mount1.share": "container",
MountPrefix + "mount1.directfs": "maybe",
MountPrefix + "mount2.source": "bar",
MountPrefix + "mount2.type": "bind",
MountPrefix + "mount2.share": "container",
MountPrefix + "mount2.directfs": "off",
},
}
podHints, err := NewPodMountHints(spec)
if err != nil {
t.Fatalf("NewPodMountHints failed: %v", err)
}
mount1, ok := podHints.Mounts["mount1"]
if !ok {
t.Fatalf("mount1 hint should be retained when directfs value is invalid")
}
if mount1.SuppressDirectFS {
t.Errorf("mount1 SuppressDirectFS, want: false, got: true")
}
mount2, ok := podHints.Mounts["mount2"]
if !ok {
t.Fatalf("mount2 hint should be retained when sibling has invalid directfs")
}
if !mount2.SuppressDirectFS {
t.Errorf("mount2 SuppressDirectFS, want: true, got: false")
}
}
func TestPodMountHintsDirectFS(t *testing.T) {
for _, tc := range []struct {
name string
value string
suppress bool
}{
{
name: "default",
value: "default",
suppress: false,
},
{
name: "off",
value: "off",
suppress: true,
},
} {
t.Run(tc.name, func(t *testing.T) {
spec := &specs.Spec{
Annotations: map[string]string{
MountPrefix + "mount1.source": "foo",
MountPrefix + "mount1.type": "bind",
MountPrefix + "mount1.share": "container",
MountPrefix + "mount1.directfs": tc.value,
},
}
podHints, err := NewPodMountHints(spec)
if err != nil {
t.Fatalf("NewPodMountHints failed: %v", err)
}
if got := podHints.Mounts["mount1"].SuppressDirectFS; got != tc.suppress {
t.Errorf("SuppressDirectFS = %t, want %t", got, tc.suppress)
}
})
}
}
func TestIgnoreInvalidMountOptions(t *testing.T) {
spec := &specs.Spec{
Annotations: map[string]string{
MountPrefix + "mount1.source": "foo",
MountPrefix + "mount1.type": "tmpfs",
MountPrefix + "mount1.share": "container",
MountPrefix + "mount1.options": "rw,shared,noexec",
},
}
podHints, err := NewPodMountHints(spec)
if err != nil {
t.Fatalf("newPodMountHints failed: %v", err)
}
mount1 := podHints.Mounts["mount1"]
if want := []string{"rw", "noexec"}; !slices.Equal(want, mount1.Mount.Options) {
t.Errorf("mount2 type, want: %q, got: %q", want, mount1.Mount.Options)
}
}
func TestHintsCheckCompatible(t *testing.T) {
for _, tc := range []struct {
name string
masterOpts []string
replicaOpts []string
err string
}{
{
name: "empty",
},
{
name: "same",
masterOpts: []string{"ro", "noatime", "noexec"},
replicaOpts: []string{"ro", "noatime", "noexec"},
},
{
name: "compatible",
masterOpts: []string{"rw", "atime", "exec"},
replicaOpts: []string{"ro", "noatime", "noexec"},
},
{
name: "unsupported",
masterOpts: []string{"nofoo", "nodev"},
replicaOpts: []string{"foo", "dev"},
},
{
name: "incompatible-ro",
masterOpts: []string{"ro"},
replicaOpts: []string{"rw"},
err: "read-write",
},
{
name: "incompatible-atime",
masterOpts: []string{"noatime"},
replicaOpts: []string{"atime"},
err: "noatime",
},
{
name: "incompatible-exec",
masterOpts: []string{"noexec"},
replicaOpts: []string{"exec"},
err: "noexec",
},
} {
t.Run(tc.name, func(t *testing.T) {
master := MountHint{Mount: specs.Mount{Options: tc.masterOpts}}
replica := specs.Mount{Options: tc.replicaOpts}
if err := master.checkCompatible(&replica); err != nil {
if !strings.Contains(err.Error(), tc.err) {
t.Fatalf("wrong error, want: %q, got: %q", tc.err, err)
}
} else {
if len(tc.err) > 0 {
t.Fatalf("error %q expected", tc.err)
}
}
})
}
}
// TestRootfsHintHappy tests that valid rootfs annotations can be parsed correctly.
func TestRootfsHintHappy(t *testing.T) {
const imagePath = "/tmp/rootfs.img"
spec := &specs.Spec{
Annotations: map[string]string{
RootfsPrefix + "source": imagePath,
RootfsPrefix + "type": erofs.Name,
RootfsPrefix + "overlay": config.MemoryOverlay.String(),
RootfsPrefix + "options": "size=100m",
},
}
hint, err := NewRootfsHint(spec)
if err != nil {
t.Fatalf("NewRootfsHint failed: %v", err)
}
// Check that fields were set correctly.
if hint.Mount.Source != imagePath {
t.Errorf("rootfs source, want: %q, got: %q", imagePath, hint.Mount.Source)
}
if hint.Mount.Type != erofs.Name {
t.Errorf("rootfs type, want: %q, got: %q", erofs.Name, hint.Mount.Type)
}
if hint.Overlay != config.MemoryOverlay {
t.Errorf("rootfs overlay, want: %q, got: %q", config.MemoryOverlay, hint.Overlay)
}
if hint.Size != "100m" {
t.Errorf("rootfs size, want: 100m, got: %q", hint.Size)
}
}
func TestRootfsHintDirectFS(t *testing.T) {
for _, tc := range []struct {
name string
value string
suppress bool
}{
{name: "default", value: "default", suppress: false},
{name: "off", value: "off", suppress: true},
} {
t.Run(tc.name, func(t *testing.T) {
spec := &specs.Spec{
Annotations: map[string]string{
RootfsPrefix + "source": "/tmp/rootfs.img",
RootfsPrefix + "type": erofs.Name,
RootfsPrefix + "directfs": tc.value,
},
}
hint, err := NewRootfsHint(spec)
if err != nil {
t.Fatalf("NewRootfsHint failed: %v", err)
}
if hint.SuppressDirectFS != tc.suppress {
t.Errorf("SuppressDirectFS = %t, want %t", hint.SuppressDirectFS, tc.suppress)
}
})
}
}
// TestRootfsHintErrors tests that proper errors will be returned when parsing
// invalid rootfs annotations.
func TestRootfsHintErrors(t *testing.T) {
const imagePath = "/tmp/rootfs.img"
for _, tst := range []struct {
name string
annotations map[string]string
error string
}{
{
name: "invalid source",
annotations: map[string]string{
RootfsPrefix + "source": "invalid",
RootfsPrefix + "type": erofs.Name,
},
error: "invalid rootfs annotation",
},
{
name: "invalid type",
annotations: map[string]string{
RootfsPrefix + "source": imagePath,
RootfsPrefix + "type": "invalid",
},
error: "invalid rootfs annotation",
},
{
name: "invalid overlay",
annotations: map[string]string{
RootfsPrefix + "source": imagePath,
RootfsPrefix + "type": erofs.Name,
RootfsPrefix + "overlay": "invalid",
},
error: "invalid rootfs annotation",
},
{
name: "invalid key",
annotations: map[string]string{
RootfsPrefix + "invalid": "invalid",
RootfsPrefix + "source": imagePath,
RootfsPrefix + "type": erofs.Name,
RootfsPrefix + "overlay": config.MemoryOverlay.String(),
},
error: "invalid rootfs annotation",
},
{
name: "missing source",
annotations: map[string]string{
RootfsPrefix + "type": erofs.Name,
RootfsPrefix + "overlay": config.MemoryOverlay.String(),
},
error: "rootfs annotations missing required field",
},
{
name: "missing type",
annotations: map[string]string{
RootfsPrefix + "source": imagePath,
RootfsPrefix + "overlay": config.MemoryOverlay.String(),
},
error: "rootfs annotations missing required field",
},
{
name: "invalid directfs",
annotations: map[string]string{
RootfsPrefix + "source": imagePath,
RootfsPrefix + "type": erofs.Name,
RootfsPrefix + "directfs": "maybe",
},
error: "invalid directfs value",
},
} {
t.Run(tst.name, func(t *testing.T) {
spec := &specs.Spec{Annotations: tst.annotations}
hint, err := NewRootfsHint(spec)
if err == nil || !strings.Contains(err.Error(), tst.error) {
t.Errorf("NewRootfsHint invalid error, want: .*%s.*, got: %v", tst.error, err)
}
if hint != nil {
t.Errorf("NewRootfsHint must return nil on failure: %+v", hint)
}
})
}
}