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
468 lines
13 KiB
Go
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)
|
|
}
|
|
})
|
|
}
|
|
}
|