Linux kernel mirror (for testing) git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
kernel os linux
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge tag 'landlock-6.19-rc1' of git://git.kernel.org/pub/scm/linux/kernel/git/mic/linux

Pull landlock updates from Mickaël Salaün:
"This mainly fixes handling of disconnected directories and adds new
tests"

* tag 'landlock-6.19-rc1' of git://git.kernel.org/pub/scm/linux/kernel/git/mic/linux:
selftests/landlock: Add disconnected leafs and branch test suites
selftests/landlock: Add tests for access through disconnected paths
landlock: Improve variable scope
landlock: Fix handling of disconnected directories
selftests/landlock: Fix makefile header list
landlock: Make docs in cred.h and domain.h visible
landlock: Minor comments improvements

+1536 -27
+10 -1
Documentation/security/landlock.rst
··· 7 7 ================================== 8 8 9 9 :Author: Mickaël Salaün 10 - :Date: March 2025 10 + :Date: September 2025 11 11 12 12 Landlock's goal is to create scoped access-control (i.e. sandboxing). To 13 13 harden a whole system, this feature should be available to any process, ··· 110 110 .. kernel-doc:: security/landlock/fs.h 111 111 :identifiers: 112 112 113 + Process credential 114 + ------------------ 115 + 116 + .. kernel-doc:: security/landlock/cred.h 117 + :identifiers: 118 + 113 119 Ruleset and domain 114 120 ------------------ 115 121 ··· 132 126 makes the reasoning much easier and helps avoid pitfalls. 133 127 134 128 .. kernel-doc:: security/landlock/ruleset.h 129 + :identifiers: 130 + 131 + .. kernel-doc:: security/landlock/domain.h 135 132 :identifiers: 136 133 137 134 Additional documentation
+16
security/landlock/errata/abi-1.h
··· 1 + /* SPDX-License-Identifier: GPL-2.0-only */ 2 + 3 + /** 4 + * DOC: erratum_3 5 + * 6 + * Erratum 3: Disconnected directory handling 7 + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 8 + * 9 + * This fix addresses an issue with disconnected directories that occur when a 10 + * directory is moved outside the scope of a bind mount. The change ensures 11 + * that evaluated access rights include both those from the disconnected file 12 + * hierarchy down to its filesystem root and those from the related mount point 13 + * hierarchy. This prevents access right widening through rename or link 14 + * actions. 15 + */ 16 + LANDLOCK_ERRATUM(3)
+32 -14
security/landlock/fs.c
··· 714 714 * is_access_to_paths_allowed - Check accesses for requests with a common path 715 715 * 716 716 * @domain: Domain to check against. 717 - * @path: File hierarchy to walk through. 717 + * @path: File hierarchy to walk through. For refer checks, this would be 718 + * the common mountpoint. 718 719 * @access_request_parent1: Accesses to check, once @layer_masks_parent1 is 719 720 * equal to @layer_masks_parent2 (if any). This is tied to the unique 720 721 * requested path for most actions, or the source in case of a refer action ··· 838 837 * restriction. 839 838 */ 840 839 while (true) { 841 - struct dentry *parent_dentry; 842 840 const struct landlock_rule *rule; 843 841 844 842 /* ··· 909 909 break; 910 910 } 911 911 } 912 + 912 913 if (unlikely(IS_ROOT(walker_path.dentry))) { 913 - /* 914 - * Stops at disconnected root directories. Only allows 915 - * access to internal filesystems (e.g. nsfs, which is 916 - * reachable through /proc/<pid>/ns/<namespace>). 917 - */ 918 - if (walker_path.mnt->mnt_flags & MNT_INTERNAL) { 914 + if (likely(walker_path.mnt->mnt_flags & MNT_INTERNAL)) { 915 + /* 916 + * Stops and allows access when reaching disconnected root 917 + * directories that are part of internal filesystems (e.g. nsfs, 918 + * which is reachable through /proc/<pid>/ns/<namespace>). 919 + */ 919 920 allowed_parent1 = true; 920 921 allowed_parent2 = true; 922 + break; 921 923 } 922 - break; 924 + 925 + /* 926 + * We reached a disconnected root directory from a bind mount. 927 + * Let's continue the walk with the mount point we missed. 928 + */ 929 + dput(walker_path.dentry); 930 + walker_path.dentry = walker_path.mnt->mnt_root; 931 + dget(walker_path.dentry); 932 + } else { 933 + struct dentry *const parent_dentry = 934 + dget_parent(walker_path.dentry); 935 + 936 + dput(walker_path.dentry); 937 + walker_path.dentry = parent_dentry; 923 938 } 924 - parent_dentry = dget_parent(walker_path.dentry); 925 - dput(walker_path.dentry); 926 - walker_path.dentry = parent_dentry; 927 939 } 928 940 path_put(&walker_path); 929 941 ··· 1033 1021 * file. While walking from @dir to @mnt_root, we record all the domain's 1034 1022 * allowed accesses in @layer_masks_dom. 1035 1023 * 1024 + * Because of disconnected directories, this walk may not reach @mnt_dir. In 1025 + * this case, the walk will continue to @mnt_dir after this call. 1026 + * 1036 1027 * This is similar to is_access_to_paths_allowed() but much simpler because it 1037 1028 * only handles walking on the same mount point and only checks one set of 1038 1029 * accesses. ··· 1077 1062 break; 1078 1063 } 1079 1064 1080 - /* We should not reach a root other than @mnt_root. */ 1081 - if (dir == mnt_root || WARN_ON_ONCE(IS_ROOT(dir))) 1065 + /* 1066 + * Stops at the mount point or the filesystem root for a disconnected 1067 + * directory. 1068 + */ 1069 + if (dir == mnt_root || unlikely(IS_ROOT(dir))) 1082 1070 break; 1083 1071 1084 1072 parent_dentry = dget_parent(dir);
+10 -2
security/landlock/ruleset.c
··· 83 83 .num_layers = ~0, 84 84 }; 85 85 86 + /* 87 + * Checks that .num_layers is large enough for at least 88 + * LANDLOCK_MAX_NUM_LAYERS layers. 89 + */ 86 90 BUILD_BUG_ON(rule.num_layers < LANDLOCK_MAX_NUM_LAYERS); 87 91 } 88 92 ··· 294 290 .access = ~0, 295 291 }; 296 292 293 + /* 294 + * Checks that .level and .access are large enough to contain their expected 295 + * maximum values. 296 + */ 297 297 BUILD_BUG_ON(layer.level < LANDLOCK_MAX_NUM_LAYERS); 298 298 BUILD_BUG_ON(layer.access < LANDLOCK_MASK_ACCESS_FS); 299 299 } ··· 652 644 bool is_empty; 653 645 654 646 /* 655 - * Records in @layer_masks which layer grants access to each 656 - * requested access. 647 + * Records in @layer_masks which layer grants access to each requested 648 + * access: bit cleared if the related layer grants access. 657 649 */ 658 650 is_empty = true; 659 651 for_each_set_bit(access_bit, &access_req, masks_array_size) {
+1 -1
security/landlock/ruleset.h
··· 27 27 */ 28 28 struct landlock_layer { 29 29 /** 30 - * @level: Position of this layer in the layer stack. 30 + * @level: Position of this layer in the layer stack. Starts from 1. 31 31 */ 32 32 u16 level; 33 33 /**
+1 -1
tools/testing/selftests/landlock/Makefile
··· 4 4 5 5 CFLAGS += -Wall -O2 $(KHDR_INCLUDES) 6 6 7 - LOCAL_HDRS += common.h 7 + LOCAL_HDRS += $(wildcard *.h) 8 8 9 9 src_test := $(wildcard *_test.c) 10 10
+1466 -8
tools/testing/selftests/landlock/fs_test.c
··· 2267 2267 return 0; 2268 2268 } 2269 2269 2270 + static int test_renameat(int olddirfd, const char *oldpath, int newdirfd, 2271 + const char *newpath) 2272 + { 2273 + if (renameat2(olddirfd, oldpath, newdirfd, newpath, 0)) 2274 + return errno; 2275 + return 0; 2276 + } 2277 + 2278 + static int test_exchangeat(int olddirfd, const char *oldpath, int newdirfd, 2279 + const char *newpath) 2280 + { 2281 + if (renameat2(olddirfd, oldpath, newdirfd, newpath, RENAME_EXCHANGE)) 2282 + return errno; 2283 + return 0; 2284 + } 2285 + 2270 2286 TEST_F_FORK(layout1, rename_file) 2271 2287 { 2272 2288 const struct rule rules[] = { ··· 4577 4561 FIXTURE(layout1_bind) {}; 4578 4562 /* clang-format on */ 4579 4563 4564 + static const char bind_dir_s1d3[] = TMP_DIR "/s2d1/s2d2/s1d3"; 4565 + static const char bind_file1_s1d3[] = TMP_DIR "/s2d1/s2d2/s1d3/f1"; 4566 + 4567 + /* Move targets for disconnected path tests. */ 4568 + static const char dir_s4d1[] = TMP_DIR "/s4d1"; 4569 + static const char file1_s4d1[] = TMP_DIR "/s4d1/f1"; 4570 + static const char file2_s4d1[] = TMP_DIR "/s4d1/f2"; 4571 + static const char dir_s4d2[] = TMP_DIR "/s4d1/s4d2"; 4572 + static const char file1_s4d2[] = TMP_DIR "/s4d1/s4d2/f1"; 4573 + static const char file1_name[] = "f1"; 4574 + static const char file2_name[] = "f2"; 4575 + 4580 4576 FIXTURE_SETUP(layout1_bind) 4581 4577 { 4582 4578 prepare_layout(_metadata); ··· 4604 4576 { 4605 4577 /* umount(dir_s2d2)) is handled by namespace lifetime. */ 4606 4578 4579 + remove_path(file1_s4d1); 4580 + remove_path(file2_s4d1); 4581 + 4607 4582 remove_layout1(_metadata); 4608 4583 4609 4584 cleanup_layout(_metadata); 4610 4585 } 4611 - 4612 - static const char bind_dir_s1d3[] = TMP_DIR "/s2d1/s2d2/s1d3"; 4613 - static const char bind_file1_s1d3[] = TMP_DIR "/s2d1/s2d2/s1d3/f1"; 4614 4586 4615 4587 /* 4616 4588 * layout1_bind hierarchy: ··· 4622 4594 * │   └── s1d2 4623 4595 * │   ├── f1 4624 4596 * │   ├── f2 4625 - * │   └── s1d3 4597 + * │   └── s1d3 [disconnected by path_disconnected] 4626 4598 * │   ├── f1 4627 4599 * │   └── f2 4628 4600 * ├── s2d1 4629 4601 * │   ├── f1 4630 - * │   └── s2d2 4602 + * │   └── s2d2 [bind mount from s1d2] 4631 4603 * │   ├── f1 4632 4604 * │   ├── f2 4633 4605 * │   └── s1d3 4634 4606 * │   ├── f1 4635 4607 * │   └── f2 4636 - * └── s3d1 4637 - * └── s3d2 4638 - * └── s3d3 4608 + * ├── s3d1 4609 + * │   └── s3d2 4610 + * │   └── s3d3 4611 + * └── s4d1 [renamed from s1d3 by path_disconnected] 4612 + *    ├── f1 4613 + *    ├── f2 4614 + * └── s4d2 4615 + * └── f1 4639 4616 */ 4640 4617 4641 4618 TEST_F_FORK(layout1_bind, no_restriction) ··· 4837 4804 4838 4805 /* Checks legitimate downgrade move. */ 4839 4806 ASSERT_EQ(0, rename(bind_file1_s1d3, file1_s2d2)); 4807 + } 4808 + 4809 + /* 4810 + * Make sure access to file through a disconnected path works as expected. 4811 + * This test moves s1d3 to s4d1. 4812 + */ 4813 + TEST_F_FORK(layout1_bind, path_disconnected) 4814 + { 4815 + const struct rule layer1_allow_all[] = { 4816 + { 4817 + .path = TMP_DIR, 4818 + .access = ACCESS_ALL, 4819 + }, 4820 + {}, 4821 + }; 4822 + const struct rule layer2_allow_just_f1[] = { 4823 + { 4824 + .path = file1_s1d3, 4825 + .access = LANDLOCK_ACCESS_FS_READ_FILE, 4826 + }, 4827 + {}, 4828 + }; 4829 + const struct rule layer3_only_s1d2[] = { 4830 + { 4831 + .path = dir_s1d2, 4832 + .access = LANDLOCK_ACCESS_FS_READ_FILE, 4833 + }, 4834 + {}, 4835 + }; 4836 + 4837 + /* Landlock should not deny access just because it is disconnected. */ 4838 + int ruleset_fd_l1 = 4839 + create_ruleset(_metadata, ACCESS_ALL, layer1_allow_all); 4840 + 4841 + /* Creates the new ruleset now before we move the dir containing the file. */ 4842 + int ruleset_fd_l2 = 4843 + create_ruleset(_metadata, ACCESS_RW, layer2_allow_just_f1); 4844 + int ruleset_fd_l3 = 4845 + create_ruleset(_metadata, ACCESS_RW, layer3_only_s1d2); 4846 + int bind_s1d3_fd; 4847 + 4848 + ASSERT_LE(0, ruleset_fd_l1); 4849 + ASSERT_LE(0, ruleset_fd_l2); 4850 + ASSERT_LE(0, ruleset_fd_l3); 4851 + 4852 + enforce_ruleset(_metadata, ruleset_fd_l1); 4853 + EXPECT_EQ(0, close(ruleset_fd_l1)); 4854 + 4855 + bind_s1d3_fd = open(bind_dir_s1d3, O_PATH | O_CLOEXEC); 4856 + ASSERT_LE(0, bind_s1d3_fd); 4857 + 4858 + /* Tests access is possible before we move. */ 4859 + EXPECT_EQ(0, test_open_rel(bind_s1d3_fd, file1_name, O_RDONLY)); 4860 + EXPECT_EQ(0, test_open_rel(bind_s1d3_fd, file2_name, O_RDONLY)); 4861 + EXPECT_EQ(0, test_open_rel(bind_s1d3_fd, "..", O_RDONLY | O_DIRECTORY)); 4862 + 4863 + /* Makes it disconnected. */ 4864 + ASSERT_EQ(0, rename(dir_s1d3, dir_s4d1)) 4865 + { 4866 + TH_LOG("Failed to rename %s to %s: %s", dir_s1d3, dir_s4d1, 4867 + strerror(errno)); 4868 + } 4869 + 4870 + /* Tests that access is still possible. */ 4871 + EXPECT_EQ(0, test_open_rel(bind_s1d3_fd, file1_name, O_RDONLY)); 4872 + EXPECT_EQ(0, test_open_rel(bind_s1d3_fd, file2_name, O_RDONLY)); 4873 + 4874 + /* 4875 + * Tests that ".." is not possible (not because of Landlock, but just 4876 + * because it's disconnected). 4877 + */ 4878 + EXPECT_EQ(ENOENT, 4879 + test_open_rel(bind_s1d3_fd, "..", O_RDONLY | O_DIRECTORY)); 4880 + 4881 + /* This should still work with a narrower rule. */ 4882 + enforce_ruleset(_metadata, ruleset_fd_l2); 4883 + EXPECT_EQ(0, close(ruleset_fd_l2)); 4884 + 4885 + EXPECT_EQ(0, test_open(file1_s4d1, O_RDONLY)); 4886 + /* 4887 + * Accessing a file through a disconnected file descriptor can still be 4888 + * allowed by a rule tied to this file, even if it is no longer visible in 4889 + * its mount point. 4890 + */ 4891 + EXPECT_EQ(0, test_open_rel(bind_s1d3_fd, file1_name, O_RDONLY)); 4892 + EXPECT_EQ(EACCES, test_open_rel(bind_s1d3_fd, file2_name, O_RDONLY)); 4893 + 4894 + enforce_ruleset(_metadata, ruleset_fd_l3); 4895 + EXPECT_EQ(0, close(ruleset_fd_l3)); 4896 + 4897 + EXPECT_EQ(EACCES, test_open(file1_s4d1, O_RDONLY)); 4898 + /* 4899 + * Accessing a file through a disconnected file descriptor can still be 4900 + * allowed by a rule tied to the original mount point, even if it is no 4901 + * longer visible in its mount point. 4902 + */ 4903 + EXPECT_EQ(0, test_open_rel(bind_s1d3_fd, file1_name, O_RDONLY)); 4904 + EXPECT_EQ(EACCES, test_open_rel(bind_s1d3_fd, file2_name, O_RDONLY)); 4905 + } 4906 + 4907 + /* 4908 + * Test that renameat with disconnected paths works under Landlock. This test 4909 + * moves s1d3 to s4d2, so that we can have a rule allowing refers on the move 4910 + * target's immediate parent. 4911 + */ 4912 + TEST_F_FORK(layout1_bind, path_disconnected_rename) 4913 + { 4914 + const struct rule layer1[] = { 4915 + { 4916 + .path = dir_s1d2, 4917 + .access = LANDLOCK_ACCESS_FS_REFER | 4918 + LANDLOCK_ACCESS_FS_MAKE_DIR | 4919 + LANDLOCK_ACCESS_FS_REMOVE_DIR | 4920 + LANDLOCK_ACCESS_FS_MAKE_REG | 4921 + LANDLOCK_ACCESS_FS_REMOVE_FILE | 4922 + LANDLOCK_ACCESS_FS_READ_FILE, 4923 + }, 4924 + { 4925 + .path = dir_s4d1, 4926 + .access = LANDLOCK_ACCESS_FS_REFER | 4927 + LANDLOCK_ACCESS_FS_MAKE_DIR | 4928 + LANDLOCK_ACCESS_FS_REMOVE_DIR | 4929 + LANDLOCK_ACCESS_FS_MAKE_REG | 4930 + LANDLOCK_ACCESS_FS_REMOVE_FILE | 4931 + LANDLOCK_ACCESS_FS_READ_FILE, 4932 + }, 4933 + {} 4934 + }; 4935 + 4936 + /* This layer only handles LANDLOCK_ACCESS_FS_READ_FILE. */ 4937 + const struct rule layer2_only_s1d2[] = { 4938 + { 4939 + .path = dir_s1d2, 4940 + .access = LANDLOCK_ACCESS_FS_READ_FILE, 4941 + }, 4942 + {}, 4943 + }; 4944 + int ruleset_fd_l1, ruleset_fd_l2; 4945 + pid_t child_pid; 4946 + int bind_s1d3_fd, status; 4947 + 4948 + ASSERT_EQ(0, mkdir(dir_s4d1, 0755)) 4949 + { 4950 + TH_LOG("Failed to create %s: %s", dir_s4d1, strerror(errno)); 4951 + } 4952 + ruleset_fd_l1 = create_ruleset(_metadata, ACCESS_ALL, layer1); 4953 + ruleset_fd_l2 = create_ruleset(_metadata, LANDLOCK_ACCESS_FS_READ_FILE, 4954 + layer2_only_s1d2); 4955 + ASSERT_LE(0, ruleset_fd_l1); 4956 + ASSERT_LE(0, ruleset_fd_l2); 4957 + 4958 + enforce_ruleset(_metadata, ruleset_fd_l1); 4959 + EXPECT_EQ(0, close(ruleset_fd_l1)); 4960 + 4961 + bind_s1d3_fd = open(bind_dir_s1d3, O_PATH | O_CLOEXEC); 4962 + ASSERT_LE(0, bind_s1d3_fd); 4963 + EXPECT_EQ(0, test_open_rel(bind_s1d3_fd, file1_name, O_RDONLY)); 4964 + 4965 + /* Tests ENOENT priority over EACCES for disconnected directory. */ 4966 + EXPECT_EQ(EACCES, test_open_rel(bind_s1d3_fd, "..", O_DIRECTORY)); 4967 + ASSERT_EQ(0, rename(dir_s1d3, dir_s4d2)) 4968 + { 4969 + TH_LOG("Failed to rename %s to %s: %s", dir_s1d3, dir_s4d2, 4970 + strerror(errno)); 4971 + } 4972 + EXPECT_EQ(ENOENT, test_open_rel(bind_s1d3_fd, "..", O_DIRECTORY)); 4973 + 4974 + /* 4975 + * The file is no longer under s1d2 but we should still be able to access it 4976 + * with layer 2 because its mount point is evaluated as the first valid 4977 + * directory because it was initially a parent. Do a fork to test this so 4978 + * we don't prevent ourselves from renaming it back later. 4979 + */ 4980 + child_pid = fork(); 4981 + ASSERT_LE(0, child_pid); 4982 + if (child_pid == 0) { 4983 + enforce_ruleset(_metadata, ruleset_fd_l2); 4984 + EXPECT_EQ(0, close(ruleset_fd_l2)); 4985 + EXPECT_EQ(0, test_open_rel(bind_s1d3_fd, file1_name, O_RDONLY)); 4986 + EXPECT_EQ(EACCES, test_open(file1_s4d2, O_RDONLY)); 4987 + 4988 + /* 4989 + * Tests that access widening checks indeed prevents us from renaming it 4990 + * back. 4991 + */ 4992 + EXPECT_EQ(-1, rename(dir_s4d2, dir_s1d3)); 4993 + EXPECT_EQ(EXDEV, errno); 4994 + 4995 + /* 4996 + * Including through the now disconnected fd (but it should return 4997 + * EXDEV). 4998 + */ 4999 + EXPECT_EQ(-1, renameat(bind_s1d3_fd, file1_name, AT_FDCWD, 5000 + file1_s2d2)); 5001 + EXPECT_EQ(EXDEV, errno); 5002 + _exit(_metadata->exit_code); 5003 + return; 5004 + } 5005 + 5006 + EXPECT_EQ(child_pid, waitpid(child_pid, &status, 0)); 5007 + EXPECT_EQ(1, WIFEXITED(status)); 5008 + EXPECT_EQ(EXIT_SUCCESS, WEXITSTATUS(status)); 5009 + 5010 + ASSERT_EQ(0, rename(dir_s4d2, dir_s1d3)) 5011 + { 5012 + TH_LOG("Failed to rename %s back to %s: %s", dir_s4d1, dir_s1d3, 5013 + strerror(errno)); 5014 + } 5015 + 5016 + /* Now checks that we can access it under l2. */ 5017 + child_pid = fork(); 5018 + ASSERT_LE(0, child_pid); 5019 + if (child_pid == 0) { 5020 + enforce_ruleset(_metadata, ruleset_fd_l2); 5021 + EXPECT_EQ(0, close(ruleset_fd_l2)); 5022 + EXPECT_EQ(0, test_open_rel(bind_s1d3_fd, file1_name, O_RDONLY)); 5023 + EXPECT_EQ(0, test_open(file1_s1d3, O_RDONLY)); 5024 + _exit(_metadata->exit_code); 5025 + return; 5026 + } 5027 + 5028 + EXPECT_EQ(child_pid, waitpid(child_pid, &status, 0)); 5029 + EXPECT_EQ(1, WIFEXITED(status)); 5030 + EXPECT_EQ(EXIT_SUCCESS, WEXITSTATUS(status)); 5031 + 5032 + /* 5033 + * Also test that we can rename via a disconnected path. We move the 5034 + * dir back to the disconnected place first, then we rename file1 to 5035 + * file2 through our dir fd. 5036 + */ 5037 + ASSERT_EQ(0, rename(dir_s1d3, dir_s4d2)) 5038 + { 5039 + TH_LOG("Failed to rename %s to %s: %s", dir_s1d3, dir_s4d2, 5040 + strerror(errno)); 5041 + } 5042 + ASSERT_EQ(0, 5043 + renameat(bind_s1d3_fd, file1_name, bind_s1d3_fd, file2_name)) 5044 + { 5045 + TH_LOG("Failed to rename %s to %s within disconnected %s: %s", 5046 + file1_name, file2_name, bind_dir_s1d3, strerror(errno)); 5047 + } 5048 + EXPECT_EQ(0, test_open_rel(bind_s1d3_fd, file2_name, O_RDONLY)); 5049 + ASSERT_EQ(0, renameat(bind_s1d3_fd, file2_name, AT_FDCWD, file1_s2d2)) 5050 + { 5051 + TH_LOG("Failed to rename %s to %s through disconnected %s: %s", 5052 + file2_name, file1_s2d2, bind_dir_s1d3, strerror(errno)); 5053 + } 5054 + EXPECT_EQ(0, test_open(file1_s2d2, O_RDONLY)); 5055 + EXPECT_EQ(0, test_open(file1_s1d2, O_RDONLY)); 5056 + 5057 + /* Move it back using the disconnected path as the target. */ 5058 + ASSERT_EQ(0, renameat(AT_FDCWD, file1_s2d2, bind_s1d3_fd, file1_name)) 5059 + { 5060 + TH_LOG("Failed to rename %s to %s through disconnected %s: %s", 5061 + file1_s1d2, file1_name, bind_dir_s1d3, strerror(errno)); 5062 + } 5063 + 5064 + /* Now make it connected again. */ 5065 + ASSERT_EQ(0, rename(dir_s4d2, dir_s1d3)) 5066 + { 5067 + TH_LOG("Failed to rename %s back to %s: %s", dir_s4d2, dir_s1d3, 5068 + strerror(errno)); 5069 + } 5070 + 5071 + /* Checks again that we can access it under l2. */ 5072 + enforce_ruleset(_metadata, ruleset_fd_l2); 5073 + EXPECT_EQ(0, close(ruleset_fd_l2)); 5074 + EXPECT_EQ(0, test_open_rel(bind_s1d3_fd, file1_name, O_RDONLY)); 5075 + EXPECT_EQ(0, test_open(file1_s1d3, O_RDONLY)); 5076 + } 5077 + 5078 + /* 5079 + * Test that linkat(2) with disconnected paths works under Landlock. This 5080 + * test moves s1d3 to s4d1. 5081 + */ 5082 + TEST_F_FORK(layout1_bind, path_disconnected_link) 5083 + { 5084 + /* Ruleset to be applied after renaming s1d3 to s4d1. */ 5085 + const struct rule layer1[] = { 5086 + { 5087 + .path = dir_s4d1, 5088 + .access = LANDLOCK_ACCESS_FS_REFER | 5089 + LANDLOCK_ACCESS_FS_READ_FILE | 5090 + LANDLOCK_ACCESS_FS_MAKE_REG | 5091 + LANDLOCK_ACCESS_FS_REMOVE_FILE, 5092 + }, 5093 + { 5094 + .path = dir_s2d2, 5095 + .access = LANDLOCK_ACCESS_FS_REFER | 5096 + LANDLOCK_ACCESS_FS_READ_FILE | 5097 + LANDLOCK_ACCESS_FS_MAKE_REG | 5098 + LANDLOCK_ACCESS_FS_REMOVE_FILE, 5099 + }, 5100 + {} 5101 + }; 5102 + int ruleset_fd, bind_s1d3_fd; 5103 + 5104 + /* Removes unneeded files created by layout1, otherwise it will EEXIST. */ 5105 + ASSERT_EQ(0, unlink(file1_s1d2)); 5106 + ASSERT_EQ(0, unlink(file2_s1d3)); 5107 + 5108 + bind_s1d3_fd = open(bind_dir_s1d3, O_PATH | O_CLOEXEC); 5109 + ASSERT_LE(0, bind_s1d3_fd); 5110 + EXPECT_EQ(0, test_open_rel(bind_s1d3_fd, file1_name, O_RDONLY)); 5111 + 5112 + /* Disconnects bind_s1d3_fd. */ 5113 + ASSERT_EQ(0, rename(dir_s1d3, dir_s4d1)) 5114 + { 5115 + TH_LOG("Failed to rename %s to %s: %s", dir_s1d3, dir_s4d1, 5116 + strerror(errno)); 5117 + } 5118 + 5119 + /* Need this later to test different parent link. */ 5120 + ASSERT_EQ(0, mkdir(dir_s4d2, 0755)) 5121 + { 5122 + TH_LOG("Failed to create %s: %s", dir_s4d2, strerror(errno)); 5123 + } 5124 + 5125 + ruleset_fd = create_ruleset(_metadata, ACCESS_ALL, layer1); 5126 + ASSERT_LE(0, ruleset_fd); 5127 + enforce_ruleset(_metadata, ruleset_fd); 5128 + EXPECT_EQ(0, close(ruleset_fd)); 5129 + 5130 + /* From disconnected to connected. */ 5131 + ASSERT_EQ(0, linkat(bind_s1d3_fd, file1_name, AT_FDCWD, file1_s2d2, 0)) 5132 + { 5133 + TH_LOG("Failed to link %s to %s via disconnected %s: %s", 5134 + file1_name, file1_s2d2, bind_dir_s1d3, strerror(errno)); 5135 + } 5136 + 5137 + /* Tests that we can access via the new link... */ 5138 + EXPECT_EQ(0, test_open(file1_s2d2, O_RDONLY)) 5139 + { 5140 + TH_LOG("Failed to open newly linked %s: %s", file1_s2d2, 5141 + strerror(errno)); 5142 + } 5143 + 5144 + /* ...as well as the old one. */ 5145 + EXPECT_EQ(0, test_open(file1_s4d1, O_RDONLY)) 5146 + { 5147 + TH_LOG("Failed to open original %s: %s", file1_s4d1, 5148 + strerror(errno)); 5149 + } 5150 + 5151 + /* From connected to disconnected. */ 5152 + ASSERT_EQ(0, unlink(file1_s4d1)); 5153 + ASSERT_EQ(0, linkat(AT_FDCWD, file1_s2d2, bind_s1d3_fd, file2_name, 0)) 5154 + { 5155 + TH_LOG("Failed to link %s to %s via disconnected %s: %s", 5156 + file1_s2d2, file2_name, bind_dir_s1d3, strerror(errno)); 5157 + } 5158 + EXPECT_EQ(0, test_open(file2_s4d1, O_RDONLY)); 5159 + ASSERT_EQ(0, unlink(file1_s2d2)); 5160 + 5161 + /* From disconnected to disconnected (same parent). */ 5162 + ASSERT_EQ(0, 5163 + linkat(bind_s1d3_fd, file2_name, bind_s1d3_fd, file1_name, 0)) 5164 + { 5165 + TH_LOG("Failed to link %s to %s within disconnected %s: %s", 5166 + file2_name, file1_name, bind_dir_s1d3, strerror(errno)); 5167 + } 5168 + EXPECT_EQ(0, test_open(file1_s4d1, O_RDONLY)) 5169 + { 5170 + TH_LOG("Failed to open newly linked %s: %s", file1_s4d1, 5171 + strerror(errno)); 5172 + } 5173 + EXPECT_EQ(0, test_open_rel(bind_s1d3_fd, file1_name, O_RDONLY)) 5174 + { 5175 + TH_LOG("Failed to open %s through newly created link under disconnected path: %s", 5176 + file1_name, strerror(errno)); 5177 + } 5178 + ASSERT_EQ(0, unlink(file2_s4d1)); 5179 + 5180 + /* From disconnected to disconnected (different parent). */ 5181 + ASSERT_EQ(0, 5182 + linkat(bind_s1d3_fd, file1_name, bind_s1d3_fd, "s4d2/f1", 0)) 5183 + { 5184 + TH_LOG("Failed to link %s to %s within disconnected %s: %s", 5185 + file1_name, "s4d2/f1", bind_dir_s1d3, strerror(errno)); 5186 + } 5187 + EXPECT_EQ(0, test_open(file1_s4d2, O_RDONLY)) 5188 + { 5189 + TH_LOG("Failed to open %s after link: %s", file1_s4d2, 5190 + strerror(errno)); 5191 + } 5192 + EXPECT_EQ(0, test_open_rel(bind_s1d3_fd, "s4d2/f1", O_RDONLY)) 5193 + { 5194 + TH_LOG("Failed to open %s through disconnected path after link: %s", 5195 + "s4d2/f1", strerror(errno)); 5196 + } 5197 + } 5198 + 5199 + /* 5200 + * layout4_disconnected_leafs with bind mount and renames: 5201 + * 5202 + * tmp 5203 + * ├── s1d1 5204 + * │   └── s1d2 [source of the bind mount] 5205 + * │ ├── s1d31 5206 + * │   │ └── s1d41 [now renamed beneath s3d1] 5207 + * │ │ ├── f1 5208 + * │ │ └── f2 5209 + * │   └── s1d32 5210 + * │ └── s1d42 [now renamed beneath s4d1] 5211 + * │ ├── f3 5212 + * │ └── f4 5213 + * ├── s2d1 5214 + * │   └── s2d2 [bind mount of s1d2] 5215 + * │ ├── s1d31 5216 + * │   │ └── s1d41 [opened FD, now renamed beneath s3d1] 5217 + * │ │ ├── f1 5218 + * │ │ └── f2 5219 + * │   └── s1d32 5220 + * │ └── s1d42 [opened FD, now renamed beneath s4d1] 5221 + * │ ├── f3 5222 + * │ └── f4 5223 + * ├── s3d1 5224 + * │  └── s1d41 [renamed here] 5225 + * │ ├── f1 5226 + * │ └── f2 5227 + * └── s4d1 5228 + * └── s1d42 [renamed here] 5229 + * ├── f3 5230 + * └── f4 5231 + */ 5232 + /* clang-format off */ 5233 + FIXTURE(layout4_disconnected_leafs) { 5234 + int s2d2_fd; 5235 + }; 5236 + /* clang-format on */ 5237 + 5238 + FIXTURE_SETUP(layout4_disconnected_leafs) 5239 + { 5240 + prepare_layout(_metadata); 5241 + 5242 + create_file(_metadata, TMP_DIR "/s1d1/s1d2/s1d31/s1d41/f1"); 5243 + create_file(_metadata, TMP_DIR "/s1d1/s1d2/s1d31/s1d41/f2"); 5244 + create_file(_metadata, TMP_DIR "/s1d1/s1d2/s1d32/s1d42/f3"); 5245 + create_file(_metadata, TMP_DIR "/s1d1/s1d2/s1d32/s1d42/f4"); 5246 + create_directory(_metadata, TMP_DIR "/s2d1/s2d2"); 5247 + create_directory(_metadata, TMP_DIR "/s3d1"); 5248 + create_directory(_metadata, TMP_DIR "/s4d1"); 5249 + 5250 + self->s2d2_fd = 5251 + open(TMP_DIR "/s2d1/s2d2", O_DIRECTORY | O_PATH | O_CLOEXEC); 5252 + ASSERT_LE(0, self->s2d2_fd); 5253 + 5254 + set_cap(_metadata, CAP_SYS_ADMIN); 5255 + ASSERT_EQ(0, mount(TMP_DIR "/s1d1/s1d2", TMP_DIR "/s2d1/s2d2", NULL, 5256 + MS_BIND, NULL)); 5257 + clear_cap(_metadata, CAP_SYS_ADMIN); 5258 + } 5259 + 5260 + FIXTURE_TEARDOWN_PARENT(layout4_disconnected_leafs) 5261 + { 5262 + /* umount(TMP_DIR "/s2d1") is handled by namespace lifetime. */ 5263 + 5264 + /* Removes files after renames. */ 5265 + remove_path(TMP_DIR "/s3d1/s1d41/f1"); 5266 + remove_path(TMP_DIR "/s3d1/s1d41/f2"); 5267 + remove_path(TMP_DIR "/s4d1/s1d42/f1"); 5268 + remove_path(TMP_DIR "/s4d1/s1d42/f3"); 5269 + remove_path(TMP_DIR "/s4d1/s1d42/f4"); 5270 + remove_path(TMP_DIR "/s4d1/s1d42/f5"); 5271 + 5272 + cleanup_layout(_metadata); 5273 + } 5274 + 5275 + FIXTURE_VARIANT(layout4_disconnected_leafs) 5276 + { 5277 + /* 5278 + * Parent of the bind mount source. It should always be ignored when 5279 + * testing against files under the s1d41 or s1d42 disconnected directories. 5280 + */ 5281 + const __u64 allowed_s1d1; 5282 + /* 5283 + * Source of bind mount (to s2d2). It should always be enforced when 5284 + * testing against files under the s1d41 or s1d42 disconnected directories. 5285 + */ 5286 + const __u64 allowed_s1d2; 5287 + /* 5288 + * Original parent of s1d41. It should always be ignored when testing 5289 + * against files under the s1d41 disconnected directory. 5290 + */ 5291 + const __u64 allowed_s1d31; 5292 + /* 5293 + * Original parent of s1d42. It should always be ignored when testing 5294 + * against files under the s1d42 disconnected directory. 5295 + */ 5296 + const __u64 allowed_s1d32; 5297 + /* 5298 + * Opened and disconnected source directory. It should always be enforced 5299 + * when testing against files under the s1d41 disconnected directory. 5300 + */ 5301 + const __u64 allowed_s1d41; 5302 + /* 5303 + * Opened and disconnected source directory. It should always be enforced 5304 + * when testing against files under the s1d42 disconnected directory. 5305 + */ 5306 + const __u64 allowed_s1d42; 5307 + /* 5308 + * File in the s1d41 disconnected directory. It should always be enforced 5309 + * when testing against itself under the s1d41 disconnected directory. 5310 + */ 5311 + const __u64 allowed_f1; 5312 + /* 5313 + * File in the s1d41 disconnected directory. It should always be enforced 5314 + * when testing against itself under the s1d41 disconnected directory. 5315 + */ 5316 + const __u64 allowed_f2; 5317 + /* 5318 + * File in the s1d42 disconnected directory. It should always be enforced 5319 + * when testing against itself under the s1d42 disconnected directory. 5320 + */ 5321 + const __u64 allowed_f3; 5322 + /* 5323 + * Parent of the bind mount destination. It should always be enforced when 5324 + * testing against files under the s1d41 or s1d42 disconnected directories. 5325 + */ 5326 + const __u64 allowed_s2d1; 5327 + /* 5328 + * Directory covered by the bind mount. It should always be ignored when 5329 + * testing against files under the s1d41 or s1d42 disconnected directories. 5330 + */ 5331 + const __u64 allowed_s2d2; 5332 + /* 5333 + * New parent of the renamed s1d41. It should always be ignored when 5334 + * testing against files under the s1d41 disconnected directory. 5335 + */ 5336 + const __u64 allowed_s3d1; 5337 + /* 5338 + * New parent of the renamed s1d42. It should always be ignored when 5339 + * testing against files under the s1d42 disconnected directory. 5340 + */ 5341 + const __u64 allowed_s4d1; 5342 + 5343 + /* Expected result of the call to open([fd:s1d41]/f1, O_RDONLY). */ 5344 + const int expected_read_result; 5345 + /* Expected result of the call to renameat([fd:s1d41]/f1, [fd:s1d42]/f1). */ 5346 + const int expected_rename_result; 5347 + /* 5348 + * Expected result of the call to renameat([fd:s1d41]/f2, [fd:s1d42]/f3, 5349 + * RENAME_EXCHANGE). 5350 + */ 5351 + const int expected_exchange_result; 5352 + /* Expected result of the call to renameat([fd:s1d42]/f4, [fd:s1d42]/f5). */ 5353 + const int expected_same_dir_rename_result; 5354 + }; 5355 + 5356 + /* clang-format off */ 5357 + FIXTURE_VARIANT_ADD(layout4_disconnected_leafs, s1d1_mount_src_parent) { 5358 + /* clang-format on */ 5359 + .allowed_s1d1 = LANDLOCK_ACCESS_FS_REFER | 5360 + LANDLOCK_ACCESS_FS_READ_FILE | 5361 + LANDLOCK_ACCESS_FS_EXECUTE | 5362 + LANDLOCK_ACCESS_FS_MAKE_REG, 5363 + .expected_read_result = EACCES, 5364 + .expected_same_dir_rename_result = EACCES, 5365 + .expected_rename_result = EACCES, 5366 + .expected_exchange_result = EACCES, 5367 + }; 5368 + 5369 + /* clang-format off */ 5370 + FIXTURE_VARIANT_ADD(layout4_disconnected_leafs, s1d2_mount_src_refer) { 5371 + /* clang-format on */ 5372 + .allowed_s1d2 = LANDLOCK_ACCESS_FS_REFER | LANDLOCK_ACCESS_FS_READ_FILE, 5373 + .expected_read_result = 0, 5374 + .expected_same_dir_rename_result = EACCES, 5375 + .expected_rename_result = EACCES, 5376 + .expected_exchange_result = EACCES, 5377 + }; 5378 + 5379 + /* clang-format off */ 5380 + FIXTURE_VARIANT_ADD(layout4_disconnected_leafs, s1d2_mount_src_create) { 5381 + /* clang-format on */ 5382 + .allowed_s1d2 = LANDLOCK_ACCESS_FS_READ_FILE | 5383 + LANDLOCK_ACCESS_FS_MAKE_REG, 5384 + .expected_read_result = 0, 5385 + .expected_same_dir_rename_result = 0, 5386 + .expected_rename_result = EXDEV, 5387 + .expected_exchange_result = EXDEV, 5388 + }; 5389 + 5390 + /* clang-format off */ 5391 + FIXTURE_VARIANT_ADD(layout4_disconnected_leafs, s1d2_mount_src_rename) { 5392 + /* clang-format on */ 5393 + .allowed_s1d2 = LANDLOCK_ACCESS_FS_REFER | LANDLOCK_ACCESS_FS_MAKE_REG, 5394 + .expected_read_result = EACCES, 5395 + .expected_same_dir_rename_result = 0, 5396 + .expected_rename_result = 0, 5397 + .expected_exchange_result = 0, 5398 + }; 5399 + 5400 + /* clang-format off */ 5401 + FIXTURE_VARIANT_ADD(layout4_disconnected_leafs, s1d31_s1d32_old_parent) { 5402 + /* clang-format on */ 5403 + .allowed_s1d31 = LANDLOCK_ACCESS_FS_REFER | 5404 + LANDLOCK_ACCESS_FS_READ_FILE | 5405 + LANDLOCK_ACCESS_FS_EXECUTE | 5406 + LANDLOCK_ACCESS_FS_MAKE_REG, 5407 + .allowed_s1d32 = LANDLOCK_ACCESS_FS_REFER | 5408 + LANDLOCK_ACCESS_FS_READ_FILE | 5409 + LANDLOCK_ACCESS_FS_EXECUTE | 5410 + LANDLOCK_ACCESS_FS_MAKE_REG, 5411 + .expected_read_result = EACCES, 5412 + .expected_same_dir_rename_result = EACCES, 5413 + .expected_rename_result = EACCES, 5414 + .expected_exchange_result = EACCES, 5415 + }; 5416 + 5417 + /* clang-format off */ 5418 + FIXTURE_VARIANT_ADD(layout4_disconnected_leafs, s1d41_s1d42_disconnected_refer) { 5419 + /* clang-format on */ 5420 + .allowed_s1d41 = LANDLOCK_ACCESS_FS_REFER | 5421 + LANDLOCK_ACCESS_FS_READ_FILE, 5422 + .allowed_s1d42 = LANDLOCK_ACCESS_FS_REFER | 5423 + LANDLOCK_ACCESS_FS_READ_FILE, 5424 + .expected_read_result = 0, 5425 + .expected_same_dir_rename_result = EACCES, 5426 + .expected_rename_result = EACCES, 5427 + .expected_exchange_result = EACCES, 5428 + }; 5429 + 5430 + /* clang-format off */ 5431 + FIXTURE_VARIANT_ADD(layout4_disconnected_leafs, s1d41_s1d42_disconnected_create) { 5432 + /* clang-format on */ 5433 + .allowed_s1d41 = LANDLOCK_ACCESS_FS_READ_FILE | 5434 + LANDLOCK_ACCESS_FS_MAKE_REG, 5435 + .allowed_s1d42 = LANDLOCK_ACCESS_FS_READ_FILE | 5436 + LANDLOCK_ACCESS_FS_MAKE_REG, 5437 + .expected_read_result = 0, 5438 + .expected_same_dir_rename_result = 0, 5439 + .expected_rename_result = EXDEV, 5440 + .expected_exchange_result = EXDEV, 5441 + }; 5442 + 5443 + /* clang-format off */ 5444 + FIXTURE_VARIANT_ADD(layout4_disconnected_leafs, s1d41_s1d42_disconnected_rename_even) { 5445 + /* clang-format on */ 5446 + .allowed_s1d41 = LANDLOCK_ACCESS_FS_REFER | LANDLOCK_ACCESS_FS_MAKE_REG, 5447 + .allowed_s1d42 = LANDLOCK_ACCESS_FS_REFER | LANDLOCK_ACCESS_FS_MAKE_REG, 5448 + .expected_read_result = EACCES, 5449 + .expected_same_dir_rename_result = 0, 5450 + .expected_rename_result = 0, 5451 + .expected_exchange_result = 0, 5452 + }; 5453 + 5454 + /* The destination directory has more access right. */ 5455 + /* clang-format off */ 5456 + FIXTURE_VARIANT_ADD(layout4_disconnected_leafs, s1d41_s1d42_disconnected_rename_more) { 5457 + /* clang-format on */ 5458 + .allowed_s1d41 = LANDLOCK_ACCESS_FS_REFER | LANDLOCK_ACCESS_FS_MAKE_REG, 5459 + .allowed_s1d42 = LANDLOCK_ACCESS_FS_REFER | 5460 + LANDLOCK_ACCESS_FS_MAKE_REG | 5461 + LANDLOCK_ACCESS_FS_EXECUTE, 5462 + .expected_read_result = EACCES, 5463 + .expected_same_dir_rename_result = 0, 5464 + /* Access denied. */ 5465 + .expected_rename_result = EXDEV, 5466 + .expected_exchange_result = EXDEV, 5467 + }; 5468 + 5469 + /* The destination directory has less access right. */ 5470 + /* clang-format off */ 5471 + FIXTURE_VARIANT_ADD(layout4_disconnected_leafs, s1d41_s1d42_disconnected_rename_less) { 5472 + /* clang-format on */ 5473 + .allowed_s1d41 = LANDLOCK_ACCESS_FS_REFER | 5474 + LANDLOCK_ACCESS_FS_MAKE_REG | 5475 + LANDLOCK_ACCESS_FS_EXECUTE, 5476 + .allowed_s1d42 = LANDLOCK_ACCESS_FS_REFER | LANDLOCK_ACCESS_FS_MAKE_REG, 5477 + .expected_read_result = EACCES, 5478 + .expected_same_dir_rename_result = 0, 5479 + /* Access allowed. */ 5480 + .expected_rename_result = 0, 5481 + .expected_exchange_result = EXDEV, 5482 + }; 5483 + 5484 + /* clang-format off */ 5485 + FIXTURE_VARIANT_ADD(layout4_disconnected_leafs, s2d1_mount_dst_parent_create) { 5486 + /* clang-format on */ 5487 + .allowed_s2d1 = LANDLOCK_ACCESS_FS_READ_FILE | 5488 + LANDLOCK_ACCESS_FS_MAKE_REG, 5489 + .expected_read_result = 0, 5490 + .expected_same_dir_rename_result = 0, 5491 + .expected_rename_result = EXDEV, 5492 + .expected_exchange_result = EXDEV, 5493 + }; 5494 + 5495 + /* clang-format off */ 5496 + FIXTURE_VARIANT_ADD(layout4_disconnected_leafs, s2d1_mount_dst_parent_refer) { 5497 + /* clang-format on */ 5498 + .allowed_s2d1 = LANDLOCK_ACCESS_FS_REFER | LANDLOCK_ACCESS_FS_READ_FILE, 5499 + .expected_read_result = 0, 5500 + .expected_same_dir_rename_result = EACCES, 5501 + .expected_rename_result = EACCES, 5502 + .expected_exchange_result = EACCES, 5503 + }; 5504 + 5505 + /* clang-format off */ 5506 + FIXTURE_VARIANT_ADD(layout4_disconnected_leafs, s2d1_mount_dst_parent_mini) { 5507 + /* clang-format on */ 5508 + .allowed_s2d1 = LANDLOCK_ACCESS_FS_REFER | 5509 + LANDLOCK_ACCESS_FS_READ_FILE | 5510 + LANDLOCK_ACCESS_FS_MAKE_REG, 5511 + .expected_read_result = 0, 5512 + .expected_same_dir_rename_result = 0, 5513 + .expected_rename_result = 0, 5514 + .expected_exchange_result = 0, 5515 + }; 5516 + 5517 + /* clang-format off */ 5518 + FIXTURE_VARIANT_ADD(layout4_disconnected_leafs, s2d2_covered_by_mount) { 5519 + /* clang-format on */ 5520 + .allowed_s2d2 = LANDLOCK_ACCESS_FS_REFER | 5521 + LANDLOCK_ACCESS_FS_READ_FILE | 5522 + LANDLOCK_ACCESS_FS_EXECUTE | 5523 + LANDLOCK_ACCESS_FS_MAKE_REG, 5524 + .expected_read_result = EACCES, 5525 + .expected_same_dir_rename_result = EACCES, 5526 + .expected_rename_result = EACCES, 5527 + .expected_exchange_result = EACCES, 5528 + }; 5529 + 5530 + /* Tests collect_domain_accesses(). */ 5531 + /* clang-format off */ 5532 + FIXTURE_VARIANT_ADD(layout4_disconnected_leafs, s3d1_s4d1_new_parent_refer) { 5533 + /* clang-format on */ 5534 + .allowed_s3d1 = LANDLOCK_ACCESS_FS_REFER | LANDLOCK_ACCESS_FS_READ_FILE, 5535 + .allowed_s4d1 = LANDLOCK_ACCESS_FS_REFER | LANDLOCK_ACCESS_FS_READ_FILE, 5536 + .expected_read_result = 0, 5537 + .expected_same_dir_rename_result = EACCES, 5538 + .expected_rename_result = EACCES, 5539 + .expected_exchange_result = EACCES, 5540 + }; 5541 + 5542 + /* clang-format off */ 5543 + FIXTURE_VARIANT_ADD(layout4_disconnected_leafs, s3d1_s4d1_new_parent_create) { 5544 + /* clang-format on */ 5545 + .allowed_s3d1 = LANDLOCK_ACCESS_FS_READ_FILE | 5546 + LANDLOCK_ACCESS_FS_MAKE_REG, 5547 + .allowed_s4d1 = LANDLOCK_ACCESS_FS_READ_FILE | 5548 + LANDLOCK_ACCESS_FS_MAKE_REG, 5549 + .expected_read_result = 0, 5550 + .expected_same_dir_rename_result = 0, 5551 + .expected_rename_result = EXDEV, 5552 + .expected_exchange_result = EXDEV, 5553 + }; 5554 + 5555 + FIXTURE_VARIANT_ADD(layout4_disconnected_leafs, 5556 + s3d1_s4d1_disconnected_rename_even){ 5557 + /* clang-format on */ 5558 + .allowed_s3d1 = LANDLOCK_ACCESS_FS_REFER | LANDLOCK_ACCESS_FS_MAKE_REG, 5559 + .allowed_s4d1 = LANDLOCK_ACCESS_FS_REFER | LANDLOCK_ACCESS_FS_MAKE_REG, 5560 + .expected_read_result = EACCES, 5561 + .expected_same_dir_rename_result = 0, 5562 + .expected_rename_result = 0, 5563 + .expected_exchange_result = 0, 5564 + }; 5565 + 5566 + /* The destination directory has more access right. */ 5567 + /* clang-format off */ 5568 + FIXTURE_VARIANT_ADD(layout4_disconnected_leafs, s3d1_s4d1_disconnected_rename_more) { 5569 + /* clang-format on */ 5570 + .allowed_s3d1 = LANDLOCK_ACCESS_FS_REFER | LANDLOCK_ACCESS_FS_MAKE_REG, 5571 + .allowed_s4d1 = LANDLOCK_ACCESS_FS_REFER | LANDLOCK_ACCESS_FS_MAKE_REG | 5572 + LANDLOCK_ACCESS_FS_EXECUTE, 5573 + .expected_read_result = EACCES, 5574 + .expected_same_dir_rename_result = 0, 5575 + /* Access denied. */ 5576 + .expected_rename_result = EXDEV, 5577 + .expected_exchange_result = EXDEV, 5578 + }; 5579 + 5580 + /* The destination directory has less access right. */ 5581 + /* clang-format off */ 5582 + FIXTURE_VARIANT_ADD(layout4_disconnected_leafs, s3d1_s4d1_disconnected_rename_less) { 5583 + /* clang-format on */ 5584 + .allowed_s3d1 = LANDLOCK_ACCESS_FS_REFER | LANDLOCK_ACCESS_FS_MAKE_REG | 5585 + LANDLOCK_ACCESS_FS_EXECUTE, 5586 + .allowed_s4d1 = LANDLOCK_ACCESS_FS_REFER | LANDLOCK_ACCESS_FS_MAKE_REG, 5587 + .expected_read_result = EACCES, 5588 + .expected_same_dir_rename_result = 0, 5589 + /* Access allowed. */ 5590 + .expected_rename_result = 0, 5591 + .expected_exchange_result = EXDEV, 5592 + }; 5593 + 5594 + /* clang-format off */ 5595 + FIXTURE_VARIANT_ADD(layout4_disconnected_leafs, f1_f2_f3) { 5596 + /* clang-format on */ 5597 + .allowed_f1 = LANDLOCK_ACCESS_FS_READ_FILE, 5598 + .allowed_f2 = LANDLOCK_ACCESS_FS_READ_FILE, 5599 + .allowed_f3 = LANDLOCK_ACCESS_FS_READ_FILE, 5600 + .expected_read_result = 0, 5601 + .expected_same_dir_rename_result = EACCES, 5602 + .expected_rename_result = EACCES, 5603 + .expected_exchange_result = EACCES, 5604 + }; 5605 + 5606 + TEST_F_FORK(layout4_disconnected_leafs, read_rename_exchange) 5607 + { 5608 + const __u64 handled_access = 5609 + LANDLOCK_ACCESS_FS_REFER | LANDLOCK_ACCESS_FS_READ_FILE | 5610 + LANDLOCK_ACCESS_FS_EXECUTE | LANDLOCK_ACCESS_FS_MAKE_REG; 5611 + const struct rule rules[] = { 5612 + { 5613 + .path = TMP_DIR "/s1d1", 5614 + .access = variant->allowed_s1d1, 5615 + }, 5616 + { 5617 + .path = TMP_DIR "/s1d1/s1d2", 5618 + .access = variant->allowed_s1d2, 5619 + }, 5620 + { 5621 + .path = TMP_DIR "/s1d1/s1d2/s1d31", 5622 + .access = variant->allowed_s1d31, 5623 + }, 5624 + { 5625 + .path = TMP_DIR "/s1d1/s1d2/s1d32", 5626 + .access = variant->allowed_s1d32, 5627 + }, 5628 + { 5629 + .path = TMP_DIR "/s1d1/s1d2/s1d31/s1d41", 5630 + .access = variant->allowed_s1d41, 5631 + }, 5632 + { 5633 + .path = TMP_DIR "/s1d1/s1d2/s1d32/s1d42", 5634 + .access = variant->allowed_s1d42, 5635 + }, 5636 + { 5637 + .path = TMP_DIR "/s1d1/s1d2/s1d31/s1d41/f1", 5638 + .access = variant->allowed_f1, 5639 + }, 5640 + { 5641 + .path = TMP_DIR "/s1d1/s1d2/s1d31/s1d41/f2", 5642 + .access = variant->allowed_f2, 5643 + }, 5644 + { 5645 + .path = TMP_DIR "/s1d1/s1d2/s1d32/s1d42/f3", 5646 + .access = variant->allowed_f3, 5647 + }, 5648 + { 5649 + .path = TMP_DIR "/s2d1", 5650 + .access = variant->allowed_s2d1, 5651 + }, 5652 + /* s2d2_fd */ 5653 + { 5654 + .path = TMP_DIR "/s3d1", 5655 + .access = variant->allowed_s3d1, 5656 + }, 5657 + { 5658 + .path = TMP_DIR "/s4d1", 5659 + .access = variant->allowed_s4d1, 5660 + }, 5661 + {}, 5662 + }; 5663 + int ruleset_fd, s1d41_bind_fd, s1d42_bind_fd; 5664 + 5665 + ruleset_fd = create_ruleset(_metadata, handled_access, rules); 5666 + ASSERT_LE(0, ruleset_fd); 5667 + 5668 + /* Adds rule for the covered directory. */ 5669 + if (variant->allowed_s2d2) { 5670 + ASSERT_EQ(0, landlock_add_rule( 5671 + ruleset_fd, LANDLOCK_RULE_PATH_BENEATH, 5672 + &(struct landlock_path_beneath_attr){ 5673 + .parent_fd = self->s2d2_fd, 5674 + .allowed_access = 5675 + variant->allowed_s2d2, 5676 + }, 5677 + 0)); 5678 + } 5679 + EXPECT_EQ(0, close(self->s2d2_fd)); 5680 + 5681 + s1d41_bind_fd = open(TMP_DIR "/s2d1/s2d2/s1d31/s1d41", 5682 + O_DIRECTORY | O_PATH | O_CLOEXEC); 5683 + ASSERT_LE(0, s1d41_bind_fd); 5684 + s1d42_bind_fd = open(TMP_DIR "/s2d1/s2d2/s1d32/s1d42", 5685 + O_DIRECTORY | O_PATH | O_CLOEXEC); 5686 + ASSERT_LE(0, s1d42_bind_fd); 5687 + 5688 + /* Disconnects and checks source and destination directories. */ 5689 + EXPECT_EQ(0, test_open_rel(s1d41_bind_fd, "..", O_DIRECTORY)); 5690 + EXPECT_EQ(0, test_open_rel(s1d42_bind_fd, "..", O_DIRECTORY)); 5691 + /* Renames to make it accessible through s3d1/s1d41 */ 5692 + ASSERT_EQ(0, test_renameat(AT_FDCWD, TMP_DIR "/s1d1/s1d2/s1d31/s1d41", 5693 + AT_FDCWD, TMP_DIR "/s3d1/s1d41")); 5694 + /* Renames to make it accessible through s4d1/s1d42 */ 5695 + ASSERT_EQ(0, test_renameat(AT_FDCWD, TMP_DIR "/s1d1/s1d2/s1d32/s1d42", 5696 + AT_FDCWD, TMP_DIR "/s4d1/s1d42")); 5697 + EXPECT_EQ(ENOENT, test_open_rel(s1d41_bind_fd, "..", O_DIRECTORY)); 5698 + EXPECT_EQ(ENOENT, test_open_rel(s1d42_bind_fd, "..", O_DIRECTORY)); 5699 + 5700 + enforce_ruleset(_metadata, ruleset_fd); 5701 + EXPECT_EQ(0, close(ruleset_fd)); 5702 + 5703 + EXPECT_EQ(variant->expected_read_result, 5704 + test_open_rel(s1d41_bind_fd, "f1", O_RDONLY)); 5705 + 5706 + EXPECT_EQ(variant->expected_rename_result, 5707 + test_renameat(s1d41_bind_fd, "f1", s1d42_bind_fd, "f1")); 5708 + EXPECT_EQ(variant->expected_exchange_result, 5709 + test_exchangeat(s1d41_bind_fd, "f2", s1d42_bind_fd, "f3")); 5710 + 5711 + EXPECT_EQ(variant->expected_same_dir_rename_result, 5712 + test_renameat(s1d42_bind_fd, "f4", s1d42_bind_fd, "f5")); 5713 + } 5714 + 5715 + /* 5716 + * layout5_disconnected_branch before rename: 5717 + * 5718 + * tmp 5719 + * ├── s1d1 5720 + * │   └── s1d2 [source of the first bind mount] 5721 + * │   └── s1d3 5722 + * │   ├── s1d41 5723 + * │   │   ├── f1 5724 + * │   │   └── f2 5725 + * │   └── s1d42 5726 + * │   ├── f3 5727 + * │   └── f4 5728 + * ├── s2d1 5729 + * │   └── s2d2 [source of the second bind mount] 5730 + * │   └── s2d3 5731 + * │   └── s2d4 [first s1d2 bind mount] 5732 + * │   └── s1d3 5733 + * │   ├── s1d41 5734 + * │   │   ├── f1 5735 + * │   │   └── f2 5736 + * │   └── s1d42 5737 + * │   ├── f3 5738 + * │   └── f4 5739 + * ├── s3d1 5740 + * │   └── s3d2 [second s2d2 bind mount] 5741 + * │   └── s2d3 5742 + * │   └── s2d4 [first s1d2 bind mount] 5743 + * │   └── s1d3 5744 + * │   ├── s1d41 5745 + * │   │   ├── f1 5746 + * │   │   └── f2 5747 + * │   └── s1d42 5748 + * │   ├── f3 5749 + * │   └── f4 5750 + * └── s4d1 5751 + * 5752 + * After rename: 5753 + * 5754 + * tmp 5755 + * ├── s1d1 5756 + * │   └── s1d2 [source of the first bind mount] 5757 + * │   └── s1d3 5758 + * │   ├── s1d41 5759 + * │   │   ├── f1 5760 + * │   │   └── f2 5761 + * │   └── s1d42 5762 + * │   ├── f3 5763 + * │   └── f4 5764 + * ├── s2d1 5765 + * │   └── s2d2 [source of the second bind mount] 5766 + * ├── s3d1 5767 + * │   └── s3d2 [second s2d2 bind mount] 5768 + * └── s4d1 5769 + * └── s2d3 [renamed here] 5770 + * └── s2d4 [first s1d2 bind mount] 5771 + * └── s1d3 5772 + * ├── s1d41 5773 + * │   ├── f1 5774 + * │   └── f2 5775 + * └── s1d42 5776 + * ├── f3 5777 + * └── f4 5778 + * 5779 + * Decision path for access from the s3d1/s3d2/s2d3/s2d4/s1d3 file descriptor: 5780 + * 1. first bind mount: s1d3 -> s1d2 5781 + * 2. second bind mount: s2d3 5782 + * 3. tmp mount: s4d1 -> tmp [disconnected branch] 5783 + * 4. second bind mount: s2d2 5784 + * 5. tmp mount: s3d1 -> tmp 5785 + * 6. parent mounts: [...] -> / 5786 + * 5787 + * The s4d1 directory is evaluated even if it is not in the s2d2 mount. 5788 + */ 5789 + 5790 + /* clang-format off */ 5791 + FIXTURE(layout5_disconnected_branch) { 5792 + int s2d4_fd, s3d2_fd; 5793 + }; 5794 + /* clang-format on */ 5795 + 5796 + FIXTURE_SETUP(layout5_disconnected_branch) 5797 + { 5798 + prepare_layout(_metadata); 5799 + 5800 + create_file(_metadata, TMP_DIR "/s1d1/s1d2/s1d3/s1d41/f1"); 5801 + create_file(_metadata, TMP_DIR "/s1d1/s1d2/s1d3/s1d41/f2"); 5802 + create_file(_metadata, TMP_DIR "/s1d1/s1d2/s1d3/s1d42/f3"); 5803 + create_file(_metadata, TMP_DIR "/s1d1/s1d2/s1d3/s1d42/f4"); 5804 + create_directory(_metadata, TMP_DIR "/s2d1/s2d2/s2d3/s2d4"); 5805 + create_directory(_metadata, TMP_DIR "/s3d1/s3d2"); 5806 + create_directory(_metadata, TMP_DIR "/s4d1"); 5807 + 5808 + self->s2d4_fd = open(TMP_DIR "/s2d1/s2d2/s2d3/s2d4", 5809 + O_DIRECTORY | O_PATH | O_CLOEXEC); 5810 + ASSERT_LE(0, self->s2d4_fd); 5811 + 5812 + self->s3d2_fd = 5813 + open(TMP_DIR "/s3d1/s3d2", O_DIRECTORY | O_PATH | O_CLOEXEC); 5814 + ASSERT_LE(0, self->s3d2_fd); 5815 + 5816 + set_cap(_metadata, CAP_SYS_ADMIN); 5817 + ASSERT_EQ(0, mount(TMP_DIR "/s1d1/s1d2", TMP_DIR "/s2d1/s2d2/s2d3/s2d4", 5818 + NULL, MS_BIND, NULL)); 5819 + ASSERT_EQ(0, mount(TMP_DIR "/s2d1/s2d2", TMP_DIR "/s3d1/s3d2", NULL, 5820 + MS_BIND | MS_REC, NULL)); 5821 + clear_cap(_metadata, CAP_SYS_ADMIN); 5822 + } 5823 + 5824 + FIXTURE_TEARDOWN_PARENT(layout5_disconnected_branch) 5825 + { 5826 + /* Bind mounts are handled by namespace lifetime. */ 5827 + 5828 + /* Removes files after renames. */ 5829 + remove_path(TMP_DIR "/s1d1/s1d2/s1d3/s1d41/f1"); 5830 + remove_path(TMP_DIR "/s1d1/s1d2/s1d3/s1d41/f2"); 5831 + remove_path(TMP_DIR "/s1d1/s1d2/s1d3/s1d42/f1"); 5832 + remove_path(TMP_DIR "/s1d1/s1d2/s1d3/s1d42/f3"); 5833 + remove_path(TMP_DIR "/s1d1/s1d2/s1d3/s1d42/f4"); 5834 + remove_path(TMP_DIR "/s1d1/s1d2/s1d3/s1d42/f5"); 5835 + 5836 + cleanup_layout(_metadata); 5837 + } 5838 + 5839 + FIXTURE_VARIANT(layout5_disconnected_branch) 5840 + { 5841 + /* 5842 + * Parent of all files. It should always be enforced when testing against 5843 + * files under the s1d41 or s1d42 disconnected directories. 5844 + */ 5845 + const __u64 allowed_base; 5846 + /* 5847 + * Parent of the first bind mount source. It should always be ignored when 5848 + * testing against files under the s1d41 or s1d42 disconnected directories. 5849 + */ 5850 + const __u64 allowed_s1d1; 5851 + const __u64 allowed_s1d2; 5852 + const __u64 allowed_s1d3; 5853 + const __u64 allowed_s2d1; 5854 + const __u64 allowed_s2d2; 5855 + const __u64 allowed_s2d3; 5856 + const __u64 allowed_s2d4; 5857 + const __u64 allowed_s3d1; 5858 + const __u64 allowed_s3d2; 5859 + const __u64 allowed_s4d1; 5860 + 5861 + /* Expected result of the call to open([fd:s1d3]/s1d41/f1, O_RDONLY). */ 5862 + const int expected_read_result; 5863 + /* 5864 + * Expected result of the call to renameat([fd:s1d3]/s1d41/f1, 5865 + * [fd:s1d3]/s1d42/f1). 5866 + */ 5867 + const int expected_rename_result; 5868 + /* 5869 + * Expected result of the call to renameat([fd:s1d3]/s1d41/f2, 5870 + * [fd:s1d3]/s1d42/f3, RENAME_EXCHANGE). 5871 + */ 5872 + const int expected_exchange_result; 5873 + /* 5874 + * Expected result of the call to renameat([fd:s1d3]/s1d42/f4, 5875 + * [fd:s1d3]/s1d42/f5). 5876 + */ 5877 + const int expected_same_dir_rename_result; 5878 + }; 5879 + 5880 + /* clang-format off */ 5881 + FIXTURE_VARIANT_ADD(layout5_disconnected_branch, s1d1_mount1_src_parent) { 5882 + /* clang-format on */ 5883 + .allowed_s1d1 = LANDLOCK_ACCESS_FS_REFER | 5884 + LANDLOCK_ACCESS_FS_READ_FILE | 5885 + LANDLOCK_ACCESS_FS_EXECUTE | 5886 + LANDLOCK_ACCESS_FS_MAKE_REG, 5887 + .expected_read_result = EACCES, 5888 + .expected_same_dir_rename_result = EACCES, 5889 + .expected_rename_result = EACCES, 5890 + .expected_exchange_result = EACCES, 5891 + }; 5892 + 5893 + /* clang-format off */ 5894 + FIXTURE_VARIANT_ADD(layout5_disconnected_branch, s1d2_mount1_src_refer) { 5895 + /* clang-format on */ 5896 + .allowed_s1d2 = LANDLOCK_ACCESS_FS_REFER | LANDLOCK_ACCESS_FS_READ_FILE, 5897 + .expected_read_result = 0, 5898 + .expected_same_dir_rename_result = EACCES, 5899 + .expected_rename_result = EACCES, 5900 + .expected_exchange_result = EACCES, 5901 + }; 5902 + 5903 + /* clang-format off */ 5904 + FIXTURE_VARIANT_ADD(layout5_disconnected_branch, s1d2_mount1_src_create) { 5905 + /* clang-format on */ 5906 + .allowed_s1d2 = LANDLOCK_ACCESS_FS_READ_FILE | 5907 + LANDLOCK_ACCESS_FS_MAKE_REG, 5908 + .expected_read_result = 0, 5909 + .expected_same_dir_rename_result = 0, 5910 + .expected_rename_result = EXDEV, 5911 + .expected_exchange_result = EXDEV, 5912 + }; 5913 + 5914 + /* clang-format off */ 5915 + FIXTURE_VARIANT_ADD(layout5_disconnected_branch, s1d2_mount1_src_rename) { 5916 + /* clang-format on */ 5917 + .allowed_s1d2 = LANDLOCK_ACCESS_FS_REFER | LANDLOCK_ACCESS_FS_MAKE_REG, 5918 + .expected_read_result = EACCES, 5919 + .expected_same_dir_rename_result = 0, 5920 + .expected_rename_result = 0, 5921 + .expected_exchange_result = 0, 5922 + }; 5923 + 5924 + /* clang-format off */ 5925 + FIXTURE_VARIANT_ADD(layout5_disconnected_branch, s1d3_fd_refer) { 5926 + /* clang-format on */ 5927 + .allowed_s1d3 = LANDLOCK_ACCESS_FS_REFER | LANDLOCK_ACCESS_FS_READ_FILE, 5928 + .expected_read_result = 0, 5929 + .expected_same_dir_rename_result = EACCES, 5930 + .expected_rename_result = EACCES, 5931 + .expected_exchange_result = EACCES, 5932 + }; 5933 + 5934 + /* clang-format off */ 5935 + FIXTURE_VARIANT_ADD(layout5_disconnected_branch, s1d3_fd_create) { 5936 + /* clang-format on */ 5937 + .allowed_s1d3 = LANDLOCK_ACCESS_FS_READ_FILE | 5938 + LANDLOCK_ACCESS_FS_MAKE_REG, 5939 + .expected_read_result = 0, 5940 + .expected_same_dir_rename_result = 0, 5941 + .expected_rename_result = EXDEV, 5942 + .expected_exchange_result = EXDEV, 5943 + }; 5944 + 5945 + /* clang-format off */ 5946 + FIXTURE_VARIANT_ADD(layout5_disconnected_branch, s1d3_fd_rename) { 5947 + /* clang-format on */ 5948 + .allowed_s1d3 = LANDLOCK_ACCESS_FS_REFER | LANDLOCK_ACCESS_FS_MAKE_REG, 5949 + .expected_read_result = EACCES, 5950 + .expected_same_dir_rename_result = 0, 5951 + .expected_rename_result = 0, 5952 + .expected_exchange_result = 0, 5953 + }; 5954 + 5955 + /* clang-format off */ 5956 + FIXTURE_VARIANT_ADD(layout5_disconnected_branch, s1d3_fd_full) { 5957 + /* clang-format on */ 5958 + .allowed_s1d3 = LANDLOCK_ACCESS_FS_REFER | 5959 + LANDLOCK_ACCESS_FS_READ_FILE | 5960 + LANDLOCK_ACCESS_FS_EXECUTE | 5961 + LANDLOCK_ACCESS_FS_MAKE_REG, 5962 + .expected_read_result = 0, 5963 + .expected_same_dir_rename_result = 0, 5964 + .expected_rename_result = 0, 5965 + .expected_exchange_result = 0, 5966 + }; 5967 + 5968 + /* clang-format off */ 5969 + FIXTURE_VARIANT_ADD(layout5_disconnected_branch, s2d1_mount2_src_parent) { 5970 + /* clang-format on */ 5971 + .allowed_s2d1 = LANDLOCK_ACCESS_FS_REFER | 5972 + LANDLOCK_ACCESS_FS_READ_FILE | 5973 + LANDLOCK_ACCESS_FS_EXECUTE | 5974 + LANDLOCK_ACCESS_FS_MAKE_REG, 5975 + .expected_read_result = EACCES, 5976 + .expected_same_dir_rename_result = EACCES, 5977 + .expected_rename_result = EACCES, 5978 + .expected_exchange_result = EACCES, 5979 + }; 5980 + 5981 + /* clang-format off */ 5982 + FIXTURE_VARIANT_ADD(layout5_disconnected_branch, s2d2_mount2_src_refer) { 5983 + /* clang-format on */ 5984 + .allowed_s2d2 = LANDLOCK_ACCESS_FS_REFER | LANDLOCK_ACCESS_FS_READ_FILE, 5985 + .expected_read_result = 0, 5986 + .expected_same_dir_rename_result = EACCES, 5987 + .expected_rename_result = EACCES, 5988 + .expected_exchange_result = EACCES, 5989 + }; 5990 + 5991 + /* clang-format off */ 5992 + FIXTURE_VARIANT_ADD(layout5_disconnected_branch, s2d2_mount2_src_create) { 5993 + /* clang-format on */ 5994 + .allowed_s2d2 = LANDLOCK_ACCESS_FS_READ_FILE | 5995 + LANDLOCK_ACCESS_FS_MAKE_REG, 5996 + .expected_read_result = 0, 5997 + .expected_same_dir_rename_result = 0, 5998 + .expected_rename_result = EXDEV, 5999 + .expected_exchange_result = EXDEV, 6000 + }; 6001 + 6002 + /* clang-format off */ 6003 + FIXTURE_VARIANT_ADD(layout5_disconnected_branch, s2d2_mount2_src_rename) { 6004 + /* clang-format on */ 6005 + .allowed_s2d2 = LANDLOCK_ACCESS_FS_REFER | LANDLOCK_ACCESS_FS_MAKE_REG, 6006 + .expected_read_result = EACCES, 6007 + .expected_same_dir_rename_result = 0, 6008 + .expected_rename_result = 0, 6009 + .expected_exchange_result = 0, 6010 + }; 6011 + 6012 + /* clang-format off */ 6013 + FIXTURE_VARIANT_ADD(layout5_disconnected_branch, s2d3_mount1_dst_parent_refer) { 6014 + /* clang-format on */ 6015 + .allowed_s2d3 = LANDLOCK_ACCESS_FS_REFER | LANDLOCK_ACCESS_FS_READ_FILE, 6016 + .expected_read_result = 0, 6017 + .expected_same_dir_rename_result = EACCES, 6018 + .expected_rename_result = EACCES, 6019 + .expected_exchange_result = EACCES, 6020 + }; 6021 + 6022 + /* clang-format off */ 6023 + FIXTURE_VARIANT_ADD(layout5_disconnected_branch, s2d3_mount1_dst_parent_create) { 6024 + /* clang-format on */ 6025 + .allowed_s2d3 = LANDLOCK_ACCESS_FS_READ_FILE | 6026 + LANDLOCK_ACCESS_FS_MAKE_REG, 6027 + .expected_read_result = 0, 6028 + .expected_same_dir_rename_result = 0, 6029 + .expected_rename_result = EXDEV, 6030 + .expected_exchange_result = EXDEV, 6031 + }; 6032 + 6033 + /* clang-format off */ 6034 + FIXTURE_VARIANT_ADD(layout5_disconnected_branch, s2d3_mount1_dst_parent_rename) { 6035 + /* clang-format on */ 6036 + .allowed_s2d3 = LANDLOCK_ACCESS_FS_REFER | LANDLOCK_ACCESS_FS_MAKE_REG, 6037 + .expected_read_result = EACCES, 6038 + .expected_same_dir_rename_result = 0, 6039 + .expected_rename_result = 0, 6040 + .expected_exchange_result = 0, 6041 + }; 6042 + 6043 + /* clang-format off */ 6044 + FIXTURE_VARIANT_ADD(layout5_disconnected_branch, s2d4_mount1_dst) { 6045 + /* clang-format on */ 6046 + .allowed_s2d4 = LANDLOCK_ACCESS_FS_REFER | 6047 + LANDLOCK_ACCESS_FS_READ_FILE | 6048 + LANDLOCK_ACCESS_FS_EXECUTE | 6049 + LANDLOCK_ACCESS_FS_MAKE_REG, 6050 + .expected_read_result = EACCES, 6051 + .expected_same_dir_rename_result = EACCES, 6052 + .expected_rename_result = EACCES, 6053 + .expected_exchange_result = EACCES, 6054 + }; 6055 + 6056 + /* clang-format off */ 6057 + FIXTURE_VARIANT_ADD(layout5_disconnected_branch, s3d1_mount2_dst_parent_refer) { 6058 + /* clang-format on */ 6059 + .allowed_s3d1 = LANDLOCK_ACCESS_FS_REFER | LANDLOCK_ACCESS_FS_READ_FILE, 6060 + .expected_read_result = 0, 6061 + .expected_same_dir_rename_result = EACCES, 6062 + .expected_rename_result = EACCES, 6063 + .expected_exchange_result = EACCES, 6064 + }; 6065 + 6066 + /* clang-format off */ 6067 + FIXTURE_VARIANT_ADD(layout5_disconnected_branch, s3d1_mount2_dst_parent_create) { 6068 + /* clang-format on */ 6069 + .allowed_s3d1 = LANDLOCK_ACCESS_FS_READ_FILE | 6070 + LANDLOCK_ACCESS_FS_MAKE_REG, 6071 + .expected_read_result = 0, 6072 + .expected_same_dir_rename_result = 0, 6073 + .expected_rename_result = EXDEV, 6074 + .expected_exchange_result = EXDEV, 6075 + }; 6076 + 6077 + /* clang-format off */ 6078 + FIXTURE_VARIANT_ADD(layout5_disconnected_branch, s3d1_mount2_dst_parent_rename) { 6079 + /* clang-format on */ 6080 + .allowed_s3d1 = LANDLOCK_ACCESS_FS_REFER | LANDLOCK_ACCESS_FS_MAKE_REG, 6081 + .expected_read_result = EACCES, 6082 + .expected_same_dir_rename_result = 0, 6083 + .expected_rename_result = 0, 6084 + .expected_exchange_result = 0, 6085 + }; 6086 + 6087 + /* clang-format off */ 6088 + FIXTURE_VARIANT_ADD(layout5_disconnected_branch, s3d2_mount1_dst) { 6089 + /* clang-format on */ 6090 + .allowed_s3d2 = LANDLOCK_ACCESS_FS_REFER | 6091 + LANDLOCK_ACCESS_FS_READ_FILE | 6092 + LANDLOCK_ACCESS_FS_EXECUTE | 6093 + LANDLOCK_ACCESS_FS_MAKE_REG, 6094 + .expected_read_result = EACCES, 6095 + .expected_same_dir_rename_result = EACCES, 6096 + .expected_rename_result = EACCES, 6097 + .expected_exchange_result = EACCES, 6098 + }; 6099 + 6100 + /* clang-format off */ 6101 + FIXTURE_VARIANT_ADD(layout5_disconnected_branch, s4d1_rename_parent_refer) { 6102 + /* clang-format on */ 6103 + .allowed_s4d1 = LANDLOCK_ACCESS_FS_REFER | LANDLOCK_ACCESS_FS_READ_FILE, 6104 + .expected_read_result = 0, 6105 + .expected_same_dir_rename_result = EACCES, 6106 + .expected_rename_result = EACCES, 6107 + .expected_exchange_result = EACCES, 6108 + }; 6109 + 6110 + /* clang-format off */ 6111 + FIXTURE_VARIANT_ADD(layout5_disconnected_branch, s4d1_rename_parent_create) { 6112 + /* clang-format on */ 6113 + .allowed_s4d1 = LANDLOCK_ACCESS_FS_READ_FILE | 6114 + LANDLOCK_ACCESS_FS_MAKE_REG, 6115 + .expected_read_result = 0, 6116 + .expected_same_dir_rename_result = 0, 6117 + .expected_rename_result = EXDEV, 6118 + .expected_exchange_result = EXDEV, 6119 + }; 6120 + 6121 + /* clang-format off */ 6122 + FIXTURE_VARIANT_ADD(layout5_disconnected_branch, s4d1_rename_parent_rename) { 6123 + /* clang-format on */ 6124 + .allowed_s4d1 = LANDLOCK_ACCESS_FS_REFER | LANDLOCK_ACCESS_FS_MAKE_REG, 6125 + .expected_read_result = EACCES, 6126 + .expected_same_dir_rename_result = 0, 6127 + .expected_rename_result = 0, 6128 + .expected_exchange_result = 0, 6129 + }; 6130 + 6131 + TEST_F_FORK(layout5_disconnected_branch, read_rename_exchange) 6132 + { 6133 + const __u64 handled_access = 6134 + LANDLOCK_ACCESS_FS_REFER | LANDLOCK_ACCESS_FS_READ_FILE | 6135 + LANDLOCK_ACCESS_FS_EXECUTE | LANDLOCK_ACCESS_FS_MAKE_REG; 6136 + const struct rule rules[] = { 6137 + { 6138 + .path = TMP_DIR "/s1d1", 6139 + .access = variant->allowed_s1d1, 6140 + }, 6141 + { 6142 + .path = TMP_DIR "/s1d1/s1d2", 6143 + .access = variant->allowed_s1d2, 6144 + }, 6145 + { 6146 + .path = TMP_DIR "/s1d1/s1d2/s1d3", 6147 + .access = variant->allowed_s1d3, 6148 + }, 6149 + { 6150 + .path = TMP_DIR "/s2d1", 6151 + .access = variant->allowed_s2d1, 6152 + }, 6153 + { 6154 + .path = TMP_DIR "/s2d1/s2d2", 6155 + .access = variant->allowed_s2d2, 6156 + }, 6157 + { 6158 + .path = TMP_DIR "/s2d1/s2d2/s2d3", 6159 + .access = variant->allowed_s2d3, 6160 + }, 6161 + /* s2d4_fd */ 6162 + { 6163 + .path = TMP_DIR "/s3d1", 6164 + .access = variant->allowed_s3d1, 6165 + }, 6166 + /* s3d2_fd */ 6167 + { 6168 + .path = TMP_DIR "/s4d1", 6169 + .access = variant->allowed_s4d1, 6170 + }, 6171 + {}, 6172 + }; 6173 + int ruleset_fd, s1d3_bind_fd; 6174 + 6175 + ruleset_fd = create_ruleset(_metadata, handled_access, rules); 6176 + ASSERT_LE(0, ruleset_fd); 6177 + 6178 + /* Adds rules for the covered directories. */ 6179 + if (variant->allowed_s2d4) { 6180 + ASSERT_EQ(0, landlock_add_rule( 6181 + ruleset_fd, LANDLOCK_RULE_PATH_BENEATH, 6182 + &(struct landlock_path_beneath_attr){ 6183 + .parent_fd = self->s2d4_fd, 6184 + .allowed_access = 6185 + variant->allowed_s2d4, 6186 + }, 6187 + 0)); 6188 + } 6189 + EXPECT_EQ(0, close(self->s2d4_fd)); 6190 + 6191 + if (variant->allowed_s3d2) { 6192 + ASSERT_EQ(0, landlock_add_rule( 6193 + ruleset_fd, LANDLOCK_RULE_PATH_BENEATH, 6194 + &(struct landlock_path_beneath_attr){ 6195 + .parent_fd = self->s3d2_fd, 6196 + .allowed_access = 6197 + variant->allowed_s3d2, 6198 + }, 6199 + 0)); 6200 + } 6201 + EXPECT_EQ(0, close(self->s3d2_fd)); 6202 + 6203 + s1d3_bind_fd = open(TMP_DIR "/s3d1/s3d2/s2d3/s2d4/s1d3", 6204 + O_DIRECTORY | O_PATH | O_CLOEXEC); 6205 + ASSERT_LE(0, s1d3_bind_fd); 6206 + 6207 + /* Disconnects and checks source and destination directories. */ 6208 + EXPECT_EQ(0, test_open_rel(s1d3_bind_fd, "..", O_DIRECTORY)); 6209 + EXPECT_EQ(0, test_open_rel(s1d3_bind_fd, "../..", O_DIRECTORY)); 6210 + /* Renames to make it accessible through s3d1/s1d41 */ 6211 + ASSERT_EQ(0, test_renameat(AT_FDCWD, TMP_DIR "/s2d1/s2d2/s2d3", 6212 + AT_FDCWD, TMP_DIR "/s4d1/s2d3")); 6213 + EXPECT_EQ(0, test_open_rel(s1d3_bind_fd, "..", O_DIRECTORY)); 6214 + EXPECT_EQ(ENOENT, test_open_rel(s1d3_bind_fd, "../..", O_DIRECTORY)); 6215 + 6216 + enforce_ruleset(_metadata, ruleset_fd); 6217 + EXPECT_EQ(0, close(ruleset_fd)); 6218 + 6219 + EXPECT_EQ(variant->expected_read_result, 6220 + test_open_rel(s1d3_bind_fd, "s1d41/f1", O_RDONLY)); 6221 + 6222 + EXPECT_EQ(variant->expected_rename_result, 6223 + test_renameat(s1d3_bind_fd, "s1d41/f1", s1d3_bind_fd, 6224 + "s1d42/f1")); 6225 + EXPECT_EQ(variant->expected_exchange_result, 6226 + test_exchangeat(s1d3_bind_fd, "s1d41/f2", s1d3_bind_fd, 6227 + "s1d42/f3")); 6228 + 6229 + EXPECT_EQ(variant->expected_same_dir_rename_result, 6230 + test_renameat(s1d3_bind_fd, "s1d42/f4", s1d3_bind_fd, 6231 + "s1d42/f5")); 4840 6232 } 4841 6233 4842 6234 #define LOWER_BASE TMP_DIR "/lower"