libcontainer: rm own error system

This removes libcontainer's own error wrapping system, consisting of a
few types and functions, aimed at typization, wrapping and unwrapping
of errors, as well as saving error stack traces.

Since Go 1.13 now provides its own error wrapping mechanism and a few
related functions, it makes sense to switch to it.

While doing that, improve some error messages so that they start
with "error", "unable to", or "can't".

A few things that are worth mentioning:

1. We lose stack traces (which were never shown anyway).

2. Users of libcontainer that relied on particular errors (like
   ContainerNotExists) need to switch to using errors.Is with
   the new errors defined in error.go.

3. encoding/json is unable to unmarshal the built-in error type,
   so we have to introduce initError and wrap the errors into it
   (basically passing the error as a string). This is the same
   as it was before, just a tad simpler (actually the initError
   is a type that got removed in commit afa844311; also suddenly
   ierr variable name makes sense now).

Signed-off-by: Kir Kolyshkin <kolyshkin@gmail.com>
This commit is contained in:
Kir Kolyshkin
2021-06-21 17:10:48 -07:00
parent 60c647a783
commit e918d02139
19 changed files with 142 additions and 427 deletions

View File

@@ -55,8 +55,7 @@ status of "ubuntu01" as "stopped" the following will delete resources held for
force := context.Bool("force")
container, err := getContainer(context)
if err != nil {
var lerr libcontainer.Error
if errors.As(err, &lerr) && lerr.Code() == libcontainer.ContainerNotExists {
if errors.Is(err, libcontainer.ErrNotExist) {
// if there was an aborted start or something of the sort then the container's directory could exist but
// libcontainer does not see it because the state.json file inside that directory was never created.
path := filepath.Join(context.GlobalString("root"), id)

View File

@@ -74,22 +74,12 @@ type BaseContainer interface {
ID() string
// Returns the current status of the container.
//
// errors:
// ContainerNotExists - Container no longer exists,
// Systemerror - System error.
Status() (Status, error)
// State returns the current container's state information.
//
// errors:
// SystemError - System error.
State() (*State, error)
// OCIState returns the current container's state information.
//
// errors:
// SystemError - System error.
OCIState() (*specs.State, error)
// Returns the current config of the container.
@@ -97,48 +87,26 @@ type BaseContainer interface {
// Returns the PIDs inside this container. The PIDs are in the namespace of the calling process.
//
// errors:
// ContainerNotExists - Container no longer exists,
// Systemerror - System error.
//
// Some of the returned PIDs may no longer refer to processes in the Container, unless
// the Container state is PAUSED in which case every PID in the slice is valid.
Processes() ([]int, error)
// Returns statistics for the container.
//
// errors:
// ContainerNotExists - Container no longer exists,
// Systemerror - System error.
Stats() (*Stats, error)
// Set resources of container as configured
//
// We can use this to change resources when containers are running.
//
// errors:
// SystemError - System error.
Set(config configs.Config) error
// Start a process inside the container. Returns error if process fails to
// start. You can track process lifecycle with passed Process structure.
//
// errors:
// ContainerNotExists - Container no longer exists,
// ConfigInvalid - config is invalid,
// ContainerPaused - Container is paused,
// SystemError - System error.
Start(process *Process) (err error)
// Run immediately starts the process inside the container. Returns error if process
// fails to start. It does not block waiting for the exec fifo after start returns but
// opens the fifo after start returns.
//
// errors:
// ContainerNotExists - Container no longer exists,
// ConfigInvalid - config is invalid,
// ContainerPaused - Container is paused,
// SystemError - System error.
Run(process *Process) (err error)
// Destroys the container, if its in a valid state, after killing any
@@ -149,25 +117,14 @@ type BaseContainer interface {
//
// Running containers must first be stopped using Signal(..).
// Paused containers must first be resumed using Resume(..).
//
// errors:
// ContainerNotStopped - Container is still running,
// ContainerPaused - Container is paused,
// SystemError - System error.
Destroy() error
// Signal sends the provided signal code to the container's initial process.
//
// If all is specified the signal is sent to all processes in the container
// including the initial process.
//
// errors:
// SystemError - System error.
Signal(s os.Signal, all bool) error
// Exec signals the container to exec the users process at the end of the init.
//
// errors:
// SystemError - System error.
Exec() error
}

View File

@@ -97,48 +97,26 @@ type Container interface {
// Methods below here are platform specific
// Checkpoint checkpoints the running container's state to disk using the criu(8) utility.
//
// errors:
// Systemerror - System error.
Checkpoint(criuOpts *CriuOpts) error
// Restore restores the checkpointed container to a running state using the criu(8) utility.
//
// errors:
// Systemerror - System error.
Restore(process *Process, criuOpts *CriuOpts) error
// If the Container state is RUNNING or CREATED, sets the Container state to PAUSING and pauses
// the execution of any user processes. Asynchronously, when the container finished being paused the
// state is changed to PAUSED.
// If the Container state is PAUSED, do nothing.
//
// errors:
// ContainerNotExists - Container no longer exists,
// ContainerNotRunning - Container not running or created,
// Systemerror - System error.
Pause() error
// If the Container state is PAUSED, resumes the execution of any user processes in the
// Container before setting the Container state to RUNNING.
// If the Container state is RUNNING, do nothing.
//
// errors:
// ContainerNotExists - Container no longer exists,
// ContainerNotPaused - Container is not paused,
// Systemerror - System error.
Resume() error
// NotifyOOM returns a read-only channel signaling when the container receives an OOM notification.
//
// errors:
// Systemerror - System error.
NotifyOOM() (<-chan struct{}, error)
// NotifyMemoryPressure returns a read-only channel signaling when the container reaches a given pressure level
//
// errors:
// Systemerror - System error.
NotifyMemoryPressure(level PressureLevel) (<-chan struct{}, error)
}
@@ -183,7 +161,7 @@ func (c *linuxContainer) Processes() ([]int, error) {
pids, err = c.cgroupManager.GetAllPids()
if err != nil {
return nil, newSystemErrorWithCause(err, "getting all container pids from cgroups")
return nil, fmt.Errorf("unable to get all container pids: %w", err)
}
return pids, nil
}
@@ -194,11 +172,11 @@ func (c *linuxContainer) Stats() (*Stats, error) {
stats = &Stats{}
)
if stats.CgroupStats, err = c.cgroupManager.GetStats(); err != nil {
return stats, newSystemErrorWithCause(err, "getting container stats from cgroups")
return stats, fmt.Errorf("unable to get container cgroup stats: %w", err)
}
if c.intelRdtManager != nil {
if stats.IntelRdtStats, err = c.intelRdtManager.GetStats(); err != nil {
return stats, newSystemErrorWithCause(err, "getting container's Intel RDT stats")
return stats, fmt.Errorf("unable to get container Intel RDT stats: %w", err)
}
}
for _, iface := range c.config.Networks {
@@ -206,7 +184,7 @@ func (c *linuxContainer) Stats() (*Stats, error) {
case "veth":
istats, err := getNetworkInterfaceStats(iface.HostInterfaceName)
if err != nil {
return stats, newSystemErrorWithCausef(err, "getting network stats for interface %q", iface.HostInterfaceName)
return stats, fmt.Errorf("unable to get network stats for interface %q: %w", iface.HostInterfaceName, err)
}
stats.Interfaces = append(stats.Interfaces, istats)
}
@@ -222,7 +200,7 @@ func (c *linuxContainer) Set(config configs.Config) error {
return err
}
if status == Stopped {
return newGenericError(errors.New("container not running"), ContainerNotRunning)
return ErrNotRunning
}
if err := c.cgroupManager.Set(config.Cgroups.Resources); err != nil {
// Set configs back
@@ -253,7 +231,7 @@ func (c *linuxContainer) Start(process *Process) error {
c.m.Lock()
defer c.m.Unlock()
if c.config.Cgroups.Resources.SkipDevices {
return newGenericError(errors.New("can't start container with SkipDevices set"), ConfigInvalid)
return &ConfigError{"can't start container with SkipDevices set"}
}
if process.Init {
if err := c.createExecFifo(); err != nil {
@@ -335,7 +313,7 @@ func fifoOpen(path string, block bool) openResult {
}
f, err := os.OpenFile(path, flags, 0)
if err != nil {
return openResult{err: newSystemErrorWithCause(err, "open exec fifo for reading")}
return openResult{err: fmt.Errorf("exec fifo: %w", err)}
}
return openResult{file: f}
}
@@ -360,7 +338,7 @@ type openResult struct {
func (c *linuxContainer) start(process *Process) (retErr error) {
parent, err := c.newParentProcess(process)
if err != nil {
return newSystemErrorWithCause(err, "creating new parent process")
return fmt.Errorf("unable to create new parent process: %w", err)
}
logsDone := parent.forwardChildLogs()
@@ -370,13 +348,13 @@ func (c *linuxContainer) start(process *Process) (retErr error) {
// runc init closing the _LIBCONTAINER_LOGPIPE log fd.
err := <-logsDone
if err != nil && retErr == nil {
retErr = newSystemErrorWithCause(err, "forwarding init logs")
retErr = fmt.Errorf("unable to forward init logs: %w", err)
}
}()
}
if err := parent.start(); err != nil {
return newSystemErrorWithCause(err, "starting container process")
return fmt.Errorf("unable to start container process: %w", err)
}
if process.Init {
@@ -415,11 +393,11 @@ func (c *linuxContainer) Signal(s os.Signal, all bool) error {
// to avoid a PID reuse attack
if status == Running || status == Created || status == Paused {
if err := c.initProcess.signal(s); err != nil {
return newSystemErrorWithCause(err, "signaling init process")
return fmt.Errorf("unable to signal init: %w", err)
}
return nil
}
return newGenericError(errors.New("container not running"), ContainerNotRunning)
return ErrNotRunning
}
func (c *linuxContainer) createExecFifo() error {
@@ -471,7 +449,7 @@ func (c *linuxContainer) includeExecFifo(cmd *exec.Cmd) error {
func (c *linuxContainer) newParentProcess(p *Process) (parentProcess, error) {
parentInitPipe, childInitPipe, err := utils.NewSockPair("init")
if err != nil {
return nil, newSystemErrorWithCause(err, "creating new init pipe")
return nil, fmt.Errorf("unable to create init pipe: %w", err)
}
messageSockPair := filePair{parentInitPipe, childInitPipe}
@@ -492,7 +470,7 @@ func (c *linuxContainer) newParentProcess(p *Process) (parentProcess, error) {
// that problem), but we no longer do that. However, there's no need to do
// this for `runc exec` so we just keep it this way to be safe.
if err := c.includeExecFifo(cmd); err != nil {
return nil, newSystemErrorWithCause(err, "including execfifo in cmd.Exec setup")
return nil, fmt.Errorf("unable to setup exec fifo: %w", err)
}
return c.newInitProcess(p, cmd, messageSockPair, logFilePair)
}
@@ -569,7 +547,7 @@ func (c *linuxContainer) newSetnsProcess(p *Process, cmd *exec.Cmd, messageSockP
cmd.Env = append(cmd.Env, "_LIBCONTAINER_INITTYPE="+string(initSetns))
state, err := c.currentState()
if err != nil {
return nil, newSystemErrorWithCause(err, "getting container's current state")
return nil, fmt.Errorf("unable to get container state: %w", err)
}
// for setns process, we don't have to set cloneflags as the process namespaces
// will only be set via setns syscall
@@ -654,7 +632,7 @@ func (c *linuxContainer) Pause() error {
c: c,
})
}
return newGenericError(fmt.Errorf("container not running or created: %s", status), ContainerNotRunning)
return ErrNotRunning
}
func (c *linuxContainer) Resume() error {
@@ -665,7 +643,7 @@ func (c *linuxContainer) Resume() error {
return err
}
if status != Paused {
return newGenericError(errors.New("container not paused"), ContainerNotPaused)
return ErrNotPaused
}
if err := c.cgroupManager.Freeze(configs.Thawed); err != nil {
return err
@@ -1476,7 +1454,7 @@ func (c *linuxContainer) criuApplyCgroups(pid int, req *criurpc.CriuReq) error {
}
if err := c.cgroupManager.Set(c.config.Cgroups.Resources); err != nil {
return newSystemError(err)
return err
}
if cgroups.IsCgroup2UnifiedMode() {
@@ -1997,16 +1975,16 @@ func (c *linuxContainer) orderNamespacePaths(namespaces map[configs.NamespaceTyp
if p, ok := namespaces[ns]; ok && p != "" {
// check if the requested namespace is supported
if !configs.IsNamespaceSupported(ns) {
return nil, newSystemError(fmt.Errorf("namespace %s is not supported", ns))
return nil, fmt.Errorf("namespace %s is not supported", ns)
}
// only set to join this namespace if it exists
if _, err := os.Lstat(p); err != nil {
return nil, newSystemErrorWithCausef(err, "running lstat on namespace path %q", p)
return nil, fmt.Errorf("namespace path: %w", err)
}
// do not allow namespace path with comma as we use it to separate
// the namespace paths
if strings.ContainsRune(p, ',') {
return nil, newSystemError(fmt.Errorf("invalid path %s", p))
return nil, fmt.Errorf("invalid namespace path %s", p)
}
paths = append(paths, fmt.Sprintf("%s:%s", configs.NsName(ns), p))
}

View File

@@ -1,67 +1,21 @@
package libcontainer
import "io"
import "errors"
// ErrorCode is the API error code type.
type ErrorCode int
// API error codes.
const (
// Factory errors
IdInUse ErrorCode = iota
InvalidIdFormat
// Container errors
ContainerNotExists
ContainerPaused
ContainerNotStopped
ContainerNotRunning
ContainerNotPaused
// Process errors
NoProcessOps
// Common errors
ConfigInvalid
SystemError
var (
ErrExist = errors.New("container with given ID already exists")
ErrInvalidID = errors.New("invalid container ID format")
ErrNotExist = errors.New("container does not exist")
ErrPaused = errors.New("container paused")
ErrRunning = errors.New("container still running")
ErrNotRunning = errors.New("container not running")
ErrNotPaused = errors.New("container not paused")
)
func (c ErrorCode) String() string {
switch c {
case IdInUse:
return "Id already in use"
case InvalidIdFormat:
return "Invalid format"
case ContainerPaused:
return "Container paused"
case ConfigInvalid:
return "Invalid configuration"
case SystemError:
return "System error"
case ContainerNotExists:
return "Container does not exist"
case ContainerNotStopped:
return "Container is not stopped"
case ContainerNotRunning:
return "Container is not running"
case ContainerNotPaused:
return "Container is not paused"
case NoProcessOps:
return "No process operations"
default:
return "Unknown error"
}
type ConfigError struct {
details string
}
// Error is the API error type.
type Error interface {
error
// Returns an error if it failed to write the detail of the Error to w.
// The detail of the Error may include the error message and a
// representation of the stack trace.
Detail(w io.Writer) error
// Returns the error code for this error.
Code() ErrorCode
func (e *ConfigError) Error() string {
return "invalid configuration: " + e.details
}

View File

@@ -1,25 +0,0 @@
package libcontainer
import "testing"
func TestErrorCode(t *testing.T) {
codes := map[ErrorCode]string{
IdInUse: "Id already in use",
InvalidIdFormat: "Invalid format",
ContainerPaused: "Container paused",
ConfigInvalid: "Invalid configuration",
SystemError: "System error",
ContainerNotExists: "Container does not exist",
ContainerNotStopped: "Container is not stopped",
ContainerNotRunning: "Container is not running",
ConsoleExists: "Console exists for process",
ContainerNotPaused: "Container is not paused",
NoProcessOps: "No process operations",
}
for code, expected := range codes {
if actual := code.String(); actual != expected {
t.Fatalf("expected string %q but received %q", expected, actual)
}
}
}

View File

@@ -14,29 +14,15 @@ type Factory interface {
//
// Returns the new container with a running process.
//
// errors:
// IdInUse - id is already in use by a container
// InvalidIdFormat - id has incorrect format
// ConfigInvalid - config is invalid
// Systemerror - System error
//
// On error, any partially created container parts are cleaned up (the operation is atomic).
Create(id string, config *configs.Config) (Container, error)
// Load takes an ID for an existing container and returns the container information
// from the state. This presents a read only view of the container.
//
// errors:
// Path does not exist
// System error
Load(id string) (Container, error)
// StartInitialization is an internal API to libcontainer used during the reexec of the
// container.
//
// Errors:
// Pipe connection error
// System error
StartInitialization() error
// Type returns info string about factory type (e.g. lxc, libcontainer...)

View File

@@ -44,7 +44,9 @@ func InitArgs(args ...string) func(*LinuxFactory) error {
// Resolve relative paths to ensure that its available
// after directory changes.
if args[0], err = filepath.Abs(args[0]); err != nil {
return newGenericError(err, ConfigInvalid)
// The only error returned from filepath.Abs is
// the one from os.Getwd, i.e. a system error.
return err
}
}
@@ -189,7 +191,7 @@ func CriuPath(criupath string) func(*LinuxFactory) error {
func New(root string, options ...func(*LinuxFactory) error) (Factory, error) {
if root != "" {
if err := os.MkdirAll(root, 0o700); err != nil {
return nil, newGenericError(err, SystemError)
return nil, err
}
}
l := &LinuxFactory{
@@ -249,28 +251,28 @@ type LinuxFactory struct {
func (l *LinuxFactory) Create(id string, config *configs.Config) (Container, error) {
if l.Root == "" {
return nil, newGenericError(errors.New("invalid root"), ConfigInvalid)
return nil, &ConfigError{"invalid root"}
}
if err := l.validateID(id); err != nil {
return nil, err
}
if err := l.Validator.Validate(config); err != nil {
return nil, newGenericError(err, ConfigInvalid)
return nil, &ConfigError{err.Error()}
}
containerRoot, err := securejoin.SecureJoin(l.Root, id)
if err != nil {
return nil, err
}
if _, err := os.Stat(containerRoot); err == nil {
return nil, newGenericError(fmt.Errorf("container with id exists: %v", id), IdInUse)
return nil, ErrExist
} else if !os.IsNotExist(err) {
return nil, newGenericError(err, SystemError)
return nil, err
}
if err := os.MkdirAll(containerRoot, 0o711); err != nil {
return nil, newGenericError(err, SystemError)
return nil, err
}
if err := os.Chown(containerRoot, unix.Geteuid(), unix.Getegid()); err != nil {
return nil, newGenericError(err, SystemError)
return nil, err
}
c := &linuxContainer{
id: id,
@@ -292,7 +294,7 @@ func (l *LinuxFactory) Create(id string, config *configs.Config) (Container, err
func (l *LinuxFactory) Load(id string) (Container, error) {
if l.Root == "" {
return nil, newGenericError(errors.New("invalid root"), ConfigInvalid)
return nil, &ConfigError{"invalid root"}
}
// when load, we need to check id is valid or not.
if err := l.validateID(id); err != nil {
@@ -389,7 +391,7 @@ func (l *LinuxFactory) StartInitialization() (err error) {
fmt.Fprintln(os.Stderr, err)
return
}
if werr := utils.WriteJSON(pipe, newSystemError(err)); werr != nil {
if werr := utils.WriteJSON(pipe, &initError{Message: err.Error()}); werr != nil {
fmt.Fprintln(os.Stderr, err)
return
}
@@ -417,21 +419,21 @@ func (l *LinuxFactory) loadState(root, id string) (*State, error) {
f, err := os.Open(stateFilePath)
if err != nil {
if os.IsNotExist(err) {
return nil, newGenericError(fmt.Errorf("container %q does not exist", id), ContainerNotExists)
return nil, ErrNotExist
}
return nil, newGenericError(err, SystemError)
return nil, err
}
defer f.Close()
var state *State
if err := json.NewDecoder(f).Decode(&state); err != nil {
return nil, newGenericError(err, SystemError)
return nil, err
}
return state, nil
}
func (l *LinuxFactory) validateID(id string) error {
if !idRegex.MatchString(id) || string(os.PathSeparator)+id != utils.CleanPath(string(os.PathSeparator)+id) {
return newGenericError(fmt.Errorf("invalid id format: %v", id), InvalidIdFormat)
return ErrInvalidID
}
return nil

View File

@@ -143,12 +143,8 @@ func TestFactoryLoadNotExists(t *testing.T) {
if err == nil {
t.Fatal("expected nil error loading non-existing container")
}
var lerr Error
if !errors.As(err, &lerr) {
t.Fatal("expected libcontainer error type")
}
if lerr.Code() != ContainerNotExists {
t.Fatalf("expected error code %s but received %s", ContainerNotExists, lerr.Code())
if !errors.Is(err, ErrNotExist) {
t.Fatalf("expected ErrNotExist, got %v", err)
}
}

View File

@@ -1,94 +0,0 @@
package libcontainer
import (
"errors"
"fmt"
"io"
"text/template"
"time"
"github.com/opencontainers/runc/libcontainer/stacktrace"
)
var errorTemplate = template.Must(template.New("error").Parse(`Timestamp: {{.Timestamp}}
Code: {{.ECode}}
{{if .Message }}
Message: {{.Message}}
{{end}}
Frames:{{range $i, $frame := .Stack.Frames}}
---
{{$i}}: {{$frame.Function}}
Package: {{$frame.Package}}
File: {{$frame.File}}@{{$frame.Line}}{{end}}
`))
func newGenericError(err error, c ErrorCode) Error {
var le Error
if errors.As(err, &le) {
return le
}
gerr := &genericError{
Timestamp: time.Now(),
Err: err,
ECode: c,
Stack: stacktrace.Capture(1),
}
if err != nil {
gerr.Message = err.Error()
}
return gerr
}
func newSystemError(err error) Error {
return createSystemError(err, "")
}
func newSystemErrorWithCausef(err error, cause string, v ...interface{}) Error {
return createSystemError(err, fmt.Sprintf(cause, v...))
}
func newSystemErrorWithCause(err error, cause string) Error {
return createSystemError(err, cause)
}
// createSystemError creates the specified error with the correct number of
// stack frames skipped. This is only to be called by the other functions for
// formatting the error.
func createSystemError(err error, cause string) Error {
gerr := &genericError{
Timestamp: time.Now(),
Err: err,
ECode: SystemError,
Cause: cause,
Stack: stacktrace.Capture(2),
}
if err != nil {
gerr.Message = err.Error()
}
return gerr
}
type genericError struct {
Timestamp time.Time
ECode ErrorCode
Err error `json:"-"`
Cause string
Message string
Stack stacktrace.Stacktrace
}
func (e *genericError) Error() string {
if e.Cause == "" {
return e.Message
}
frame := e.Stack.Frames[0]
return fmt.Sprintf("%s:%d: %s caused: %s", frame.File, frame.Line, e.Cause, e.Message)
}
func (e *genericError) Code() ErrorCode {
return e.ECode
}
func (e *genericError) Detail(w io.Writer) error {
return errorTemplate.Execute(w, e)
}

View File

@@ -1,49 +0,0 @@
package libcontainer
import (
"errors"
"io/ioutil"
"testing"
)
func TestErrorDetail(t *testing.T) {
err := newGenericError(errors.New("test error"), SystemError)
if derr := err.Detail(ioutil.Discard); derr != nil {
t.Fatal(derr)
}
}
func TestErrorWithCode(t *testing.T) {
err := newGenericError(errors.New("test error"), SystemError)
if code := err.Code(); code != SystemError {
t.Fatalf("expected err code %q but %q", SystemError, code)
}
}
func TestErrorWithError(t *testing.T) {
cc := []struct {
errmsg string
cause string
}{
{
errmsg: "test error",
},
{
errmsg: "test error",
cause: "test",
},
}
for _, v := range cc {
err := newSystemErrorWithCause(errors.New(v.errmsg), v.cause)
msg := err.Error()
if v.cause == "" && msg != v.errmsg {
t.Fatalf("expected err(%q) equal errmsg(%q)", msg, v.errmsg)
}
if v.cause != "" && msg == v.errmsg {
t.Fatalf("unexpected err(%q) equal errmsg(%q)", msg, v.errmsg)
}
}
}

View File

@@ -86,7 +86,7 @@ type Process struct {
// Wait releases any resources associated with the Process
func (p Process) Wait() (*os.ProcessState, error) {
if p.ops == nil {
return nil, newGenericError(errInvalidProcess, NoProcessOps)
return nil, errInvalidProcess
}
return p.ops.wait()
}
@@ -96,7 +96,7 @@ func (p Process) Pid() (int, error) {
// math.MinInt32 is returned here, because it's invalid value
// for the kill() system call.
if p.ops == nil {
return math.MinInt32, newGenericError(errInvalidProcess, NoProcessOps)
return math.MinInt32, errInvalidProcess
}
return p.ops.pid(), nil
}
@@ -104,7 +104,7 @@ func (p Process) Pid() (int, error) {
// Signal sends a signal to the Process.
func (p Process) Signal(sig os.Signal) error {
if p.ops == nil {
return newGenericError(errInvalidProcess, NoProcessOps)
return errInvalidProcess
}
return p.ops.signal(sig)
}

View File

@@ -96,7 +96,7 @@ func (p *setnsProcess) start() (retErr error) {
p.messageSockPair.child.Close()
p.logFilePair.child.Close()
if err != nil {
return newSystemErrorWithCause(err, "starting setns process")
return fmt.Errorf("error starting setns process: %w", err)
}
waitInit := initWaiter(p.messageSockPair.parent)
@@ -104,7 +104,7 @@ func (p *setnsProcess) start() (retErr error) {
if retErr != nil {
if newOom, err := p.manager.OOMKillCount(); err == nil && newOom != oom {
// Someone in this cgroup was killed, this _might_ be us.
retErr = newSystemErrorWithCause(retErr, "possibly OOM-killed")
retErr = fmt.Errorf("%w (possibly OOM-killed)", retErr)
}
werr := <-waitInit
if werr != nil {
@@ -119,7 +119,7 @@ func (p *setnsProcess) start() (retErr error) {
if p.bootstrapData != nil {
if _, err := io.Copy(p.messageSockPair.parent, p.bootstrapData); err != nil {
return newSystemErrorWithCause(err, "copying bootstrap data to pipe")
return fmt.Errorf("error copying bootstrap data to pipe: %w", err)
}
}
err = <-waitInit
@@ -127,7 +127,7 @@ func (p *setnsProcess) start() (retErr error) {
return err
}
if err := p.execSetns(); err != nil {
return newSystemErrorWithCause(err, "executing setns process")
return fmt.Errorf("error executing setns process: %w", err)
}
if len(p.cgroupPaths) > 0 {
if err := cgroups.EnterPid(p.cgroupPaths, p.pid()); err != nil && !p.rootlessCgroups {
@@ -148,7 +148,7 @@ func (p *setnsProcess) start() (retErr error) {
}
}
if err != nil {
return newSystemErrorWithCausef(err, "adding pid %d to cgroups", p.pid())
return fmt.Errorf("error adding pid %d to cgroups: %w", p.pid(), err)
}
}
}
@@ -157,17 +157,17 @@ func (p *setnsProcess) start() (retErr error) {
_, err := os.Stat(p.intelRdtPath)
if err == nil {
if err := intelrdt.WriteIntelRdtTasks(p.intelRdtPath, p.pid()); err != nil {
return newSystemErrorWithCausef(err, "adding pid %d to Intel RDT resource control filesystem", p.pid())
return fmt.Errorf("error adding pid %d to Intel RDT: %w", p.pid(), err)
}
}
}
// set rlimits, this has to be done here because we lose permissions
// to raise the limits once we enter a user-namespace
if err := setupRlimits(p.config.Rlimits, p.pid()); err != nil {
return newSystemErrorWithCause(err, "setting rlimits for process")
return fmt.Errorf("error setting rlimits for process: %w", err)
}
if err := utils.WriteJSON(p.messageSockPair.parent, p.config); err != nil {
return newSystemErrorWithCause(err, "writing config to pipe")
return fmt.Errorf("error writing config to pipe: %w", err)
}
ierr := parseSync(p.messageSockPair.parent, func(sync *syncT) error {
@@ -179,12 +179,12 @@ func (p *setnsProcess) start() (retErr error) {
// This shouldn't happen.
panic("unexpected procHooks in setns")
default:
return newSystemError(errors.New("invalid JSON payload from child"))
return errors.New("invalid JSON payload from child")
}
})
if err := unix.Shutdown(int(p.messageSockPair.parent.Fd()), unix.SHUT_WR); err != nil {
return newSystemErrorWithCause(err, "calling shutdown on init pipe")
return &os.PathError{Op: "shutdown", Path: "(init pipe)", Err: err}
}
// Must be done after Shutdown so the child will exit and we can wait for it.
if ierr != nil {
@@ -202,16 +202,16 @@ func (p *setnsProcess) execSetns() error {
status, err := p.cmd.Process.Wait()
if err != nil {
_ = p.cmd.Wait()
return newSystemErrorWithCause(err, "waiting on setns process to finish")
return fmt.Errorf("error waiting on setns process to finish: %w", err)
}
if !status.Success() {
_ = p.cmd.Wait()
return newSystemError(&exec.ExitError{ProcessState: status})
return &exec.ExitError{ProcessState: status}
}
var pid *pid
if err := json.NewDecoder(p.messageSockPair.parent).Decode(&pid); err != nil {
_ = p.cmd.Wait()
return newSystemErrorWithCause(err, "reading pid from init pipe")
return fmt.Errorf("error reading pid from init pipe: %w", err)
}
// Clean up the zombie parent process
@@ -335,7 +335,7 @@ func (p *initProcess) start() (retErr error) {
_ = p.logFilePair.child.Close()
if err != nil {
p.process.ops = nil
return newSystemErrorWithCause(err, "starting init process command")
return fmt.Errorf("unable to start init: %w", err)
}
waitInit := initWaiter(p.messageSockPair.parent)
@@ -355,9 +355,9 @@ func (p *initProcess) start() (retErr error) {
if logrus.GetLevel() >= logrus.DebugLevel {
// Only show the original error if debug is set,
// as it is not generally very useful.
retErr = newSystemErrorWithCause(retErr, oomError)
retErr = fmt.Errorf(oomError+": %w", retErr)
} else {
retErr = newSystemError(errors.New(oomError))
retErr = errors.New(oomError)
}
}
@@ -382,15 +382,15 @@ func (p *initProcess) start() (retErr error) {
// cgroup. We don't need to worry about not doing this and not being root
// because we'd be using the rootless cgroup manager in that case.
if err := p.manager.Apply(p.pid()); err != nil {
return newSystemErrorWithCause(err, "applying cgroup configuration for process")
return fmt.Errorf("unable to apply cgroup configuration: %w", err)
}
if p.intelRdtManager != nil {
if err := p.intelRdtManager.Apply(p.pid()); err != nil {
return newSystemErrorWithCause(err, "applying Intel RDT configuration for process")
return fmt.Errorf("unable to apply Intel RDT configuration: %w", err)
}
}
if _, err := io.Copy(p.messageSockPair.parent, p.bootstrapData); err != nil {
return newSystemErrorWithCause(err, "copying bootstrap data to pipe")
return fmt.Errorf("can't copy bootstrap data to pipe: %w", err)
}
err = <-waitInit
if err != nil {
@@ -399,7 +399,7 @@ func (p *initProcess) start() (retErr error) {
childPid, err := p.getChildPid()
if err != nil {
return newSystemErrorWithCause(err, "getting the final child's pid from pipe")
return fmt.Errorf("can't get final child's PID from pipe: %w", err)
}
// Save the standard descriptor names before the container process
@@ -407,30 +407,30 @@ func (p *initProcess) start() (retErr error) {
// we won't know at checkpoint time which file descriptor to look up.
fds, err := getPipeFds(childPid)
if err != nil {
return newSystemErrorWithCausef(err, "getting pipe fds for pid %d", childPid)
return fmt.Errorf("error getting pipe fds for pid %d: %w", childPid, err)
}
p.setExternalDescriptors(fds)
// Now it's time to setup cgroup namesapce
if p.config.Config.Namespaces.Contains(configs.NEWCGROUP) && p.config.Config.Namespaces.PathOf(configs.NEWCGROUP) == "" {
if _, err := p.messageSockPair.parent.Write([]byte{createCgroupns}); err != nil {
return newSystemErrorWithCause(err, "sending synchronization value to init process")
return fmt.Errorf("error sending synchronization value to init process: %w", err)
}
}
// Wait for our first child to exit
if err := p.waitForChildExit(childPid); err != nil {
return newSystemErrorWithCause(err, "waiting for our first child to exit")
return fmt.Errorf("error waiting for our first child to exit: %w", err)
}
if err := p.createNetworkInterfaces(); err != nil {
return newSystemErrorWithCause(err, "creating network interfaces")
return fmt.Errorf("error creating network interfaces: %w", err)
}
if err := p.updateSpecState(); err != nil {
return newSystemErrorWithCause(err, "updating the spec state")
return fmt.Errorf("error updating spec state: %w", err)
}
if err := p.sendConfig(); err != nil {
return newSystemErrorWithCause(err, "sending config to init process")
return fmt.Errorf("error sending config to init process: %w", err)
}
var (
sentRun bool
@@ -443,17 +443,17 @@ func (p *initProcess) start() (retErr error) {
// set rlimits, this has to be done here because we lose permissions
// to raise the limits once we enter a user-namespace
if err := setupRlimits(p.config.Rlimits, p.pid()); err != nil {
return newSystemErrorWithCause(err, "setting rlimits for ready process")
return fmt.Errorf("error setting rlimits for ready process: %w", err)
}
// call prestart and CreateRuntime hooks
if !p.config.Config.Namespaces.Contains(configs.NEWNS) {
// Setup cgroup before the hook, so that the prestart and CreateRuntime hook could apply cgroup permissions.
if err := p.manager.Set(p.config.Config.Cgroups.Resources); err != nil {
return newSystemErrorWithCause(err, "setting cgroup config for ready process")
return fmt.Errorf("error setting cgroup config for ready process: %w", err)
}
if p.intelRdtManager != nil {
if err := p.intelRdtManager.Set(p.config.Config); err != nil {
return newSystemErrorWithCause(err, "setting Intel RDT config for ready process")
return fmt.Errorf("error setting Intel RDT config for ready process: %w", err)
}
}
@@ -493,23 +493,23 @@ func (p *initProcess) start() (retErr error) {
// procRun sync.
state, uerr := p.container.updateState(p)
if uerr != nil {
return newSystemErrorWithCause(err, "store init state")
return fmt.Errorf("unable to store init state: %w", err)
}
p.container.initProcessStartTime = state.InitProcessStartTime
// Sync with child.
if err := writeSync(p.messageSockPair.parent, procRun); err != nil {
return newSystemErrorWithCause(err, "writing syncT 'run'")
return fmt.Errorf("error writing syncT 'run': %w", err)
}
sentRun = true
case procHooks:
// Setup cgroup before prestart hook, so that the prestart hook could apply cgroup permissions.
if err := p.manager.Set(p.config.Config.Cgroups.Resources); err != nil {
return newSystemErrorWithCause(err, "setting cgroup config for procHooks process")
return fmt.Errorf("error setting cgroup config for procHooks process: %w", err)
}
if p.intelRdtManager != nil {
if err := p.intelRdtManager.Set(p.config.Config); err != nil {
return newSystemErrorWithCause(err, "setting Intel RDT config for procHooks process")
return fmt.Errorf("error setting Intel RDT config for procHooks process: %w", err)
}
}
if p.config.Config.Hooks != nil {
@@ -531,24 +531,24 @@ func (p *initProcess) start() (retErr error) {
}
// Sync with child.
if err := writeSync(p.messageSockPair.parent, procResume); err != nil {
return newSystemErrorWithCause(err, "writing syncT 'resume'")
return fmt.Errorf("error writing syncT 'resume': %w", err)
}
sentResume = true
default:
return newSystemError(errors.New("invalid JSON payload from child"))
return errors.New("invalid JSON payload from child")
}
return nil
})
if !sentRun {
return newSystemErrorWithCause(ierr, "container init")
return fmt.Errorf("error during container init: %w", ierr)
}
if p.config.Config.Namespaces.Contains(configs.NEWNS) && !sentResume {
return newSystemError(errors.New("could not synchronise after executing prestart and CreateRuntime hooks with container process"))
return errors.New("could not synchronise after executing prestart and CreateRuntime hooks with container process")
}
if err := unix.Shutdown(int(p.messageSockPair.parent.Fd()), unix.SHUT_WR); err != nil {
return newSystemErrorWithCause(err, "shutting down init pipe")
return &os.PathError{Op: "shutdown", Path: "(init pipe)", Err: err}
}
// Must be done after Shutdown so the child will exit and we can wait for it.
@@ -719,7 +719,7 @@ func initWaiter(r io.Reader) chan error {
return
}
}
ch <- newSystemErrorWithCause(err, "waiting for init preliminary setup")
ch <- fmt.Errorf("waiting for init preliminary setup: %w", err)
}()
return ch

View File

@@ -31,7 +31,7 @@ type restoredProcess struct {
}
func (p *restoredProcess) start() error {
return newGenericError(errors.New("restored process cannot be started"), SystemError)
return errors.New("restored process cannot be started")
}
func (p *restoredProcess) pid() int {
@@ -90,7 +90,7 @@ type nonChildProcess struct {
}
func (p *nonChildProcess) start() error {
return newGenericError(errors.New("restored process cannot be started"), SystemError)
return errors.New("restored process cannot be started")
}
func (p *nonChildProcess) pid() int {
@@ -98,11 +98,11 @@ func (p *nonChildProcess) pid() int {
}
func (p *nonChildProcess) terminate() error {
return newGenericError(errors.New("restored process cannot be terminated"), SystemError)
return errors.New("restored process cannot be terminated")
}
func (p *nonChildProcess) wait() (*os.ProcessState, error) {
return nil, newGenericError(errors.New("restored process cannot be waited on"), SystemError)
return nil, errors.New("restored process cannot be waited on")
}
func (p *nonChildProcess) startTime() (uint64, error) {

View File

@@ -57,7 +57,7 @@ func needsSetupDev(config *configs.Config) bool {
func prepareRootfs(pipe io.ReadWriter, iConfig *initConfig) (err error) {
config := iConfig.Config
if err := prepareRoot(config); err != nil {
return newSystemErrorWithCause(err, "preparing rootfs")
return fmt.Errorf("error preparing rootfs: %w", err)
}
mountConfig := &mountConfig{
@@ -71,29 +71,29 @@ func prepareRootfs(pipe io.ReadWriter, iConfig *initConfig) (err error) {
for _, m := range config.Mounts {
for _, precmd := range m.PremountCmds {
if err := mountCmd(precmd); err != nil {
return newSystemErrorWithCause(err, "running premount command")
return fmt.Errorf("error running premount command: %w", err)
}
}
if err := mountToRootfs(m, mountConfig); err != nil {
return newSystemErrorWithCausef(err, "mounting %q to rootfs at %q", m.Source, m.Destination)
return fmt.Errorf("error mounting %q to rootfs at %q: %w", m.Source, m.Destination, err)
}
for _, postcmd := range m.PostmountCmds {
if err := mountCmd(postcmd); err != nil {
return newSystemErrorWithCause(err, "running postmount command")
return fmt.Errorf("error running postmount command: %w", err)
}
}
}
if setupDev {
if err := createDevices(config); err != nil {
return newSystemErrorWithCause(err, "creating device nodes")
return fmt.Errorf("error creating device nodes: %w", err)
}
if err := setupPtmx(config); err != nil {
return newSystemErrorWithCause(err, "setting up ptmx")
return fmt.Errorf("error setting up ptmx: %w", err)
}
if err := setupDevSymlinks(config.Rootfs); err != nil {
return newSystemErrorWithCause(err, "setting up /dev symlinks")
return fmt.Errorf("error setting up /dev symlinks: %w", err)
}
}
@@ -115,7 +115,7 @@ func prepareRootfs(pipe io.ReadWriter, iConfig *initConfig) (err error) {
// operation not being perfectly split).
if err := unix.Chdir(config.Rootfs); err != nil {
return newSystemErrorWithCausef(err, "changing dir to %q", config.Rootfs)
return &os.PathError{Op: "chdir", Path: config.Rootfs, Err: err}
}
s := iConfig.SpecState
@@ -133,12 +133,12 @@ func prepareRootfs(pipe io.ReadWriter, iConfig *initConfig) (err error) {
err = chroot()
}
if err != nil {
return newSystemErrorWithCause(err, "jailing process inside rootfs")
return fmt.Errorf("error jailing process inside rootfs: %w", err)
}
if setupDev {
if err := reOpenDevNull(); err != nil {
return newSystemErrorWithCause(err, "reopening /dev/null inside container")
return fmt.Errorf("error reopening /dev/null inside container: %w", err)
}
}
@@ -161,7 +161,7 @@ func finalizeRootfs(config *configs.Config) (err error) {
if libcontainerUtils.CleanPath(m.Destination) == "/dev" {
if m.Flags&unix.MS_RDONLY == unix.MS_RDONLY {
if err := remountReadonly(m); err != nil {
return newSystemErrorWithCausef(err, "remounting %q as readonly", m.Destination)
return err
}
}
break
@@ -171,7 +171,7 @@ func finalizeRootfs(config *configs.Config) (err error) {
// set rootfs ( / ) as readonly
if config.Readonlyfs {
if err := setReadonly(); err != nil {
return newSystemErrorWithCause(err, "setting rootfs as readonly")
return fmt.Errorf("error setting rootfs as readonly: %w", err)
}
}
@@ -337,12 +337,12 @@ func doTmpfsCopyUp(m *configs.Mount, rootfs, mountLabel string) (Err error) {
// Set up a scratch dir for the tmpfs on the host.
tmpdir, err := prepareTmp("/tmp")
if err != nil {
return newSystemErrorWithCause(err, "tmpcopyup: failed to setup tmpdir")
return fmt.Errorf("tmpcopyup: failed to setup tmpdir: %w", err)
}
defer cleanupTmp(tmpdir)
tmpDir, err := ioutil.TempDir(tmpdir, "runctmpdir")
if err != nil {
return newSystemErrorWithCause(err, "tmpcopyup: failed to create tmpdir")
return fmt.Errorf("tmpcopyup: failed to create tmpdir: %w", err)
}
defer os.RemoveAll(tmpDir)

View File

@@ -7,6 +7,7 @@ import (
"fmt"
"os"
"runtime"
"strconv"
"github.com/opencontainers/selinux/go-selinux"
"github.com/sirupsen/logrus"
@@ -87,13 +88,13 @@ func (l *linuxSetnsInit) Init() error {
// enable in their seccomp profiles).
if l.config.Config.Seccomp != nil && l.config.NoNewPrivileges {
if err := seccomp.InitSeccomp(l.config.Config.Seccomp); err != nil {
return newSystemErrorWithCause(err, "init seccomp")
return fmt.Errorf("unable to init seccomp: %w", err)
}
}
logrus.Debugf("setns_init: about to exec")
// Close the log pipe fd so the parent's ForwardLogs can exit.
if err := unix.Close(l.logFd); err != nil {
return newSystemErrorWithCause(err, "closing log pipe fd")
return &os.PathError{Op: "close log pipe", Path: "fd " + strconv.Itoa(l.logFd), Err: err}
}
return system.Execv(l.config.Args[0], l.config.Args[0:], os.Environ())

View File

@@ -189,19 +189,20 @@ func (l *linuxStandardInit) Init() error {
// Close the log pipe fd so the parent's ForwardLogs can exit.
if err := unix.Close(l.logFd); err != nil {
return newSystemErrorWithCause(err, "closing log pipe fd")
return &os.PathError{Op: "close log pipe", Path: "fd " + strconv.Itoa(l.logFd), Err: err}
}
// Wait for the FIFO to be opened on the other side before exec-ing the
// user process. We open it through /proc/self/fd/$fd, because the fd that
// was given to us was an O_PATH fd to the fifo itself. Linux allows us to
// re-open an O_PATH fd through /proc.
fd, err := unix.Open("/proc/self/fd/"+strconv.Itoa(l.fifoFd), unix.O_WRONLY|unix.O_CLOEXEC, 0)
fifoPath := "/proc/self/fd/" + strconv.Itoa(l.fifoFd)
fd, err := unix.Open(fifoPath, unix.O_WRONLY|unix.O_CLOEXEC, 0)
if err != nil {
return newSystemErrorWithCause(err, "open exec fifo")
return &os.PathError{Op: "open exec fifo", Path: fifoPath, Err: err}
}
if _, err := unix.Write(fd, []byte("0")); err != nil {
return newSystemErrorWithCause(err, "write 0 exec fifo")
return &os.PathError{Op: "write exec fifo", Path: fifoPath, Err: err}
}
// Close the O_PATH fifofd fd before exec because the kernel resets
// dumpable in the wrong order. This has been fixed in newer kernels, but
@@ -215,7 +216,7 @@ func (l *linuxStandardInit) Init() error {
// enable in their seccomp profiles).
if l.config.Config.Seccomp != nil && l.config.NoNewPrivileges {
if err := seccomp.InitSeccomp(l.config.Config.Seccomp); err != nil {
return newSystemErrorWithCause(err, "init seccomp")
return fmt.Errorf("unable to init seccomp: %w", err)
}
}
@@ -227,7 +228,7 @@ func (l *linuxStandardInit) Init() error {
}
if err := unix.Exec(name, l.config.Args[0:], os.Environ()); err != nil {
return newSystemErrorWithCause(err, "exec user process")
return fmt.Errorf("can't exec user process: %w", err)
}
return nil
}

View File

@@ -3,7 +3,6 @@
package libcontainer
import (
"errors"
"fmt"
"os"
"path/filepath"
@@ -118,7 +117,7 @@ func (r *runningState) transition(s containerState) error {
switch s.(type) {
case *stoppedState:
if r.c.runType() == Running {
return newGenericError(errors.New("container still running"), ContainerNotStopped)
return ErrRunning
}
r.c.state = s
return nil
@@ -133,7 +132,7 @@ func (r *runningState) transition(s containerState) error {
func (r *runningState) destroy() error {
if r.c.runType() == Running {
return newGenericError(errors.New("container is not destroyed"), ContainerNotStopped)
return ErrRunning
}
return destroy(r.c)
}
@@ -191,7 +190,7 @@ func (p *pausedState) destroy() error {
}
return destroy(p.c)
}
return newGenericError(errors.New("container is paused"), ContainerPaused)
return ErrPaused
}
// restoredState is the same as the running state but also has associated checkpoint

View File

@@ -13,7 +13,7 @@ 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 a &genericError).
// response which is followed by an &initError).
//
// [ child ] <-> [ parent ]
//
@@ -34,6 +34,16 @@ type syncT struct {
Type syncType `json:"type"`
}
// 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
}
// writeSync is used to write to a synchronisation pipe. An error is returned
// if there was a problem writing the payload.
func writeSync(pipe io.Writer, sync syncType) error {
@@ -41,7 +51,7 @@ func writeSync(pipe io.Writer, sync syncType) error {
}
// readSync is used to read from a synchronisation pipe. An error is returned
// if we got a genericError, the pipe was closed, or we got an unexpected flag.
// if we got an initError, the pipe was closed, or we got an unexpected flag.
func readSync(pipe io.Reader, expected syncType) error {
var procSync syncT
if err := json.NewDecoder(pipe).Decode(&procSync); err != nil {
@@ -52,7 +62,7 @@ func readSync(pipe io.Reader, expected syncType) error {
}
if procSync.Type == procError {
var ierr genericError
var ierr initError
if err := json.NewDecoder(pipe).Decode(&ierr); err != nil {
return fmt.Errorf("failed reading error from parent: %w", err)
@@ -81,10 +91,10 @@ func parseSync(pipe io.Reader, fn func(*syncT) error) error {
}
// We handle this case outside fn for cleanliness reasons.
var ierr *genericError
var ierr *initError
if sync.Type == procError {
if err := dec.Decode(&ierr); err != nil && !errors.Is(err, io.EOF) {
return newSystemErrorWithCause(err, "decoding proc error from init")
return fmt.Errorf("error decoding proc error from init: %w", err)
}
if ierr != nil {
return ierr

View File

@@ -22,7 +22,7 @@ function setup() {
runc run -d --console-socket "$CONSOLE_SOCKET" test_cgroups_permissions
[ "$status" -eq 1 ]
[[ "$output" == *"applying cgroup configuration"*"permission denied"* ]]
[[ "$output" == *"unable to apply cgroup configuration"*"permission denied"* ]]
}
@test "runc create (rootless + limits + no cgrouppath + no permission) fails with informative error" {