From 8acad9054fc3a129c7a755914357974ddf807670 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=BCnther=20Noack?= Date: Sun, 2 Jun 2024 17:44:33 +0200 Subject: [PATCH] Landlock ABI v5 support (IOCTL on device files) Make ioctl(2) requests for device files restrictable with Landlock. In the Go library, the LANDLOCK_ACCESS_FS_IOCTL_DEV right is *not* part of the RWFiles and ROFiles convenience functions. When you upgrade from an earlier ABI version to `landlock.V5`, and when you are restricting all access rights available at this version, please double check whether your program uses any IOCTLs on device files. Some of the simpler IOCTL commands are exempt and are unconditionally permitted by Landlock. Fixes: #29 Link: https://lore.kernel.org/linux-security-module/20240419161122.2023765-1-gnoack@google.com/ --- cmd/landlock-restrict/main.go | 29 ++++++++++++++++----- landlock/abi_versions.go | 5 ++++ landlock/abi_versions_test.go | 2 +- landlock/accessfs.go | 1 + landlock/config.go | 2 ++ landlock/config_test.go | 2 +- landlock/landlock.go | 14 +++++----- landlock/path_opt.go | 24 +++++++++++++++++ landlock/restrict_test.go | 49 +++++++++++++++++++++++++++++++++++ landlock/syscall/landlock.go | 1 + 10 files changed, 113 insertions(+), 16 deletions(-) diff --git a/cmd/landlock-restrict/main.go b/cmd/landlock-restrict/main.go index 3348055..2e47297 100644 --- a/cmd/landlock-restrict/main.go +++ b/cmd/landlock-restrict/main.go @@ -11,15 +11,19 @@ import ( ) func parseFlags(args []string) (verbose bool, cfg landlock.Config, opts []landlock.Rule, cmd []string) { - cfg = landlock.V3 + cfg = landlock.V5 takeArgs := func(makeOpt func(...string) landlock.FSRule) landlock.Rule { var paths []string needRefer := false + needIoctlDev := false for len(args) > 0 && !strings.HasPrefix(args[0], "-") { - if args[0] == "+refer" { + switch args[0] { + case "+refer": needRefer = true - } else { + case "+ioctl_dev": + needIoctlDev = true + default: paths = append(paths, args[0]) } args = args[1:] @@ -31,6 +35,9 @@ func parseFlags(args []string) (verbose bool, cfg landlock.Config, opts []landlo if needRefer { opt = opt.WithRefer() } + if needIoctlDev { + opt = opt.WithIoctlDev() + } if verbose { fmt.Println("Path option:", opt) } @@ -41,6 +48,14 @@ func parseFlags(args []string) (verbose bool, cfg landlock.Config, opts []landlo ArgParsing: for len(args) > 0 { switch args[0] { + case "-5": + cfg = landlock.V5 + args = args[1:] + continue + case "-4": + cfg = landlock.V4 + args = args[1:] + continue case "-3": cfg = landlock.V3 args = args[1:] @@ -107,20 +122,20 @@ func main() { fmt.Println("Usage:") fmt.Println(" landlock-restrict") fmt.Println(" [-v]") - fmt.Println(" [-1] [-2] [-3] [-strict]") - fmt.Println(" [-ro [+refer] PATH...] [-rw [+refer] PATH...]") + fmt.Println(" [-1] [-2] [-3] [-4] [-5] [-strict]") + fmt.Println(" [-ro [+refer] PATH...] [-rw [+refer] [+ioctl_dev] PATH...]") fmt.Println(" [-rofiles [+refer] PATH] [-rwfiles [+refer] PATH]") fmt.Println(" -- COMMAND...") fmt.Println() fmt.Println("Options:") fmt.Println(" -ro, -rw, -rofiles, -rwfiles paths to restrict to") - fmt.Println(" -1, -2, -3 select Landlock version") + fmt.Println(" -1, -2, -3, -4, -5 select Landlock version") fmt.Println(" -strict use strict mode (instead of best effort)") fmt.Println(" -v verbose logging") fmt.Println() fmt.Println("A path list that contains the word '+refer' will additionally grant the refer access right.") fmt.Println() - fmt.Println("Default mode for Landlock is V3 in best effort mode (best compatibility)") + fmt.Println("Default mode for Landlock is V5 in best effort mode (best compatibility)") fmt.Println() log.Fatalf("Need proper command, got %v", cmdArgs) diff --git a/landlock/abi_versions.go b/landlock/abi_versions.go index 4f67979..2c1a662 100644 --- a/landlock/abi_versions.go +++ b/landlock/abi_versions.go @@ -30,6 +30,11 @@ var abiInfos = []abiInfo{ supportedAccessFS: (1 << 15) - 1, supportedAccessNet: (1 << 2) - 1, }, + { + version: 5, + supportedAccessFS: (1 << 16) - 1, + supportedAccessNet: (1 << 2) - 1, + }, } func (a abiInfo) asConfig() Config { diff --git a/landlock/abi_versions_test.go b/landlock/abi_versions_test.go index a80c0ae..c5f781e 100644 --- a/landlock/abi_versions_test.go +++ b/landlock/abi_versions_test.go @@ -13,7 +13,7 @@ func TestAbiVersionsIncrementing(t *testing.T) { } func TestSupportedAccessFS(t *testing.T) { - got := abiInfos[4].supportedAccessFS + got := abiInfos[5].supportedAccessFS want := supportedAccessFS if got != want { diff --git a/landlock/accessfs.go b/landlock/accessfs.go index 79d81a7..28ad8c2 100644 --- a/landlock/accessfs.go +++ b/landlock/accessfs.go @@ -21,6 +21,7 @@ var accessFSNames = []string{ "make_sym", "refer", "truncate", + "ioctl_dev", } // AccessFSSet is a set of Landlockable file system access operations. diff --git a/landlock/config.go b/landlock/config.go index d8a7502..6f54cd7 100644 --- a/landlock/config.go +++ b/landlock/config.go @@ -37,6 +37,8 @@ var ( V3 = abiInfos[3].asConfig() // Landlock V4 support (V3 + networking) V4 = abiInfos[4].asConfig() + // Landlock V5 support (V4 + ioctl on device files) + V5 = abiInfos[5].asConfig() ) // v0 denotes "no Landlock support". Only used internally. diff --git a/landlock/config_test.go b/landlock/config_test.go index e80b0b2..7b4324a 100644 --- a/landlock/config_test.go +++ b/landlock/config_test.go @@ -79,7 +79,7 @@ func TestNewConfigFailures(t *testing.T) { // May not specify two AccessFSSets {AccessFSSet(ll.AccessFSWriteFile), AccessFSSet(ll.AccessFSReadFile)}, // May not specify an unsupported AccessFSSet value - {AccessFSSet(1 << 15)}, + {AccessFSSet(1 << 16)}, {AccessFSSet(1 << 63)}, } { _, err := NewConfig(args...) diff --git a/landlock/landlock.go b/landlock/landlock.go index c7a530f..0e8a0f2 100644 --- a/landlock/landlock.go +++ b/landlock/landlock.go @@ -5,12 +5,12 @@ // The following invocation will restrict all goroutines so that they // can only read from /usr, /bin and /tmp, and only write to /tmp: // -// err := landlock.V4.BestEffort().RestrictPaths( +// err := landlock.V5.BestEffort().RestrictPaths( // landlock.RODirs("/usr", "/bin"), // landlock.RWDirs("/tmp"), // ) // -// This will restrict file access using Landlock V4, if available. If +// This will restrict file access using Landlock V5, if available. If // unavailable, it will attempt using earlier Landlock versions than // the one requested. If no Landlock version is available, it will // still succeed, without restricting file accesses. @@ -20,12 +20,12 @@ // The following invocation will restrict all goroutines so that they // can only bind to TCP port 8080 and only connect to TCP port 53: // -// err := landlock.V4.BestEffort().RestrictNet( +// err := landlock.V5.BestEffort().RestrictNet( // landlock.BindTCP(8080), // landlock.ConnectTCP(53), // ) // -// This functionality is available since Landlock V4. +// This functionality is available since Landlock V5. // // # Restricting file access and networking at once // @@ -33,7 +33,7 @@ // once. The effect is the same as calling [Config.RestrictPaths] and // [Config.RestrictNet] one after another, but it happens in one step. // -// err := landlock.V4.BestEffort().Restrict( +// err := landlock.V5.BestEffort().Restrict( // landlock.RODirs("/usr", "/bin"), // landlock.RWDirs("/tmp"), // landlock.BindTCP(8080), @@ -42,9 +42,9 @@ // // # More possible invocations // -// landlock.V4.RestrictPaths(...) (without the call to +// landlock.V5.RestrictPaths(...) (without the call to // [Config.BestEffort]) enforces the given rules using the -// capabilities of Landlock V4, but returns an error if that +// capabilities of Landlock V5, but returns an error if that // functionality is not available on the system that the program is // running on. // diff --git a/landlock/path_opt.go b/landlock/path_opt.go index 9ad3bcb..c35b28e 100644 --- a/landlock/path_opt.go +++ b/landlock/path_opt.go @@ -39,6 +39,14 @@ func (r FSRule) WithRefer() FSRule { return r.withRights(ll.AccessFSRefer) } +// WithIoctlDev adds the "ioctl dev" access right to a FSRule. +// +// It is uncommon to need this access right, so it is not part of +// [RWFiles] or [RWDirs]. +func (r FSRule) WithIoctlDev() FSRule { + return r.withRights(ll.AccessFSIoctlDev) +} + // IgnoreIfMissing gracefully ignores missing paths. // // Under normal circumstances, referring to a non-existing path in a rule would @@ -128,6 +136,16 @@ func RODirs(paths ...string) FSRule { // RWDirs is a [Rule] which grants full (read and write) access to // files and directories under the given paths. +// +// Noteworthy operations which are *not* covered by RWDirs: +// +// - RWDirs does *not* grant the right to *reparent or link* files +// across different directories. If this access right is +// required, use [FSRule.WithRefer]. +// +// - RWDirs does *not* grant the right to *use IOCTL* on device +// files. If this access right is required, use +// [FSRule.WithIoctlDev]. func RWDirs(paths ...string) FSRule { return FSRule{ accessFS: accessFSReadWrite, @@ -150,6 +168,12 @@ func ROFiles(paths ...string) FSRule { // RWFiles is a [Rule] which grants common read and write access to // files under the given paths, but it does not permit access to // directories. +// +// Noteworthy operations which are *not* covered by RWFiles: +// +// - RWFiles does *not* grant the right to *use IOCTL* on device +// files. If this access right is required, use +// [FSRule.WithIoctlDev]. func RWFiles(paths ...string) FSRule { return FSRule{ accessFS: accessFSReadWrite & accessFile, diff --git a/landlock/restrict_test.go b/landlock/restrict_test.go index 02600b9..255c376 100644 --- a/landlock/restrict_test.go +++ b/landlock/restrict_test.go @@ -451,6 +451,55 @@ func tryListen(port int) error { return err } +func TestIoctlDev(t *testing.T) { + const ( + path = "/dev/zero" + FIONREAD = 0x541b + ) + for _, tt := range []struct { + Name string + Rule landlock.Rule + WantErr error + }{ + { + Name: "WithoutIoctlDev", + Rule: landlock.RWFiles(path), + WantErr: syscall.EACCES, + }, + { + Name: "WithIoctlDev", + Rule: landlock.RWFiles(path).WithIoctlDev(), + // ENOTTY means that the IOCTL was dispatched + // to device. (Would be nicer to find an + // IOCTL that returns success here, but the + // available devices on qemu are limited.) + WantErr: syscall.ENOTTY, + }, + } { + t.Run(tt.Name, func(t *testing.T) { + RunInSubprocess(t, func() { + RequireLandlockABI(t, 5) + + err := landlock.V5.BestEffort().RestrictPaths(tt.Rule) + if err != nil { + t.Fatalf("Enabling Landlock: %v", err) + } + + f, err := os.Open(path) + if err != nil { + t.Fatalf("os.Open(%q): %v", path, err) + } + defer func() { f.Close() }() + + _, err = unix.IoctlGetInt(int(f.Fd()), FIONREAD) + if !errEqual(err, tt.WantErr) { + t.Errorf("ioctl(%v, FIONREAD): got err «%v», want «%v»", f, err, tt.WantErr) + } + }) + }) + } +} + func errEqual(got, want error) bool { if got == nil && want == nil { return true diff --git a/landlock/syscall/landlock.go b/landlock/syscall/landlock.go index f12dad6..6303988 100644 --- a/landlock/syscall/landlock.go +++ b/landlock/syscall/landlock.go @@ -34,6 +34,7 @@ const ( AccessFSMakeSym AccessFSRefer AccessFSTruncate + AccessFSIoctlDev ) // Landlock network access rights.