...

Source file src/github.com/opencontainers/runc/libcontainer/configs/validate/validator.go

Documentation: github.com/opencontainers/runc/libcontainer/configs/validate

     1  package validate
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"os"
     7  	"path/filepath"
     8  	"strings"
     9  	"sync"
    10  
    11  	"github.com/opencontainers/runc/libcontainer/cgroups"
    12  	"github.com/opencontainers/runc/libcontainer/configs"
    13  	"github.com/opencontainers/runc/libcontainer/intelrdt"
    14  	selinux "github.com/opencontainers/selinux/go-selinux"
    15  	"github.com/sirupsen/logrus"
    16  	"golang.org/x/sys/unix"
    17  )
    18  
    19  type Validator interface {
    20  	Validate(*configs.Config) error
    21  }
    22  
    23  func New() Validator {
    24  	return &ConfigValidator{}
    25  }
    26  
    27  type ConfigValidator struct{}
    28  
    29  type check func(config *configs.Config) error
    30  
    31  func (v *ConfigValidator) Validate(config *configs.Config) error {
    32  	checks := []check{
    33  		v.cgroups,
    34  		v.rootfs,
    35  		v.network,
    36  		v.hostname,
    37  		v.security,
    38  		v.usernamespace,
    39  		v.cgroupnamespace,
    40  		v.sysctl,
    41  		v.intelrdt,
    42  		v.rootlessEUID,
    43  	}
    44  	for _, c := range checks {
    45  		if err := c(config); err != nil {
    46  			return err
    47  		}
    48  	}
    49  	// Relaxed validation rules for backward compatibility
    50  	warns := []check{
    51  		v.mounts, // TODO (runc v1.x.x): make this an error instead of a warning
    52  	}
    53  	for _, c := range warns {
    54  		if err := c(config); err != nil {
    55  			logrus.WithError(err).Warn("invalid configuration")
    56  		}
    57  	}
    58  	return nil
    59  }
    60  
    61  // rootfs validates if the rootfs is an absolute path and is not a symlink
    62  // to the container's root filesystem.
    63  func (v *ConfigValidator) rootfs(config *configs.Config) error {
    64  	if _, err := os.Stat(config.Rootfs); err != nil {
    65  		return fmt.Errorf("invalid rootfs: %w", err)
    66  	}
    67  	cleaned, err := filepath.Abs(config.Rootfs)
    68  	if err != nil {
    69  		return fmt.Errorf("invalid rootfs: %w", err)
    70  	}
    71  	if cleaned, err = filepath.EvalSymlinks(cleaned); err != nil {
    72  		return fmt.Errorf("invalid rootfs: %w", err)
    73  	}
    74  	if filepath.Clean(config.Rootfs) != cleaned {
    75  		return errors.New("invalid rootfs: not an absolute path, or a symlink")
    76  	}
    77  	return nil
    78  }
    79  
    80  func (v *ConfigValidator) network(config *configs.Config) error {
    81  	if !config.Namespaces.Contains(configs.NEWNET) {
    82  		if len(config.Networks) > 0 || len(config.Routes) > 0 {
    83  			return errors.New("unable to apply network settings without a private NET namespace")
    84  		}
    85  	}
    86  	return nil
    87  }
    88  
    89  func (v *ConfigValidator) hostname(config *configs.Config) error {
    90  	if config.Hostname != "" && !config.Namespaces.Contains(configs.NEWUTS) {
    91  		return errors.New("unable to set hostname without a private UTS namespace")
    92  	}
    93  	return nil
    94  }
    95  
    96  func (v *ConfigValidator) security(config *configs.Config) error {
    97  	// restrict sys without mount namespace
    98  	if (len(config.MaskPaths) > 0 || len(config.ReadonlyPaths) > 0) &&
    99  		!config.Namespaces.Contains(configs.NEWNS) {
   100  		return errors.New("unable to restrict sys entries without a private MNT namespace")
   101  	}
   102  	if config.ProcessLabel != "" && !selinux.GetEnabled() {
   103  		return errors.New("selinux label is specified in config, but selinux is disabled or not supported")
   104  	}
   105  
   106  	return nil
   107  }
   108  
   109  func (v *ConfigValidator) usernamespace(config *configs.Config) error {
   110  	if config.Namespaces.Contains(configs.NEWUSER) {
   111  		if _, err := os.Stat("/proc/self/ns/user"); os.IsNotExist(err) {
   112  			return errors.New("user namespaces aren't enabled in the kernel")
   113  		}
   114  		hasPath := config.Namespaces.PathOf(configs.NEWUSER) != ""
   115  		hasMappings := config.UidMappings != nil || config.GidMappings != nil
   116  		if !hasPath && !hasMappings {
   117  			return errors.New("user namespaces enabled, but no namespace path to join nor mappings to apply specified")
   118  		}
   119  		// The hasPath && hasMappings validation case is handled in specconv --
   120  		// we cache the mappings in Config during specconv in the hasPath case,
   121  		// so we cannot do that validation here.
   122  	} else {
   123  		if config.UidMappings != nil || config.GidMappings != nil {
   124  			return errors.New("user namespace mappings specified, but user namespace isn't enabled in the config")
   125  		}
   126  	}
   127  	return nil
   128  }
   129  
   130  func (v *ConfigValidator) cgroupnamespace(config *configs.Config) error {
   131  	if config.Namespaces.Contains(configs.NEWCGROUP) {
   132  		if _, err := os.Stat("/proc/self/ns/cgroup"); os.IsNotExist(err) {
   133  			return errors.New("cgroup namespaces aren't enabled in the kernel")
   134  		}
   135  	}
   136  	return nil
   137  }
   138  
   139  // convertSysctlVariableToDotsSeparator can return sysctl variables in dots separator format.
   140  // The '/' separator is also accepted in place of a '.'.
   141  // Convert the sysctl variables to dots separator format for validation.
   142  // More info: sysctl(8), sysctl.d(5).
   143  //
   144  // For example:
   145  // Input sysctl variable "net/ipv4/conf/eno2.100.rp_filter"
   146  // will return the converted value "net.ipv4.conf.eno2/100.rp_filter"
   147  func convertSysctlVariableToDotsSeparator(val string) string {
   148  	if val == "" {
   149  		return val
   150  	}
   151  	firstSepIndex := strings.IndexAny(val, "./")
   152  	if firstSepIndex == -1 || val[firstSepIndex] == '.' {
   153  		return val
   154  	}
   155  
   156  	f := func(r rune) rune {
   157  		switch r {
   158  		case '.':
   159  			return '/'
   160  		case '/':
   161  			return '.'
   162  		}
   163  		return r
   164  	}
   165  	return strings.Map(f, val)
   166  }
   167  
   168  // sysctl validates that the specified sysctl keys are valid or not.
   169  // /proc/sys isn't completely namespaced and depending on which namespaces
   170  // are specified, a subset of sysctls are permitted.
   171  func (v *ConfigValidator) sysctl(config *configs.Config) error {
   172  	validSysctlMap := map[string]bool{
   173  		"kernel.msgmax":          true,
   174  		"kernel.msgmnb":          true,
   175  		"kernel.msgmni":          true,
   176  		"kernel.sem":             true,
   177  		"kernel.shmall":          true,
   178  		"kernel.shmmax":          true,
   179  		"kernel.shmmni":          true,
   180  		"kernel.shm_rmid_forced": true,
   181  	}
   182  
   183  	var (
   184  		netOnce    sync.Once
   185  		hostnet    bool
   186  		hostnetErr error
   187  	)
   188  
   189  	for s := range config.Sysctl {
   190  		s := convertSysctlVariableToDotsSeparator(s)
   191  		if validSysctlMap[s] || strings.HasPrefix(s, "fs.mqueue.") {
   192  			if config.Namespaces.Contains(configs.NEWIPC) {
   193  				continue
   194  			} else {
   195  				return fmt.Errorf("sysctl %q is not allowed in the hosts ipc namespace", s)
   196  			}
   197  		}
   198  		if strings.HasPrefix(s, "net.") {
   199  			// Is container using host netns?
   200  			// Here "host" means "current", not "initial".
   201  			netOnce.Do(func() {
   202  				if !config.Namespaces.Contains(configs.NEWNET) {
   203  					hostnet = true
   204  					return
   205  				}
   206  				path := config.Namespaces.PathOf(configs.NEWNET)
   207  				if path == "" {
   208  					// own netns, so hostnet = false
   209  					return
   210  				}
   211  				hostnet, hostnetErr = isHostNetNS(path)
   212  			})
   213  			if hostnetErr != nil {
   214  				return fmt.Errorf("invalid netns path: %w", hostnetErr)
   215  			}
   216  			if hostnet {
   217  				return fmt.Errorf("sysctl %q not allowed in host network namespace", s)
   218  			}
   219  			continue
   220  		}
   221  		if config.Namespaces.Contains(configs.NEWUTS) {
   222  			switch s {
   223  			case "kernel.domainname":
   224  				// This is namespaced and there's no explicit OCI field for it.
   225  				continue
   226  			case "kernel.hostname":
   227  				// This is namespaced but there's a conflicting (dedicated) OCI field for it.
   228  				return fmt.Errorf("sysctl %q is not allowed as it conflicts with the OCI %q field", s, "hostname")
   229  			}
   230  		}
   231  		return fmt.Errorf("sysctl %q is not in a separate kernel namespace", s)
   232  	}
   233  
   234  	return nil
   235  }
   236  
   237  func (v *ConfigValidator) intelrdt(config *configs.Config) error {
   238  	if config.IntelRdt != nil {
   239  		if config.IntelRdt.ClosID == "." || config.IntelRdt.ClosID == ".." || strings.Contains(config.IntelRdt.ClosID, "/") {
   240  			return fmt.Errorf("invalid intelRdt.ClosID %q", config.IntelRdt.ClosID)
   241  		}
   242  
   243  		if !intelrdt.IsCATEnabled() && config.IntelRdt.L3CacheSchema != "" {
   244  			return errors.New("intelRdt.l3CacheSchema is specified in config, but Intel RDT/CAT is not enabled")
   245  		}
   246  		if !intelrdt.IsMBAEnabled() && config.IntelRdt.MemBwSchema != "" {
   247  			return errors.New("intelRdt.memBwSchema is specified in config, but Intel RDT/MBA is not enabled")
   248  		}
   249  	}
   250  
   251  	return nil
   252  }
   253  
   254  func (v *ConfigValidator) cgroups(config *configs.Config) error {
   255  	c := config.Cgroups
   256  	if c == nil {
   257  		return nil
   258  	}
   259  
   260  	if (c.Name != "" || c.Parent != "") && c.Path != "" {
   261  		return fmt.Errorf("cgroup: either Path or Name and Parent should be used, got %+v", c)
   262  	}
   263  
   264  	r := c.Resources
   265  	if r == nil {
   266  		return nil
   267  	}
   268  
   269  	if !cgroups.IsCgroup2UnifiedMode() && r.Unified != nil {
   270  		return cgroups.ErrV1NoUnified
   271  	}
   272  
   273  	if cgroups.IsCgroup2UnifiedMode() {
   274  		_, err := cgroups.ConvertMemorySwapToCgroupV2Value(r.MemorySwap, r.Memory)
   275  		if err != nil {
   276  			return err
   277  		}
   278  	}
   279  
   280  	return nil
   281  }
   282  
   283  func (v *ConfigValidator) mounts(config *configs.Config) error {
   284  	for _, m := range config.Mounts {
   285  		if !filepath.IsAbs(m.Destination) {
   286  			return fmt.Errorf("invalid mount %+v: mount destination not absolute", m)
   287  		}
   288  	}
   289  
   290  	return nil
   291  }
   292  
   293  func isHostNetNS(path string) (bool, error) {
   294  	const currentProcessNetns = "/proc/self/ns/net"
   295  
   296  	var st1, st2 unix.Stat_t
   297  
   298  	if err := unix.Stat(currentProcessNetns, &st1); err != nil {
   299  		return false, &os.PathError{Op: "stat", Path: currentProcessNetns, Err: err}
   300  	}
   301  	if err := unix.Stat(path, &st2); err != nil {
   302  		return false, &os.PathError{Op: "stat", Path: path, Err: err}
   303  	}
   304  
   305  	return (st1.Dev == st2.Dev) && (st1.Ino == st2.Ino), nil
   306  }
   307  

View as plain text