mirror of
https://github.com/opencontainers/runc.git
synced 2026-07-11 06:03:57 +08:00
45cc290f02
Since Go 1.19, godoc recognizes lists, code blocks, headings etc. It also reformats the sources making it more apparent that these features are used. Fix a few places where it misinterpreted the formatting (such as indented vs unindented), and format the result using the gofumpt from HEAD, which already incorporates gofmt 1.19 changes. Some more fixes (and enhancements) might be required. Signed-off-by: Kir Kolyshkin <kolyshkin@gmail.com>
286 lines
7.7 KiB
Go
286 lines
7.7 KiB
Go
package validate
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/opencontainers/runc/libcontainer/cgroups"
|
|
"github.com/opencontainers/runc/libcontainer/configs"
|
|
"github.com/opencontainers/runc/libcontainer/intelrdt"
|
|
selinux "github.com/opencontainers/selinux/go-selinux"
|
|
"github.com/sirupsen/logrus"
|
|
"golang.org/x/sys/unix"
|
|
)
|
|
|
|
type check func(config *configs.Config) error
|
|
|
|
func Validate(config *configs.Config) error {
|
|
checks := []check{
|
|
cgroupsCheck,
|
|
rootfs,
|
|
network,
|
|
hostname,
|
|
security,
|
|
namespaces,
|
|
sysctl,
|
|
intelrdtCheck,
|
|
rootlessEUIDCheck,
|
|
}
|
|
for _, c := range checks {
|
|
if err := c(config); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
// Relaxed validation rules for backward compatibility
|
|
warns := []check{
|
|
mounts, // TODO (runc v1.x.x): make this an error instead of a warning
|
|
}
|
|
for _, c := range warns {
|
|
if err := c(config); err != nil {
|
|
logrus.WithError(err).Warn("invalid configuration")
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// rootfs validates if the rootfs is an absolute path and is not a symlink
|
|
// to the container's root filesystem.
|
|
func rootfs(config *configs.Config) error {
|
|
if _, err := os.Stat(config.Rootfs); err != nil {
|
|
return fmt.Errorf("invalid rootfs: %w", err)
|
|
}
|
|
cleaned, err := filepath.Abs(config.Rootfs)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid rootfs: %w", err)
|
|
}
|
|
if cleaned, err = filepath.EvalSymlinks(cleaned); err != nil {
|
|
return fmt.Errorf("invalid rootfs: %w", err)
|
|
}
|
|
if filepath.Clean(config.Rootfs) != cleaned {
|
|
return errors.New("invalid rootfs: not an absolute path, or a symlink")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func network(config *configs.Config) error {
|
|
if !config.Namespaces.Contains(configs.NEWNET) {
|
|
if len(config.Networks) > 0 || len(config.Routes) > 0 {
|
|
return errors.New("unable to apply network settings without a private NET namespace")
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func hostname(config *configs.Config) error {
|
|
if config.Hostname != "" && !config.Namespaces.Contains(configs.NEWUTS) {
|
|
return errors.New("unable to set hostname without a private UTS namespace")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func security(config *configs.Config) error {
|
|
// restrict sys without mount namespace
|
|
if (len(config.MaskPaths) > 0 || len(config.ReadonlyPaths) > 0) &&
|
|
!config.Namespaces.Contains(configs.NEWNS) {
|
|
return errors.New("unable to restrict sys entries without a private MNT namespace")
|
|
}
|
|
if config.ProcessLabel != "" && !selinux.GetEnabled() {
|
|
return errors.New("selinux label is specified in config, but selinux is disabled or not supported")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func namespaces(config *configs.Config) error {
|
|
if config.Namespaces.Contains(configs.NEWUSER) {
|
|
if _, err := os.Stat("/proc/self/ns/user"); os.IsNotExist(err) {
|
|
return errors.New("USER namespaces aren't enabled in the kernel")
|
|
}
|
|
} else {
|
|
if config.UidMappings != nil || config.GidMappings != nil {
|
|
return errors.New("User namespace mappings specified, but USER namespace isn't enabled in the config")
|
|
}
|
|
}
|
|
|
|
if config.Namespaces.Contains(configs.NEWCGROUP) {
|
|
if _, err := os.Stat("/proc/self/ns/cgroup"); os.IsNotExist(err) {
|
|
return errors.New("cgroup namespaces aren't enabled in the kernel")
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// convertSysctlVariableToDotsSeparator can return sysctl variables in dots separator format.
|
|
// The '/' separator is also accepted in place of a '.'.
|
|
// Convert the sysctl variables to dots separator format for validation.
|
|
// More info: sysctl(8), sysctl.d(5).
|
|
//
|
|
// For example:
|
|
// Input sysctl variable "net/ipv4/conf/eno2.100.rp_filter"
|
|
// will return the converted value "net.ipv4.conf.eno2/100.rp_filter"
|
|
func convertSysctlVariableToDotsSeparator(val string) string {
|
|
if val == "" {
|
|
return val
|
|
}
|
|
firstSepIndex := strings.IndexAny(val, "./")
|
|
if firstSepIndex == -1 || val[firstSepIndex] == '.' {
|
|
return val
|
|
}
|
|
|
|
f := func(r rune) rune {
|
|
switch r {
|
|
case '.':
|
|
return '/'
|
|
case '/':
|
|
return '.'
|
|
}
|
|
return r
|
|
}
|
|
return strings.Map(f, val)
|
|
}
|
|
|
|
// sysctl validates that the specified sysctl keys are valid or not.
|
|
// /proc/sys isn't completely namespaced and depending on which namespaces
|
|
// are specified, a subset of sysctls are permitted.
|
|
func sysctl(config *configs.Config) error {
|
|
validSysctlMap := map[string]bool{
|
|
"kernel.msgmax": true,
|
|
"kernel.msgmnb": true,
|
|
"kernel.msgmni": true,
|
|
"kernel.sem": true,
|
|
"kernel.shmall": true,
|
|
"kernel.shmmax": true,
|
|
"kernel.shmmni": true,
|
|
"kernel.shm_rmid_forced": true,
|
|
}
|
|
|
|
var (
|
|
netOnce sync.Once
|
|
hostnet bool
|
|
hostnetErr error
|
|
)
|
|
|
|
for s := range config.Sysctl {
|
|
s := convertSysctlVariableToDotsSeparator(s)
|
|
if validSysctlMap[s] || strings.HasPrefix(s, "fs.mqueue.") {
|
|
if config.Namespaces.Contains(configs.NEWIPC) {
|
|
continue
|
|
} else {
|
|
return fmt.Errorf("sysctl %q is not allowed in the hosts ipc namespace", s)
|
|
}
|
|
}
|
|
if strings.HasPrefix(s, "net.") {
|
|
// Is container using host netns?
|
|
// Here "host" means "current", not "initial".
|
|
netOnce.Do(func() {
|
|
if !config.Namespaces.Contains(configs.NEWNET) {
|
|
hostnet = true
|
|
return
|
|
}
|
|
path := config.Namespaces.PathOf(configs.NEWNET)
|
|
if path == "" {
|
|
// own netns, so hostnet = false
|
|
return
|
|
}
|
|
hostnet, hostnetErr = isHostNetNS(path)
|
|
})
|
|
if hostnetErr != nil {
|
|
return fmt.Errorf("invalid netns path: %w", hostnetErr)
|
|
}
|
|
if hostnet {
|
|
return fmt.Errorf("sysctl %q not allowed in host network namespace", s)
|
|
}
|
|
continue
|
|
}
|
|
if config.Namespaces.Contains(configs.NEWUTS) {
|
|
switch s {
|
|
case "kernel.domainname":
|
|
// This is namespaced and there's no explicit OCI field for it.
|
|
continue
|
|
case "kernel.hostname":
|
|
// This is namespaced but there's a conflicting (dedicated) OCI field for it.
|
|
return fmt.Errorf("sysctl %q is not allowed as it conflicts with the OCI %q field", s, "hostname")
|
|
}
|
|
}
|
|
return fmt.Errorf("sysctl %q is not in a separate kernel namespace", s)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func intelrdtCheck(config *configs.Config) error {
|
|
if config.IntelRdt != nil {
|
|
if config.IntelRdt.ClosID == "." || config.IntelRdt.ClosID == ".." || strings.Contains(config.IntelRdt.ClosID, "/") {
|
|
return fmt.Errorf("invalid intelRdt.ClosID %q", config.IntelRdt.ClosID)
|
|
}
|
|
|
|
if !intelrdt.IsCATEnabled() && config.IntelRdt.L3CacheSchema != "" {
|
|
return errors.New("intelRdt.l3CacheSchema is specified in config, but Intel RDT/CAT is not enabled")
|
|
}
|
|
if !intelrdt.IsMBAEnabled() && config.IntelRdt.MemBwSchema != "" {
|
|
return errors.New("intelRdt.memBwSchema is specified in config, but Intel RDT/MBA is not enabled")
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func cgroupsCheck(config *configs.Config) error {
|
|
c := config.Cgroups
|
|
if c == nil {
|
|
return nil
|
|
}
|
|
|
|
if (c.Name != "" || c.Parent != "") && c.Path != "" {
|
|
return fmt.Errorf("cgroup: either Path or Name and Parent should be used, got %+v", c)
|
|
}
|
|
|
|
r := c.Resources
|
|
if r == nil {
|
|
return nil
|
|
}
|
|
|
|
if !cgroups.IsCgroup2UnifiedMode() && r.Unified != nil {
|
|
return cgroups.ErrV1NoUnified
|
|
}
|
|
|
|
if cgroups.IsCgroup2UnifiedMode() {
|
|
_, err := cgroups.ConvertMemorySwapToCgroupV2Value(r.MemorySwap, r.Memory)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func mounts(config *configs.Config) error {
|
|
for _, m := range config.Mounts {
|
|
if !filepath.IsAbs(m.Destination) {
|
|
return fmt.Errorf("invalid mount %+v: mount destination not absolute", m)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func isHostNetNS(path string) (bool, error) {
|
|
const currentProcessNetns = "/proc/self/ns/net"
|
|
|
|
var st1, st2 unix.Stat_t
|
|
|
|
if err := unix.Stat(currentProcessNetns, &st1); err != nil {
|
|
return false, &os.PathError{Op: "stat", Path: currentProcessNetns, Err: err}
|
|
}
|
|
if err := unix.Stat(path, &st2); err != nil {
|
|
return false, &os.PathError{Op: "stat", Path: path, Err: err}
|
|
}
|
|
|
|
return (st1.Dev == st2.Dev) && (st1.Ino == st2.Ino), nil
|
|
}
|