Files
Shayon MukherjeeandgVisor bot f9d6366dfa hostinet: support checkpoint/restore
Host networking did not have checkpoint/restore support. A sandbox running with `--network=host` could not be checkpointed because `hostinet.Stack` was not savable, and restore did not define what should happen to open host sockets.

This change makes the hostinet stack savable and defines that restore contract. Host socket fds cannot cross a checkpoint boundary, so `Socket.fd` is tagged `state:"nosave"` and save does not touch host sockets. With checkpoint `--leave-running`, the original sandbox keeps serving traffic on its existing connections. If save fails, the sandbox is left unharmed.

Without `--leave-running`, the sandbox exits after save and the host kernel closes its sockets. That sends `FIN` or `RST` so remote peers do not hang on half-open connections.

In the restored sandbox, `afterLoad` sets each socket fd to `-1`. Host syscalls then fail with `EBADF`. `Readiness` reports hangup and error so `epoll` waiters wake immediately, and tasks that were blocked in I/O see `EBADF` when their syscall restarts. `State()` reports no TCP state for these sockets rather than logging a warning on every `/proc/net/tcp` read. Applications are expected to reconnect after restore.

The stack's host-derived state is not saved. This includes `/proc/net/dev`, `/proc/net/snmp`, TCP buffer sizes, and allowed socket types. During restore, runsc configures a fresh hostinet stack before seccomp filters are installed. `ReplaceConfig` then copies the destination host configuration into the deserialized stack and transfers ownership of the fresh proc net handles. The restored sandbox therefore uses the destination host configuration and limits.

A hostinet checkpoint contains no netstack state and a netstack checkpoint contains no hostinet state, so the checkpoint now records the network type in its metadata. Restore fails with a clear error when the network stack kind differs between checkpoint and restore, instead of panicking inside `ReplaceConfig` during kernel load. Sandbox and none networking both use netstack and remain interchangeable. Checkpoints taken before the network type was recorded skip this check.

The hostinet `save_resume` syscall tests are enabled because save does not touch host sockets. The save and restore syscall tests remain excluded because they expect sockets to keep working after restore.

Container tests cover restored socket behavior for TCP, UDP, `epoll`, and a blocked accept. They also cover remote peer close after checkpoint, continued service after checkpoint `--leave-running`, and the error when restoring a host networking checkpoint with sandbox networking. The focused hostinet unit tests and the hostinet `save_resume` syscall tests pass on `x86_64`. The checkpoint/restore user guide documents the restored socket behavior.

Fixes #6243

COPYBARA_INTEGRATE_REVIEW=https://github.com/google/gvisor/pull/13404 from shayonj:hostinet-save-restore-v2 734480ee348d774346ec024b6483b89ea0a91adb
PiperOrigin-RevId: 950854464
2026-07-20 08:29:43 -07:00

299 lines
8.7 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 waiter provides the implementation of a wait queue, where waiters can
// be enqueued to be notified when an event of interest happens.
//
// Becoming readable and/or writable are examples of events. Waiters are
// expected to use a pattern similar to this to make a blocking function out of
// a non-blocking one:
//
// func (o *object) blockingRead(...) error {
// err := o.nonBlockingRead(...)
// if err != ErrAgain {
// // Completed with no need to wait!
// return err
// }
//
// e := createOrGetWaiterEntry(...)
// o.EventRegister(&e, waiter.EventIn)
// defer o.EventUnregister(&e)
//
// // We need to try to read again after registration because the
// // object may have become readable between the last attempt to
// // read and read registration.
// err = o.nonBlockingRead(...)
// for err == ErrAgain {
// wait()
// err = o.nonBlockingRead(...)
// }
//
// return err
// }
//
// Another goroutine needs to notify waiters when events happen. For example:
//
// func (o *object) Write(...) ... {
// // Do write work.
// [...]
//
// if oldDataAvailableSize == 0 && dataAvailableSize > 0 {
// // If no data was available and now some data is
// // available, the object became readable, so notify
// // potential waiters about this.
// o.Notify(waiter.EventIn)
// }
// }
package waiter
import (
"gvisor.dev/gvisor/pkg/sync"
)
// EventMask represents io events as used in the poll() syscall.
type EventMask uint64
// Events that waiters can wait on. The meaning is the same as those in the
// poll() syscall.
const (
EventIn EventMask = 0x01 // POLLIN
EventPri EventMask = 0x02 // POLLPRI
EventOut EventMask = 0x04 // POLLOUT
EventErr EventMask = 0x08 // POLLERR
EventHUp EventMask = 0x10 // POLLHUP
EventRdNorm EventMask = 0x0040 // POLLRDNORM
EventWrNorm EventMask = 0x0100 // POLLWRNORM
EventInternal EventMask = 0x1000
EventRdHUp EventMask = 0x2000 // POLLRDHUP
AllEvents EventMask = 0x1f | EventRdNorm | EventWrNorm | EventRdHUp
ReadableEvents EventMask = EventIn | EventRdNorm
WritableEvents EventMask = EventOut | EventWrNorm
)
// EventMaskFromLinux returns an EventMask representing the supported events
// from the Linux events e, which is in the format used by poll(2).
func EventMaskFromLinux(e uint32) EventMask {
// Our flag definitions are currently identical to Linux.
return EventMask(e) & AllEvents
}
// ToLinux returns e in the format used by Linux poll(2).
func (e EventMask) ToLinux() uint32 {
// Our flag definitions are currently identical to Linux.
return uint32(e)
}
// Waitable contains the methods that need to be implemented by waitable
// objects.
type Waitable interface {
// Readiness returns what the object is currently ready for. If it's
// not ready for a desired purpose, the caller may use EventRegister and
// EventUnregister to get notifications once the object becomes ready.
//
// Implementations should allow for events like EventHUp and EventErr
// to be returned regardless of whether they are in the input EventMask.
Readiness(mask EventMask) EventMask
// EventRegister registers the given waiter entry to receive
// notifications when an event occurs that makes the object ready for
// at least one of the events in mask.
EventRegister(e *Entry) error
// EventUnregister unregisters a waiter entry previously registered with
// EventRegister().
EventUnregister(e *Entry)
}
// EventListener provides a notify callback.
type EventListener interface {
// NotifyEvent is the function to be called when the waiter entry is
// notified. It is responsible for doing whatever is needed to wake up
// the waiter.
//
// The callback is supposed to perform minimal work, and cannot call
// any method on the queue itself because it will be locked while the
// callback is running.
//
// The mask indicates the events that occurred and that the entry is
// interested in.
NotifyEvent(mask EventMask)
}
// Entry represents a waiter that can be add to the a wait queue. It can
// only be in one queue at a time, and is added "intrusively" to the queue with
// no extra memory allocations.
//
// +stateify savable
type Entry struct {
waiterEntry
// eventListener receives the notification.
eventListener EventListener
// mask should be immutable once queued.
mask EventMask
}
// Init initializes the Entry.
//
// This must only be called when unregistered.
func (e *Entry) Init(eventListener EventListener, mask EventMask) {
e.eventListener = eventListener
e.mask = mask
}
// SetQueuedMask changes the entry mask.
//
// Preconditions: The Entry must be registered to the given Queue.
func (e *Entry) SetQueuedMask(q *Queue, mask EventMask) {
q.mu.Lock()
e.mask = mask
q.mu.Unlock()
}
// Mask returns the entry mask.
func (e *Entry) Mask() EventMask {
return e.mask
}
// NotifyEvent notifies the event listener.
//
// Mask should be the full set of active events.
func (e *Entry) NotifyEvent(mask EventMask) {
if m := mask & e.mask; m != 0 {
e.eventListener.NotifyEvent(m)
}
}
// ChannelNotifier is a simple channel-based notification.
type ChannelNotifier chan struct{}
// NotifyEvent implements waiter.EventListener.NotifyEvent.
func (c ChannelNotifier) NotifyEvent(EventMask) {
select {
case c <- struct{}{}:
default:
}
}
// NewChannelEntry initializes a new Entry that does a non-blocking write to a
// struct{} channel when the callback is called. It returns the new Entry
// instance and the channel being used.
func NewChannelEntry(mask EventMask) (e Entry, ch chan struct{}) {
ch = make(chan struct{}, 1)
e.Init(ChannelNotifier(ch), mask)
return e, ch
}
type functionNotifier func(EventMask)
// NotifyEvent implements waiter.EventListener.NotifyEvent.
func (f functionNotifier) NotifyEvent(mask EventMask) {
f(mask)
}
// NewFunctionEntry initializes a new Entry that calls the given function.
func NewFunctionEntry(mask EventMask, fn func(EventMask)) (e Entry) {
e.Init(functionNotifier(fn), mask)
return e
}
// NoopListener is an EventListener that does nothing.
//
// +stateify savable
type NoopListener struct{}
// NotifyEvent implements EventListener.NotifyEvent.
func (NoopListener) NotifyEvent(mask EventMask) {}
// Queue represents the wait queue where waiters can be added and
// notifiers can notify them when events happen.
//
// The zero value for waiter.Queue is an empty queue ready for use.
//
// +stateify savable
type Queue struct {
list waiterList
mu sync.RWMutex `state:"nosave"`
}
// EventRegister adds a waiter to the wait queue.
func (q *Queue) EventRegister(e *Entry) {
q.mu.Lock()
q.list.PushBack(e)
q.mu.Unlock()
}
// EventUnregister removes the given waiter entry from the wait queue.
func (q *Queue) EventUnregister(e *Entry) {
q.mu.Lock()
q.list.Remove(e)
q.mu.Unlock()
}
// Notify notifies all waiters in the queue whose masks have at least one bit
// in common with the notification mask.
func (q *Queue) Notify(mask EventMask) {
q.mu.RLock()
for e := q.list.Front(); e != nil; e = e.Next() {
m := mask & e.mask
if m == 0 {
continue
}
e.eventListener.NotifyEvent(m) // Skip intermediate call.
}
q.mu.RUnlock()
}
// Events returns the set of events being waited on. It is the union of the
// masks of all registered entries.
func (q *Queue) Events() EventMask {
q.mu.RLock()
defer q.mu.RUnlock()
ret := EventMask(0)
for e := q.list.Front(); e != nil; e = e.Next() {
ret |= e.mask
}
return ret
}
// IsEmpty returns if the wait queue is empty or not.
func (q *Queue) IsEmpty() bool {
q.mu.RLock()
defer q.mu.RUnlock()
return q.list.Front() == nil
}
// NeverReady implements the Waitable interface but is never ready. Otherwise,
// this is exactly the same as AlwaysReady.
type NeverReady struct {
}
// Readiness always returns 0 because this object is never ready.
func (*NeverReady) Readiness(EventMask) EventMask {
return 0
}
// EventRegister doesn't do anything because this object doesn't need to issue
// notifications because its readiness never changes.
func (*NeverReady) EventRegister(*Entry) error {
return nil
}
// EventUnregister doesn't do anything because this object doesn't need to issue
// notifications because its readiness never changes.
func (*NeverReady) EventUnregister(*Entry) {
}