Files
zeekayandHanzo Dev f42ae7beb2 Carry urpc payloads as ZAP, not JSON
encoding/json walked every payload on every call. ZAP reads a field at a
constant offset out of the buffer that arrived, and //tools/zap has
already stated those offsets for every payload, so the transport now asks
the payload for its own bytes and the parse step is gone.

The envelope collapses with it. clientCall and serverCall existed only
because JSON needed one type to hold an any and another to hold a
RawMessage; bytes are bytes, so one Call serves both ends. Framing comes
from the ZAP header, which declares its own length.

A payload that is nothing -- the *struct{} a method takes when it has no
argument or no result -- carries nothing. Anything else without a layout
fails the call naming the type, so a payload that would have arrived
silently empty does not arrive at all.

The methods that took a bare string or returned a bare int now take and
return named payloads, because a scalar has no layout to state and the
rest of the API was already shaped this way.


Co-authored-by: Hanzo Dev <[email protected]>
2026-09-22 23:10:33 -07:00

2494 lines
88 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 container creates and manipulates containers.
package container
import (
"bufio"
"errors"
"fmt"
"os"
"os/exec"
"path"
"regexp"
"slices"
"strconv"
"strings"
"syscall"
"time"
specs "github.com/opencontainers/runtime-spec/specs-go"
"golang.org/x/sys/unix"
"gvisor.dev/gvisor/pkg/abi/linux"
"gvisor.dev/gvisor/pkg/cleanup"
"gvisor.dev/gvisor/pkg/log"
"gvisor.dev/gvisor/pkg/pinring"
"gvisor.dev/gvisor/pkg/sentry/control"
"gvisor.dev/gvisor/pkg/sentry/fsimpl/erofs"
"gvisor.dev/gvisor/pkg/sentry/fsimpl/tmpfs"
"gvisor.dev/gvisor/pkg/sentry/pgalloc"
"gvisor.dev/gvisor/pkg/sighandling"
"gvisor.dev/gvisor/pkg/unet"
"gvisor.dev/gvisor/pkg/urpc"
"gvisor.dev/gvisor/runsc/boot"
"gvisor.dev/gvisor/runsc/cgroup"
"gvisor.dev/gvisor/runsc/config"
"gvisor.dev/gvisor/runsc/console"
"gvisor.dev/gvisor/runsc/donation"
"gvisor.dev/gvisor/runsc/profile"
"gvisor.dev/gvisor/runsc/sandbox"
"gvisor.dev/gvisor/runsc/specutils"
"gvisor.dev/gvisor/runsc/starttime"
)
const cgroupParentAnnotation = "dev.gvisor.spec.cgroup-parent"
// validateID validates the container id.
func validateID(id string) error {
// See libcontainer/factory_linux.go.
idRegex := regexp.MustCompile(`^[\w+\.-]+$`)
if !idRegex.MatchString(id) {
return fmt.Errorf("invalid container id: %v", id)
}
return nil
}
// Container represents a containerized application. When running, the
// container is associated with a single Sandbox.
//
// Container metadata can be saved and loaded to disk. Within a root directory,
// we maintain subdirectories for each container named with the container id.
// The container metadata is stored as a json within the container directory
// in a file named "meta.json". This metadata format is defined by us and is
// not part of the OCI spec.
//
// Containers must write their metadata files after any change to their internal
// states. The entire container directory is deleted when the container is
// destroyed.
//
// When the container is stopped, all processes that belong to the container
// must be stopped before Destroy() returns. containerd makes roughly the
// following calls to stop a container:
// - First it attempts to kill the container process with
// 'runsc kill SIGTERM'. After some time, it escalates to SIGKILL. In a
// separate thread, it's waiting on the container. As soon as the wait
// returns, it moves on to the next step:
// - It calls 'runsc kill --all SIGKILL' to stop every process that belongs to
// the container. 'kill --all SIGKILL' waits for all processes before
// returning.
// - Containerd waits for stdin, stdout and stderr to drain and be closed.
// - It calls 'runsc delete'. runc implementation kills --all SIGKILL once
// again just to be sure, waits, and then proceeds with remaining teardown.
//
// Container is thread-unsafe.
type Container struct {
// ID is the container ID.
ID string `json:"id"`
// Spec is the OCI runtime spec that configures this container.
Spec *specs.Spec `json:"spec"`
// BundleDir is the directory containing the container bundle.
BundleDir string `json:"bundleDir"`
// CreatedAt is the time the container was created.
CreatedAt time.Time `json:"createdAt"`
// Owner is the container owner.
Owner string `json:"owner"`
// ConsoleSocket is the path to a unix domain socket that will receive
// the console FD.
ConsoleSocket string `json:"consoleSocket"`
// Status is the current container Status.
Status Status `json:"status"`
// GoferPid is the PID of the gofer running along side the sandbox. May
// be 0 if the gofer has been killed.
GoferPid sandbox.Pid `json:"goferPid"`
// Sandbox is the sandbox this container is running in. It's set when the
// container is created and reset when the sandbox is destroyed.
Sandbox *sandbox.Sandbox `json:"sandbox"`
// CompatCgroup has the cgroup configuration for the container. For the single
// container case, container cgroup is set in `c.Sandbox` only. CompactCgroup
// is only set for multi-container, where the `c.Sandbox` cgroup represents
// the entire pod.
//
// Note that CompatCgroup is created only for compatibility with tools
// that expect container cgroups to exist. Setting limits here makes no change
// to the container in question.
CompatCgroup cgroup.CgroupJSON `json:"compatCgroup"`
// Saver handles load from/save to the state file safely from multiple
// processes.
Saver StateFile `json:"saver"`
// GoferMountConfs contains information about how the gofer mounts have been
// overlaid (with tmpfs or overlayfs). The first entry is for rootfs and the
// following entries are for bind mounts in Spec.Mounts (in the same order).
GoferMountConfs specutils.GoferMountConfFlags `json:"goferMountConfs"`
//
// Fields below this line are not saved in the state file and will not
// be preserved across commands.
//
// goferIsChild is set if a gofer process is a child of the current process.
//
// This field isn't saved to json, because only a creator of a gofer
// process will have it as a child process.
goferIsChild bool `nojson:"true"`
// sentryExitSock is the creator's end of the root gofer's
// `--sync-sentry-exit-fd` socket,
// A pidfd of the sandbox process is sent over it once the sandbox has been
// spawned, and then it is closed.
sentryExitSock *os.File `nojson:"true"`
// pinRingFile is the pin ring (see `//pkg/pinring`)
// It is held between the gofer and the boot process's spawn.
pinRingFile *os.File `nojson:"true"`
}
// Args is used to configure a new container.
type Args struct {
// ID is the container unique identifier.
ID string
// Spec is the OCI spec that describes the container.
Spec *specs.Spec
// BundleDir is the directory containing the container bundle.
BundleDir string
// ConsoleSocket is the path to a unix domain socket that will receive
// the console FD. It may be empty.
ConsoleSocket string
// PIDFile is the filename where the container's root process PID will be
// written to. It may be empty.
PIDFile string
// UserLog is the filename to send user-visible logs to. It may be empty.
//
// It only applies for the init container.
UserLog string
// Attached indicates that the sandbox lifecycle is attached with the caller.
// If the caller exits, the sandbox should exit too.
//
// It only applies for the init container.
Attached bool
// PassFiles are user-supplied files from the host to be exposed to the
// sandboxed app.
PassFiles map[int]*os.File
// ExecFile is the host file used for program execution.
ExecFile *os.File
// If FSRestoreImagePath is non-empty, it is a path to a filesystem
// checkpoint that should be restored. FSRestoreImagePath may only be set
// for containers in a new Sandbox process.
FSRestoreImagePath string
FSRestoreDirect bool
}
// New creates the container in a new Sandbox process, unless the metadata
// indicates that an existing Sandbox should be used. The caller must call
// Destroy() on the container.
func New(conf *config.Config, args Args) (*Container, error) {
log.Debugf("Create container, cid: %s, rootDir: %q", args.ID, conf.RootDir)
if err := validateID(args.ID); err != nil {
return nil, err
}
if err := os.MkdirAll(conf.RootDir, 0711); err != nil {
return nil, fmt.Errorf("creating container root directory %q: %v", conf.RootDir, err)
}
if err := modifySpecForDirectfs(conf, args.Spec); err != nil {
return nil, fmt.Errorf("failed to modify spec for directfs: %v", err)
}
sandboxID := args.ID
if !specutils.IsRootContainer(args.Spec) {
var ok bool
sandboxID, ok = specutils.SandboxID(args.Spec)
if !ok {
return nil, fmt.Errorf("no sandbox ID found when creating container")
}
if args.FSRestoreImagePath != "" {
return nil, fmt.Errorf("cannot set FSRestoreImagePath when creating container in existing sandbox")
}
}
c := &Container{
ID: args.ID,
Spec: args.Spec,
ConsoleSocket: args.ConsoleSocket,
BundleDir: args.BundleDir,
Status: Creating,
CreatedAt: time.Now(),
Owner: os.Getenv("USER"),
Saver: StateFile{
RootDir: conf.RootDir,
ID: FullID{
SandboxID: sandboxID,
ContainerID: args.ID,
},
},
}
// The Cleanup object cleans up partially created containers when an error
// occurs. Any errors occurring during cleanup itself are ignored.
cu := cleanup.Make(func() { _ = c.Destroy() })
defer cu.Clean()
// Lock the container metadata file to prevent concurrent creations of
// containers with the same id.
if err := c.Saver.LockForNew(); err != nil {
// As we have not allocated any resources yet, we revoke the clean-up operation.
// Otherwise, we may accidentally destroy an existing container.
cu.Release()
return nil, fmt.Errorf("cannot lock container metadata file: %w", err)
}
defer c.Saver.UnlockOrDie()
// If the metadata annotations indicate that this container should be started
// in an existing sandbox, we must do so. These are the possible metadata
// annotation states:
// 1. No annotations: it means that there is a single container and this
// container is obviously the root. Both container and sandbox share the
// ID.
// 2. Container type == sandbox: it means this is the root container
// starting the sandbox. Both container and sandbox share the same ID.
// 3. Container type == container: it means this is a subcontainer of an
// already started sandbox. In this case, container ID is different than
// the sandbox ID.
if specutils.IsRootContainer(args.Spec) {
if err := c.createRoot(conf, args, sandboxID); err != nil {
return nil, err
}
} else {
log.Debugf("Creating new container, cid: %s, sandbox: %s", c.ID, sandboxID)
// Find the sandbox associated with this ID.
fullID := FullID{
SandboxID: sandboxID,
ContainerID: sandboxID,
}
sb, err := Load(conf.RootDir, fullID, LoadOpts{Exact: true})
if err != nil {
return nil, fmt.Errorf("cannot load sandbox: %w", err)
}
c.Sandbox = sb.Sandbox
if err := c.createSubcontainer(conf, args.Spec); err != nil {
return nil, err
}
}
c.changeStatus(Created)
// Save the metadata file.
if err := c.saveLocked(); err != nil {
return nil, err
}
// "If any prestart hook fails, the runtime MUST generate an error,
// stop and destroy the container" -OCI spec.
if c.Spec.Hooks != nil {
// Even though the hook name is Prestart, runc used to call it from create.
// For this reason, it's now deprecated, but the spec requires it to be
// called *before* CreateRuntime and CreateRuntime must be called in create.
//
// "For runtimes that implement the deprecated prestart hooks as
// createRuntime hooks, createRuntime hooks MUST be called after the
// prestart hooks."
if err := specutils.ExecuteHooks(c.Spec.Hooks.Prestart, c.State()); err != nil {
return nil, err
}
if err := specutils.ExecuteHooks(c.Spec.Hooks.CreateRuntime, c.State()); err != nil {
return nil, err
}
}
// Write the PID file. Containerd considers the call to create complete after
// this file is created, so it must be the last thing we do.
if args.PIDFile != "" {
if err := os.WriteFile(args.PIDFile, []byte(strconv.Itoa(c.SandboxPid())), 0644); err != nil {
return nil, fmt.Errorf("error writing PID file: %v", err)
}
}
cu.Release()
return c, nil
}
func (c *Container) createRoot(conf *config.Config, args Args, sandboxID string) error {
log.Debugf("Creating new sandbox for container, cid: %s", args.ID)
if args.Spec.Linux == nil {
args.Spec.Linux = &specs.Linux{}
}
// Don't force the use of cgroups in tests because they lack permission to do so.
if args.Spec.Linux.CgroupsPath == "" && !conf.TestOnlyAllowRunAsCurrentUserWithoutChroot {
args.Spec.Linux.CgroupsPath = "/" + args.ID
}
var subCgroup, parentCgroup, containerCgroup cgroup.Cgroup
if !conf.IgnoreCgroups {
var err error
// Create and join cgroup before processes are created to ensure they are
// part of the cgroup from the start (and all their children processes).
parentCgroup, subCgroup, err = c.setupCgroupForRoot(conf, args.Spec)
if err != nil {
return fmt.Errorf("cannot set up cgroup for root: %w", err)
}
// Join the child cgroup when using cgroupfs. Joining non leaf-node
// cgroups is illegal in cgroupsv2 and will return EBUSY.
if subCgroup != nil && !conf.SystemdCgroup && cgroup.IsOnlyV2() {
containerCgroup = subCgroup
} else {
containerCgroup = parentCgroup
}
}
c.CompatCgroup = cgroup.CgroupJSON{Cgroup: subCgroup}
mountHints, err := boot.NewPodMountHints(args.Spec)
if err != nil {
return fmt.Errorf("error creating pod mount hints: %w", err)
}
if err := nvProxyPreGoferHostSetup(args.Spec, conf); err != nil {
return err
}
if err := cgroup.RunInCgroup(containerCgroup, func(cloneIntoCgroupFD *os.File) error {
ioFiles, goferFilestores, devIOFile, specFile, err := c.createGoferProcess(conf, mountHints, args.Attached, cloneIntoCgroupFD)
if err != nil {
return fmt.Errorf("cannot create gofer process: %w", err)
}
// Start a new sandbox for this container. Any errors after this point
// must destroy the container.
sandArgs := &sandbox.Args{
ID: sandboxID,
Spec: args.Spec,
BundleDir: args.BundleDir,
ConsoleSocket: args.ConsoleSocket,
UserLog: args.UserLog,
IOFiles: ioFiles,
DevIOFile: devIOFile,
MountsFile: specFile,
Cgroup: containerCgroup,
CloneIntoCgroupFD: cloneIntoCgroupFD,
Attached: args.Attached,
GoferFilestoreFiles: goferFilestores,
GoferMountConfs: c.GoferMountConfs,
MountHints: mountHints,
PassFiles: args.PassFiles,
ExecFile: args.ExecFile,
FSRestoreImagePath: args.FSRestoreImagePath,
FSRestoreDirect: args.FSRestoreDirect,
PinRingFile: c.pinRingFile,
}
sand, err := sandbox.New(conf, sandArgs)
if err != nil {
return fmt.Errorf("cannot create sandbox: %w", err)
}
c.Sandbox = sand
c.pinRingFile = nil // Now owned by the sandbox's donation agency.
if c.sentryExitSock != nil {
// The gofer waits on this pidfd before exiting, so it is
// always the last holder of the pin ring.
if err := pinring.SendPidfd(c.sentryExitSock, sand.Pid.Load()); err != nil {
log.Warningf("Cannot send the sandbox's pidfd to the gofer (gofer exit may race the sandbox's): %v", err)
}
c.sentryExitSock.Close()
c.sentryExitSock = nil
}
return nil
}); err != nil {
return err
}
return nil
}
func (c *Container) createSubcontainer(conf *config.Config, spec *specs.Spec) error {
subCgroup, err := c.setupCgroupForSubcontainer(conf, spec)
if err != nil {
return err
}
c.CompatCgroup = cgroup.CgroupJSON{Cgroup: subCgroup}
// If the console control socket file is provided, then create a new
// pty master/slave pair and send the TTY to the sandbox process.
var tty *os.File
if c.ConsoleSocket != "" {
// Create a new TTY pair and send the master on the provided socket.
var err error
tty, err = console.NewWithSocket(c.ConsoleSocket)
if err != nil {
return fmt.Errorf("setting up console with socket %q: %w", c.ConsoleSocket, err)
}
// tty file is transferred to the sandbox, then it can be closed here.
defer tty.Close()
}
if err := c.Sandbox.CreateSubcontainer(conf, c.ID, tty); err != nil {
return fmt.Errorf("cannot create subcontainer: %w", err)
}
return nil
}
// Start starts running the containerized process inside the sandbox.
func (c *Container) Start(conf *config.Config) error {
log.Debugf("Start container, cid: %s", c.ID)
return c.startImpl(conf, "start", c.Sandbox.StartRoot, c.Sandbox.StartSubcontainer)
}
// Restore takes a container and replaces its kernel and file system
// to restore a container from its state file.
func (c *Container) Restore(conf *config.Config, imagePath string, direct, background bool, networkArgs *boot.CreateLinksAndRoutesArgs) error {
log.Debugf("Restore container, cid: %s", c.ID)
restore := func(conf *config.Config, spec *specs.Spec) error {
return c.Sandbox.Restore(conf, spec, c.ID, imagePath, direct, background, networkArgs)
}
return c.startImpl(conf, "restore", restore, c.Sandbox.RestoreSubcontainer)
}
func (c *Container) startImpl(conf *config.Config, action string, startRoot func(conf *config.Config, spec *specs.Spec) error, startSub func(spec *specs.Spec, conf *config.Config, cid string, stdios, goferFiles, goferFilestores []*os.File, devIOFile *os.File, goferConfs []specutils.GoferMountConf) error) error {
if err := c.Saver.lock(BlockAcquire); err != nil {
return err
}
unlock := cleanup.Make(c.Saver.UnlockOrDie)
defer unlock.Clean()
if err := c.requireStatus(action, Created); err != nil {
return err
}
// "If any prestart hook fails, the runtime MUST generate an error,
// stop and destroy the container" -OCI spec.
if c.Spec.Hooks != nil && len(c.Spec.Hooks.StartContainer) > 0 {
log.Warningf("StartContainer hook skipped because running inside container namespace is not supported")
}
if specutils.IsRootContainer(c.Spec) {
if err := startRoot(conf, c.Spec); err != nil {
return err
}
} else {
// Join cgroup to start gofer process to ensure it's part of the cgroup from
// the start (and all their children processes).
if err := cgroup.RunInCgroup(c.Sandbox.CgroupJSON.Cgroup, func(cloneIntoCgroupFD *os.File) error {
// Create the gofer process.
goferFiles, goferFilestores, devIOFile, mountsFile, err := c.createGoferProcess(conf, c.Sandbox.MountHints, false /* attached */, cloneIntoCgroupFD)
if err != nil {
return err
}
defer func() {
if mountsFile != nil {
_ = mountsFile.Close()
}
if devIOFile != nil {
_ = devIOFile.Close()
}
for _, f := range goferFiles {
_ = f.Close()
}
for _, f := range goferFilestores {
_ = f.Close()
}
}()
if mountsFile != nil {
cleanMounts, err := specutils.ReadMounts(mountsFile)
if err != nil {
return fmt.Errorf("reading mounts file: %v", err)
}
c.Spec.Mounts = cleanMounts
}
// Setup stdios if the container is not using terminal. Otherwise TTY was
// already setup in create.
var stdios []*os.File
if !c.Spec.Process.Terminal {
stdios = []*os.File{os.Stdin, os.Stdout, os.Stderr}
}
return startSub(c.Spec, conf, c.ID, stdios, goferFiles, goferFilestores, devIOFile, c.GoferMountConfs)
}); err != nil {
return err
}
}
// "If any poststart hook fails, the runtime MUST generate an error,
// stop the container, and continue the lifecycle at step 12" - OCI spec.
if c.Spec.Hooks != nil {
if err := specutils.ExecuteHooks(c.Spec.Hooks.Poststart, c.State()); err != nil {
return err
}
}
c.changeStatus(Running)
if err := c.saveLocked(); err != nil {
return err
}
// Release lock before adjusting OOM score because the lock is acquired there.
unlock.Clean()
// Adjust the oom_score_adj for sandbox. This must be done after saveLocked().
if err := adjustSandboxOOMScoreAdj(c.Sandbox, c.Spec, c.Saver.RootDir, false); err != nil {
return err
}
// Set container's oom_score_adj to the gofer since it is dedicated to
// the container, in case the gofer uses up too much memory.
return c.adjustGoferOOMScoreAdj()
}
// Run is a helper that calls Create + Start + Wait.
func Run(conf *config.Config, args Args) (unix.WaitStatus, error) {
log.Debugf("Run container, cid: %s, rootDir: %q", args.ID, conf.RootDir)
c, err := New(conf, args)
if err != nil {
return 0, fmt.Errorf("creating container: %v", err)
}
// Clean up partially created container if an error occurs.
// Any errors returned by Destroy() itself are ignored.
cu := cleanup.Make(func() {
c.Destroy()
})
defer cu.Clean()
if err := c.Start(conf); err != nil {
return 0, fmt.Errorf("starting container: %v", err)
}
// If we allocate a terminal, forward signals to the sandbox process.
// Otherwise, Ctrl+C will terminate this process and its children,
// including the terminal.
if c.Spec.Process.Terminal {
stopForwarding := c.ForwardSignals(0, true /* fgProcess */)
defer stopForwarding()
}
if args.Attached {
return c.Wait()
}
cu.Release()
return 0, nil
}
// Update sets the resources of a running container as configured.
func (c *Container) Update(res *specs.LinuxResources) error {
log.Debugf("Set resources for container, cid: %s", c.ID)
if err := c.requireStatus("set resources for", Created, Running); err != nil {
return err
}
if c.Sandbox == nil {
return fmt.Errorf("sandbox cannot be nil")
}
if c.Sandbox.IsRootContainer(c.ID) {
cg := c.Sandbox.CgroupJSON.Cgroup
if cg == nil {
return fmt.Errorf("cgroup cannot be nil")
}
if err := cg.Update(res); err != nil {
// set back to original
if err2 := cg.Update(c.Spec.Linux.Resources); err2 != nil {
return fmt.Errorf("setting back cgroup configs failed due to error: %v, your state file and actual configs might be inconsistent", err2)
}
return err
}
}
c.Spec.Linux.Resources = res
c.Saver.lock(BlockAcquire)
defer c.Saver.unlock()
return c.saveLocked()
}
// Execute runs the specified command in the container. It returns the PID of
// the newly created process.
func (c *Container) Execute(conf *config.Config, args *control.ExecArgs) (int32, error) {
log.Debugf("Execute in container, cid: %s, args: %+v", c.ID, args)
if err := c.requireStatus("execute in", Created, Running); err != nil {
return 0, err
}
args.ContainerID = c.ID
return c.Sandbox.Execute(conf, args)
}
// Event returns events for the container.
func (c *Container) Event() (*boot.EventOut, error) {
log.Debugf("Getting events for container, cid: %s", c.ID)
if err := c.requireStatus("get events for", Created, Running, Paused); err != nil {
return nil, err
}
event, err := c.Sandbox.Event(c.ID)
if err != nil {
return nil, err
}
// CPU stats can utilize host cgroups for accuracy.
c.populateStats(event)
return event, nil
}
// PortForward starts port forwarding to the container.
func (c *Container) PortForward(opts *boot.PortForwardOpts) error {
if err := c.requireStatus("port forward", Running); err != nil {
return err
}
opts.ContainerID = c.ID
return c.Sandbox.PortForward(opts)
}
// SandboxPid returns the Getpid of the sandbox the container is running in, or -1 if the
// container is not running.
func (c *Container) SandboxPid() int {
if err := c.requireStatus("get PID", Created, Running, Paused); err != nil {
return -1
}
return c.Sandbox.Getpid()
}
// Wait waits for the container to exit, and returns its WaitStatus.
// Call to wait on a stopped container is needed to retrieve the exit status
// and wait returns immediately.
func (c *Container) Wait() (unix.WaitStatus, error) {
log.Debugf("Wait on container, cid: %s", c.ID)
if c.goferIsChild {
// The gofer process is a child of the current process, so we need to wait
// on it and collect its zombie. Start a goroutine to reap c.GoferPid. Do
// not block the main thread waiting for the gofer to exit. In certain
// multi-container scenarios, the gofer may outlive its container. For
// example, if a gofer-backed FD is donated by the application to another
// container within the sandbox. The lifecycle of the gofer will be tied to
// both containers, whichever lives longer.
go func() {
goferPid := c.GoferPid.Load()
if goferPid == 0 {
return
}
// The gofer process could be racily reaped by c.waitForStopped() so
// ignore ECHILD errors.
var status unix.WaitStatus
if _, err := unix.Wait4(goferPid, &status, 0, nil); err == nil {
log.Infof("Gofer process (PID=%d) reaped: exit code = %v", goferPid, status.ExitStatus())
} else if !errors.Is(err, unix.ECHILD) {
log.Warningf("error reaping the gofer process (PID=%d) from background goroutine: %v", goferPid, err)
}
c.GoferPid.Store(0)
}()
}
ws, err := c.Sandbox.Wait(c.ID)
if err == nil {
// Wait succeeded, container is not running anymore.
c.changeStatus(Stopped)
}
return ws, err
}
// WaitRootPID waits for process 'pid' in the sandbox's PID namespace and
// returns its WaitStatus.
func (c *Container) WaitRootPID(pid int32) (unix.WaitStatus, error) {
log.Debugf("Wait on process %d in sandbox, cid: %s", pid, c.Sandbox.ID)
if !c.IsSandboxRunning() {
return 0, fmt.Errorf("sandbox is not running")
}
return c.Sandbox.WaitPID(c.Sandbox.ID, pid)
}
// WaitPID waits for process 'pid' in the container's PID namespace and returns
// its WaitStatus.
func (c *Container) WaitPID(pid int32) (unix.WaitStatus, error) {
log.Debugf("Wait on process %d in container, cid: %s", pid, c.ID)
if !c.IsSandboxRunning() {
return 0, fmt.Errorf("sandbox is not running")
}
return c.Sandbox.WaitPID(c.ID, pid)
}
// WaitCheckpoint waits for the Kernel to have been successfully checkpointed.
func (c *Container) WaitCheckpoint() error {
log.Debugf("Waiting for checkpoint to complete in container, cid: %s", c.ID)
if !c.IsSandboxRunning() {
return fmt.Errorf("sandbox is not running")
}
return c.Sandbox.WaitCheckpoint()
}
// WaitRestore waits for the Kernel to have been successfully restored.
func (c *Container) WaitRestore() error {
log.Debugf("Waiting for restore to complete in container, cid: %s", c.ID)
if !c.IsSandboxRunning() {
return fmt.Errorf("sandbox is not running")
}
return c.Sandbox.WaitRestore()
}
// WaitFSCheckpoint waits for a filesystem checkpoint to have successfully been
// saved.
func (c *Container) WaitFSCheckpoint() error {
log.Debugf("Waiting for filesystem checkpoint to complete in container, cid: %s", c.ID)
if !c.IsSandboxRunning() {
return fmt.Errorf("sandbox is not running")
}
return c.Sandbox.WaitFSCheckpoint()
}
// WaitFSRestore waits for filesystems to have been successfully restored from
// checkpoint.
func (c *Container) WaitFSRestore() error {
log.Debugf("Waiting for filesystem restore to complete in container, cid: %s", c.ID)
if !c.IsSandboxRunning() {
return fmt.Errorf("sandbox is not running")
}
return c.Sandbox.WaitFSRestore(c.ID)
}
// TarRootfsUpperLayer serializes the rootfs upper layer of the container to a tar file. When
// the rootfs is not an overlayfs, it returns an error. It writes the tar file
// to outFD.
func (c *Container) TarRootfsUpperLayer(outFD *os.File) error {
log.Debugf("TarRootfsUpperLayer, cid: %s", c.ID)
if !c.IsSandboxRunning() {
return fmt.Errorf("sandbox is not running")
}
return c.Sandbox.TarRootfsUpperLayer(c.ID, outFD)
}
// SignalContainer sends the signal to the container. If all is true and signal
// is SIGKILL, then waits for all processes to exit before returning.
// SignalContainer returns an error if the container is already stopped.
// TODO(b/113680494): Distinguish different error types.
func (c *Container) SignalContainer(sig unix.Signal, all bool) error {
log.Debugf("Signal container, cid: %s, signal: %v (%d)", c.ID, sig, sig)
if c.Status == Created {
return c.signalCreated(sig, all)
}
return c.signalRunning(sig, all)
}
// signalRunning delivers a signal to the processes of a Running (or Stopped)
// container via the sandbox.
func (c *Container) signalRunning(sig unix.Signal, all bool) error {
// Signaling container in Stopped state is allowed. When all=false,
// an error will be returned anyway; when all=true, this allows
// sending signal to other processes inside the container even
// after the init process exits. This is especially useful for
// container cleanup.
if err := c.requireStatus("signal", Running, Stopped); err != nil {
return err
}
if !c.IsSandboxRunning() {
return fmt.Errorf("sandbox is not running")
}
return c.Sandbox.SignalContainer(c.ID, sig, all)
}
// signalCreated handles a signal sent to a container that appears to be in the
// Created state (start was never called). Such a container has no process to
// receive the signal, so a terminating signal (SIGKILL/SIGTERM) is honored by
// stopping the container.
func (c *Container) signalCreated(sig unix.Signal, all bool) error {
if err := c.Saver.lock(BlockAcquire); err != nil {
return err
}
defer c.Saver.UnlockOrDie()
// The status read before acquiring the lock may be stale: a concurrent start
// could have moved the container to Running (or a kill/delete to Stopped).
// Re-read the persisted state under the lock before acting on it.
reloaded := &Container{}
if err := c.Saver.loadLocked(reloaded); err != nil {
return err
}
c.Status = reloaded.Status
if c.Status != Created {
// The container now has a process (or has already stopped), so deliver
// the signal normally.
return c.signalRunning(sig, all)
}
// Still Created: only a terminating signal is actionable; there is nothing
// to deliver other signals to, so drop them.
if sig != unix.SIGKILL && sig != unix.SIGTERM {
return nil
}
// Tear down the container. For the root container this SIGKILLs the sandbox
// (boot) process; for a subcontainer it destroys the not-yet-started
// container in the sentry.
if c.Sandbox != nil {
if err := c.Sandbox.DestroyContainer(c.ID); err != nil {
return fmt.Errorf("destroying created container %q: %w", c.ID, err)
}
}
c.changeStatus(Stopped)
return c.saveLocked()
}
// SignalProcess sends sig to a specific process in the container.
func (c *Container) SignalProcess(sig unix.Signal, pid int32) error {
log.Debugf("Signal process %d in container, cid: %s, signal: %v (%d)", pid, c.ID, sig, sig)
if err := c.requireStatus("signal a process inside", Running); err != nil {
return err
}
if !c.IsSandboxRunning() {
return fmt.Errorf("sandbox is not running")
}
return c.Sandbox.SignalProcess(c.ID, int32(pid), sig, false)
}
// SignalProcessGroup sends sig to all processes in the given process group
// inside the container.
func (c *Container) SignalProcessGroup(sig unix.Signal, pgid int32) error {
log.Debugf("Signal process group %d in container, cid: %s, signal: %v (%d)", pgid, c.ID, sig, sig)
if err := c.requireStatus("signal a process group inside", Running); err != nil {
return err
}
if !c.IsSandboxRunning() {
return fmt.Errorf("sandbox is not running")
}
return c.Sandbox.SignalProcessGroup(c.ID, pgid, sig)
}
// ForwardSignals forwards all signals received by the current process to the
// container process inside the sandbox. It returns a function that will stop
// forwarding signals.
func (c *Container) ForwardSignals(pid int32, fgProcess bool) func() {
log.Debugf("Forwarding all signals to container, cid: %s, PIDPID: %d, fgProcess: %t", c.ID, pid, fgProcess)
stop := sighandling.StartSignalForwarding(func(sig linux.Signal) {
log.Debugf("Forwarding signal %d to container, cid: %s, PID: %d, fgProcess: %t", sig, c.ID, pid, fgProcess)
if err := c.Sandbox.SignalProcess(c.ID, pid, unix.Signal(sig), fgProcess); err != nil {
log.Warningf("error forwarding signal %d to container %q: %v", sig, c.ID, err)
}
})
return func() {
log.Debugf("Done forwarding signals to container, cid: %s, PID: %d, fgProcess: %t", c.ID, pid, fgProcess)
stop()
}
}
// Checkpoint sends the checkpoint call to the container.
// The statefile will be written to f, the file at the specified image-path.
func (c *Container) Checkpoint(conf *config.Config, imagePath string, opts sandbox.CheckpointOpts) error {
log.Debugf("Checkpoint container, cid: %s", c.ID)
if err := c.requireStatus("checkpoint", Created, Running, Paused); err != nil {
return err
}
return c.Sandbox.Checkpoint(conf, c.ID, imagePath, opts)
}
// FSSave sends the filesystem checkpointing call to the container.
func (c *Container) FSSave(conf *config.Config, imagePath string, opts sandbox.FSSaveOpts) error {
log.Debugf("Checkpoint container filesystem, cid: %s", c.ID)
if err := c.requireStatus("filesystem checkpoint", Created, Running, Paused); err != nil {
return err
}
return c.Sandbox.FSSave(conf, c.ID, imagePath, opts)
}
// Pause suspends the container and its kernel.
// The call only succeeds if the container's status is created or running.
func (c *Container) Pause() error {
log.Debugf("Pausing container, cid: %s", c.ID)
if err := c.Saver.lock(BlockAcquire); err != nil {
return err
}
defer c.Saver.UnlockOrDie()
if c.Status != Created && c.Status != Running {
return fmt.Errorf("cannot pause container %q in state %v", c.ID, c.Status)
}
if err := c.Sandbox.Pause(c.ID); err != nil {
return fmt.Errorf("pausing container %q: %v", c.ID, err)
}
c.changeStatus(Paused)
return c.saveLocked()
}
// Resume unpauses the container and its kernel.
// The call only succeeds if the container's status is paused.
func (c *Container) Resume() error {
log.Debugf("Resuming container, cid: %s", c.ID)
if err := c.Saver.lock(BlockAcquire); err != nil {
return err
}
defer c.Saver.UnlockOrDie()
if c.Status != Paused {
return fmt.Errorf("cannot resume container %q in state %v", c.ID, c.Status)
}
if err := c.Sandbox.Resume(c.ID); err != nil {
return fmt.Errorf("resuming container: %v", err)
}
c.changeStatus(Running)
return c.saveLocked()
}
// State returns the metadata of the container.
func (c *Container) State() specs.State {
return specs.State{
Version: specs.Version,
ID: c.ID,
Status: c.Status,
Pid: c.SandboxPid(),
Bundle: c.BundleDir,
Annotations: c.Spec.Annotations,
}
}
// Processes retrieves the list of processes and associated metadata inside a
// container.
func (c *Container) Processes() ([]*control.Process, error) {
if err := c.requireStatus("get processes of", Running, Paused); err != nil {
return nil, err
}
return c.Sandbox.Processes(c.ID)
}
// Destroy stops all processes and frees all resources associated with the
// container.
func (c *Container) Destroy() error {
log.Debugf("Destroy container, cid: %s", c.ID)
if err := c.Saver.lock(BlockAcquire); err != nil {
return err
}
defer func() {
c.Saver.UnlockOrDie()
_ = c.Saver.close()
}()
// Stored for later use as stop() sets c.Sandbox to nil.
sb := c.Sandbox
// We must perform the following cleanup steps:
// * stop the container and gofer processes,
// * remove the container filesystem on the host, and
// * delete the container metadata directory.
//
// It's possible for one or more of these steps to fail, but we should
// do our best to perform all of the cleanups. Hence, we keep a slice
// of errors return their concatenation.
var errs []string
if err := c.stop(); err != nil {
err = fmt.Errorf("stopping container: %v", err)
log.Warningf("%v", err)
errs = append(errs, err.Error())
}
if err := c.Saver.Destroy(); err != nil {
err = fmt.Errorf("deleting container state files: %v", err)
log.Warningf("%v", err)
errs = append(errs, err.Error())
}
// Clean up self-backed filestore files created in their respective mounts.
c.forEachSelfMount(func(mountSrc string) {
if sb != nil {
if hint := sb.MountHints.FindMount(mountSrc); hint != nil && hint.ShouldShareMount() {
// Don't delete filestore file for shared mounts. The sandbox owns a
// shared master mount which uses this filestore and is shared with
// multiple mount points.
return
}
}
filestorePath := boot.SelfFilestorePath(mountSrc, c.sandboxID())
if err := os.Remove(filestorePath); err != nil {
err = fmt.Errorf("failed to delete filestore file %q: %v", filestorePath, err)
log.Warningf("%v", err)
errs = append(errs, err.Error())
}
})
if sb != nil && sb.IsRootContainer(c.ID) {
// When the root container is being destroyed, we can clean up filestores
// used by shared mounts.
for _, hint := range sb.MountHints.Mounts {
if !hint.ShouldShareMount() {
continue
}
// Assume this is a self-backed shared mount and try to delete the
// filestore. Subsequently ignore the ENOENT if the assumption is wrong.
filestorePath := boot.SelfFilestorePath(hint.Mount.Source, c.sandboxID())
if err := os.Remove(filestorePath); err != nil && !os.IsNotExist(err) {
err = fmt.Errorf("failed to delete shared filestore file %q: %v", filestorePath, err)
log.Warningf("%v", err)
errs = append(errs, err.Error())
}
}
}
c.changeStatus(Stopped)
// Adjust oom_score_adj for the sandbox. This must be done after the container
// is stopped and the directory at c.Root is removed.
//
// Use 'sb' to tell whether it has been executed before because Destroy must
// be idempotent.
if sb != nil {
if err := adjustSandboxOOMScoreAdj(sb, c.Spec, c.Saver.RootDir, true); err != nil {
errs = append(errs, err.Error())
}
}
// "If any poststop hook fails, the runtime MUST log a warning, but the
// remaining hooks and lifecycle continue as if the hook had
// succeeded" - OCI spec.
//
// Based on the OCI, "The post-stop hooks MUST be called after the container
// is deleted but before the delete operation returns"
// Run it here to:
// 1) Conform to the OCI.
// 2) Make sure it only runs once, because the root has been deleted, the
// container can't be loaded again.
if c.Spec.Hooks != nil {
specutils.ExecuteHooksBestEffort(c.Spec.Hooks.Poststop, c.State())
}
if len(errs) == 0 {
return nil
}
return fmt.Errorf("%s", strings.Join(errs, "\n"))
}
func (c *Container) sandboxID() string {
return c.Saver.ID.SandboxID
}
func (c *Container) forEachSelfMount(fn func(mountSrc string)) {
if c.GoferMountConfs == nil {
// Container not started? Skip.
return
}
if c.GoferMountConfs[0].IsSelfBacked() {
fn(c.Spec.Root.Path)
}
goferMntIdx := 1 // First index is for rootfs.
for i := range c.Spec.Mounts {
if !specutils.HasMountConfig(c.Spec.Mounts[i]) {
continue
}
if c.GoferMountConfs[goferMntIdx].IsSelfBacked() {
fn(c.Spec.Mounts[i].Source)
}
goferMntIdx++
}
}
func createGoferConf(overlayMedium config.OverlayMedium, overlaySize string, mountType string, mountSrc string) (specutils.GoferMountConf, error) {
var lower specutils.GoferMountConfLowerType
switch mountType {
case boot.Bind:
lower = specutils.Lisafs
case tmpfs.Name:
lower = specutils.NoneLower
case erofs.Name:
lower = specutils.Erofs
default:
return specutils.GoferMountConf{}, fmt.Errorf("unsupported mount type %q in mount hint", mountType)
}
switch overlayMedium {
case config.NoOverlay:
return specutils.GoferMountConf{Lower: lower, Upper: specutils.NoOverlay}, nil
case config.MemoryOverlay:
return specutils.GoferMountConf{Lower: lower, Upper: specutils.MemoryOverlay, Size: overlaySize}, nil
case config.SelfOverlay:
mountSrcInfo, err := os.Stat(mountSrc)
if err != nil {
return specutils.GoferMountConf{}, fmt.Errorf("failed to stat mount %q to see if it were a directory: %v", mountSrc, err)
}
if !mountSrcInfo.IsDir() {
log.Warningf("self filestore is only supported for directory mounts, but mount %q is not a directory, falling back to memory", mountSrc)
return specutils.GoferMountConf{Lower: lower, Upper: specutils.MemoryOverlay, Size: overlaySize}, nil
}
return specutils.GoferMountConf{Lower: lower, Upper: specutils.SelfOverlay, Size: overlaySize}, nil
default:
if overlayMedium.IsBackedByAnon() {
return specutils.GoferMountConf{Lower: lower, Upper: specutils.AnonOverlay, Size: overlaySize}, nil
}
return specutils.GoferMountConf{}, fmt.Errorf("unexpected overlay medium %q", overlayMedium)
}
}
// initGoferConfs initializes c.GoferMountConfs with all the gofer configs that
// dictate how each gofer mount should be configured.
func (c *Container) initGoferConfs(ovlConf config.Overlay2, mountHints *boot.PodMountHints, rootfsHint *boot.RootfsHint) error {
// Handle root mount first.
overlayMedium := ovlConf.RootOverlayMedium()
overlaySize := ovlConf.RootOverlaySize()
mountType := boot.Bind
if rootfsHint != nil {
overlayMedium = rootfsHint.Overlay
if !specutils.IsGoferMount(rootfsHint.Mount) {
mountType = rootfsHint.Mount.Type
}
overlaySize = rootfsHint.Size
}
if c.Spec.Root.Readonly {
overlayMedium = config.NoOverlay
}
goferConf, err := createGoferConf(overlayMedium, overlaySize, mountType, c.Spec.Root.Path)
if err != nil {
return err
}
c.GoferMountConfs = append(c.GoferMountConfs, goferConf)
// Handle sub mounts.
for i := range c.Spec.Mounts {
if !specutils.HasMountConfig(c.Spec.Mounts[i]) {
continue
}
// Determine mount type: Bind for gofer mounts, erofs.Name for EROFS mounts
mountType := boot.Bind
if specutils.IsErofsMount(c.Spec.Mounts[i]) {
mountType = erofs.Name
}
overlayMedium := ovlConf.SubMountOverlayMedium()
overlaySize := ovlConf.SubMountOverlaySize()
if specutils.IsReadonlyMount(c.Spec.Mounts[i].Options) {
overlayMedium = config.NoOverlay
}
if hint := mountHints.FindMount(c.Spec.Mounts[i].Source); hint != nil && hint.IsSandboxLocal() {
// Note that we want overlayMedium=self even if this is a read-only mount so that
// the shared mount is created correctly. Future containers may mount this writably.
overlayMedium = config.SelfOverlay
if !specutils.IsGoferMount(hint.Mount) {
mountType = hint.Mount.Type
}
overlaySize = ""
}
goferConf, err := createGoferConf(overlayMedium, overlaySize, mountType, c.Spec.Mounts[i].Source)
if err != nil {
return err
}
c.GoferMountConfs = append(c.GoferMountConfs, goferConf)
}
return nil
}
// createGoferFilestores creates the regular files that will back the
// tmpfs/overlayfs mounts that will overlay some gofer mounts.
//
// Precondition: gofer process must be running.
func (c *Container) createGoferFilestores(ovlConf config.Overlay2, mountHints *boot.PodMountHints) ([]*os.File, error) {
var goferFilestores []*os.File
// NOTE(gvisor.dev/issue/9834): Create the filestores in the gofer mount
// namespace, so that they don't prevent the host mount points from being
// unmounted from the host's mount namespace. We will use /proc/pid/root
// to access gofer's mount namespace. See proc_pid_root(5).
goferRootfs := fmt.Sprintf("/proc/%d/root", c.GoferPid.Load())
// Handle rootfs first.
rootfsConf := c.GoferMountConfs[0]
filestore, err := c.createGoferFilestore(goferRootfs, ovlConf, rootfsConf, c.Spec.Root.Path, mountHints)
if err != nil {
return nil, err
}
if filestore != nil {
goferFilestores = append(goferFilestores, filestore)
}
// Then handle all the bind mounts.
mountIdx := 1 // first one is the root
for _, m := range c.Spec.Mounts {
if !specutils.HasMountConfig(m) {
continue
}
mountConf := c.GoferMountConfs[mountIdx]
mountIdx++
filestore, err := c.createGoferFilestore(goferRootfs, ovlConf, mountConf, m.Source, mountHints)
if err != nil {
return nil, err
}
if filestore != nil {
goferFilestores = append(goferFilestores, filestore)
}
}
for _, filestore := range goferFilestores {
// Perform this work around outside the sandbox. The sandbox may already be
// running with seccomp filters that do not allow this.
pgalloc.IMAWorkAroundForMemFile(filestore.Fd())
}
return goferFilestores, nil
}
func (c *Container) createGoferFilestore(goferRootfs string, ovlConf config.Overlay2, goferConf specutils.GoferMountConf, mountSrc string, mountHints *boot.PodMountHints) (*os.File, error) {
if !goferConf.IsFilestorePresent() {
return nil, nil
}
switch goferConf.Upper {
case specutils.SelfOverlay:
return c.createGoferFilestoreInSelf(goferRootfs, mountSrc, mountHints)
case specutils.AnonOverlay:
return c.createGoferFilestoreInDir(goferRootfs, ovlConf.Medium().HostFileDir())
default:
return nil, fmt.Errorf("unexpected upper layer with filestore %s", goferConf)
}
}
func (c *Container) createGoferFilestoreInSelf(goferRootfs string, mountSrc string, mountHints *boot.PodMountHints) (*os.File, error) {
// Create the self filestore file.
createFlags := unix.O_RDWR | unix.O_CREAT | unix.O_CLOEXEC | unix.O_NOFOLLOW | unix.O_NONBLOCK
if hint := mountHints.FindMount(mountSrc); hint == nil || !hint.ShouldShareMount() {
// Allow shared mounts to reuse existing filestore. A previous shared user
// may have already set up the filestore.
createFlags |= unix.O_EXCL
}
dirPath := path.Join(goferRootfs, mountSrc)
fileName := boot.SelfFilestoreName(c.sandboxID())
dirFD, err := unix.Open(dirPath, unix.O_RDONLY|unix.O_DIRECTORY|unix.O_CLOEXEC|unix.O_NOFOLLOW, 0)
if err != nil {
return nil, fmt.Errorf("failed to open directory %q: %v", dirPath, err)
}
defer unix.Close(dirFD)
filestoreFD, err := unix.Openat2(dirFD, fileName, &unix.OpenHow{
Flags: uint64(createFlags),
Mode: 0666,
Resolve: unix.RESOLVE_BENEATH | unix.RESOLVE_NO_SYMLINKS | unix.RESOLVE_NO_MAGICLINKS | unix.RESOLVE_NO_XDEV,
})
if err != nil {
if err == unix.EEXIST {
// Note that if the same submount is mounted multiple times within the
// same sandbox, and is not shared, then the overlay option doesn't work
// correctly. Because each overlay mount is independent and changes to
// one are not visible to the other.
return nil, fmt.Errorf("%q mount source already has a filestore file %q; repeated submounts are not supported with overlay optimizations", mountSrc, fileName)
}
return nil, fmt.Errorf("failed to create filestore file %q inside %q: %v", fileName, mountSrc, err)
}
var stat unix.Stat_t
if err := unix.Fstat(filestoreFD, &stat); err != nil {
_ = unix.Close(filestoreFD)
return nil, fmt.Errorf("failed to stat filestore file %q inside %q: %v", fileName, mountSrc, err)
}
if (stat.Mode & unix.S_IFMT) != unix.S_IFREG {
_ = unix.Close(filestoreFD)
return nil, fmt.Errorf("filestore file %q inside %q is not a regular file (mode: %o)", fileName, mountSrc, stat.Mode)
}
if stat.Nlink != 1 {
_ = unix.Close(filestoreFD)
return nil, fmt.Errorf("filestore file %q inside %q has unexpected link count: %d", fileName, mountSrc, stat.Nlink)
}
log.Debugf("Created filestore file %q for mount source %q", fileName, mountSrc)
// Filestore in self should be a named path because it needs to be
// discoverable via path traversal so that k8s can scan the filesystem
// and apply any limits appropriately (like local ephemeral storage
// limits). So don't delete it. These files will be unlinked when the
// container is destroyed. This makes self medium appropriate for k8s.
return os.NewFile(uintptr(filestoreFD), fileName), nil
}
func (c *Container) createGoferFilestoreInDir(goferRootfs string, filestoreDir string) (*os.File, error) {
fileInfo, err := os.Stat(filestoreDir)
if err != nil {
return nil, fmt.Errorf("failed to stat filestore directory %q: %v", filestoreDir, err)
}
if !fileInfo.IsDir() {
return nil, fmt.Errorf("overlay2 flag should specify an existing directory")
}
// Create an unnamed temporary file in filestore directory which will be
// deleted when the last FD on it is closed. We don't use O_TMPFILE because
// it is not supported on all filesystems. So we simulate it by creating a
// named file and then immediately unlinking it while keeping an FD on it.
// This file will be deleted when the container exits.
filestoreFile, err := os.CreateTemp(path.Join(goferRootfs, filestoreDir), "runsc-filestore-")
if err != nil {
return nil, fmt.Errorf("failed to create a temporary file inside %q: %v", filestoreDir, err)
}
if err := unix.Unlink(filestoreFile.Name()); err != nil {
return nil, fmt.Errorf("failed to unlink temporary file %q: %v", filestoreFile.Name(), err)
}
log.Debugf("Created an unnamed filestore file at %q", filestoreDir)
return filestoreFile, nil
}
// saveLocked saves the container metadata to a file.
//
// Precondition: container must be locked with container.lock().
func (c *Container) saveLocked() error {
log.Debugf("Save container, cid: %s", c.ID)
if err := c.Saver.SaveLocked(c); err != nil {
return fmt.Errorf("saving container metadata: %v", err)
}
return nil
}
// stop stops the container (for regular containers) or the sandbox (for
// root containers), and waits for the container or sandbox and the gofer
// to stop. If any of them doesn't stop before timeout, an error is returned.
func (c *Container) stop() error {
var parentCgroup cgroup.Cgroup
if c.Sandbox != nil {
log.Debugf("Destroying container, cid: %s", c.ID)
if err := c.Sandbox.DestroyContainer(c.ID); err != nil {
return fmt.Errorf("destroying container %q: %v", c.ID, err)
}
// Only uninstall parentCgroup for sandbox stop.
if c.Sandbox.IsRootContainer(c.ID) {
parentCgroup = c.Sandbox.CgroupJSON.Cgroup
}
// Only set sandbox to nil after it has been told to destroy the container.
c.Sandbox = nil
}
// Try killing gofer if it does not exit with container.
if goferPid := c.GoferPid.Load(); goferPid != 0 {
log.Debugf("Killing gofer for container, cid: %s, PID: %d", c.ID, goferPid)
// The gofer process may have been racily reaped by the goroutine from
// c.Wait() so ignore ESRCH errors.
if err := unix.Kill(goferPid, unix.SIGKILL); err != nil && !errors.Is(err, unix.ESRCH) {
log.Warningf("Error sending signal %d to gofer %d: %v", unix.SIGKILL, goferPid, err)
}
}
if err := c.waitForStopped(); err != nil {
return err
}
// Delete container cgroup if any.
if c.CompatCgroup.Cgroup != nil {
if err := c.CompatCgroup.Cgroup.Uninstall(); err != nil {
return err
}
}
// Gofer is running inside parentCgroup, so Cgroup.Uninstall has to be called
// after the gofer has stopped.
if parentCgroup != nil {
if err := parentCgroup.Uninstall(); err != nil {
return err
}
}
return nil
}
func (c *Container) waitForStopped() error {
goferPid := c.GoferPid.Load()
if goferPid == 0 {
return nil
}
if c.IsSandboxRunning() {
if err := c.SignalContainer(unix.Signal(0), false); err == nil {
return fmt.Errorf("container is still running")
}
}
if c.goferIsChild {
// The gofer process is a child of the current process, so we can wait it
// and collect its zombie. The gofer process could be racily reaped by the
// goroutine from c.Wait() so ignore ECHILD errors.
if _, err := unix.Wait4(int(goferPid), nil, 0, nil); err != nil && !errors.Is(err, unix.ECHILD) {
return fmt.Errorf("error waiting the gofer process: %v", err)
}
c.GoferPid.Store(0)
return nil
}
if err := specutils.WaitForNonChildExit(int(goferPid), 5*time.Second); err != nil {
return fmt.Errorf("waiting for gofer (PID %d) to exit: %v", goferPid, err)
}
c.GoferPid.Store(0)
return nil
}
// shouldCreateDeviceGofer indicates whether a device gofer connection should
// be created.
func shouldCreateDeviceGofer(spec *specs.Spec, conf *config.Config) bool {
return specutils.GPUFunctionalityRequested(spec, conf) || specutils.TPUFunctionalityRequested(spec, conf) || specutils.RDMAEnabled(spec, conf)
}
// shouldSpawnGofer indicates whether the gofer process should be spawned.
func shouldSpawnGofer(spec *specs.Spec, conf *config.Config, goferConfs []specutils.GoferMountConf) bool {
// Lisafs mounts need the gofer.
for _, cfg := range goferConfs {
if cfg.ShouldUseLisafs() {
return true
}
}
// Device gofer needs a gofer process.
return shouldCreateDeviceGofer(spec, conf)
}
// createLisafsSocketPair creates a socket pair for Lisafs communication and
// appends the sandbox end to sandEnds while donating the gofer end.
func createLisafsSocketPair(sandEnds *[]*os.File, donations *donation.Agency) error {
fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0)
if err != nil {
return err
}
*sandEnds = append(*sandEnds, os.NewFile(uintptr(fds[0]), "sandbox IO FD"))
goferEnd := os.NewFile(uintptr(fds[1]), "gofer IO FD")
donations.DonateAndClose("io-fds", goferEnd)
return nil
}
// createGoferProcess returns an IO file list and a mounts file on success.
// The IO file list consists of image files and/or socket files to connect to
// a gofer endpoint for the mount points using Gofers. The mounts file is the
// file to read list of mounts after they have been resolved (direct paths,
// no symlinks), and will be nil if there is no cleaning required for mounts.
func (c *Container) createGoferProcess(conf *config.Config, mountHints *boot.PodMountHints, attached bool, cloneIntoCgroupFD *os.File) ([]*os.File, []*os.File, *os.File, *os.File, error) {
rootfsHint, err := boot.NewRootfsHint(c.Spec)
if err != nil {
return nil, nil, nil, nil, fmt.Errorf("error creating rootfs hint: %w", err)
}
if err := c.initGoferConfs(conf.GetOverlay2(), mountHints, rootfsHint); err != nil {
return nil, nil, nil, nil, fmt.Errorf("error initializing gofer confs: %w", err)
}
if !c.GoferMountConfs[0].ShouldUseLisafs() && specutils.GPUFunctionalityRequestedViaHook(c.Spec, conf) {
// nvidia-container-runtime-hook attempts to populate the container
// rootfs with NVIDIA libraries and devices. With EROFS, spec.Root.Path
// points to an empty directory and populating that has no effect.
return nil, nil, nil, nil, fmt.Errorf("nvidia-container-runtime-hook cannot be used together with non-lisafs backed root mount")
}
if !shouldSpawnGofer(c.Spec, conf, c.GoferMountConfs) {
if !c.GoferMountConfs[0].ShouldUseErofs() {
panic("goferless mode is only possible with EROFS rootfs")
}
ioFile, err := os.Open(rootfsHint.Mount.Source)
if err != nil {
return nil, nil, nil, nil, fmt.Errorf("opening rootfs image %q: %v", rootfsHint.Mount.Source, err)
}
ioFiles := []*os.File{ioFile}
cu := cleanup.Make(func() {
for _, f := range ioFiles {
_ = f.Close()
}
})
defer cu.Clean()
cfgIdx := 1
for _, m := range c.Spec.Mounts {
if !specutils.HasMountConfig(m) {
continue
}
if c.GoferMountConfs[cfgIdx].ShouldUseErofs() {
f, err := os.Open(m.Source)
if err != nil {
return nil, nil, nil, nil, fmt.Errorf("opening EROFS image %q: %v", m.Source, err)
}
ioFiles = append(ioFiles, f)
}
cfgIdx++
}
cu.Release()
return ioFiles, nil, nil, nil, nil
}
// Ensure we don't leak FDs to the gofer process.
if err := sandbox.SetCloExeOnAllFDs(); err != nil {
return nil, nil, nil, nil, fmt.Errorf("setting CLOEXEC on all FDs: %w", err)
}
donations := donation.Agency{}
defer donations.Close()
if err := donations.DonateLogFile("log-fd", conf.LogFilename, os.O_CREATE|os.O_WRONLY|os.O_APPEND, &log.DefaultFileOpts{}); err != nil {
return nil, nil, nil, nil, err
}
// The startTime here can mean one of two things:
// - If this is the first gofer started at the same time as the sandbox,
// then this starttime will exactly match the one used by the sandbox
// itself (i.e. `Sandbox.StartTime`). This is desirable, such that the
// first gofer's log filename will have the exact same timestamp as
// the sandbox's log filename timestamp.
// - If this is not the first gofer, then this starttime will be later
// than the sandbox start time; this is desirable such that we can
// distinguish the gofer log filenames between each other.
// In either case, `starttime.Get` gets us the timestamp we want.
lfOpts := &specutils.LogFileOpts{
SandboxID: c.sandboxID(),
CID: c.ID,
Command: "gofer",
Timestamp: starttime.Get(),
Test: specutils.TestName(conf, c.Spec),
}
if specutils.IsDebugCommand(conf, "gofer") {
if err := donations.DonateDebugLogFile("debug-log-fd", conf.DebugLog, lfOpts); err != nil {
return nil, nil, nil, nil, err
}
}
// Start with the general config flags.
cmd := exec.Command(specutils.ExePath, conf.ToFlags()...)
// Don't forward GOMAXPROCS defaults that apply to this process (in
// particular, containerd-shim-runsc-v1 passes GOMAXPROCS=2 in
// v1.service.newCommand()).
cmd.Env = slices.DeleteFunc(os.Environ(), func(env string) bool { return strings.HasPrefix(env, "GOMAXPROCS=") })
cmd.SysProcAttr = &unix.SysProcAttr{
// Detach from session. Otherwise, signals sent to the foreground process
// will also be forwarded by this process, resulting in duplicate signals.
Setsid: true,
}
if cloneIntoCgroupFD != nil {
cmd.SysProcAttr.UseCgroupFD = true
cmd.SysProcAttr.CgroupFD = int(cloneIntoCgroupFD.Fd())
}
// Set Args[0] to make easier to spot the gofer process. Otherwise it's
// shown as `exe`.
cmd.Args[0] = "runsc-gofer"
// Tranfer FDs that need to be present before the "gofer" command.
// Start at 3 because 0, 1, and 2 are taken by stdin/out/err.
nextFD := donations.Transfer(cmd, 3)
cmd.Args = append(cmd.Args, "gofer", "--bundle", c.BundleDir)
cmd.Args = append(cmd.Args, "--gofer-mount-confs="+c.GoferMountConfs.String())
// Open the spec file to donate to the sandbox.
specFile, err := specutils.OpenSpec(c.BundleDir)
if err != nil {
return nil, nil, nil, nil, fmt.Errorf("opening spec file: %v", err)
}
donations.DonateAndClose("spec-fd", specFile)
// Donate any profile FDs to the gofer.
if err := profile.DonateProfileFDs(conf, &donations, true /* isGofer */, lfOpts); err != nil {
return nil, nil, nil, nil, fmt.Errorf("donating gofer profile fds: %w", err)
}
// Create pipe that allows gofer to send mount list to sandbox after all paths
// have been resolved.
mountsSand, mountsGofer, err := os.Pipe()
if err != nil {
return nil, nil, nil, nil, err
}
donations.DonateAndClose("mounts-fd", mountsGofer)
rpcServ, rpcClnt, err := unet.SocketPair(false)
if err != nil {
return nil, nil, nil, nil, fmt.Errorf("failed to create an rpc socket pair: %w", err)
}
rpcClntFD, _ := rpcClnt.Release()
donations.DonateAndClose("rpc-fd", os.NewFile(uintptr(rpcClntFD), "gofer-rpc"))
rpcPidCh := make(chan int, 1)
defer close(rpcPidCh)
go func() {
pid := <-rpcPidCh
if pid == 0 {
rpcServ.Close()
return
}
s := urpc.NewServer()
s.Register(&goferToHostRPC{goferPID: pid})
s.StartHandling(rpcServ)
}()
if ring, err := pinring.NewDisabledIOURing(); err != nil {
log.Warningf("Cannot create disabled io_uring ring: %v. This slows down gVisor sandbox teardown.", err)
} else {
donations.Donate("pin-ring-fd", ring)
c.pinRingFile = ring
fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_SEQPACKET|unix.SOCK_CLOEXEC, 0)
if err != nil {
return nil, nil, nil, nil, err
}
donations.DonateAndClose("sync-sentry-exit-fd", os.NewFile(uintptr(fds[1]), "sentry exit sync gofer FD"))
c.sentryExitSock = os.NewFile(uintptr(fds[0]), "sentry exit sync runsc FD")
}
// Count the number of mounts that needs an IO file.
ioFileCount := 0
for _, cfg := range c.GoferMountConfs {
if cfg.ShouldUseLisafs() || cfg.ShouldUseErofs() {
ioFileCount++
}
}
sandEnds := make([]*os.File, 0, ioFileCount)
// Handle rootfs (index 0)
switch {
case c.GoferMountConfs[0].ShouldUseLisafs():
if err := createLisafsSocketPair(&sandEnds, &donations); err != nil {
return nil, nil, nil, nil, err
}
case c.GoferMountConfs[0].ShouldUseErofs():
f, err := os.Open(rootfsHint.Mount.Source)
if err != nil {
return nil, nil, nil, nil, fmt.Errorf("opening rootfs image %q: %v", rootfsHint.Mount.Source, err)
}
sandEnds = append(sandEnds, f)
}
// Handle sub mounts
cfgIdx := 1
for _, m := range c.Spec.Mounts {
if !specutils.HasMountConfig(m) {
continue
}
switch {
case c.GoferMountConfs[cfgIdx].ShouldUseLisafs():
if err := createLisafsSocketPair(&sandEnds, &donations); err != nil {
return nil, nil, nil, nil, err
}
case c.GoferMountConfs[cfgIdx].ShouldUseErofs():
f, err := os.Open(m.Source)
if err != nil {
return nil, nil, nil, nil, fmt.Errorf("opening EROFS image %q: %v", m.Source, err)
}
sandEnds = append(sandEnds, f)
}
cfgIdx++
}
var devSandEnd *os.File
if shouldCreateDeviceGofer(c.Spec, conf) {
fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0)
if err != nil {
return nil, nil, nil, nil, err
}
devSandEnd = os.NewFile(uintptr(fds[0]), "sandbox dev IO FD")
donations.DonateAndClose("dev-io-fd", os.NewFile(uintptr(fds[1]), "gofer dev IO FD"))
}
if attached {
// The gofer is attached to the lifetime of this process, so it
// should synchronously die when this process dies.
cmd.SysProcAttr.Pdeathsig = unix.SIGKILL
}
// Enter new namespaces to isolate from the rest of the system. Don't unshare
// cgroup because gofer is added to a cgroup in the caller's namespace.
nss := []specs.LinuxNamespace{
{Type: specs.IPCNamespace},
{Type: specs.MountNamespace},
{Type: specs.PIDNamespace},
{Type: specs.UTSNamespace},
}
goferNetNS, goferNetNSFile, pinGoferNetNS := goferNetworkNamespace(conf)
if goferNetNSFile != nil {
// goferNetNS.Path must remain open until the gofer has started.
defer goferNetNSFile.Close()
}
if goferNetNS != nil {
nss = append(nss, *goferNetNS)
}
rootlessEUID := unix.Geteuid() != 0
setUserMappings := false
// Setup any uid/gid mappings, and create or join the configured user
// namespace so the gofer's view of the filesystem aligns with the
// users in the sandbox.
if !rootlessEUID {
if userNS, ok := specutils.GetNS(specs.UserNamespace, c.Spec); ok {
nss = append(nss, userNS)
specutils.SetUIDGIDMappings(cmd, c.Spec)
// We need to set UID and GID to have capabilities in a new user namespace.
cmd.SysProcAttr.Credential = &syscall.Credential{Uid: 0, Gid: 0}
}
} else {
userNS, ok := specutils.GetNS(specs.UserNamespace, c.Spec)
if !ok {
return nil, nil, nil, nil, fmt.Errorf("unable to run a rootless container without userns")
}
nss = append(nss, userNS)
if sandbox.CanUseUnprivilegedMapping(c.Spec) {
specutils.SetUIDGIDMappings(cmd, c.Spec)
cmd.SysProcAttr.GidMappingsEnableSetgroups = false
} else {
setUserMappings = true
}
syncFile, err := sandbox.ConfigureCmdForRootless(cmd, &donations)
if err != nil {
return nil, nil, nil, nil, err
}
defer syncFile.Close()
uid, gid := sandbox.SandboxUserGroupIDs(c.Spec)
if uid != 0 {
cmd.Args = append(cmd.Args, fmt.Sprintf("--uid=%d", uid))
}
if gid != 0 {
cmd.Args = append(cmd.Args, fmt.Sprintf("--gid=%d", gid))
}
}
// Create synchronization FD for chroot.
fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0)
if err != nil {
return nil, nil, nil, nil, err
}
chrootSyncSandEnd := os.NewFile(uintptr(fds[0]), "chroot sync runsc FD")
chrootSyncGoferEnd := os.NewFile(uintptr(fds[1]), "chroot sync gofer FD")
donations.DonateAndClose("sync-chroot-fd", chrootSyncGoferEnd)
defer chrootSyncSandEnd.Close()
donations.Transfer(cmd, nextFD)
// Add container ID as the last argument.
cmd.Args = append(cmd.Args, c.ID)
log.Infof("Starting gofer with command: %v", cmd.Args)
// Start the gofer in the given namespace.
donation.LogDonations(cmd)
log.Debugf("Starting gofer: %s %v", cmd.Path, cmd.Args)
if err := specutils.StartInNS(cmd, nss); err != nil {
return nil, nil, nil, nil, fmt.Errorf("gofer: %v", err)
}
log.Infof("Gofer started, PID: %d", cmd.Process.Pid)
c.GoferPid.Store(cmd.Process.Pid)
c.goferIsChild = true
rpcPidCh <- cmd.Process.Pid
if pinGoferNetNS {
// The gofer was started in a new empty network namespace.
// Pin it, future gofers will join it instead of creating a new one.
if err := pinNullNetNS(conf, cmd.Process.Pid); err != nil {
log.Warningf("Unable to pin the gofer's network namespace at %q (%v); future gofers will create new network namespaces. This slows down gVisor startup.", nullNetNSPath(conf), err)
} else {
log.Infof("Pinned null network namespace at %q", nullNetNSPath(conf))
}
}
// Set up and synchronize rootless mode userns mappings.
if setUserMappings {
if err := sandbox.SetUserMappings(c.Spec, cmd.Process.Pid); err != nil {
return nil, nil, nil, nil, err
}
}
// Set up nvproxy with the Gofer's mount namespaces while chrootSyncSandEnd is
// is still open.
if err := nvproxySetup(c.Spec, conf, c.GoferPid.Load()); err != nil {
return nil, nil, nil, nil, fmt.Errorf("setting up nvproxy for gofer: %w", err)
}
// Create gofer filestore files with the Gofer's mount namespaces while
// chrootSyncSandEnd is still open.
goferFilestores, err := c.createGoferFilestores(conf.GetOverlay2(), mountHints)
if err != nil {
return nil, nil, nil, nil, fmt.Errorf("creating gofer filestore files: %w", err)
}
return sandEnds, goferFilestores, devSandEnd, mountsSand, nil
}
// goferNetworkNamespace returns the network namespace that the gofer process
// should be started in.
// A nil namespace means to stay in the current network namespace.
// The returned `os.File` keeps the namespace referred to by the returned
// namespace's Path alive, so it must be kept open until gofer start.
// If `pinNetNS` is true, the gofer should be started in a new network
// namespace, which should then be pinned as the shared "null" network
// namespace (see `pinNullNetNS`) once the gofer starts.
func goferNetworkNamespace(conf *config.Config) (ns *specs.LinuxNamespace, nsFile *os.File, pinNetNS bool) {
switch conf.GoferNetworkNamespace {
case config.GoferNetworkNamespaceNew:
return &specs.LinuxNamespace{Type: specs.NetworkNamespace}, nil, false
case config.GoferNetworkNamespaceHost:
return nil, nil, false
case config.GoferNetworkNamespaceNull:
nsFile, err := openNullNetNS(nullNetNSPath(conf))
if err != nil {
if !os.IsNotExist(err) {
// Unexpected error.
log.Warningf("No usable null network namespace at %q (%v); creating a new one. This issue is slowing down your sandboxes' startup.", nullNetNSPath(conf), err)
} else if conf.SharedRootDir == "" && conf.RootDir != config.DefaultRootDir() {
// User has customized `--root` but not `--shared-root`, so they are likely missing out on
// the performance benefits of a shared root directory.
log.Warningf("gVisor startup is slower if `--root` (or `--shared-root`) is not reused across multiple sandboxes. You can speed up sandbox startup by setting `--shared-root` to a shared system-wide directory so that the null gofer network namespace is reused across sandboxes.")
} else {
// Informational-only as this is expected on first sandbox creation.
log.Infof("No usable null network namespace at %q (%v); creating a new one. This is expected if this sandbox is the first sandbox to start with this `--shared-root` directory (%v). If this is not the first sandbox, this issue is slowing down your sandboxes' startup.", nullNetNSPath(conf), err, conf.SharedRoot())
}
return &specs.LinuxNamespace{Type: specs.NetworkNamespace}, nil, true
}
return &specs.LinuxNamespace{
Type: specs.NetworkNamespace,
// Refer to the namespace through our own open FD so that
// it stays valid even if racing with an unmount.
Path: fmt.Sprintf("/proc/self/fd/%d", nsFile.Fd()),
}, nsFile, false
default:
return &specs.LinuxNamespace{
Type: specs.NetworkNamespace,
Path: string(conf.GoferNetworkNamespace),
}, nil, false
}
}
// changeStatus transitions from one status to another ensuring that the
// transition is valid.
func (c *Container) changeStatus(s Status) {
switch s {
case Creating:
// Initial state, never transitions to it.
panic(fmt.Sprintf("invalid state transition: %v => %v", c.Status, s))
case Created:
if c.Status != Creating {
panic(fmt.Sprintf("invalid state transition: %v => %v", c.Status, s))
}
if c.Sandbox == nil {
panic("sandbox cannot be nil")
}
case Paused:
if c.Status != Running {
panic(fmt.Sprintf("invalid state transition: %v => %v", c.Status, s))
}
if c.Sandbox == nil {
panic("sandbox cannot be nil")
}
case Running:
if c.Status != Created && c.Status != Paused {
panic(fmt.Sprintf("invalid state transition: %v => %v", c.Status, s))
}
if c.Sandbox == nil {
panic("sandbox cannot be nil")
}
case Stopped:
// All states can transition to Stopped.
default:
panic(fmt.Sprintf("invalid new state: %v", s))
}
c.Status = s
}
// IsSandboxRunning returns true if the sandbox exists and is running.
func (c *Container) IsSandboxRunning() bool {
return c.Sandbox != nil && c.Sandbox.IsRunning()
}
// HasCapabilityInAnySet returns true if the given capability is in any of the
// capability sets of the container process.
func (c *Container) HasCapabilityInAnySet(capability linux.Capability) bool {
capString := capability.String()
for _, set := range [5][]string{
c.Spec.Process.Capabilities.Bounding,
c.Spec.Process.Capabilities.Effective,
c.Spec.Process.Capabilities.Inheritable,
c.Spec.Process.Capabilities.Permitted,
c.Spec.Process.Capabilities.Ambient,
} {
for _, c := range set {
if c == capString {
return true
}
}
}
return false
}
// RunsAsUID0 returns true if the container process runs with UID 0 (root).
func (c *Container) RunsAsUID0() bool {
return c.Spec.Process.User.UID == 0
}
func (c *Container) requireStatus(action string, statuses ...Status) error {
for _, s := range statuses {
if c.Status == s {
return nil
}
}
return fmt.Errorf("cannot %s container %q in state %s", action, c.ID, c.Status)
}
// IsSandboxRoot returns true if this container is its sandbox's root container.
func (c *Container) IsSandboxRoot() bool {
return specutils.IsRootContainer(c.Spec)
}
// adjustGoferOOMScoreAdj sets the oom_store_adj for the container's gofer.
func (c *Container) adjustGoferOOMScoreAdj() error {
goferPid := c.GoferPid.Load()
if goferPid == 0 || c.Spec.Process.OOMScoreAdj == nil {
return nil
}
return setOOMScoreAdj(goferPid, *c.Spec.Process.OOMScoreAdj)
}
// adjustSandboxOOMScoreAdj sets the oom_score_adj for the sandbox.
// oom_score_adj is set to the lowest oom_score_adj among the containers
// running in the sandbox.
//
// TODO(gvisor.dev/issue/238): This call could race with other containers being
// created at the same time and end up setting the wrong oom_score_adj to the
// sandbox. Use rpc client to synchronize.
func adjustSandboxOOMScoreAdj(s *sandbox.Sandbox, spec *specs.Spec, rootDir string, destroy bool) error {
// Adjustment can be skipped if the root container is exiting, because it
// brings down the entire sandbox.
if specutils.IsRootContainer(spec) && destroy {
return nil
}
containers, err := LoadSandbox(rootDir, s.ID, LoadOpts{})
if err != nil {
return fmt.Errorf("loading sandbox containers: %v", err)
}
// Do nothing if the sandbox has been terminated.
if len(containers) == 0 {
return nil
}
// Get the lowest score for all containers.
var lowScore int
scoreFound := false
for _, container := range containers {
// Special multi-container support for CRI. Ignore the root container when
// calculating oom_score_adj for the sandbox because it is the
// infrastructure (pause) container and always has a very low oom_score_adj.
//
// We will use OOMScoreAdj in the single-container case where the
// containerd container-type annotation is not present.
if specutils.SpecContainerType(container.Spec) == specutils.ContainerTypeSandbox {
continue
}
if container.Spec.Process.OOMScoreAdj != nil && (!scoreFound || *container.Spec.Process.OOMScoreAdj < lowScore) {
scoreFound = true
lowScore = *container.Spec.Process.OOMScoreAdj
}
}
// If the container is destroyed and remaining containers have no
// oomScoreAdj specified then we must revert to the original oom_score_adj
// saved with the root container.
if !scoreFound && destroy {
lowScore = containers[0].Sandbox.OriginalOOMScoreAdj
scoreFound = true
}
// Only set oom_score_adj if one of the containers has oom_score_adj set. If
// not, oom_score_adj is inherited from the parent process.
//
// See: https://github.com/opencontainers/runtime-spec/blob/master/config.md#linux-process
if !scoreFound {
return nil
}
// Set the lowest of all containers oom_score_adj to the sandbox.
return setOOMScoreAdj(s.Getpid(), lowScore)
}
// setOOMScoreAdj sets oom_score_adj to the given value for the given PID.
// /proc must be available and mounted read-write. scoreAdj should be between
// -1000 and 1000. It's a noop if the process has already exited.
func setOOMScoreAdj(pid int, scoreAdj int) error {
f, err := os.OpenFile(fmt.Sprintf("/proc/%d/oom_score_adj", pid), os.O_WRONLY, 0644)
if err != nil {
// Ignore NotExist errors because it can race with process exit.
if os.IsNotExist(err) {
log.Warningf("Process (%d) not found setting oom_score_adj", pid)
return nil
}
return err
}
defer f.Close()
if _, err := f.WriteString(strconv.Itoa(scoreAdj)); err != nil {
if errors.Is(err, unix.ESRCH) {
log.Warningf("Process (%d) exited while setting oom_score_adj", pid)
return nil
}
return fmt.Errorf("setting oom_score_adj to %d: %v", scoreAdj, err)
}
return nil
}
// populateStats populates event with stats estimates based on cgroups and the
// sentry's accounting.
func (c *Container) populateStats(event *boot.EventOut) {
// The events command, when run for all running containers, should
// account for the full cgroup CPU usage. We split cgroup usage
// proportionally according to the sentry-internal usage measurements,
// only counting Running containers.
log.Debugf("event.ContainerUsage: %v", event.ContainerUsage)
numContainers := uint64(len(event.ContainerUsage))
if numContainers == 0 {
log.Warningf("events: no containers listed in usage, returning zero CPU usage")
event.Event.Data.CPU.Usage.Total = 0
return
}
var containerUsage uint64
var allContainersUsage uint64
for _, u := range event.ContainerUsage {
allContainersUsage += u.CPU
if u.ContainerID == c.ID {
containerUsage = u.CPU
}
}
cgroup, err := c.Sandbox.NewCGroup()
if err != nil {
// No cgroup, so rely purely on the sentry's accounting.
log.Warningf("events: no cgroups")
event.Event.Data.CPU.Usage.Total = containerUsage
return
}
// Get the host cgroup CPU usage.
cgroupsUsage, err := cgroup.CPUUsage()
if err != nil || cgroupsUsage == 0 {
// No cgroup usage, so rely purely on the sentry's accounting.
log.Warningf("events: failed when getting cgroup CPU usage for container: usage=%d, err: %v", cgroupsUsage, err)
event.Event.Data.CPU.Usage.Total = containerUsage
return
}
// If the sentry reports no CPU usage, fall back on cgroups and split usage
// equally across containers.
if allContainersUsage == 0 {
log.Warningf("events: no sentry CPU usage reported")
allContainersUsage = cgroupsUsage
containerUsage = cgroupsUsage / numContainers
}
// Scaling can easily overflow a uint64 (e.g. a containerUsage and
// cgroupsUsage of 16 seconds each will overflow), so use floats.
total := float64(containerUsage) * (float64(cgroupsUsage) / float64(allContainersUsage))
log.Debugf("Usage, container: %d, cgroups: %d, all: %d, total: %.0f", containerUsage, cgroupsUsage, allContainersUsage, total)
event.Event.Data.CPU.Usage.Total = uint64(total)
}
func (c *Container) createParentCgroup(parentPath string, conf *config.Config) (cgroup.Cgroup, error) {
var err error
useSystemd := conf.SystemdCgroup
if conf.SystemdCgroup {
parentPath, err = cgroup.TransformSystemdPath(parentPath, c.ID, conf.Rootless)
if err != nil {
return nil, err
}
} else if cgroup.LikelySystemdPath(parentPath) {
// Auto-enable systemd cgroup handling when a systemd path is detected.
// This ensures proper cgroup initialization when the shim is invoked
// directly with systemd paths (e.g., via Docker's --runtime flag).
log.Infof("Detected systemd cgroup path %q, enabling systemd cgroup handling", parentPath)
useSystemd = true
parentPath, err = cgroup.TransformSystemdPath(parentPath, c.ID, conf.Rootless)
if err != nil {
return nil, err
}
}
parentCgroup, err := cgroup.NewFromPath(parentPath, useSystemd)
if err != nil {
return nil, err
}
return parentCgroup, nil
}
// setupCgroupForRoot configures and returns cgroup for the sandbox and the
// root container. If `cgroupParentAnnotation` is set, use that path as the
// sandbox cgroup and use Spec.Linux.CgroupsPath as the root container cgroup.
func (c *Container) setupCgroupForRoot(conf *config.Config, spec *specs.Spec) (cgroup.Cgroup, cgroup.Cgroup, error) {
var parentCgroup cgroup.Cgroup
if parentPath, ok := spec.Annotations[cgroupParentAnnotation]; ok {
var err error
parentCgroup, err = c.createParentCgroup(parentPath, conf)
if err != nil {
return nil, nil, err
}
} else {
var err error
if spec.Linux == nil || spec.Linux.CgroupsPath == "" {
return nil, nil, nil
}
parentCgroup, err = c.createParentCgroup(spec.Linux.CgroupsPath, conf)
if parentCgroup == nil || err != nil {
return nil, nil, err
}
}
var err error
parentCgroup, err = cgroupInstall(conf, parentCgroup, spec.Linux.Resources)
if parentCgroup == nil || err != nil {
return nil, nil, err
}
subCgroup, err := c.setupCgroupForSubcontainer(conf, spec)
if err != nil {
_ = parentCgroup.Uninstall()
return nil, nil, err
}
return parentCgroup, subCgroup, nil
}
// setupCgroupForSubcontainer sets up empty cgroups for subcontainers. Since
// subcontainers run exclusively inside the sandbox, subcontainer cgroups on the
// host have no effect on them. However, some tools (e.g. cAdvisor) uses cgroups
// paths to discover new containers and report stats for them.
func (c *Container) setupCgroupForSubcontainer(conf *config.Config, spec *specs.Spec) (cgroup.Cgroup, error) {
if specutils.IsRootContainer(spec) {
if _, ok := spec.Annotations[cgroupParentAnnotation]; !ok {
return nil, nil
}
}
if spec.Linux == nil || spec.Linux.CgroupsPath == "" {
return nil, nil
}
cg, err := c.createParentCgroup(spec.Linux.CgroupsPath, conf)
if cg == nil || err != nil {
return nil, err
}
// Use empty resources, just want the directory structure created.
return cgroupInstall(conf, cg, &specs.LinuxResources{})
}
// cgroupInstall creates cgroups dir structure and sets their respective
// resources. In case of success, returns the cgroups instance and nil error.
// For rootless, it's possible that cgroups operations fail, in this case the
// error is suppressed and a nil cgroups instance is returned to indicate that
// no cgroups was configured.
func cgroupInstall(conf *config.Config, cg cgroup.Cgroup, res *specs.LinuxResources) (cgroup.Cgroup, error) {
if err := cg.Install(res); err != nil {
switch {
case (errors.Is(err, unix.EACCES) || errors.Is(err, unix.EROFS)) && conf.Rootless:
log.Warningf("Skipping cgroup configuration in rootless mode: %v", err)
return nil, nil
default:
return nil, fmt.Errorf("configuring cgroup: %v", err)
}
}
return cg, nil
}
func modifySpecForDirectfs(conf *config.Config, spec *specs.Spec) error {
if !conf.DirectFS || conf.TestOnlyAllowRunAsCurrentUserWithoutChroot {
return nil
}
if conf.Network == config.NetworkHost {
// Hostnet feature requires the sandbox to run in the current user
// namespace, in which the network namespace is configured.
return nil
}
if _, ok := specutils.GetNS(specs.UserNamespace, spec); ok {
// If the spec already defines a userns, use that.
return nil
}
if spec.Linux == nil {
spec.Linux = &specs.Linux{}
}
if len(spec.Linux.UIDMappings) > 0 || len(spec.Linux.GIDMappings) > 0 {
// The spec can only define UID/GID mappings with a userns (checked above).
return fmt.Errorf("spec defines UID/GID mappings without defining userns")
}
// Run the sandbox in a new user namespace with identity UID/GID mappings.
log.Debugf("Configuring container with a new userns with identity user mappings into current userns")
spec.Linux.Namespaces = append(spec.Linux.Namespaces, specs.LinuxNamespace{Type: specs.UserNamespace})
uidMappings, err := getIdentityMapping("uid_map")
if err != nil {
return err
}
spec.Linux.UIDMappings = uidMappings
logIDMappings(uidMappings, "UID")
gidMappings, err := getIdentityMapping("gid_map")
if err != nil {
return err
}
spec.Linux.GIDMappings = gidMappings
logIDMappings(gidMappings, "GID")
return nil
}
func getIdentityMapping(mapFileName string) ([]specs.LinuxIDMapping, error) {
// See user_namespaces(7) to understand how /proc/self/{uid/gid}_map files
// are organized.
mapFile := path.Join("/proc/self", mapFileName)
file, err := os.Open(mapFile)
if err != nil {
return nil, fmt.Errorf("failed to open %s: %v", mapFile, err)
}
defer file.Close()
var mappings []specs.LinuxIDMapping
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
var myStart, parentStart, rangeLen uint32
numParsed, err := fmt.Sscanf(line, "%d %d %d", &myStart, &parentStart, &rangeLen)
if err != nil {
return nil, fmt.Errorf("failed to parse line %q in file %s: %v", line, mapFile, err)
}
if numParsed != 3 {
return nil, fmt.Errorf("failed to parse 3 integers from line %q in file %s", line, mapFile)
}
// Create an identity mapping with the current userns.
mappings = append(mappings, specs.LinuxIDMapping{
ContainerID: myStart,
HostID: myStart,
Size: rangeLen,
})
}
if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("failed to scan file %s: %v", mapFile, err)
}
return mappings, nil
}
func logIDMappings(mappings []specs.LinuxIDMapping, idType string) {
if !log.IsLogging(log.Debug) {
return
}
log.Debugf("%s Mappings:", idType)
for _, m := range mappings {
log.Debugf("\tContainer ID: %d, Host ID: %d, Range Length: %d", m.ContainerID, m.HostID, m.Size)
}
}
// nvProxyPreGoferHostSetup does host setup work so that `nvidia-container-cli
// configure` can be run in the future. It runs before any Gofers start.
// It verifies that all the required dependencies are in place, loads kernel
// modules, and ensures the correct device files exist and are accessible.
// This should only be necessary once on the host. It should be run during the
// root container setup sequence to make sure it has run at least once.
func nvProxyPreGoferHostSetup(spec *specs.Spec, conf *config.Config) error {
if !specutils.GPUFunctionalityRequestedViaHook(spec, conf) {
return nil
}
// Locate binaries. For security reasons, unlike
// nvidia-container-runtime-hook, we don't add the container's filesystem
// to the search path. We also don't support
// /etc/nvidia-container-runtime/config.toml to avoid importing a TOML
// parser.
cliPath, err := exec.LookPath("nvidia-container-cli")
if err != nil {
return fmt.Errorf("failed to locate nvidia-container-cli in PATH: %w", err)
}
// nvidia-container-cli --load-kmods seems to be a noop; load kernel modules ourselves.
nvproxyLoadKernelModules()
if _, err := os.Stat("/dev/nvidiactl"); err != nil {
if !os.IsNotExist(err) {
return fmt.Errorf("stat(2) for /dev/nvidiactl failed: %w", err)
}
// Run `nvidia-container-cli info`.
// This has the side-effect of automatically creating GPU device files.
argv := []string{cliPath, "--load-kmods", "info"}
log.Debugf("Executing %q", argv)
var infoOut, infoErr strings.Builder
cmd := exec.Cmd{
Path: argv[0],
Args: argv,
Env: os.Environ(),
Stdout: &infoOut,
Stderr: &infoErr,
}
if err := cmd.Run(); err != nil {
return fmt.Errorf("nvidia-container-cli info failed, err: %v\nstdout: %s\nstderr: %s", err, infoOut.String(), infoErr.String())
}
log.Debugf("nvidia-container-cli info: %v", infoOut.String())
}
return nil
}
// nvproxyLoadKernelModules loads NVIDIA-related kernel modules with modprobe.
func nvproxyLoadKernelModules() {
for _, mod := range [...]string{
"nvidia",
"nvidia-uvm",
} {
argv := []string{
"/sbin/modprobe",
mod,
}
log.Debugf("Executing %q", argv)
var stdout, stderr strings.Builder
cmd := exec.Cmd{
Path: argv[0],
Args: argv,
Env: os.Environ(),
Stdout: &stdout,
Stderr: &stderr,
}
if err := cmd.Run(); err != nil {
// This might not be fatal since modules may already be loaded. Log
// the failure but continue.
log.Warningf("modprobe %s failed, err: %v\nstdout: %s\nstderr: %s", mod, err, stdout.String(), stderr.String())
}
}
}
// nvproxySetup runs `nvidia-container-cli configure` with gofer's PID. This
// sets up the container filesystem with bind mounts that allow it to use
// NVIDIA devices and libraries.
//
// This should be called after the Gofer has started but before it has
// pivot-root'd, as the bind mounts are created in the Gofer's mount namespace.
// This function has no effect if nvproxy functionality is not requested.
//
// This function essentially replicates
// nvidia-container-toolkit:cmd/nvidia-container-runtime-hook, i.e. the
// binary that executeHook() is hard-coded to skip, with differences noted
// inline. We do this rather than move the prestart hook because the
// "runtime environment" in which prestart hooks execute is vaguely
// defined, such that nvidia-container-runtime-hook and existing runsc
// hooks differ in their expected environment.
//
// Note that nvidia-container-cli will set up files in /proc which are useless,
// since they will be hidden by sentry procfs.
func nvproxySetup(spec *specs.Spec, conf *config.Config, goferPid int) error {
if !specutils.GPUFunctionalityRequestedViaHook(spec, conf) {
return nil
}
if spec.Root == nil {
return fmt.Errorf("spec missing root filesystem")
}
// nvidia-container-cli does not create this directory.
if err := os.MkdirAll(path.Join(spec.Root.Path, "proc", "driver", "nvidia"), 0555); err != nil {
return fmt.Errorf("failed to create /proc/driver/nvidia in app filesystem: %w", err)
}
cliPath, err := exec.LookPath("nvidia-container-cli")
if err != nil {
return fmt.Errorf("failed to locate nvidia-container-cli in PATH: %w", err)
}
// On Ubuntu, ldconfig is a wrapper around ldconfig.real, and we need the latter.
var ldconfigPath string
if _, err := os.Stat("/sbin/ldconfig.real"); err == nil {
ldconfigPath = "/sbin/ldconfig.real"
} else {
ldconfigPath = "/sbin/ldconfig"
}
devices, err := specutils.ParseNvidiaVisibleDevices(spec, conf)
if err != nil {
return fmt.Errorf("failed to get nvidia device numbers: %w", err)
}
argv := []string{
cliPath,
"--load-kmods",
"configure",
fmt.Sprintf("--ldconfig=@%s", ldconfigPath),
"--no-cgroups", // runsc doesn't configure device cgroups yet
fmt.Sprintf("--pid=%d", goferPid),
fmt.Sprintf("--device=%s", devices),
}
if nvidiaContainerCliConfigureNeedsCudaCompatModeFlag(cliPath) {
// "mount" is the flag's intended default value.
argv = append(argv, "--cuda-compat-mode=mount")
}
// Pass driver capabilities allowed by configuration as flags. See
// nvidia-container-toolkit/cmd/nvidia-container-runtime-hook/main.go:doPrestart().
driverCaps, err := specutils.NVProxyDriverCapsFromEnv(spec, conf)
if err != nil {
return fmt.Errorf("failed to get driver capabilities: %w", err)
}
argv = append(argv, driverCaps.NVIDIAFlags()...)
// Add rootfs path as the final argument.
argv = append(argv, spec.Root.Path)
log.Debugf("Executing %q", argv)
var stdout, stderr strings.Builder
cmd := exec.Cmd{
Path: argv[0],
Args: argv,
Env: os.Environ(),
Stdout: &stdout,
Stderr: &stderr,
}
if err := cmd.Run(); err != nil {
return fmt.Errorf("nvidia-container-cli configure failed, err: %v\nstdout: %s\nstderr: %s", err, stdout.String(), stderr.String())
}
return nil
}
func nvidiaContainerCliConfigureNeedsCudaCompatModeFlag(cliPath string) bool {
cmd := exec.Cmd{
Path: cliPath,
Args: []string{cliPath, "--version"},
}
log.Debugf("Executing %q", cmd.Args)
out, err := cmd.Output()
if err != nil {
log.Warningf("Failed to execute nvidia-container-cli --version: %v", err)
return false
}
m := regexp.MustCompile(`^cli-version: (\d+)\.(\d+)\.(\d+)`).FindSubmatch(out)
if m == nil {
log.Warningf("Failed to find version number in nvidia-container-cli --version: %s", out)
return false
}
major, err := strconv.Atoi(string(m[1]))
if err != nil {
log.Warningf("Invalid major version number in nvidia-container-cli --version: %v", err)
return false
}
minor, err := strconv.Atoi(string(m[2]))
if err != nil {
log.Warningf("Invalid minor version number in nvidia-container-cli --version: %v", err)
return false
}
release, err := strconv.Atoi(string(m[3]))
if err != nil {
log.Warningf("Invalid release version number in nvidia-container-cli --version: %v", err)
return false
}
// In nvidia-container-cli 1.17.7, in which the --cuda-compat-mode flag
// first appears, failing to pass this flag to nvidia-container-cli
// configure causes all other flags to be ignored.
return major == 1 && minor == 17 && release == 7
}
// CheckStopped checks if the container is stopped and updates its status.
func (c *Container) CheckStopped() {
if state, err := c.Sandbox.ContainerRuntimeState(c.ID); err != nil {
log.Warningf("Cannot find if container %v exists, checking if sandbox %v is running, err: %v", c.ID, c.Sandbox.ID, err)
if !c.IsSandboxRunning() {
log.Warningf("Sandbox isn't running anymore, marking container %v as stopped:", c.ID)
c.changeStatus(Stopped)
}
} else {
if state == boot.RuntimeStateStopped {
log.Warningf("Container %v is stopped", c.ID)
c.changeStatus(Stopped)
}
}
}
// GetNetworkConfig returns the network configuration.
func (c *Container) GetNetworkConfig() (*boot.CreateLinksAndRoutesArgs, error) {
log.Debugf("Returns network config, cid: %s", c.ID)
if !c.IsSandboxRunning() {
return nil, fmt.Errorf("sandbox is not running")
}
return c.Sandbox.GetNetworkConfig()
}