Files
runc/libcontainer/sync.go
T
Aleksa Sarai 8da42aaec2 sync: split init config (stream) and synchronisation (seqpacket) pipes
We have different requirements for the initial configuration and
initWaiter pipe (just send netlink and JSON blobs with no complicated
handling needed for message coalescing) and the packet-based
synchronisation pipe.

Tests with switching everything to SOCK_SEQPACKET lead to endless issues
with runc hanging on start-up because random things would try to do
short reads (which SOCK_SEQPACKET will not allow and the Go stdlib
explicitly treats as a streaming source), so splitting it was the only
reasonable solution. Even doing somewhat dodgy tricks such as adding a
Read() wrapper which actually calls ReadPacket() and makes it seem like
a stream source doesn't work -- and is a bit too magical.

One upside is that doing it this way makes the difference between the
modes clearer -- INITPIPE is still used for initWaiter syncrhonisation
but aside from that all other synchronisation is done by SYNCPIPE.

Signed-off-by: Aleksa Sarai <cyphar@cyphar.com>
2023-09-24 20:31:14 +08:00

197 lines
5.0 KiB
Go

package libcontainer
import (
"encoding/json"
"errors"
"fmt"
"io"
"os"
"strconv"
"github.com/opencontainers/runc/libcontainer/utils"
"github.com/sirupsen/logrus"
)
type syncType string
// Constants that are used for synchronisation between the parent and child
// during container setup. They come in pairs (with procError being a generic
// response which is followed by an &initError).
//
// [ child ] <-> [ parent ]
//
// procSeccomp --> [forward fd to listenerPath]
// file: seccomp fd
// --- no return synchronisation
//
// procHooks --> [run hooks]
// <-- procHooksDone
//
// procReady --> [final setup]
// <-- procRun
//
// procSeccomp --> [grab seccomp fd with pidfd_getfd()]
// <-- procSeccompDone
const (
procError syncType = "procError"
procReady syncType = "procReady"
procRun syncType = "procRun"
procHooks syncType = "procHooks"
procHooksDone syncType = "procHooksDone"
procSeccomp syncType = "procSeccomp"
procSeccompDone syncType = "procSeccompDone"
)
type syncFlags int
const (
syncFlagHasFd syncFlags = (1 << iota)
)
type syncT struct {
Type syncType `json:"type"`
Flags syncFlags `json:"flags"`
Arg *json.RawMessage `json:"arg,omitempty"`
File *os.File `json:"-"` // passed oob through SCM_RIGHTS
}
func (s syncT) String() string {
str := "type:" + string(s.Type)
if s.Flags != 0 {
str += " flags:0b" + strconv.FormatInt(int64(s.Flags), 2)
}
if s.Arg != nil {
str += " arg:" + string(*s.Arg)
}
if s.File != nil {
str += " file:" + s.File.Name() + " (fd:" + strconv.Itoa(int(s.File.Fd())) + ")"
}
return str
}
// initError is used to wrap errors for passing them via JSON,
// as encoding/json can't unmarshal into error type.
type initError struct {
Message string `json:"message,omitempty"`
}
func (i initError) Error() string {
return i.Message
}
func doWriteSync(pipe *syncSocket, sync syncT) error {
sync.Flags &= ^syncFlagHasFd
if sync.File != nil {
sync.Flags |= syncFlagHasFd
}
logrus.Debugf("writing sync %s", sync)
data, err := json.Marshal(sync)
if err != nil {
return fmt.Errorf("marshal sync %v: %w", sync.Type, err)
}
if _, err := pipe.WritePacket(data); err != nil {
return fmt.Errorf("writing sync %v: %w", sync.Type, err)
}
if sync.Flags&syncFlagHasFd != 0 {
logrus.Debugf("writing sync file %s", sync)
if err := utils.SendFile(pipe.File(), sync.File); err != nil {
return fmt.Errorf("sending file after sync %q: %w", sync.Type, err)
}
}
return nil
}
func writeSync(pipe *syncSocket, sync syncType) error {
return doWriteSync(pipe, syncT{Type: sync})
}
func writeSyncArg(pipe *syncSocket, sync syncType, arg interface{}) error {
argJSON, err := json.Marshal(arg)
if err != nil {
return fmt.Errorf("writing sync %v: marshal argument failed: %w", sync, err)
}
argJSONMsg := json.RawMessage(argJSON)
return doWriteSync(pipe, syncT{Type: sync, Arg: &argJSONMsg})
}
func doReadSync(pipe *syncSocket) (syncT, error) {
var sync syncT
logrus.Debugf("reading sync")
packet, err := pipe.ReadPacket()
if err != nil {
if errors.Is(err, io.EOF) {
logrus.Debugf("sync pipe closed")
return sync, err
}
return sync, fmt.Errorf("reading from parent failed: %w", err)
}
if err := json.Unmarshal(packet, &sync); err != nil {
return sync, fmt.Errorf("unmarshal sync from parent failed: %w", err)
}
logrus.Debugf("read sync %s", sync)
if sync.Type == procError {
var ierr initError
if sync.Arg == nil {
return sync, errors.New("procError missing error payload")
}
if err := json.Unmarshal(*sync.Arg, &ierr); err != nil {
return sync, fmt.Errorf("unmarshal procError failed: %w", err)
}
return sync, &ierr
}
if sync.Flags&syncFlagHasFd != 0 {
logrus.Debugf("reading sync file %s", sync)
file, err := utils.RecvFile(pipe.File())
if err != nil {
return sync, fmt.Errorf("receiving fd from sync %v failed: %w", sync.Type, err)
}
sync.File = file
}
return sync, nil
}
func readSyncFull(pipe *syncSocket, expected syncType) (syncT, error) {
sync, err := doReadSync(pipe)
if err != nil {
return sync, err
}
if sync.Type != expected {
return sync, fmt.Errorf("unexpected synchronisation flag: got %q, expected %q", sync.Type, expected)
}
return sync, nil
}
func readSync(pipe *syncSocket, expected syncType) error {
sync, err := readSyncFull(pipe, expected)
if err != nil {
return err
}
if sync.Arg != nil {
return fmt.Errorf("sync %v had unexpected argument passed: %q", expected, string(*sync.Arg))
}
if sync.File != nil {
_ = sync.File.Close()
return fmt.Errorf("sync %v had unexpected file passed", sync.Type)
}
return nil
}
// parseSync runs the given callback function on each syncT received from the
// child. It will return once io.EOF is returned from the given pipe.
func parseSync(pipe *syncSocket, fn func(*syncT) error) error {
for {
sync, err := doReadSync(pipe)
if err != nil {
if errors.Is(err, io.EOF) {
break
}
return err
}
if err := fn(&sync); err != nil {
return err
}
}
return nil
}