mirror of
https://github.com/opencontainers/runc.git
synced 2026-07-11 06:03:57 +08:00
8e8b136c49
With the idmap work, we will have a tainted Go thread in our thread-group that has a different mount namespace to the other threads. It seems that (due to some bad luck) the Go scheduler tends to make this thread the thread-group leader in our tests, which results in very baffling failures where /proc/self/mountinfo produces gibberish results. In order to avoid this, switch to using /proc/thread-self for everything that is thread-local. This primarily includes switching all file descriptor paths (CLONE_FS), all of the places that check the current cgroup (technically we never will run a single runc thread in a separate cgroup, but better to be safe than sorry), and the aforementioned mountinfo code. We don't need to do anything for the following because the results we need aren't thread-local: * Checks that certain namespaces are supported by stat(2)ing /proc/self/ns/... * /proc/self/exe and /proc/self/cmdline are not thread-local. * While threads can be in different cgroups, we do not do this for the runc binary (or libcontainer) and thus we do not need to switch to the thread-local version of /proc/self/cgroups. * All of the CLONE_NEWUSER files are not thread-local because you cannot set the usernamespace of a single thread (setns(CLONE_NEWUSER) is blocked for multi-threaded programs). Note that we have to use runtime.LockOSThread when we have an open handle to a tid-specific procfs file that we are operating on multiple times. Go can reschedule us such that we are running on a different thread and then kill the original thread (causing -ENOENT or similarly confusing errors). This is not strictly necessary for most usages of /proc/thread-self (such as using /proc/thread-self/fd/$n directly) since only operating on the actual inodes associated with the tid requires this locking, but because of the pre-3.17 fallback for CentOS, we have to do this in most cases. In addition, CentOS's kernel is too old for /proc/thread-self, which requires us to emulate it -- however in rootfs_linux.go, we are in the container pid namespace but /proc is the host's procfs. This leads to the incredibly frustrating situation where there is no way (on pre-4.1 Linux) to figure out which /proc/self/task/... entry refers to the current tid. We can just use /proc/self in this case. Yes this is all pretty ugly. I also wish it wasn't necessary. Signed-off-by: Aleksa Sarai <cyphar@cyphar.com>
182 lines
4.8 KiB
Go
182 lines
4.8 KiB
Go
package cgroups
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/opencontainers/runc/libcontainer/utils"
|
|
"github.com/sirupsen/logrus"
|
|
"golang.org/x/sys/unix"
|
|
)
|
|
|
|
// OpenFile opens a cgroup file in a given dir with given flags.
|
|
// It is supposed to be used for cgroup files only, and returns
|
|
// an error if the file is not a cgroup file.
|
|
//
|
|
// Arguments dir and file are joined together to form an absolute path
|
|
// to a file being opened.
|
|
func OpenFile(dir, file string, flags int) (*os.File, error) {
|
|
if dir == "" {
|
|
return nil, fmt.Errorf("no directory specified for %s", file)
|
|
}
|
|
return openFile(dir, file, flags)
|
|
}
|
|
|
|
// ReadFile reads data from a cgroup file in dir.
|
|
// It is supposed to be used for cgroup files only.
|
|
func ReadFile(dir, file string) (string, error) {
|
|
fd, err := OpenFile(dir, file, unix.O_RDONLY)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer fd.Close()
|
|
var buf bytes.Buffer
|
|
|
|
_, err = buf.ReadFrom(fd)
|
|
return buf.String(), err
|
|
}
|
|
|
|
// WriteFile writes data to a cgroup file in dir.
|
|
// It is supposed to be used for cgroup files only.
|
|
func WriteFile(dir, file, data string) error {
|
|
fd, err := OpenFile(dir, file, unix.O_WRONLY)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer fd.Close()
|
|
if _, err := fd.WriteString(data); err != nil {
|
|
// Having data in the error message helps in debugging.
|
|
return fmt.Errorf("failed to write %q: %w", data, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
const (
|
|
cgroupfsDir = "/sys/fs/cgroup"
|
|
cgroupfsPrefix = cgroupfsDir + "/"
|
|
)
|
|
|
|
var (
|
|
// TestMode is set to true by unit tests that need "fake" cgroupfs.
|
|
TestMode bool
|
|
|
|
cgroupFd int = -1
|
|
prepOnce sync.Once
|
|
prepErr error
|
|
resolveFlags uint64
|
|
)
|
|
|
|
func prepareOpenat2() error {
|
|
prepOnce.Do(func() {
|
|
fd, err := unix.Openat2(-1, cgroupfsDir, &unix.OpenHow{
|
|
Flags: unix.O_DIRECTORY | unix.O_PATH,
|
|
})
|
|
if err != nil {
|
|
prepErr = &os.PathError{Op: "openat2", Path: cgroupfsDir, Err: err}
|
|
if err != unix.ENOSYS {
|
|
logrus.Warnf("falling back to securejoin: %s", prepErr)
|
|
} else {
|
|
logrus.Debug("openat2 not available, falling back to securejoin")
|
|
}
|
|
return
|
|
}
|
|
var st unix.Statfs_t
|
|
if err = unix.Fstatfs(fd, &st); err != nil {
|
|
prepErr = &os.PathError{Op: "statfs", Path: cgroupfsDir, Err: err}
|
|
logrus.Warnf("falling back to securejoin: %s", prepErr)
|
|
return
|
|
}
|
|
|
|
cgroupFd = fd
|
|
|
|
resolveFlags = unix.RESOLVE_BENEATH | unix.RESOLVE_NO_MAGICLINKS
|
|
if st.Type == unix.CGROUP2_SUPER_MAGIC {
|
|
// cgroupv2 has a single mountpoint and no "cpu,cpuacct" symlinks
|
|
resolveFlags |= unix.RESOLVE_NO_XDEV | unix.RESOLVE_NO_SYMLINKS
|
|
}
|
|
})
|
|
|
|
return prepErr
|
|
}
|
|
|
|
func openFile(dir, file string, flags int) (*os.File, error) {
|
|
mode := os.FileMode(0)
|
|
if TestMode && flags&os.O_WRONLY != 0 {
|
|
// "emulate" cgroup fs for unit tests
|
|
flags |= os.O_TRUNC | os.O_CREATE
|
|
mode = 0o600
|
|
}
|
|
path := path.Join(dir, utils.CleanPath(file))
|
|
if prepareOpenat2() != nil {
|
|
return openFallback(path, flags, mode)
|
|
}
|
|
relPath := strings.TrimPrefix(path, cgroupfsPrefix)
|
|
if len(relPath) == len(path) { // non-standard path, old system?
|
|
return openFallback(path, flags, mode)
|
|
}
|
|
|
|
fd, err := unix.Openat2(cgroupFd, relPath,
|
|
&unix.OpenHow{
|
|
Resolve: resolveFlags,
|
|
Flags: uint64(flags) | unix.O_CLOEXEC,
|
|
Mode: uint64(mode),
|
|
})
|
|
if err != nil {
|
|
err = &os.PathError{Op: "openat2", Path: path, Err: err}
|
|
// Check if cgroupFd is still opened to cgroupfsDir
|
|
// (happens when this package is incorrectly used
|
|
// across the chroot/pivot_root/mntns boundary, or
|
|
// when /sys/fs/cgroup is remounted).
|
|
//
|
|
// TODO: if such usage will ever be common, amend this
|
|
// to reopen cgroupFd and retry openat2.
|
|
fdPath, closer := utils.ProcThreadSelf("fd/" + strconv.Itoa(cgroupFd))
|
|
defer closer()
|
|
fdDest, _ := os.Readlink(fdPath)
|
|
if fdDest != cgroupfsDir {
|
|
// Wrap the error so it is clear that cgroupFd
|
|
// is opened to an unexpected/wrong directory.
|
|
err = fmt.Errorf("cgroupFd %d unexpectedly opened to %s != %s: %w",
|
|
cgroupFd, fdDest, cgroupfsDir, err)
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
return os.NewFile(uintptr(fd), path), nil
|
|
}
|
|
|
|
var errNotCgroupfs = errors.New("not a cgroup file")
|
|
|
|
// Can be changed by unit tests.
|
|
var openFallback = openAndCheck
|
|
|
|
// openAndCheck is used when openat2(2) is not available. It checks the opened
|
|
// file is on cgroupfs, returning an error otherwise.
|
|
func openAndCheck(path string, flags int, mode os.FileMode) (*os.File, error) {
|
|
fd, err := os.OpenFile(path, flags, mode)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if TestMode {
|
|
return fd, nil
|
|
}
|
|
// Check this is a cgroupfs file.
|
|
var st unix.Statfs_t
|
|
if err := unix.Fstatfs(int(fd.Fd()), &st); err != nil {
|
|
_ = fd.Close()
|
|
return nil, &os.PathError{Op: "statfs", Path: path, Err: err}
|
|
}
|
|
if st.Type != unix.CGROUP_SUPER_MAGIC && st.Type != unix.CGROUP2_SUPER_MAGIC {
|
|
_ = fd.Close()
|
|
return nil, &os.PathError{Op: "open", Path: path, Err: errNotCgroupfs}
|
|
}
|
|
|
|
return fd, nil
|
|
}
|