Repository: landlock-lsm/go-landlock (language: go)

Issue:
Prepare for Landlock V2
Landlock V2 is scheduled to appear in kernel 5.19 ([mail with links to the patches](https://lore.kernel.org/all/20220523161245.2451265-1-mic@digikod.net/)) - let's get support into Go-Landlock for it.


Discussion / hints:
Final patch set so far: https://lore.kernel.org/all/20220506161102.525323-1-mic@digikod.net/
Right, it has been merged here: https://git.kernel.org/torvalds/c/cb44e4f061e16be65b8a16505e121490c66d30d0
I'll Cc you next time :wink: 
Thanks!

Fix patch:
diff --git a/cmd/landlock-restrict/main.go b/cmd/landlock-restrict/main.go
index 7f9751b..0d6593c 100644
--- a/cmd/landlock-restrict/main.go
+++ b/cmd/landlock-restrict/main.go
@@ -10,71 +10,115 @@ import (
 	"github.com/landlock-lsm/go-landlock/landlock"
 )
 
-func takeArgs(args, out []string) (argsOut, outOut []string) {
-	for len(args) > 0 && !strings.HasPrefix(args[0], "-") {
-		out = append(out, args[0])
-		args = args[1:]
+
+func parseFlags(args []string) (verbose bool, cfg landlock.Config, opts []landlock.PathOpt, cmd []string) {
+	cfg = landlock.V2
+
+	takeArgs := func(makeOpt func(...string) landlock.PathOpt) landlock.PathOpt {
+		var paths []string
+		needRefer := false
+		for len(args) > 0 && !strings.HasPrefix(args[0], "-") {
+			if args[0] == "+refer" {
+				needRefer = true
+			} else {
+				paths = append(paths, args[0])
+			}
+			args = args[1:]
+		}
+		opt := makeOpt(paths...)
+		if verbose {
+			fmt.Println("Path option:", opt)
+		}
+		if needRefer {
+			opt = opt.WithRefer()
+		}
+		if verbose {
+			fmt.Println("Path option:", opt)
+		}
+		return opt
 	}
-	return args, out
-}
 
-func parseFlags(args []string) (verbose bool, roDirs, rwDirs, roFiles, rwFiles, cmd []string) {
+	bestEffort := true
+ArgParsing:
 	for len(args) > 0 {
 		switch args[0] {
+		case "-2":
+			cfg = landlock.V2
+			args = args[1:]
+			continue
+		case "-1":
+			cfg = landlock.V1
+			args = args[1:]
+			continue
+		case "-strict":
+			bestEffort=false
+			args = args[1:]
+			continue
 		case "-v":
 			verbose = true
 			args = args[1:]
 			continue
 		case "-ro":
 			args = args[1:]
-			args, roDirs = takeArgs(args, roDirs)
+			opts = append(opts, takeArgs(landlock.RODirs))
 			continue
 		case "-rw":
 			args = args[1:]
-			args, rwDirs = takeArgs(args, rwDirs)
+			opts = append(opts, takeArgs(landlock.RWDirs))
 			continue
 		case "-rofiles":
 			args = args[1:]
-			args, roFiles = takeArgs(args, roFiles)
+			opts = append(opts, takeArgs(landlock.ROFiles))
 			continue
 		case "-rwfiles":
 			args = args[1:]
-			args, rwFiles = takeArgs(args, rwFiles)
+			opts = append(opts, takeArgs(landlock.RWFiles))
 			continue
 		case "--":
 			args = args[1:]
 			// Remaining args are the command
-			cmd = args
-			return verbose, roDirs, rwDirs, roFiles, rwFiles, cmd
+			break ArgParsing
 		default:
 			log.Fatalf("Unrecognized option %q", args[0])
 		}
 	}
 
 	cmd = args
-	return verbose, roDirs, rwDirs, roFiles, rwFiles, cmd
+	if bestEffort {
+		cfg = cfg.BestEffort()
+	}
+	return verbose, cfg, opts, cmd
 }
 
 func main() {
-	verbose, roDirs, rwDirs, roFiles, rwFiles, cmdArgs := parseFlags(os.Args[1:])
+	verbose, cfg, opts, cmdArgs := parseFlags(os.Args[1:])
 	if verbose {
-		fmt.Println("Landlock restricting to:")
-		fmt.Printf("  RO dirs : %v\n", roDirs)
-		fmt.Printf("  RW dirs : %v\n", rwDirs)
-		fmt.Printf("  RO files: %v\n", roFiles)
-		fmt.Printf("  RW files: %v\n", rwFiles)
+		fmt.Println("Args: ", os.Args)
+		fmt.Println()
+		fmt.Printf("Config: %v\n", cfg)
 		fmt.Println()
 		fmt.Printf("Executing command %v\n", cmdArgs)
 	}
 
 	if len(cmdArgs) < 1 {
 		fmt.Println("Usage:")
-		fmt.Println("  landlock-restrict [-ro PATH...] [-rw PATH...] [-rofiles PATH] [-rwfiles PATH] -- COMMAND...")
+		fmt.Println("  landlock-restrict")
+		fmt.Println("     [-verbose]")
+		fmt.Println("     [-1] [-2] [-strict]")
+		fmt.Println("     [-ro [+refer] PATH...] [-rw [+refer] 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                         select Landlock version")
+		fmt.Println("  -strict                        use strict mode (instead of best effort)")
 		fmt.Println("  -verbose                       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 V2 in best effort mode (best compatibility)")
+		fmt.Println()
 
 		log.Fatalf("Need proper command, got %v", cmdArgs)
 	}
@@ -83,12 +127,7 @@ func main() {
 		log.Fatalf("Need absolute binary path, got %q", cmdArgs[0])
 	}
 
-	err := landlock.V1.BestEffort().RestrictPaths(
-		landlock.RODirs(roDirs...),
-		landlock.RWDirs(rwDirs...),
-		landlock.ROFiles(roFiles...),
-		landlock.RWFiles(rwFiles...),
-	)
+	err := cfg.RestrictPaths(opts...)
 	if err != nil {
 		log.Fatalf("landlock: %v", err)
 	}
diff --git a/landlock/abi_versions.go b/landlock/abi_versions.go
index f53ad7d..439d39f 100644
--- a/landlock/abi_versions.go
+++ b/landlock/abi_versions.go
@@ -16,6 +16,10 @@ var abiInfos = []abiInfo{
 		version:           1,
 		supportedAccessFS: (1 << 13) - 1,
 	},
+	{
+		version:           2,
+		supportedAccessFS: (1 << 14) - 1,
+	},
 }
 
 // getSupportedABIVersion returns the kernel-supported ABI version.
diff --git a/landlock/accessfs.go b/landlock/accessfs.go
index 7725b1c..d1364af 100644
--- a/landlock/accessfs.go
+++ b/landlock/accessfs.go
@@ -19,12 +19,13 @@ var flagNames = []string{
 	"make_fifo",
 	"make_block",
 	"make_sym",
+	"refer",
 }
 
 // AccessFSSet is a set of Landlockable file system access operations.
 type AccessFSSet uint64
 
-var supportedAccessFS = AccessFSSet((1 << 13) - 1)
+var supportedAccessFS = AccessFSSet((1 << len(flagNames)) - 1)
 
 func (a AccessFSSet) String() string {
 	if a.isEmpty() {
@@ -57,6 +58,10 @@ func (a AccessFSSet) intersect(b AccessFSSet) AccessFSSet {
 	return a & b
 }
 
+func (a AccessFSSet) union(b AccessFSSet) AccessFSSet {
+	return a | b
+}
+
 func (a AccessFSSet) isEmpty() bool {
 	return a == 0
 }
diff --git a/landlock/config.go b/landlock/config.go
index e3632bd..309c4ef 100644
--- a/landlock/config.go
+++ b/landlock/config.go
@@ -33,6 +33,10 @@ var (
 	V1 = Config{
 		handledAccessFS: abiInfos[1].supportedAccessFS,
 	}
+	// Landlock V2 support (V1 + file reparenting between different directories)
+	V2 = Config{
+		handledAccessFS: abiInfos[2].supportedAccessFS,
+	}
 )
 
 // The Landlock configuration describes the desired set of
@@ -86,14 +90,15 @@ func MustConfig(args ...interface{}) Config {
 // String builds a human-readable representation of the Config.
 func (c Config) String() string {
 	abi := abiInfo{version: -1} // invalid
-	for _, a := range abiInfos {
+	for i := len(abiInfos) - 1; i >= 0; i-- {
+		a := abiInfos[i]
 		if c.handledAccessFS.isSubset(a.supportedAccessFS) {
 			abi = a
 		}
 	}
 
 	var desc = c.handledAccessFS.String()
-	if abi.supportedAccessFS == c.handledAccessFS {
+	if abi.supportedAccessFS == c.handledAccessFS && c.handledAccessFS != 0 {
 		desc = "all"
 	}
 
@@ -131,6 +136,41 @@ type PathOpt struct {
 	paths         []string
 }
 
+// withRights adds the given access rights to the right enforced in the path option
+// and returns the result as a new PathOpt.
+func (p PathOpt) withRights(a AccessFSSet) PathOpt {
+	return PathOpt{
+		accessFS:      p.accessFS.union(a),
+		enforceSubset: p.enforceSubset,
+		paths:         p.paths,
+	}
+}
+
+// WithRefer adds the "refer" access right to a path option.
+//
+// Notably, asking for the "refer" access right does not work on
+// kernels below 5.19. In best effort mode, this will fall back to not
+// using Landlock enforcement at all on these kernel versions. If you
+// want to use Landlock on these kernels, do not use the "refer"
+// access right.
+func (p PathOpt) WithRefer() PathOpt {
+	return p.withRights(ll.AccessFSRefer)
+}
+
+func (p PathOpt) String() string {
+	return fmt.Sprintf("REQUIRE %v for paths %v", p.accessFS, p.paths)
+}
+
+func (p PathOpt) compatibleWithHandledAccessFS(handledAccessFS AccessFSSet) bool {
+	a := p.accessFS
+	if !p.enforceSubset {
+		// Even when we are lax about enforcing flag subsets,
+		// the "refer" flag always gets checked.
+		a = a.intersect(ll.AccessFSRefer)
+	}
+	return a.isSubset(handledAccessFS)
+}
+
 // PathAccess is a RestrictPaths() option that grants the access right
 // specified by accessFS to the file hierarchies under the given paths.
 //
@@ -208,12 +248,12 @@ func RWFiles(paths ...string) PathOpt {
 // that it can only read from /usr, /bin and /tmp, and only write to
 // /tmp:
 //
-//   err := landlock.V1.RestrictPaths(
+//   err := landlock.V2.RestrictPaths(
 //       landlock.RODirs("/usr", "/bin"),
 //       landlock.RWDirs("/tmp"),
 //   )
 //   if err != nil {
-//       log.Fatalf("landlock.V1.RestrictPaths(): %v", err)
+//       log.Fatalf("landlock.V2.RestrictPaths(): %v", err)
 //   }
 //
 // RestrictPaths returns an error if any of the given paths does not
@@ -269,6 +309,8 @@ func RWFiles(paths ...string) PathOpt {
 //
 // • Creating (or renaming or linking) a symbolic link (V1+)
 //
+// • Renaming or linking a file between directories (V2+)
+//
 // Future versions of Landlock will be able to inhibit more operations.
 // Quoting the Landlock documentation:
 //
@@ -289,7 +331,8 @@ func RWFiles(paths ...string) PathOpt {
 // In V1, this means reading files, listing directories and executing files.
 //
 // • RWDirs() selects access rights in the group "for reading", "for writing" and
-// "for directory manipulation". In V1, this grants the full set of access rights.
+// "for directory manipulation". This grants the full set of access rights which are
+// available within the configuration.
 //
 // • ROFiles() is like RODirs(), but does not select directory-specific access rights.
 // In V1, this means reading and executing files.
diff --git a/landlock/landlock.go b/landlock/landlock.go
index 5f6ddcd..7e59584 100644
--- a/landlock/landlock.go
+++ b/landlock/landlock.go
@@ -3,28 +3,26 @@
 // 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.V1.BestEffort().RestrictPaths(
+//     err := landlock.V2.BestEffort().RestrictPaths(
 //         landlock.RODirs("/usr", "/bin"),
 //         landlock.RWDirs("/tmp"),
 //     )
 //
-// This will restrict file access using Landlock V1, if available. If
+// This will restrict file access using Landlock V2, 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.
 //
 // More possible invocations
 //
-// landlock.V1.RestrictPaths(...) enforces the given rules using the
-// capabilities of Landlock V1, but returns an error if that is not
+// landlock.V2.RestrictPaths(...) enforces the given rules using the
+// capabilities of Landlock V2, but returns an error if that is not
 // available.
 //
 // Landlock ABI versioning
 //
 // Callers need to identify at which ABI level they want to use
 // Landlock and call RestrictPaths on the corresponding ABI constant.
-// Currently the only available ABI variant is V1, which restricts
-// basic filesystem operations.
 //
 // When new Landlock versions become available in landlock, users
 // will need to upgrade their usages manually to higher Landlock
@@ -46,7 +44,10 @@
 // Enabling Landlock implicitly turns off the following file system
 // features:
 //
-// - File reparenting: renaming or linking a file to a different parent directory is always denied.
+// - File reparenting: renaming or linking a file to a different parent directory is denied,
+//   unless it is explicitly enabled on both directories with the "Refer" access modifier,
+//   and the new target directory does not grant the file additional rights through its
+//   Landlock access rules.
 //
 // - Filesystem topology modification: arbitrary mounts are always denied.
 //
diff --git a/landlock/restrict.go b/landlock/restrict.go
index 8f26868..7f2ec34 100644
--- a/landlock/restrict.go
+++ b/landlock/restrict.go
@@ -9,23 +9,38 @@ import (
 	"golang.org/x/sys/unix"
 )
 
+// downgrade calculates the actual ruleset to be enforced given the
+// current kernel's Landlock ABI level.
+//
+// It establishes that opt.accessFS ⊆ handledAccessFS ⊆ abi.supportedAccessFS.
 func downgrade(handledAccessFS AccessFSSet, opts []PathOpt, abi abiInfo) (AccessFSSet, []PathOpt) {
 	handledAccessFS = handledAccessFS.intersect(abi.supportedAccessFS)
 
 	resOpts := make([]PathOpt, len(opts))
 	copy(resOpts, opts)
 	for i := 0; i < len(resOpts); i++ {
+		// In case that "refer" is requested on a path, we
+		// require Landlock V2+, or we have to downgrade to V0.
+		// You can't get the refer capability with V1, but linking/
+		// renaming files is always implicitly restricted.
+		if hasRefer(resOpts[i].accessFS) && !hasRefer(handledAccessFS) {
+			return 0, nil // Use "ABI V0" (do nothing)
+		}
 		resOpts[i].accessFS = resOpts[i].accessFS.intersect(handledAccessFS)
 	}
 	return handledAccessFS, resOpts
 }
 
-// The actual restrictPaths implementation.
+func hasRefer(a AccessFSSet) bool {
+	return a&ll.AccessFSRefer != 0
+}
+
+// restrictPaths is the actual RestrictPaths implementation.
 func restrictPaths(c Config, opts ...PathOpt) error {
 	handledAccessFS := c.handledAccessFS
 	// Check validity of options early.
 	for _, opt := range opts {
-		if opt.enforceSubset && !opt.accessFS.isSubset(handledAccessFS) {
+		if !opt.compatibleWithHandledAccessFS(handledAccessFS) {
 			return fmt.Errorf("too broad option %v: %w", opt.accessFS, unix.EINVAL)
 		}
 	}
@@ -34,11 +49,14 @@ func restrictPaths(c Config, opts ...PathOpt) error {
 	if c.bestEffort {
 		handledAccessFS, opts = downgrade(handledAccessFS, opts, abi)
 	}
-
 	if !handledAccessFS.isSubset(abi.supportedAccessFS) {
 		return fmt.Errorf("missing kernel Landlock support. Got Landlock ABI v%v, wanted %v", abi.version, c.String())
 	}
 
+	// TODO: This might be incorrect - the "refer" permission is
+	// always implicit, even in Landlock V1. So enabling Landlock
+	// on a Landlock V1 kernel without any handled access rights
+	// will still forbid linking files between directories.
 	if handledAccessFS.isEmpty() {
 		return nil // Success: Nothing to restrict.
 	}
diff --git a/landlock/syscall/landlock.go b/landlock/syscall/landlock.go
index 85d4a13..ab91af7 100644
--- a/landlock/syscall/landlock.go
+++ b/landlock/syscall/landlock.go
@@ -40,6 +40,7 @@ const (
 	AccessFSMakeFifo   = unix.LANDLOCK_ACCESS_FS_MAKE_FIFO
 	AccessFSMakeBlock  = unix.LANDLOCK_ACCESS_FS_MAKE_BLOCK
 	AccessFSMakeSym    = unix.LANDLOCK_ACCESS_FS_MAKE_SYM
+	AccessFSRefer      = 1 << 13 // unix.LANDLOCK_ACCESS_FS_REFER
 )
 
 // RulesetAttr is the Landlock ruleset definition.


Test patch:
diff --git a/landlock/abi_versions_test.go b/landlock/abi_versions_test.go
index a8eeb4a..e5539cf 100644
--- a/landlock/abi_versions_test.go
+++ b/landlock/abi_versions_test.go
@@ -13,10 +13,10 @@ func TestAbiVersionsIncrementing(t *testing.T) {
 }
 
 func TestSupportedAccessFS(t *testing.T) {
-	got := abiInfos[1].supportedAccessFS
+	got := abiInfos[2].supportedAccessFS
 	want := supportedAccessFS
 
 	if got != want {
-		t.Errorf("V1 supported access FS: got %x, want %x", got, want)
+		t.Errorf("V2 supported access FS: got %x, want %x", got, want)
 	}
 }
diff --git a/landlock/accessfs_test.go b/landlock/accessfs_test.go
index 78e3dea..45592f5 100644
--- a/landlock/accessfs_test.go
+++ b/landlock/accessfs_test.go
@@ -46,6 +46,7 @@ func TestPrettyPrint(t *testing.T) {
 		{a: ll.AccessFSMakeFifo, want: "{make_fifo}"},
 		{a: ll.AccessFSMakeBlock, want: "{make_block}"},
 		{a: ll.AccessFSMakeSym, want: "{make_sym}"},
+		{a: ll.AccessFSRefer, want: "{refer}"},
 		{a: ll.AccessFSReadFile | 1<<63, want: "{read_file,1<<63}"},
 	} {
 		got := tc.a.String()
@@ -54,3 +55,14 @@ func TestPrettyPrint(t *testing.T) {
 		}
 	}
 }
+
+func TestValid(t *testing.T) {
+	for _, a := range []AccessFSSet{
+		ll.AccessFSExecute, ll.AccessFSMakeDir, ll.AccessFSMakeSym, ll.AccessFSRefer,
+	} {
+		gotIsValid := a.valid()
+		if !gotIsValid {
+			t.Errorf("%v.valid() = false, want true", a)
+		}
+	}
+}
diff --git a/landlock/config_test.go b/landlock/config_test.go
index 65f6956..723faf5 100644
--- a/landlock/config_test.go
+++ b/landlock/config_test.go
@@ -14,7 +14,7 @@ func TestConfigString(t *testing.T) {
 	}{
 		{
 			cfg:  Config{handledAccessFS: 0},
-			want: fmt.Sprintf("{Landlock V1; HandledAccessFS: %v}", AccessFSSet(0)),
+			want: fmt.Sprintf("{Landlock V0; HandledAccessFS: %v}", AccessFSSet(0)),
 		},
 		{
 			cfg:  Config{handledAccessFS: ll.AccessFSWriteFile},
@@ -41,13 +41,17 @@ func TestConfigString(t *testing.T) {
 }
 
 func TestNewConfig(t *testing.T) {
-	c, err := NewConfig(AccessFSSet(ll.AccessFSWriteFile))
-	if err != nil {
-		t.Errorf("NewConfig(): expected success, got %v", err)
-	}
-	want := AccessFSSet(ll.AccessFSWriteFile)
-	if c.handledAccessFS != want {
-		t.Errorf("c.handledAccessFS = %v, want %v", c.handledAccessFS, want)
+	for _, a := range []AccessFSSet{
+		ll.AccessFSWriteFile, ll.AccessFSRefer,
+	} {
+		c, err := NewConfig(a)
+		if err != nil {
+			t.Errorf("NewConfig(): expected success, got %v", err)
+		}
+		want := a
+		if c.handledAccessFS != want {
+			t.Errorf("c.handledAccessFS = %v, want %v", c.handledAccessFS, want)
+		}
 	}
 }
 
@@ -72,6 +76,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 << 63)},
 	} {
 		_, err := NewConfig(args...)
diff --git a/landlock/restrict_downgrade_test.go b/landlock/restrict_downgrade_test.go
index 8fe961b..776e658 100644
--- a/landlock/restrict_downgrade_test.go
+++ b/landlock/restrict_downgrade_test.go
@@ -2,6 +2,8 @@ package landlock
 
 import (
 	"testing"
+
+	ll "github.com/landlock-lsm/go-landlock/landlock/syscall"
 )
 
 func TestDowngrade(t *testing.T) {
@@ -14,6 +16,8 @@ func TestDowngrade(t *testing.T) {
 
 		WantHandled   AccessFSSet
 		WantRequested AccessFSSet
+
+		WantFallbackToV0 bool
 	}{
 		{
 			Name:          "RestrictHandledToSupported",
@@ -39,6 +43,21 @@ func TestDowngrade(t *testing.T) {
 			WantHandled:   0b0,
 			WantRequested: 0b0,
 		},
+		{
+			Name:          "ReferSupportedOnV2",
+			SupportedABI:  2,
+			Handled:       ll.AccessFSRefer | ll.AccessFSReadFile,
+			Requested:     ll.AccessFSRefer | ll.AccessFSReadFile,
+			WantHandled:   ll.AccessFSRefer | ll.AccessFSReadFile,
+			WantRequested: ll.AccessFSRefer | ll.AccessFSReadFile,
+		},
+		{
+			Name:             "ReferNotSupportedOnV1",
+			SupportedABI:     1,
+			Handled:          ll.AccessFSRefer | ll.AccessFSReadFile,
+			Requested:        ll.AccessFSRefer | ll.AccessFSReadFile,
+			WantFallbackToV0: true,
+		},
 	} {
 		t.Run(tc.Name, func(t *testing.T) {
 			abi := abiInfos[tc.SupportedABI]
@@ -46,6 +65,17 @@ func TestDowngrade(t *testing.T) {
 			opts := []PathOpt{PathAccess(tc.Requested, "foo")}
 			gotHandled, gotOpts := downgrade(tc.Handled, opts, abi)
 
+			if tc.WantFallbackToV0 {
+				if gotHandled != 0 {
+					t.Errorf(
+						"downgrade(%v, %v, ABIv%d) = %v, %v; want fallback to V0",
+						tc.Handled, tc.Requested, tc.SupportedABI,
+						gotHandled, gotOpts,
+					)
+				}
+				return
+			}
+
 			if len(gotOpts) != 1 {
 				t.Fatalf("wrong number of opts returned: got %d, want 1", len(gotOpts))
 			}
diff --git a/landlock/restrict_failure_test.go b/landlock/restrict_failure_test.go
index 7d5888a..f8a0fd8 100644
--- a/landlock/restrict_failure_test.go
+++ b/landlock/restrict_failure_test.go
@@ -28,6 +28,14 @@ func MustWriteFile(t testing.TB, path string) {
 	}
 }
 
+func MustMkdir(t testing.TB, path string) {
+	t.Helper()
+
+	if err := os.Mkdir(path, 0700); err != nil {
+		t.Fatalf("os.Mkdir(%q): %v", path, err)
+	}
+}
+
 // TempDir is a replacement for t.TempDir() to be used in Landlock tests.
 // If we were using t.TempDir(), the test framework would try to remove it
 // after the test, even in Landlocked subprocess tests where this fails.
@@ -109,3 +117,25 @@ func TestOverlyBroadPathOpt(t *testing.T) {
 		t.Errorf("expected 'invalid argument' error, got: %v", err)
 	}
 }
+
+func TestReferNotPermittedInStrictV1(t *testing.T) {
+	RequireLandlockABI(t, 1)
+
+	// 'refer' is incompatible with Landlock ABI V1.
+	// Users should use Landlock V2 instead or construct a custom
+	// config that handles the 'refer' access right.
+	// You can technically also just enable V1 best-effort mode,
+	// but that combination always falls back to "no enforcement".
+	for _, opt := range []landlock.PathOpt{
+		landlock.RWDirs("/etc").WithRefer(),
+		landlock.PathAccess(0, "/etc").WithRefer(),
+	} {
+		err := landlock.V1.RestrictPaths(opt)
+		if !errors.Is(err, unix.EINVAL) {
+			t.Errorf("expected 'invalid argument' error, got: %v", err)
+		}
+		if !strings.Contains(err.Error(), "too broad option") {
+			t.Errorf("expected a 'too broad option' error, got: %v", err)
+		}
+	}
+}
diff --git a/landlock/restrict_test.go b/landlock/restrict_test.go
index 89f2d4f..3a0e36a 100644
--- a/landlock/restrict_test.go
+++ b/landlock/restrict_test.go
@@ -15,15 +15,18 @@ func TestRestrictPaths(t *testing.T) {
 	for _, tt := range []struct {
 		Name           string
 		EnableLandlock func(dir, fpath string) error
+		RequiredABI    int
 		WantOpenErr    error
 		WantReadDirErr error
 		WantCreateErr  error
 		WantMkdirErr   error
 		WantUnlinkErr  error
 		WantMkfifoErr  error
+		WantReferErr   error
 	}{
 		{
-			Name: "EverythingForbidden",
+			Name:        "EverythingForbidden",
+			RequiredABI: 1,
 			EnableLandlock: func(dir, fpath string) error {
 				return landlock.V1.RestrictPaths()
 			},
@@ -33,9 +36,11 @@ func TestRestrictPaths(t *testing.T) {
 			WantMkdirErr:   syscall.EACCES,
 			WantUnlinkErr:  syscall.EACCES,
 			WantMkfifoErr:  syscall.EACCES,
+			WantReferErr:   syscall.EXDEV,
 		},
 		{
-			Name: "ROFilesPermissionsOnFile",
+			Name:        "ROFilesPermissionsOnFile",
+			RequiredABI: 1,
 			EnableLandlock: func(dir, fpath string) error {
 				return landlock.V1.RestrictPaths(landlock.ROFiles(fpath))
 			},
@@ -45,9 +50,11 @@ func TestRestrictPaths(t *testing.T) {
 			WantMkdirErr:   syscall.EACCES,
 			WantUnlinkErr:  syscall.EACCES,
 			WantMkfifoErr:  syscall.EACCES,
+			WantReferErr:   syscall.EXDEV,
 		},
 		{
-			Name: "RWFilesPermissionsOnFile",
+			Name:        "RWFilesPermissionsOnFile",
+			RequiredABI: 1,
 			EnableLandlock: func(dir, fpath string) error {
 				return landlock.V1.RestrictPaths(landlock.RWFiles(fpath))
 			},
@@ -57,9 +64,11 @@ func TestRestrictPaths(t *testing.T) {
 			WantMkdirErr:   syscall.EACCES,
 			WantUnlinkErr:  syscall.EACCES,
 			WantMkfifoErr:  syscall.EACCES,
+			WantReferErr:   syscall.EXDEV,
 		},
 		{
-			Name: "ROFilesPermissionsOnDir",
+			Name:        "ROFilesPermissionsOnDir",
+			RequiredABI: 1,
 			EnableLandlock: func(dir, fpath string) error {
 				return landlock.V1.RestrictPaths(landlock.ROFiles(dir))
 			},
@@ -69,9 +78,11 @@ func TestRestrictPaths(t *testing.T) {
 			WantMkdirErr:   syscall.EACCES,
 			WantUnlinkErr:  syscall.EACCES,
 			WantMkfifoErr:  syscall.EACCES,
+			WantReferErr:   syscall.EXDEV,
 		},
 		{
-			Name: "RWFilesPermissionsOnDir",
+			Name:        "RWFilesPermissionsOnDir",
+			RequiredABI: 1,
 			EnableLandlock: func(dir, fpath string) error {
 				return landlock.V1.RestrictPaths(landlock.RWFiles(dir))
 			},
@@ -81,9 +92,11 @@ func TestRestrictPaths(t *testing.T) {
 			WantMkdirErr:   syscall.EACCES,
 			WantUnlinkErr:  syscall.EACCES,
 			WantMkfifoErr:  syscall.EACCES,
+			WantReferErr:   syscall.EXDEV,
 		},
 		{
-			Name: "RODirsPermissionsOnDir",
+			Name:        "RODirsPermissionsOnDir",
+			RequiredABI: 1,
 			EnableLandlock: func(dir, fpath string) error {
 				return landlock.V1.RestrictPaths(landlock.RODirs(dir))
 			},
@@ -93,9 +106,11 @@ func TestRestrictPaths(t *testing.T) {
 			WantMkdirErr:   syscall.EACCES,
 			WantUnlinkErr:  syscall.EACCES,
 			WantMkfifoErr:  syscall.EACCES,
+			WantReferErr:   syscall.EXDEV,
 		},
 		{
-			Name: "RWDirsPermissionsOnDir",
+			Name:        "RWDirsPermissionsOnDir",
+			RequiredABI: 1,
 			EnableLandlock: func(dir, fpath string) error {
 				return landlock.V1.RestrictPaths(landlock.RWDirs(dir))
 			},
@@ -105,15 +120,48 @@ func TestRestrictPaths(t *testing.T) {
 			WantMkdirErr:   nil,
 			WantUnlinkErr:  nil,
 			WantMkfifoErr:  nil,
+			WantReferErr:   syscall.EXDEV,
+		},
+		{
+			Name:        "RWDirsWithRefer",
+			RequiredABI: 2,
+			EnableLandlock: func(dir, fpath string) error {
+				return landlock.V2.RestrictPaths(landlock.RWDirs(dir).WithRefer())
+			},
+			WantOpenErr:    nil,
+			WantReadDirErr: nil,
+			WantCreateErr:  nil,
+			WantMkdirErr:   nil,
+			WantUnlinkErr:  nil,
+			WantMkfifoErr:  nil,
+			WantReferErr:   nil,
+		},
+		{
+			Name:        "RWDirsWithoutRefer",
+			RequiredABI: 2,
+			EnableLandlock: func(dir, fpath string) error {
+				return landlock.V2.RestrictPaths(landlock.RWDirs(dir) /* without refer */)
+			},
+			WantOpenErr:    nil,
+			WantReadDirErr: nil,
+			WantCreateErr:  nil,
+			WantMkdirErr:   nil,
+			WantUnlinkErr:  nil,
+			WantMkfifoErr:  nil,
+			WantReferErr:   syscall.EXDEV,
 		},
 	} {
 		t.Run(tt.Name, func(t *testing.T) {
 			RunInSubprocess(t, func() {
-				RequireLandlockABI(t, 1)
+				RequireLandlockABI(t, tt.RequiredABI)
 
 				dir := TempDir(t)
 				fpath := filepath.Join(dir, "lolcat.txt")
 				MustWriteFile(t, fpath)
+				renameMeFpath := filepath.Join(dir, "renameme.txt")
+				MustWriteFile(t, renameMeFpath)
+				dstDirPath := filepath.Join(dir, "dst")
+				MustMkdir(t, dstDirPath)
 
 				err := tt.EnableLandlock(dir, fpath)
 				if err != nil {
@@ -132,18 +180,23 @@ func TestRestrictPaths(t *testing.T) {
 					t.Errorf("os.Create(%q) = «%v», want «%v»", fpath, err, tt.WantCreateErr)
 				}
 
-				subdirpath := filepath.Join(dir, "subdir")
-				if err := os.Mkdir(subdirpath, 0600); !errEqual(err, tt.WantMkdirErr) {
-					t.Errorf("os.Mkdir(%q) = «%v», want «%v»", subdirpath, err, tt.WantMkdirErr)
+				subdirPath := filepath.Join(dir, "subdir")
+				if err := os.Mkdir(subdirPath, 0600); !errEqual(err, tt.WantMkdirErr) {
+					t.Errorf("os.Mkdir(%q) = «%v», want «%v»", subdirPath, err, tt.WantMkdirErr)
 				}
 
 				if err := os.Remove(fpath); !errEqual(err, tt.WantUnlinkErr) {
 					t.Errorf("os.Remove(%q) = «%v», want «%v»", fpath, err, tt.WantUnlinkErr)
 				}
 
-				fifopath := filepath.Join(dir, "fifo")
-				if err := unix.Mkfifo(fifopath, 0600); !errEqual(err, tt.WantMkfifoErr) {
-					t.Errorf("os.Mkfifo(%q, ...) = «%v», want «%v»", fifopath, err, tt.WantMkfifoErr)
+				fifoPath := filepath.Join(dir, "fifo")
+				if err := unix.Mkfifo(fifoPath, 0600); !errEqual(err, tt.WantMkfifoErr) {
+					t.Errorf("os.Mkfifo(%q, ...) = «%v», want «%v»", fifoPath, err, tt.WantMkfifoErr)
+				}
+
+				dstFpath := filepath.Join(dstDirPath, "target.txt")
+				if err := os.Rename(renameMeFpath, dstFpath); !errEqual(err, tt.WantReferErr) {
+					t.Errorf("os.Rename(%q, %q) = «%v», want «%v»", renameMeFpath, dstFpath, err, tt.WantReferErr)
 				}
 			})
 		})