* [PATCH] selftests/landlock: Add tests for access through disconnected paths @ 2025-06-14 18:25 Tingmao Wang 2025-06-15 16:16 ` Tingmao Wang ` (2 more replies) 0 siblings, 3 replies; 11+ messages in thread From: Tingmao Wang @ 2025-06-14 18:25 UTC (permalink / raw) To: Mickaël Salaün, Günther Noack Cc: Tingmao Wang, Song Liu, linux-security-module, linux-fsdevel This adds a test for the edge case discussed in [1], and in addition also test rename operations when the operands are through disconnected paths, as that go through a separate code path in Landlock. [1]: https://lore.kernel.org/linux-security-module/027d5190-b37a-40a8-84e9-4ccbc352bcdf@maowtm.org/ This has resulted in a WARNING, due to collect_domain_accesses() not expecting to reach a different root from path->mnt: # RUN layout1_bind.path_disconnected ... # OK layout1_bind.path_disconnected ok 96 layout1_bind.path_disconnected # RUN layout1_bind.path_disconnected_rename ... [..] ------------[ cut here ]------------ [..] WARNING: CPU: 3 PID: 385 at security/landlock/fs.c:1065 collect_domain_accesses [..] ... [..] RIP: 0010:collect_domain_accesses (security/landlock/fs.c:1065 (discriminator 2) security/landlock/fs.c:1031 (discriminator 2)) [..] current_check_refer_path (security/landlock/fs.c:1205) [..] ... [..] hook_path_rename (security/landlock/fs.c:1526) [..] security_path_rename (security/security.c:2026 (discriminator 1)) [..] do_renameat2 (fs/namei.c:5264) # OK layout1_bind.path_disconnected_rename ok 97 layout1_bind.path_disconnected_rename My understanding is that terminating at the mountpoint is basically an optimization, so that for rename operations we only walks the path from the mountpoint to the real root once. We probably want to keep this optimization, as disconnected paths are probably a very rare edge case. This might need more thinking, but maybe if one of the operands is disconnected, we can just let it walk until IS_ROOT(dentry), and also collect access for the other path until IS_ROOT(dentry), then call is_access_to_paths_allowed() passing in the root dentry we walked to? (In this case is_access_to_paths_allowed will not do any walking and just make an access decision.) Letting the walk continue until IS_ROOT(dentry) is what is_access_to_paths_allowed() effectively does for non-renames. (Also note: moving the const char definitions a bit above so that we can use the path for s4d1 in cleanup code.) Signed-off-by: Tingmao Wang <m@maowtm.org> --- tools/testing/selftests/landlock/fs_test.c | 271 ++++++++++++++++++++- 1 file changed, 268 insertions(+), 3 deletions(-) diff --git a/tools/testing/selftests/landlock/fs_test.c b/tools/testing/selftests/landlock/fs_test.c index 73729382d40f..d042a742a1c5 100644 --- a/tools/testing/selftests/landlock/fs_test.c +++ b/tools/testing/selftests/landlock/fs_test.c @@ -4521,6 +4521,17 @@ TEST_F_FORK(ioctl, handle_file_access_file) FIXTURE(layout1_bind) {}; /* clang-format on */ +static const char bind_dir_s1d3[] = TMP_DIR "/s2d1/s2d2/s1d3"; +static const char bind_file1_s1d3[] = TMP_DIR "/s2d1/s2d2/s1d3/f1"; +/* Move targets for disconnected path tests */ +static const char dir_s4d1[] = TMP_DIR "/s4d1"; +static const char file1_s4d1[] = TMP_DIR "/s4d1/f1"; +static const char file2_s4d1[] = TMP_DIR "/s4d1/f2"; +static const char dir_s4d2[] = TMP_DIR "/s4d1/s4d2"; +static const char file1_s4d2[] = TMP_DIR "/s4d1/s4d2/f1"; +static const char file1_name[] = "f1"; +static const char file2_name[] = "f2"; + FIXTURE_SETUP(layout1_bind) { prepare_layout(_metadata); @@ -4536,14 +4547,14 @@ FIXTURE_TEARDOWN_PARENT(layout1_bind) { /* umount(dir_s2d2)) is handled by namespace lifetime. */ + remove_path(file1_s4d1); + remove_path(file2_s4d1); + remove_layout1(_metadata); cleanup_layout(_metadata); } -static const char bind_dir_s1d3[] = TMP_DIR "/s2d1/s2d2/s1d3"; -static const char bind_file1_s1d3[] = TMP_DIR "/s2d1/s2d2/s1d3/f1"; - /* * layout1_bind hierarchy: * @@ -4766,6 +4777,260 @@ TEST_F_FORK(layout1_bind, reparent_cross_mount) ASSERT_EQ(0, rename(bind_file1_s1d3, file1_s2d2)); } +/* + * Make sure access to file through a disconnected path works as expected. + * This test uses s4d1 as the move target. + */ +TEST_F_FORK(layout1_bind, path_disconnected) +{ + const struct rule layer1_allow_all[] = { + { + .path = TMP_DIR, + .access = ACCESS_ALL, + }, + {}, + }; + + const struct rule layer2_allow_just_f1[] = { + { + .path = file1_s1d3, + .access = LANDLOCK_ACCESS_FS_READ_FILE, + }, + {}, + }; + + const struct rule layer3_only_s1d2[] = { + { + .path = dir_s1d2, + .access = LANDLOCK_ACCESS_FS_READ_FILE, + }, + {}, + }; + + /* Landlock should not deny access just because it is disconnected */ + int ruleset_fd = + create_ruleset(_metadata, ACCESS_ALL, layer1_allow_all); + /* + * Create the new ruleset now before we move the dir containing the + * file + */ + int ruleset_fd_l2 = + create_ruleset(_metadata, ACCESS_RW, layer2_allow_just_f1); + int ruleset_fd_l3 = + create_ruleset(_metadata, ACCESS_RW, layer3_only_s1d2); + int bind_s1d3_fd; + + ASSERT_LE(0, ruleset_fd); + ASSERT_LE(0, ruleset_fd_l2); + ASSERT_LE(0, ruleset_fd_l3); + + enforce_ruleset(_metadata, ruleset_fd); + ASSERT_EQ(0, close(ruleset_fd)); + + bind_s1d3_fd = open(bind_dir_s1d3, O_PATH | O_CLOEXEC); + + ASSERT_LE(0, bind_s1d3_fd); + /* Test access is possible before we move */ + ASSERT_EQ(0, test_open_rel(bind_s1d3_fd, file1_name, O_RDONLY)); + /* Make it disconnected */ + ASSERT_EQ(0, rename(dir_s1d3, dir_s4d1)) + { + TH_LOG("Failed to rename %s to %s: %s", dir_s1d3, dir_s4d1, + strerror(errno)); + } + /* Test access still possible */ + ASSERT_EQ(0, test_open_rel(bind_s1d3_fd, file1_name, O_RDONLY)); + /* + * Test ".." not possibe (not because of landlock, but just because + * it's disconnected) + */ + ASSERT_EQ(ENOENT, + test_open_rel(bind_s1d3_fd, "..", O_RDONLY | O_DIRECTORY)); + + /* Should still work with a narrower rule */ + enforce_ruleset(_metadata, ruleset_fd_l2); + ASSERT_EQ(0, close(ruleset_fd_l2)); + + ASSERT_EQ(0, test_open(file1_s4d1, O_RDONLY)); + ASSERT_EQ(0, test_open_rel(bind_s1d3_fd, file1_name, O_RDONLY)); + ASSERT_EQ(EACCES, test_open_rel(bind_s1d3_fd, file2_name, O_RDONLY)); + + /* + * But if we only allow access to under the original dir, then it + * should be denied. + */ + enforce_ruleset(_metadata, ruleset_fd_l3); + ASSERT_EQ(0, close(ruleset_fd_l3)); + ASSERT_EQ(EACCES, test_open(file1_s4d1, O_RDONLY)); + ASSERT_EQ(EACCES, test_open_rel(bind_s1d3_fd, file1_name, O_RDONLY)); +} + +/* + * Test that we can rename to make files disconnected, and rename it back, + * under landlock. This test uses s4d2 as the move target, so that we can + * have a rule allowing refers on the move target's immediate parent. + */ +TEST_F_FORK(layout1_bind, path_disconnected_rename) +{ + const struct rule layer1[] = { + { + .path = dir_s1d2, + .access = LANDLOCK_ACCESS_FS_REFER | + LANDLOCK_ACCESS_FS_MAKE_DIR | + LANDLOCK_ACCESS_FS_REMOVE_DIR | + LANDLOCK_ACCESS_FS_MAKE_REG | + LANDLOCK_ACCESS_FS_REMOVE_FILE | + LANDLOCK_ACCESS_FS_READ_FILE, + }, + { + .path = dir_s4d1, + .access = LANDLOCK_ACCESS_FS_REFER | + LANDLOCK_ACCESS_FS_MAKE_DIR | + LANDLOCK_ACCESS_FS_REMOVE_DIR | + LANDLOCK_ACCESS_FS_MAKE_REG | + LANDLOCK_ACCESS_FS_REMOVE_FILE | + LANDLOCK_ACCESS_FS_READ_FILE, + }, + {} + }; + + const struct rule layer2_only_s1d2[] = { + { + .path = dir_s1d2, + .access = LANDLOCK_ACCESS_FS_READ_FILE, + }, + {}, + }; + + ASSERT_EQ(0, mkdir(dir_s4d1, 0755)) + { + TH_LOG("Failed to create %s: %s", dir_s4d1, strerror(errno)); + } + + int ruleset_fd = create_ruleset(_metadata, ACCESS_ALL, layer1); + int ruleset_fd_l2 = create_ruleset( + _metadata, LANDLOCK_ACCESS_FS_READ_FILE, layer2_only_s1d2); + pid_t child_pid; + int bind_s1d3_fd, status; + + ASSERT_LE(0, ruleset_fd); + ASSERT_LE(0, ruleset_fd_l2); + + enforce_ruleset(_metadata, ruleset_fd); + ASSERT_EQ(0, close(ruleset_fd)); + + bind_s1d3_fd = open(bind_dir_s1d3, O_PATH | O_CLOEXEC); + ASSERT_LE(0, bind_s1d3_fd); + ASSERT_EQ(0, test_open_rel(bind_s1d3_fd, file1_name, O_RDONLY)); + + ASSERT_EQ(0, rename(dir_s1d3, dir_s4d2)) + { + TH_LOG("Failed to rename %s to %s: %s", dir_s1d3, dir_s4d2, + strerror(errno)); + } + + /* + * Since file is no longer under s1d2, we should not be able to access + * it if we enforced layer 2. Do a fork to test this so we don't + * prevent ourselves from renaming it back later. + */ + child_pid = fork(); + if (child_pid == 0) { + enforce_ruleset(_metadata, ruleset_fd_l2); + ASSERT_EQ(0, close(ruleset_fd_l2)); + ASSERT_EQ(EACCES, + test_open_rel(bind_s1d3_fd, file1_name, O_RDONLY)); + ASSERT_EQ(EACCES, test_open(file1_s4d2, O_RDONLY)); + + /* + * Test that access widening checks indeed prevents us from + * renaming it back + */ + ASSERT_EQ(-1, rename(dir_s4d2, dir_s1d3)); + ASSERT_EQ(EXDEV, errno); + /* + * Including through the now disconnected fd (but it should return + * EXDEV) + */ + ASSERT_EQ(-1, renameat(bind_s1d3_fd, file1_name, AT_FDCWD, + file1_s2d2)); + ASSERT_EQ(EXDEV, errno); + _exit(!__test_passed(_metadata)); + return; + } + + ASSERT_NE(-1, child_pid); + ASSERT_EQ(child_pid, waitpid(child_pid, &status, 0)); + ASSERT_EQ(1, WIFEXITED(status)); + ASSERT_EQ(EXIT_SUCCESS, WEXITSTATUS(status)); + + ASSERT_EQ(0, rename(dir_s4d2, dir_s1d3)) + { + TH_LOG("Failed to rename %s back to %s: %s", dir_s4d1, dir_s1d3, + strerror(errno)); + } + + /* Now check that we can access it under l2 */ + child_pid = fork(); + if (child_pid == 0) { + enforce_ruleset(_metadata, ruleset_fd_l2); + ASSERT_EQ(0, close(ruleset_fd_l2)); + ASSERT_EQ(0, test_open_rel(bind_s1d3_fd, file1_name, O_RDONLY)); + ASSERT_EQ(0, test_open(file1_s1d3, O_RDONLY)); + _exit(!__test_passed(_metadata)); + return; + } + ASSERT_NE(-1, child_pid); + ASSERT_EQ(child_pid, waitpid(child_pid, &status, 0)); + ASSERT_EQ(1, WIFEXITED(status)); + ASSERT_EQ(EXIT_SUCCESS, WEXITSTATUS(status)); + + /* + * Also test that we can rename via a disconnected path. We move the + * dir back to the disconnected place first, then we rename file1 to + * file2 through our dir fd. + */ + ASSERT_EQ(0, rename(dir_s1d3, dir_s4d2)) + { + TH_LOG("Failed to rename %s to %s: %s", dir_s1d3, dir_s4d2, + strerror(errno)); + } + ASSERT_EQ(0, + renameat(bind_s1d3_fd, file1_name, bind_s1d3_fd, file2_name)) + { + TH_LOG("Failed to rename %s to %s through disconnected %s: %s", + file1_name, file2_name, bind_dir_s1d3, strerror(errno)); + } + ASSERT_EQ(0, test_open_rel(bind_s1d3_fd, file2_name, O_RDONLY)); + ASSERT_EQ(0, renameat(bind_s1d3_fd, file2_name, AT_FDCWD, file1_s2d2)) + { + TH_LOG("Failed to rename %s to %s through disconnected %s: %s", + file2_name, file1_s2d2, bind_dir_s1d3, strerror(errno)); + } + ASSERT_EQ(0, test_open(file1_s2d2, O_RDONLY)); + ASSERT_EQ(0, test_open(file1_s1d2, O_RDONLY)); + + /* Move it back using the disconnected path as the target */ + ASSERT_EQ(0, renameat(AT_FDCWD, file1_s2d2, bind_s1d3_fd, file1_name)) + { + TH_LOG("Failed to rename %s to %s through disconnected %s: %s", + file1_s1d2, file1_name, bind_dir_s1d3, strerror(errno)); + } + + /* Now make it connected again */ + ASSERT_EQ(0, rename(dir_s4d2, dir_s1d3)) + { + TH_LOG("Failed to rename %s back to %s: %s", dir_s4d2, dir_s1d3, + strerror(errno)); + } + + /* Check again that we can access it under l2 */ + enforce_ruleset(_metadata, ruleset_fd_l2); + ASSERT_EQ(0, close(ruleset_fd_l2)); + ASSERT_EQ(0, test_open_rel(bind_s1d3_fd, file1_name, O_RDONLY)); + ASSERT_EQ(0, test_open(file1_s1d3, O_RDONLY)); +} + #define LOWER_BASE TMP_DIR "/lower" #define LOWER_DATA LOWER_BASE "/data" static const char lower_fl1[] = LOWER_DATA "/fl1"; base-commit: 19272b37aa4f83ca52bdf9c16d5d81bdd1354494 -- 2.49.0 ^ permalink raw reply related [flat|nested] 11+ messages in thread
* Re: [PATCH] selftests/landlock: Add tests for access through disconnected paths 2025-06-14 18:25 [PATCH] selftests/landlock: Add tests for access through disconnected paths Tingmao Wang @ 2025-06-15 16:16 ` Tingmao Wang 2025-06-15 16:35 ` Tingmao Wang 2025-06-19 11:38 ` Mickaël Salaün 2 siblings, 0 replies; 11+ messages in thread From: Tingmao Wang @ 2025-06-15 16:16 UTC (permalink / raw) To: Mickaël Salaün, Günther Noack Cc: Song Liu, linux-security-module, linux-fsdevel On 6/14/25 19:25, Tingmao Wang wrote: > This adds a test for the edge case discussed in [1], and in addition also > test rename operations when the operands are through disconnected paths, > as that go through a separate code path in Landlock. > [..] Slightly improve comments a bit... (Another edit to add test for linkat to follow) --- tools/testing/selftests/landlock/fs_test.c | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tools/testing/selftests/landlock/fs_test.c b/tools/testing/selftests/landlock/fs_test.c index d042a742a1c5..53b167dbd39c 100644 --- a/tools/testing/selftests/landlock/fs_test.c +++ b/tools/testing/selftests/landlock/fs_test.c @@ -4779,7 +4779,7 @@ TEST_F_FORK(layout1_bind, reparent_cross_mount) /* * Make sure access to file through a disconnected path works as expected. - * This test uses s4d1 as the move target. + * This test moves s1d3 to s4d1. */ TEST_F_FORK(layout1_bind, path_disconnected) { @@ -4866,9 +4866,9 @@ TEST_F_FORK(layout1_bind, path_disconnected) } /* - * Test that we can rename to make files disconnected, and rename it back, - * under landlock. This test uses s4d2 as the move target, so that we can - * have a rule allowing refers on the move target's immediate parent. + * Test that renameat with disconnected paths works under landlock. This + * test moves s1d3 to s4d2, so that we can have a rule allowing refers on + * the move target's immediate parent. */ TEST_F_FORK(layout1_bind, path_disconnected_rename) { @@ -4998,7 +4998,7 @@ TEST_F_FORK(layout1_bind, path_disconnected_rename) ASSERT_EQ(0, renameat(bind_s1d3_fd, file1_name, bind_s1d3_fd, file2_name)) { - TH_LOG("Failed to rename %s to %s through disconnected %s: %s", + TH_LOG("Failed to rename %s to %s within disconnected %s: %s", file1_name, file2_name, bind_dir_s1d3, strerror(errno)); } ASSERT_EQ(0, test_open_rel(bind_s1d3_fd, file2_name, O_RDONLY)); -- 2.49.0 ^ permalink raw reply related [flat|nested] 11+ messages in thread
* Re: [PATCH] selftests/landlock: Add tests for access through disconnected paths 2025-06-14 18:25 [PATCH] selftests/landlock: Add tests for access through disconnected paths Tingmao Wang 2025-06-15 16:16 ` Tingmao Wang @ 2025-06-15 16:35 ` Tingmao Wang 2025-06-19 11:38 ` Mickaël Salaün 2 siblings, 0 replies; 11+ messages in thread From: Tingmao Wang @ 2025-06-15 16:35 UTC (permalink / raw) To: Mickaël Salaün, Günther Noack Cc: Song Liu, linux-security-module, linux-fsdevel On 6/14/25 19:25, Tingmao Wang wrote: > This adds a test for the edge case discussed in [1], and in addition also > test rename operations when the operands are through disconnected paths, > as that go through a separate code path in Landlock. > [..] Hi, Since linkat also uses the separate refer check, and since it's not too difficult given what we already have, I've made an edit to add tests for linkat under disconnected paths as well. Hopefully this helps better confirm that any fix we make does not change existing user-facing behaviour (unless, of course, if we want to). Tested with qemu as well as UML. Passes without panic_on_warn. Signed-off-by: Tingmao Wang <m@maowtm.org> --- tools/testing/selftests/landlock/fs_test.c | 118 +++++++++++++++++++++ 1 file changed, 118 insertions(+) diff --git a/tools/testing/selftests/landlock/fs_test.c b/tools/testing/selftests/landlock/fs_test.c index 53b167dbd39c..055f6de25d05 100644 --- a/tools/testing/selftests/landlock/fs_test.c +++ b/tools/testing/selftests/landlock/fs_test.c @@ -5031,6 +5031,124 @@ TEST_F_FORK(layout1_bind, path_disconnected_rename) ASSERT_EQ(0, test_open(file1_s1d3, O_RDONLY)); } +/* + * Test that linkat with disconnected paths works under Landlock. This + * test moves s1d3 to s4d1. + */ +TEST_F_FORK(layout1_bind, path_disconnected_link) +{ + /* Ruleset to be applied after renaming s1d3 to s4d1 */ + const struct rule layer1[] = { + { + .path = dir_s4d1, + .access = LANDLOCK_ACCESS_FS_REFER | + LANDLOCK_ACCESS_FS_READ_FILE | + LANDLOCK_ACCESS_FS_MAKE_REG | + LANDLOCK_ACCESS_FS_REMOVE_FILE, + }, + { + .path = dir_s2d2, + .access = LANDLOCK_ACCESS_FS_REFER | + LANDLOCK_ACCESS_FS_READ_FILE | + LANDLOCK_ACCESS_FS_MAKE_REG | + LANDLOCK_ACCESS_FS_REMOVE_FILE, + }, + {} + }; + int ruleset_fd, bind_s1d3_fd; + + /* Remove unneeded files created by layout1, otherwise we will EEXIST */ + ASSERT_EQ(0, unlink(file1_s1d2)); + ASSERT_EQ(0, unlink(file2_s1d3)); + + bind_s1d3_fd = open(bind_dir_s1d3, O_PATH | O_CLOEXEC); + ASSERT_LE(0, bind_s1d3_fd); + ASSERT_EQ(0, test_open_rel(bind_s1d3_fd, file1_name, O_RDONLY)); + + /* Make bind_s1d3_fd disconnected */ + ASSERT_EQ(0, rename(dir_s1d3, dir_s4d1)) + { + TH_LOG("Failed to rename %s to %s: %s", dir_s1d3, dir_s4d1, + strerror(errno)); + } + /* Need this later to test different parent link */ + ASSERT_EQ(0, mkdir(dir_s4d2, 0755)) + { + TH_LOG("Failed to create %s: %s", dir_s4d2, strerror(errno)); + } + + ruleset_fd = create_ruleset(_metadata, ACCESS_ALL, layer1); + ASSERT_LE(0, ruleset_fd); + enforce_ruleset(_metadata, ruleset_fd); + ASSERT_EQ(0, close(ruleset_fd)); + + /* disconnected to connected */ + ASSERT_EQ(0, linkat(bind_s1d3_fd, file1_name, AT_FDCWD, file1_s2d2, 0)) + { + TH_LOG("Failed to link %s to %s via disconnected %s: %s", + file1_name, file1_s2d2, bind_dir_s1d3, strerror(errno)); + } + /* Test that we can access via the new link */ + ASSERT_EQ(0, test_open(file1_s2d2, O_RDONLY)) + { + TH_LOG("Failed to open newly linked %s: %s", file1_s2d2, + strerror(errno)); + } + /* As well as the old one */ + ASSERT_EQ(0, test_open(file1_s4d1, O_RDONLY)) + { + TH_LOG("Failed to open original %s: %s", file1_s4d1, + strerror(errno)); + } + + /* connected to disconnected */ + ASSERT_EQ(0, unlink(file1_s4d1)); + ASSERT_EQ(0, linkat(AT_FDCWD, file1_s2d2, bind_s1d3_fd, file2_name, 0)) + { + TH_LOG("Failed to link %s to %s via disconnected %s: %s", + file1_s2d2, file2_name, bind_dir_s1d3, strerror(errno)); + } + ASSERT_EQ(0, test_open(file2_s4d1, O_RDONLY)); + ASSERT_EQ(0, unlink(file1_s2d2)); + + /* disconnected to disconnected (same parent) */ + ASSERT_EQ(0, + linkat(bind_s1d3_fd, file2_name, bind_s1d3_fd, file1_name, 0)) + { + TH_LOG("Failed to link %s to %s within disconnected %s: %s", + file2_name, file1_name, bind_dir_s1d3, strerror(errno)); + } + ASSERT_EQ(0, test_open(file1_s4d1, O_RDONLY)) + { + TH_LOG("Failed to open newly linked %s: %s", file1_s4d1, + strerror(errno)); + } + ASSERT_EQ(0, test_open_rel(bind_s1d3_fd, file1_name, O_RDONLY)) + { + TH_LOG("Failed to open %s through newly created link under disconnected path: %s", + file1_name, strerror(errno)); + } + ASSERT_EQ(0, unlink(file2_s4d1)); + + /* disconnected to disconnected (different parent) */ + ASSERT_EQ(0, + linkat(bind_s1d3_fd, file1_name, bind_s1d3_fd, "s4d2/f1", 0)) + { + TH_LOG("Failed to link %s to %s within disconnected %s: %s", + file1_name, "s4d2/f1", bind_dir_s1d3, strerror(errno)); + } + ASSERT_EQ(0, test_open(file1_s4d2, O_RDONLY)) + { + TH_LOG("Failed to open %s after link: %s", file1_s4d2, + strerror(errno)); + } + ASSERT_EQ(0, test_open_rel(bind_s1d3_fd, "s4d2/f1", O_RDONLY)) + { + TH_LOG("Failed to open %s through disconnected path after link: %s", + "s4d2/f1", strerror(errno)); + } +} + #define LOWER_BASE TMP_DIR "/lower" #define LOWER_DATA LOWER_BASE "/data" static const char lower_fl1[] = LOWER_DATA "/fl1"; -- 2.49.0 ^ permalink raw reply related [flat|nested] 11+ messages in thread
* Re: [PATCH] selftests/landlock: Add tests for access through disconnected paths 2025-06-14 18:25 [PATCH] selftests/landlock: Add tests for access through disconnected paths Tingmao Wang 2025-06-15 16:16 ` Tingmao Wang 2025-06-15 16:35 ` Tingmao Wang @ 2025-06-19 11:38 ` Mickaël Salaün 2025-06-22 15:42 ` Tingmao Wang 2 siblings, 1 reply; 11+ messages in thread From: Mickaël Salaün @ 2025-06-19 11:38 UTC (permalink / raw) To: Tingmao Wang Cc: Günther Noack, Song Liu, linux-security-module, linux-fsdevel On Sat, Jun 14, 2025 at 07:25:02PM +0100, Tingmao Wang wrote: > This adds a test for the edge case discussed in [1], and in addition also > test rename operations when the operands are through disconnected paths, > as that go through a separate code path in Landlock. > > [1]: https://lore.kernel.org/linux-security-module/027d5190-b37a-40a8-84e9-4ccbc352bcdf@maowtm.org/ > > This has resulted in a WARNING, due to collect_domain_accesses() not > expecting to reach a different root from path->mnt: > > # RUN layout1_bind.path_disconnected ... > # OK layout1_bind.path_disconnected > ok 96 layout1_bind.path_disconnected > # RUN layout1_bind.path_disconnected_rename ... > [..] ------------[ cut here ]------------ > [..] WARNING: CPU: 3 PID: 385 at security/landlock/fs.c:1065 collect_domain_accesses > [..] ... > [..] RIP: 0010:collect_domain_accesses (security/landlock/fs.c:1065 (discriminator 2) security/landlock/fs.c:1031 (discriminator 2)) > [..] current_check_refer_path (security/landlock/fs.c:1205) > [..] ... > [..] hook_path_rename (security/landlock/fs.c:1526) > [..] security_path_rename (security/security.c:2026 (discriminator 1)) > [..] do_renameat2 (fs/namei.c:5264) > # OK layout1_bind.path_disconnected_rename > ok 97 layout1_bind.path_disconnected_rename Good catch and thanks for the tests! I sent a fix: https://lore.kernel.org/all/20250618134734.1673254-1-mic@digikod.net/ > > My understanding is that terminating at the mountpoint is basically an > optimization, so that for rename operations we only walks the path from > the mountpoint to the real root once. We probably want to keep this > optimization, as disconnected paths are probably a very rare edge case. Rename operations can only happen within the same mount point, otherwise the kernel returns -EXDEV. The collect_domain_accesses() is called for the source and the destination of a rename to walk to their common mount point, if any. We could maybe improve this walk by doing them at the same time but because we don't know the depth of each path, I'm not sure the required extra complexity would be worth it. The current approach is simple and opportunistically limits the walks. > > This might need more thinking, but maybe if one of the operands is > disconnected, we can just let it walk until IS_ROOT(dentry), and also > collect access for the other path until IS_ROOT(dentry), then call > is_access_to_paths_allowed() passing in the root dentry we walked to? (In > this case is_access_to_paths_allowed will not do any walking and just make > an access decision.) If one side is in a disconnected directory and not the other side, the rename would be denied by the VFS, but Landlock should still log (and then deny) the side that would be denied anyway. > > Letting the walk continue until IS_ROOT(dentry) is what > is_access_to_paths_allowed() effectively does for non-renames. > > (Also note: moving the const char definitions a bit above so that we can > use the path for s4d1 in cleanup code.) > > Signed-off-by: Tingmao Wang <m@maowtm.org> I squashed your patches and push them to my next branch with some minor changes. Please let me know if there is something wrong. > --- > tools/testing/selftests/landlock/fs_test.c | 271 ++++++++++++++++++++- > 1 file changed, 268 insertions(+), 3 deletions(-) > > diff --git a/tools/testing/selftests/landlock/fs_test.c b/tools/testing/selftests/landlock/fs_test.c > index 73729382d40f..d042a742a1c5 100644 > --- a/tools/testing/selftests/landlock/fs_test.c > +++ b/tools/testing/selftests/landlock/fs_test.c > @@ -4521,6 +4521,17 @@ TEST_F_FORK(ioctl, handle_file_access_file) > FIXTURE(layout1_bind) {}; > /* clang-format on */ > > +static const char bind_dir_s1d3[] = TMP_DIR "/s2d1/s2d2/s1d3"; > +static const char bind_file1_s1d3[] = TMP_DIR "/s2d1/s2d2/s1d3/f1"; > +/* Move targets for disconnected path tests */ > +static const char dir_s4d1[] = TMP_DIR "/s4d1"; > +static const char file1_s4d1[] = TMP_DIR "/s4d1/f1"; > +static const char file2_s4d1[] = TMP_DIR "/s4d1/f2"; > +static const char dir_s4d2[] = TMP_DIR "/s4d1/s4d2"; > +static const char file1_s4d2[] = TMP_DIR "/s4d1/s4d2/f1"; > +static const char file1_name[] = "f1"; > +static const char file2_name[] = "f2"; > + > FIXTURE_SETUP(layout1_bind) > { > prepare_layout(_metadata); > @@ -4536,14 +4547,14 @@ FIXTURE_TEARDOWN_PARENT(layout1_bind) > { > /* umount(dir_s2d2)) is handled by namespace lifetime. */ > > + remove_path(file1_s4d1); > + remove_path(file2_s4d1); > + > remove_layout1(_metadata); > > cleanup_layout(_metadata); > } > > -static const char bind_dir_s1d3[] = TMP_DIR "/s2d1/s2d2/s1d3"; > -static const char bind_file1_s1d3[] = TMP_DIR "/s2d1/s2d2/s1d3/f1"; > - > /* > * layout1_bind hierarchy: > * > @@ -4766,6 +4777,260 @@ TEST_F_FORK(layout1_bind, reparent_cross_mount) > ASSERT_EQ(0, rename(bind_file1_s1d3, file1_s2d2)); > } > > +/* > + * Make sure access to file through a disconnected path works as expected. > + * This test uses s4d1 as the move target. > + */ > +TEST_F_FORK(layout1_bind, path_disconnected) > +{ > + const struct rule layer1_allow_all[] = { > + { > + .path = TMP_DIR, > + .access = ACCESS_ALL, > + }, > + {}, > + }; > + > + const struct rule layer2_allow_just_f1[] = { > + { > + .path = file1_s1d3, > + .access = LANDLOCK_ACCESS_FS_READ_FILE, > + }, > + {}, > + }; > + > + const struct rule layer3_only_s1d2[] = { > + { > + .path = dir_s1d2, > + .access = LANDLOCK_ACCESS_FS_READ_FILE, > + }, > + {}, > + }; > + > + /* Landlock should not deny access just because it is disconnected */ > + int ruleset_fd = > + create_ruleset(_metadata, ACCESS_ALL, layer1_allow_all); > + /* > + * Create the new ruleset now before we move the dir containing the > + * file Please use third person for most comments, and end them with a final dot (for sentences). > + */ > + int ruleset_fd_l2 = > + create_ruleset(_metadata, ACCESS_RW, layer2_allow_just_f1); > + int ruleset_fd_l3 = > + create_ruleset(_metadata, ACCESS_RW, layer3_only_s1d2); > + int bind_s1d3_fd; > + > + ASSERT_LE(0, ruleset_fd); > + ASSERT_LE(0, ruleset_fd_l2); > + ASSERT_LE(0, ruleset_fd_l3); > + > + enforce_ruleset(_metadata, ruleset_fd); > + ASSERT_EQ(0, close(ruleset_fd)); I'll replace most ASSERT_* with EXPECT_* ASSERT should only be used when checking a dependency for the following tests. It's not apply everywhere but new tests should follow this rule. > + > + bind_s1d3_fd = open(bind_dir_s1d3, O_PATH | O_CLOEXEC); > + > + ASSERT_LE(0, bind_s1d3_fd); > + /* Test access is possible before we move */ > + ASSERT_EQ(0, test_open_rel(bind_s1d3_fd, file1_name, O_RDONLY)); > + /* Make it disconnected */ > + ASSERT_EQ(0, rename(dir_s1d3, dir_s4d1)) > + { > + TH_LOG("Failed to rename %s to %s: %s", dir_s1d3, dir_s4d1, > + strerror(errno)); > + } > + /* Test access still possible */ > + ASSERT_EQ(0, test_open_rel(bind_s1d3_fd, file1_name, O_RDONLY)); > + /* > + * Test ".." not possibe (not because of landlock, but just because > + * it's disconnected) > + */ > + ASSERT_EQ(ENOENT, > + test_open_rel(bind_s1d3_fd, "..", O_RDONLY | O_DIRECTORY)); > + > + /* Should still work with a narrower rule */ > + enforce_ruleset(_metadata, ruleset_fd_l2); > + ASSERT_EQ(0, close(ruleset_fd_l2)); > + > + ASSERT_EQ(0, test_open(file1_s4d1, O_RDONLY)); > + ASSERT_EQ(0, test_open_rel(bind_s1d3_fd, file1_name, O_RDONLY)); > + ASSERT_EQ(EACCES, test_open_rel(bind_s1d3_fd, file2_name, O_RDONLY)); > + > + /* > + * But if we only allow access to under the original dir, then it > + * should be denied. > + */ > + enforce_ruleset(_metadata, ruleset_fd_l3); > + ASSERT_EQ(0, close(ruleset_fd_l3)); > + ASSERT_EQ(EACCES, test_open(file1_s4d1, O_RDONLY)); > + ASSERT_EQ(EACCES, test_open_rel(bind_s1d3_fd, file1_name, O_RDONLY)); > +} > + > +/* > + * Test that we can rename to make files disconnected, and rename it back, > + * under landlock. This test uses s4d2 as the move target, so that we can > + * have a rule allowing refers on the move target's immediate parent. > + */ > +TEST_F_FORK(layout1_bind, path_disconnected_rename) > +{ > + const struct rule layer1[] = { > + { > + .path = dir_s1d2, > + .access = LANDLOCK_ACCESS_FS_REFER | > + LANDLOCK_ACCESS_FS_MAKE_DIR | > + LANDLOCK_ACCESS_FS_REMOVE_DIR | > + LANDLOCK_ACCESS_FS_MAKE_REG | > + LANDLOCK_ACCESS_FS_REMOVE_FILE | > + LANDLOCK_ACCESS_FS_READ_FILE, > + }, > + { > + .path = dir_s4d1, > + .access = LANDLOCK_ACCESS_FS_REFER | > + LANDLOCK_ACCESS_FS_MAKE_DIR | > + LANDLOCK_ACCESS_FS_REMOVE_DIR | > + LANDLOCK_ACCESS_FS_MAKE_REG | > + LANDLOCK_ACCESS_FS_REMOVE_FILE | > + LANDLOCK_ACCESS_FS_READ_FILE, > + }, > + {} > + }; > + > + const struct rule layer2_only_s1d2[] = { > + { > + .path = dir_s1d2, > + .access = LANDLOCK_ACCESS_FS_READ_FILE, > + }, > + {}, > + }; > + > + ASSERT_EQ(0, mkdir(dir_s4d1, 0755)) > + { > + TH_LOG("Failed to create %s: %s", dir_s4d1, strerror(errno)); > + } > + > + int ruleset_fd = create_ruleset(_metadata, ACCESS_ALL, layer1); > + int ruleset_fd_l2 = create_ruleset( > + _metadata, LANDLOCK_ACCESS_FS_READ_FILE, layer2_only_s1d2); > + pid_t child_pid; > + int bind_s1d3_fd, status; I'll move variable declarations before mkdir call. > + > + ASSERT_LE(0, ruleset_fd); > + ASSERT_LE(0, ruleset_fd_l2); > + > + enforce_ruleset(_metadata, ruleset_fd); > + ASSERT_EQ(0, close(ruleset_fd)); > + > + bind_s1d3_fd = open(bind_dir_s1d3, O_PATH | O_CLOEXEC); > + ASSERT_LE(0, bind_s1d3_fd); > + ASSERT_EQ(0, test_open_rel(bind_s1d3_fd, file1_name, O_RDONLY)); > + I'll add these checks to test ENOENT priority over EACCES for disconnected directory: EXPECT_EQ(EACCES, test_open_rel(bind_s1d3_fd, "..", O_DIRECTORY)); > + ASSERT_EQ(0, rename(dir_s1d3, dir_s4d2)) > + { > + TH_LOG("Failed to rename %s to %s: %s", dir_s1d3, dir_s4d2, > + strerror(errno)); > + } EXPECT_EQ(ENOENT, test_open_rel(bind_s1d3_fd, "..", O_DIRECTORY)); > + > + /* > + * Since file is no longer under s1d2, we should not be able to access > + * it if we enforced layer 2. Do a fork to test this so we don't > + * prevent ourselves from renaming it back later. > + */ > + child_pid = fork(); > + if (child_pid == 0) { > + enforce_ruleset(_metadata, ruleset_fd_l2); > + ASSERT_EQ(0, close(ruleset_fd_l2)); > + ASSERT_EQ(EACCES, > + test_open_rel(bind_s1d3_fd, file1_name, O_RDONLY)); > + ASSERT_EQ(EACCES, test_open(file1_s4d2, O_RDONLY)); > + > + /* > + * Test that access widening checks indeed prevents us from > + * renaming it back > + */ > + ASSERT_EQ(-1, rename(dir_s4d2, dir_s1d3)); > + ASSERT_EQ(EXDEV, errno); > + /* > + * Including through the now disconnected fd (but it should return > + * EXDEV) > + */ > + ASSERT_EQ(-1, renameat(bind_s1d3_fd, file1_name, AT_FDCWD, > + file1_s2d2)); > + ASSERT_EQ(EXDEV, errno); > + _exit(!__test_passed(_metadata)); I prefer to use _exit(_metadata->exit_code) for the parent to get the exit code (which would be printed with the ASSERT/EXPECT if unexpected) and for consistency with other tests. I'll change that. > + return; > + } > + > + ASSERT_NE(-1, child_pid); > + ASSERT_EQ(child_pid, waitpid(child_pid, &status, 0)); > + ASSERT_EQ(1, WIFEXITED(status)); > + ASSERT_EQ(EXIT_SUCCESS, WEXITSTATUS(status)); > + > + ASSERT_EQ(0, rename(dir_s4d2, dir_s1d3)) > + { > + TH_LOG("Failed to rename %s back to %s: %s", dir_s4d1, dir_s1d3, > + strerror(errno)); > + } > + > + /* Now check that we can access it under l2 */ > + child_pid = fork(); > + if (child_pid == 0) { > + enforce_ruleset(_metadata, ruleset_fd_l2); > + ASSERT_EQ(0, close(ruleset_fd_l2)); > + ASSERT_EQ(0, test_open_rel(bind_s1d3_fd, file1_name, O_RDONLY)); > + ASSERT_EQ(0, test_open(file1_s1d3, O_RDONLY)); > + _exit(!__test_passed(_metadata)); > + return; > + } > + ASSERT_NE(-1, child_pid); > + ASSERT_EQ(child_pid, waitpid(child_pid, &status, 0)); > + ASSERT_EQ(1, WIFEXITED(status)); > + ASSERT_EQ(EXIT_SUCCESS, WEXITSTATUS(status)); > + > + /* > + * Also test that we can rename via a disconnected path. We move the > + * dir back to the disconnected place first, then we rename file1 to > + * file2 through our dir fd. > + */ > + ASSERT_EQ(0, rename(dir_s1d3, dir_s4d2)) > + { > + TH_LOG("Failed to rename %s to %s: %s", dir_s1d3, dir_s4d2, > + strerror(errno)); > + } > + ASSERT_EQ(0, > + renameat(bind_s1d3_fd, file1_name, bind_s1d3_fd, file2_name)) Renaming files inside the same directory should always work. The following renameat(2) call checks with different directory, as well as the link tests, so we are good. > + { > + TH_LOG("Failed to rename %s to %s through disconnected %s: %s", > + file1_name, file2_name, bind_dir_s1d3, strerror(errno)); > + } > + ASSERT_EQ(0, test_open_rel(bind_s1d3_fd, file2_name, O_RDONLY)); > + ASSERT_EQ(0, renameat(bind_s1d3_fd, file2_name, AT_FDCWD, file1_s2d2)) > + { > + TH_LOG("Failed to rename %s to %s through disconnected %s: %s", > + file2_name, file1_s2d2, bind_dir_s1d3, strerror(errno)); > + } > + ASSERT_EQ(0, test_open(file1_s2d2, O_RDONLY)); > + ASSERT_EQ(0, test_open(file1_s1d2, O_RDONLY)); > + > + /* Move it back using the disconnected path as the target */ > + ASSERT_EQ(0, renameat(AT_FDCWD, file1_s2d2, bind_s1d3_fd, file1_name)) > + { > + TH_LOG("Failed to rename %s to %s through disconnected %s: %s", > + file1_s1d2, file1_name, bind_dir_s1d3, strerror(errno)); > + } > + > + /* Now make it connected again */ > + ASSERT_EQ(0, rename(dir_s4d2, dir_s1d3)) > + { > + TH_LOG("Failed to rename %s back to %s: %s", dir_s4d2, dir_s1d3, > + strerror(errno)); > + } > + > + /* Check again that we can access it under l2 */ > + enforce_ruleset(_metadata, ruleset_fd_l2); > + ASSERT_EQ(0, close(ruleset_fd_l2)); > + ASSERT_EQ(0, test_open_rel(bind_s1d3_fd, file1_name, O_RDONLY)); > + ASSERT_EQ(0, test_open(file1_s1d3, O_RDONLY)); > +} > + > #define LOWER_BASE TMP_DIR "/lower" > #define LOWER_DATA LOWER_BASE "/data" > static const char lower_fl1[] = LOWER_DATA "/fl1"; > > base-commit: 19272b37aa4f83ca52bdf9c16d5d81bdd1354494 > -- > 2.49.0 > > ^ permalink raw reply [flat|nested] 11+ messages in thread
* Re: [PATCH] selftests/landlock: Add tests for access through disconnected paths 2025-06-19 11:38 ` Mickaël Salaün @ 2025-06-22 15:42 ` Tingmao Wang 2025-06-22 17:04 ` Tingmao Wang 2025-06-23 19:40 ` Mickaël Salaün 0 siblings, 2 replies; 11+ messages in thread From: Tingmao Wang @ 2025-06-22 15:42 UTC (permalink / raw) To: Mickaël Salaün Cc: Günther Noack, Song Liu, linux-security-module, linux-fsdevel On 6/19/25 12:38, Mickaël Salaün wrote: > On Sat, Jun 14, 2025 at 07:25:02PM +0100, Tingmao Wang wrote: >> This adds a test for the edge case discussed in [1], and in addition also >> test rename operations when the operands are through disconnected paths, >> as that go through a separate code path in Landlock. >> >> [1]: https://lore.kernel.org/linux-security-module/027d5190-b37a-40a8-84e9-4ccbc352bcdf@maowtm.org/ >> >> This has resulted in a WARNING, due to collect_domain_accesses() not >> expecting to reach a different root from path->mnt: >> >> # RUN layout1_bind.path_disconnected ... >> # OK layout1_bind.path_disconnected >> ok 96 layout1_bind.path_disconnected >> # RUN layout1_bind.path_disconnected_rename ... >> [..] ------------[ cut here ]------------ >> [..] WARNING: CPU: 3 PID: 385 at security/landlock/fs.c:1065 collect_domain_accesses >> [..] ... >> [..] RIP: 0010:collect_domain_accesses (security/landlock/fs.c:1065 (discriminator 2) security/landlock/fs.c:1031 (discriminator 2)) >> [..] current_check_refer_path (security/landlock/fs.c:1205) >> [..] ... >> [..] hook_path_rename (security/landlock/fs.c:1526) >> [..] security_path_rename (security/security.c:2026 (discriminator 1)) >> [..] do_renameat2 (fs/namei.c:5264) >> # OK layout1_bind.path_disconnected_rename >> ok 97 layout1_bind.path_disconnected_rename > > Good catch and thanks for the tests! I sent a fix: > https://lore.kernel.org/all/20250618134734.1673254-1-mic@digikod.net/ > >> >> My understanding is that terminating at the mountpoint is basically an >> optimization, so that for rename operations we only walks the path from >> the mountpoint to the real root once. We probably want to keep this >> optimization, as disconnected paths are probably a very rare edge case. > > Rename operations can only happen within the same mount point, otherwise > the kernel returns -EXDEV. The collect_domain_accesses() is called for > the source and the destination of a rename to walk to their common mount > point, if any. We could maybe improve this walk by doing them at the > same time but because we don't know the depth of each path, I'm not sure > the required extra complexity would be worth it. The current approach > is simple and opportunistically limits the walks. > >> >> This might need more thinking, but maybe if one of the operands is >> disconnected, we can just let it walk until IS_ROOT(dentry), and also >> collect access for the other path until IS_ROOT(dentry), then call >> is_access_to_paths_allowed() passing in the root dentry we walked to? (In >> this case is_access_to_paths_allowed will not do any walking and just make >> an access decision.) > > If one side is in a disconnected directory and not the other side, the > rename would be denied by the VFS, Not always, right? For example in the path_disconnected_rename test we did: 5051. ASSERT_EQ(0, renameat(bind_s1d3_fd, file2_name, AT_FDCWD, file1_s2d2)) ^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^ Disconnected Connected (and it also has the other way) So looks like as long as they are still reached from two fds with two paths that have the same mnt, it will be allowed. It's just that when we do parent walk we end up missing the mount. This also means that for this refer check, if after doing the two separate walks (with the disconnected side walking all the way to IS_ROOT), we then walk from mnt again, we would allow the rename if there is a rule on mnt (or its parents) allowing file creation and refers, even if the disconnected side technically now lives outside the file hierarchy under mnt and does not have a parent with a rule allowing file creation. (I'm not saying this is necessary wrong or needs fixing, but I think it's an interesting consequence of the current implementation.) > but Landlock should still log (and then deny) the side that would be > denied anyway. > >> >> Letting the walk continue until IS_ROOT(dentry) is what >> is_access_to_paths_allowed() effectively does for non-renames. >> >> (Also note: moving the const char definitions a bit above so that we can >> use the path for s4d1 in cleanup code.) >> >> Signed-off-by: Tingmao Wang <m@maowtm.org> > > I squashed your patches and push them to my next branch with some minor > changes. Please let me know if there is something wrong. Thanks for the edits! I did notice two things: diff --git a/tools/testing/selftests/landlock/fs_test.c b/tools/testing/selftests/landlock/fs_test.c index fa0f18ec62c4..c0a54dde7225 100644 --- a/tools/testing/selftests/landlock/fs_test.c +++ b/tools/testing/selftests/landlock/fs_test.c @@ -4561,6 +4561,17 @@ TEST_F_FORK(ioctl, handle_file_access_file) FIXTURE(layout1_bind) {}; /* clang-format on */ +static const char bind_dir_s1d3[] = TMP_DIR "/s2d1/s2d2/s1d3"; +static const char bind_file1_s1d3[] = TMP_DIR "/s2d1/s2d2/s1d3/f1"; +/* Moved targets for disconnected path tests. */ ^^^^^^^^^^^^^ I had "Move targets" here as a noun (i.e. the target/destinations of the renames) +static const char dir_s4d1[] = TMP_DIR "/s4d1"; +static const char file1_s4d1[] = TMP_DIR "/s4d1/f1"; ... Also, I was just re-reading path_disconnected_rename and I managed to get confused (i.e. "how is the rename in the forked child allowed at all (i.e. how did we get EXDEV instead of EACCES) after applying layer 2?"). If you end up amending that commit, can you add this short note: diff --git a/tools/testing/selftests/landlock/fs_test.c b/tools/testing/selftests/landlock/fs_test.c index c0a54dde7225..84615c4bb7c0 100644 --- a/tools/testing/selftests/landlock/fs_test.c +++ b/tools/testing/selftests/landlock/fs_test.c @@ -4936,6 +4936,8 @@ TEST_F_FORK(layout1_bind, path_disconnected_rename) }, {} }; + + /* This layer only handles LANDLOCK_ACCESS_FS_READ_FILE only. */ const struct rule layer2_only_s1d2[] = { { .path = dir_s1d2, Wish I had caught this earlier. I mean neither of the two things are hugely important, but I assume until you actually send the merge request you can amend stuff relatively easily? If not then it's also alright :) > > [...] Ack to all suggestions, thanks! Best, Tingmao ^ permalink raw reply related [flat|nested] 11+ messages in thread
* Re: [PATCH] selftests/landlock: Add tests for access through disconnected paths 2025-06-22 15:42 ` Tingmao Wang @ 2025-06-22 17:04 ` Tingmao Wang 2025-06-23 19:40 ` Mickaël Salaün 1 sibling, 0 replies; 11+ messages in thread From: Tingmao Wang @ 2025-06-22 17:04 UTC (permalink / raw) To: Mickaël Salaün; +Cc: linux-security-module, linux-fsdevel On 6/22/25 16:42, Tingmao Wang wrote: > + /* This layer only handles LANDLOCK_ACCESS_FS_READ_FILE only. */ ^^^^^ sorry remove this ^ permalink raw reply [flat|nested] 11+ messages in thread
* Re: [PATCH] selftests/landlock: Add tests for access through disconnected paths 2025-06-22 15:42 ` Tingmao Wang 2025-06-22 17:04 ` Tingmao Wang @ 2025-06-23 19:40 ` Mickaël Salaün 2025-06-23 23:16 ` Tingmao Wang 1 sibling, 1 reply; 11+ messages in thread From: Mickaël Salaün @ 2025-06-23 19:40 UTC (permalink / raw) To: Tingmao Wang, Günther Noack Cc: Song Liu, linux-security-module, linux-fsdevel On Sun, Jun 22, 2025 at 04:42:49PM +0100, Tingmao Wang wrote: > On 6/19/25 12:38, Mickaël Salaün wrote: > > On Sat, Jun 14, 2025 at 07:25:02PM +0100, Tingmao Wang wrote: > >> This adds a test for the edge case discussed in [1], and in addition also > >> test rename operations when the operands are through disconnected paths, > >> as that go through a separate code path in Landlock. > >> > >> [1]: https://lore.kernel.org/linux-security-module/027d5190-b37a-40a8-84e9-4ccbc352bcdf@maowtm.org/ > >> > >> This has resulted in a WARNING, due to collect_domain_accesses() not > >> expecting to reach a different root from path->mnt: > >> > >> # RUN layout1_bind.path_disconnected ... > >> # OK layout1_bind.path_disconnected > >> ok 96 layout1_bind.path_disconnected > >> # RUN layout1_bind.path_disconnected_rename ... > >> [..] ------------[ cut here ]------------ > >> [..] WARNING: CPU: 3 PID: 385 at security/landlock/fs.c:1065 collect_domain_accesses > >> [..] ... > >> [..] RIP: 0010:collect_domain_accesses (security/landlock/fs.c:1065 (discriminator 2) security/landlock/fs.c:1031 (discriminator 2)) > >> [..] current_check_refer_path (security/landlock/fs.c:1205) > >> [..] ... > >> [..] hook_path_rename (security/landlock/fs.c:1526) > >> [..] security_path_rename (security/security.c:2026 (discriminator 1)) > >> [..] do_renameat2 (fs/namei.c:5264) > >> # OK layout1_bind.path_disconnected_rename > >> ok 97 layout1_bind.path_disconnected_rename > > > > Good catch and thanks for the tests! I sent a fix: > > https://lore.kernel.org/all/20250618134734.1673254-1-mic@digikod.net/ > > > >> > >> My understanding is that terminating at the mountpoint is basically an > >> optimization, so that for rename operations we only walks the path from > >> the mountpoint to the real root once. We probably want to keep this > >> optimization, as disconnected paths are probably a very rare edge case. > > > > Rename operations can only happen within the same mount point, otherwise > > the kernel returns -EXDEV. The collect_domain_accesses() is called for > > the source and the destination of a rename to walk to their common mount > > point, if any. We could maybe improve this walk by doing them at the > > same time but because we don't know the depth of each path, I'm not sure > > the required extra complexity would be worth it. The current approach > > is simple and opportunistically limits the walks. > > > >> > >> This might need more thinking, but maybe if one of the operands is > >> disconnected, we can just let it walk until IS_ROOT(dentry), and also > >> collect access for the other path until IS_ROOT(dentry), then call > >> is_access_to_paths_allowed() passing in the root dentry we walked to? (In > >> this case is_access_to_paths_allowed will not do any walking and just make > >> an access decision.) > > > > If one side is in a disconnected directory and not the other side, the > > rename would be denied by the VFS, > > Not always, right? For example in the path_disconnected_rename test we did: Correct, only the mount point matter. > > 5051. ASSERT_EQ(0, renameat(bind_s1d3_fd, file2_name, AT_FDCWD, file1_s2d2)) > ^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^ > Disconnected Connected > > (and it also has the other way) > > So looks like as long as they are still reached from two fds with two > paths that have the same mnt, it will be allowed. It's just that when we > do parent walk we end up missing the mount. This also means that for this > refer check, if after doing the two separate walks (with the disconnected > side walking all the way to IS_ROOT), we then walk from mnt again, we > would allow the rename if there is a rule on mnt (or its parents) allowing > file creation and refers, even if the disconnected side technically now > lives outside the file hierarchy under mnt and does not have a parent with > a rule allowing file creation. > > (I'm not saying this is necessary wrong or needs fixing, but I think it's > an interesting consequence of the current implementation.) Hmm, that's indeed a very subtle side effect. One issue with the current implementation is that if a directory between the mount point and the source has REFER, and another directory not part of the source hierarchy but part of the disconnected directory's hierarchy has REFER and no other directory has REFER, and either the source or the destination hierarchy is disconnected between the mount point and the directory with the REFER, then Landlock will still deny such rename/link. A directory with REFER initially between the mount point and the disconnected directory would also be ignored. There is also the case where both the source and the destination are disconnected. I didn't consider such cases with collect_domain_accesses(). I'm wondering if this path walk gap should be fixed (instead of applying https://lore.kernel.org/all/20250618134734.1673254-1-mic@digikod.net/ ) or not. We should not rely on optimization side effects, but I'm not sure which behavior would make more sense... Any though? > > > but Landlock should still log (and then deny) the side that would be > > denied anyway. > > > >> > >> Letting the walk continue until IS_ROOT(dentry) is what > >> is_access_to_paths_allowed() effectively does for non-renames. > >> A> >> (Also note: moving the const char definitions a bit above so that we can > >> use the path for s4d1 in cleanup code.) > >> > >> Signed-off-by: Tingmao Wang <m@maowtm.org> > > > > I squashed your patches and push them to my next branch with some minor > > changes. Please let me know if there is something wrong. > > Thanks for the edits! I did notice two things: > > diff --git a/tools/testing/selftests/landlock/fs_test.c b/tools/testing/selftests/landlock/fs_test.c > index fa0f18ec62c4..c0a54dde7225 100644 > --- a/tools/testing/selftests/landlock/fs_test.c > +++ b/tools/testing/selftests/landlock/fs_test.c > @@ -4561,6 +4561,17 @@ TEST_F_FORK(ioctl, handle_file_access_file) > FIXTURE(layout1_bind) {}; > /* clang-format on */ > > +static const char bind_dir_s1d3[] = TMP_DIR "/s2d1/s2d2/s1d3"; > +static const char bind_file1_s1d3[] = TMP_DIR "/s2d1/s2d2/s1d3/f1"; > +/* Moved targets for disconnected path tests. */ > ^^^^^^^^^^^^^ > I had "Move targets" here as a noun (i.e. the target/destinations of > the renames) Makes sense now :) > > +static const char dir_s4d1[] = TMP_DIR "/s4d1"; > +static const char file1_s4d1[] = TMP_DIR "/s4d1/f1"; > ... > > Also, I was just re-reading path_disconnected_rename and I managed to get > confused (i.e. "how is the rename in the forked child allowed at all (i.e. > how did we get EXDEV instead of EACCES) after applying layer 2?"). If you > end up amending that commit, can you add this short note: > > diff --git a/tools/testing/selftests/landlock/fs_test.c b/tools/testing/selftests/landlock/fs_test.c > index c0a54dde7225..84615c4bb7c0 100644 > --- a/tools/testing/selftests/landlock/fs_test.c > +++ b/tools/testing/selftests/landlock/fs_test.c > @@ -4936,6 +4936,8 @@ TEST_F_FORK(layout1_bind, path_disconnected_rename) > }, > {} > }; > + > + /* This layer only handles LANDLOCK_ACCESS_FS_READ_FILE only. */ That can be useful. > const struct rule layer2_only_s1d2[] = { > { > .path = dir_s1d2, > > Wish I had caught this earlier. I mean neither of the two things are > hugely important, but I assume until you actually send the merge request > you can amend stuff relatively easily? If not then it's also alright :) I apply your changes. Commits should usually wait at least a week in linux-next. > > > > > [...] > > Ack to all suggestions, thanks! > > Best, > Tingmao > ^ permalink raw reply [flat|nested] 11+ messages in thread
* Re: [PATCH] selftests/landlock: Add tests for access through disconnected paths 2025-06-23 19:40 ` Mickaël Salaün @ 2025-06-23 23:16 ` Tingmao Wang 2025-06-25 14:52 ` Mickaël Salaün 0 siblings, 1 reply; 11+ messages in thread From: Tingmao Wang @ 2025-06-23 23:16 UTC (permalink / raw) To: Mickaël Salaün, Günther Noack Cc: Song Liu, linux-security-module, linux-fsdevel On 6/23/25 20:40, Mickaël Salaün wrote: > On Sun, Jun 22, 2025 at 04:42:49PM +0100, Tingmao Wang wrote: >> On 6/19/25 12:38, Mickaël Salaün wrote: >>> On Sat, Jun 14, 2025 at 07:25:02PM +0100, Tingmao Wang wrote: >>>> [...] >>>> >>>> This might need more thinking, but maybe if one of the operands is >>>> disconnected, we can just let it walk until IS_ROOT(dentry), and also >>>> collect access for the other path until IS_ROOT(dentry), then call >>>> is_access_to_paths_allowed() passing in the root dentry we walked to? (In >>>> this case is_access_to_paths_allowed will not do any walking and just make >>>> an access decision.) >>> >>> If one side is in a disconnected directory and not the other side, the >>> rename would be denied by the VFS, >> >> Not always, right? For example in the path_disconnected_rename test we did: > > Correct, only the mount point matter. > >> >> 5051. ASSERT_EQ(0, renameat(bind_s1d3_fd, file2_name, AT_FDCWD, file1_s2d2)) >> ^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^ >> Disconnected Connected >> >> (and it also has the other way) >> >> So looks like as long as they are still reached from two fds with two >> paths that have the same mnt, it will be allowed. It's just that when we >> do parent walk we end up missing the mount. This also means that for this >> refer check, if after doing the two separate walks (with the disconnected >> side walking all the way to IS_ROOT), we then walk from mnt again, we >> would allow the rename if there is a rule on mnt (or its parents) allowing >> file creation and refers, even if the disconnected side technically now >> lives outside the file hierarchy under mnt and does not have a parent with >> a rule allowing file creation. >> >> (I'm not saying this is necessary wrong or needs fixing, but I think it's >> an interesting consequence of the current implementation.) > > Hmm, that's indeed a very subtle side effect. One issue with the > current implementation is that if a directory between the mount > point and the source has REFER, and another directory not part of the > source hierarchy but part of the disconnected directory's hierarchy has > REFER and no other directory has REFER, and either the source or the > destination hierarchy is disconnected between the mount point and the > directory with the REFER, then Landlock will still deny such > rename/link. A directory with REFER initially between the mount point > and the disconnected directory would also be ignored. There is also the > case where both the source and the destination are disconnected. Sorry, I'm having trouble following this. Can you maybe give a more specific example, perhaps with commands? By "mount point" do you mean the bind mount? If a path has became disconnected because the directory moved away from under the mountpoint, and is therefore not covered by any REFER (you said "either the source or the destination hierarchy is disconnected between the mount point and the directory with the REFER") wouldn't it make sense for the rename to be denied? > > I didn't consider such cases with collect_domain_accesses(). I'm > wondering if this path walk gap should be fixed (instead of applying > https://lore.kernel.org/all/20250618134734.1673254-1-mic@digikod.net/ ) > or not. We should not rely on optimization side effects, but I'm not > sure which behavior would make more sense... Any though? I didn't quite understand your example above and how is it possible for us to end up denying something that should be allowed. My understanding of the current implementation is, when either operands are disconnected, it will walk all the way to the current filesystem's root and stop there. However, it will then still do the walk from the original bind mount up to the real root (/), and if there is any REFER rules on that path, we will still allow the rename. This means that if the rename still ends up being denied, then it wouldn't have been allowed in the first place, even if the path has not become disconnected. An interesting concrete example I came up with: /# uname -a Linux 5610c72ba8a0 6.16.0-rc2-dev #43 SMP ... /# mkdir /a /b /# mkdir /a/a1 /b/b1 /# mount -t tmpfs none /a/a1 /# mkdir /a/a1/a11 /# mount --bind /a/a1/a11 /b/b1 /# mkdir /a/a1/a11/a111 /# tree /a /b /a `-- a1 `-- a11 `-- a111 /b `-- b1 `-- a111 7 directories, 0 files /# cd /b/b1/a111/ /b/b1/a111# mv /a/a1/a11/a111 /a/a1/a12 /b/b1/a111# ls .. # we're disconnected now ls: cannot access '..': No such file or directory /b/b1/a111 [2]# touch /a/a1/a12/file /b/b1/a111# LL_FS_RO=/:/a/a1 LL_FS_RW=/:/b/b1 /sandboxer ls Executing the sandboxed command... file /b/b1/a111# LL_FS_RO=/:/a/a1 LL_FS_RW=/:/b/b1 /sandboxer mv -v file file2 Executing the sandboxed command... mv: cannot move 'file' to 'file2': Permission denied ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ # This fails because for same dir rename we just use is_access_to_path_allowed, # which will stop at /a/a1 (and thus never reach either /b/b1 or /). /b/b1/a111 [1]# mkdir subdir /b/b1/a111# LL_FS_RO=/:/a/a1 LL_FS_RW=/b/b1 /sandboxer mv -v file subdir/file2 Executing the sandboxed command... [..] WARNING: CPU: 1 PID: 656 at security/landlock/fs.c:1065 ... renamed 'file' -> 'subdir/file2' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ # This works because now we restart walk from /b/b1 (the bind mnt) /b/b1/a111# mv subdir/file2 file /b/b1/a111# LL_FS_RO=/:/a/a1 LL_FS_RW=/a /sandboxer mv -v file subdir/file2 Executing the sandboxed command... mv: cannot move 'file' to 'subdir/file2': Permission denied ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ # This is also not allowed, but that's OK since even though technically we're # actually moving /a/a1/a12/file to /a/a1/a12/subdir/file2, we're not doing it # through /a (we're walking into a12 via /b/b1, so rules on /a shouldn't # apply anyway) /b/b1/a111 [1]# LL_FS_RO=/:/a/a1 LL_FS_RW=/b /sandboxer mv -v file subdir/file2 Executing the sandboxed command... renamed 'file' -> 'subdir/file2' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ # And this works because we walk from /b/b1 after doing collect_domain_accesses I think overall this is just a very strange edge case and people should not rely on the exact behavior whether it's intentional or optimization side-effect (as long as it deny access / renames when there is no rules at any of the reasonable upper directories). Also, since as far as I can tell this "optimization" only accidentally allows more access (i.e. rules anywhere between the bind mountpoint to real root would apply, even if technically the now disconnected directory belongs outside of the mountpoint), I think it might be fine to leave it as-is, rather than potentially complicating this code to deal with this quite unusual edge case? (I mean, it's not exactly obvious to me whether it is more correct to respect rules placed between the original bind mountpoint and root, or more correct to ignore these rules (i.e. the behaviour of non-refer access checks)) It is a bit weird that `mv -v file file2` and `mv -v file subdir/file2` behaves differently tho. If you would like to fix it, what do you think about my initial idea?: > This might need more thinking, but maybe if one of the operands is > disconnected, we can just let it walk until IS_ROOT(dentry), and also > collect access for the other path until IS_ROOT(dentry), then call > is_access_to_paths_allowed() passing in the root dentry we walked to? (In > this case is_access_to_paths_allowed will not do any walking and just make > an access decision.) This will basically make the refer checks behave the same as non-refer checks on disconnected paths - walk until IS_ROOT, and stop there. ^ permalink raw reply [flat|nested] 11+ messages in thread
* Re: [PATCH] selftests/landlock: Add tests for access through disconnected paths 2025-06-23 23:16 ` Tingmao Wang @ 2025-06-25 14:52 ` Mickaël Salaün 2025-06-25 20:46 ` Tingmao Wang 0 siblings, 1 reply; 11+ messages in thread From: Mickaël Salaün @ 2025-06-25 14:52 UTC (permalink / raw) To: Tingmao Wang Cc: Günther Noack, Song Liu, linux-security-module, linux-fsdevel On Tue, Jun 24, 2025 at 12:16:55AM +0100, Tingmao Wang wrote: > On 6/23/25 20:40, Mickaël Salaün wrote: > > On Sun, Jun 22, 2025 at 04:42:49PM +0100, Tingmao Wang wrote: > >> On 6/19/25 12:38, Mickaël Salaün wrote: > >>> On Sat, Jun 14, 2025 at 07:25:02PM +0100, Tingmao Wang wrote: > >>>> [...] > >>>> > >>>> This might need more thinking, but maybe if one of the operands is > >>>> disconnected, we can just let it walk until IS_ROOT(dentry), and also > >>>> collect access for the other path until IS_ROOT(dentry), then call > >>>> is_access_to_paths_allowed() passing in the root dentry we walked to? (In > >>>> this case is_access_to_paths_allowed will not do any walking and just make > >>>> an access decision.) > >>> > >>> If one side is in a disconnected directory and not the other side, the > >>> rename would be denied by the VFS, > >> > >> Not always, right? For example in the path_disconnected_rename test we did: > > > > Correct, only the mount point matter. > > > >> > >> 5051. ASSERT_EQ(0, renameat(bind_s1d3_fd, file2_name, AT_FDCWD, file1_s2d2)) > >> ^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^ > >> Disconnected Connected > >> > >> (and it also has the other way) > >> > >> So looks like as long as they are still reached from two fds with two > >> paths that have the same mnt, it will be allowed. It's just that when we > >> do parent walk we end up missing the mount. This also means that for this > >> refer check, if after doing the two separate walks (with the disconnected > >> side walking all the way to IS_ROOT), we then walk from mnt again, we > >> would allow the rename if there is a rule on mnt (or its parents) allowing > >> file creation and refers, even if the disconnected side technically now > >> lives outside the file hierarchy under mnt and does not have a parent with > >> a rule allowing file creation. > >> > >> (I'm not saying this is necessary wrong or needs fixing, but I think it's > >> an interesting consequence of the current implementation.) > > > > Hmm, that's indeed a very subtle side effect. One issue with the > > current implementation is that if a directory between the mount > > point and the source has REFER, and another directory not part of the > > source hierarchy but part of the disconnected directory's hierarchy has > > REFER and no other directory has REFER, and either the source or the > > destination hierarchy is disconnected between the mount point and the > > directory with the REFER, then Landlock will still deny such > > rename/link. A directory with REFER initially between the mount point > > and the disconnected directory would also be ignored. There is also the > > case where both the source and the destination are disconnected. > > Sorry, I'm having trouble following this. Can you maybe give a more > specific example, perhaps with commands? Let's say we initially have this hierarchy: root ├── s1d1 │ └── s1d2 [REFER] │ └── s1d3 │ └── s1d4 │ └── f1 ├── s2d1 [bind mount of s1d1] │ └── s1d2 [REFER] │ └── s1d3 │ └── s1d4 │ └── f1 └── s3d1 [REFER] s1d3 has s1d2 as parent with the REFER right. We open [fd:s1d4]. Now, s1d1/s1d2/s1d3 is moved to s3d1/s1d3, which makes [fd:s1d4]/.. disconnected: root ├── s1d1 │ └── s1d2 [REFER] ├── s2d1 [bind mount of s1d1] │ └── s1d2 [REFER] └── s3d1 [REFER] └── s1d3 [moved from s1d2] └── s1d4 └── f1 Now, s1d3 has s3d1 as parent with the REFER right. Moving [fd:s1d4]/f1 to s2d1/s1d2/f1 would be deny by Landlock whereas the source and destination had and still have REFER in their hierarchies. This is because s3d1 and s1d2 are evaluated for [fd:s1d4]/f1. We could have a similar scenario for the destination and for both. > > By "mount point" do you mean the bind mount? If a path has became > disconnected because the directory moved away from under the mountpoint, > and is therefore not covered by any REFER (you said "either the source or > the destination hierarchy is disconnected between the mount point and the > directory with the REFER") wouldn't it make sense for the rename to be > denied? Yes if the new hierarchy (e.g. s3d1 in my example) doesn't have REFER. > > > > > I didn't consider such cases with collect_domain_accesses(). I'm > > wondering if this path walk gap should be fixed (instead of applying > > https://lore.kernel.org/all/20250618134734.1673254-1-mic@digikod.net/ ) > > or not. We should not rely on optimization side effects, but I'm not > > sure which behavior would make more sense... Any though? > > I didn't quite understand your example above and how is it possible for us > to end up denying something that should be allowed. My understanding of > the current implementation is, when either operands are disconnected, it > will walk all the way to the current filesystem's root and stop there. > However, it will then still do the walk from the original bind mount up to > the real root (/), and if there is any REFER rules on that path, we will > still allow the rename. This means that if the rename still ends up being > denied, then it wouldn't have been allowed in the first place, even if the > path has not become disconnected. > > An interesting concrete example I came up with: > > /# uname -a > Linux 5610c72ba8a0 6.16.0-rc2-dev #43 SMP ... > /# mkdir /a /b > /# mkdir /a/a1 /b/b1 > /# mount -t tmpfs none /a/a1 > /# mkdir /a/a1/a11 > /# mount --bind /a/a1/a11 /b/b1 > /# mkdir /a/a1/a11/a111 > /# tree /a /b > /a > `-- a1 > `-- a11 > `-- a111 > /b > `-- b1 > `-- a111 > > 7 directories, 0 files > /# cd /b/b1/a111/ > /b/b1/a111# mv /a/a1/a11/a111 /a/a1/a12 > /b/b1/a111# ls .. # we're disconnected now > ls: cannot access '..': No such file or directory > /b/b1/a111 [2]# touch /a/a1/a12/file > > /b/b1/a111# LL_FS_RO=/:/a/a1 LL_FS_RW=/:/b/b1 /sandboxer ls > Executing the sandboxed command... > file > > /b/b1/a111# LL_FS_RO=/:/a/a1 LL_FS_RW=/:/b/b1 /sandboxer mv -v file file2 > Executing the sandboxed command... > mv: cannot move 'file' to 'file2': Permission denied > ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ > # This fails because for same dir rename we just use is_access_to_path_allowed, > # which will stop at /a/a1 (and thus never reach either /b/b1 or /). Good example, and this rename should probably be allowed because the root directory allows REFER. > > /b/b1/a111 [1]# mkdir subdir > /b/b1/a111# LL_FS_RO=/:/a/a1 LL_FS_RW=/b/b1 /sandboxer mv -v file subdir/file2 > Executing the sandboxed command... > [..] WARNING: CPU: 1 PID: 656 at security/landlock/fs.c:1065 ... > renamed 'file' -> 'subdir/file2' > ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ > # This works because now we restart walk from /b/b1 (the bind mnt) > > /b/b1/a111# mv subdir/file2 file > /b/b1/a111# LL_FS_RO=/:/a/a1 LL_FS_RW=/a /sandboxer mv -v file subdir/file2 > Executing the sandboxed command... > mv: cannot move 'file' to 'subdir/file2': Permission denied > ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ > # This is also not allowed, but that's OK since even though technically we're > # actually moving /a/a1/a12/file to /a/a1/a12/subdir/file2, we're not doing it > # through /a (we're walking into a12 via /b/b1, so rules on /a shouldn't > # apply anyway) Yes > > /b/b1/a111 [1]# LL_FS_RO=/:/a/a1 LL_FS_RW=/b /sandboxer mv -v file subdir/file2 > Executing the sandboxed command... > renamed 'file' -> 'subdir/file2' > ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ > # And this works because we walk from /b/b1 after doing collect_domain_accesses > > I think overall this is just a very strange edge case and people should > not rely on the exact behavior whether it's intentional or optimization > side-effect (as long as it deny access / renames when there is no rules at > any of the reasonable upper directories). Also, since as far as I can > tell this "optimization" only accidentally allows more access (i.e. rules > anywhere between the bind mountpoint to real root would apply, even if > technically the now disconnected directory belongs outside of the > mountpoint), I think it might be fine to leave it as-is, rather than > potentially complicating this code to deal with this quite unusual edge > case? (I mean, it's not exactly obvious to me whether it is more correct > to respect rules placed between the original bind mountpoint and root, or > more correct to ignore these rules (i.e. the behaviour of non-refer access > checks)) I kind of agree, overall it's not really a security issue (if we consider the origin of files), but it may still be inconsistent for users in rare cases. Anyway, even if we don't want it, users could rely on this edge case (cf. Hyrum's law). > > It is a bit weird that `mv -v file file2` and `mv -v file subdir/file2` > behaves differently tho. Yes, `mv file file2` doesn't depends on REFER because it cannot impact a Landlock security policy. This behavior is a bit weird without kernel and Landlock knowledge though. > > If you would like to fix it, what do you think about my initial idea?: > > This might need more thinking, but maybe if one of the operands is > > disconnected, we can just let it walk until IS_ROOT(dentry), and also > > collect access for the other path until IS_ROOT(dentry), then call Until then, it would be unchanged, right? > > is_access_to_paths_allowed() passing in the root dentry we walked to? (In > > this case is_access_to_paths_allowed will not do any walking and just make > > an access decision.) Are you suggesting to not evaluate mnt_dir for disconnected paths? What about the case where both the source and the destination are disconnected? > > This will basically make the refer checks behave the same as non-refer > checks on disconnected paths - walk until IS_ROOT, and stop there. I think it would make more sense indeed. ^ permalink raw reply [flat|nested] 11+ messages in thread
* Re: [PATCH] selftests/landlock: Add tests for access through disconnected paths 2025-06-25 14:52 ` Mickaël Salaün @ 2025-06-25 20:46 ` Tingmao Wang 2025-07-01 19:59 ` Mickaël Salaün 0 siblings, 1 reply; 11+ messages in thread From: Tingmao Wang @ 2025-06-25 20:46 UTC (permalink / raw) To: Mickaël Salaün Cc: Günther Noack, Song Liu, linux-security-module, linux-fsdevel On 6/25/25 15:52, Mickaël Salaün wrote: > On Tue, Jun 24, 2025 at 12:16:55AM +0100, Tingmao Wang wrote: >> [..] > > Let's say we initially have this hierarchy: > > root > ├── s1d1 > │ └── s1d2 [REFER] > │ └── s1d3 > │ └── s1d4 > │ └── f1 > ├── s2d1 [bind mount of s1d1] > │ └── s1d2 [REFER] > │ └── s1d3 > │ └── s1d4 > │ └── f1 > └── s3d1 [REFER] > > s1d3 has s1d2 as parent with the REFER right. > > We open [fd:s1d4]. > > Now, s1d1/s1d2/s1d3 is moved to s3d1/s1d3, which makes [fd:s1d4]/.. > disconnected: > > root > ├── s1d1 > │ └── s1d2 [REFER] > ├── s2d1 [bind mount of s1d1] > │ └── s1d2 [REFER] > └── s3d1 [REFER] > └── s1d3 [moved from s1d2] > └── s1d4 > └── f1 > > Now, s1d3 has s3d1 as parent with the REFER right. > > Moving [fd:s1d4]/f1 to s2d1/s1d2/f1 would be deny by Landlock Maybe I'm misunderstanding your description, but this seems to work for me? diff --git a/tools/testing/selftests/landlock/fs_test.c b/tools/testing/selftests/landlock/fs_test.c index d8f9259fffe4..5e550e6da49c 100644 --- a/tools/testing/selftests/landlock/fs_test.c +++ b/tools/testing/selftests/landlock/fs_test.c @@ -5201,6 +5201,72 @@ TEST_F_FORK(layout1_bind, path_disconnected_link) } } +FIXTURE(layout_disconnected_special){}; +FIXTURE_SETUP(layout_disconnected_special) +{ + prepare_layout(_metadata); + + create_file(_metadata, TMP_DIR "/s1d1/s1d2/s1d3/s1d4/f1"); + create_directory(_metadata, TMP_DIR "/s2d1"); + create_directory(_metadata, TMP_DIR "/s3d1"); + + set_cap(_metadata, CAP_SYS_ADMIN); + ASSERT_EQ(0, + mount(TMP_DIR "/s1d1", TMP_DIR "/s2d1", NULL, MS_BIND, NULL)); + clear_cap(_metadata, CAP_SYS_ADMIN); +} + +FIXTURE_TEARDOWN_PARENT(layout_disconnected_special) +{ + /* umount(TMP_DIR "/s2d1") is handled by namespace lifetime. */ + + remove_path(TMP_DIR "/s1d1/s1d2/s1d3/s1d4/f1"); + + cleanup_layout(_metadata); +} + +TEST_F_FORK(layout_disconnected_special, disconnected_special) +{ + const __u64 access = + LANDLOCK_ACCESS_FS_REFER | LANDLOCK_ACCESS_FS_MAKE_REG | + LANDLOCK_ACCESS_FS_REMOVE_FILE | LANDLOCK_ACCESS_FS_READ_FILE; + const struct rule rules[] = { + { + .path = TMP_DIR "/s1d1/s1d2", + .access = access, + }, + { + .path = TMP_DIR "/s3d1", + .access = access, + }, + {}, + }; + int s1d4_bind_fd, ruleset_fd; + + s1d4_bind_fd = open(TMP_DIR "/s2d1/s1d2/s1d3/s1d4", O_PATH | O_CLOEXEC); + EXPECT_LE(0, s1d4_bind_fd); + + ruleset_fd = create_ruleset(_metadata, access, rules); + ASSERT_LE(0, ruleset_fd); + + /* Make it disconnected. */ + EXPECT_EQ(0, renameat(AT_FDCWD, TMP_DIR "/s1d1/s1d2/s1d3", AT_FDCWD, + TMP_DIR "/s3d1/s1d3")); + + /* Check it's disconnected. */ + ASSERT_EQ(ENOENT, test_open_rel(s1d4_bind_fd, "..", O_DIRECTORY)); + + enforce_ruleset(_metadata, ruleset_fd); + EXPECT_EQ(0, close(ruleset_fd)); + + EXPECT_EQ(0, renameat(s1d4_bind_fd, "f1", AT_FDCWD, + TMP_DIR "/s2d1/s1d2/f1")); + EXPECT_EQ(0, test_open(TMP_DIR "/s2d1/s1d2/f1", O_RDONLY)); + EXPECT_EQ(0, renameat(AT_FDCWD, TMP_DIR "/s2d1/s1d2/f1", s1d4_bind_fd, + "f1")); + EXPECT_EQ(0, test_open(TMP_DIR "/s3d1/s1d3/s1d4/f1", O_RDONLY)); +} + #define LOWER_BASE TMP_DIR "/lower" #define LOWER_DATA LOWER_BASE "/data" static const char lower_fl1[] = LOWER_DATA "/fl1"; Output: root@5610c72ba8a0 /t/landlock# cp /linux/tools/testing/selftests/landlock/ /tmp -r root@5610c72ba8a0 /t/landlock# ./fs_test -t disconnected_special TAP version 13 1..1 # Starting 1 tests from 1 test cases. # RUN layout_disconnected_special.disconnected_special ... # OK layout_disconnected_special.disconnected_special ok 1 layout_disconnected_special.disconnected_special # PASSED: 1 / 1 tests passed. # Totals: pass:1 fail:0 xfail:0 xpass:0 skip:0 error:0 (I think this is similar to an existing test case, but if you think it's worth having, feel free to add it to the commit (maybe it needs a better name than disconnected_special)) I tested this manually initially which would have been on virtiofs instead of tmpfs, and the behaviour is the same - rename was allowed. > whereas > the source and destination had and still have REFER in their > hierarchies. This is because s3d1 and s1d2 are evaluated for > [fd:s1d4]/f1. We could have a similar scenario for the destination and > for both. > > [...] >> An interesting concrete example I came up with: >> >> /# uname -a >> Linux 5610c72ba8a0 6.16.0-rc2-dev #43 SMP ... >> /# mkdir /a /b >> /# mkdir /a/a1 /b/b1 >> /# mount -t tmpfs none /a/a1 >> /# mkdir /a/a1/a11 >> /# mount --bind /a/a1/a11 /b/b1 >> /# mkdir /a/a1/a11/a111 >> /# tree /a /b >> /a >> `-- a1 >> `-- a11 >> `-- a111 >> /b >> `-- b1 >> `-- a111 >> >> 7 directories, 0 files >> /# cd /b/b1/a111/ >> /b/b1/a111# mv /a/a1/a11/a111 /a/a1/a12 >> /b/b1/a111# ls .. # we're disconnected now >> ls: cannot access '..': No such file or directory >> /b/b1/a111 [2]# touch /a/a1/a12/file >> >> /b/b1/a111# LL_FS_RO=/:/a/a1 LL_FS_RW=/:/b/b1 /sandboxer ls >> Executing the sandboxed command... >> file >> >> /b/b1/a111# LL_FS_RO=/:/a/a1 LL_FS_RW=/:/b/b1 /sandboxer mv -v file file2 >> Executing the sandboxed command... >> mv: cannot move 'file' to 'file2': Permission denied >> ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ >> # This fails because for same dir rename we just use is_access_to_path_allowed, >> # which will stop at /a/a1 (and thus never reach either /b/b1 or /). > > Good example, and this rename should probably be allowed because the > root directory allows REFER. Well, it is disallowed for the same reason why a read to [disconnected cwd]/file would be disallowed, even if root has an allow everything rule - landlock never gets to the root because this is on a separate filesystem, and so if the path is disconnected it can't get out of it. > >> >> /b/b1/a111 [1]# mkdir subdir >> /b/b1/a111# LL_FS_RO=/:/a/a1 LL_FS_RW=/b/b1 /sandboxer mv -v file subdir/file2 >> Executing the sandboxed command... >> [..] WARNING: CPU: 1 PID: 656 at security/landlock/fs.c:1065 ... >> renamed 'file' -> 'subdir/file2' >> ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ >> # This works because now we restart walk from /b/b1 (the bind mnt) >> >> /b/b1/a111# mv subdir/file2 file >> /b/b1/a111# LL_FS_RO=/:/a/a1 LL_FS_RW=/a /sandboxer mv -v file subdir/file2 >> Executing the sandboxed command... >> mv: cannot move 'file' to 'subdir/file2': Permission denied >> ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ >> # This is also not allowed, but that's OK since even though technically we're >> # actually moving /a/a1/a12/file to /a/a1/a12/subdir/file2, we're not doing it >> # through /a (we're walking into a12 via /b/b1, so rules on /a shouldn't >> # apply anyway) > > Yes > >> >> /b/b1/a111 [1]# LL_FS_RO=/:/a/a1 LL_FS_RW=/b /sandboxer mv -v file subdir/file2 >> Executing the sandboxed command... >> renamed 'file' -> 'subdir/file2' >> ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ >> # And this works because we walk from /b/b1 after doing collect_domain_accesses >> >> I think overall this is just a very strange edge case and people should >> not rely on the exact behavior whether it's intentional or optimization >> side-effect (as long as it deny access / renames when there is no rules at >> any of the reasonable upper directories). Also, since as far as I can >> tell this "optimization" only accidentally allows more access (i.e. rules >> anywhere between the bind mountpoint to real root would apply, even if >> technically the now disconnected directory belongs outside of the >> mountpoint), I think it might be fine to leave it as-is, rather than >> potentially complicating this code to deal with this quite unusual edge >> case? (I mean, it's not exactly obvious to me whether it is more correct >> to respect rules placed between the original bind mountpoint and root, or >> more correct to ignore these rules (i.e. the behaviour of non-refer access >> checks)) > > I kind of agree, overall it's not really a security issue (if we > consider the origin of files), but it may still be inconsistent for > users in rare cases. Anyway, even if we don't want it, users could rely > on this edge case (cf. Hyrum's law). > >> >> It is a bit weird that `mv -v file file2` and `mv -v file subdir/file2` >> behaves differently tho. > > Yes, `mv file file2` doesn't depends on REFER because it cannot impact a > Landlock security policy. This behavior is a bit weird without kernel > and Landlock knowledge though. > >> >> If you would like to fix it, what do you think about my initial idea?: >>> This might need more thinking, but maybe if one of the operands is >>> disconnected, we can just let it walk until IS_ROOT(dentry), and also >>> collect access for the other path until IS_ROOT(dentry), then call > > Until then, it would be unchanged, right? Sorry, I'm not fully clear on what you meant by "until then", and what would be unchanged, but on second thought I think this proposal is problematic as it would mean that we won't follow_up on mountpoints even for the other connected path (as collect_domain_accesses does not do that). > >>> is_access_to_paths_allowed() passing in the root dentry we walked to? (In >>> this case is_access_to_paths_allowed will not do any walking and just make >>> an access decision.) > > Are you suggesting to not evaluate mnt_dir for disconnected paths? What > about the case where both the source and the destination are > disconnected? Yes basically, but I think my proposal was problematic. > >> >> This will basically make the refer checks behave the same as non-refer >> checks on disconnected paths - walk until IS_ROOT, and stop there. > > I think it would make more sense indeed. It would make sense if both sides are disconnected, but not if just one side is. In that case we still want to walk the other connected path normally. ^ permalink raw reply related [flat|nested] 11+ messages in thread
* Re: [PATCH] selftests/landlock: Add tests for access through disconnected paths 2025-06-25 20:46 ` Tingmao Wang @ 2025-07-01 19:59 ` Mickaël Salaün 0 siblings, 0 replies; 11+ messages in thread From: Mickaël Salaün @ 2025-07-01 19:59 UTC (permalink / raw) To: Tingmao Wang Cc: Günther Noack, Song Liu, linux-security-module, linux-fsdevel On Wed, Jun 25, 2025 at 09:46:08PM +0100, Tingmao Wang wrote: > On 6/25/25 15:52, Mickaël Salaün wrote: > > On Tue, Jun 24, 2025 at 12:16:55AM +0100, Tingmao Wang wrote: > >> [..] > > > > Let's say we initially have this hierarchy: > > > > root > > ├── s1d1 > > │ └── s1d2 [REFER] > > │ └── s1d3 > > │ └── s1d4 > > │ └── f1 > > ├── s2d1 [bind mount of s1d1] > > │ └── s1d2 [REFER] > > │ └── s1d3 > > │ └── s1d4 > > │ └── f1 > > └── s3d1 [REFER] > > > > s1d3 has s1d2 as parent with the REFER right. > > > > We open [fd:s1d4]. > > > > Now, s1d1/s1d2/s1d3 is moved to s3d1/s1d3, which makes [fd:s1d4]/.. > > disconnected: > > > > root > > ├── s1d1 > > │ └── s1d2 [REFER] > > ├── s2d1 [bind mount of s1d1] > > │ └── s1d2 [REFER] > > └── s3d1 [REFER] > > └── s1d3 [moved from s1d2] > > └── s1d4 > > └── f1 > > > > Now, s1d3 has s3d1 as parent with the REFER right. > > > > Moving [fd:s1d4]/f1 to s2d1/s1d2/f1 would be deny by Landlock > > Maybe I'm misunderstanding your description, but this seems to work for > me? You're right, this is a wrong example. > > diff --git a/tools/testing/selftests/landlock/fs_test.c b/tools/testing/selftests/landlock/fs_test.c > index d8f9259fffe4..5e550e6da49c 100644 > --- a/tools/testing/selftests/landlock/fs_test.c > +++ b/tools/testing/selftests/landlock/fs_test.c > @@ -5201,6 +5201,72 @@ TEST_F_FORK(layout1_bind, path_disconnected_link) > } > } > > +FIXTURE(layout_disconnected_special){}; > +FIXTURE_SETUP(layout_disconnected_special) > +{ > + prepare_layout(_metadata); > + > + create_file(_metadata, TMP_DIR "/s1d1/s1d2/s1d3/s1d4/f1"); > + create_directory(_metadata, TMP_DIR "/s2d1"); > + create_directory(_metadata, TMP_DIR "/s3d1"); > + > + set_cap(_metadata, CAP_SYS_ADMIN); > + ASSERT_EQ(0, > + mount(TMP_DIR "/s1d1", TMP_DIR "/s2d1", NULL, MS_BIND, NULL)); > + clear_cap(_metadata, CAP_SYS_ADMIN); > +} > + > +FIXTURE_TEARDOWN_PARENT(layout_disconnected_special) > +{ > + /* umount(TMP_DIR "/s2d1") is handled by namespace lifetime. */ > + > + remove_path(TMP_DIR "/s1d1/s1d2/s1d3/s1d4/f1"); > + > + cleanup_layout(_metadata); > +} > + > +TEST_F_FORK(layout_disconnected_special, disconnected_special) > +{ > + const __u64 access = > + LANDLOCK_ACCESS_FS_REFER | LANDLOCK_ACCESS_FS_MAKE_REG | > + LANDLOCK_ACCESS_FS_REMOVE_FILE | LANDLOCK_ACCESS_FS_READ_FILE; > + const struct rule rules[] = { > + { > + .path = TMP_DIR "/s1d1/s1d2", > + .access = access, > + }, > + { > + .path = TMP_DIR "/s3d1", > + .access = access, > + }, > + {}, > + }; > + int s1d4_bind_fd, ruleset_fd; > + > + s1d4_bind_fd = open(TMP_DIR "/s2d1/s1d2/s1d3/s1d4", O_PATH | O_CLOEXEC); > + EXPECT_LE(0, s1d4_bind_fd); > + > + ruleset_fd = create_ruleset(_metadata, access, rules); > + ASSERT_LE(0, ruleset_fd); > + > + /* Make it disconnected. */ > + EXPECT_EQ(0, renameat(AT_FDCWD, TMP_DIR "/s1d1/s1d2/s1d3", AT_FDCWD, > + TMP_DIR "/s3d1/s1d3")); > + > + /* Check it's disconnected. */ > + ASSERT_EQ(ENOENT, test_open_rel(s1d4_bind_fd, "..", O_DIRECTORY)); > + > + enforce_ruleset(_metadata, ruleset_fd); > + EXPECT_EQ(0, close(ruleset_fd)); > + > + EXPECT_EQ(0, renameat(s1d4_bind_fd, "f1", AT_FDCWD, > + TMP_DIR "/s2d1/s1d2/f1")); > + EXPECT_EQ(0, test_open(TMP_DIR "/s2d1/s1d2/f1", O_RDONLY)); > + EXPECT_EQ(0, renameat(AT_FDCWD, TMP_DIR "/s2d1/s1d2/f1", s1d4_bind_fd, > + "f1")); > + EXPECT_EQ(0, test_open(TMP_DIR "/s3d1/s1d3/s1d4/f1", O_RDONLY)); > +} > + > #define LOWER_BASE TMP_DIR "/lower" > #define LOWER_DATA LOWER_BASE "/data" > static const char lower_fl1[] = LOWER_DATA "/fl1"; > > Output: > > root@5610c72ba8a0 /t/landlock# cp /linux/tools/testing/selftests/landlock/ /tmp -r > root@5610c72ba8a0 /t/landlock# ./fs_test -t disconnected_special > TAP version 13 > 1..1 > # Starting 1 tests from 1 test cases. > # RUN layout_disconnected_special.disconnected_special ... > # OK layout_disconnected_special.disconnected_special > ok 1 layout_disconnected_special.disconnected_special > # PASSED: 1 / 1 tests passed. > # Totals: pass:1 fail:0 xfail:0 xpass:0 skip:0 error:0 > > (I think this is similar to an existing test case, but if you think it's > worth having, feel free to add it to the commit (maybe it needs a better > name than disconnected_special)) > > I tested this manually initially which would have been on virtiofs instead > of tmpfs, and the behaviour is the same - rename was allowed. > > > whereas > > the source and destination had and still have REFER in their > > hierarchies. This is because s3d1 and s1d2 are evaluated for > > [fd:s1d4]/f1. We could have a similar scenario for the destination and > > for both. > > > > [...] > >> An interesting concrete example I came up with: > >> > >> /# uname -a > >> Linux 5610c72ba8a0 6.16.0-rc2-dev #43 SMP ... > >> /# mkdir /a /b > >> /# mkdir /a/a1 /b/b1 > >> /# mount -t tmpfs none /a/a1 > >> /# mkdir /a/a1/a11 > >> /# mount --bind /a/a1/a11 /b/b1 > >> /# mkdir /a/a1/a11/a111 > >> /# tree /a /b > >> /a > >> `-- a1 > >> `-- a11 > >> `-- a111 > >> /b > >> `-- b1 > >> `-- a111 > >> > >> 7 directories, 0 files > >> /# cd /b/b1/a111/ > >> /b/b1/a111# mv /a/a1/a11/a111 /a/a1/a12 > >> /b/b1/a111# ls .. # we're disconnected now > >> ls: cannot access '..': No such file or directory > >> /b/b1/a111 [2]# touch /a/a1/a12/file > >> > >> /b/b1/a111# LL_FS_RO=/:/a/a1 LL_FS_RW=/:/b/b1 /sandboxer ls > >> Executing the sandboxed command... > >> file > >> > >> /b/b1/a111# LL_FS_RO=/:/a/a1 LL_FS_RW=/:/b/b1 /sandboxer mv -v file file2 > >> Executing the sandboxed command... > >> mv: cannot move 'file' to 'file2': Permission denied > >> ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ > >> # This fails because for same dir rename we just use is_access_to_path_allowed, > >> # which will stop at /a/a1 (and thus never reach either /b/b1 or /). > > > > Good example, and this rename should probably be allowed because the > > root directory allows REFER. > > Well, it is disallowed for the same reason why a read to [disconnected > cwd]/file would be disallowed, even if root has an allow everything rule - > landlock never gets to the root because this is on a separate filesystem, > and so if the path is disconnected it can't get out of it. > > > > >> > >> /b/b1/a111 [1]# mkdir subdir > >> /b/b1/a111# LL_FS_RO=/:/a/a1 LL_FS_RW=/b/b1 /sandboxer mv -v file subdir/file2 > >> Executing the sandboxed command... > >> [..] WARNING: CPU: 1 PID: 656 at security/landlock/fs.c:1065 ... > >> renamed 'file' -> 'subdir/file2' > >> ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ > >> # This works because now we restart walk from /b/b1 (the bind mnt) > >> > >> /b/b1/a111# mv subdir/file2 file > >> /b/b1/a111# LL_FS_RO=/:/a/a1 LL_FS_RW=/a /sandboxer mv -v file subdir/file2 > >> Executing the sandboxed command... > >> mv: cannot move 'file' to 'subdir/file2': Permission denied > >> ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ > >> # This is also not allowed, but that's OK since even though technically we're > >> # actually moving /a/a1/a12/file to /a/a1/a12/subdir/file2, we're not doing it > >> # through /a (we're walking into a12 via /b/b1, so rules on /a shouldn't > >> # apply anyway) > > > > Yes > > > >> > >> /b/b1/a111 [1]# LL_FS_RO=/:/a/a1 LL_FS_RW=/b /sandboxer mv -v file subdir/file2 > >> Executing the sandboxed command... > >> renamed 'file' -> 'subdir/file2' > >> ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ > >> # And this works because we walk from /b/b1 after doing collect_domain_accesses > >> > >> I think overall this is just a very strange edge case and people should > >> not rely on the exact behavior whether it's intentional or optimization > >> side-effect (as long as it deny access / renames when there is no rules at > >> any of the reasonable upper directories). Also, since as far as I can > >> tell this "optimization" only accidentally allows more access (i.e. rules > >> anywhere between the bind mountpoint to real root would apply, even if > >> technically the now disconnected directory belongs outside of the > >> mountpoint), I think it might be fine to leave it as-is, rather than > >> potentially complicating this code to deal with this quite unusual edge > >> case? (I mean, it's not exactly obvious to me whether it is more correct > >> to respect rules placed between the original bind mountpoint and root, or > >> more correct to ignore these rules (i.e. the behaviour of non-refer access > >> checks)) > > > > I kind of agree, overall it's not really a security issue (if we > > consider the origin of files), but it may still be inconsistent for > > users in rare cases. Anyway, even if we don't want it, users could rely > > on this edge case (cf. Hyrum's law). > > > >> > >> It is a bit weird that `mv -v file file2` and `mv -v file subdir/file2` > >> behaves differently tho. > > > > Yes, `mv file file2` doesn't depends on REFER because it cannot impact a > > Landlock security policy. This behavior is a bit weird without kernel > > and Landlock knowledge though. > > > >> > >> If you would like to fix it, what do you think about my initial idea?: > >>> This might need more thinking, but maybe if one of the operands is > >>> disconnected, we can just let it walk until IS_ROOT(dentry), and also > >>> collect access for the other path until IS_ROOT(dentry), then call > > > > Until then, it would be unchanged, right? > > Sorry, I'm not fully clear on what you meant by "until then", and what > would be unchanged, but on second thought I think this proposal is > problematic as it would mean that we won't follow_up on mountpoints even > for the other connected path (as collect_domain_accesses does not do > that). > > > > >>> is_access_to_paths_allowed() passing in the root dentry we walked to? (In > >>> this case is_access_to_paths_allowed will not do any walking and just make > >>> an access decision.) > > > > Are you suggesting to not evaluate mnt_dir for disconnected paths? What > > about the case where both the source and the destination are > > disconnected? > > Yes basically, but I think my proposal was problematic. > > > > >> > >> This will basically make the refer checks behave the same as non-refer > >> checks on disconnected paths - walk until IS_ROOT, and stop there. > > > > I think it would make more sense indeed. > > It would make sense if both sides are disconnected, but not if just one > side is. In that case we still want to walk the other connected path > normally. This discussion made me think about different edge cases and I sent a proposal for a fix with tests that should cover all the cases we talked about here: https://lore.kernel.org/all/20250701183812.3201231-1-mic@digikod.net/ ^ permalink raw reply [flat|nested] 11+ messages in thread
end of thread, other threads:[~2025-07-01 19:59 UTC | newest] Thread overview: 11+ messages (download: mbox.gz follow: Atom feed -- links below jump to the message on this page -- 2025-06-14 18:25 [PATCH] selftests/landlock: Add tests for access through disconnected paths Tingmao Wang 2025-06-15 16:16 ` Tingmao Wang 2025-06-15 16:35 ` Tingmao Wang 2025-06-19 11:38 ` Mickaël Salaün 2025-06-22 15:42 ` Tingmao Wang 2025-06-22 17:04 ` Tingmao Wang 2025-06-23 19:40 ` Mickaël Salaün 2025-06-23 23:16 ` Tingmao Wang 2025-06-25 14:52 ` Mickaël Salaün 2025-06-25 20:46 ` Tingmao Wang 2025-07-01 19:59 ` Mickaël Salaün
This is a public inbox, see mirroring instructions for how to clone and mirror all data and code used for this inbox; as well as URLs for NNTP newsgroup(s).