Files
runc/vendor/github.com/checkpoint-restore/go-criu/v8/main.go
T
Kir Kolyshkin f66ace4cfa deps: bump to go-criu v8.3.0
go-criu v8.3.0 switches to protobuf-go-lite, which helps to remove
google.golang.org/protobuf dependency from here, reducing the runc
binary size from ~16M to ~14M.

The only missing piece is proto.String, proto.Bool, proto.Int32 etc.
helpers that return a pointer to a given variable. Those are replaced
by a generic mkPtr, which in turn is to be replaced by the new builtin
once Go < 1.26 is no longer supported.

Signed-off-by: Kir Kolyshkin <kolyshkin@gmail.com>
2026-06-15 12:09:36 -07:00

340 lines
7.5 KiB
Go

package criu
import (
"errors"
"fmt"
"os"
"os/exec"
"sort"
"strconv"
"syscall"
proto "github.com/checkpoint-restore/go-criu/v8/internal/proto"
"github.com/checkpoint-restore/go-criu/v8/rpc"
)
// extraFilesStartFd is the first fd number assigned to cmd.ExtraFiles by os/exec.
// As documented in os/exec: "entry i becomes file descriptor 3+i"
const extraFilesStartFd = 3
// Criu struct
type Criu struct {
swrkCmd *exec.Cmd
swrkSk *os.File
swrkPath string
inheritFds map[string]*os.File
}
// MakeCriu returns the Criu object required for most operations
func MakeCriu() *Criu {
return &Criu{
swrkPath: "criu",
}
}
// SetCriuPath allows setting the path to the CRIU binary
// if it is in a non standard location
func (c *Criu) SetCriuPath(path string) {
c.swrkPath = path
}
// AddInheritFd registers a file descriptor to be passed to CRIU.
// If opts.InheritFd is not set for an operation, it will be populated
// from these registrations using the same key order.
func (c *Criu) AddInheritFd(key string, file *os.File) {
if c.inheritFds == nil {
c.inheritFds = make(map[string]*os.File)
}
c.inheritFds[key] = file
}
func (c *Criu) inheritFdKeys() []string {
if len(c.inheritFds) == 0 {
return nil
}
keys := make([]string, 0, len(c.inheritFds))
for k := range c.inheritFds {
keys = append(keys, k)
}
sort.Strings(keys)
return keys
}
func (c *Criu) ensureInheritFd(opts *rpc.CriuOpts) {
if opts == nil || len(opts.GetInheritFd()) > 0 || len(c.inheritFds) == 0 {
return
}
keys := c.inheritFdKeys()
if len(keys) == 0 {
return
}
opts.InheritFd = make([]*rpc.InheritFd, 0, len(keys))
for i, key := range keys {
fd := int32(extraFilesStartFd + i)
opts.InheritFd = append(opts.InheritFd, &rpc.InheritFd{
Key: proto.Ptr(key),
Fd: proto.Ptr(fd),
})
}
}
// Prepare sets up everything for the RPC communication to CRIU
func (c *Criu) Prepare() error {
return c.doPrepare(nil)
}
func (c *Criu) doPrepare(opts *rpc.CriuOpts) error {
fds, err := syscall.Socketpair(syscall.AF_LOCAL, syscall.SOCK_SEQPACKET, 0)
if err != nil {
return err
}
cln := os.NewFile(uintptr(fds[0]), "criu-xprt-cln")
syscall.CloseOnExec(fds[0])
srv := os.NewFile(uintptr(fds[1]), "criu-xprt-srv")
defer func() { _ = srv.Close() }()
args := []string{"swrk", strconv.Itoa(fds[1])}
// #nosec G204
cmd := exec.Command(c.swrkPath, args...)
// Collect file descriptors to pass to child
inheritKeys := c.inheritFdKeys()
extraFiles := make([]*os.File, 0, len(inheritKeys))
// Add fds from AddInheritFd (sorted for stable ordering)
for _, k := range inheritKeys {
extraFiles = append(extraFiles, c.inheritFds[k])
}
c.ensureInheritFd(opts)
cmd.ExtraFiles = extraFiles
err = cmd.Start()
if err != nil {
_ = cln.Close()
return err
}
c.swrkCmd = cmd
c.swrkSk = cln
return nil
}
// Cleanup cleans up
func (c *Criu) Cleanup() error {
var errs []error
if c.swrkCmd != nil {
if err := c.swrkSk.Close(); err != nil {
errs = append(errs, err)
}
c.swrkSk = nil
if err := c.swrkCmd.Wait(); err != nil {
errs = append(errs, fmt.Errorf("criu swrk failed: %w", err))
}
c.swrkCmd = nil
}
return errors.Join(errs...)
}
func (c *Criu) sendAndRecv(reqB []byte) ([]byte, int, error) {
cln := c.swrkSk
_, err := cln.Write(reqB)
if err != nil {
return nil, 0, err
}
respB := make([]byte, 2*4096)
n, err := cln.Read(respB)
if err != nil {
return nil, 0, err
}
return respB, n, nil
}
func (c *Criu) doSwrk(reqType rpc.CriuReqType, opts *rpc.CriuOpts, nfy Notify) error {
resp, err := c.doSwrkWithResp(reqType, opts, nfy, nil)
if err != nil {
return err
}
respType := resp.GetType()
if respType != reqType {
return errors.New("unexpected CRIU RPC response")
}
return nil
}
func (c *Criu) doSwrkWithResp(reqType rpc.CriuReqType, opts *rpc.CriuOpts, nfy Notify, features *rpc.CriuFeatures) (resp *rpc.CriuResp, retErr error) {
c.ensureInheritFd(opts)
req := rpc.CriuReq{
Type: &reqType,
Opts: opts,
}
if nfy != nil {
opts.NotifyScripts = proto.Ptr(true)
}
if features != nil {
req.Features = features
}
if c.swrkCmd == nil {
err := c.doPrepare(opts)
if err != nil {
return nil, err
}
defer func() {
// append any cleanup errors to the returned error
err := c.Cleanup()
if err != nil {
retErr = errors.Join(retErr, err)
}
}()
}
for {
reqB, err := req.MarshalVT()
if err != nil {
return nil, err
}
respB, respS, err := c.sendAndRecv(reqB)
if err != nil {
return nil, err
}
resp = &rpc.CriuResp{}
err = resp.UnmarshalVT(respB[:respS])
if err != nil {
return nil, err
}
if !resp.GetSuccess() {
return resp, fmt.Errorf("operation failed (msg:%s err:%d)",
resp.GetCrErrmsg(), resp.GetCrErrno())
}
respType := resp.GetType()
if respType != rpc.CriuReqType_NOTIFY {
break
}
if nfy == nil {
return resp, errors.New("unexpected notify")
}
notify := resp.GetNotify()
switch notify.GetScript() {
case "pre-dump":
err = nfy.PreDump()
case "post-dump":
err = nfy.PostDump()
case "pre-restore":
err = nfy.PreRestore()
case "post-restore":
err = nfy.PostRestore(notify.GetPid())
case "network-lock":
err = nfy.NetworkLock()
case "network-unlock":
err = nfy.NetworkUnlock()
case "setup-namespaces":
err = nfy.SetupNamespaces(notify.GetPid())
case "post-setup-namespaces":
err = nfy.PostSetupNamespaces()
case "post-resume":
err = nfy.PostResume()
default:
err = nil
}
if err != nil {
return resp, err
}
req = rpc.CriuReq{
Type: &respType,
NotifySuccess: proto.Ptr(true),
}
}
return resp, nil
}
// Dump dumps a process
func (c *Criu) Dump(opts *rpc.CriuOpts, nfy Notify) error {
return c.doSwrk(rpc.CriuReqType_DUMP, opts, nfy)
}
// Restore restores a process
func (c *Criu) Restore(opts *rpc.CriuOpts, nfy Notify) error {
return c.doSwrk(rpc.CriuReqType_RESTORE, opts, nfy)
}
// PreDump does a pre-dump
func (c *Criu) PreDump(opts *rpc.CriuOpts, nfy Notify) error {
return c.doSwrk(rpc.CriuReqType_PRE_DUMP, opts, nfy)
}
// StartPageServer starts the page server
func (c *Criu) StartPageServer(opts *rpc.CriuOpts) error {
return c.doSwrk(rpc.CriuReqType_PAGE_SERVER, opts, nil)
}
// StartPageServerChld starts the page server and returns PID and port
func (c *Criu) StartPageServerChld(opts *rpc.CriuOpts) (int, int, error) {
resp, err := c.doSwrkWithResp(rpc.CriuReqType_PAGE_SERVER_CHLD, opts, nil, nil)
if err != nil {
return 0, 0, err
}
return int(resp.GetPs().GetPid()), int(resp.GetPs().GetPort()), nil
}
// GetCriuVersion executes the VERSION RPC call and returns the version
// as an integer. Major * 10000 + Minor * 100 + SubLevel
func (c *Criu) GetCriuVersion() (int, error) {
resp, err := c.doSwrkWithResp(rpc.CriuReqType_VERSION, nil, nil, nil)
if err != nil {
return 0, err
}
if resp.GetType() != rpc.CriuReqType_VERSION {
return 0, errors.New("unexpected CRIU RPC response")
}
version := resp.GetVersion().GetMajorNumber() * 10000
version += resp.GetVersion().GetMinorNumber() * 100
if resp.GetVersion().GetSublevel() != 0 {
version += resp.GetVersion().GetSublevel()
}
if resp.GetVersion().GetGitid() != "" {
// taken from runc: if it is a git release -> increase minor by 1
version -= (version % 100)
version += 100
}
return int(version), nil
}
// IsCriuAtLeast checks if the version is at least the same
// as the parameter version
func (c *Criu) IsCriuAtLeast(version int) (bool, error) {
criuVersion, err := c.GetCriuVersion()
if err != nil {
return false, err
}
if criuVersion >= version {
return true, nil
}
return false, nil
}