Merge pull request #14137 from EtiennePerot:le-pinring

PiperOrigin-RevId: 968808565
This commit is contained in:
gVisor bot
2026-08-21 18:39:39 -07:00
22 changed files with 601 additions and 4 deletions
+5
View File
@@ -68,6 +68,11 @@ const (
IORING_OP_READV = 1
)
// Constants for io_uring_register(2). See include/uapi/linux/io_uring.h.
const (
IORING_REGISTER_FILES = 2
)
// IORingIndex represents SQE array indexes.
//
// +marshal
+30
View File
@@ -0,0 +1,30 @@
load("//tools:defs.bzl", "go_library", "go_test")
package(
default_applicable_licenses = ["//:license"],
licenses = ["notice"],
)
go_library(
name = "pinring",
srcs = [
"pinring.go",
"pinring_unsafe.go",
],
visibility = ["//:sandbox"],
deps = [
"//pkg/abi/linux",
"//pkg/log",
"@org_golang_x_sys//unix:go_default_library",
],
)
go_test(
name = "pinring_test",
size = "medium",
srcs = ["pinring_test.go"],
library = ":pinring",
deps = [
"@org_golang_x_sys//unix:go_default_library",
],
)
+157
View File
@@ -0,0 +1,157 @@
// Copyright 2026 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 pinring allows asynchronous release of
// otherwise-expensive-to-release files.
//
// In Linux, some types of files require a full RCU grace period to release,
// such as `AF_PACKET` sockets (used in gVisor userspace networking), and KVM
// VM FDs (used, unsurprisingly, by the gVisor KVM platform).
// By releasing them asynchronously, we free up the sandbox process memory
// earlier, allowing better density of sandboxes to exist.
//
// How does this work? Kernel FDs are released when the last reference holder
// loses its ref, and typically that holder is the process that pays the cost.
// However, here, we want *none* of the gVisor processes to pay the cost, so we
// use a neat trick with `io_uring`. We create a disabled `io_uring` to which
// we can pin file descriptors (in what `io_uring` calls its "fixed-file
// table"). The `io_uring` itself is useless, won't accept work (disabled
// state), and our seccomp filters won't allow `io_uring`-related syscalls
// after sandbox startup anyway. But what we want is the side-effect:
// releasing an `io_uring` with FDs pinned to it causes the release to happen
// in a kernel thread, i.e. asynchronously. Victory.
package pinring
import (
"fmt"
"os"
"sync"
"time"
"golang.org/x/sys/unix"
"gvisor.dev/gvisor/pkg/log"
)
// A PinRing accumulates host fds to pin into a donated pin ring.
// The zero value means "no ring" and pins nothing.
type PinRing struct {
// FD is the donated ring's FD number.
FD int
// mu protects the fields below.
mu sync.Mutex
fds []int32
finalized bool
}
// Exists reports whether there is a ring to pin into.
func (r *PinRing) Exists() bool {
// stdio FDs can never be rings, so we use this as a proxy to know if
// we have a legit io_uring FD at `r.FD`.
// Has the nice property that the zero value of PinRing is meaningful.
return r != nil && r.FD >= 3
}
// Add records host FD `fd` for pinning. No-op if there is no ring.
// It duplicates `fd`, so no ownership transfer.
func (r *PinRing) Add(fd int) {
if !r.Exists() {
return
}
dup, err := unix.FcntlInt(uintptr(fd), unix.F_DUPFD_CLOEXEC, 0)
if err != nil {
log.Warningf("Pin ring: cannot dup FD %d, not pinning it: %v", fd, err)
return
}
r.mu.Lock()
defer r.mu.Unlock()
if r.finalized {
panic("tried to pin an FD to PinRing after finalization")
}
r.fds = append(r.fds, int32(dup))
}
// Finalize pins the added FDs into the ring.
// No-op if there is no ring.
func (r *PinRing) Finalize() error {
if !r.Exists() {
return nil
}
r.mu.Lock()
defer r.mu.Unlock()
if r.finalized {
return nil
}
r.finalized = true
log.Debugf("Pin ring: finalizing with %d FDs (ring at FD %d)", len(r.fds), r.FD)
defer unix.Close(r.FD)
if len(r.fds) == 0 {
return nil
}
err := registerFiles(r.FD, r.fds)
for _, fd := range r.fds {
unix.Close(int(fd))
}
return err
}
// SendPidfd sends a `pidfd` of process `pid` over `sock`.
// See `WaitExit` below.
func SendPidfd(sock *os.File, pid int) error {
pidfd, err := unix.PidfdOpen(pid, 0)
if err != nil {
return fmt.Errorf("pidfd_open(%d): %w", pid, err)
}
defer unix.Close(pidfd)
return unix.Sendmsg(int(sock.Fd()), []byte{0}, unix.UnixRights(pidfd), nil, 0)
}
// WaitExit blocks until the process whose `pidfd` arrives over socket `sockFD`
// (sent by `SendPidfd`) has fully exited, and reports false only if it is
// confirmed still running after `timeout`.
func WaitExit(sockFD int, timeout time.Duration) bool {
pollIn := func(fd int32, timeout time.Duration) bool {
ts := unix.NsecToTimespec(timeout.Nanoseconds())
for {
n, err := unix.Ppoll([]unix.PollFd{{Fd: fd, Events: unix.POLLIN}}, &ts, nil)
if err == unix.EINTR {
continue
}
return err == nil && n > 0
}
}
// Wait for the pidfd to come in.
if !pollIn(int32(sockFD), timeout) {
return true
}
// The flags must be exactly what the gofer's seccomp filter allows for `recvmsg`.
buf := make([]byte, 1)
oob := make([]byte, unix.CmsgSpace(4))
_, oobn, _, _, err := unix.Recvmsg(sockFD, buf, oob, unix.MSG_DONTWAIT|unix.MSG_TRUNC)
if err != nil {
return true
}
msgs, err := unix.ParseSocketControlMessage(oob[:oobn])
if err != nil || len(msgs) == 0 {
return true
}
fds, err := unix.ParseUnixRights(&msgs[0])
if err != nil || len(fds) == 0 {
return true
}
defer unix.Close(fds[0])
// Wait for the process to die.
return pollIn(int32(fds[0]), timeout)
}
+235
View File
@@ -0,0 +1,235 @@
// Copyright 2026 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 pinring
// This file verifies the Linux kernel behavior that the pin ring hack relies
// on; see the package comment.
import (
"errors"
"fmt"
"os"
"os/exec"
"slices"
"strconv"
"strings"
"testing"
"time"
"golang.org/x/sys/unix"
)
// packetProbeEnv marks an invocation of this binary as an `AF_PACKET` probe
// child and selects its mode:
// - "true": pin the packet sockets into a disabled io_uring before exiting
// - "false": just hold the AF_PACKET sockets
// - "" or unset: Run the test as normal.
const packetProbeEnv = "AFPACKET_DEFERRED_RELEASE_PROBE"
// kvmProbeEnv is packetProbeEnv's equivalent for the KVM probe child, which
// holds bare KVM VM FDs instead of packet sockets.
const kvmProbeEnv = "KVM_DEFERRED_RELEASE_PROBE"
// Hold this many `AF_PACKET` sockets to amplify the effect of the deferred
// release.
const numSockets = 16
// Hold this many KVM VMs.
const numVMs = 4
// kvmCreateVM is _IO(KVMIO, 0x01) from <linux/kvm.h>.
// Importing the kvm platform package would create an import cycle.
const kvmCreateVM = 0xAE01
func TestMain(m *testing.M) {
for env, probe := range map[string]func(bool){
packetProbeEnv: packetProbe,
kvmProbeEnv: kvmProbe,
} {
v := os.Getenv(env)
if v == "" {
continue
}
deferRelease, err := strconv.ParseBool(v)
if err != nil {
fmt.Printf("%s=%q: %v\n", env, v, err)
os.Exit(1)
}
probe(deferRelease)
os.Exit(0)
}
os.Exit(m.Run())
}
func htons(v uint16) uint16 {
return v<<8 | v>>8
}
// packetProbe executes when `AFPACKET_DEFERRED_RELEASE_PROBE` is set.
// It creates `AF_PACKET` sockets and exits without closing them.
// With deferRelease, the sockets are also registered as files of a
// permanently-disabled `io_uring` (as `PinRing` does), which should
// make the exit cheap at least as of this writing on Linux 6.18.
// A "SKIP: ..." line on stdout means the environment cannot run the probe
// and that the test should be skipped.
func packetProbe(deferRelease bool) {
fds := make([]int32, numSockets)
for i := range fds {
sock, err := unix.Socket(unix.AF_PACKET, unix.SOCK_RAW, int(htons(unix.ETH_P_ALL)))
if err == unix.EPERM || err == unix.EACCES {
fmt.Printf("SKIP: creating an AF_PACKET socket requires CAP_NET_RAW: %v\n", err)
return
}
if err != nil {
fmt.Printf("socket(AF_PACKET, SOCK_RAW, ETH_P_ALL): %v\n", err)
os.Exit(1)
}
fds[i] = int32(sock)
}
if deferRelease {
pinToRing(fds)
}
// `fds` being left open is very much intentional.
}
// kvmProbe is the same idea, but for bare KVM VM fds.
func kvmProbe(deferRelease bool) {
kvmFD, err := unix.Open("/dev/kvm", unix.O_RDWR, 0)
if err == unix.ENOENT || err == unix.EACCES || err == unix.EPERM {
fmt.Printf("SKIP: cannot open /dev/kvm: %v\n", err)
return
}
if err != nil {
fmt.Printf("open(/dev/kvm): %v\n", err)
os.Exit(1)
}
fds := make([]int32, numVMs)
for i := range fds {
vm, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(kvmFD), kvmCreateVM, 0)
if errno == unix.EPERM || errno == unix.EACCES {
fmt.Printf("SKIP: cannot create a KVM VM: %v\n", errno)
return
}
if errno != 0 {
fmt.Printf("ioctl(KVM_CREATE_VM): %v\n", errno)
os.Exit(1)
}
fds[i] = int32(vm)
}
if deferRelease {
pinToRing(fds)
}
// `fds` being left open is, again, very much intentional.
}
// pinnedRing holds pinToRing's ring open until the probe's `do_exit`.
// This is necessary because go GC would otherwise auto-close the FD if
// it sees a dangling `*os.File`.
var pinnedRing *os.File
// pinToRing parks `fds` in the fixed-file table of a new permanently
// disabled io_uring, which is deliberately left open for `do_exit` to drop.
// Prints "SKIP: ..." and returns if the environment has no usable io_uring.
func pinToRing(fds []int32) {
ring, err := NewDisabledIOURing()
switch {
case err == nil:
case errors.Is(err, unix.ENOSYS):
fmt.Println("SKIP: kernel doesn't support io_uring (ENOSYS)")
return
case errors.Is(err, unix.EPERM):
fmt.Println("SKIP: kernel has io_uring disabled (EPERM)")
return
case errors.Is(err, unix.EINVAL):
fmt.Println("SKIP: kernel doesn't understand IORING_SETUP_R_DISABLED (EINVAL)")
return
default:
fmt.Println(err)
os.Exit(1)
}
pinnedRing = ring
if err := registerFiles(int(ring.Fd()), fds); err != nil {
fmt.Println(err)
os.Exit(1)
}
}
// TestAFPacketDeferredRelease compares the exit time of children processes
// holding AF_PACKET sockets with and without the io_uring pin hack.
func TestAFPacketDeferredRelease(t *testing.T) {
compareProbes(t, packetProbeEnv, "AF_PACKET socket")
}
// TestKVMVMDeferredRelease is TestAFPacketDeferredRelease for KVM VM fds.
func TestKVMVMDeferredRelease(t *testing.T) {
compareProbes(t, kvmProbeEnv, "KVM VM fd")
}
// compareProbes runs the probe child and checks that we do gain time by
// using the pinring hack to release FDs early.
func compareProbes(t *testing.T, env, what string) {
expedited, err := os.ReadFile("/sys/kernel/rcu_expedited")
if err != nil {
t.Fatalf("cannot read rcu_expedited: %v", err)
}
if strings.TrimSpace(string(expedited)) != "0" {
t.Skip("rcu_expedited is set; a normal grace period cannot be observed")
}
// One warm run to warm up every page of the binary and check if we
// need to skip.
out, _, err := runProbe(env, true)
if err != nil {
t.Fatalf("warmup probe: %v\noutput: %s", err, out)
}
if strings.HasPrefix(out, "SKIP: ") {
t.Skip(strings.TrimSuffix(strings.TrimPrefix(out, "SKIP: "), "\n"))
}
var plain, deferred []time.Duration
for range 32 {
plain = append(plain, timeProbe(t, env, false))
deferred = append(deferred, timeProbe(t, env, true))
}
p50Plain, p50Deferred := median(plain), median(deferred)
t.Logf("p50: plain=%v, with deferral=%v", p50Plain, p50Deferred)
if p50Plain < p50Deferred {
t.Errorf("pinning the %s did not help with latency: plain exit p50 = %v vs deferred exit p50 = %v", what, p50Plain, p50Deferred)
}
}
func runProbe(env string, deferRelease bool) (string, time.Duration, error) {
cmd := exec.Command(os.Args[0])
cmd.Env = append(os.Environ(), env+"="+strconv.FormatBool(deferRelease))
start := time.Now()
out, err := cmd.CombinedOutput()
return string(out), time.Since(start), err
}
func timeProbe(t *testing.T, env string, deferRelease bool) time.Duration {
t.Helper()
out, d, err := runProbe(env, deferRelease)
if err != nil || out != "" {
t.Fatalf("probe(%s=%t): %v\noutput: %s", env, deferRelease, err, out)
}
return d
}
func median(samples []time.Duration) time.Duration {
s := slices.Clone(samples)
slices.Sort(s)
return (s[(len(s)-1)/2] + s[len(s)/2]) / 2
}
+46
View File
@@ -0,0 +1,46 @@
// Copyright 2026 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 pinring
import (
"fmt"
"os"
"unsafe"
"golang.org/x/sys/unix"
"gvisor.dev/gvisor/pkg/abi/linux"
)
// NewDisabledIOURing returns a new pin ring for donation to its holder.
// Created with `IORING_SETUP_R_DISABLED` and never enabled.
// It accepts no `io_uring` submissions. Only its FD table is ever used.
func NewDisabledIOURing() (*os.File, error) {
params := linux.IOUringParams{Flags: linux.IORING_SETUP_R_DISABLED}
ring, _, errno := unix.Syscall(unix.SYS_IO_URING_SETUP, 1, uintptr(unsafe.Pointer(&params)), 0)
if errno != 0 {
return nil, fmt.Errorf("io_uring_setup: %w", errno)
}
return os.NewFile(ring, "pin ring"), nil
}
// registerFiles pins `fds` into `ring`'s fixed-file table.
// Can only be called once per `io_uring` ring.
func registerFiles(ring int, fds []int32) error {
if _, _, errno := unix.Syscall6(unix.SYS_IO_URING_REGISTER, uintptr(ring), linux.IORING_REGISTER_FILES, uintptr(unsafe.Pointer(&fds[0])), uintptr(len(fds)), 0, 0); errno != 0 {
return fmt.Errorf("io_uring_register(IORING_REGISTER_FILES): %w", errno)
}
return nil
}
+1
View File
@@ -21,6 +21,7 @@ go_library(
"//pkg/cpuid",
"//pkg/fd",
"//pkg/hostarch",
"//pkg/pinring",
"//pkg/seccomp",
"//pkg/seccomp/precompiledseccomp",
"//pkg/sentry/arch",
+1
View File
@@ -86,6 +86,7 @@ go_library(
"//pkg/hosttid",
"//pkg/log",
"//pkg/metric",
"//pkg/pinring",
"//pkg/ring0",
"//pkg/ring0/pagetables",
"//pkg/seccomp",
+4
View File
@@ -18,6 +18,7 @@
package kvm
import (
"gvisor.dev/gvisor/pkg/pinring"
"gvisor.dev/gvisor/pkg/timing"
)
@@ -34,6 +35,9 @@ type Config struct {
// StartupTimer is used to track KVM initialization milestones.
StartupTimer *timing.Timer
// PinRing is used to pin the VM FD and to release it asynchronously.
PinRing *pinring.PinRing
}
func (*machine) applyConfig(config *Config) error { return nil }
+5
View File
@@ -19,6 +19,7 @@ import (
"fmt"
"golang.org/x/sys/unix"
pkgcontext "gvisor.dev/gvisor/pkg/context"
"gvisor.dev/gvisor/pkg/fd"
"gvisor.dev/gvisor/pkg/hostarch"
@@ -126,6 +127,9 @@ func New(deviceFile *fd.FD, config Config) (*KVM, error) {
deviceFile.Close()
config.StartupTimer.Reached("kvm VM created")
// `kvm_destroy_vm` costs tens of milliseconds. Do that async.
config.PinRing.Add(int(vm))
// Create a VM context.
machine, err := newMachine(int(vm), &config)
if err != nil {
@@ -236,6 +240,7 @@ func (*constructor) New(opts platform.Options) (platform.Platform, error) {
ApplicationCores: opts.ApplicationCores,
UseCPUNums: opts.UseCPUNums,
StartupTimer: opts.StartupTimer,
PinRing: opts.PinRing,
})
}
+6
View File
@@ -27,6 +27,7 @@ import (
"gvisor.dev/gvisor/pkg/context"
"gvisor.dev/gvisor/pkg/fd"
"gvisor.dev/gvisor/pkg/hostarch"
"gvisor.dev/gvisor/pkg/pinring"
"gvisor.dev/gvisor/pkg/seccomp"
"gvisor.dev/gvisor/pkg/seccomp/precompiledseccomp"
"gvisor.dev/gvisor/pkg/sentry/arch"
@@ -610,6 +611,11 @@ type Options struct {
// StartupTimer is the timer tracking overall sandbox startup.
// Platform constructors should record any relevant midpoints on it.
StartupTimer *timing.Timer
// PinRing is as an accumulator for host FDs that are expensive to release.
// It allows releasing them asynchronously.
// See `//pkg/pinring`.
PinRing *pinring.PinRing
}
// Constructor represents a platform type.
+1
View File
@@ -56,6 +56,7 @@ go_library(
"//pkg/log",
"//pkg/memutil",
"//pkg/metric",
"//pkg/pinring",
"//pkg/rand",
"//pkg/rdma",
"//pkg/refs",
+1
View File
@@ -1283,6 +1283,7 @@ func (cm *containerManager) SetNetworkArgs(args *CreateLinksAndRoutesArgs, _ *st
}
f := dupedFDs[fdIdx]
fdIdx++
cm.l.pinRing.Add(f.FD())
isSocket, err := fdbased.IsSocketFD(f.FD())
if err != nil {
+17 -2
View File
@@ -38,6 +38,7 @@ import (
"gvisor.dev/gvisor/pkg/log"
"gvisor.dev/gvisor/pkg/memutil"
"gvisor.dev/gvisor/pkg/metric"
"gvisor.dev/gvisor/pkg/pinring"
"gvisor.dev/gvisor/pkg/rand"
"gvisor.dev/gvisor/pkg/rdma"
"gvisor.dev/gvisor/pkg/refs"
@@ -340,6 +341,10 @@ type Loader struct {
// host network namespace during sandbox creation.
networkArgs *CreateLinksAndRoutesArgs
// pinRing accumulates host FDs to pin before seccomp filters are
// installed.
pinRing pinring.PinRing
// fsSaveFDs are FDs used for user-triggered filesystem checkpoint saving.
fsSaveFDs []*fd.FD
@@ -406,6 +411,10 @@ type Args struct {
// ControllerFD is the FD to the URPC controller. The Loader takes ownership
// of this FD and may close it at any time.
ControllerFD int
// PinRingFD is the FD of the donated pin ring where the sentry registers
// its expensive-to-release files into. See `//pkg/pinring`.
// -1 if there is no ring.
PinRingFD int
// Device is an optional argument that is passed to the platform. The Loader
// takes ownership of this file and may close it at any time.
Device *fd.FD
@@ -713,7 +722,8 @@ func New(args Args) (*Loader, error) {
}
// Create kernel and platform.
p, err := createPlatform(args.Conf, args.NumCPU, args.Device, args.ID, args.StartupTimer)
l.pinRing.FD = args.PinRingFD
p, err := createPlatform(args.Conf, args.NumCPU, args.Device, args.ID, args.StartupTimer, &l.pinRing)
if err != nil {
return nil, fmt.Errorf("creating platform: %w", err)
}
@@ -1069,7 +1079,7 @@ func (l *Loader) Destroy() {
refs.OnExit()
}
func createPlatform(conf *config.Config, numCPU int, deviceFile *fd.FD, sandboxID string, startupTimer *timing.Timer) (platform.Platform, error) {
func createPlatform(conf *config.Config, numCPU int, deviceFile *fd.FD, sandboxID string, startupTimer *timing.Timer, pinRing *pinring.PinRing) (platform.Platform, error) {
platformName := conf.Platform
p, err := platform.Lookup(conf.Platform)
if err != nil {
@@ -1085,6 +1095,7 @@ func createPlatform(conf *config.Config, numCPU int, deviceFile *fd.FD, sandboxI
UseCPUNums: platformName == "kvm" && conf.UseCPUNums,
SandboxID: sandboxID,
StartupTimer: startupTimer,
PinRing: pinRing,
})
}
@@ -1246,6 +1257,10 @@ func (l *Loader) run() error {
l.startupTimer.Reached("network configured")
}
if err := l.pinRing.Finalize(); err != nil {
log.Warningf("Cannot pin files to the pin ring: %v. This slows down gVisor sandbox teardown.", err)
}
// Finally done with all configuration. Setup filters before user code
// is loaded.
if err := l.installSeccompFilters(); err != nil {
+1 -1
View File
@@ -371,7 +371,7 @@ func (r *restorer) restore(l *Loader) error {
}
r.timer.Reached("specs validated")
p, err := createPlatform(l.root.conf, l.root.applicationCores, r.deviceFile, l.sandboxID, r.timer)
p, err := createPlatform(l.root.conf, l.root.applicationCores, r.deviceFile, l.sandboxID, r.timer, &l.pinRing)
if err != nil {
return fmt.Errorf("creating platform: %v", err)
}
+1
View File
@@ -62,6 +62,7 @@ go_library(
"//pkg/lisafs",
"//pkg/log",
"//pkg/metric",
"//pkg/pinring",
"//pkg/prometheus",
"//pkg/sentry/checkpoint",
"//pkg/sentry/control",
+19
View File
@@ -20,6 +20,7 @@ import (
"os"
"path/filepath"
"runtime/debug"
"time"
"github.com/google/subcommands"
specs "github.com/opencontainers/runtime-spec/specs-go"
@@ -27,6 +28,7 @@ import (
"gvisor.dev/gvisor/pkg/lisafs"
"gvisor.dev/gvisor/pkg/log"
"gvisor.dev/gvisor/pkg/pinring"
"gvisor.dev/gvisor/pkg/unet"
"gvisor.dev/gvisor/pkg/urpc"
"gvisor.dev/gvisor/runsc/boot"
@@ -118,6 +120,16 @@ type Gofer struct {
profileFDs profile.FDArgs
syncFDs goferSyncFDs
stopProfiling func()
// pinRingFD is the donated pin ring (see `//pkg/pinring`).
// Never used (on purpose), and never closed.
// It must stay open until this process exits to reap the benefits.
// See `--pin-ring-fd`.
pinRingFD int
// sentryExitSyncFD receives a pidfd of the sandbox process.
// The gofer waits on it before exiting. See `--sync-sentry-exit-fd`.
sentryExitSyncFD int
}
// Name implements subcommands.Command.
@@ -148,6 +160,8 @@ func (g *Gofer) SetFlags(f *flag.FlagSet) {
f.IntVar(&g.specFD, "spec-fd", -1, "required fd with the container spec")
f.IntVar(&g.mountsFD, "mounts-fd", -1, "mountsFD is the file descriptor to write list of mounts after they have been resolved (direct paths, no symlinks).")
f.IntVar(&g.goferToHostRPCFD, "rpc-fd", -1, "gofer-to-host RPC file descriptor.")
f.IntVar(&g.pinRingFD, "pin-ring-fd", -1, "FD of a disabled io_uring with slow-to-release sandbox FDs pinned to it. Deliberately held open and unused by the gofer such that the sandbox process is not the last ref holder to them, making their release asynchronous.")
f.IntVar(&g.sentryExitSyncFD, "sync-sentry-exit-fd", -1, "socket FD over which a pidfd of the sandbox process is received. Used by the gofer to wait for the sandbox to fully exit before exiting itself.")
// IDs to run gofer as.
f.IntVar(&g.uid, "uid", 0, "User ID")
@@ -497,6 +511,11 @@ func (g *Gofer) serve(spec *specs.Spec, conf *config.Config, root string, ruid i
server.Wait()
server.Destroy()
log.Infof("All lisafs servers exited.")
if g.sentryExitSyncFD >= 0 {
if !pinring.WaitExit(g.sentryExitSyncFD, 500*time.Millisecond) {
log.Warningf("Sandbox still running 500ms after its gofer connections closed; something is making sandbox teardown slower than necessary. Exiting anyway.")
}
}
if g.stopProfiling != nil {
g.stopProfiling()
}
+6
View File
@@ -99,6 +99,10 @@ type Boot struct {
// deviceFD is the file descriptor for the platform device file.
deviceFD int
// pinRingFD is the file descriptor of the donated pin ring.
// See `//pkg/pinring`.
pinRingFD int
// ioFDs is the list of FDs used to connect to FS gofers.
ioFDs sandboxsetup.IntFlags
@@ -273,6 +277,7 @@ func (b *Boot) SetFlags(f *flag.FlagSet) {
f.IntVar(&b.specFD, "spec-fd", -1, "required fd with the container spec")
f.IntVar(&b.controllerFD, "controller-fd", -1, "required FD of a stream socket for the control server that must be donated to this process")
f.IntVar(&b.deviceFD, "device-fd", -1, "FD for the platform device file")
f.IntVar(&b.pinRingFD, "pin-ring-fd", -1, "FD of a disabled io_uring in which the sentry pins its expensive-to-release files")
f.Var(&b.ioFDs, "io-fds", "list of image FDs and/or socket FDs to connect gofer clients. They must follow this order: root first, then mounts as defined in the spec")
f.IntVar(&b.devIoFD, "dev-io-fd", -1, "FD to connect dev gofer client")
f.Var(&b.stdioFDs, "stdio-fds", "list of FDs containing sandbox stdin, stdout, and stderr in that order")
@@ -663,6 +668,7 @@ func (b *Boot) Execute(_ context.Context, f *flag.FlagSet, args ...any) subcomma
Conf: conf,
ControllerFD: b.controllerFD,
Device: fd.New(b.deviceFD),
PinRingFD: b.pinRingFD,
GoferFDs: b.ioFDs.GetArray(),
DevGoferFD: b.devIoFD,
StdioFDs: b.stdioFDs.GetArray(),
+1
View File
@@ -22,6 +22,7 @@ go_library(
"//pkg/abi/linux",
"//pkg/cleanup",
"//pkg/log",
"//pkg/pinring",
"//pkg/sentry/control",
"//pkg/sentry/fsimpl/erofs",
"//pkg/sentry/fsimpl/tmpfs",
+35
View File
@@ -35,6 +35,7 @@ import (
"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"
@@ -153,6 +154,16 @@ type Container struct {
// 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.
@@ -393,12 +404,23 @@ func (c *Container) createRoot(conf *config.Config, args Args, sandboxID string)
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 {
@@ -1511,6 +1533,19 @@ func (c *Container) createGoferProcess(conf *config.Config, mountHints *boot.Pod
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 {
+23
View File
@@ -32,10 +32,16 @@ func LogDonations(cmd *exec.Cmd) {
}
}
const multipleFDTransferred = -42
// Agency keeps track of files that need to be donated to a child process.
type Agency struct {
donations []donation
closePending []*os.File
// transferred maps flag names to FD numbers.
// The special value `multipleFDTransferred` means this flag was used
// multiple times, so it cannot be mapped to a single FD.
transferred map[string]int
}
type donation struct {
@@ -100,6 +106,14 @@ func (f *Agency) Transfer(cmd *exec.Cmd, nextFD int) int {
nextFD++
}
cmd.Args = append(cmd.Args, fmt.Sprintf("--%s=%d", d.flag, fd))
if f.transferred == nil {
f.transferred = make(map[string]int)
}
if _, alreadyExist := f.transferred[d.flag]; alreadyExist {
f.transferred[d.flag] = multipleFDTransferred
} else {
f.transferred[d.flag] = fd
}
}
}
// Reset donations made so far in case more transfers are needed.
@@ -107,6 +121,15 @@ func (f *Agency) Transfer(cmd *exec.Cmd, nextFD int) int {
return nextFD
}
// TransferredFD returns the FD number that the file donated under flag was
// given in the child, or -1 if nothing or multiple FDs were donated with it.
func (f *Agency) TransferredFD(flag string) int {
if fd, ok := f.transferred[flag]; ok && fd != multipleFDTransferred {
return fd
}
return -1
}
// DonateAndTransferCustomFiles sets up the flags for passing file descriptors from the
// host to the sandbox. Making use of the agency is not necessary,
func DonateAndTransferCustomFiles(cmd *exec.Cmd, nextFD int, files map[int]*os.File) int {
+5
View File
@@ -321,6 +321,10 @@ type Args struct {
// open filesystem checkpoint files using O_DIRECT.
FSRestoreImagePath string
FSRestoreDirect bool
// PinRingFile, if non-nil, is the pin ring to donate to the boot
// process (see `//pkg/pinring`).
PinRingFile *os.File
}
// New creates the sandbox process. The caller must call Destroy() on the
@@ -1017,6 +1021,7 @@ func (s *Sandbox) createSandboxProcess(conf *config.Config, args *Args, startSyn
donations.DonateAndClose("gofer-filestore-fds", args.GoferFilestoreFiles...)
donations.DonateAndClose("mounts-fd", args.MountsFile)
donations.Donate("start-sync-fd", startSyncFile)
donations.DonateAndClose("pin-ring-fd", args.PinRingFile)
if err := donations.DonateLogFile("user-log-fd", args.UserLog, os.O_CREATE|os.O_WRONLY|os.O_APPEND, lfOpts); err != nil {
return err
}
+1 -1
View File
@@ -755,7 +755,7 @@ func runRunsc(tc *gtest.TestCase, spec *specs.Spec) error {
}
// Regex matching some performance-related warnings that are safe to ignore in tests.
var performanceWarningRegexp = regexp.MustCompile(`(?i).*(slows?|slowing).*startup.*`)
var performanceWarningRegexp = regexp.MustCompile(`(?i).*(slows?|slowing).*(startup|teardown).*`)
func isWarning(line string) bool {
if len(line) >= 5 && line[:5] == "panic" {