From: Farid Zakaria <farid.m.zakaria@gmail.com>
To: "Mickaël Salaün" <mic@digikod.net>, "Günther Noack" <gnoack@google.com>
Cc: Paul Moore <paul@paul-moore.com>,
James Morris <jmorris@namei.org>,
"Serge E. Hallyn" <serge@hallyn.com>,
Shuah Khan <shuah@kernel.org>, Jonathan Corbet <corbet@lwn.net>,
John Ericson <mail@johnericson.me>,
linux-security-module@vger.kernel.org,
linux-kselftest@vger.kernel.org, linux-doc@vger.kernel.org,
linux-kernel@vger.kernel.org,
Farid Zakaria <farid.m.zakaria@gmail.com>
Subject: [PATCH RFC 3/3] selftests/landlock: Test LANDLOCK_ADD_RULE_CREATED
Date: Sat, 24 Oct 2026 13:39:19 -0700 [thread overview]
Message-ID: <20261024-landlock-created-rights-v1-3-77c8db3f6bd2@gmail.com> (raw)
In-Reply-To: <20261024-landlock-created-rights-v1-0-77c8db3f6bd2@gmail.com>
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@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
next prev parent reply other threads:[~2026-10-24 20:39 UTC|newest]
Thread overview: 7+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-24 20:39 [PATCH RFC 0/3] landlock: Add access rights on created files Farid Zakaria
2026-10-24 20:39 ` [PATCH RFC 1/3] lsm: Call inode_init_security hooks without xattrs Farid Zakaria
2026-10-24 20:39 ` [PATCH RFC 2/3] landlock: Add access rights on created files Farid Zakaria
2026-10-24 20:39 ` Farid Zakaria [this message]
2026-10-24 21:43 ` [PATCH RFC 0/3] " Justin Suess
2026-10-24 22:03 ` Justin Suess
2026-10-24 23:59 ` Farid Zakaria
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20261024-landlock-created-rights-v1-3-77c8db3f6bd2@gmail.com \
--to=farid.m.zakaria@gmail.com \
--cc=corbet@lwn.net \
--cc=gnoack@google.com \
--cc=jmorris@namei.org \
--cc=linux-doc@vger.kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=linux-kselftest@vger.kernel.org \
--cc=linux-security-module@vger.kernel.org \
--cc=mail@johnericson.me \
--cc=mic@digikod.net \
--cc=paul@paul-moore.com \
--cc=serge@hallyn.com \
--cc=shuah@kernel.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®