Repository: opencontainers/runc (go)

Issue:
Process leak if cgroup init process shares process namespace
The cgroup init process wait function uses Cmd.Wait from os/exec. That Wait function can return a non-nil error if the init process does not complete successfully say if it exits with a non-zero exit code or is terminated.

If the cgroup's init process shares process namespace, all the other processes, if any, in the cgroup will not be killed, resulting in a process leak.

From libcontainer/process_linux.go:
```go
func (p *initProcess) wait() (*os.ProcessState, error) {
	err := p.cmd.Wait()
	if err != nil {
		return p.cmd.ProcessState, err
	}
	// we should kill all processes in cgroup when init is died if we use host PID namespace
	if p.sharePidns {
		signalAllProcesses(p.manager, unix.SIGKILL)
	}
	return p.cmd.ProcessState, nil
}
```

Pull request:
Kill processes in cgroup even if process Wait fails
If the cgroup's init process doesn't complete successfully, Wait returns a
non-nil error. We should still kill all the processes in the cgroup in that
case, if process namespace is shared. Otherwise, it may result in process leak.

Fixes #2632

Signed-off-by: Chaitanya Bandi <kbandi@cs.stonybrook.edu>

Patch:
diff --git a/docs/cgroup-v2.md b/docs/cgroup-v2.md
index e74e67fe..af4ba98b 100644
--- a/docs/cgroup-v2.md
+++ b/docs/cgroup-v2.md
@@ -24,13 +24,6 @@ On cgroup v2 hosts, it is highly recommended to run runc with the systemd cgroup
 
 The recommended systemd version is 244 or later. Older systemd does not support delegation of `cpuset` controller.
 
-Make sure you also have the `dbus-user-session` (Debian/Ubuntu) or `dbus-daemon` (CentOS/Fedora) package installed, and that `dbus` is running. On Debian-flavored distros, this can be accomplished like so:
-
-```console
-$ sudo apt install -y dbus-user-session
-$ systemctl --user start dbus
-```
-
 ## Rootless
 On cgroup v2 hosts, rootless runc can talk to systemd to get cgroup permissions to be delegated.
 
diff --git a/libcontainer/cgroups/fs/cpuacct.go b/libcontainer/cgroups/fs/cpuacct.go
index 9ee1119c..7c74b4e9 100644
--- a/libcontainer/cgroups/fs/cpuacct.go
+++ b/libcontainer/cgroups/fs/cpuacct.go
@@ -5,6 +5,7 @@ package fs
 import (
 	"bufio"
 	"fmt"
+	"io/ioutil"
 	"os"
 	"path/filepath"
 	"strconv"
@@ -91,11 +92,11 @@ func getCpuUsageBreakdown(path string) (uint64, uint64, error) {
 	// Expected format:
 	// user <usage in ticks>
 	// system <usage in ticks>
-	data, err := fscommon.ReadFile(path, cgroupCpuacctStat)
+	data, err := ioutil.ReadFile(filepath.Join(path, cgroupCpuacctStat))
 	if err != nil {
 		return 0, 0, err
 	}
-	fields := strings.Fields(data)
+	fields := strings.Fields(string(data))
 	if len(fields) < 4 {
 		return 0, 0, fmt.Errorf("failure - %s is expected to have at least 4 fields", filepath.Join(path, cgroupCpuacctStat))
 	}
@@ -117,11 +118,11 @@ func getCpuUsageBreakdown(path string) (uint64, uint64, error) {
 
 func getPercpuUsage(path string) ([]uint64, error) {
 	percpuUsage := []uint64{}
-	data, err := fscommon.ReadFile(path, "cpuacct.usage_percpu")
+	data, err := ioutil.ReadFile(filepath.Join(path, "cpuacct.usage_percpu"))
 	if err != nil {
 		return percpuUsage, err
 	}
-	for _, value := range strings.Fields(data) {
+	for _, value := range strings.Fields(string(data)) {
 		value, err := strconv.ParseUint(value, 10, 64)
 		if err != nil {
 			return percpuUsage, fmt.Errorf("Unable to convert param value to uint64: %s", err)
diff --git a/libcontainer/cgroups/fs/cpuset.go b/libcontainer/cgroups/fs/cpuset.go
index e7b5cf67..ba17519b 100644
--- a/libcontainer/cgroups/fs/cpuset.go
+++ b/libcontainer/cgroups/fs/cpuset.go
@@ -3,6 +3,8 @@
 package fs
 
 import (
+	"bytes"
+	"io/ioutil"
 	"os"
 	"path/filepath"
 
@@ -79,7 +81,7 @@ func (s *CpusetGroup) ApplyDir(dir string, cgroup *configs.Cgroup, pid int) erro
 	// 'ensureParent' start with parent because we don't want to
 	// explicitly inherit from parent, it could conflict with
 	// 'cpuset.cpu_exclusive'.
-	if err := cpusetEnsureParent(filepath.Dir(dir), root); err != nil {
+	if err := s.ensureParent(filepath.Dir(dir), root); err != nil {
 		return err
 	}
 	if err := os.MkdirAll(dir, 0755); err != nil {
@@ -101,20 +103,20 @@ func (s *CpusetGroup) ApplyDir(dir string, cgroup *configs.Cgroup, pid int) erro
 	return cgroups.WriteCgroupProc(dir, pid)
 }
 
-func getCpusetSubsystemSettings(parent string) (cpus, mems string, err error) {
-	if cpus, err = fscommon.ReadFile(parent, "cpuset.cpus"); err != nil {
+func (s *CpusetGroup) getSubsystemSettings(parent string) (cpus []byte, mems []byte, err error) {
+	if cpus, err = ioutil.ReadFile(filepath.Join(parent, "cpuset.cpus")); err != nil {
 		return
 	}
-	if mems, err = fscommon.ReadFile(parent, "cpuset.mems"); err != nil {
+	if mems, err = ioutil.ReadFile(filepath.Join(parent, "cpuset.mems")); err != nil {
 		return
 	}
 	return cpus, mems, nil
 }
 
-// cpusetEnsureParent makes sure that the parent directory of current is created
+// ensureParent makes sure that the parent directory of current is created
 // and populated with the proper cpus and mems files copied from
-// its parent.
-func cpusetEnsureParent(current, root string) error {
+// it's parent.
+func (s *CpusetGroup) ensureParent(current, root string) error {
 	parent := filepath.Dir(current)
 	if libcontainerUtils.CleanPath(parent) == root {
 		return nil
@@ -123,33 +125,37 @@ func cpusetEnsureParent(current, root string) error {
 	if parent == current {
 		return errors.New("cpuset: cgroup parent path outside cgroup root")
 	}
-	if err := cpusetEnsureParent(parent, root); err != nil {
+	if err := s.ensureParent(parent, root); err != nil {
 		return err
 	}
 	if err := os.MkdirAll(current, 0755); err != nil {
 		return err
 	}
-	return cpusetCopyIfNeeded(current, parent)
+	return s.copyIfNeeded(current, parent)
 }
 
-// cpusetCopyIfNeeded copies the cpuset.cpus and cpuset.mems from the parent
+// copyIfNeeded copies the cpuset.cpus and cpuset.mems from the parent
 // directory to the current directory if the file's contents are 0
-func cpusetCopyIfNeeded(current, parent string) error {
-	currentCpus, currentMems, err := getCpusetSubsystemSettings(current)
-	if err != nil {
+func (s *CpusetGroup) copyIfNeeded(current, parent string) error {
+	var (
+		err                      error
+		currentCpus, currentMems []byte
+		parentCpus, parentMems   []byte
+	)
+
+	if currentCpus, currentMems, err = s.getSubsystemSettings(current); err != nil {
 		return err
 	}
-	parentCpus, parentMems, err := getCpusetSubsystemSettings(parent)
-	if err != nil {
+	if parentCpus, parentMems, err = s.getSubsystemSettings(parent); err != nil {
 		return err
 	}
 
-	if isEmptyCpuset(currentCpus) {
+	if s.isEmpty(currentCpus) {
 		if err := fscommon.WriteFile(current, "cpuset.cpus", string(parentCpus)); err != nil {
 			return err
 		}
 	}
-	if isEmptyCpuset(currentMems) {
+	if s.isEmpty(currentMems) {
 		if err := fscommon.WriteFile(current, "cpuset.mems", string(parentMems)); err != nil {
 			return err
 		}
@@ -157,13 +163,13 @@ func cpusetCopyIfNeeded(current, parent string) error {
 	return nil
 }
 
-func isEmptyCpuset(str string) bool {
-	return str == "" || str == "\n"
+func (s *CpusetGroup) isEmpty(b []byte) bool {
+	return len(bytes.Trim(b, "\n")) == 0
 }
 
 func (s *CpusetGroup) ensureCpusAndMems(path string, cgroup *configs.Cgroup) error {
 	if err := s.Set(path, cgroup); err != nil {
 		return err
 	}
-	return cpusetCopyIfNeeded(path, filepath.Dir(path))
+	return s.copyIfNeeded(path, filepath.Dir(path))
 }
diff --git a/libcontainer/cgroups/fs/kmem.go b/libcontainer/cgroups/fs/kmem.go
index 86858f90..8d97a679 100644
--- a/libcontainer/cgroups/fs/kmem.go
+++ b/libcontainer/cgroups/fs/kmem.go
@@ -5,11 +5,11 @@ package fs
 import (
 	"errors"
 	"fmt"
+	"io/ioutil"
 	"path/filepath"
 	"strconv"
 
 	"github.com/opencontainers/runc/libcontainer/cgroups"
-	"github.com/opencontainers/runc/libcontainer/cgroups/fscommon"
 	"golang.org/x/sys/unix"
 )
 
@@ -42,7 +42,7 @@ func setKernelMemory(path string, kernelMemoryLimit int64) error {
 		// doesn't support it we *must* error out.
 		return errors.New("kernel memory accounting not supported by this kernel")
 	}
-	if err := fscommon.WriteFile(path, cgroupKernelMemoryLimit, strconv.FormatInt(kernelMemoryLimit, 10)); err != nil {
+	if err := ioutil.WriteFile(filepath.Join(path, cgroupKernelMemoryLimit), []byte(strconv.FormatInt(kernelMemoryLimit, 10)), 0700); err != nil {
 		// Check if the error number returned by the syscall is "EBUSY"
 		// The EBUSY signal is returned on attempts to write to the
 		// memory.kmem.limit_in_bytes file if the cgroup has children or
@@ -50,7 +50,7 @@ func setKernelMemory(path string, kernelMemoryLimit int64) error {
 		if errors.Is(err, unix.EBUSY) {
 			return fmt.Errorf("failed to set %s, because either tasks have already joined this cgroup or it has children", cgroupKernelMemoryLimit)
 		}
-		return err
+		return fmt.Errorf("failed to write %v to %v: %v", kernelMemoryLimit, cgroupKernelMemoryLimit, err)
 	}
 	return nil
 }
diff --git a/libcontainer/cgroups/fs2/create.go b/libcontainer/cgroups/fs2/create.go
index 30cf51ee..568cee6c 100644
--- a/libcontainer/cgroups/fs2/create.go
+++ b/libcontainer/cgroups/fs2/create.go
@@ -1,17 +1,19 @@
 package fs2
 
 import (
+	"bytes"
 	"fmt"
+	"io/ioutil"
 	"os"
 	"path/filepath"
 	"strings"
 
-	"github.com/opencontainers/runc/libcontainer/cgroups/fscommon"
 	"github.com/opencontainers/runc/libcontainer/configs"
 )
 
-func supportedControllers(cgroup *configs.Cgroup) (string, error) {
-	return fscommon.ReadFile(UnifiedMountpoint, "/cgroup.controllers")
+func supportedControllers(cgroup *configs.Cgroup) ([]byte, error) {
+	const file = UnifiedMountpoint + "/cgroup.controllers"
+	return ioutil.ReadFile(file)
 }
 
 // needAnyControllers returns whether we enable some supported controllers or not,
@@ -29,7 +31,7 @@ func needAnyControllers(cgroup *configs.Cgroup) (bool, error) {
 		return false, err
 	}
 	avail := make(map[string]struct{})
-	for _, ctr := range strings.Fields(content) {
+	for _, ctr := range strings.Fields(string(content)) {
 		avail[ctr] = struct{}{}
 	}
 
@@ -79,12 +81,8 @@ func CreateCgroupPath(path string, c *configs.Cgroup) (Err error) {
 		return err
 	}
 
-	const (
-		cgTypeFile  = "cgroup.type"
-		cgStCtlFile = "cgroup.subtree_control"
-	)
-	ctrs := strings.Fields(content)
-	res := "+" + strings.Join(ctrs, " +")
+	ctrs := bytes.Fields(content)
+	res := append([]byte("+"), bytes.Join(ctrs, []byte(" +"))...)
 
 	elements := strings.Split(path, "/")
 	elements = elements[3:]
@@ -105,9 +103,9 @@ func CreateCgroupPath(path string, c *configs.Cgroup) (Err error) {
 					}
 				}()
 			}
-			cgType, _ := fscommon.ReadFile(current, cgTypeFile)
-			cgType = strings.TrimSpace(cgType)
-			switch cgType {
+			cgTypeFile := filepath.Join(current, "cgroup.type")
+			cgType, _ := ioutil.ReadFile(cgTypeFile)
+			switch string(bytes.TrimSpace(cgType)) {
 			// If the cgroup is in an invalid mode (usually this means there's an internal
 			// process in the cgroup tree, because we created a cgroup under an
 			// already-populated-by-other-processes cgroup), then we have to error out if
@@ -122,7 +120,7 @@ func CreateCgroupPath(path string, c *configs.Cgroup) (Err error) {
 					// since that means we're a properly delegated cgroup subtree) but in
 					// this case there's not much we can do and it's better than giving an
 					// error.
-					_ = fscommon.WriteFile(current, cgTypeFile, "threaded")
+					_ = ioutil.WriteFile(cgTypeFile, []byte("threaded"), 0644)
 				}
 			// If the cgroup is in (threaded) or (domain threaded) mode, we can only use thread-aware controllers
 			// (and you cannot usually take a cgroup out of threaded mode).
@@ -130,17 +128,18 @@ func CreateCgroupPath(path string, c *configs.Cgroup) (Err error) {
 				fallthrough
 			case "threaded":
 				if containsDomainController(c) {
-					return fmt.Errorf("cannot enter cgroupv2 %q with domain controllers -- it is in %s mode", current, cgType)
+					return fmt.Errorf("cannot enter cgroupv2 %q with domain controllers -- it is in %s mode", current, string(bytes.TrimSpace(cgType)))
 				}
 			}
 		}
 		// enable all supported controllers
 		if i < len(elements)-1 {
-			if err := fscommon.WriteFile(current, cgStCtlFile, res); err != nil {
+			file := filepath.Join(current, "cgroup.subtree_control")
+			if err := ioutil.WriteFile(file, res, 0644); err != nil {
 				// try write one by one
-				allCtrs := strings.Split(res, " ")
+				allCtrs := bytes.Split(res, []byte(" "))
 				for _, ctr := range allCtrs {
-					_ = fscommon.WriteFile(current, cgStCtlFile, ctr)
+					_ = ioutil.WriteFile(file, ctr, 0644)
 				}
 			}
 			// Some controllers might not be enabled when rootless or containerized,
diff --git a/libcontainer/cgroups/fs2/fs2.go b/libcontainer/cgroups/fs2/fs2.go
index 3f0b9e0d..268a630e 100644
--- a/libcontainer/cgroups/fs2/fs2.go
+++ b/libcontainer/cgroups/fs2/fs2.go
@@ -4,7 +4,9 @@ package fs2
 
 import (
 	"fmt"
+	"io/ioutil"
 	"os"
+	"path/filepath"
 	"strings"
 
 	"github.com/opencontainers/runc/libcontainer/cgroups"
@@ -51,14 +53,15 @@ func (m *manager) getControllers() error {
 		return nil
 	}
 
-	data, err := fscommon.ReadFile(m.dirPath, "cgroup.controllers")
+	file := filepath.Join(m.dirPath, "cgroup.controllers")
+	data, err := ioutil.ReadFile(file)
 	if err != nil {
 		if m.rootless && m.config.Path == "" {
 			return nil
 		}
 		return err
 	}
-	fields := strings.Fields(data)
+	fields := strings.Fields(string(data))
 	m.controllers = make(map[string]struct{}, len(fields))
 	for _, c := range fields {
 		m.controllers[c] = struct{}{}
diff --git a/libcontainer/cgroups/fs2/hugetlb.go b/libcontainer/cgroups/fs2/hugetlb.go
index 18cd411c..60ec78fd 100644
--- a/libcontainer/cgroups/fs2/hugetlb.go
+++ b/libcontainer/cgroups/fs2/hugetlb.go
@@ -3,6 +3,8 @@
 package fs2
 
 import (
+	"io/ioutil"
+	"path/filepath"
 	"strconv"
 
 	"github.com/pkg/errors"
@@ -44,11 +46,12 @@ func statHugeTlb(dirPath string, stats *cgroups.Stats) error {
 		hugetlbStats.Usage = value
 
 		fileName := "hugetlb." + pagesize + ".events"
-		contents, err := fscommon.ReadFile(dirPath, fileName)
+		filePath := filepath.Join(dirPath, fileName)
+		contents, err := ioutil.ReadFile(filePath)
 		if err != nil {
 			return errors.Wrap(err, "failed to read stats")
 		}
-		_, value, err = fscommon.GetCgroupParamKeyValue(contents)
+		_, value, err = fscommon.GetCgroupParamKeyValue(string(contents))
 		if err != nil {
 			return errors.Wrap(err, "failed to parse "+fileName)
 		}
diff --git a/libcontainer/cgroups/fs2/pids.go b/libcontainer/cgroups/fs2/pids.go
index 16e1c219..b8153d28 100644
--- a/libcontainer/cgroups/fs2/pids.go
+++ b/libcontainer/cgroups/fs2/pids.go
@@ -3,6 +3,7 @@
 package fs2
 
 import (
+	"io/ioutil"
 	"path/filepath"
 	"strings"
 
@@ -33,15 +34,15 @@ func setPids(dirPath string, cgroup *configs.Cgroup) error {
 func statPidsWithoutController(dirPath string, stats *cgroups.Stats) error {
 	// if the controller is not enabled, let's read PIDS from cgroups.procs
 	// (or threads if cgroup.threads is enabled)
-	contents, err := fscommon.ReadFile(dirPath, "cgroup.procs")
+	contents, err := ioutil.ReadFile(filepath.Join(dirPath, "cgroup.procs"))
 	if errors.Is(err, unix.ENOTSUP) {
-		contents, err = fscommon.ReadFile(dirPath, "cgroup.threads")
+		contents, err = ioutil.ReadFile(filepath.Join(dirPath, "cgroup.threads"))
 	}
 	if err != nil {
 		return err
 	}
 	pids := make(map[string]string)
-	for _, i := range strings.Split(contents, "\n") {
+	for _, i := range strings.Split(string(contents), "\n") {
 		if i != "" {
 			pids[i] = i
 		}
diff --git a/libcontainer/cgroups/systemd/user.go b/libcontainer/cgroups/systemd/user.go
index 9698e140..cb070cec 100644
--- a/libcontainer/cgroups/systemd/user.go
+++ b/libcontainer/cgroups/systemd/user.go
@@ -102,5 +102,5 @@ func DetectUserDbusSessionBusAddress() (string, error) {
 			return strings.TrimPrefix(s, "DBUS_SESSION_BUS_ADDRESS="), nil
 		}
 	}
-	return "", errors.New("could not detect DBUS_SESSION_BUS_ADDRESS from `systemctl --user --no-pager show-environment`. Make sure you have installed the dbus-user-session or dbus-daemon package and then run: `systemctl --user start dbus`")
+	return "", errors.New("could not detect DBUS_SESSION_BUS_ADDRESS from `systemctl --user --no-pager show-environment`")
 }
diff --git a/libcontainer/cgroups/systemd/v1.go b/libcontainer/cgroups/systemd/v1.go
index 2d85fdef..eeda18a9 100644
--- a/libcontainer/cgroups/systemd/v1.go
+++ b/libcontainer/cgroups/systemd/v1.go
@@ -4,6 +4,7 @@ package systemd
 
 import (
 	"errors"
+	"io/ioutil"
 	"os"
 	"path/filepath"
 	"strings"
@@ -12,7 +13,6 @@ import (
 	systemdDbus "github.com/coreos/go-systemd/v22/dbus"
 	"github.com/opencontainers/runc/libcontainer/cgroups"
 	"github.com/opencontainers/runc/libcontainer/cgroups/fs"
-	"github.com/opencontainers/runc/libcontainer/cgroups/fscommon"
 	"github.com/opencontainers/runc/libcontainer/configs"
 	"github.com/sirupsen/logrus"
 )
@@ -415,7 +415,7 @@ func enableKmem(c *configs.Cgroup) error {
 	}
 	// do not try to enable the kernel memory if we already have
 	// tasks in the cgroup.
-	content, err := fscommon.ReadFile(path, "tasks")
+	content, err := ioutil.ReadFile(filepath.Join(path, "tasks"))
 	if err != nil {
 		return err
 	}
diff --git a/libcontainer/cgroups/utils.go b/libcontainer/cgroups/utils.go
index 05018bd4..bc98375e 100644
--- a/libcontainer/cgroups/utils.go
+++ b/libcontainer/cgroups/utils.go
@@ -15,7 +15,6 @@ import (
 	"sync"
 	"time"
 
-	"github.com/opencontainers/runc/libcontainer/cgroups/fscommon"
 	"github.com/sirupsen/logrus"
 	"golang.org/x/sys/unix"
 )
@@ -80,11 +79,11 @@ func GetAllSubsystems() ([]string, error) {
 		// - freezer: implemented in kernel 5.2
 		// We assume these are always available, as it is hard to detect availability.
 		pseudo := []string{"devices", "freezer"}
-		data, err := fscommon.ReadFile("/sys/fs/cgroup", "cgroup.controllers")
+		data, err := ioutil.ReadFile("/sys/fs/cgroup/cgroup.controllers")
 		if err != nil {
 			return nil, err
 		}
-		subsystems := append(pseudo, strings.Fields(data)...)
+		subsystems := append(pseudo, strings.Fields(string(data))...)
 		return subsystems, nil
 	}
 	f, err := os.Open("/proc/cgroups")
@@ -270,7 +269,6 @@ func RemovePaths(paths map[string]string) (err error) {
 			}
 		}
 		if len(paths) == 0 {
-			//nolint:ineffassign // done to help garbage collecting: opencontainers/runc#2506
 			paths = make(map[string]string)
 			return nil
 		}
diff --git a/libcontainer/init_linux.go b/libcontainer/init_linux.go
index 575dd461..11dfe17e 100644
--- a/libcontainer/init_linux.go
+++ b/libcontainer/init_linux.go
@@ -432,7 +432,6 @@ func setupRlimits(limits []configs.Rlimit, pid int) error {
 
 const _P_PID = 1
 
-//nolint:structcheck,unused
 type siginfo struct {
 	si_signo int32
 	si_errno int32
diff --git a/libcontainer/intelrdt/intelrdt.go b/libcontainer/intelrdt/intelrdt.go
index 9e1af7aa..0e5a89bc 100644
--- a/libcontainer/intelrdt/intelrdt.go
+++ b/libcontainer/intelrdt/intelrdt.go
@@ -295,7 +295,11 @@ func getIntelRdtRoot() (string, error) {
 
 func isIntelRdtMounted() bool {
 	_, err := getIntelRdtRoot()
-	return err == nil
+	if err != nil {
+		return false
+	}
+
+	return true
 }
 
 type cpuInfoFlags struct {
diff --git a/libcontainer/process_linux.go b/libcontainer/process_linux.go
index 1402b516..834268b9 100644
--- a/libcontainer/process_linux.go
+++ b/libcontainer/process_linux.go
@@ -511,14 +511,11 @@ func (p *initProcess) start() (retErr error) {
 
 func (p *initProcess) wait() (*os.ProcessState, error) {
 	err := p.cmd.Wait()
-	if err != nil {
-		return p.cmd.ProcessState, err
-	}
 	// we should kill all processes in cgroup when init is died if we use host PID namespace
 	if p.sharePidns {
 		signalAllProcesses(p.manager, unix.SIGKILL)
 	}
-	return p.cmd.ProcessState, nil
+	return p.cmd.ProcessState, err
 }
 
 func (p *initProcess) terminate() error {
diff --git a/main.go b/main.go
index 57d7f428..3c4a5821 100644
--- a/main.go
+++ b/main.go
@@ -147,9 +147,6 @@ func main() {
 				fatal(err)
 			}
 		}
-		if err := reviseRootDir(context); err != nil {
-			return err
-		}
 		return logs.ConfigureLogging(createLogConfig(context))
 	}
 
diff --git a/utils.go b/utils.go
index 8b31be03..c687a478 100644
--- a/utils.go
+++ b/utils.go
@@ -92,21 +92,6 @@ func revisePidFile(context *cli.Context) error {
 	return context.Set("pid-file", pidFile)
 }
 
-// reviseRootDir convert the root to absolute path
-func reviseRootDir(context *cli.Context) error {
-	root := context.GlobalString("root")
-	if root == "" {
-		return nil
-	}
-
-	root, err := filepath.Abs(root)
-	if err != nil {
-		return err
-	}
-
-	return context.GlobalSet("root", root)
-}
-
 // parseBoolOrAuto returns (nil, nil) if s is empty or "auto"
 func parseBoolOrAuto(s string) (*bool, error) {
 	if s == "" || strings.ToLower(s) == "auto" {


Test patch:
diff --git a/libcontainer/cgroups/fscommon/fscommon_test.go b/libcontainer/cgroups/fscommon/fscommon_test.go
index 70a5ebce..e56052e0 100644
--- a/libcontainer/cgroups/fscommon/fscommon_test.go
+++ b/libcontainer/cgroups/fscommon/fscommon_test.go
@@ -9,16 +9,18 @@ import (
 	"strconv"
 	"testing"
 	"time"
+
+	"github.com/opencontainers/runc/libcontainer/cgroups"
 )
 
 func TestWriteCgroupFileHandlesInterrupt(t *testing.T) {
-	const (
-		memoryCgroupMount = "/sys/fs/cgroup/memory"
-		memoryLimit       = "memory.limit_in_bytes"
-	)
-	if _, err := os.Stat(memoryCgroupMount); err != nil {
-		// most probably cgroupv2
-		t.Skip(err)
+	if cgroups.IsCgroup2UnifiedMode() {
+		t.Skip("cgroup v2 is not supported")
+	}
+
+	memoryCgroupMount, err := cgroups.FindCgroupMountpoint("", "memory")
+	if err != nil {
+		t.Fatal(err)
 	}
 
 	cgroupName := fmt.Sprintf("test-eint-%d", time.Now().Nanosecond())
@@ -28,14 +30,9 @@ func TestWriteCgroupFileHandlesInterrupt(t *testing.T) {
 	}
 	defer os.RemoveAll(cgroupPath)
 
-	if _, err := os.Stat(filepath.Join(cgroupPath, memoryLimit)); err != nil {
-		// either cgroupv2, or memory controller is not avalable
-		t.Skip(err)
-	}
-
 	for i := 0; i < 100000; i++ {
 		limit := 1024*1024 + i
-		if err := WriteFile(cgroupPath, memoryLimit, strconv.Itoa(limit)); err != nil {
+		if err := WriteFile(cgroupPath, "memory.limit_in_bytes", strconv.Itoa(limit)); err != nil {
 			t.Fatalf("Failed to write %d on attempt %d: %+v", limit, i, err)
 		}
 	}
diff --git a/libcontainer/integration/exec_test.go b/libcontainer/integration/exec_test.go
index 9c865233..85569954 100644
--- a/libcontainer/integration/exec_test.go
+++ b/libcontainer/integration/exec_test.go
@@ -11,6 +11,7 @@ import (
 	"reflect"
 	"strconv"
 	"strings"
+	"syscall"
 	"testing"
 
 	"github.com/opencontainers/runc/libcontainer"
@@ -1660,6 +1661,59 @@ func TestPIDHost(t *testing.T) {
 	}
 }
 
+func TestPIDHostInitProcessWait(t *testing.T) {
+	if testing.Short() {
+		return
+	}
+
+	rootfs, err := newRootfs()
+	ok(t, err)
+	defer remove(rootfs)
+
+	pidns := "/proc/1/ns/pid"
+
+	// Run a container with two long-running processes.
+	config := newTemplateConfig(rootfs)
+	config.Namespaces.Add(configs.NEWPID, pidns)
+	container, err := newContainerWithName("test", config)
+	ok(t, err)
+	defer func() {
+		_ = container.Destroy()
+	}()
+
+	process1 := &libcontainer.Process{
+		Cwd:  "/",
+		Args: []string{"sleep", "100"},
+		Env:  standardEnvironment,
+		Init: true,
+	}
+	err = container.Run(process1)
+	ok(t, err)
+
+	process2 := &libcontainer.Process{
+		Cwd:  "/",
+		Args: []string{"sleep", "100"},
+		Env:  standardEnvironment,
+		Init: false,
+	}
+	err = container.Run(process2)
+	ok(t, err)
+
+	// Kill the init process and Wait for it.
+	err = process1.Signal(syscall.SIGKILL)
+	ok(t, err)
+	_, err = process1.Wait()
+	if err == nil {
+		t.Fatal("expected Wait to indicate failure")
+	}
+
+	// The non-init process must've been killed.
+	err = process2.Signal(syscall.Signal(0))
+	if err == nil || err.Error() != "no such process" {
+		t.Fatalf("expected process to have been killed: %v", err)
+	}
+}
+
 func TestInitJoinPID(t *testing.T) {
 	if testing.Short() {
 		return