[PATCH] landlock: Fix layer composition of LANDLOCK_ACCESS_FS_REFER

From: pandaWall

Date: Wed Nov 11 2026 - 21:57:50 EST


landlock_get_fs_access_mask() unconditionally ORs
_LANDLOCK_ACCESS_FS_INITIALLY_DENIED (i.e. LANDLOCK_ACCESS_FS_REFER)
into every layer's handled filesystem mask. Layers created from
rulesets that only handle other rule types (network ports, scopes) or
only permissions therefore report REFER as handled even though
landlock_upgrade_handled_layer_config() only adds the bit when
handled.fs is non-zero, and no filesystem rule can ever clear a bit
of such a layer.

The consequence is that stacking any non-filesystem ruleset on top of
a filesystem ruleset makes link(2), rename(2) and
renameat2(RENAME_EXCHANGE) fail with -EXDEV/-EACCES even when each
ruleset individually allows the operation: landlock_unmask_layers()
requires every layer's mask to be cleared, and a non-filesystem
layer's mask can never be.

Only implicitly handle REFER for layers that already handle
filesystem access rights.

Fixes: fff69fb03dde ("landlock: Support network rules with TCP bind and connect")
Signed-off-by: pandaWall <mangosteen922@xxxxxxx>

Add a corresponding regression test to the layout1 fs_test suite:
stacking a network-only ruleset (handled_access_net only) on top of a
filesystem ruleset allowing REFER and MAKE_REG must not deny
different-parent rename/exchange.
---
security/landlock/domain.h | 15 +++++++--
tools/testing/selftests/landlock/fs_test.c | 37 ++++++++++++++++++++++
2 files changed, 49 insertions(+), 3 deletions(-)

diff --git a/security/landlock/domain.h b/security/landlock/domain.h
index 935e18b12..07d4a23fc 100644
--- a/security/landlock/domain.h
+++ b/security/landlock/domain.h
@@ -262,9 +262,18 @@ static inline access_mask_t
landlock_get_fs_access_mask(const struct landlock_domain *const domain,
const u16 layer_level)
{
- /* Handles all initially denied by default access rights. */
- return domain->layers[layer_level].handled.fs |
- _LANDLOCK_ACCESS_FS_INITIALLY_DENIED;
+ access_mask_t handled = domain->layers[layer_level].handled.fs;
+
+ /*
+ * Only layers that handle filesystem access rights implicitly
+ * handle REFER. A layer that handles other rule types (e.g.
+ * network, scopes) must not imply REFER, otherwise it would
+ * spuriously deny link and rename operations.
+ */
+ if (handled)
+ handled |= _LANDLOCK_ACCESS_FS_INITIALLY_DENIED;
+
+ return handled;
}

static inline access_mask_t
diff --git a/tools/testing/selftests/landlock/fs_test.c b/tools/testing/selftests/landlock/fs_test.c
index f725882fa..5588f0ee5 100644
--- a/tools/testing/selftests/landlock/fs_test.c
+++ b/tools/testing/selftests/landlock/fs_test.c
@@ -2570,6 +2570,43 @@ const struct rule layer_dir_s2d1_execute[] = {
{},
};

+/*
+ * Tests that a layer not handling filesystem access rights does not
+ * implicitly handle LANDLOCK_ACCESS_FS_REFER, which would otherwise deny
+ * different-parent renames and links stacked on top of a filesystem ruleset.
+ */
+TEST_F_FORK(layout1, refer_with_net_layer)
+{
+ const struct rule layer_fs[] = {
+ {
+ .path = dir_s1d2,
+ .access = LANDLOCK_ACCESS_FS_REFER,
+ },
+ {
+ .path = dir_s1d3,
+ .access = LANDLOCK_ACCESS_FS_MAKE_REG,
+ },
+ {},
+ };
+ const struct landlock_ruleset_attr ruleset_net = {
+ .handled_access_net = LANDLOCK_ACCESS_NET_BIND_TCP,
+ };
+
+ /* layer_fs alone: different-parent rename is allowed. */
+ enforce_fs(_metadata, LANDLOCK_ACCESS_FS_REFER |
+ LANDLOCK_ACCESS_FS_MAKE_REG,
+ layer_fs);
+ ASSERT_EQ(0, test_rename(file1_s1d2, file1_s1d3));
+ ASSERT_EQ(0, test_rename(file1_s1d3, file1_s1d2));
+ ASSERT_EQ(0, test_exchange(file2_s1d2, file2_s1d3));
+
+ /* Stacks a network-only layer: still allowed. */
+ drop_access_rights(_metadata, &ruleset_net);
+ ASSERT_EQ(0, test_rename(file1_s1d2, file1_s1d3));
+ ASSERT_EQ(0, test_rename(file1_s1d3, file1_s1d2));
+ ASSERT_EQ(0, test_exchange(file2_s1d2, file2_s1d3));
+}
+
/*
* Tests precedence over renames: denied by default for different parent
* directories, *with* a rule matching a parent directory, but not directly
--
2.56.0