mirror of
https://github.com/opencontainers/runc.git
synced 2026-07-11 06:03:57 +08:00
c2d9668cc5
Fix reading cgroup files from the top cgroup directory, i.e.
/sys/fs/cgroup.
The code was working for for any subdirectory of /sys/fs/cgroup, but
for dir="/sys/fs/cgroup" a fallback (open and fstatfs) was used, because
of the way the function worked with the dir argument.
Fix those cases, and add unit tests to make sure they work. While at it,
make the rules for dir and name components more relaxed, and add test
cases for this, too.
While at it, improve OpenFile documentation, and remove a duplicated
doc comment for openFile.
Without these fixes, the unit test fails the following cases:
file_test.go:67: case {path:/sys/fs/cgroup name:cgroup.controllers}: fallback
file_test.go:67: case {path:/sys/fs/cgroup/ name:cgroup.controllers}: openat2 /sys/fs/cgroup//cgroup.controllers: invalid cross-device link
file_test.go:67: case {path:/sys/fs/cgroup/ name:/cgroup.controllers}: openat2 /sys/fs/cgroup///cgroup.controllers: invalid cross-device link
file_test.go:67: case {path:/ name:/sys/fs/cgroup/cgroup.controllers}: fallback
file_test.go:67: case {path:/ name:sys/fs/cgroup/cgroup.controllers}: fallback
file_test.go:67: case {path:/sys/fs/cgroup/cgroup.controllers name:}: openat2 /sys/fs/cgroup/cgroup.controllers/: not a directory
Here "fallback" means openat2-based implementation fails, and the fallback code
is used (and works).
Signed-off-by: Kir Kolyshkin <kolyshkin@gmail.com>
174 lines
4.4 KiB
Go
174 lines
4.4 KiB
Go
package cgroups
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path"
|
|
"strings"
|
|
"sync"
|
|
|
|
"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 := retryingWriteFile(fd, data); err != nil {
|
|
// Having data in the error message helps in debugging.
|
|
return fmt.Errorf("failed to write %q: %w", data, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func retryingWriteFile(fd *os.File, data string) error {
|
|
for {
|
|
_, err := fd.Write([]byte(data))
|
|
if errors.Is(err, unix.EINTR) {
|
|
logrus.Infof("interrupted while writing %s to %s", data, fd.Name())
|
|
continue
|
|
}
|
|
return err
|
|
}
|
|
}
|
|
|
|
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 { //nolint:errorlint // unix errors are bare
|
|
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, 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 {
|
|
return nil, &os.PathError{Op: "openat2", Path: path, Err: 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
|
|
}
|