Skip to content

Commit

Permalink
Landlock ABI v5 support (IOCTL on device files)
Browse files Browse the repository at this point in the history
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/[email protected]/
  • Loading branch information
gnoack committed Jul 15, 2024
1 parent 3b15ce3 commit 8acad90
Show file tree
Hide file tree
Showing 10 changed files with 113 additions and 16 deletions.
29 changes: 22 additions & 7 deletions cmd/landlock-restrict/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:]
Expand All @@ -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)
}
Expand All @@ -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:]
Expand Down Expand Up @@ -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)
Expand Down
5 changes: 5 additions & 0 deletions landlock/abi_versions.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion landlock/abi_versions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions landlock/accessfs.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ var accessFSNames = []string{
"make_sym",
"refer",
"truncate",
"ioctl_dev",
}

// AccessFSSet is a set of Landlockable file system access operations.
Expand Down
2 changes: 2 additions & 0 deletions landlock/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion landlock/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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...)
Expand Down
14 changes: 7 additions & 7 deletions landlock/landlock.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -20,20 +20,20 @@
// 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
//
// The following invocation restricts both file and network access at
// 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),
Expand All @@ -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.
//
Expand Down
24 changes: 24 additions & 0 deletions landlock/path_opt.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down
49 changes: 49 additions & 0 deletions landlock/restrict_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions landlock/syscall/landlock.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const (
AccessFSMakeSym
AccessFSRefer
AccessFSTruncate
AccessFSIoctlDev
)

// Landlock network access rights.
Expand Down

0 comments on commit 8acad90

Please sign in to comment.