Files
visor/runsc/boot/loader_test.go
T
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

677 lines
17 KiB
Go

// Copyright 2018 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"
"math/rand"
"net"
"os"
"strings"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/moby/sys/capability"
specs "github.com/opencontainers/runtime-spec/specs-go"
"golang.org/x/sys/unix"
"gvisor.dev/gvisor/pkg/abi/linux"
"gvisor.dev/gvisor/pkg/control/server"
"gvisor.dev/gvisor/pkg/cpuid"
"gvisor.dev/gvisor/pkg/fspath"
"gvisor.dev/gvisor/pkg/lisafs"
"gvisor.dev/gvisor/pkg/log"
"gvisor.dev/gvisor/pkg/sentry/kernel/auth"
"gvisor.dev/gvisor/pkg/sentry/seccheck"
"gvisor.dev/gvisor/pkg/sentry/vfs"
"gvisor.dev/gvisor/pkg/sync"
"gvisor.dev/gvisor/pkg/unet"
"gvisor.dev/gvisor/runsc/config"
"gvisor.dev/gvisor/runsc/flag"
"gvisor.dev/gvisor/runsc/fsgofer"
"gvisor.dev/gvisor/runsc/specutils"
)
func init() {
log.SetLevel(log.Debug)
if err := fsgofer.OpenProcSelfFD("/proc/self/fd"); err != nil {
panic(err)
}
}
func testConfig() *config.Config {
testFlags := flag.NewFlagSet("test", flag.ContinueOnError)
config.RegisterFlags(testFlags)
conf, err := config.NewFromFlags(testFlags)
if err != nil {
panic(err)
}
// Change test defaults.
conf.RootDir = "unused_root_dir"
conf.Network = config.NetworkNone
conf.DisableSeccomp = true
return conf
}
// testSpec returns a simple spec that can be used in tests.
func testSpec() *specs.Spec {
return &specs.Spec{
// The host filesystem root is the sandbox root.
Root: &specs.Root{
Path: "/",
Readonly: true,
},
Process: &specs.Process{
Args: []string{"/bin/true"},
},
}
}
// startGofer starts a new gofer routine serving 'root' path. It returns the
// sandbox side of the connection, and a function that when called will stop the
// gofer.
func startGofer(root string, conf *config.Config) (int, func(), error) {
fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0)
if err != nil {
return 0, nil, err
}
sandboxEnd, goferEnd := fds[0], fds[1]
socket, err := unet.NewSocket(goferEnd)
if err != nil {
unix.Close(sandboxEnd)
unix.Close(goferEnd)
return 0, nil, fmt.Errorf("error creating server on FD %d: %v", goferEnd, err)
}
server := lisafs.NewServer()
connImpl := fsgofer.NewConnectionImpl(&fsgofer.Config{
HostUDS: conf.GetHostUDS(),
HostFifo: conf.HostFifo,
DonateMountPointFD: conf.DirectFS,
})
c, err := server.CreateConnection(socket, root, fsgofer.ConnectionOpts(true), connImpl)
if err != nil {
return 0, nil, err
}
server.StartConnection(c)
// Closing the gofer socket will stop the gofer and exit goroutine above.
cleanup := func() {
if err := socket.Close(); err != nil {
log.Warningf("Error closing gofer socket: %v", err)
}
server.Wait()
server.Destroy()
}
return sandboxEnd, cleanup, nil
}
func createLoader(conf *config.Config, spec *specs.Spec) (*Loader, func(), error) {
sock := fmt.Sprintf("\x00loader-test.%010d", rand.Int())
fd, err := server.CreateSocket(sock)
if err != nil {
return nil, nil, fmt.Errorf("failed to create socket: %w", err)
}
sandEnd, cleanup, err := startGofer(spec.Root.Path, conf)
if err != nil {
return nil, nil, fmt.Errorf("failed to start gofer: %w", err)
}
// Loader takes ownership of stdio.
var stdio []int
for _, f := range []*os.File{os.Stdin, os.Stdout, os.Stderr} {
fd := int(f.Fd())
newFd, err := unix.Dup(fd)
if err != nil {
return nil, nil, fmt.Errorf("failed to dup FD %d: %w", fd, err)
}
stdio = append(stdio, newFd)
}
args := Args{
ID: "foo",
Spec: spec,
Conf: conf,
ControllerFD: fd,
GoferFDs: []int{sandEnd},
DevGoferFD: -1,
StdioFDs: stdio,
GoferMountConfs: []specutils.GoferMountConf{{Lower: specutils.Lisafs, Upper: specutils.NoOverlay}},
PodInitConfigFD: -1,
ExecFD: -1,
RootfsUpperTarFD: -1,
}
l, err := New(args)
if err != nil {
cleanup()
return nil, nil, fmt.Errorf("boot.New: %w", err)
}
return l, cleanup, nil
}
// TestRun runs a simple application in a sandbox and checks that it succeeds.
func TestRun(t *testing.T) {
l, cleanup, err := createLoader(testConfig(), testSpec())
if err != nil {
t.Fatalf("error creating loader: %v", err)
}
defer l.Destroy()
defer cleanup()
// Start a goroutine to read the start chan result, otherwise Run will
// block forever.
var resultChanErr error
var wg sync.WaitGroup
wg.Add(1)
go func() {
resultChanErr = <-l.ctrl.manager.startResultChan
wg.Done()
}()
// Run the container.
if err := l.Run(); err != nil {
t.Errorf("error running container: %v", err)
}
// We should have not gotten an error on the startResultChan.
wg.Wait()
if resultChanErr != nil {
t.Errorf("error on startResultChan: %v", resultChanErr)
}
// Wait for the application to exit. It should succeed.
if status := l.WaitExit(); !status.Exited() || status.ExitStatus() != 0 {
t.Errorf("application exited with %s, want exit status 0", status)
}
}
// TestStartSignal tests that the controller Start message will cause
// WaitForStartSignal to return.
func TestStartSignal(t *testing.T) {
l, cleanup, err := createLoader(testConfig(), testSpec())
if err != nil {
t.Fatalf("error creating loader: %v", err)
}
defer l.Destroy()
defer cleanup()
// We aren't going to wait on this application, so the control server
// needs to be shut down manually.
defer l.ctrl.srv.Stop(time.Hour)
// Start a goroutine that calls WaitForStartSignal and writes to a
// channel when it returns.
waitFinished := make(chan struct{})
go func() {
l.WaitForStartSignal()
// Pretend that Run() executed and returned no error.
l.ctrl.manager.startResultChan <- nil
waitFinished <- struct{}{}
}()
// Nothing has been written to the channel, so waitFinished should not
// return. Give it a little bit of time to make sure the goroutine has
// started.
select {
case <-waitFinished:
t.Errorf("WaitForStartSignal completed but it should not have")
case <-time.After(50 * time.Millisecond):
// OK.
}
// Trigger the control server StartRoot method.
cid := "foo"
if err := l.ctrl.manager.StartRoot(&cid, nil); err != nil {
t.Errorf("error calling StartRoot: %v", err)
}
// Now WaitForStartSignal should return (within a short amount of
// time).
select {
case <-waitFinished:
// OK.
case <-time.After(50 * time.Millisecond):
t.Errorf("WaitForStartSignal did not complete but it should have")
}
}
// Test that network=host with raw sockets enabled requires CAP_NET_RAW on the
// host.
func TestHostnetWithRawSockets(t *testing.T) {
// Drop CAP_NET_RAW from effective capabilities, if we have it.
pid := os.Getpid()
caps, err := capability.NewPid2(0)
if err != nil {
t.Fatalf("error getting capabilities for pid %d: %v", pid, err)
}
if err := caps.Load(); err != nil {
t.Fatalf("error loading capabilities: %v", err)
}
if caps.Get(capability.EFFECTIVE, capability.CAP_NET_RAW) {
caps.Unset(capability.EFFECTIVE, capability.CAP_NET_RAW)
if err := caps.Apply(capability.EFFECTIVE); err != nil {
t.Fatalf("error applying capabilities")
}
// Be nice and add it back when we are done.
defer func() {
caps.Set(capability.EFFECTIVE, capability.CAP_NET_RAW)
if err := caps.Apply(capability.EFFECTIVE); err != nil {
t.Fatalf("error restoring capabilities")
}
}()
}
// Configure host network with raw sockets.
conf := testConfig()
conf.Network = config.NetworkHost
conf.EnableRaw = true
// Creating loader should fail.
l, err := New(Args{
ID: "should-fail",
Spec: testSpec(),
Conf: conf,
DevGoferFD: -1,
PodInitConfigFD: -1,
ExecFD: -1,
})
if err == nil {
l.Destroy()
t.Fatalf("expected loader.New() to fail but it did not")
}
// Error message must be about CAP_NET_RAW.
if !strings.Contains(err.Error(), "CAP_NET_RAW") {
t.Errorf("expected error to contain CAP_NET_RAW but got %q", err)
}
}
type CreateMountTestcase struct {
name string
// Spec that will be used to create the mount manager. Note
// that we can't mount procfs without a kernel, so each spec
// MUST contain something other than procfs mounted at /proc.
spec specs.Spec
// Paths that are expected to exist in the resulting fs.
expectedPaths []string
}
func createMountTestcases() []*CreateMountTestcase {
testCases := []*CreateMountTestcase{
{
// Only proc.
name: "only proc mount",
spec: specs.Spec{
Root: &specs.Root{
Path: os.TempDir(),
Readonly: true,
},
Mounts: []specs.Mount{
{
Destination: "/proc",
Type: "tmpfs",
},
},
},
// /proc, /dev, and /sys should always be mounted.
expectedPaths: []string{"/proc", "/dev", "/sys"},
},
{
// Mount at a deep path, with many components that do
// not exist in the root.
name: "deep mount path",
spec: specs.Spec{
Root: &specs.Root{
Path: os.TempDir(),
Readonly: true,
},
Mounts: []specs.Mount{
{
Destination: "/some/very/very/deep/path",
Type: "tmpfs",
},
{
Destination: "/proc",
Type: "tmpfs",
},
},
},
// /some/deep/path should be mounted, along with /proc, /dev, and /sys.
expectedPaths: []string{"/some/very/very/deep/path", "/proc", "/dev", "/sys"},
},
{
// Mounts are nested inside each other.
name: "nested mounts",
spec: specs.Spec{
Root: &specs.Root{
Path: os.TempDir(),
Readonly: true,
},
Mounts: []specs.Mount{
{
Destination: "/proc",
Type: "tmpfs",
},
{
Destination: "/foo",
Type: "tmpfs",
},
{
Destination: "/foo/qux",
Type: "tmpfs",
},
{
// File mounts with the same prefix.
Destination: "/foo/qux-quz",
Type: "tmpfs",
},
{
Destination: "/foo/bar",
Type: "tmpfs",
},
{
Destination: "/foo/bar/baz",
Type: "tmpfs",
},
{
// A deep path that is in foo but not the other mounts.
Destination: "/foo/some/very/very/deep/path",
Type: "tmpfs",
},
},
},
expectedPaths: []string{"/foo", "/foo/bar", "/foo/bar/baz", "/foo/qux",
"/foo/qux-quz", "/foo/some/very/very/deep/path", "/proc", "/dev", "/sys"},
},
{
name: "mount inside /dev",
spec: specs.Spec{
Root: &specs.Root{
Path: os.TempDir(),
Readonly: true,
},
Mounts: []specs.Mount{
{
Destination: "/proc",
Type: "tmpfs",
},
{
Destination: "/dev",
Type: "tmpfs",
},
{
// Mount with the same prefix.
Destination: "/dev/fd-foo",
Type: "tmpfs",
},
{
// Unsupported fs type.
Destination: "/dev/mqueue",
Type: "mqueue",
},
{
Destination: "/dev/foo",
Type: "tmpfs",
},
{
Destination: "/dev/bar",
Type: "tmpfs",
},
},
},
expectedPaths: []string{"/proc", "/dev", "/dev/fd-foo", "/dev/foo", "/dev/bar", "/sys"},
},
{
name: "mounts inside mandatory mounts",
spec: specs.Spec{
Root: &specs.Root{
Path: os.TempDir(),
Readonly: true,
},
Mounts: []specs.Mount{
{
Destination: "/proc",
Type: "tmpfs",
},
{
Destination: "/sys/bar",
Type: "tmpfs",
},
{
Destination: "/tmp/baz",
Type: "tmpfs",
},
{
Destination: "/dev/goo",
Type: "tmpfs",
},
},
},
expectedPaths: []string{"/proc", "/sys", "/sys/bar", "/tmp", "/tmp/baz", "/dev/goo"},
},
}
return testCases
}
// Test that MountNamespace can be created with various specs.
func TestCreateMountNamespace(t *testing.T) {
for _, tc := range createMountTestcases() {
t.Run(tc.name, func(t *testing.T) {
spec := testSpec()
spec.Mounts = tc.spec.Mounts
spec.Root = tc.spec.Root
t.Logf("Using root: %q", spec.Root.Path)
l, loaderCleanup, err := createLoader(testConfig(), spec)
if err != nil {
t.Fatalf("failed to create loader: %v", err)
}
defer l.Destroy()
defer loaderCleanup()
l.mu.Lock()
defer l.mu.Unlock()
mntr := l.newContainerMounter(&l.root)
ctx := l.k.SupervisorContext()
creds := auth.NewRootCredentials(l.root.procArgs.Credentials.UserNamespace)
mns, err := mntr.mountAll(ctx, creds, l.root.spec, l.root.conf, &l.root.procArgs)
if err != nil {
t.Fatalf("mountAll: %v", err)
}
root := mns.Root(ctx)
defer root.DecRef(ctx)
for _, p := range tc.expectedPaths {
target := &vfs.PathOperation{
Root: root,
Start: root,
Path: fspath.Parse(p),
}
if d, err := l.k.VFS().GetDentryAt(ctx, l.root.procArgs.Credentials, target, &vfs.GetDentryOptions{}); err != nil {
t.Errorf("expected path %v to exist with spec %v, but got error %v", p, tc.spec, err)
} else {
d.DecRef(ctx)
}
}
})
}
}
type createMountPointTestCase struct {
name string
path string
mode linux.FileMode
fail bool
}
func TestCreateMountPoint(t *testing.T) {
createMountPointTestCases := []createMountPointTestCase{
{
name: "File",
path: "/test/test-file",
mode: linux.ModeRegular,
},
{
name: "Directory",
path: "/test/test-dir",
mode: linux.ModeDirectory,
},
{
name: "FileWithIntDirs",
path: "/test/a/b/c/test-file",
mode: linux.ModeRegular,
},
{
name: "DirectoryWithIntDirs",
path: "/test/d/e/f/g/test-dir",
mode: linux.ModeDirectory,
},
{
name: "ExistingFile",
path: "/test/test-file",
mode: linux.ModeRegular,
},
{
name: "ExistingDirectory",
path: "/test/test-dir",
mode: linux.ModeDirectory,
},
{
name: "DirVSFile",
path: "/test/test-file",
mode: linux.ModeDirectory,
fail: true,
},
{
name: "FileVSDir",
path: "/test/test-dir",
mode: linux.ModeRegular,
fail: true,
},
}
spec := testSpec()
spec.Root = &specs.Root{
Path: os.TempDir(),
}
spec.Mounts = append(spec.Mounts, specs.Mount{
Destination: "/test",
Type: "tmpfs",
})
t.Logf("Using root: %q", spec.Root.Path)
l, loaderCleanup, err := createLoader(testConfig(), spec)
if err != nil {
t.Fatalf("failed to create loader: %v", err)
}
defer l.Destroy()
defer loaderCleanup()
l.mu.Lock()
defer l.mu.Unlock()
mntr := l.newContainerMounter(&l.root)
ctx := l.k.SupervisorContext()
creds := auth.NewRootCredentials(l.root.procArgs.Credentials.UserNamespace)
mns, err := mntr.mountAll(ctx, creds, l.root.spec, l.root.conf, &l.root.procArgs)
if err != nil {
t.Fatalf("mountAll: %v", err)
}
root := mns.Root(ctx)
defer root.DecRef(ctx)
for _, tc := range createMountPointTestCases {
t.Run(tc.name, func(t *testing.T) {
if err := mntr.makeMountPoint(ctx, creds, mns, tc.path, tc.mode); err != nil {
if tc.fail {
return
}
t.Fatalf("makeMountPoint failed: %v", err)
} else {
if tc.fail {
t.Fatalf("makeMountPoint doesn't fail")
}
}
target := &vfs.PathOperation{
Root: root,
Start: root,
Path: fspath.Parse(tc.path),
}
if mode, err := mntr.getPathMode(ctx, creds, target); err != nil {
t.Errorf("expected path %v, but got error %v", tc.path, err)
} else {
if mode.IsDir() != tc.mode.IsDir() {
t.Errorf("path mode %v doesn't match the mount mode %v", mode, tc.mode)
}
}
})
}
}
func TestMain(m *testing.M) {
cpuid.Initialize()
seccheck.Initialize()
os.Exit(m.Run())
}
func TestNetworkConfig(t *testing.T) {
l, cleanup, err := createLoader(testConfig(), testSpec())
if err != nil {
t.Fatalf("error creating loader: %v", err)
}
defer l.Destroy()
defer cleanup()
// We aren't going to wait on this application, so the control server
// needs to be shut down manually.
defer l.ctrl.srv.Stop(time.Hour)
go func() {
l.WaitForStartSignal()
l.ctrl.manager.startResultChan <- nil
}()
args := &CreateLinksAndRoutesArgs{
LoopbackLinks: []LoopbackLink{
{
Name: "lo",
Addresses: []IPWithPrefix{
{Address: net.IP("\x7f\x00\x00\x01"), PrefixLen: 8},
},
Routes: []Route{
{
Destination: Subnet{
IP: net.IP{127, 0, 0, 0},
Mask: net.IPMask{255, 0, 0, 0},
},
},
},
},
},
}
if err := l.ctrl.manager.SetNetworkArgs(args, nil); err != nil {
t.Errorf("error calling SetNetworkConfig: %v", err)
}
var networkArgs CreateLinksAndRoutesArgs
if err := l.ctrl.manager.GetNetworkConfig(nil, &networkArgs); err != nil {
t.Errorf("error calling NetworkConfig: %v", err)
}
if diff := cmp.Diff(networkArgs.LoopbackLinks, args.LoopbackLinks); diff != "" {
t.Errorf("Network config content mismatch (-want +got):\n%s", diff)
}
}