Skip to content

Commit 8acad90

Browse files
committed
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/
1 parent 3b15ce3 commit 8acad90

File tree

10 files changed

+113
-16
lines changed

10 files changed

+113
-16
lines changed

cmd/landlock-restrict/main.go

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,19 @@ import (
1111
)
1212

1313
func parseFlags(args []string) (verbose bool, cfg landlock.Config, opts []landlock.Rule, cmd []string) {
14-
cfg = landlock.V3
14+
cfg = landlock.V5
1515

1616
takeArgs := func(makeOpt func(...string) landlock.FSRule) landlock.Rule {
1717
var paths []string
1818
needRefer := false
19+
needIoctlDev := false
1920
for len(args) > 0 && !strings.HasPrefix(args[0], "-") {
20-
if args[0] == "+refer" {
21+
switch args[0] {
22+
case "+refer":
2123
needRefer = true
22-
} else {
24+
case "+ioctl_dev":
25+
needIoctlDev = true
26+
default:
2327
paths = append(paths, args[0])
2428
}
2529
args = args[1:]
@@ -31,6 +35,9 @@ func parseFlags(args []string) (verbose bool, cfg landlock.Config, opts []landlo
3135
if needRefer {
3236
opt = opt.WithRefer()
3337
}
38+
if needIoctlDev {
39+
opt = opt.WithIoctlDev()
40+
}
3441
if verbose {
3542
fmt.Println("Path option:", opt)
3643
}
@@ -41,6 +48,14 @@ func parseFlags(args []string) (verbose bool, cfg landlock.Config, opts []landlo
4148
ArgParsing:
4249
for len(args) > 0 {
4350
switch args[0] {
51+
case "-5":
52+
cfg = landlock.V5
53+
args = args[1:]
54+
continue
55+
case "-4":
56+
cfg = landlock.V4
57+
args = args[1:]
58+
continue
4459
case "-3":
4560
cfg = landlock.V3
4661
args = args[1:]
@@ -107,20 +122,20 @@ func main() {
107122
fmt.Println("Usage:")
108123
fmt.Println(" landlock-restrict")
109124
fmt.Println(" [-v]")
110-
fmt.Println(" [-1] [-2] [-3] [-strict]")
111-
fmt.Println(" [-ro [+refer] PATH...] [-rw [+refer] PATH...]")
125+
fmt.Println(" [-1] [-2] [-3] [-4] [-5] [-strict]")
126+
fmt.Println(" [-ro [+refer] PATH...] [-rw [+refer] [+ioctl_dev] PATH...]")
112127
fmt.Println(" [-rofiles [+refer] PATH] [-rwfiles [+refer] PATH]")
113128
fmt.Println(" -- COMMAND...")
114129
fmt.Println()
115130
fmt.Println("Options:")
116131
fmt.Println(" -ro, -rw, -rofiles, -rwfiles paths to restrict to")
117-
fmt.Println(" -1, -2, -3 select Landlock version")
132+
fmt.Println(" -1, -2, -3, -4, -5 select Landlock version")
118133
fmt.Println(" -strict use strict mode (instead of best effort)")
119134
fmt.Println(" -v verbose logging")
120135
fmt.Println()
121136
fmt.Println("A path list that contains the word '+refer' will additionally grant the refer access right.")
122137
fmt.Println()
123-
fmt.Println("Default mode for Landlock is V3 in best effort mode (best compatibility)")
138+
fmt.Println("Default mode for Landlock is V5 in best effort mode (best compatibility)")
124139
fmt.Println()
125140

126141
log.Fatalf("Need proper command, got %v", cmdArgs)

landlock/abi_versions.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ var abiInfos = []abiInfo{
3030
supportedAccessFS: (1 << 15) - 1,
3131
supportedAccessNet: (1 << 2) - 1,
3232
},
33+
{
34+
version: 5,
35+
supportedAccessFS: (1 << 16) - 1,
36+
supportedAccessNet: (1 << 2) - 1,
37+
},
3338
}
3439

3540
func (a abiInfo) asConfig() Config {

landlock/abi_versions_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ func TestAbiVersionsIncrementing(t *testing.T) {
1313
}
1414

1515
func TestSupportedAccessFS(t *testing.T) {
16-
got := abiInfos[4].supportedAccessFS
16+
got := abiInfos[5].supportedAccessFS
1717
want := supportedAccessFS
1818

1919
if got != want {

landlock/accessfs.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ var accessFSNames = []string{
2121
"make_sym",
2222
"refer",
2323
"truncate",
24+
"ioctl_dev",
2425
}
2526

2627
// AccessFSSet is a set of Landlockable file system access operations.

landlock/config.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ var (
3737
V3 = abiInfos[3].asConfig()
3838
// Landlock V4 support (V3 + networking)
3939
V4 = abiInfos[4].asConfig()
40+
// Landlock V5 support (V4 + ioctl on device files)
41+
V5 = abiInfos[5].asConfig()
4042
)
4143

4244
// v0 denotes "no Landlock support". Only used internally.

landlock/config_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ func TestNewConfigFailures(t *testing.T) {
7979
// May not specify two AccessFSSets
8080
{AccessFSSet(ll.AccessFSWriteFile), AccessFSSet(ll.AccessFSReadFile)},
8181
// May not specify an unsupported AccessFSSet value
82-
{AccessFSSet(1 << 15)},
82+
{AccessFSSet(1 << 16)},
8383
{AccessFSSet(1 << 63)},
8484
} {
8585
_, err := NewConfig(args...)

landlock/landlock.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@
55
// The following invocation will restrict all goroutines so that they
66
// can only read from /usr, /bin and /tmp, and only write to /tmp:
77
//
8-
// err := landlock.V4.BestEffort().RestrictPaths(
8+
// err := landlock.V5.BestEffort().RestrictPaths(
99
// landlock.RODirs("/usr", "/bin"),
1010
// landlock.RWDirs("/tmp"),
1111
// )
1212
//
13-
// This will restrict file access using Landlock V4, if available. If
13+
// This will restrict file access using Landlock V5, if available. If
1414
// unavailable, it will attempt using earlier Landlock versions than
1515
// the one requested. If no Landlock version is available, it will
1616
// still succeed, without restricting file accesses.
@@ -20,20 +20,20 @@
2020
// The following invocation will restrict all goroutines so that they
2121
// can only bind to TCP port 8080 and only connect to TCP port 53:
2222
//
23-
// err := landlock.V4.BestEffort().RestrictNet(
23+
// err := landlock.V5.BestEffort().RestrictNet(
2424
// landlock.BindTCP(8080),
2525
// landlock.ConnectTCP(53),
2626
// )
2727
//
28-
// This functionality is available since Landlock V4.
28+
// This functionality is available since Landlock V5.
2929
//
3030
// # Restricting file access and networking at once
3131
//
3232
// The following invocation restricts both file and network access at
3333
// once. The effect is the same as calling [Config.RestrictPaths] and
3434
// [Config.RestrictNet] one after another, but it happens in one step.
3535
//
36-
// err := landlock.V4.BestEffort().Restrict(
36+
// err := landlock.V5.BestEffort().Restrict(
3737
// landlock.RODirs("/usr", "/bin"),
3838
// landlock.RWDirs("/tmp"),
3939
// landlock.BindTCP(8080),
@@ -42,9 +42,9 @@
4242
//
4343
// # More possible invocations
4444
//
45-
// landlock.V4.RestrictPaths(...) (without the call to
45+
// landlock.V5.RestrictPaths(...) (without the call to
4646
// [Config.BestEffort]) enforces the given rules using the
47-
// capabilities of Landlock V4, but returns an error if that
47+
// capabilities of Landlock V5, but returns an error if that
4848
// functionality is not available on the system that the program is
4949
// running on.
5050
//

landlock/path_opt.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,14 @@ func (r FSRule) WithRefer() FSRule {
3939
return r.withRights(ll.AccessFSRefer)
4040
}
4141

42+
// WithIoctlDev adds the "ioctl dev" access right to a FSRule.
43+
//
44+
// It is uncommon to need this access right, so it is not part of
45+
// [RWFiles] or [RWDirs].
46+
func (r FSRule) WithIoctlDev() FSRule {
47+
return r.withRights(ll.AccessFSIoctlDev)
48+
}
49+
4250
// IgnoreIfMissing gracefully ignores missing paths.
4351
//
4452
// Under normal circumstances, referring to a non-existing path in a rule would
@@ -128,6 +136,16 @@ func RODirs(paths ...string) FSRule {
128136

129137
// RWDirs is a [Rule] which grants full (read and write) access to
130138
// files and directories under the given paths.
139+
//
140+
// Noteworthy operations which are *not* covered by RWDirs:
141+
//
142+
// - RWDirs does *not* grant the right to *reparent or link* files
143+
// across different directories. If this access right is
144+
// required, use [FSRule.WithRefer].
145+
//
146+
// - RWDirs does *not* grant the right to *use IOCTL* on device
147+
// files. If this access right is required, use
148+
// [FSRule.WithIoctlDev].
131149
func RWDirs(paths ...string) FSRule {
132150
return FSRule{
133151
accessFS: accessFSReadWrite,
@@ -150,6 +168,12 @@ func ROFiles(paths ...string) FSRule {
150168
// RWFiles is a [Rule] which grants common read and write access to
151169
// files under the given paths, but it does not permit access to
152170
// directories.
171+
//
172+
// Noteworthy operations which are *not* covered by RWFiles:
173+
//
174+
// - RWFiles does *not* grant the right to *use IOCTL* on device
175+
// files. If this access right is required, use
176+
// [FSRule.WithIoctlDev].
153177
func RWFiles(paths ...string) FSRule {
154178
return FSRule{
155179
accessFS: accessFSReadWrite & accessFile,

landlock/restrict_test.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -451,6 +451,55 @@ func tryListen(port int) error {
451451
return err
452452
}
453453

454+
func TestIoctlDev(t *testing.T) {
455+
const (
456+
path = "/dev/zero"
457+
FIONREAD = 0x541b
458+
)
459+
for _, tt := range []struct {
460+
Name string
461+
Rule landlock.Rule
462+
WantErr error
463+
}{
464+
{
465+
Name: "WithoutIoctlDev",
466+
Rule: landlock.RWFiles(path),
467+
WantErr: syscall.EACCES,
468+
},
469+
{
470+
Name: "WithIoctlDev",
471+
Rule: landlock.RWFiles(path).WithIoctlDev(),
472+
// ENOTTY means that the IOCTL was dispatched
473+
// to device. (Would be nicer to find an
474+
// IOCTL that returns success here, but the
475+
// available devices on qemu are limited.)
476+
WantErr: syscall.ENOTTY,
477+
},
478+
} {
479+
t.Run(tt.Name, func(t *testing.T) {
480+
RunInSubprocess(t, func() {
481+
RequireLandlockABI(t, 5)
482+
483+
err := landlock.V5.BestEffort().RestrictPaths(tt.Rule)
484+
if err != nil {
485+
t.Fatalf("Enabling Landlock: %v", err)
486+
}
487+
488+
f, err := os.Open(path)
489+
if err != nil {
490+
t.Fatalf("os.Open(%q): %v", path, err)
491+
}
492+
defer func() { f.Close() }()
493+
494+
_, err = unix.IoctlGetInt(int(f.Fd()), FIONREAD)
495+
if !errEqual(err, tt.WantErr) {
496+
t.Errorf("ioctl(%v, FIONREAD): got err «%v», want «%v»", f, err, tt.WantErr)
497+
}
498+
})
499+
})
500+
}
501+
}
502+
454503
func errEqual(got, want error) bool {
455504
if got == nil && want == nil {
456505
return true

landlock/syscall/landlock.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ const (
3434
AccessFSMakeSym
3535
AccessFSRefer
3636
AccessFSTruncate
37+
AccessFSIoctlDev
3738
)
3839

3940
// Landlock network access rights.

0 commit comments

Comments
 (0)