From e9341f2076c8a89187109ac02cb727764156d107 Mon Sep 17 00:00:00 2001 From: Kailun Qin Date: Wed, 1 Sep 2021 13:42:31 -0400 Subject: [PATCH 1/5] libcontainer: add support for Landlock This patch introduces Landlock Linux Security Module (LSM) support in runc, which was landed in Linux kernel 5.13. This allows unprivileged processes to create safe security sandboxes that can securely restrict the ambient rights (e.g. global filesystem access) for themselves. runtime-spec: https://github.com/opencontainers/runtime-spec/pull/1111 Fixes https://github.com/opencontainers/runc/issues/2859 Signed-off-by: Kailun Qin --- go.mod | 3 +- go.sum | 8 ++- libcontainer/configs/config.go | 51 ++++++++++++++ libcontainer/landlock/config.go | 31 +++++++++ libcontainer/landlock/landlock_linux.go | 67 +++++++++++++++++++ libcontainer/landlock/landlock_unsupported.go | 19 ++++++ libcontainer/setns_init_linux.go | 7 ++ libcontainer/specconv/spec_linux.go | 58 ++++++++++++++++ libcontainer/standard_init_linux.go | 8 +++ 9 files changed, 249 insertions(+), 3 deletions(-) create mode 100644 libcontainer/landlock/config.go create mode 100644 libcontainer/landlock/landlock_linux.go create mode 100644 libcontainer/landlock/landlock_unsupported.go diff --git a/go.mod b/go.mod index 64d19452f6b..dfeb440c16c 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/cyphar/filepath-securejoin v0.2.3 github.com/docker/go-units v0.4.0 github.com/godbus/dbus/v5 v5.0.4 + github.com/landlock-lsm/go-landlock v0.0.0-20210828133255-ec6c6b87a946 github.com/moby/sys/mountinfo v0.4.1 github.com/mrunalp/fileutils v0.5.0 github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417 @@ -22,6 +23,6 @@ require ( github.com/urfave/cli v1.22.1 github.com/vishvananda/netlink v1.1.0 golang.org/x/net v0.0.0-20201224014010-6772e930b67b - golang.org/x/sys v0.0.0-20210426230700-d19ff857e887 + golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf google.golang.org/protobuf v1.27.1 ) diff --git a/go.sum b/go.sum index 186bf5354f9..39ac0e0726c 100644 --- a/go.sum +++ b/go.sum @@ -40,6 +40,8 @@ github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfn github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/landlock-lsm/go-landlock v0.0.0-20210828133255-ec6c6b87a946 h1:RRTOwBnwZR4a3IMyPq1uchxJcrLKWF4NTCHB2fbvo5Y= +github.com/landlock-lsm/go-landlock v0.0.0-20210828133255-ec6c6b87a946/go.mod h1:wjznJ04q4Tvsbx3vkzfmgfEOe6w5dSGlXFa+xbSl9X8= github.com/moby/sys/mountinfo v0.4.1 h1:1O+1cHA1aujwEwwVMa2Xm2l+gIpUHyd3+D+d7LZh1kM= github.com/moby/sys/mountinfo v0.4.1/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A= github.com/mrunalp/fileutils v0.5.0 h1:NKzVxiH7eSk+OQ4M+ZYW1K6h27RUV3MI6NUTsHhU6Z4= @@ -76,8 +78,8 @@ golang.org/x/sys v0.0.0-20191115151921-52ab43148777/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210426230700-d19ff857e887 h1:dXfMednGJh/SUUFjTLsWJz3P+TQt9qnR11GgeI3vWKs= -golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf h1:2ucpDCmfkl8Bd/FsLtiD653Wf96cW37s+iGx93zsu4k= +golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -94,3 +96,5 @@ google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+Rur google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +kernel.org/pub/linux/libs/security/libcap/psx v1.2.51 h1:VXVXjnTUsA9zeHIolNb6moSXZavDe1pD8Q0lPXZEOwc= +kernel.org/pub/linux/libs/security/libcap/psx v1.2.51/go.mod h1:+l6Ee2F59XiJ2I6WR5ObpC1utCQJZ/VLsEbQCD8RG24= diff --git a/libcontainer/configs/config.go b/libcontainer/configs/config.go index e0db8e01782..e6278a029a6 100644 --- a/libcontainer/configs/config.go +++ b/libcontainer/configs/config.go @@ -81,6 +81,53 @@ type Syscall struct { Args []*Arg `json:"args"` } +// Landlock specifies the Landlock unprivileged access control settings for the container process. +type Landlock struct { + Ruleset *Ruleset `json:"ruleset"` + Rules *Rules `json:"rules"` + DisableBestEffort bool `json:"disableBestEffort"` +} + +// Ruleset identifies a set of rules (i.e., actions on objects) that need to be handled in Landlock. +type Ruleset struct { + HandledAccessFS AccessFS `json:"handledAccessFS"` +} + +// Rules represents the security policies (i.e., actions allowed on objects) in Landlock. +type Rules struct { + PathBeneath []*RulePathBeneath `json:"pathBeneath"` +} + +// RulePathBeneath defines the file-hierarchy typed rule that grants the access rights specified by +// `AllowedAccess` to the file hierarchies under the given `Paths` in Landlock. +type RulePathBeneath struct { + AllowedAccess AccessFS `json:"allowedAccess"` + Paths []string `json:"paths"` +} + +// AccessFS is taken upon ruleset and rule setup in Landlock. +type AccessFS uint64 + +// Landlock access rights for FS. +// +// Please see the full documentation at +// https://www.kernel.org/doc/html/latest/userspace-api/landlock.html#access-rights. +const ( + Execute AccessFS = (1 << 0) + WriteFile AccessFS = (1 << 1) + ReadFile AccessFS = (1 << 2) + ReadDir AccessFS = (1 << 3) + RemoveDir AccessFS = (1 << 4) + RemoveFile AccessFS = (1 << 5) + MakeChar AccessFS = (1 << 6) + MakeDir AccessFS = (1 << 7) + MakeReg AccessFS = (1 << 8) + MakeSock AccessFS = (1 << 9) + MakeFifo AccessFS = (1 << 10) + MakeBlock AccessFS = (1 << 11) + MakeSym AccessFS = (1 << 12) +) + // TODO Windows. Many of these fields should be factored out into those parts // which are common across platforms, and those which are platform specific. @@ -209,6 +256,10 @@ type Config struct { // RootlessCgroups is set when unlikely to have the full access to cgroups. // When RootlessCgroups is set, cgroups errors are ignored. RootlessCgroups bool `json:"rootless_cgroups,omitempty"` + + // Landlock specifies the Landlock unprivileged access control settings for the container process. + // `noNewPrivileges` must be enabled to use Landlock. + Landlock *Landlock `json:"landlock,omitempty"` } type ( diff --git a/libcontainer/landlock/config.go b/libcontainer/landlock/config.go new file mode 100644 index 00000000000..c91ef4d3720 --- /dev/null +++ b/libcontainer/landlock/config.go @@ -0,0 +1,31 @@ +package landlock + +import ( + "fmt" + + "github.com/opencontainers/runc/libcontainer/configs" +) + +var accessFSs = map[string]configs.AccessFS{ + "execute": configs.Execute, + "write_file": configs.WriteFile, + "read_file": configs.ReadFile, + "read_dir": configs.ReadDir, + "remove_dir": configs.RemoveDir, + "remove_file": configs.RemoveFile, + "make_char": configs.MakeChar, + "make_dir": configs.MakeDir, + "make_reg": configs.MakeReg, + "make_sock": configs.MakeSock, + "make_fifo": configs.MakeFifo, + "make_block": configs.MakeBlock, + "make_sym": configs.MakeSym, +} + +// ConvertStringToAccessFS converts a string into a Landlock access right. +func ConvertStringToAccessFS(in string) (configs.AccessFS, error) { + if access, ok := accessFSs[in]; ok { + return access, nil + } + return 0, fmt.Errorf("string %s is not a valid access right for landlock", in) +} diff --git a/libcontainer/landlock/landlock_linux.go b/libcontainer/landlock/landlock_linux.go new file mode 100644 index 00000000000..158f23cc126 --- /dev/null +++ b/libcontainer/landlock/landlock_linux.go @@ -0,0 +1,67 @@ +// +build linux + +package landlock + +import ( + "errors" + "fmt" + + "github.com/landlock-lsm/go-landlock/landlock" + + "github.com/opencontainers/runc/libcontainer/configs" +) + +// Initialize Landlock unprivileged access control for the container process +// based on the given settings. +// The specified `ruleset` identifies a set of rules (i.e., actions on objects) +// that need to be handled (i.e., restricted) by Landlock. And if no `rule` +// explicitly allow them, they should then be forbidden. +// The `disableBestEffort` input gives control over whether the best-effort +// security approach should be applied for Landlock access rights. +func InitLandlock(config *configs.Landlock) error { + if config == nil { + return errors.New("cannot initialize Landlock - nil config passed") + } + + var llConfig landlock.Config + + ruleset := getAccess(config.Ruleset.HandledAccessFS) + // Panic on error when constructing the Landlock configuration using invalid config values. + if config.DisableBestEffort { + llConfig = landlock.MustConfig(ruleset) + } else { + llConfig = landlock.MustConfig(ruleset).BestEffort() + } + + if err := llConfig.RestrictPaths( + getPathAccesses(config.Rules)..., + ); err != nil { + return fmt.Errorf("Could not restrict paths: %v", err) + } + + return nil +} + +// Convert Libcontainer AccessFS to go-landlock AccessFSSet. +func getAccess(access configs.AccessFS) landlock.AccessFSSet { + return landlock.AccessFSSet(access) +} + +// Convert Libcontainer RulePathBeneath to go-landlock PathOpt. +func getPathAccess(rule *configs.RulePathBeneath) landlock.PathOpt { + return landlock.PathAccess( + getAccess(rule.AllowedAccess), + rule.Paths...) +} + +// Convert Libcontainer Rules to an array of go-landlock PathOpt. +func getPathAccesses(rules *configs.Rules) []landlock.PathOpt { + pathAccesses := []landlock.PathOpt{} + + for _, rule := range rules.PathBeneath { + opt := getPathAccess(rule) + pathAccesses = append(pathAccesses, opt) + } + + return pathAccesses +} diff --git a/libcontainer/landlock/landlock_unsupported.go b/libcontainer/landlock/landlock_unsupported.go new file mode 100644 index 00000000000..e2fe222ee99 --- /dev/null +++ b/libcontainer/landlock/landlock_unsupported.go @@ -0,0 +1,19 @@ +// +build !linux + +package landlock + +import ( + "errors" + + "github.com/opencontainers/runc/libcontainer/configs" +) + +var ErrLandlockNotSupported = errors.New("land: config provided but Landlock not supported") + +// InitLandlock does nothing because Landlock is not supported. +func InitSLandlock(config *configs.Landlock) error { + if config != nil { + return ErrLandlockNotSupported + } + return nil +} diff --git a/libcontainer/setns_init_linux.go b/libcontainer/setns_init_linux.go index a2c9efc4f2a..759baa3d340 100644 --- a/libcontainer/setns_init_linux.go +++ b/libcontainer/setns_init_linux.go @@ -13,6 +13,7 @@ import ( "github.com/opencontainers/runc/libcontainer/apparmor" "github.com/opencontainers/runc/libcontainer/keys" + "github.com/opencontainers/runc/libcontainer/landlock" "github.com/opencontainers/runc/libcontainer/seccomp" "github.com/opencontainers/runc/libcontainer/system" ) @@ -86,6 +87,12 @@ func (l *linuxSetnsInit) Init() error { if err := apparmor.ApplyProfile(l.config.AppArmorProfile); err != nil { return err } + // `noNewPrivileges` must be enabled to use Landlock. + if l.config.Config.Landlock != nil && l.config.NoNewPrivileges { + if err := landlock.InitLandlock(l.config.Config.Landlock); err != nil { + return fmt.Errorf("unable to init Landlock: %w", err) + } + } // Set seccomp as close to execve as possible, so as few syscalls take // place afterward (reducing the amount of syscalls that users need to // enable in their seccomp profiles). diff --git a/libcontainer/specconv/spec_linux.go b/libcontainer/specconv/spec_linux.go index 991c08a1996..25c2b96434c 100644 --- a/libcontainer/specconv/spec_linux.go +++ b/libcontainer/specconv/spec_linux.go @@ -16,6 +16,7 @@ import ( "github.com/opencontainers/runc/libcontainer/cgroups" "github.com/opencontainers/runc/libcontainer/configs" "github.com/opencontainers/runc/libcontainer/devices" + "github.com/opencontainers/runc/libcontainer/landlock" "github.com/opencontainers/runc/libcontainer/seccomp" libcontainerUtils "github.com/opencontainers/runc/libcontainer/utils" "github.com/opencontainers/runtime-spec/specs-go" @@ -319,6 +320,14 @@ func CreateLibcontainerConfig(opts *CreateOpts) (*configs.Config, error) { Ambient: spec.Process.Capabilities.Ambient, } } + if spec.Process.Landlock != nil { + landlock, err := SetupLandlock(spec.Process.Landlock) + if err != nil { + return nil, err + } + config.Landlock = landlock + } + } createHooks(spec, config) config.Version = specs.Version @@ -845,6 +854,55 @@ func parseMountOptions(options []string) (int, []int, string, int) { return flag, pgflag, strings.Join(data, ","), extFlags } +func SetupLandlock(ll *specs.Landlock) (*configs.Landlock, error) { + if ll == nil { + return nil, nil + } + + // No ruleset specified, assume landlock disabled. + if ll.Ruleset == nil || len(ll.Ruleset.HandledAccessFS) == 0 { + return nil, nil + } + + newConfig := &configs.Landlock{ + Ruleset: new(configs.Ruleset), + Rules: &configs.Rules{ + PathBeneath: []*configs.RulePathBeneath{}, + }, + DisableBestEffort: ll.DisableBestEffort, + } + + for _, access := range ll.Ruleset.HandledAccessFS { + newAccessFs, err := landlock.ConvertStringToAccessFS(string(access)) + if err != nil { + return nil, err + } + newConfig.Ruleset.HandledAccessFS |= newAccessFs + } + + // Loop through all Landlock path beneath rule blocks and convert them to libcontainer format. + for _, rulePath := range ll.Rules.PathBeneath { + if len(rulePath.AllowedAccess) > 0 { + newRule := configs.RulePathBeneath{ + AllowedAccess: 0, + Paths: rulePath.Paths, + } + + for _, access := range rulePath.AllowedAccess { + newAllowedAccess, err := landlock.ConvertStringToAccessFS(string(access)) + if err != nil { + return nil, err + } + newRule.AllowedAccess |= newAllowedAccess + } + + newConfig.Rules.PathBeneath = append(newConfig.Rules.PathBeneath, &newRule) + } + } + + return newConfig, nil +} + func SetupSeccomp(config *specs.LinuxSeccomp) (*configs.Seccomp, error) { if config == nil { return nil, nil diff --git a/libcontainer/standard_init_linux.go b/libcontainer/standard_init_linux.go index 6dfea99983a..15a1e7d7f1b 100644 --- a/libcontainer/standard_init_linux.go +++ b/libcontainer/standard_init_linux.go @@ -16,6 +16,7 @@ import ( "github.com/opencontainers/runc/libcontainer/apparmor" "github.com/opencontainers/runc/libcontainer/configs" "github.com/opencontainers/runc/libcontainer/keys" + "github.com/opencontainers/runc/libcontainer/landlock" "github.com/opencontainers/runc/libcontainer/seccomp" "github.com/opencontainers/runc/libcontainer/system" ) @@ -231,6 +232,13 @@ func (l *linuxStandardInit) Init() error { // https://github.com/torvalds/linux/blob/v4.9/fs/exec.c#L1290-L1318 _ = unix.Close(l.fifoFd) + // `noNewPrivileges` must be enabled to use Landlock. + if l.config.Config.Landlock != nil && l.config.NoNewPrivileges { + if err := landlock.InitLandlock(l.config.Config.Landlock); err != nil { + return fmt.Errorf("unable to init Landlock: %w", err) + } + } + s := l.config.SpecState s.Pid = unix.Getpid() s.Status = specs.StateCreated From 6f64c97df8eee754959ff1eaec21396979d5983a Mon Sep 17 00:00:00 2001 From: Kailun Qin Date: Fri, 3 Sep 2021 07:28:48 -0400 Subject: [PATCH 2/5] Address comments * use landlock.AccessFSSet type directly * remove non-linux files * some minor updates Signed-off-by: Kailun Qin --- libcontainer/configs/config.go | 34 +++------------- libcontainer/landlock/config.go | 40 ++++++++++--------- .../{landlock_linux.go => landlock.go} | 23 ++++------- libcontainer/landlock/landlock_unsupported.go | 19 --------- libcontainer/specconv/spec_linux.go | 18 ++++----- libcontainer/standard_init_linux.go | 2 +- 6 files changed, 44 insertions(+), 92 deletions(-) rename libcontainer/landlock/{landlock_linux.go => landlock.go} (70%) delete mode 100644 libcontainer/landlock/landlock_unsupported.go diff --git a/libcontainer/configs/config.go b/libcontainer/configs/config.go index e6278a029a6..ee7506d92fb 100644 --- a/libcontainer/configs/config.go +++ b/libcontainer/configs/config.go @@ -9,6 +9,7 @@ import ( "github.com/sirupsen/logrus" + "github.com/landlock-lsm/go-landlock/landlock" "github.com/opencontainers/runc/libcontainer/devices" "github.com/opencontainers/runtime-spec/specs-go" ) @@ -90,7 +91,7 @@ type Landlock struct { // Ruleset identifies a set of rules (i.e., actions on objects) that need to be handled in Landlock. type Ruleset struct { - HandledAccessFS AccessFS `json:"handledAccessFS"` + HandledAccessFS landlock.AccessFSSet `json:"handledAccessFS"` } // Rules represents the security policies (i.e., actions allowed on objects) in Landlock. @@ -99,35 +100,12 @@ type Rules struct { } // RulePathBeneath defines the file-hierarchy typed rule that grants the access rights specified by -// `AllowedAccess` to the file hierarchies under the given `Paths` in Landlock. +// AllowedAccess to the file hierarchies under the given Paths in Landlock. type RulePathBeneath struct { - AllowedAccess AccessFS `json:"allowedAccess"` - Paths []string `json:"paths"` + AllowedAccess landlock.AccessFSSet `json:"allowedAccess"` + Paths []string `json:"paths"` } -// AccessFS is taken upon ruleset and rule setup in Landlock. -type AccessFS uint64 - -// Landlock access rights for FS. -// -// Please see the full documentation at -// https://www.kernel.org/doc/html/latest/userspace-api/landlock.html#access-rights. -const ( - Execute AccessFS = (1 << 0) - WriteFile AccessFS = (1 << 1) - ReadFile AccessFS = (1 << 2) - ReadDir AccessFS = (1 << 3) - RemoveDir AccessFS = (1 << 4) - RemoveFile AccessFS = (1 << 5) - MakeChar AccessFS = (1 << 6) - MakeDir AccessFS = (1 << 7) - MakeReg AccessFS = (1 << 8) - MakeSock AccessFS = (1 << 9) - MakeFifo AccessFS = (1 << 10) - MakeBlock AccessFS = (1 << 11) - MakeSym AccessFS = (1 << 12) -) - // TODO Windows. Many of these fields should be factored out into those parts // which are common across platforms, and those which are platform specific. @@ -258,7 +236,7 @@ type Config struct { RootlessCgroups bool `json:"rootless_cgroups,omitempty"` // Landlock specifies the Landlock unprivileged access control settings for the container process. - // `noNewPrivileges` must be enabled to use Landlock. + // NoNewPrivileges must be enabled to use Landlock. Landlock *Landlock `json:"landlock,omitempty"` } diff --git a/libcontainer/landlock/config.go b/libcontainer/landlock/config.go index c91ef4d3720..b2b4302b0ec 100644 --- a/libcontainer/landlock/config.go +++ b/libcontainer/landlock/config.go @@ -3,28 +3,32 @@ package landlock import ( "fmt" - "github.com/opencontainers/runc/libcontainer/configs" + "github.com/landlock-lsm/go-landlock/landlock" + ll "github.com/landlock-lsm/go-landlock/landlock/syscall" ) -var accessFSs = map[string]configs.AccessFS{ - "execute": configs.Execute, - "write_file": configs.WriteFile, - "read_file": configs.ReadFile, - "read_dir": configs.ReadDir, - "remove_dir": configs.RemoveDir, - "remove_file": configs.RemoveFile, - "make_char": configs.MakeChar, - "make_dir": configs.MakeDir, - "make_reg": configs.MakeReg, - "make_sock": configs.MakeSock, - "make_fifo": configs.MakeFifo, - "make_block": configs.MakeBlock, - "make_sym": configs.MakeSym, +var accessFSSets = map[string]landlock.AccessFSSet{ + "execute": ll.AccessFSExecute, + "write_file": ll.AccessFSWriteFile, + "read_file": ll.AccessFSReadFile, + "read_dir": ll.AccessFSReadDir, + "remove_dir": ll.AccessFSRemoveDir, + "remove_file": ll.AccessFSRemoveFile, + "make_char": ll.AccessFSMakeChar, + "make_dir": ll.AccessFSMakeDir, + "make_reg": ll.AccessFSMakeReg, + "make_sock": ll.AccessFSMakeSock, + "make_fifo": ll.AccessFSMakeFifo, + "make_block": ll.AccessFSMakeBlock, + "make_sym": ll.AccessFSMakeSym, } -// ConvertStringToAccessFS converts a string into a Landlock access right. -func ConvertStringToAccessFS(in string) (configs.AccessFS, error) { - if access, ok := accessFSs[in]; ok { +// ConvertStringToAccessFSSet converts a string into a go-landlock AccessFSSet +// access right. +// This gives more explicit control over the mapping between the permitted +// values in the spec and the ones supported in go-landlock library. +func ConvertStringToAccessFSSet(in string) (landlock.AccessFSSet, error) { + if access, ok := accessFSSets[in]; ok { return access, nil } return 0, fmt.Errorf("string %s is not a valid access right for landlock", in) diff --git a/libcontainer/landlock/landlock_linux.go b/libcontainer/landlock/landlock.go similarity index 70% rename from libcontainer/landlock/landlock_linux.go rename to libcontainer/landlock/landlock.go index 158f23cc126..c15ef8a4c2b 100644 --- a/libcontainer/landlock/landlock_linux.go +++ b/libcontainer/landlock/landlock.go @@ -1,5 +1,3 @@ -// +build linux - package landlock import ( @@ -25,7 +23,7 @@ func InitLandlock(config *configs.Landlock) error { var llConfig landlock.Config - ruleset := getAccess(config.Ruleset.HandledAccessFS) + ruleset := config.Ruleset.HandledAccessFS // Panic on error when constructing the Landlock configuration using invalid config values. if config.DisableBestEffort { llConfig = landlock.MustConfig(ruleset) @@ -34,32 +32,25 @@ func InitLandlock(config *configs.Landlock) error { } if err := llConfig.RestrictPaths( - getPathAccesses(config.Rules)..., + pathAccesses(config.Rules)..., ); err != nil { - return fmt.Errorf("Could not restrict paths: %v", err) + return fmt.Errorf("could not restrict paths: %w", err) } return nil } -// Convert Libcontainer AccessFS to go-landlock AccessFSSet. -func getAccess(access configs.AccessFS) landlock.AccessFSSet { - return landlock.AccessFSSet(access) -} - // Convert Libcontainer RulePathBeneath to go-landlock PathOpt. -func getPathAccess(rule *configs.RulePathBeneath) landlock.PathOpt { - return landlock.PathAccess( - getAccess(rule.AllowedAccess), - rule.Paths...) +func pathAccess(rule *configs.RulePathBeneath) landlock.PathOpt { + return landlock.PathAccess(rule.AllowedAccess, rule.Paths...) } // Convert Libcontainer Rules to an array of go-landlock PathOpt. -func getPathAccesses(rules *configs.Rules) []landlock.PathOpt { +func pathAccesses(rules *configs.Rules) []landlock.PathOpt { pathAccesses := []landlock.PathOpt{} for _, rule := range rules.PathBeneath { - opt := getPathAccess(rule) + opt := pathAccess(rule) pathAccesses = append(pathAccesses, opt) } diff --git a/libcontainer/landlock/landlock_unsupported.go b/libcontainer/landlock/landlock_unsupported.go deleted file mode 100644 index e2fe222ee99..00000000000 --- a/libcontainer/landlock/landlock_unsupported.go +++ /dev/null @@ -1,19 +0,0 @@ -// +build !linux - -package landlock - -import ( - "errors" - - "github.com/opencontainers/runc/libcontainer/configs" -) - -var ErrLandlockNotSupported = errors.New("land: config provided but Landlock not supported") - -// InitLandlock does nothing because Landlock is not supported. -func InitSLandlock(config *configs.Landlock) error { - if config != nil { - return ErrLandlockNotSupported - } - return nil -} diff --git a/libcontainer/specconv/spec_linux.go b/libcontainer/specconv/spec_linux.go index 25c2b96434c..18266ae37ab 100644 --- a/libcontainer/specconv/spec_linux.go +++ b/libcontainer/specconv/spec_linux.go @@ -320,14 +320,12 @@ func CreateLibcontainerConfig(opts *CreateOpts) (*configs.Config, error) { Ambient: spec.Process.Capabilities.Ambient, } } - if spec.Process.Landlock != nil { - landlock, err := SetupLandlock(spec.Process.Landlock) - if err != nil { - return nil, err - } - config.Landlock = landlock - } + landlock, err := SetupLandlock(spec.Process.Landlock) + if err != nil { + return nil, err + } + config.Landlock = landlock } createHooks(spec, config) config.Version = specs.Version @@ -873,11 +871,11 @@ func SetupLandlock(ll *specs.Landlock) (*configs.Landlock, error) { } for _, access := range ll.Ruleset.HandledAccessFS { - newAccessFs, err := landlock.ConvertStringToAccessFS(string(access)) + newAccessFS, err := landlock.ConvertStringToAccessFSSet(string(access)) if err != nil { return nil, err } - newConfig.Ruleset.HandledAccessFS |= newAccessFs + newConfig.Ruleset.HandledAccessFS |= newAccessFS } // Loop through all Landlock path beneath rule blocks and convert them to libcontainer format. @@ -889,7 +887,7 @@ func SetupLandlock(ll *specs.Landlock) (*configs.Landlock, error) { } for _, access := range rulePath.AllowedAccess { - newAllowedAccess, err := landlock.ConvertStringToAccessFS(string(access)) + newAllowedAccess, err := landlock.ConvertStringToAccessFSSet(string(access)) if err != nil { return nil, err } diff --git a/libcontainer/standard_init_linux.go b/libcontainer/standard_init_linux.go index 15a1e7d7f1b..2d0be66a57c 100644 --- a/libcontainer/standard_init_linux.go +++ b/libcontainer/standard_init_linux.go @@ -232,7 +232,7 @@ func (l *linuxStandardInit) Init() error { // https://github.com/torvalds/linux/blob/v4.9/fs/exec.c#L1290-L1318 _ = unix.Close(l.fifoFd) - // `noNewPrivileges` must be enabled to use Landlock. + // NoNewPrivileges must be enabled to use Landlock. if l.config.Config.Landlock != nil && l.config.NoNewPrivileges { if err := landlock.InitLandlock(l.config.Config.Landlock); err != nil { return fmt.Errorf("unable to init Landlock: %w", err) From 6a698a60ac1cc47a1e7208cd5660fb5f8b6358f6 Mon Sep 17 00:00:00 2001 From: Kailun Qin Date: Fri, 3 Sep 2021 11:03:47 -0400 Subject: [PATCH 3/5] Add unit test for SetupLandlock Signed-off-by: Kailun Qin --- libcontainer/specconv/spec_linux_test.go | 99 ++++++++++++++++++++++++ 1 file changed, 99 insertions(+) diff --git a/libcontainer/specconv/spec_linux_test.go b/libcontainer/specconv/spec_linux_test.go index 963d803a6a2..40321145161 100644 --- a/libcontainer/specconv/spec_linux_test.go +++ b/libcontainer/specconv/spec_linux_test.go @@ -2,10 +2,12 @@ package specconv import ( "os" + "reflect" "strings" "testing" dbus "github.com/godbus/dbus/v5" + ll "github.com/landlock-lsm/go-landlock/landlock" "github.com/opencontainers/runc/libcontainer/configs" "github.com/opencontainers/runc/libcontainer/configs/validate" "github.com/opencontainers/runc/libcontainer/devices" @@ -185,6 +187,103 @@ func TestSetupSeccompWrongArchitecture(t *testing.T) { } } +func TestSetupLandlock(t *testing.T) { + conf := &specs.Landlock{ + Ruleset: &specs.LandlockRuleset{ + HandledAccessFS: []specs.LandlockFSAction{ + specs.FSActExecute, + specs.FSActWriteFile, + specs.FSActReadFile, + specs.FSActReadDir, + specs.FSActRemoveDir, + specs.FSActRemoveFile, + specs.FSActMakeChar, + specs.FSActMakeDir, + specs.FSActMakeReg, + specs.FSActMakeSock, + specs.FSActMakeFifo, + specs.FSActMakeBlock, + specs.FSActMakeSym, + }, + }, + Rules: &specs.LandlockRules{ + PathBeneath: []specs.LandlockRulePathBeneath{ + { + AllowedAccess: []specs.LandlockFSAction{ + specs.FSActExecute, + specs.FSActReadFile, + specs.FSActReadDir, + }, + Paths: []string{ + "/usr", + "/bin", + }, + }, + { + AllowedAccess: []specs.LandlockFSAction{ + specs.FSActExecute, + specs.FSActWriteFile, + specs.FSActReadFile, + specs.FSActRemoveFile, + specs.FSActMakeChar, + specs.FSActMakeReg, + specs.FSActMakeSock, + specs.FSActMakeFifo, + specs.FSActMakeBlock, + specs.FSActMakeSym, + }, + Paths: []string{ + "/tmp", + }, + }, + }, + }, + DisableBestEffort: false, + } + + landlock, err := SetupLandlock(conf) + if err != nil { + t.Errorf("Couldn't create Landlock config: %v", err) + } + + // Execute | WriteFile | ReadFile | ReadDir | RemoveDir | RemoveFile | MakeChar | + // MakeDir | MakeReg | MakeSock | MakeFifo | MakeBlock | MakeSym + expectedRulesetAccess := ll.AccessFSSet(0x1FFF) + ruleset := landlock.Ruleset + if ruleset.HandledAccessFS != expectedRulesetAccess { + t.Errorf("Expected ruleset not found, expected %v, got: %v", + expectedRulesetAccess, ruleset.HandledAccessFS) + } + + pathRules := landlock.Rules.PathBeneath + + pathRulesLength := len(pathRules) + if pathRulesLength != 2 { + t.Errorf("Expected 2 path beneath rules, got :%d", pathRulesLength) + } + + expectedPathRulesAccess := []configs.RulePathBeneath{ + { + // Execute | ReadFile | ReadDir + AllowedAccess: 0xD, + Paths: []string{"/usr", "/bin"}, + }, + { + // Execute | WriteFile | ReadFile | RemoveFile | MakeChar | MakeReg | MakeSock | MakeFifo | + // MakeBlock | MakeSym + AllowedAccess: 0x1F67, + Paths: []string{"/tmp"}, + }, + } + + for i, rule := range pathRules { + if !reflect.DeepEqual(*rule, expectedPathRulesAccess[i]) { + t.Errorf("Wrong rule conversion for the rule %d under test, expected %v, got: %v", + i, expectedPathRulesAccess[i], rule) + } + } +} + func TestSetupSeccomp(t *testing.T) { errnoRet := uint(55) conf := &specs.LinuxSeccomp{ From 22802a8d35decb60791a6852f23bf43298bd7383 Mon Sep 17 00:00:00 2001 From: Kailun Qin Date: Fri, 3 Sep 2021 11:14:15 -0400 Subject: [PATCH 4/5] Add check for DisableBestEffort in the unit test Signed-off-by: Kailun Qin --- libcontainer/specconv/spec_linux_test.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/libcontainer/specconv/spec_linux_test.go b/libcontainer/specconv/spec_linux_test.go index 40321145161..63361106acd 100644 --- a/libcontainer/specconv/spec_linux_test.go +++ b/libcontainer/specconv/spec_linux_test.go @@ -282,6 +282,10 @@ func TestSetupLandlock(t *testing.T) { i, expectedPathRulesAccess[i], rule) } } + + if landlock.DisableBestEffort { + t.Error("Wrong conversion for DisableBestEffort") + } } func TestSetupSeccomp(t *testing.T) { From 075542f99e501ac15c5500febf3e4df8e70f13c0 Mon Sep 17 00:00:00 2001 From: Kailun Qin Date: Thu, 9 Sep 2021 09:49:35 -0400 Subject: [PATCH 5/5] Update go-landlock and use NewConfig instead Signed-off-by: Kailun Qin --- go.mod | 2 +- go.sum | 4 ++-- libcontainer/landlock/landlock.go | 14 +++++++------- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/go.mod b/go.mod index dfeb440c16c..e849271220f 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/cyphar/filepath-securejoin v0.2.3 github.com/docker/go-units v0.4.0 github.com/godbus/dbus/v5 v5.0.4 - github.com/landlock-lsm/go-landlock v0.0.0-20210828133255-ec6c6b87a946 + github.com/landlock-lsm/go-landlock v0.0.0-20210908180355-c56710719da4 github.com/moby/sys/mountinfo v0.4.1 github.com/mrunalp/fileutils v0.5.0 github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417 diff --git a/go.sum b/go.sum index 39ac0e0726c..59568008d61 100644 --- a/go.sum +++ b/go.sum @@ -40,8 +40,8 @@ github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfn github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/landlock-lsm/go-landlock v0.0.0-20210828133255-ec6c6b87a946 h1:RRTOwBnwZR4a3IMyPq1uchxJcrLKWF4NTCHB2fbvo5Y= -github.com/landlock-lsm/go-landlock v0.0.0-20210828133255-ec6c6b87a946/go.mod h1:wjznJ04q4Tvsbx3vkzfmgfEOe6w5dSGlXFa+xbSl9X8= +github.com/landlock-lsm/go-landlock v0.0.0-20210908180355-c56710719da4 h1:5FNPB9FxONNZ10VtNC2n15+0O4O6wfCqCBmkxm2O5x0= +github.com/landlock-lsm/go-landlock v0.0.0-20210908180355-c56710719da4/go.mod h1:wjznJ04q4Tvsbx3vkzfmgfEOe6w5dSGlXFa+xbSl9X8= github.com/moby/sys/mountinfo v0.4.1 h1:1O+1cHA1aujwEwwVMa2Xm2l+gIpUHyd3+D+d7LZh1kM= github.com/moby/sys/mountinfo v0.4.1/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A= github.com/mrunalp/fileutils v0.5.0 h1:NKzVxiH7eSk+OQ4M+ZYW1K6h27RUV3MI6NUTsHhU6Z4= diff --git a/libcontainer/landlock/landlock.go b/libcontainer/landlock/landlock.go index c15ef8a4c2b..52b7aa49a02 100644 --- a/libcontainer/landlock/landlock.go +++ b/libcontainer/landlock/landlock.go @@ -21,14 +21,14 @@ func InitLandlock(config *configs.Landlock) error { return errors.New("cannot initialize Landlock - nil config passed") } - var llConfig landlock.Config - ruleset := config.Ruleset.HandledAccessFS - // Panic on error when constructing the Landlock configuration using invalid config values. - if config.DisableBestEffort { - llConfig = landlock.MustConfig(ruleset) - } else { - llConfig = landlock.MustConfig(ruleset).BestEffort() + llConfig, err := landlock.NewConfig(ruleset) + if err != nil { + return fmt.Errorf("could not create ruleset: %w", err) + } + + if !config.DisableBestEffort { + *llConfig = llConfig.BestEffort() } if err := llConfig.RestrictPaths(