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.