diff --git a/delete.go b/delete.go index 13c63b4e2..fc0c73f93 100644 --- a/delete.go +++ b/delete.go @@ -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) diff --git a/libcontainer/container.go b/libcontainer/container.go index ba7541c5f..300c9526c 100644 --- a/libcontainer/container.go +++ b/libcontainer/container.go @@ -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 } diff --git a/libcontainer/container_linux.go b/libcontainer/container_linux.go index e41e15d0a..8400a690e 100644 --- a/libcontainer/container_linux.go +++ b/libcontainer/container_linux.go @@ -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)) } diff --git a/libcontainer/error.go b/libcontainer/error.go index fb801ef15..bd5ad1f8b 100644 --- a/libcontainer/error.go +++ b/libcontainer/error.go @@ -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 } diff --git a/libcontainer/error_test.go b/libcontainer/error_test.go deleted file mode 100644 index 36841ad83..000000000 --- a/libcontainer/error_test.go +++ /dev/null @@ -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) - } - } -} diff --git a/libcontainer/factory.go b/libcontainer/factory.go index 0986cd77e..9f9e8fc58 100644 --- a/libcontainer/factory.go +++ b/libcontainer/factory.go @@ -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...) diff --git a/libcontainer/factory_linux.go b/libcontainer/factory_linux.go index efc40efc0..63cf57b09 100644 --- a/libcontainer/factory_linux.go +++ b/libcontainer/factory_linux.go @@ -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 diff --git a/libcontainer/factory_linux_test.go b/libcontainer/factory_linux_test.go index 2f0dac05d..928e4f21b 100644 --- a/libcontainer/factory_linux_test.go +++ b/libcontainer/factory_linux_test.go @@ -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) } } diff --git a/libcontainer/generic_error.go b/libcontainer/generic_error.go deleted file mode 100644 index 7457ea980..000000000 --- a/libcontainer/generic_error.go +++ /dev/null @@ -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) -} diff --git a/libcontainer/generic_error_test.go b/libcontainer/generic_error_test.go deleted file mode 100644 index 771ab4605..000000000 --- a/libcontainer/generic_error_test.go +++ /dev/null @@ -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) - } - - } -} diff --git a/libcontainer/process.go b/libcontainer/process.go index cff1eff4b..7cd180389 100644 --- a/libcontainer/process.go +++ b/libcontainer/process.go @@ -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) } diff --git a/libcontainer/process_linux.go b/libcontainer/process_linux.go index 80f25e334..490a7c8d7 100644 --- a/libcontainer/process_linux.go +++ b/libcontainer/process_linux.go @@ -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 diff --git a/libcontainer/restored_process.go b/libcontainer/restored_process.go index 3cc247541..6987c8fd5 100644 --- a/libcontainer/restored_process.go +++ b/libcontainer/restored_process.go @@ -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) { diff --git a/libcontainer/rootfs_linux.go b/libcontainer/rootfs_linux.go index 0882cf3e0..fc9652f30 100644 --- a/libcontainer/rootfs_linux.go +++ b/libcontainer/rootfs_linux.go @@ -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) diff --git a/libcontainer/setns_init_linux.go b/libcontainer/setns_init_linux.go index ca623e4bb..89b5609a6 100644 --- a/libcontainer/setns_init_linux.go +++ b/libcontainer/setns_init_linux.go @@ -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()) diff --git a/libcontainer/standard_init_linux.go b/libcontainer/standard_init_linux.go index ce9117fa2..89a7d0096 100644 --- a/libcontainer/standard_init_linux.go +++ b/libcontainer/standard_init_linux.go @@ -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 } diff --git a/libcontainer/state_linux.go b/libcontainer/state_linux.go index 4fa0eab1e..3b71a0b03 100644 --- a/libcontainer/state_linux.go +++ b/libcontainer/state_linux.go @@ -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 diff --git a/libcontainer/sync.go b/libcontainer/sync.go index cc4748191..906a8c2ca 100644 --- a/libcontainer/sync.go +++ b/libcontainer/sync.go @@ -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 diff --git a/tests/integration/cgroups.bats b/tests/integration/cgroups.bats index 1ea3e07a0..ee95a01f6 100644 --- a/tests/integration/cgroups.bats +++ b/tests/integration/cgroups.bats @@ -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" {