[PATCH RFC 3/3] selftests/landlock: Test LANDLOCK_ADD_RULE_CREATED
Farid Zakaria
farid.m.zakaria at gmail.com
Sat Oct 24 20:39:19 UTC 2026
Cover the new flag: entries created directly in the directory are
usable while the directory and its existing entries stay unreadable, a
renamed file keeps the rights it had at its source and gains none, a
parent layer covers the files a nested domain creates, an unrelated
domain with the same policy cannot use them, and invalid flag
combinations are rejected.
Assisted-by: LLM
Signed-off-by: Farid Zakaria <farid.m.zakaria at gmail.com>
---
tools/testing/selftests/landlock/fs_test.c | 227 +++++++++++++++++++++++++++++
1 file changed, 227 insertions(+)
diff --git a/tools/testing/selftests/landlock/fs_test.c b/tools/testing/selftests/landlock/fs_test.c
index 7e25a6f6073c..84980d426436 100644
--- a/tools/testing/selftests/landlock/fs_test.c
+++ b/tools/testing/selftests/landlock/fs_test.c
@@ -2640,6 +2640,233 @@ TEST_F_FORK(layout1, refer_mount_root_deny)
EXPECT_EQ(0, close(root_fd));
}
+/* clang-format off */
+#define ACCESS_CREATED ( \
+ ACCESS_RW | \
+ LANDLOCK_ACCESS_FS_MAKE_REG | \
+ LANDLOCK_ACCESS_FS_MAKE_DIR)
+/* clang-format on */
+
+/*
+ * Creates a ruleset where dir_s2d1 plays a shared store: tasks may create and
+ * remove entries in it and use what they create, but not read it otherwise.
+ * dir_s1d1 is a read-write source and dir_s3d1 a source without read access.
+ */
+static int create_created_ruleset(struct __test_metadata *const _metadata)
+{
+ const struct rule rules[] = {
+ {
+ .path = dir_s1d1,
+ .access = ACCESS_RW | LANDLOCK_ACCESS_FS_REFER |
+ LANDLOCK_ACCESS_FS_REMOVE_FILE,
+ },
+ {
+ .path = dir_s2d1,
+ .access = LANDLOCK_ACCESS_FS_MAKE_REG |
+ LANDLOCK_ACCESS_FS_MAKE_DIR |
+ LANDLOCK_ACCESS_FS_REFER |
+ LANDLOCK_ACCESS_FS_REMOVE_FILE,
+ },
+ {
+ .path = dir_s3d1,
+ .access = LANDLOCK_ACCESS_FS_REFER |
+ LANDLOCK_ACCESS_FS_REMOVE_FILE,
+ },
+ {},
+ };
+ const int ruleset_fd = create_ruleset(
+ _metadata, ACCESS_CREATED | LANDLOCK_ACCESS_FS_REFER |
+ LANDLOCK_ACCESS_FS_REMOVE_FILE,
+ rules);
+
+ add_path_beneath(_metadata, ruleset_fd, ACCESS_CREATED, dir_s2d1,
+ LANDLOCK_ADD_RULE_CREATED);
+ return ruleset_fd;
+}
+
+/* Creates a new regular file and returns 0 or the open(2) errno. */
+static int create_file_at(const char *const path)
+{
+ const int fd = open(path, O_CREAT | O_EXCL | O_WRONLY | O_CLOEXEC, 0700);
+
+ if (fd < 0)
+ return errno;
+ close(fd);
+ return 0;
+}
+
+/*
+ * Checks that entries created directly in a LANDLOCK_ADD_RULE_CREATED
+ * directory are usable, while the directory, its pre-existing entries, and
+ * entries created deeper in pre-existing subdirectories stay inaccessible.
+ */
+TEST_F_FORK(layout1, created_rule)
+{
+ const char *const new_file = TMP_DIR "/s2d1/new";
+ const char *const new_dir = TMP_DIR "/s2d1/newdir";
+ const char *const new_nested = TMP_DIR "/s2d1/newdir/f";
+ const int ruleset_fd = create_created_ruleset(_metadata);
+
+ enforce_ruleset(_metadata, ruleset_fd);
+ ASSERT_EQ(0, close(ruleset_fd));
+
+ /* The directory and its existing entries are not readable. */
+ ASSERT_EQ(EACCES, test_open(dir_s2d1, O_RDONLY | O_DIRECTORY));
+ ASSERT_EQ(EACCES, test_open(file1_s2d1, O_RDONLY));
+ ASSERT_EQ(EACCES, test_open(dir_s2d2, O_RDONLY | O_DIRECTORY));
+
+ /* New entries are usable. */
+ ASSERT_EQ(0, create_file_at(new_file));
+ ASSERT_EQ(0, test_open(new_file, O_RDWR));
+ ASSERT_EQ(0, mkdir(new_dir, 0700));
+ ASSERT_EQ(0, test_open(new_dir, O_RDONLY | O_DIRECTORY));
+ ASSERT_EQ(0, create_file_at(new_nested));
+ ASSERT_EQ(0, test_open(new_nested, O_RDWR));
+
+ /*
+ * Only direct children of the directory are covered. The file is
+ * created before open(2) fails, and the layout teardown expects
+ * dir_s2d2 to contain only its own files.
+ */
+ ASSERT_EQ(EACCES, create_file_at(TMP_DIR "/s2d1/s2d2/new"));
+ ASSERT_EQ(0, unlink(TMP_DIR "/s2d1/s2d2/new"));
+}
+
+/*
+ * Checks that a file renamed into a LANDLOCK_ADD_RULE_CREATED directory keeps
+ * the access rights it had at its source and gains none it did not have, by
+ * moving one file from a readable source and one from an unreadable source.
+ */
+TEST_F_FORK(layout1, created_rule_refer)
+{
+ const char *const moved_rw = TMP_DIR "/s2d1/moved_rw";
+ const char *const moved_ro = TMP_DIR "/s2d1/moved_ro";
+ const int ruleset_fd = create_created_ruleset(_metadata);
+
+ enforce_ruleset(_metadata, ruleset_fd);
+ ASSERT_EQ(0, close(ruleset_fd));
+
+ /* Keeps the access rights a file already had at its source. */
+ ASSERT_EQ(0, rename(file1_s1d1, moved_rw));
+ ASSERT_EQ(0, test_open(moved_rw, O_RDWR));
+
+ /* Does not gain access rights a file did not have at its source. */
+ ASSERT_EQ(EACCES, test_open(file1_s3d1, O_RDONLY));
+ ASSERT_EQ(0, rename(file1_s3d1, moved_ro));
+ ASSERT_EQ(EACCES, test_open(moved_ro, O_RDONLY));
+}
+
+/*
+ * Checks that the layer holding the LANDLOCK_ADD_RULE_CREATED rule grants
+ * access to files created by a nested domain, by stacking a permissive layer
+ * and creating the file from within it.
+ */
+TEST_F_FORK(layout1, created_rule_nested_domain)
+{
+ const char *const new_file = TMP_DIR "/s2d1/new";
+ const struct rule rules[] = {
+ {
+ .path = TMP_DIR,
+ .access = ACCESS_CREATED,
+ },
+ {},
+ };
+ const int ruleset_fd = create_created_ruleset(_metadata);
+
+ enforce_ruleset(_metadata, ruleset_fd);
+ ASSERT_EQ(0, close(ruleset_fd));
+ enforce_fs(_metadata, ACCESS_CREATED, rules);
+
+ /* The parent layer grants access to what nested domains create. */
+ ASSERT_EQ(0, create_file_at(new_file));
+ ASSERT_EQ(0, test_open(new_file, O_RDWR));
+ ASSERT_EQ(EACCES, test_open(file1_s2d1, O_RDONLY));
+}
+
+/*
+ * Checks that an unrelated domain with the same policy cannot use a file
+ * another domain created, by keeping the creating child alive while the
+ * parent enforces its own domain and tries to read the file.
+ */
+TEST_F_FORK(layout1, created_rule_other_domain)
+{
+ const char *const new_file = TMP_DIR "/s2d1/new";
+ int pipe_created[2], pipe_done[2], status;
+ pid_t child;
+ char buf;
+
+ ASSERT_EQ(0, pipe2(pipe_created, O_CLOEXEC));
+ ASSERT_EQ(0, pipe2(pipe_done, O_CLOEXEC));
+
+ child = fork();
+ ASSERT_LE(0, child);
+ if (child == 0) {
+ const int ruleset_fd = create_created_ruleset(_metadata);
+
+ ASSERT_EQ(0, close(pipe_created[0]));
+ ASSERT_EQ(0, close(pipe_done[1]));
+
+ enforce_ruleset(_metadata, ruleset_fd);
+ ASSERT_EQ(0, create_file_at(new_file));
+ ASSERT_EQ(1, write(pipe_created[1], ".", 1));
+ ASSERT_EQ(1, read(pipe_done[0], &buf, 1));
+ _exit(_metadata->exit_code);
+ }
+
+ /*
+ * Closing the unused ends makes a read return if the other process
+ * fails before writing.
+ */
+ ASSERT_EQ(0, close(pipe_created[1]));
+ ASSERT_EQ(0, close(pipe_done[0]));
+
+ /* A domain with the same policy can't use what another one created. */
+ ASSERT_EQ(1, read(pipe_created[0], &buf, 1));
+ enforce_ruleset(_metadata, create_created_ruleset(_metadata));
+ ASSERT_EQ(EACCES, test_open(new_file, O_RDONLY));
+ ASSERT_EQ(1, write(pipe_done[1], ".", 1));
+
+ ASSERT_EQ(child, waitpid(child, &status, 0));
+ ASSERT_EQ(1, WIFEXITED(status));
+ ASSERT_EQ(EXIT_SUCCESS, WEXITSTATUS(status));
+}
+
+/*
+ * Checks that LANDLOCK_ADD_RULE_CREATED is rejected on a file, combined with
+ * LANDLOCK_ADD_RULE_QUIET, or with an empty allowed_access.
+ */
+TEST_F_FORK(layout1, created_rule_invalid)
+{
+ const int ruleset_fd = create_ruleset(_metadata, ACCESS_CREATED, NULL);
+ struct landlock_path_beneath_attr path_beneath = {
+ .allowed_access = ACCESS_RW,
+ };
+
+ path_beneath.parent_fd = open(file1_s1d1, O_PATH | O_CLOEXEC);
+ ASSERT_LE(0, path_beneath.parent_fd);
+ ASSERT_EQ(-1, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH,
+ &path_beneath,
+ LANDLOCK_ADD_RULE_CREATED));
+ ASSERT_EQ(EINVAL, errno);
+ ASSERT_EQ(0, close(path_beneath.parent_fd));
+
+ path_beneath.parent_fd = open(dir_s1d1, O_PATH | O_CLOEXEC);
+ ASSERT_LE(0, path_beneath.parent_fd);
+ ASSERT_EQ(-1, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH,
+ &path_beneath,
+ LANDLOCK_ADD_RULE_CREATED |
+ LANDLOCK_ADD_RULE_QUIET));
+ ASSERT_EQ(EINVAL, errno);
+
+ path_beneath.allowed_access = 0;
+ ASSERT_EQ(-1, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH,
+ &path_beneath,
+ LANDLOCK_ADD_RULE_CREATED));
+ ASSERT_EQ(ENOMSG, errno);
+ ASSERT_EQ(0, close(path_beneath.parent_fd));
+ ASSERT_EQ(0, close(ruleset_fd));
+}
+
TEST_F_FORK(layout1, refer_part_mount_tree_is_allowed)
{
const struct rule layer1[] = {
--
2.55.0
More information about the Linux-security-module-archive
mailing list