* [PATCH 1/6] ipc: Move mqueue fs magic to uapi magic header
From: Oxana Kharitonova @ 2026-07-22 12:29 UTC (permalink / raw)
To: mic, gnoack
Cc: paul, jmorris, serge, wangyan01, linux-security-module,
linux-kernel, landlock, oxana, webprosto
In-Reply-To: <20260722122952.42149-1-oxana@cloudflare.com>
Move MQUEUE_MAGIC from ipc/mqueue.c to include/uapi/linux/magic.h so
it can be shared by code outside of ipc/mqueue.c without duplicating
the constant.
Signed-off-by: Oxana Kharitonova <oxana@cloudflare.com>
---
include/uapi/linux/magic.h | 2 ++
ipc/mqueue.c | 2 +-
2 files changed, 3 insertions(+), 1 deletion(-)
diff --git a/include/uapi/linux/magic.h b/include/uapi/linux/magic.h
index 4f2da935a76c..8e5d33bb1453 100644
--- a/include/uapi/linux/magic.h
+++ b/include/uapi/linux/magic.h
@@ -78,6 +78,8 @@
#define V9FS_MAGIC 0x01021997
+#define MQUEUE_MAGIC 0x19800202
+
#define BDEVFS_MAGIC 0x62646576
#define DAXFS_MAGIC 0x64646178
#define BINFMTFS_MAGIC 0x42494e4d
diff --git a/ipc/mqueue.c b/ipc/mqueue.c
index 4798b375972b..9515597d45ce 100644
--- a/ipc/mqueue.c
+++ b/ipc/mqueue.c
@@ -22,6 +22,7 @@
#include <linux/sysctl.h>
#include <linux/poll.h>
#include <linux/mqueue.h>
+#include <linux/magic.h>
#include <linux/msg.h>
#include <linux/skbuff.h>
#include <linux/vmalloc.h>
@@ -47,7 +48,6 @@ struct mqueue_fs_context {
bool newns; /* Set if newly created ipc namespace */
};
-#define MQUEUE_MAGIC 0x19800202
#define DIRENT_SIZE 20
#define FILENT_SIZE 80
--
2.50.1 (Apple Git-155)
^ permalink raw reply related
* [PATCH 2/6] landlock: Scope POSIX message queue opens
From: Oxana Kharitonova @ 2026-07-22 12:29 UTC (permalink / raw)
To: mic, gnoack
Cc: paul, jmorris, serge, wangyan01, linux-security-module,
linux-kernel, landlock, oxana, webprosto
In-Reply-To: <20260722122952.42149-1-oxana@cloudflare.com>
Add support for enforcing LANDLOCK_SCOPE_POSIX_MSG_QUEUE when opening
POSIX message queues.
Tag mqueuefs inodes at instantiation time with the landlock domain of
the task that created the queue. This domain is stored in the landlock
inode security blob and kept alive until the inode security blob is
released.
On file open, detect mqueuefs regular files and compare the queue
creator's domain with the opener's scoped domain. Deny the open when
the opener is restricted by LANDLOCK_SCOPE_POSIX_MSG_QUEUE and the queue
was created outside of an allowed parent domain.
This makes POSIX message queues follow the same scoped-domain model as
the existing landlock IPC restrictions, while keeping the queue creator
domain tied to the lifetime of the queue inode.
Signed-off-by: Oxana Kharitonova <oxana@cloudflare.com>
---
include/uapi/linux/landlock.h | 1 +
security/landlock/audit.c | 9 ++++++++
security/landlock/audit.h | 1 +
security/landlock/fs.c | 35 ++++++++++++++++++++++++++++
security/landlock/fs.h | 15 ++++++++++++
security/landlock/limits.h | 2 +-
security/landlock/ruleset.c | 1 -
security/landlock/task.c | 43 +++++++++++++++++++++++++++++++++++
security/landlock/task.h | 4 ++++
9 files changed, 109 insertions(+), 2 deletions(-)
diff --git a/include/uapi/linux/landlock.h b/include/uapi/linux/landlock.h
index 272f047df438..96d0c3b423ac 100644
--- a/include/uapi/linux/landlock.h
+++ b/include/uapi/linux/landlock.h
@@ -491,6 +491,7 @@ struct landlock_net_port_attr {
/* clang-format off */
#define LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET (1ULL << 0)
#define LANDLOCK_SCOPE_SIGNAL (1ULL << 1)
+#define LANDLOCK_SCOPE_POSIX_MSG_QUEUE (1ULL << 2)
/* clang-format on*/
#endif /* _UAPI_LINUX_LANDLOCK_H */
diff --git a/security/landlock/audit.c b/security/landlock/audit.c
index 50536c568526..895397b5cbca 100644
--- a/security/landlock/audit.c
+++ b/security/landlock/audit.c
@@ -82,6 +82,10 @@ get_blocker(const enum landlock_request_type type,
case LANDLOCK_REQUEST_SCOPE_SIGNAL:
WARN_ON_ONCE(access_bit != -1);
return "scope.signal";
+
+ case LANDLOCK_REQUEST_SCOPE_POSIX_MSG_QUEUE:
+ WARN_ON_ONCE(access_bit != -1);
+ return "scope.posix_msg_queue";
}
WARN_ON_ONCE(1);
@@ -646,6 +650,11 @@ void landlock_log_denial(const struct landlock_cred_security *const subject,
!!(quiet_mask &
LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET);
break;
+ case LANDLOCK_REQUEST_SCOPE_POSIX_MSG_QUEUE:
+ quiet_applicable_to_access =
+ !!(quiet_mask &
+ LANDLOCK_SCOPE_POSIX_MSG_QUEUE);
+ break;
/*
* Leave LANDLOCK_REQUEST_PTRACE and
* LANDLOCK_REQUEST_FS_CHANGE_TOPOLOGY unhandled for now - they
diff --git a/security/landlock/audit.h b/security/landlock/audit.h
index 620f8a24291d..ce85417548fa 100644
--- a/security/landlock/audit.h
+++ b/security/landlock/audit.h
@@ -21,6 +21,7 @@ enum landlock_request_type {
LANDLOCK_REQUEST_NET_ACCESS,
LANDLOCK_REQUEST_SCOPE_ABSTRACT_UNIX_SOCKET,
LANDLOCK_REQUEST_SCOPE_SIGNAL,
+ LANDLOCK_REQUEST_SCOPE_POSIX_MSG_QUEUE,
};
/*
diff --git a/security/landlock/fs.c b/security/landlock/fs.c
index f7e5e4ef9eac..c6558b448a06 100644
--- a/security/landlock/fs.c
+++ b/security/landlock/fs.c
@@ -51,6 +51,7 @@
#include "object.h"
#include "ruleset.h"
#include "setup.h"
+#include "task.h"
/* Underlying object management */
@@ -1268,6 +1269,33 @@ static void hook_inode_free_security_rcu(void *inode_security)
*/
inode_sec = inode_security + landlock_blob_sizes.lbs_inode;
WARN_ON_ONCE(inode_sec->object);
+
+ landlock_put_ruleset_deferred(inode_sec->mq_domain);
+}
+
+/*
+ * Tag a newly created POSIX message queue with its creator's domain.
+ *
+ * This is the earliest reachable point with both the creator's context and a
+ * fully initialized inode.
+ */
+static void hook_d_instantiate(struct dentry *const dentry,
+ struct inode *const inode)
+{
+ struct landlock_ruleset *dom;
+
+ if (!landlock_is_posix_mqueue_inode(inode))
+ return;
+
+ dom = landlock_get_current_domain();
+ if (!dom)
+ return;
+
+ if (WARN_ON_ONCE(landlock_inode(inode)->mq_domain))
+ return;
+
+ landlock_get_ruleset(dom);
+ landlock_inode(inode)->mq_domain = dom;
}
/* Super-block hooks */
@@ -1756,6 +1784,12 @@ static int hook_file_open(struct file *const file)
const struct landlock_cred_security *const subject =
landlock_get_applicable_subject(file->f_cred, any_fs, NULL);
struct landlock_request request = {};
+ int err;
+
+ /* POSIX message queue scoping is independent of FS access rights. */
+ err = landlock_check_posix_mqueue_open(file);
+ if (err)
+ return err;
if (!subject)
return 0;
@@ -1979,6 +2013,7 @@ static void hook_file_free_security(struct file *file)
static struct security_hook_list landlock_hooks[] __ro_after_init = {
LSM_HOOK_INIT(inode_free_security_rcu, hook_inode_free_security_rcu),
+ LSM_HOOK_INIT(d_instantiate, hook_d_instantiate),
LSM_HOOK_INIT(sb_delete, hook_sb_delete),
LSM_HOOK_INIT(sb_mount, hook_sb_mount),
diff --git a/security/landlock/fs.h b/security/landlock/fs.h
index b4421d9df68f..aefe078845fa 100644
--- a/security/landlock/fs.h
+++ b/security/landlock/fs.h
@@ -14,6 +14,7 @@
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/rcupdate.h>
+#include <uapi/linux/magic.h>
#include "access.h"
#include "cred.h"
@@ -38,6 +39,14 @@ struct landlock_inode_security {
* performed by get_inode_object().
*/
struct landlock_object __rcu *object;
+ /**
+ * @mq_domain: Domain of the task that created POSIX message queue.
+ * Only set for mqueuefs inodes (i.e. s_magic == MQUEUE_MAGIC) at
+ * creation time by hook_d_instantiate(), never modified afterwards.
+ * Used to check LANDLOCK_SCOPE_POSIX_MSG_QUEUE against the
+ * accessing task's domain.
+ */
+ struct landlock_ruleset *mq_domain;
};
/**
@@ -141,6 +150,12 @@ landlock_inode(const struct inode *const inode)
return inode->i_security + landlock_blob_sizes.lbs_inode;
}
+static inline bool
+landlock_is_posix_mqueue_inode(const struct inode *const inode)
+{
+ return S_ISREG(inode->i_mode) && inode->i_sb->s_magic == MQUEUE_MAGIC;
+}
+
static inline struct landlock_superblock_security *
landlock_superblock(const struct super_block *const superblock)
{
diff --git a/security/landlock/limits.h b/security/landlock/limits.h
index 08d5f2f6d321..70a7c5c7af85 100644
--- a/security/landlock/limits.h
+++ b/security/landlock/limits.h
@@ -27,7 +27,7 @@
#define LANDLOCK_MASK_ACCESS_NET ((LANDLOCK_LAST_ACCESS_NET << 1) - 1)
#define LANDLOCK_NUM_ACCESS_NET __const_hweight64(LANDLOCK_MASK_ACCESS_NET)
-#define LANDLOCK_LAST_SCOPE LANDLOCK_SCOPE_SIGNAL
+#define LANDLOCK_LAST_SCOPE LANDLOCK_SCOPE_POSIX_MSG_QUEUE
#define LANDLOCK_MASK_SCOPE ((LANDLOCK_LAST_SCOPE << 1) - 1)
#define LANDLOCK_NUM_SCOPE __const_hweight64(LANDLOCK_MASK_SCOPE)
diff --git a/security/landlock/ruleset.c b/security/landlock/ruleset.c
index 4dd09ea22c84..aa37d50ead87 100644
--- a/security/landlock/ruleset.c
+++ b/security/landlock/ruleset.c
@@ -520,7 +520,6 @@ static void free_ruleset_work(struct work_struct *const work)
free_ruleset(ruleset);
}
-/* Only called by hook_cred_free(). */
void landlock_put_ruleset_deferred(struct landlock_ruleset *const ruleset)
{
if (ruleset && refcount_dec_and_test(&ruleset->usage)) {
diff --git a/security/landlock/task.c b/security/landlock/task.c
index 55522a601367..d65e43550b26 100644
--- a/security/landlock/task.c
+++ b/security/landlock/task.c
@@ -453,6 +453,49 @@ static int hook_file_send_sigiotask(struct task_struct *tsk,
return -EPERM;
}
+static const struct access_masks posix_mqueue_scope = {
+ .scope = LANDLOCK_SCOPE_POSIX_MSG_QUEUE,
+};
+
+/**
+ * landlock_check_posix_mqueue_open - Deny opening a POSIX message queue
+ * created by a task from a different (non-ancestor) domain
+ *
+ * @file: The mqueuefs file being opened.
+ *
+ * Return: -EPERM if the open must be denied, 0 otherwise.
+ */
+int landlock_check_posix_mqueue_open(struct file *const file)
+{
+ const struct inode *const inode = file_inode(file);
+ const struct landlock_cred_security *subject;
+ size_t handle_layer;
+
+ if (!landlock_is_posix_mqueue_inode(inode))
+ return 0;
+
+ subject = landlock_get_applicable_subject(file->f_cred,
+ posix_mqueue_scope,
+ &handle_layer);
+ if (!subject)
+ return 0;
+
+ if (!domain_is_scoped(subject->domain,
+ landlock_inode(inode)->mq_domain,
+ LANDLOCK_SCOPE_POSIX_MSG_QUEUE))
+ return 0;
+
+ landlock_log_denial(subject, &(struct landlock_request) {
+ .type = LANDLOCK_REQUEST_SCOPE_POSIX_MSG_QUEUE,
+ .audit = {
+ .type = LSM_AUDIT_DATA_FILE,
+ .u.file = file,
+ },
+ .layer_plus_one = handle_layer + 1,
+ });
+ return -EPERM;
+}
+
static struct security_hook_list landlock_hooks[] __ro_after_init = {
LSM_HOOK_INIT(ptrace_access_check, hook_ptrace_access_check),
LSM_HOOK_INIT(ptrace_traceme, hook_ptrace_traceme),
diff --git a/security/landlock/task.h b/security/landlock/task.h
index 7c00360219a2..0b031dc17bbf 100644
--- a/security/landlock/task.h
+++ b/security/landlock/task.h
@@ -9,6 +9,10 @@
#ifndef _SECURITY_LANDLOCK_TASK_H
#define _SECURITY_LANDLOCK_TASK_H
+#include <linux/fs.h>
+
__init void landlock_add_task_hooks(void);
+int landlock_check_posix_mqueue_open(struct file *const file);
+
#endif /* _SECURITY_LANDLOCK_TASK_H */
--
2.50.1 (Apple Git-155)
^ permalink raw reply related
* [PATCH 3/6] landlock: Bump ABI for LANDLOCK_SCOPE_POSIX_MSG_QUEUE
From: Oxana Kharitonova @ 2026-07-22 12:29 UTC (permalink / raw)
To: mic, gnoack
Cc: paul, jmorris, serge, wangyan01, linux-security-module,
linux-kernel, landlock, oxana, webprosto
In-Reply-To: <20260722122952.42149-1-oxana@cloudflare.com>
Increase the landlock ABI version so userspace can detect support for
LANDLOCK_SCOPE_POSIX_MSG_QUEUE.
Signed-off-by: Oxana Kharitonova <oxana@cloudflare.com>
---
security/landlock/syscalls.c | 2 +-
tools/testing/selftests/landlock/base_test.c | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/security/landlock/syscalls.c b/security/landlock/syscalls.c
index 36b02892c62f..84521a31bf75 100644
--- a/security/landlock/syscalls.c
+++ b/security/landlock/syscalls.c
@@ -169,7 +169,7 @@ static const struct file_operations ruleset_fops = {
* If the change involves a fix that requires userspace awareness, also update
* the errata documentation in Documentation/userspace-api/landlock.rst .
*/
-const int landlock_abi_version = 10;
+const int landlock_abi_version = 11;
/**
* sys_landlock_create_ruleset - Create a new ruleset
diff --git a/tools/testing/selftests/landlock/base_test.c b/tools/testing/selftests/landlock/base_test.c
index cbd3c1669951..b8b5fa1042ba 100644
--- a/tools/testing/selftests/landlock/base_test.c
+++ b/tools/testing/selftests/landlock/base_test.c
@@ -76,7 +76,7 @@ TEST(abi_version)
const struct landlock_ruleset_attr ruleset_attr = {
.handled_access_fs = LANDLOCK_ACCESS_FS_READ_FILE,
};
- ASSERT_EQ(10, landlock_create_ruleset(NULL, 0,
+ ASSERT_EQ(11, landlock_create_ruleset(NULL, 0,
LANDLOCK_CREATE_RULESET_VERSION));
ASSERT_EQ(-1, landlock_create_ruleset(&ruleset_attr, 0,
--
2.50.1 (Apple Git-155)
^ permalink raw reply related
* [PATCH 4/6] selftests/landlock: Test POSIX message queue scoping
From: Oxana Kharitonova @ 2026-07-22 12:29 UTC (permalink / raw)
To: mic, gnoack
Cc: paul, jmorris, serge, wangyan01, linux-security-module,
linux-kernel, landlock, oxana, webprosto
In-Reply-To: <20260722122952.42149-1-oxana@cloudflare.com>
Add tests for LANDLOCK_SCOPE_POSIX_MSG_QUEUE.
Verify that opening a queue created outside the current scoped domain is
denied, and that opening a queue created within the same domain remains
allowed.
Signed-off-by: Oxana Kharitonova <oxana@cloudflare.com>
---
.../landlock/scoped_posix_msg_queue_test.c | 223 ++++++++++++++++++
.../testing/selftests/landlock/scoped_test.c | 2 +-
2 files changed, 224 insertions(+), 1 deletion(-)
create mode 100644 tools/testing/selftests/landlock/scoped_posix_msg_queue_test.c
diff --git a/tools/testing/selftests/landlock/scoped_posix_msg_queue_test.c b/tools/testing/selftests/landlock/scoped_posix_msg_queue_test.c
new file mode 100644
index 000000000000..602ba5c7a83d
--- /dev/null
+++ b/tools/testing/selftests/landlock/scoped_posix_msg_queue_test.c
@@ -0,0 +1,223 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Landlock tests - POSIX message queue scoping
+ *
+ * Copyright © 2024-2026 Microsoft Corporation
+ */
+
+#define _GNU_SOURCE
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <linux/landlock.h>
+#include <mqueue.h>
+#include <stddef.h>
+#include <string.h>
+#include <sys/prctl.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include "common.h"
+#include "scoped_common.h"
+
+static void set_mq_name(char *const name, const size_t size)
+{
+ snprintf(name, size, "/selftests-landlock-mq-tid%d", sys_gettid());
+}
+
+FIXTURE(scoped_domains)
+{
+ char mq_name[NAME_MAX];
+};
+
+#include "scoped_base_variants.h"
+
+FIXTURE_SETUP(scoped_domains)
+{
+ drop_caps(_metadata);
+
+ set_mq_name(self->mq_name, sizeof(self->mq_name));
+ /* Removes a possibly stale queue from a previous run. */
+ mq_unlink(self->mq_name);
+}
+
+FIXTURE_TEARDOWN(scoped_domains)
+{
+}
+
+/*
+ * The parent creates the message queue, then the child tries
+ * to open it, with scoped domain(s) or no domain at all.
+ */
+TEST_F(scoped_domains, open_parent_queue)
+{
+ pid_t child;
+ int status;
+ int pipe_parent[2], pipe_child[2];
+ char buf;
+ mqd_t mq;
+
+ ASSERT_EQ(0, pipe2(pipe_parent, O_CLOEXEC));
+ ASSERT_EQ(0, pipe2(pipe_child, O_CLOEXEC));
+
+ if (variant->domain_both)
+ create_scoped_domain(_metadata, LANDLOCK_SCOPE_POSIX_MSG_QUEUE);
+
+ child = fork();
+ ASSERT_LE(0, child);
+ if (child == 0) {
+ mqd_t mq_child;
+
+ EXPECT_EQ(0, close(pipe_parent[1]));
+ EXPECT_EQ(0, close(pipe_child[0]));
+
+ if (variant->domain_child)
+ create_scoped_domain(_metadata,
+ LANDLOCK_SCOPE_POSIX_MSG_QUEUE);
+
+ /* Waits for the parent to create the queue. */
+ ASSERT_EQ(1, read(pipe_parent[0], &buf, 1));
+
+ mq_child = mq_open(self->mq_name, O_RDWR);
+ if (!variant->domain_child) {
+ EXPECT_LE(0, mq_child);
+ if (mq_child >= 0)
+ EXPECT_EQ(0, mq_close(mq_child));
+ } else {
+ EXPECT_EQ(-1, mq_child);
+ EXPECT_EQ(EPERM, errno);
+ }
+
+ ASSERT_EQ(1, write(pipe_child[1], ".", 1));
+ _exit(_metadata->exit_code);
+ return;
+ }
+ EXPECT_EQ(0, close(pipe_parent[0]));
+ EXPECT_EQ(0, close(pipe_child[1]));
+
+ if (variant->domain_parent)
+ create_scoped_domain(_metadata, LANDLOCK_SCOPE_POSIX_MSG_QUEUE);
+
+ mq = mq_open(self->mq_name, O_CREAT | O_RDWR, 0600, NULL);
+ ASSERT_LE(0, mq);
+
+ ASSERT_EQ(1, write(pipe_parent[1], ".", 1));
+ ASSERT_EQ(1, read(pipe_child[0], &buf, 1));
+
+ ASSERT_EQ(child, waitpid(child, &status, 0));
+ EXPECT_EQ(0, mq_close(mq));
+ EXPECT_EQ(0, mq_unlink(self->mq_name));
+
+ if (WIFSIGNALED(status) || !WIFEXITED(status) ||
+ WEXITSTATUS(status) != EXIT_SUCCESS)
+ _metadata->exit_code = KSFT_FAIL;
+}
+
+/*
+ * The child creates the message queue, then the parent tries
+ * to open it, with scoped domain(s) or no domain at all.
+ */
+TEST_F(scoped_domains, open_child_queue)
+{
+ pid_t child;
+ bool can_open_child_queue;
+ int status;
+ int pipe_parent[2], pipe_child[2];
+ char buf;
+ mqd_t mq;
+
+ can_open_child_queue = !variant->domain_parent;
+
+ ASSERT_EQ(0, pipe2(pipe_parent, O_CLOEXEC));
+ ASSERT_EQ(0, pipe2(pipe_child, O_CLOEXEC));
+
+ if (variant->domain_both)
+ create_scoped_domain(_metadata, LANDLOCK_SCOPE_POSIX_MSG_QUEUE);
+
+ child = fork();
+ ASSERT_LE(0, child);
+ if (child == 0) {
+ mqd_t mq_child;
+
+ EXPECT_EQ(0, close(pipe_parent[1]));
+ EXPECT_EQ(0, close(pipe_child[0]));
+
+ if (variant->domain_child)
+ create_scoped_domain(_metadata,
+ LANDLOCK_SCOPE_POSIX_MSG_QUEUE);
+
+ /* Waits for the parent to be in a domain, if any. */
+ ASSERT_EQ(1, read(pipe_parent[0], &buf, 1));
+
+ mq_child = mq_open(self->mq_name, O_CREAT | O_RDWR, 0600, NULL);
+ ASSERT_LE(0, mq_child);
+
+ ASSERT_EQ(1, write(pipe_child[1], ".", 1));
+
+ ASSERT_EQ(1, read(pipe_parent[0], &buf, 1));
+ EXPECT_EQ(0, mq_close(mq_child));
+ EXPECT_EQ(0, mq_unlink(self->mq_name));
+ _exit(_metadata->exit_code);
+ return;
+ }
+ EXPECT_EQ(0, close(pipe_parent[0]));
+ EXPECT_EQ(0, close(pipe_child[1]));
+
+ if (variant->domain_parent)
+ create_scoped_domain(_metadata, LANDLOCK_SCOPE_POSIX_MSG_QUEUE);
+
+ /* Signals that the parent is in a domain, if any. */
+ ASSERT_EQ(1, write(pipe_parent[1], ".", 1));
+
+ /* Waits for the child to create the queue. */
+ ASSERT_EQ(1, read(pipe_child[0], &buf, 1));
+
+ mq = mq_open(self->mq_name, O_RDWR);
+ if (can_open_child_queue) {
+ EXPECT_LE(0, mq);
+ if (mq >= 0)
+ EXPECT_EQ(0, mq_close(mq));
+ } else {
+ EXPECT_EQ(-1, mq);
+ EXPECT_EQ(EPERM, errno);
+ }
+
+ /* Signals to the child that the open attempt is done. */
+ ASSERT_EQ(1, write(pipe_parent[1], ".", 1));
+
+ ASSERT_EQ(child, waitpid(child, &status, 0));
+ if (WIFSIGNALED(status) || !WIFEXITED(status) ||
+ WEXITSTATUS(status) != EXIT_SUCCESS)
+ _metadata->exit_code = KSFT_FAIL;
+}
+
+/*
+ * A process must always be able to open a queue it created within its own
+ * domain, whatever the enforced scope is.
+ */
+TEST(create_and_open_same_domain)
+{
+ char mq_name[NAME_MAX];
+ mqd_t mq_create, mq_open_again;
+
+ drop_caps(_metadata);
+ set_mq_name(mq_name, sizeof(mq_name));
+ mq_unlink(mq_name);
+
+ create_scoped_domain(_metadata, LANDLOCK_SCOPE_POSIX_MSG_QUEUE);
+
+ mq_create = mq_open(mq_name, O_CREAT | O_RDWR, 0600, NULL);
+ ASSERT_LE(0, mq_create);
+
+ mq_open_again = mq_open(mq_name, O_RDWR);
+ EXPECT_LE(0, mq_open_again);
+ if (mq_open_again >= 0)
+ EXPECT_EQ(0, mq_close(mq_open_again));
+
+ EXPECT_EQ(0, mq_close(mq_create));
+ EXPECT_EQ(0, mq_unlink(mq_name));
+}
+
+TEST_HARNESS_MAIN
diff --git a/tools/testing/selftests/landlock/scoped_test.c b/tools/testing/selftests/landlock/scoped_test.c
index b90f76ed0d9c..c530baa50948 100644
--- a/tools/testing/selftests/landlock/scoped_test.c
+++ b/tools/testing/selftests/landlock/scoped_test.c
@@ -12,7 +12,7 @@
#include "common.h"
-#define ACCESS_LAST LANDLOCK_SCOPE_SIGNAL
+#define ACCESS_LAST LANDLOCK_SCOPE_POSIX_MSG_QUEUE
TEST(ruleset_with_unknown_scope)
{
--
2.50.1 (Apple Git-155)
^ permalink raw reply related
* [PATCH 5/6] samples/landlock: Support POSIX message queue scoping
From: Oxana Kharitonova @ 2026-07-22 12:29 UTC (permalink / raw)
To: mic, gnoack
Cc: paul, jmorris, serge, wangyan01, linux-security-module,
linux-kernel, landlock, oxana, webprosto
In-Reply-To: <20260722122952.42149-1-oxana@cloudflare.com>
Teach the sandboxer sample to request LANDLOCK_SCOPE_POSIX_MSG_QUEUE
through LL_SCOPED.
Add the "q" scope selector for POSIX message queues and document it in
the sample help text. Also allow POSIX message queue denials to be
quieted with "posix_msg_queue" through LL_QUIET_ACCESS.
Signed-off-by: Oxana Kharitonova <oxana@cloudflare.com>
---
samples/landlock/sandboxer.c | 16 ++++++++++++++--
1 file changed, 14 insertions(+), 2 deletions(-)
diff --git a/samples/landlock/sandboxer.c b/samples/landlock/sandboxer.c
index ac71019e6212..59024ca53398 100644
--- a/samples/landlock/sandboxer.c
+++ b/samples/landlock/sandboxer.c
@@ -240,10 +240,12 @@ static bool check_ruleset_scope(const char *const env_var,
bool error = false;
bool abstract_scoping = false;
bool signal_scoping = false;
+ bool posix_mqueue_scoping = false;
/* Scoping is not supported by Landlock ABI */
if (!(ruleset_attr->scoped &
- (LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET | LANDLOCK_SCOPE_SIGNAL)))
+ (LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET | LANDLOCK_SCOPE_SIGNAL |
+ LANDLOCK_SCOPE_POSIX_MSG_QUEUE)))
goto out_unset;
env_type_scope = getenv(env_var);
@@ -260,6 +262,9 @@ static bool check_ruleset_scope(const char *const env_var,
} else if (strcmp("s", ipc_scoping_name) == 0 &&
!signal_scoping) {
signal_scoping = true;
+ } else if (strcmp("q", ipc_scoping_name) == 0 &&
+ !posix_mqueue_scoping) {
+ posix_mqueue_scoping = true;
} else {
fprintf(stderr, "Unknown or duplicate scope \"%s\"\n",
ipc_scoping_name);
@@ -276,6 +281,8 @@ static bool check_ruleset_scope(const char *const env_var,
ruleset_attr->scoped &= ~LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET;
if (!signal_scoping)
ruleset_attr->scoped &= ~LANDLOCK_SCOPE_SIGNAL;
+ if (!posix_mqueue_scoping)
+ ruleset_attr->scoped &= ~LANDLOCK_SCOPE_POSIX_MSG_QUEUE;
unsetenv(env_var);
return error;
@@ -354,6 +361,9 @@ static int add_quiet_access(const char *const env_var,
LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET;
else if (strcmp(str_access, "signal") == 0)
ruleset_attr->quiet_scoped |= LANDLOCK_SCOPE_SIGNAL;
+ else if (strcmp(str_access, "posix_msg_queue") == 0)
+ ruleset_attr->quiet_scoped |=
+ LANDLOCK_SCOPE_POSIX_MSG_QUEUE;
else {
fprintf(stderr, "Unknown quiet access \"%s\"\n",
str_access);
@@ -400,6 +410,7 @@ static const char help[] =
"* " ENV_SCOPED_NAME ": actions denied on the outside of the landlock domain\n"
" - \"a\" to restrict opening abstract unix sockets\n"
" - \"s\" to restrict sending signals\n"
+ " - \"q\" to restrict opening POSIX message queues\n"
"\n"
"A sandboxer should not log denied access requests to avoid spamming logs, "
"but to test audit we can set " ENV_FORCE_LOG_NAME "=1\n"
@@ -416,6 +427,7 @@ static const char help[] =
" - \"udp_connect\" to quiet udp connect / send denials\n"
" - \"abstract_unix_socket\" to quiet abstract unix socket denials\n"
" - \"signal\" to quiet signal denials\n"
+ " - \"posix_msg_queue\" to quiet POSIX message queue denials\n"
"\n"
"Example:\n"
ENV_FS_RO_NAME "=\"${PATH}:/lib:/usr:/proc:/etc:/dev/urandom\" "
@@ -423,7 +435,7 @@ static const char help[] =
ENV_TCP_BIND_NAME "=\"9418\" "
ENV_TCP_CONNECT_NAME "=\"80:443\" "
ENV_UDP_CONNECT_SEND_NAME "=\"53\" "
- ENV_SCOPED_NAME "=\"a:s\" "
+ ENV_SCOPED_NAME "=\"a:s:q\" "
"%1$s bash -i\n"
"\n"
"This sandboxer can use Landlock features up to ABI version "
--
2.50.1 (Apple Git-155)
^ permalink raw reply related
* [PATCH 6/6] landlock: Document POSIX message queue scoping
From: Oxana Kharitonova @ 2026-07-22 12:29 UTC (permalink / raw)
To: mic, gnoack
Cc: paul, jmorris, serge, wangyan01, linux-security-module,
linux-kernel, landlock, oxana, webprosto
In-Reply-To: <20260722122952.42149-1-oxana@cloudflare.com>
Document LANDLOCK_SCOPE_POSIX_MSG_QUEUE in the userspace API and UAPI
kernel-doc.
Signed-off-by: Oxana Kharitonova <oxana@cloudflare.com>
---
Documentation/admin-guide/LSM/landlock.rst | 6 ++++--
Documentation/userspace-api/landlock.rst | 11 ++++++++++-
include/uapi/linux/landlock.h | 6 +++++-
3 files changed, 19 insertions(+), 4 deletions(-)
diff --git a/Documentation/admin-guide/LSM/landlock.rst b/Documentation/admin-guide/LSM/landlock.rst
index 8eb85c9381ff..1ecb2017271b 100644
--- a/Documentation/admin-guide/LSM/landlock.rst
+++ b/Documentation/admin-guide/LSM/landlock.rst
@@ -60,9 +60,11 @@ AUDIT_LANDLOCK_ACCESS
- net.bind_udp - UDP port binding was denied
- net.connect_send_udp - UDP connection and send was denied
- **scope.*** - IPC scoping restrictions (ABI 6+):
+ **scope.*** - IPC scoping restrictions:
- scope.abstract_unix_socket - Abstract UNIX socket connection denied
- - scope.signal - Signal sending denied
+ (ABI 6+)
+ - scope.signal - Signal sending denied (ABI 6+)
+ - scope.posix_msg_queue - POSIX message queue opening denied (ABI 11+)
Multiple blockers can appear in a single event (comma-separated) when
multiple access rights are missing. For example, creating a regular file
diff --git a/Documentation/userspace-api/landlock.rst b/Documentation/userspace-api/landlock.rst
index 5a63d4476c1c..c4fa84e6d281 100644
--- a/Documentation/userspace-api/landlock.rst
+++ b/Documentation/userspace-api/landlock.rst
@@ -86,7 +86,8 @@ to be explicit about the denied-by-default access rights.
LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP,
.scoped =
LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET |
- LANDLOCK_SCOPE_SIGNAL,
+ LANDLOCK_SCOPE_SIGNAL |
+ LANDLOCK_SCOPE_POSIX_MSG_QUEUE,
};
Because we may not know which kernel version an application will be executed
@@ -140,6 +141,10 @@ version, and only use the available subset of access rights:
ruleset_attr.handled_access_net &=
~(LANDLOCK_ACCESS_NET_BIND_UDP |
LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP);
+ __attribute__((fallthrough));
+ case 10:
+ /* Removes LANDLOCK_SCOPE_POSIX_MSG_QUEUE for ABI < 11 */
+ ruleset_attr.scoped &= ~LANDLOCK_SCOPE_POSIX_MSG_QUEUE;
}
This enables the creation of an inclusive ruleset that will contain our rules.
@@ -420,6 +425,10 @@ The operations which can be scoped are:
A :manpage:`sendto(2)` on a socket which was previously connected will not
be restricted. This works for both datagram and stream sockets.
+``LANDLOCK_SCOPE_POSIX_MSG_QUEUE``
+ This limits opening POSIX message queues to queues created by a process in
+ the same or a nested Landlock domain.
+
IPC scoping does not support exceptions via :manpage:`landlock_add_rule(2)`.
If an operation is scoped within a domain, no rules can be added to allow access
to resources or processes outside of the scope.
diff --git a/include/uapi/linux/landlock.h b/include/uapi/linux/landlock.h
index 96d0c3b423ac..21ba31ad4d4b 100644
--- a/include/uapi/linux/landlock.h
+++ b/include/uapi/linux/landlock.h
@@ -478,7 +478,9 @@ struct landlock_net_port_attr {
* Setting a flag for a ruleset will isolate the Landlock domain to forbid
* connections to resources outside the domain.
*
- * This is supported since Landlock ABI version 6.
+ * This is supported since Landlock ABI version 6. The
+ * %LANDLOCK_SCOPE_POSIX_MSG_QUEUE scope is supported since Landlock ABI version
+ * 11.
*
* Scopes:
*
@@ -487,6 +489,8 @@ struct landlock_net_port_attr {
* related Landlock domain (e.g., a parent domain or a non-sandboxed process).
* - %LANDLOCK_SCOPE_SIGNAL: Restrict a sandboxed process from sending a signal
* to another process outside the domain.
+ * - %LANDLOCK_SCOPE_POSIX_MSG_QUEUE: Restrict a sandboxed process from opening
+ * a POSIX message queue created outside the domain.
*/
/* clang-format off */
#define LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET (1ULL << 0)
--
2.50.1 (Apple Git-155)
^ permalink raw reply related
* Re: [PATCH 1/6] ipc: Move mqueue fs magic to uapi magic header
From: Günther Noack @ 2026-07-22 12:43 UTC (permalink / raw)
To: Oxana Kharitonova
Cc: mic, paul, jmorris, serge, wangyan01, linux-security-module,
linux-kernel, landlock, webprosto
In-Reply-To: <20260722122952.42149-2-oxana@cloudflare.com>
Hello!
On Wed, Jul 22, 2026 at 01:29:37PM +0100, Oxana Kharitonova wrote:
> Move MQUEUE_MAGIC from ipc/mqueue.c to include/uapi/linux/magic.h so
> it can be shared by code outside of ipc/mqueue.c without duplicating
> the constant.
>
> Signed-off-by: Oxana Kharitonova <oxana@cloudflare.com>
> ---
> include/uapi/linux/magic.h | 2 ++
> ipc/mqueue.c | 2 +-
> 2 files changed, 3 insertions(+), 1 deletion(-)
>
> diff --git a/include/uapi/linux/magic.h b/include/uapi/linux/magic.h
> index 4f2da935a76c..8e5d33bb1453 100644
> --- a/include/uapi/linux/magic.h
> +++ b/include/uapi/linux/magic.h
> @@ -78,6 +78,8 @@
>
> #define V9FS_MAGIC 0x01021997
>
> +#define MQUEUE_MAGIC 0x19800202
> +
> #define BDEVFS_MAGIC 0x62646576
> #define DAXFS_MAGIC 0x64646178
> #define BINFMTFS_MAGIC 0x42494e4d
> diff --git a/ipc/mqueue.c b/ipc/mqueue.c
> index 4798b375972b..9515597d45ce 100644
> --- a/ipc/mqueue.c
> +++ b/ipc/mqueue.c
> @@ -22,6 +22,7 @@
> #include <linux/sysctl.h>
> #include <linux/poll.h>
> #include <linux/mqueue.h>
> +#include <linux/magic.h>
> #include <linux/msg.h>
> #include <linux/skbuff.h>
> #include <linux/vmalloc.h>
> @@ -47,7 +48,6 @@ struct mqueue_fs_context {
> bool newns; /* Set if newly created ipc namespace */
> };
>
> -#define MQUEUE_MAGIC 0x19800202
> #define DIRENT_SIZE 20
> #define FILENT_SIZE 80
>
> --
> 2.50.1 (Apple Git-155)
>
The only reference to MQUEUE_MAGIC that I find in your patchset is from
Landlock's kernel code -- For a value that gets serialized to disk, like
for the other file systems, I think there is actually a legitimate use
case for userspace to use that value, when dealing with raw images? But
that's not the case here, I think?
Should this live in include/linux/ipc_namespace.h instead? Or even be
exposed as a helper function that lets you check whether an inode is
referring to an mqueue?
—Günther
^ permalink raw reply
* [PATCH v1] selftests/landlock: Add tests for O_TMPFILE
From: Mickaël Salaün @ 2026-07-22 15:13 UTC (permalink / raw)
To: Günther Noack
Cc: Mickaël Salaün, linux-security-module, Takao Sato,
Willy Tarreau, brauner, viro
open(2) with O_TMPFILE creates its unnamed inode through vfs_tmpfile(),
which, unlike normal file creation, calls neither security_path_mknod()
nor security_inode_create(). It is nonetheless mediated: vfs_tmpfile()
opens the inode through the filesystem's ->tmpfile() operation, which
reaches security_file_open() via finish_open(). The open is therefore
checked like any other, and materializing the file with linkat(2) is
checked like any other link.
Add tests for both paths so O_TMPFILE cannot bypass Landlock.
Cc: Günther Noack <gnoack@google.com>
Signed-off-by: Mickaël Salaün <mic@digikod.net>
---
tools/testing/selftests/landlock/fs_test.c | 251 +++++++++++++++++++++
1 file changed, 251 insertions(+)
diff --git a/tools/testing/selftests/landlock/fs_test.c b/tools/testing/selftests/landlock/fs_test.c
index cdb47fc1fc0a..b826b2c6a8f0 100644
--- a/tools/testing/selftests/landlock/fs_test.c
+++ b/tools/testing/selftests/landlock/fs_test.c
@@ -450,6 +450,25 @@ static int test_open(const char *const path, const int flags)
return test_open_rel(AT_FDCWD, path, flags);
}
+/*
+ * Opens an anonymous O_TMPFILE inode in the directory dir. O_TMPFILE is always
+ * combined with O_WRONLY or O_RDWR, so the caller must pass one of them in
+ * flags.
+ */
+static int test_tmpfile(const char *const dir, const int flags)
+{
+ int fd;
+
+ fd = open(dir, O_TMPFILE | flags | O_CLOEXEC, 0700);
+ if (fd < 0)
+ return errno;
+
+ if (close(fd) != 0)
+ return errno;
+
+ return 0;
+}
+
TEST_F_FORK(layout1, no_restriction)
{
ASSERT_EQ(0, test_open(dir_s1d1, O_RDONLY));
@@ -2140,6 +2159,238 @@ TEST_F_FORK(layout1, link)
ASSERT_EQ(0, link(file1_s1d3, file2_s1d3));
}
+/*
+ * O_TMPFILE does not go through the path_mknod hook: vfs_tmpfile() creates the
+ * inode without calling security_path_mknod(). These tests verify that the
+ * resulting file is still mediated, via the file_open hook, so O_TMPFILE cannot
+ * be used to bypass Landlock.
+ */
+
+/*
+ * An O_TMPFILE open requires WRITE_FILE (and READ_FILE for O_RDWR) on the
+ * directory hierarchy, exactly like any other writable open. It does not
+ * require (nor is it granted by) MAKE_REG: the anonymous inode is not yet a
+ * named file. O_TMPFILE always implies write access, so a read-only request is
+ * rejected by the VFS with EINVAL before Landlock is consulted; Landlock must
+ * not change that into EACCES.
+ */
+TEST_F_FORK(layout1, open_tmpfile)
+{
+ const struct rule rules[] = {
+ /* Write allowed, but neither MAKE_REG nor READ_FILE. */
+ {
+ .path = dir_s1d1,
+ .access = LANDLOCK_ACCESS_FS_WRITE_FILE,
+ },
+ /* Both read and write allowed. */
+ {
+ .path = dir_s1d2,
+ .access = LANDLOCK_ACCESS_FS_READ_FILE |
+ LANDLOCK_ACCESS_FS_WRITE_FILE,
+ },
+ /* File-creation right without write. */
+ {
+ .path = dir_s2d1,
+ .access = LANDLOCK_ACCESS_FS_MAKE_REG,
+ },
+ {},
+ };
+
+ /* Baseline: an unsandboxed O_TMPFILE open works. */
+ EXPECT_EQ(0, test_tmpfile(dir_s1d1, O_WRONLY));
+ EXPECT_EQ(0, test_tmpfile(dir_s2d1, O_RDWR));
+ EXPECT_EQ(0, test_tmpfile(dir_s3d1, O_RDWR));
+
+ /* O_TMPFILE requires write access: read-only is EINVAL at the VFS. */
+ EXPECT_EQ(EINVAL, test_tmpfile(dir_s1d1, O_RDONLY));
+
+ enforce_fs(_metadata,
+ LANDLOCK_ACCESS_FS_READ_FILE |
+ LANDLOCK_ACCESS_FS_WRITE_FILE |
+ LANDLOCK_ACCESS_FS_MAKE_REG,
+ rules);
+
+ /* Write is enough for an O_WRONLY tmpfile; MAKE_REG is not needed. */
+ EXPECT_EQ(0, test_tmpfile(dir_s1d1, O_WRONLY));
+ /* O_RDWR additionally needs READ_FILE, which is absent here. */
+ EXPECT_EQ(EACCES, test_tmpfile(dir_s1d1, O_RDWR));
+
+ /* Read and write allowed: both open modes succeed. */
+ EXPECT_EQ(0, test_tmpfile(dir_s1d2, O_WRONLY));
+ EXPECT_EQ(0, test_tmpfile(dir_s1d2, O_RDWR));
+
+ /* MAKE_REG without WRITE_FILE does not allow the open. */
+ EXPECT_EQ(EACCES, test_tmpfile(dir_s2d1, O_WRONLY));
+ EXPECT_EQ(EACCES, test_tmpfile(dir_s2d1, O_RDWR));
+
+ /* No rule at all: the open is denied. */
+ EXPECT_EQ(EACCES, test_tmpfile(dir_s3d1, O_WRONLY));
+ EXPECT_EQ(EACCES, test_tmpfile(dir_s3d1, O_RDWR));
+
+ /*
+ * A read-only O_TMPFILE stays EINVAL under Landlock, whether the
+ * directory is fully allowed or has no rule: the VFS rejects the flag
+ * combination before the file_open hook, so Landlock never turns it
+ * into EACCES.
+ */
+ EXPECT_EQ(EINVAL, test_tmpfile(dir_s1d2, O_RDONLY));
+ EXPECT_EQ(EINVAL, test_tmpfile(dir_s3d1, O_RDONLY));
+}
+
+/*
+ * When the ruleset handles neither file read nor write access, Landlock has no
+ * opinion on an O_TMPFILE open and must not interfere with it.
+ */
+TEST_F_FORK(layout1, open_tmpfile_unhandled)
+{
+ const struct rule rules[] = {
+ {
+ .path = dir_s1d2,
+ .access = LANDLOCK_ACCESS_FS_READ_DIR,
+ },
+ {},
+ };
+
+ enforce_fs(_metadata, LANDLOCK_ACCESS_FS_READ_DIR, rules);
+
+ EXPECT_EQ(0, test_tmpfile(dir_s1d1, O_WRONLY));
+ EXPECT_EQ(0, test_tmpfile(dir_s1d3, O_RDWR));
+ EXPECT_EQ(0, test_tmpfile(dir_s3d1, O_RDWR));
+}
+
+/*
+ * Materializing an anonymous O_TMPFILE into its creation directory with
+ * linkat(AT_EMPTY_PATH) is gated by MAKE_REG on that directory, even though
+ * obtaining the writable tmpfile only required WRITE_FILE. This is the check
+ * that stops O_TMPFILE from creating a named file where the sandbox forbids
+ * file creation. Linking into the same directory does not involve reparenting,
+ * so REFER is not required.
+ */
+TEST_F_FORK(layout1, link_tmpfile)
+{
+ int fd;
+ const struct rule rules[] = {
+ /* Write only: the tmpfile opens but cannot be linked. */
+ {
+ .path = dir_s1d1,
+ .access = LANDLOCK_ACCESS_FS_WRITE_FILE,
+ },
+ /* Write and MAKE_REG: the tmpfile opens and can be linked. */
+ {
+ .path = dir_s2d1,
+ .access = LANDLOCK_ACCESS_FS_WRITE_FILE |
+ LANDLOCK_ACCESS_FS_MAKE_REG,
+ },
+ {},
+ };
+
+ /* Frees names in the two directories for the new links. */
+ ASSERT_EQ(0, unlink(file1_s1d1));
+ ASSERT_EQ(0, unlink(file1_s2d1));
+
+ enforce_fs(_metadata,
+ LANDLOCK_ACCESS_FS_WRITE_FILE | LANDLOCK_ACCESS_FS_MAKE_REG,
+ rules);
+
+ /*
+ * WRITE_FILE is enough to obtain the anonymous tmpfile. linkat(2) with
+ * AT_EMPTY_PATH needs no capability because the fd's open-time
+ * credentials match the caller's. Linking into the same directory does
+ * not require REFER (no reparenting), only MAKE_REG, which is absent
+ * here.
+ */
+ fd = open(dir_s1d1, O_TMPFILE | O_WRONLY | O_CLOEXEC, 0700);
+ ASSERT_LE(0, fd);
+ ASSERT_EQ(-1, linkat(fd, "", AT_FDCWD, file1_s1d1, AT_EMPTY_PATH));
+ EXPECT_EQ(EACCES, errno);
+ EXPECT_EQ(0, close(fd));
+
+ /* With MAKE_REG on the directory, the same link is allowed. */
+ fd = open(dir_s2d1, O_TMPFILE | O_WRONLY | O_CLOEXEC, 0700);
+ ASSERT_LE(0, fd);
+ EXPECT_EQ(0, linkat(fd, "", AT_FDCWD, file1_s2d1, AT_EMPTY_PATH));
+ EXPECT_EQ(0, close(fd));
+}
+
+/*
+ * Linking a tmpfile into a different directory is a reparenting operation: like
+ * any cross-directory link it requires LANDLOCK_ACCESS_FS_REFER. Without it,
+ * materializing the tmpfile outside its creation directory is denied with
+ * EXDEV, so a tmpfile cannot escape its origin hierarchy.
+ */
+TEST_F_FORK(layout1, link_tmpfile_reparent_without_refer)
+{
+ int fd;
+ const struct rule rules[] = {
+ /* Source directory: only the tmpfile open is allowed. */
+ {
+ .path = dir_s1d1,
+ .access = LANDLOCK_ACCESS_FS_WRITE_FILE,
+ },
+ /* Destination directory: file creation is allowed. */
+ {
+ .path = dir_s2d1,
+ .access = LANDLOCK_ACCESS_FS_MAKE_REG,
+ },
+ {},
+ };
+
+ /* Frees a name in the destination directory for the new link. */
+ ASSERT_EQ(0, unlink(file1_s2d1));
+
+ enforce_fs(_metadata,
+ LANDLOCK_ACCESS_FS_WRITE_FILE | LANDLOCK_ACCESS_FS_MAKE_REG,
+ rules);
+
+ fd = open(dir_s1d1, O_TMPFILE | O_WRONLY | O_CLOEXEC, 0700);
+ ASSERT_LE(0, fd);
+ /* Cross-directory link without REFER is denied with EXDEV. */
+ ASSERT_EQ(-1, linkat(fd, "", AT_FDCWD, file1_s2d1, AT_EMPTY_PATH));
+ EXPECT_EQ(EXDEV, errno);
+ EXPECT_EQ(0, close(fd));
+}
+
+/*
+ * With LANDLOCK_ACCESS_FS_REFER on both directories, a tmpfile created in one
+ * directory can be linked into another. The destination needs only MAKE_REG
+ * (plus REFER), not WRITE_FILE: the reparenting check compares file access
+ * rights, and the tmpfile gains none by moving to a directory that grants only
+ * the directory-level creation right.
+ */
+TEST_F_FORK(layout1, link_tmpfile_reparent_with_refer)
+{
+ int fd;
+ const struct rule rules[] = {
+ /* Source: tmpfile open (write) and reparenting. */
+ {
+ .path = dir_s1d1,
+ .access = LANDLOCK_ACCESS_FS_WRITE_FILE |
+ LANDLOCK_ACCESS_FS_REFER,
+ },
+ /* Destination: file creation and reparenting, but no write. */
+ {
+ .path = dir_s2d1,
+ .access = LANDLOCK_ACCESS_FS_MAKE_REG |
+ LANDLOCK_ACCESS_FS_REFER,
+ },
+ {},
+ };
+
+ /* Frees a name in the destination directory for the new link. */
+ ASSERT_EQ(0, unlink(file1_s2d1));
+
+ enforce_fs(_metadata,
+ LANDLOCK_ACCESS_FS_WRITE_FILE | LANDLOCK_ACCESS_FS_MAKE_REG |
+ LANDLOCK_ACCESS_FS_REFER,
+ rules);
+
+ fd = open(dir_s1d1, O_TMPFILE | O_WRONLY | O_CLOEXEC, 0700);
+ ASSERT_LE(0, fd);
+ /* REFER on both sides plus MAKE_REG on the destination allows it. */
+ EXPECT_EQ(0, linkat(fd, "", AT_FDCWD, file1_s2d1, AT_EMPTY_PATH));
+ EXPECT_EQ(0, close(fd));
+}
+
static int test_rename(const char *const oldpath, const char *const newpath)
{
if (rename(oldpath, newpath))
--
2.54.0
^ permalink raw reply related
* Re: [PATCH 1/6] ipc: Move mqueue fs magic to uapi magic header
From: Oxana Kharitonova @ 2026-07-22 15:14 UTC (permalink / raw)
To: gnoack
Cc: landlock, linux-kernel, linux-security-module, mic, oxana, paul,
serge, wangyan01, webprosto
In-Reply-To: <amC69LnwcgG7xo-5@google.com>
On Wed, Jul 22, 2026 at 1:43 PM Günther Noack <gnoack@google.com> wrote:
>
> Should this live in include/linux/ipc_namespace.h instead? Or even be
> exposed as a helper function that lets you check whether an inode is
> referring to an mqueue?
>
> —Günther
Hi Günther,
Thanks for the feedback. Agree, better to move it. I'll check how to do it
better and include in v2.
^ permalink raw reply
* Re: [PATCH v3 0/3] keys: fix keyring assoc-array out-of-bounds read and index inconsistency
From: Jarkko Sakkinen @ 2026-07-22 15:30 UTC (permalink / raw)
To: Michael Bommarito
Cc: David Howells, Andrew Morton, Paul Moore, James Morris,
Serge E . Hallyn, keyrings, linux-security-module, linux-kernel
In-Reply-To: <20260719161505.2423935-1-michael.bommarito@gmail.com>
On Sun, Jul 19, 2026 at 12:15:02PM -0400, Michael Bommarito wrote:
> An unprivileged keyring whose keys collide through the description-chunk
> path can drive assoc_array node splitting into an out-of-bounds slot write.
> Patch 1 stops the out-of-bounds read in keyring_get_key_chunk(); patch 2
> makes the chunk byte order agree with keyring_diff_objects(); patch 3 fixes
> the shortcut-walk trim so the walk cannot be steered down the wrong
> descendant.
>
> v3 changes (patch 1 only; patches 2 and 3 are unchanged):
> Per Jarkko's review, patch 1 no longer extends the existing
> keyring_get_key_chunk() declaration line; the new offset is declared on its
> own line as unsigned int. No functional change.
>
> Patches 2 and 3 are unchanged from v2 and carry Jarkko's Reviewed-by.
>
> v2: https://lore.kernel.org/keyrings/20260714115451.3773164-1-michael.bommarito@gmail.com/
> v1: https://lore.kernel.org/keyrings/20260712014500.480410-1-michael.bommarito@gmail.com/
>
> Michael Bommarito (3):
> keys: fix out-of-bounds read in keyring_get_key_chunk()
> keys: make keyring key-chunk byte order agree with
> keyring_diff_objects()
> assoc_array: trim the final shortcut word using the current chunk end
>
> lib/assoc_array.c | 3 ++-
> security/keys/keyring.c | 14 ++++++++------
> 2 files changed, 10 insertions(+), 7 deletions(-)
>
>
> base-commit: 2c7c88a412aa6d09cd04b414211b4ef8553b5309
> --
> 2.53.0
>
I'm setting up the testing environment now and hopefully have final
feedback within let's say "hours" (i.e. I'll do the job, and it takes what
it takes).
BR, Jarkko
^ permalink raw reply
* Re: unix_stream_connect and socket address resolution
From: John Ericson @ 2026-07-22 16:02 UTC (permalink / raw)
To: Günther Noack
Cc: David Laight, Kuniyuki Iwashima, David S . Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Cong Wang, Simon Horman,
Christian Brauner, David Rheinsberg, Andy Lutomirski,
Sergei Zimmerman, network dev, Mickaël Salaün,
Günther Noack, Paul Moore, linux-security-module, LKML
In-Reply-To: <20260722.bfca37efd700@gnoack.org>
Thanks Günther!
This example makes sense to me. The behavior still feels a little odd to
me, but I can understand the practical benefit of what you describe, and
also why changing it would definitely cause breakage.
I have one addendum to ask then which is: what if before the loop we
pre-resolve the parent directory as a concrete `struct path`, and then
on each iteration of the loop resolve only the final path component to
the socket itself? (That is a single-component lookup relative to the
pre-resolved parent, to be clear.)
Per your example, a legitimate server restart recreates the socket inode
in the same directory, so this still picks up the new socket and
reconnects, while avoiding the effect where a concurrent
ancestor-directory rename causes a wildly different socket to be
resolved. Hopefully this preserves the intended use-case.
Note that this does mean recreating the parent directory itself at the
same path would no longer be followed, and a rename/unlink of the pinned
parent would cause the lookup to fail rather than resolve elsewhere.
That seems like the intended, safer direction to me, but flagging it as
a deliberate semantic change rather than an accident.
If this sounds like an acceptable middle-ground to everyone, I'd be happy to implement it.
Cheers,
John
^ permalink raw reply
* Re: [PATCH] apparmor: update website link
From: Ryan Lee @ 2026-07-22 16:35 UTC (permalink / raw)
To: Baruch Siach
Cc: John Johansen, John Johansen, Georgia Garcia, apparmor,
linux-security-module
In-Reply-To: <2ce1d4ec18b2b47ee7ddd9c1ad7be68e185aa69c.1784632742.git.baruch@tkos.co.il>
On Tue, Jul 21, 2026 at 4:40 AM Baruch Siach <baruch@tkos.co.il> wrote:
>
> apparmor.wiki.kernel.org redirects to the current website.
>
> Signed-off-by: Baruch Siach <baruch@tkos.co.il>
> ---
> security/apparmor/Kconfig | 2 +-
> 1 file changed, 1 insertion(+), 1 deletion(-)
>
> diff --git a/security/apparmor/Kconfig b/security/apparmor/Kconfig
> index 1e3bd44643da..472aa38254be 100644
> --- a/security/apparmor/Kconfig
> +++ b/security/apparmor/Kconfig
> @@ -11,7 +11,7 @@ config SECURITY_APPARMOR
> This enables the AppArmor security module.
> Required userspace tools (if they are not included in your
> distribution) and further information may be found at
> - http://apparmor.wiki.kernel.org
> + https://wiki.apparmor.net
>
> If you are unsure how to answer this question, answer N.
>
> --
> 2.53.0
>
>
Reviewed-by: Ryan Lee <ryan.lee@canonical.com>
^ permalink raw reply
* [PATCH v3 00/20] Landlock tracepoints
From: Mickaël Salaün @ 2026-07-22 17:11 UTC (permalink / raw)
To: Günther Noack, Steven Rostedt
Cc: Mickaël Salaün, Christian Brauner, Jann Horn, Jeff Xu,
Justin Suess, Kees Cook, Masami Hiramatsu, Mathieu Desnoyers,
Matthieu Buffet, Mikhail Ivanov, Tingmao Wang, kernel-team,
linux-security-module, linux-trace-kernel
Hi,
This series adds 14 tracepoints that cover the full Landlock lifecycle,
from ruleset creation to domain destruction. They can be used directly
via /sys/kernel/tracing/events/landlock/* or attached by eBPF programs
for richer introspection.
Christian, please take a look at patch 9, which adds
DEFINE_FREE(__putname) to include/linux/fs.h .
Steven, Masami, Mathieu, please take a look at patch 7, which adds
__print_untrusted_str() for trace events, and patches 8-14 which add
the new tracepoints.
This series now looks good to be merged in my "next" tree, provided VFS
and tracing maintainers are ok with it.
Patches 1-6 refactor Landlock internals: they split struct
landlock_domain from struct landlock_ruleset, move denial logging into a
common framework shared by audit and tracing, decouple the per-denial
logging decision from audit, and consolidate the access-right and scope
names into a shared header. Patch 7 adds __print_untrusted_str() to the
tracing core. Patches 8-12 add lifecycle tracepoints: ruleset creation
and destruction, rule addition for filesystem and network, domain
creation, enforcement, and destruction, and per-rule access checks.
Patches 13-14 add denial tracepoints for filesystem, network, ptrace,
and scope operations. Patches 15-19 add selftests and patch 20 adds
documentation.
Each rule type has a dedicated tracepoint with strongly-typed fields
(dev/ino for filesystem, port for network), following the same approach
as the audit logs.
The scope and ptrace denial events also record the other party's
Landlock domain ID, so a consumer that tracked domain creation can
reproduce the two-domain verdict behind a relational denial.
This feature is useful to troubleshoot policy issues and should limit
the need for custom debugging kernel code when developing new Landlock
features.
Landlock already has audit support for logging denied access requests,
which is useful to identify security issues or sandbox misconfiguration.
However, audit might not be enough to debug Landlock policies. The main
difference with audit events is that traces are disabled by default, can
be very verbose, and can be filtered according to process and Landlock
properties (e.g. domain ID).
As for audit, tracing may expose sensitive information about all
sandboxed processes on the system, and must only be accessible to the
system administrator. For unprivileged monitoring scoped to a single
sandbox (e.g., interactive permission prompts), Tingmao Wang's "Landlock
supervise" RFC [1] proposes a dedicated userspace API. The
infrastructure changes in this series (the domain type split, the denial
framework, and the tracepoint consistency guarantees) benefit that
approach.
I will release a companion tool that leverages these tracepoints to
monitor Landlock events in real time.
This series applies on top of v7.2-rc4.
Changes since v2:
https://patch.msgid.link/20260406143717.1815792-1-mic@digikod.net
- Renamed the landlock_restrict_self tracepoint to
landlock_create_domain and reordered its arguments to
(domain, ruleset) for domain-first consistency with the other
domain-lifecycle events.
- Ordered the add_rule_net and check_rule tracepoint arguments with the
common part first: access_rights before port (add_rule_net), and
domain, rule before access_request and the object (check_rule).
- Recorded the other party's Landlock domain as a scalar ID on scope
and ptrace denials: target_domain=, peer_domain=, tracee_domain=
(0 when unsandboxed).
- Replaced the deny events' log_same_exec and log_new_exec fields with a
single kernel-computed logged field; logged and the quiet suppression
are gated on CONFIG_SECURITY_LANDLOCK_LOG, so the value is identical
in a tracepoints-only build (CONFIG_AUDIT=n).
- Fixed a NULL dereference in landlock_deny_access_fs on a denied
ftruncate() or ioctl() by selecting the path from the request's
audit-data type.
- Moved landlock_restrict_self's creation event under the ruleset lock,
before the thread-sync; a thread-sync abort still emits the matching
free_domain, keeping the create/free pair balanced.
- Split the denial-logging framework three ways (common log.c, audit.c,
and a tracepoint-only trace.c), each behind its own CONFIG, so the
tracepoints work without CONFIG_AUDIT and every commit stays
bisectable.
- Rendered trace access masks as symbolic names (check_rule grants=,
denial blockers=) instead of raw hex, and renamed the check_rule
request= and denial comm= labels to access_request= and
tracee_comm=/target_comm=.
- Reworked the trace documentation: consolidated the shared eBPF and
trace-event guarantees into a DOC block, de-duplicated the admin
guide and the trace event reference, and documented the logged field
and the grants= format.
- Extended the trace selftests: asserted the relational domain-ID
fields across the signal, abstract-unix-socket, and ptrace hooks,
added a check_rule_net field test, and added landlock_enforce_domain
coverage of the single-threaded, TSYNC, multi-threaded, non-leader,
flags-only, and thread-sync-abort cases.
- New patch: added the landlock_enforce_domain tracepoint, the
per-thread enforcement outcome, emitted once per thread the domain is
applied to after its commit_creds(). It carries complete (the single
event concluding the operation) and process_wide (the enforcement
covers every eligible thread, via LANDLOCK_RESTRICT_SELF_TSYNC or a
single-threaded process); complete && process_wide is the
whole-process-enforced guarantee.
- New patch 5 factors the per-denial logging decision into a shared,
config-independent logged verdict used by audit and tracing.
- New patch 6 consolidates the access-right and scope names in a shared
header (enabling the symbolic rendering above).
- Dropped v2 patch 10 ("landlock: Set audit_net.sk for socket access
checks") and its follow-up AF_UNSPEC UDP-send fix, both merged
upstream; subsequent patches renumbered.
Changes since RFC v1:
https://patch.msgid.link/20250523165741.693976-1-mic@digikod.net
- New patches 1-4: split struct landlock_domain from struct
landlock_ruleset; split denial logging from audit into common
framework with CONFIG_SECURITY_LANDLOCK_LOG.
- Patch 5 (was v1 3/5): removed WARN_ON() (pointed out by Steven
Rostedt).
- New patch 6: added create_ruleset and free_ruleset tracepoints
(split from the v1 add_rule_fs tracepoint patch).
- Patch 7 (was v1 4/5): added add_rule_net tracepoint, used
ruleset Landlock ID instead of kernel pointer, added version
field to struct landlock_ruleset, differentiated d_absolute_path()
error cases (suggested by Tingmao Wang), moved
DEFINE_FREE(__putname) to include/linux/fs.h (noticed by Tingmao
Wang).
- New patch 8: added restrict_self and free_domain tracepoints.
- Patch 9 (was v1 5/5): merged find-rule consolidation, added
check_rule_net tracepoint.
- New patch 10: split audit_net.sk fix with Fixes: tag.
- New patches 11-12: added denial tracepoints for filesystem,
network, ptrace, and scope operations.
- New patches 13-17: split selftests into per-feature commits with
documentation.
Closes: https://github.com/landlock-lsm/linux/issues/47
[1] https://lore.kernel.org/r/cover.1741047969.git.m@maowtm.org
Regards,
Mickaël Salaün (20):
landlock: Prepare ruleset and domain type split
landlock: Move domain query functions to domain.c
landlock: Split struct landlock_domain from struct landlock_ruleset
landlock: Split denial logging from audit into common framework
landlock: Decouple the per-denial logging decision from CONFIG_AUDIT
landlock: Consolidate access-right and scope names in a shared header
tracing: Add __print_untrusted_str()
landlock: Add create_ruleset and free_ruleset tracepoints
landlock: Add landlock_add_rule_fs and landlock_add_rule_net
tracepoints
landlock: Add create_domain and free_domain tracepoints
landlock: Add landlock_enforce_domain tracepoint
landlock: Add tracepoints for rule checking
landlock: Add landlock_deny_access_fs and landlock_deny_access_net
landlock: Add tracepoints for ptrace and scope denials
selftests/landlock: Add trace event test infrastructure and tests
selftests/landlock: Add filesystem tracepoint tests
selftests/landlock: Add network tracepoint tests
selftests/landlock: Add scope and ptrace tracepoint tests
selftests/landlock: Add landlock_enforce_domain trace tests
landlock: Document tracepoints
Documentation/admin-guide/LSM/landlock.rst | 105 +-
Documentation/security/landlock.rst | 38 +-
Documentation/trace/events-landlock.rst | 313 ++++
Documentation/trace/index.rst | 1 +
Documentation/userspace-api/landlock.rst | 14 +-
MAINTAINERS | 3 +
include/linux/fs.h | 1 +
include/linux/landlock.h | 56 +
include/linux/trace_events.h | 2 +
include/trace/events/landlock.h | 908 +++++++++
include/trace/stages/stage3_trace_output.h | 4 +
include/trace/stages/stage7_class_define.h | 1 +
kernel/trace/trace_output.c | 44 +
security/landlock/Kconfig | 5 +
security/landlock/Makefile | 12 +-
security/landlock/access.h | 6 +-
security/landlock/audit.c | 641 +------
security/landlock/audit.h | 57 +-
security/landlock/cred.c | 14 +-
security/landlock/cred.h | 29 +-
security/landlock/domain.c | 475 ++++-
security/landlock/domain.h | 162 +-
security/landlock/fs.c | 218 ++-
security/landlock/fs.h | 38 +-
security/landlock/id.h | 6 +-
security/landlock/log.c | 586 ++++++
security/landlock/log.h | 86 +
security/landlock/net.c | 38 +-
security/landlock/ruleset.c | 546 +-----
security/landlock/ruleset.h | 250 +--
security/landlock/syscalls.c | 89 +-
security/landlock/task.c | 76 +-
security/landlock/trace.c | 185 ++
security/landlock/trace.h | 44 +
security/landlock/tsync.c | 15 +
tools/testing/selftests/landlock/audit.h | 35 -
tools/testing/selftests/landlock/common.h | 47 +
tools/testing/selftests/landlock/config | 2 +
tools/testing/selftests/landlock/fs_test.c | 483 +++++
tools/testing/selftests/landlock/net_test.c | 747 +++++++-
.../testing/selftests/landlock/ptrace_test.c | 402 ++++
.../landlock/scoped_abstract_unix_test.c | 463 +++++
.../selftests/landlock/scoped_signal_test.c | 404 ++++
tools/testing/selftests/landlock/trace.h | 646 +++++++
.../selftests/landlock/trace_fs_test.c | 496 +++++
tools/testing/selftests/landlock/trace_test.c | 1623 +++++++++++++++++
tools/testing/selftests/landlock/true.c | 10 +
47 files changed, 8981 insertions(+), 1445 deletions(-)
create mode 100644 Documentation/trace/events-landlock.rst
create mode 100644 include/linux/landlock.h
create mode 100644 include/trace/events/landlock.h
create mode 100644 security/landlock/log.c
create mode 100644 security/landlock/log.h
create mode 100644 security/landlock/trace.c
create mode 100644 security/landlock/trace.h
create mode 100644 tools/testing/selftests/landlock/trace.h
create mode 100644 tools/testing/selftests/landlock/trace_fs_test.c
create mode 100644 tools/testing/selftests/landlock/trace_test.c
--
2.54.0
^ permalink raw reply
* [PATCH v3 03/20] landlock: Split struct landlock_domain from struct landlock_ruleset
From: Mickaël Salaün @ 2026-07-22 17:11 UTC (permalink / raw)
To: Günther Noack, Steven Rostedt
Cc: Mickaël Salaün, Christian Brauner, Jann Horn, Jeff Xu,
Justin Suess, Kees Cook, Masami Hiramatsu, Mathieu Desnoyers,
Matthieu Buffet, Mikhail Ivanov, Tingmao Wang, kernel-team,
linux-security-module, linux-trace-kernel
In-Reply-To: <20260722171159.2776765-1-mic@digikod.net>
Switch all domain users to the new struct landlock_domain type
introduced by a previous commit, eliminating the conflation between
mutable rulesets and immutable domains. landlock_merge_ruleset() now
returns and allocates a struct landlock_domain, and the merge and
inherit helpers move next to it; the former static insert_rule() is
exported as landlock_rule_insert() for its new caller across the
translation-unit boundary.
The merge destination is now a private struct landlock_domain still
under construction (owned by the calling thread, not yet shared), so the
merge and inherit helpers lock only the source ruleset: the previous
lock of both destination and source collapses to a single
mutex_lock(&src->lock).
Rename the per-layer access-mask field from access_masks to
handled_masks, naming it by the role it plays (the rights each layer
handles) rather than by its type, paralleling the struct access_masks
quiet_masks field. Drop the now domain-only fields (hierarchy,
work_free, num_layers) from struct landlock_ruleset.
The new struct landlock_domain field in cred.h pulls in domain.h, which
includes audit.h, which previously included cred.h, forming an include
cycle. Break it by having audit.h forward-declare the struct
landlock_cred_security and struct landlock_hierarchy it uses instead of
including cred.h.
Cc: Günther Noack <gnoack@google.com>
Cc: Tingmao Wang <m@maowtm.org>
Signed-off-by: Mickaël Salaün <mic@digikod.net>
---
Changes since v2:
https://patch.msgid.link/20260406143717.1815792-4-mic@digikod.net
- Rename the per-layer FAM (struct landlock_domain) and the
unmerged-ruleset field (struct landlock_ruleset) from access_masks to
handled_masks, naming them by role alongside the per-layer quiet_masks
field.
- Rename struct layer_access_masks to the base's struct layer_masks.
Changes since v1:
- New patch.
---
security/landlock/access.h | 2 +-
security/landlock/audit.c | 12 +-
security/landlock/audit.h | 4 +-
security/landlock/cred.c | 6 +-
security/landlock/cred.h | 21 ++-
security/landlock/domain.c | 258 +++++++++++++++++++++++++-
security/landlock/domain.h | 43 ++++-
security/landlock/fs.c | 25 ++-
security/landlock/net.c | 3 +-
security/landlock/ruleset.c | 338 +++++------------------------------
security/landlock/ruleset.h | 141 +++------------
security/landlock/syscalls.c | 10 +-
security/landlock/task.c | 20 +--
13 files changed, 403 insertions(+), 480 deletions(-)
diff --git a/security/landlock/access.h b/security/landlock/access.h
index d926078bf0a5..1a8227a709d9 100644
--- a/security/landlock/access.h
+++ b/security/landlock/access.h
@@ -19,7 +19,7 @@
/*
* All access rights that are denied by default whether they are handled or not
- * by a ruleset/layer. This must be ORed with all ruleset->access_masks[]
+ * by a ruleset/layer. This must be ORed with all domain->handled_masks[]
* entries when we need to get the absolute handled access masks, see
* landlock_upgrade_handled_access_masks().
*/
diff --git a/security/landlock/audit.c b/security/landlock/audit.c
index 50536c568526..7cb00619a741 100644
--- a/security/landlock/audit.c
+++ b/security/landlock/audit.c
@@ -138,7 +138,7 @@ static void log_domain(struct landlock_hierarchy *const hierarchy)
}
static struct landlock_hierarchy *
-get_hierarchy(const struct landlock_ruleset *const domain, const size_t layer)
+get_hierarchy(const struct landlock_domain *const domain, const size_t layer)
{
struct landlock_hierarchy *hierarchy = domain->hierarchy;
ssize_t i;
@@ -171,7 +171,7 @@ static void test_get_hierarchy(struct kunit *const test)
.parent = &dom1_hierarchy,
.id = 30,
};
- struct landlock_ruleset dom2 = {
+ struct landlock_domain dom2 = {
.hierarchy = &dom2_hierarchy,
.num_layers = 3,
};
@@ -185,7 +185,7 @@ static void test_get_hierarchy(struct kunit *const test)
#endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */
/* Get the youngest layer that denied the access_request. */
-static size_t get_denied_layer(const struct landlock_ruleset *const domain,
+static size_t get_denied_layer(const struct landlock_domain *const domain,
access_mask_t *const access_request,
const struct layer_masks *masks)
{
@@ -205,7 +205,7 @@ static size_t get_denied_layer(const struct landlock_ruleset *const domain,
static void test_get_denied_layer(struct kunit *const test)
{
- const struct landlock_ruleset dom = {
+ const struct landlock_domain dom = {
.num_layers = 5,
};
const struct layer_masks masks = {
@@ -682,8 +682,8 @@ void landlock_log_denial(const struct landlock_cred_security *const subject,
* Only domains which previously appeared in the audit logs are logged again.
* This is useful to know when a domain will never show again in the audit log.
*
- * Called in a work queue scheduled by landlock_put_ruleset_deferred() called
- * by hook_cred_free().
+ * Called in a work queue scheduled by landlock_put_domain_deferred() called by
+ * hook_cred_free().
*/
void landlock_log_drop_domain(const struct landlock_hierarchy *const hierarchy)
{
diff --git a/security/landlock/audit.h b/security/landlock/audit.h
index 620f8a24291d..4617a855712c 100644
--- a/security/landlock/audit.h
+++ b/security/landlock/audit.h
@@ -12,7 +12,9 @@
#include <linux/lsm_audit.h>
#include "access.h"
-#include "cred.h"
+
+struct landlock_cred_security;
+struct landlock_hierarchy;
enum landlock_request_type {
LANDLOCK_REQUEST_PTRACE = 1,
diff --git a/security/landlock/cred.c b/security/landlock/cred.c
index cc419de75cd6..58b544993db4 100644
--- a/security/landlock/cred.c
+++ b/security/landlock/cred.c
@@ -22,7 +22,7 @@ static void hook_cred_transfer(struct cred *const new,
const struct landlock_cred_security *const old_llcred =
landlock_cred(old);
- landlock_get_ruleset(old_llcred->domain);
+ landlock_get_domain(old_llcred->domain);
*landlock_cred(new) = *old_llcred;
}
@@ -35,10 +35,10 @@ static int hook_cred_prepare(struct cred *const new,
static void hook_cred_free(struct cred *const cred)
{
- struct landlock_ruleset *const dom = landlock_cred(cred)->domain;
+ struct landlock_domain *const dom = landlock_cred(cred)->domain;
if (dom)
- landlock_put_ruleset_deferred(dom);
+ landlock_put_domain_deferred(dom);
}
#ifdef CONFIG_AUDIT
diff --git a/security/landlock/cred.h b/security/landlock/cred.h
index f287c56b5fd4..e7830cfed041 100644
--- a/security/landlock/cred.h
+++ b/security/landlock/cred.h
@@ -16,6 +16,7 @@
#include <linux/rcupdate.h>
#include "access.h"
+#include "domain.h"
#include "limits.h"
#include "ruleset.h"
#include "setup.h"
@@ -31,9 +32,9 @@
*/
struct landlock_cred_security {
/**
- * @domain: Immutable ruleset enforced on a task.
+ * @domain: Immutable domain enforced on a task.
*/
- struct landlock_ruleset *domain;
+ struct landlock_domain *domain;
#ifdef CONFIG_AUDIT
/**
@@ -70,22 +71,20 @@ landlock_cred(const struct cred *cred)
static inline void landlock_cred_copy(struct landlock_cred_security *dst,
const struct landlock_cred_security *src)
{
- landlock_put_ruleset(dst->domain);
+ landlock_put_domain(dst->domain);
*dst = *src;
- landlock_get_ruleset(src->domain);
+ landlock_get_domain(src->domain);
}
-static inline struct landlock_ruleset *landlock_get_current_domain(void)
+static inline struct landlock_domain *landlock_get_current_domain(void)
{
return landlock_cred(current_cred())->domain;
}
-/*
- * The call needs to come from an RCU read-side critical section.
- */
-static inline const struct landlock_ruleset *
+/* The call needs to come from an RCU read-side critical section. */
+static inline const struct landlock_domain *
landlock_get_task_domain(const struct task_struct *const task)
{
return landlock_cred(__task_cred(task))->domain;
@@ -126,7 +125,7 @@ landlock_get_applicable_subject(const struct cred *const cred,
const union access_masks_all masks_all = {
.masks = masks,
};
- const struct landlock_ruleset *domain;
+ const struct landlock_domain *domain;
ssize_t layer_level;
if (!cred)
@@ -139,7 +138,7 @@ landlock_get_applicable_subject(const struct cred *const cred,
for (layer_level = domain->num_layers - 1; layer_level >= 0;
layer_level--) {
union access_masks_all layer = {
- .masks = domain->access_masks[layer_level],
+ .masks = domain->handled_masks[layer_level],
};
if (layer.all & masks_all.all) {
diff --git a/security/landlock/domain.c b/security/landlock/domain.c
index 8fb74107c2ef..2e0d75175c2a 100644
--- a/security/landlock/domain.c
+++ b/security/landlock/domain.c
@@ -14,7 +14,9 @@
#include <linux/cred.h>
#include <linux/err.h>
#include <linux/file.h>
+#include <linux/lockdep.h>
#include <linux/mm.h>
+#include <linux/mutex.h>
#include <linux/overflow.h>
#include <linux/path.h>
#include <linux/pid.h>
@@ -33,6 +35,36 @@
#include "limits.h"
#include "ruleset.h"
+static void build_check_domain(void)
+{
+ const struct landlock_domain domain = {
+ .num_layers = ~0,
+ };
+
+ BUILD_BUG_ON(domain.num_layers < LANDLOCK_MAX_NUM_LAYERS);
+}
+
+static struct landlock_domain *create_domain(const u32 num_layers)
+{
+ struct landlock_domain *new_domain;
+
+ build_check_domain();
+ new_domain = kzalloc_flex(*new_domain, handled_masks, num_layers,
+ GFP_KERNEL_ACCOUNT);
+ if (!new_domain)
+ return ERR_PTR(-ENOMEM);
+
+ refcount_set(&new_domain->usage, 1);
+ new_domain->rules.root_inode = RB_ROOT;
+
+#if IS_ENABLED(CONFIG_INET)
+ new_domain->rules.root_net_port = RB_ROOT;
+#endif /* IS_ENABLED(CONFIG_INET) */
+
+ new_domain->num_layers = num_layers;
+ return new_domain;
+}
+
static void free_domain(struct landlock_domain *const domain)
{
might_sleep();
@@ -66,13 +98,13 @@ void landlock_put_domain_deferred(struct landlock_domain *const domain)
/* The returned access has the same lifetime as the domain. */
const struct landlock_rule *
-landlock_find_rule(const struct landlock_ruleset *const ruleset,
+landlock_find_rule(const struct landlock_domain *const domain,
const struct landlock_id id)
{
const struct rb_root *root;
const struct rb_node *node;
- root = landlock_get_rule_root((struct landlock_rules *)&ruleset->rules,
+ root = landlock_get_rule_root((struct landlock_rules *)&domain->rules,
id.type);
if (IS_ERR(root))
return NULL;
@@ -154,7 +186,7 @@ bool landlock_unmask_layers(const struct landlock_rule *const rule,
}
typedef access_mask_t
-get_access_mask_t(const struct landlock_ruleset *const ruleset,
+get_access_mask_t(const struct landlock_domain *const domain,
const u16 layer_level);
/**
@@ -173,7 +205,7 @@ get_access_mask_t(const struct landlock_ruleset *const ruleset,
* any of the active layers in @domain.
*/
access_mask_t
-landlock_init_layer_masks(const struct landlock_ruleset *const domain,
+landlock_init_layer_masks(const struct landlock_domain *const domain,
const access_mask_t access_request,
struct layer_masks *const masks,
const enum landlock_key_type key_type)
@@ -221,6 +253,224 @@ landlock_init_layer_masks(const struct landlock_ruleset *const domain,
return handled_accesses;
}
+static int merge_tree(struct landlock_domain *const dst,
+ struct landlock_ruleset *const src,
+ const enum landlock_key_type key_type)
+{
+ struct landlock_rule *walker_rule, *next_rule;
+ struct rb_root *src_root;
+ int err = 0;
+
+ might_sleep();
+ lockdep_assert_held(&src->lock);
+
+ src_root = landlock_get_rule_root(&src->rules, key_type);
+ if (IS_ERR(src_root))
+ return PTR_ERR(src_root);
+
+ /* Merges the @src tree. */
+ rbtree_postorder_for_each_entry_safe(walker_rule, next_rule, src_root,
+ node) {
+ struct landlock_layer layers[] = { {
+ .level = dst->num_layers,
+ } };
+ const struct landlock_id id = {
+ .key = walker_rule->key,
+ .type = key_type,
+ };
+
+ if (WARN_ON_ONCE(walker_rule->num_layers != 1))
+ return -EINVAL;
+
+ if (WARN_ON_ONCE(walker_rule->layers[0].level != 0))
+ return -EINVAL;
+
+ layers[0].access = walker_rule->layers[0].access;
+ layers[0].flags = walker_rule->layers[0].flags;
+
+ err = landlock_rule_insert(&dst->rules, id, &layers,
+ ARRAY_SIZE(layers));
+ if (err)
+ return err;
+ }
+ return err;
+}
+
+static int merge_ruleset(struct landlock_domain *const dst,
+ struct landlock_ruleset *const src)
+{
+ int err = 0;
+
+ might_sleep();
+ /* Should already be checked by landlock_merge_ruleset() */
+ if (WARN_ON_ONCE(!src))
+ return 0;
+ /* Only merge into a domain. */
+ if (WARN_ON_ONCE(!dst || !dst->hierarchy))
+ return -EINVAL;
+
+ mutex_lock(&src->lock);
+
+ /* Stacks the new layer. */
+ if (WARN_ON_ONCE(dst->num_layers < 1)) {
+ err = -EINVAL;
+ goto out_unlock;
+ }
+ dst->handled_masks[dst->num_layers - 1] =
+ landlock_upgrade_handled_access_masks(src->handled_masks);
+
+ /* Merges the @src inode tree. */
+ err = merge_tree(dst, src, LANDLOCK_KEY_INODE);
+ if (err)
+ goto out_unlock;
+
+#if IS_ENABLED(CONFIG_INET)
+ /* Merges the @src network port tree. */
+ err = merge_tree(dst, src, LANDLOCK_KEY_NET_PORT);
+ if (err)
+ goto out_unlock;
+#endif /* IS_ENABLED(CONFIG_INET) */
+
+out_unlock:
+ mutex_unlock(&src->lock);
+ return err;
+}
+
+static int inherit_tree(struct landlock_domain *const parent,
+ struct landlock_domain *const child,
+ const enum landlock_key_type key_type)
+{
+ struct landlock_rule *walker_rule, *next_rule;
+ struct rb_root *parent_root;
+ int err = 0;
+
+ might_sleep();
+
+ parent_root = landlock_get_rule_root(&parent->rules, key_type);
+ if (IS_ERR(parent_root))
+ return PTR_ERR(parent_root);
+
+ /* Copies the @parent inode or network tree. */
+ rbtree_postorder_for_each_entry_safe(walker_rule, next_rule,
+ parent_root, node) {
+ const struct landlock_id id = {
+ .key = walker_rule->key,
+ .type = key_type,
+ };
+
+ err = landlock_rule_insert(&child->rules, id,
+ &walker_rule->layers,
+ walker_rule->num_layers);
+ if (err)
+ return err;
+ }
+ return err;
+}
+
+static int inherit_ruleset(struct landlock_domain *const parent,
+ struct landlock_domain *const child)
+{
+ int err = 0;
+
+ might_sleep();
+ if (!parent)
+ return 0;
+
+ /* Copies the @parent inode tree. */
+ err = inherit_tree(parent, child, LANDLOCK_KEY_INODE);
+ if (err)
+ return err;
+
+#if IS_ENABLED(CONFIG_INET)
+ /* Copies the @parent network port tree. */
+ err = inherit_tree(parent, child, LANDLOCK_KEY_NET_PORT);
+ if (err)
+ return err;
+#endif /* IS_ENABLED(CONFIG_INET) */
+
+ if (WARN_ON_ONCE(child->num_layers <= parent->num_layers))
+ return -EINVAL;
+
+ /*
+ * Copies the parent layer stack and leaves a space for the new layer.
+ */
+ memcpy(child->handled_masks, parent->handled_masks,
+ flex_array_size(parent, handled_masks, parent->num_layers));
+
+ if (WARN_ON_ONCE(!parent->hierarchy))
+ return -EINVAL;
+
+ landlock_get_hierarchy(parent->hierarchy);
+ child->hierarchy->parent = parent->hierarchy;
+
+ return 0;
+}
+
+/**
+ * landlock_merge_ruleset - Merge a ruleset with a domain
+ *
+ * @parent: Parent domain.
+ * @ruleset: New ruleset to be merged.
+ *
+ * The current task is requesting to be restricted. The subjective credentials
+ * must not be in an overridden state. cf. landlock_init_hierarchy_log().
+ *
+ * Return: A new domain merging @parent and @ruleset on success, or ERR_PTR() on
+ * failure. If @parent is NULL, the new domain duplicates @ruleset.
+ */
+struct landlock_domain *
+landlock_merge_ruleset(struct landlock_domain *const parent,
+ struct landlock_ruleset *const ruleset)
+{
+ struct landlock_domain *new_dom __free(landlock_put_domain) = NULL;
+ u32 num_layers;
+ int err;
+
+ might_sleep();
+ if (WARN_ON_ONCE(!ruleset))
+ return ERR_PTR(-EINVAL);
+
+ if (parent) {
+ if (parent->num_layers >= LANDLOCK_MAX_NUM_LAYERS)
+ return ERR_PTR(-E2BIG);
+ num_layers = parent->num_layers + 1;
+ } else {
+ num_layers = 1;
+ }
+
+ /* Creates a new domain... */
+ new_dom = create_domain(num_layers);
+ if (IS_ERR(new_dom))
+ return new_dom;
+
+ new_dom->hierarchy =
+ kzalloc_obj(*new_dom->hierarchy, GFP_KERNEL_ACCOUNT);
+ if (!new_dom->hierarchy)
+ return ERR_PTR(-ENOMEM);
+
+ refcount_set(&new_dom->hierarchy->usage, 1);
+
+ /* ...as a child of @parent... */
+ err = inherit_ruleset(parent, new_dom);
+ if (err)
+ return ERR_PTR(err);
+
+ /* ...and including @ruleset. */
+ err = merge_ruleset(new_dom, ruleset);
+ if (err)
+ return ERR_PTR(err);
+
+ err = landlock_init_hierarchy_log(new_dom->hierarchy);
+ if (err)
+ return ERR_PTR(err);
+
+#ifdef CONFIG_AUDIT
+ new_dom->hierarchy->quiet_masks = ruleset->quiet_masks;
+#endif /* CONFIG_AUDIT */
+
+ return no_free_ptr(new_dom);
+}
+
#ifdef CONFIG_AUDIT
/**
diff --git a/security/landlock/domain.h b/security/landlock/domain.h
index 9f9b892ab17d..f0925297dcba 100644
--- a/security/landlock/domain.h
+++ b/security/landlock/domain.h
@@ -202,7 +202,7 @@ struct landlock_domain {
* @work_free: Enables to free a domain within a lockless
* section. This is only used by landlock_put_domain_deferred()
* when @usage reaches zero. The fields @usage, @num_layers and
- * @access_masks are then unused.
+ * @handled_masks are then unused.
*/
struct work_struct work_free;
struct {
@@ -218,7 +218,7 @@ struct landlock_domain {
*/
u32 num_layers;
/**
- * @access_masks: Contains the subset of filesystem and
+ * @handled_masks: Contains the subset of filesystem and
* network actions that are restricted by a domain. A
* domain saves all layers of merged rulesets in a stack
* (FAM), starting from the first layer to the last one.
@@ -228,28 +228,51 @@ struct landlock_domain {
* overlapping access rights. These layers are set once
* and never changed for the lifetime of the domain.
*/
- struct access_masks access_masks[];
+ struct access_masks handled_masks[];
};
};
};
+static inline access_mask_t
+landlock_get_fs_access_mask(const struct landlock_domain *const domain,
+ const u16 layer_level)
+{
+ /* Handles all initially denied by default access rights. */
+ return domain->handled_masks[layer_level].fs |
+ _LANDLOCK_ACCESS_FS_INITIALLY_DENIED;
+}
+
+static inline access_mask_t
+landlock_get_net_access_mask(const struct landlock_domain *const domain,
+ const u16 layer_level)
+{
+ return domain->handled_masks[layer_level].net;
+}
+
+static inline access_mask_t
+landlock_get_scope_mask(const struct landlock_domain *const domain,
+ const u16 layer_level)
+{
+ return domain->handled_masks[layer_level].scope;
+}
+
/**
* landlock_union_access_masks - Return all access rights handled in the
* domain
*
- * @domain: Landlock ruleset (used as a domain)
+ * @domain: Landlock domain
*
* Return: An access_masks result of the OR of all the domain's access masks.
*/
static inline struct access_masks
-landlock_union_access_masks(const struct landlock_ruleset *const domain)
+landlock_union_access_masks(const struct landlock_domain *const domain)
{
union access_masks_all matches = {};
size_t layer_level;
for (layer_level = 0; layer_level < domain->num_layers; layer_level++) {
union access_masks_all layer = {
- .masks = domain->access_masks[layer_level],
+ .masks = domain->handled_masks[layer_level],
};
matches.all |= layer.all;
@@ -264,15 +287,19 @@ void landlock_put_domain_deferred(struct landlock_domain *const domain);
DEFINE_FREE(landlock_put_domain, struct landlock_domain *,
if (!IS_ERR_OR_NULL(_T)) landlock_put_domain(_T))
+struct landlock_domain *
+landlock_merge_ruleset(struct landlock_domain *const parent,
+ struct landlock_ruleset *const ruleset);
+
const struct landlock_rule *
-landlock_find_rule(const struct landlock_ruleset *const ruleset,
+landlock_find_rule(const struct landlock_domain *const domain,
const struct landlock_id id);
bool landlock_unmask_layers(const struct landlock_rule *const rule,
struct layer_masks *masks);
access_mask_t
-landlock_init_layer_masks(const struct landlock_ruleset *const domain,
+landlock_init_layer_masks(const struct landlock_domain *const domain,
const access_mask_t access_request,
struct layer_masks *masks,
const enum landlock_key_type key_type);
diff --git a/security/landlock/fs.c b/security/landlock/fs.c
index f7e5e4ef9eac..0b77d310ffd4 100644
--- a/security/landlock/fs.c
+++ b/security/landlock/fs.c
@@ -336,12 +336,11 @@ int landlock_append_fs_rule(struct landlock_ruleset *const ruleset,
if (!d_is_dir(path->dentry) &&
!access_mask_subset(access_rights, ACCESS_FILE))
return -EINVAL;
- if (WARN_ON_ONCE(ruleset->num_layers != 1))
- return -EINVAL;
/* Transforms relative access rights to absolute ones. */
access_rights |= LANDLOCK_MASK_ACCESS_FS &
- ~landlock_get_fs_access_mask(ruleset, 0);
+ ~(ruleset->handled_masks.fs |
+ _LANDLOCK_ACCESS_FS_INITIALLY_DENIED);
id.key.object = get_inode_object(d_backing_inode(path->dentry));
if (IS_ERR(id.key.object))
return PTR_ERR(id.key.object);
@@ -364,7 +363,7 @@ int landlock_append_fs_rule(struct landlock_ruleset *const ruleset,
* Returns NULL if no rule is found or if @dentry is negative.
*/
static const struct landlock_rule *
-find_rule(const struct landlock_ruleset *const domain,
+find_rule(const struct landlock_domain *const domain,
const struct dentry *const dentry)
{
const struct landlock_rule *rule;
@@ -749,7 +748,7 @@ static void test_is_eacces_with_write(struct kunit *const test)
* Return: True if the access request is granted, false otherwise.
*/
static bool
-is_access_to_paths_allowed(const struct landlock_ruleset *const domain,
+is_access_to_paths_allowed(const struct landlock_domain *const domain,
const struct path *const path,
const access_mask_t access_request_parent1,
struct layer_masks *layer_masks_parent1,
@@ -1039,7 +1038,7 @@ static access_mask_t maybe_remove(const struct dentry *const dentry)
* Return: True if all the domain access rights are allowed for @dir, false if
* the walk reached @mnt_root.
*/
-static bool collect_domain_accesses(const struct landlock_ruleset *const domain,
+static bool collect_domain_accesses(const struct landlock_domain *const domain,
const struct dentry *const mnt_root,
struct dentry *dir,
struct layer_masks *layer_masks_dom)
@@ -1589,8 +1588,8 @@ static int hook_path_truncate(const struct path *const path)
* @masks: Layer access masks to unmask
* @access: Access bits that control scoping
*/
-static void unmask_scoped_access(const struct landlock_ruleset *const client,
- const struct landlock_ruleset *const server,
+static void unmask_scoped_access(const struct landlock_domain *const client,
+ const struct landlock_domain *const server,
struct layer_masks *const masks,
const access_mask_t access)
{
@@ -1644,7 +1643,7 @@ static void unmask_scoped_access(const struct landlock_ruleset *const client,
static int hook_unix_find(const struct path *const path, struct sock *other,
int flags)
{
- const struct landlock_ruleset *dom_other;
+ const struct landlock_domain *dom_other;
const struct landlock_cred_security *subject;
struct layer_masks layer_masks;
struct landlock_request request = {};
@@ -1939,7 +1938,7 @@ static bool control_current_fowner(struct fown_struct *const fown)
static void hook_file_set_fowner(struct file *file)
{
- struct landlock_ruleset *prev_dom;
+ struct landlock_domain *prev_dom;
struct landlock_cred_security fown_subject = {};
struct pid *prev_tg, *fown_tg = NULL;
size_t fown_layer = 0;
@@ -1952,7 +1951,7 @@ static void hook_file_set_fowner(struct file *file)
landlock_get_applicable_subject(
current_cred(), signal_scope, &fown_layer);
if (new_subject) {
- landlock_get_ruleset(new_subject->domain);
+ landlock_get_domain(new_subject->domain);
fown_subject = *new_subject;
fown_tg = get_pid(task_tgid(current));
}
@@ -1967,14 +1966,14 @@ static void hook_file_set_fowner(struct file *file)
#endif /* CONFIG_AUDIT*/
/* May be called in an RCU read-side critical section. */
- landlock_put_ruleset_deferred(prev_dom);
+ landlock_put_domain_deferred(prev_dom);
put_pid(prev_tg);
}
static void hook_file_free_security(struct file *file)
{
put_pid(landlock_file(file)->fown_tg);
- landlock_put_ruleset_deferred(landlock_file(file)->fown_subject.domain);
+ landlock_put_domain_deferred(landlock_file(file)->fown_subject.domain);
}
static struct security_hook_list landlock_hooks[] __ro_after_init = {
diff --git a/security/landlock/net.c b/security/landlock/net.c
index feecc8c77ab4..7447738ca2e6 100644
--- a/security/landlock/net.c
+++ b/security/landlock/net.c
@@ -33,8 +33,7 @@ int landlock_append_net_rule(struct landlock_ruleset *const ruleset,
BUILD_BUG_ON(sizeof(port) > sizeof(id.key.data));
/* Transforms relative access rights to absolute ones. */
- access_rights |= LANDLOCK_MASK_ACCESS_NET &
- ~landlock_get_net_access_mask(ruleset, 0);
+ access_rights |= LANDLOCK_MASK_ACCESS_NET & ~ruleset->handled_masks.net;
mutex_lock(&ruleset->lock);
err = landlock_insert_rule(ruleset, id, access_rights, flags);
diff --git a/security/landlock/ruleset.c b/security/landlock/ruleset.c
index 96e65804d2ff..13edb77f07a0 100644
--- a/security/landlock/ruleset.c
+++ b/security/landlock/ruleset.c
@@ -20,23 +20,28 @@
#include <linux/refcount.h>
#include <linux/slab.h>
#include <linux/spinlock.h>
-#include <linux/workqueue.h>
#include <uapi/linux/landlock.h>
#include "access.h"
-#include "domain.h"
#include "limits.h"
#include "object.h"
#include "ruleset.h"
-static struct landlock_ruleset *create_ruleset(const u32 num_layers)
+struct landlock_ruleset *
+landlock_create_ruleset(const access_mask_t fs_access_mask,
+ const access_mask_t net_access_mask,
+ const access_mask_t scope_mask)
{
struct landlock_ruleset *new_ruleset;
- new_ruleset = kzalloc_flex(*new_ruleset, access_masks, num_layers,
- GFP_KERNEL_ACCOUNT);
+ /* Informs about useless ruleset. */
+ if (!fs_access_mask && !net_access_mask && !scope_mask)
+ return ERR_PTR(-ENOMSG);
+
+ new_ruleset = kzalloc_obj(*new_ruleset, GFP_KERNEL_ACCOUNT);
if (!new_ruleset)
return ERR_PTR(-ENOMEM);
+
refcount_set(&new_ruleset->usage, 1);
mutex_init(&new_ruleset->lock);
new_ruleset->rules.root_inode = RB_ROOT;
@@ -45,34 +50,27 @@ static struct landlock_ruleset *create_ruleset(const u32 num_layers)
new_ruleset->rules.root_net_port = RB_ROOT;
#endif /* IS_ENABLED(CONFIG_INET) */
- new_ruleset->num_layers = num_layers;
- /*
- * hierarchy = NULL
- * rules.num_rules = 0
- * access_masks[] = 0
- */
- return new_ruleset;
-}
+ /* Should already be checked in landlock_create_ruleset(). */
+ if (fs_access_mask) {
+ const access_mask_t mask = fs_access_mask &
+ LANDLOCK_MASK_ACCESS_FS;
-struct landlock_ruleset *
-landlock_create_ruleset(const access_mask_t fs_access_mask,
- const access_mask_t net_access_mask,
- const access_mask_t scope_mask)
-{
- struct landlock_ruleset *new_ruleset;
+ WARN_ON_ONCE(fs_access_mask != mask);
+ new_ruleset->handled_masks.fs |= mask;
+ }
+ if (net_access_mask) {
+ const access_mask_t mask = net_access_mask &
+ LANDLOCK_MASK_ACCESS_NET;
- /* Informs about useless ruleset. */
- if (!fs_access_mask && !net_access_mask && !scope_mask)
- return ERR_PTR(-ENOMSG);
- new_ruleset = create_ruleset(1);
- if (IS_ERR(new_ruleset))
- return new_ruleset;
- if (fs_access_mask)
- landlock_add_fs_access_mask(new_ruleset, fs_access_mask, 0);
- if (net_access_mask)
- landlock_add_net_access_mask(new_ruleset, net_access_mask, 0);
- if (scope_mask)
- landlock_add_scope_mask(new_ruleset, scope_mask, 0);
+ WARN_ON_ONCE(net_access_mask != mask);
+ new_ruleset->handled_masks.net |= mask;
+ }
+ if (scope_mask) {
+ const access_mask_t mask = scope_mask & LANDLOCK_MASK_SCOPE;
+
+ WARN_ON_ONCE(scope_mask != mask);
+ new_ruleset->handled_masks.scope |= mask;
+ }
return new_ruleset;
}
@@ -129,7 +127,7 @@ create_rule(const struct landlock_id id,
return ERR_PTR(-ENOMEM);
RB_CLEAR_NODE(&new_rule->node);
if (is_object_pointer(id.type)) {
- /* This should have been caught by insert_rule(). */
+ /* This should have been caught by landlock_rule_insert(). */
WARN_ON_ONCE(!id.key.object);
landlock_get_object(id.key.object);
}
@@ -145,12 +143,6 @@ create_rule(const struct landlock_id id,
return new_rule;
}
-static struct rb_root *get_root(struct landlock_ruleset *const ruleset,
- const enum landlock_key_type key_type)
-{
- return landlock_get_rule_root(&ruleset->rules, key_type);
-}
-
static void free_rule(struct landlock_rule *const rule,
const enum landlock_key_type key_type)
{
@@ -167,16 +159,12 @@ static void build_check_ruleset(void)
const struct landlock_rules rules = {
.num_rules = ~0,
};
- const struct landlock_ruleset ruleset = {
- .num_layers = ~0,
- };
BUILD_BUG_ON(rules.num_rules < LANDLOCK_MAX_NUM_RULES);
- BUILD_BUG_ON(ruleset.num_layers < LANDLOCK_MAX_NUM_LAYERS);
}
/**
- * insert_rule - Create and insert a rule into the rule storage
+ * landlock_rule_insert - Create and insert a rule into the rule storage
*
* @rules: The rule storage to be updated. The caller is responsible for
* any required locking. For rulesets, this means holding
@@ -198,10 +186,10 @@ static void build_check_ruleset(void)
*
* Return: 0 on success, -errno on failure.
*/
-static int insert_rule(struct landlock_rules *const rules,
- const struct landlock_id id,
- const struct landlock_layer (*layers)[],
- const size_t num_layers)
+int landlock_rule_insert(struct landlock_rules *const rules,
+ const struct landlock_id id,
+ const struct landlock_layer (*layers)[],
+ const size_t num_layers)
{
struct rb_node **walker_node;
struct rb_node *parent_node = NULL;
@@ -241,7 +229,7 @@ static int insert_rule(struct landlock_rules *const rules,
if ((*layers)[0].level == 0) {
/*
* Extends access rights when the request comes from
- * landlock_add_rule(2), i.e. contained by a ruleset.
+ * landlock_add_rule(2), i.e. @rules is not a domain.
*/
if (WARN_ON_ONCE(this->num_layers != 1))
return -EINVAL;
@@ -303,7 +291,9 @@ int landlock_insert_rule(struct landlock_ruleset *const ruleset,
{
struct landlock_layer layers[] = { {
.access = access,
- /* When @level is zero, insert_rule() extends @ruleset. */
+ /*
+ * When @level is zero, landlock_rule_insert() extends @ruleset.
+ */
.level = 0,
.flags = {
.quiet = !!(flags & LANDLOCK_ADD_RULE_QUIET),
@@ -312,171 +302,8 @@ int landlock_insert_rule(struct landlock_ruleset *const ruleset,
build_check_layer();
lockdep_assert_held(&ruleset->lock);
- return insert_rule(&ruleset->rules, id, &layers, ARRAY_SIZE(layers));
-}
-
-static int merge_tree(struct landlock_ruleset *const dst,
- struct landlock_ruleset *const src,
- const enum landlock_key_type key_type)
-{
- struct landlock_rule *walker_rule, *next_rule;
- struct rb_root *src_root;
- int err = 0;
-
- might_sleep();
- lockdep_assert_held(&dst->lock);
- lockdep_assert_held(&src->lock);
-
- src_root = get_root(src, key_type);
- if (IS_ERR(src_root))
- return PTR_ERR(src_root);
-
- /* Merges the @src tree. */
- rbtree_postorder_for_each_entry_safe(walker_rule, next_rule, src_root,
- node) {
- struct landlock_layer layers[] = { {
- .level = dst->num_layers,
- } };
- const struct landlock_id id = {
- .key = walker_rule->key,
- .type = key_type,
- };
-
- if (WARN_ON_ONCE(walker_rule->num_layers != 1))
- return -EINVAL;
-
- if (WARN_ON_ONCE(walker_rule->layers[0].level != 0))
- return -EINVAL;
-
- layers[0].access = walker_rule->layers[0].access;
- layers[0].flags = walker_rule->layers[0].flags;
-
- err = insert_rule(&dst->rules, id, &layers, ARRAY_SIZE(layers));
- if (err)
- return err;
- }
- return err;
-}
-
-static int merge_ruleset(struct landlock_ruleset *const dst,
- struct landlock_ruleset *const src)
-{
- int err = 0;
-
- might_sleep();
- /* Should already be checked by landlock_merge_ruleset() */
- if (WARN_ON_ONCE(!src))
- return 0;
- /* Only merge into a domain. */
- if (WARN_ON_ONCE(!dst || !dst->hierarchy))
- return -EINVAL;
-
- /* Locks @dst first because we are its only owner. */
- mutex_lock(&dst->lock);
- mutex_lock_nested(&src->lock, SINGLE_DEPTH_NESTING);
-
- /* Stacks the new layer. */
- if (WARN_ON_ONCE(src->num_layers != 1 || dst->num_layers < 1)) {
- err = -EINVAL;
- goto out_unlock;
- }
- dst->access_masks[dst->num_layers - 1] =
- landlock_upgrade_handled_access_masks(src->access_masks[0]);
-
- /* Merges the @src inode tree. */
- err = merge_tree(dst, src, LANDLOCK_KEY_INODE);
- if (err)
- goto out_unlock;
-
-#if IS_ENABLED(CONFIG_INET)
- /* Merges the @src network port tree. */
- err = merge_tree(dst, src, LANDLOCK_KEY_NET_PORT);
- if (err)
- goto out_unlock;
-#endif /* IS_ENABLED(CONFIG_INET) */
-
-out_unlock:
- mutex_unlock(&src->lock);
- mutex_unlock(&dst->lock);
- return err;
-}
-
-static int inherit_tree(struct landlock_ruleset *const parent,
- struct landlock_ruleset *const child,
- const enum landlock_key_type key_type)
-{
- struct landlock_rule *walker_rule, *next_rule;
- struct rb_root *parent_root;
- int err = 0;
-
- might_sleep();
- lockdep_assert_held(&parent->lock);
- lockdep_assert_held(&child->lock);
-
- parent_root = get_root(parent, key_type);
- if (IS_ERR(parent_root))
- return PTR_ERR(parent_root);
-
- /* Copies the @parent inode or network tree. */
- rbtree_postorder_for_each_entry_safe(walker_rule, next_rule,
- parent_root, node) {
- const struct landlock_id id = {
- .key = walker_rule->key,
- .type = key_type,
- };
-
- err = insert_rule(&child->rules, id, &walker_rule->layers,
- walker_rule->num_layers);
- if (err)
- return err;
- }
- return err;
-}
-
-static int inherit_ruleset(struct landlock_ruleset *const parent,
- struct landlock_ruleset *const child)
-{
- int err = 0;
-
- might_sleep();
- if (!parent)
- return 0;
-
- /* Locks @child first because we are its only owner. */
- mutex_lock(&child->lock);
- mutex_lock_nested(&parent->lock, SINGLE_DEPTH_NESTING);
-
- /* Copies the @parent inode tree. */
- err = inherit_tree(parent, child, LANDLOCK_KEY_INODE);
- if (err)
- goto out_unlock;
-
-#if IS_ENABLED(CONFIG_INET)
- /* Copies the @parent network port tree. */
- err = inherit_tree(parent, child, LANDLOCK_KEY_NET_PORT);
- if (err)
- goto out_unlock;
-#endif /* IS_ENABLED(CONFIG_INET) */
-
- if (WARN_ON_ONCE(child->num_layers <= parent->num_layers)) {
- err = -EINVAL;
- goto out_unlock;
- }
- /* Copies the parent layer stack and leaves a space for the new layer. */
- memcpy(child->access_masks, parent->access_masks,
- flex_array_size(parent, access_masks, parent->num_layers));
-
- if (WARN_ON_ONCE(!parent->hierarchy)) {
- err = -EINVAL;
- goto out_unlock;
- }
- landlock_get_hierarchy(parent->hierarchy);
- child->hierarchy->parent = parent->hierarchy;
-
-out_unlock:
- mutex_unlock(&parent->lock);
- mutex_unlock(&child->lock);
- return err;
+ return landlock_rule_insert(&ruleset->rules, id, &layers,
+ ARRAY_SIZE(layers));
}
void landlock_free_rules(struct landlock_rules *const rules)
@@ -499,7 +326,6 @@ static void free_ruleset(struct landlock_ruleset *const ruleset)
{
might_sleep();
landlock_free_rules(&ruleset->rules);
- landlock_put_hierarchy(ruleset->hierarchy);
kfree(ruleset);
}
@@ -509,85 +335,3 @@ void landlock_put_ruleset(struct landlock_ruleset *const ruleset)
if (ruleset && refcount_dec_and_test(&ruleset->usage))
free_ruleset(ruleset);
}
-
-static void free_ruleset_work(struct work_struct *const work)
-{
- struct landlock_ruleset *ruleset;
-
- ruleset = container_of(work, struct landlock_ruleset, work_free);
- free_ruleset(ruleset);
-}
-
-/* Only called by hook_cred_free(). */
-void landlock_put_ruleset_deferred(struct landlock_ruleset *const ruleset)
-{
- if (ruleset && refcount_dec_and_test(&ruleset->usage)) {
- INIT_WORK(&ruleset->work_free, free_ruleset_work);
- schedule_work(&ruleset->work_free);
- }
-}
-
-/**
- * landlock_merge_ruleset - Merge a ruleset with a domain
- *
- * @parent: Parent domain.
- * @ruleset: New ruleset to be merged.
- *
- * The current task is requesting to be restricted. The subjective credentials
- * must not be in an overridden state. cf. landlock_init_hierarchy_log().
- *
- * Return: A new domain merging @parent and @ruleset on success, or ERR_PTR()
- * on failure. If @parent is NULL, the new domain duplicates @ruleset.
- */
-struct landlock_ruleset *
-landlock_merge_ruleset(struct landlock_ruleset *const parent,
- struct landlock_ruleset *const ruleset)
-{
- struct landlock_ruleset *new_dom __free(landlock_put_ruleset) = NULL;
- u32 num_layers;
- int err;
-
- might_sleep();
- if (WARN_ON_ONCE(!ruleset || parent == ruleset))
- return ERR_PTR(-EINVAL);
-
- if (parent) {
- if (parent->num_layers >= LANDLOCK_MAX_NUM_LAYERS)
- return ERR_PTR(-E2BIG);
- num_layers = parent->num_layers + 1;
- } else {
- num_layers = 1;
- }
-
- /* Creates a new domain... */
- new_dom = create_ruleset(num_layers);
- if (IS_ERR(new_dom))
- return new_dom;
-
- new_dom->hierarchy =
- kzalloc_obj(*new_dom->hierarchy, GFP_KERNEL_ACCOUNT);
- if (!new_dom->hierarchy)
- return ERR_PTR(-ENOMEM);
-
- refcount_set(&new_dom->hierarchy->usage, 1);
-
- /* ...as a child of @parent... */
- err = inherit_ruleset(parent, new_dom);
- if (err)
- return ERR_PTR(err);
-
- /* ...and including @ruleset. */
- err = merge_ruleset(new_dom, ruleset);
- if (err)
- return ERR_PTR(err);
-
- err = landlock_init_hierarchy_log(new_dom->hierarchy);
- if (err)
- return ERR_PTR(err);
-
-#ifdef CONFIG_AUDIT
- new_dom->hierarchy->quiet_masks = ruleset->quiet_masks;
-#endif /* CONFIG_AUDIT */
-
- return no_free_ptr(new_dom);
-}
diff --git a/security/landlock/ruleset.h b/security/landlock/ruleset.h
index f9486c06b7ec..da6a12a8e066 100644
--- a/security/landlock/ruleset.h
+++ b/security/landlock/ruleset.h
@@ -14,14 +14,11 @@
#include <linux/mutex.h>
#include <linux/rbtree.h>
#include <linux/refcount.h>
-#include <linux/workqueue.h>
#include "access.h"
#include "limits.h"
#include "object.h"
-struct landlock_hierarchy;
-
/**
* struct landlock_layer - Access rights for a given layer
*/
@@ -158,60 +155,26 @@ struct landlock_ruleset {
* @rules: Red-black tree storage for rules.
*/
struct landlock_rules rules;
-
/**
- * @hierarchy: Enables hierarchy identification even when a parent
- * domain vanishes. This is needed for the ptrace protection.
+ * @lock: Protects against concurrent modifications of @rules, if @usage
+ * is greater than zero.
*/
- struct landlock_hierarchy *hierarchy;
- union {
- /**
- * @work_free: Enables to free a ruleset within a lockless
- * section. This is only used by
- * landlock_put_ruleset_deferred() when @usage reaches zero.
- * The fields @lock, @usage, @num_layers, @quiet_masks and
- * @access_masks are then unused.
- */
- struct work_struct work_free;
- struct {
- /**
- * @lock: Protects against concurrent modifications of
- * @root, if @usage is greater than zero.
- */
- struct mutex lock;
- /**
- * @usage: Number of processes (i.e. domains) or file
- * descriptors referencing this ruleset.
- */
- refcount_t usage;
- /**
- * @num_layers: Number of layers that are used in this
- * ruleset. This enables to check that all the layers
- * allow an access request. A value of 0 identifies a
- * non-merged ruleset (i.e. not a domain).
- */
- u32 num_layers;
- /**
- * @quiet_masks: Stores the quiet flags for an unmerged
- * ruleset. For a merged domain, this is stored in each
- * layer's struct landlock_hierarchy instead.
- */
- struct access_masks quiet_masks;
- /**
- * @access_masks: Contains the subset of filesystem and
- * network actions that are restricted by a ruleset.
- * A domain saves all layers of merged rulesets in a
- * stack (FAM), starting from the first layer to the
- * last one. These layers are used when merging
- * rulesets, for user space backward compatibility
- * (i.e. future-proof), and to properly handle merged
- * rulesets without overlapping access rights. These
- * layers are set once and never changed for the
- * lifetime of the ruleset.
- */
- struct access_masks access_masks[];
- };
- };
+ struct mutex lock;
+ /**
+ * @usage: Number of file descriptors referencing this ruleset.
+ */
+ refcount_t usage;
+ /**
+ * @quiet_masks: Stores the quiet flags for an unmerged ruleset. For a
+ * merged domain, this is stored in each layer's struct
+ * landlock_hierarchy instead.
+ */
+ struct access_masks quiet_masks;
+ /**
+ * @handled_masks: Contains the subset of filesystem and network actions
+ * that are handled by this ruleset.
+ */
+ struct access_masks handled_masks;
};
struct landlock_ruleset *
@@ -220,7 +183,6 @@ landlock_create_ruleset(const access_mask_t access_mask_fs,
const access_mask_t scope_mask);
void landlock_put_ruleset(struct landlock_ruleset *const ruleset);
-void landlock_put_ruleset_deferred(struct landlock_ruleset *const ruleset);
DEFINE_FREE(landlock_put_ruleset, struct landlock_ruleset *,
if (!IS_ERR_OR_NULL(_T)) landlock_put_ruleset(_T))
@@ -229,11 +191,12 @@ int landlock_insert_rule(struct landlock_ruleset *const ruleset,
const struct landlock_id id,
const access_mask_t access, const u32 flags);
-void landlock_free_rules(struct landlock_rules *const rules);
+int landlock_rule_insert(struct landlock_rules *const rules,
+ const struct landlock_id id,
+ const struct landlock_layer (*layers)[],
+ const size_t num_layers);
-struct landlock_ruleset *
-landlock_merge_ruleset(struct landlock_ruleset *const parent,
- struct landlock_ruleset *const ruleset);
+void landlock_free_rules(struct landlock_rules *const rules);
/**
* landlock_get_rule_root - Get the root of a rule tree by key type
@@ -268,62 +231,4 @@ static inline void landlock_get_ruleset(struct landlock_ruleset *const ruleset)
refcount_inc(&ruleset->usage);
}
-static inline void
-landlock_add_fs_access_mask(struct landlock_ruleset *const ruleset,
- const access_mask_t fs_access_mask,
- const u16 layer_level)
-{
- access_mask_t fs_mask = fs_access_mask & LANDLOCK_MASK_ACCESS_FS;
-
- /* Should already be checked in sys_landlock_create_ruleset(). */
- WARN_ON_ONCE(fs_access_mask != fs_mask);
- ruleset->access_masks[layer_level].fs |= fs_mask;
-}
-
-static inline void
-landlock_add_net_access_mask(struct landlock_ruleset *const ruleset,
- const access_mask_t net_access_mask,
- const u16 layer_level)
-{
- access_mask_t net_mask = net_access_mask & LANDLOCK_MASK_ACCESS_NET;
-
- /* Should already be checked in sys_landlock_create_ruleset(). */
- WARN_ON_ONCE(net_access_mask != net_mask);
- ruleset->access_masks[layer_level].net |= net_mask;
-}
-
-static inline void
-landlock_add_scope_mask(struct landlock_ruleset *const ruleset,
- const access_mask_t scope_mask, const u16 layer_level)
-{
- access_mask_t mask = scope_mask & LANDLOCK_MASK_SCOPE;
-
- /* Should already be checked in sys_landlock_create_ruleset(). */
- WARN_ON_ONCE(scope_mask != mask);
- ruleset->access_masks[layer_level].scope |= mask;
-}
-
-static inline access_mask_t
-landlock_get_fs_access_mask(const struct landlock_ruleset *const ruleset,
- const u16 layer_level)
-{
- /* Handles all initially denied by default access rights. */
- return ruleset->access_masks[layer_level].fs |
- _LANDLOCK_ACCESS_FS_INITIALLY_DENIED;
-}
-
-static inline access_mask_t
-landlock_get_net_access_mask(const struct landlock_ruleset *const ruleset,
- const u16 layer_level)
-{
- return ruleset->access_masks[layer_level].net;
-}
-
-static inline access_mask_t
-landlock_get_scope_mask(const struct landlock_ruleset *const ruleset,
- const u16 layer_level)
-{
- return ruleset->access_masks[layer_level].scope;
-}
-
#endif /* _SECURITY_LANDLOCK_RULESET_H */
diff --git a/security/landlock/syscalls.c b/security/landlock/syscalls.c
index 36b02892c62f..fe8505dc0ba5 100644
--- a/security/landlock/syscalls.c
+++ b/security/landlock/syscalls.c
@@ -308,8 +308,6 @@ static struct landlock_ruleset *get_ruleset_from_fd(const int fd,
if (!(fd_file(ruleset_f)->f_mode & mode))
return ERR_PTR(-EPERM);
ruleset = fd_file(ruleset_f)->private_data;
- if (WARN_ON_ONCE(ruleset->num_layers != 1))
- return ERR_PTR(-EINVAL);
landlock_get_ruleset(ruleset);
return ruleset;
}
@@ -367,7 +365,7 @@ static int add_rule_path_beneath(struct landlock_ruleset *const ruleset,
return -ENOMSG;
/* Checks that allowed_access matches the @ruleset constraints. */
- mask = ruleset->access_masks[0].fs;
+ mask = ruleset->handled_masks.fs;
if ((path_beneath_attr.allowed_access | mask) != mask)
return -EINVAL;
@@ -408,7 +406,7 @@ static int add_rule_net_port(struct landlock_ruleset *ruleset,
return -ENOMSG;
/* Checks that allowed_access matches the @ruleset constraints. */
- mask = landlock_get_net_access_mask(ruleset, 0);
+ mask = ruleset->handled_masks.net;
if ((net_port_attr.allowed_access | mask) != mask)
return -EINVAL;
@@ -595,7 +593,7 @@ SYSCALL_DEFINE2(landlock_restrict_self, const int, ruleset_fd, const __u32,
* manipulating the current credentials because they are
* dedicated per thread.
*/
- struct landlock_ruleset *const new_dom =
+ struct landlock_domain *const new_dom =
landlock_merge_ruleset(new_llcred->domain, ruleset);
if (IS_ERR(new_dom)) {
abort_creds(new_cred);
@@ -610,7 +608,7 @@ SYSCALL_DEFINE2(landlock_restrict_self, const int, ruleset_fd, const __u32,
#endif /* CONFIG_AUDIT */
/* Replaces the old (prepared) domain. */
- landlock_put_ruleset(new_llcred->domain);
+ landlock_put_domain(new_llcred->domain);
new_llcred->domain = new_dom;
#ifdef CONFIG_AUDIT
diff --git a/security/landlock/task.c b/security/landlock/task.c
index 360d226d0f51..40e6bfa4bc75 100644
--- a/security/landlock/task.c
+++ b/security/landlock/task.c
@@ -41,8 +41,8 @@
* Return: True if @parent is an ancestor of or equal to @child, false
* otherwise.
*/
-static bool domain_scope_le(const struct landlock_ruleset *const parent,
- const struct landlock_ruleset *const child)
+static bool domain_scope_le(const struct landlock_domain *const parent,
+ const struct landlock_domain *const child)
{
const struct landlock_hierarchy *walker;
@@ -63,8 +63,8 @@ static bool domain_scope_le(const struct landlock_ruleset *const parent,
return false;
}
-static int domain_ptrace(const struct landlock_ruleset *const parent,
- const struct landlock_ruleset *const child)
+static int domain_ptrace(const struct landlock_domain *const parent,
+ const struct landlock_domain *const child)
{
if (domain_scope_le(parent, child))
return 0;
@@ -96,7 +96,7 @@ static int hook_ptrace_access_check(struct task_struct *const child,
return 0;
scoped_guard(rcu) {
- const struct landlock_ruleset *const child_dom =
+ const struct landlock_domain *const child_dom =
landlock_get_task_domain(child);
err = domain_ptrace(parent_subject->domain, child_dom);
}
@@ -135,7 +135,7 @@ static int hook_ptrace_access_check(struct task_struct *const child,
static int hook_ptrace_traceme(struct task_struct *const parent)
{
const struct landlock_cred_security *parent_subject;
- const struct landlock_ruleset *child_dom;
+ const struct landlock_domain *child_dom;
int err;
child_dom = landlock_get_current_domain();
@@ -176,8 +176,8 @@ static int hook_ptrace_traceme(struct task_struct *const parent)
* Return: True if @server is in a different domain from @client and @client
* is scoped to access @server (i.e. access should be denied), false otherwise.
*/
-static bool domain_is_scoped(const struct landlock_ruleset *const client,
- const struct landlock_ruleset *const server,
+static bool domain_is_scoped(const struct landlock_domain *const client,
+ const struct landlock_domain *const server,
access_mask_t scope)
{
int client_layer, server_layer;
@@ -236,9 +236,9 @@ static bool domain_is_scoped(const struct landlock_ruleset *const client,
}
static bool sock_is_scoped(struct sock *const other,
- const struct landlock_ruleset *const domain)
+ const struct landlock_domain *const domain)
{
- const struct landlock_ruleset *dom_other;
+ const struct landlock_domain *dom_other;
/* The credentials will not change. */
lockdep_assert_held(&unix_sk(other)->lock);
--
2.54.0
^ permalink raw reply related
* [PATCH v3 01/20] landlock: Prepare ruleset and domain type split
From: Mickaël Salaün @ 2026-07-22 17:11 UTC (permalink / raw)
To: Günther Noack, Steven Rostedt
Cc: Mickaël Salaün, Christian Brauner, Jann Horn, Jeff Xu,
Justin Suess, Kees Cook, Masami Hiramatsu, Mathieu Desnoyers,
Matthieu Buffet, Mikhail Ivanov, Tingmao Wang, kernel-team,
linux-security-module, linux-trace-kernel
In-Reply-To: <20260722171159.2776765-1-mic@digikod.net>
Rulesets and domains serve fundamentally different purposes: a ruleset
is mutable and user-facing, created by landlock_create_ruleset(), while
a domain is immutable after construction and enforced on tasks via
landlock_restrict_self(). Today both are represented by struct
landlock_ruleset, which conflates mutable and immutable state in a
single type: the lock field is unused by domains, the hierarchy field is
unused by rulesets, and lifecycle functions must handle both cases.
Prepare for a clean type split by introducing two new structures:
- struct landlock_rules: the red-black tree roots and rule count, shared
by both rulesets and domains. Decoupling rule storage from the domain
API lets the backing data structure change independently (e.g. to a
hash table, cf. [1]).
- struct landlock_domain: the immutable domain enforced on tasks, with
no lock field because its rules and access masks are fixed once
construction completes. The name reflects the role, not the internal
data structure.
Add the domain lifecycle helpers (landlock_get_domain(),
landlock_put_domain(), landlock_put_domain_deferred()) and move domain.o
from landlock-$(CONFIG_AUDIT) to landlock-y, because these are needed
unconditionally, not just for audit logging.
No behavioral change. The new types and lifecycle functions are not yet
used by any caller.
Cc: Günther Noack <gnoack@google.com>
Cc: Tingmao Wang <m@maowtm.org>
Link: https://patch.msgid.link/20250523165741.693976-1-mic@digikod.net [1]
Signed-off-by: Mickaël Salaün <mic@digikod.net>
---
Changes since v2:
https://patch.msgid.link/20260406143717.1815792-2-mic@digikod.net
- Adapt rule insertion to the base's quiet flag (landlock_insert_rule()
gains a flags argument).
- Drop Tingmao Wang's Reviewed-by; the rebase reworked the patch.
Changes since v1:
- New patch.
---
security/landlock/Makefile | 6 +--
security/landlock/domain.c | 35 ++++++++++++++++
security/landlock/domain.h | 69 ++++++++++++++++++++++++++++++++
security/landlock/ruleset.c | 71 ++++++++++++++++-----------------
security/landlock/ruleset.h | 79 +++++++++++++++++++++++++++----------
5 files changed, 200 insertions(+), 60 deletions(-)
diff --git a/security/landlock/Makefile b/security/landlock/Makefile
index ffa7646d99f3..23e13644916f 100644
--- a/security/landlock/Makefile
+++ b/security/landlock/Makefile
@@ -8,11 +8,11 @@ landlock-y := \
cred.o \
task.o \
fs.o \
- tsync.o
+ tsync.o \
+ domain.o
landlock-$(CONFIG_INET) += net.o
landlock-$(CONFIG_AUDIT) += \
id.o \
- audit.o \
- domain.o
+ audit.o
diff --git a/security/landlock/domain.c b/security/landlock/domain.c
index 9a8355fccd26..c5efb7635a7b 100644
--- a/security/landlock/domain.c
+++ b/security/landlock/domain.c
@@ -15,14 +15,49 @@
#include <linux/mm.h>
#include <linux/path.h>
#include <linux/pid.h>
+#include <linux/refcount.h>
#include <linux/sched.h>
#include <linux/signal.h>
+#include <linux/slab.h>
#include <linux/uidgid.h>
+#include <linux/workqueue.h>
#include "access.h"
#include "common.h"
#include "domain.h"
#include "id.h"
+#include "ruleset.h"
+
+static void free_domain(struct landlock_domain *const domain)
+{
+ might_sleep();
+ landlock_free_rules(&domain->rules);
+ landlock_put_hierarchy(domain->hierarchy);
+ kfree(domain);
+}
+
+void landlock_put_domain(struct landlock_domain *const domain)
+{
+ might_sleep();
+ if (domain && refcount_dec_and_test(&domain->usage))
+ free_domain(domain);
+}
+
+static void free_domain_work(struct work_struct *const work)
+{
+ struct landlock_domain *domain;
+
+ domain = container_of(work, struct landlock_domain, work_free);
+ free_domain(domain);
+}
+
+void landlock_put_domain_deferred(struct landlock_domain *const domain)
+{
+ if (domain && refcount_dec_and_test(&domain->usage)) {
+ INIT_WORK(&domain->work_free, free_domain_work);
+ schedule_work(&domain->work_free);
+ }
+}
#ifdef CONFIG_AUDIT
diff --git a/security/landlock/domain.h b/security/landlock/domain.h
index 2a1660e3dea7..bcfd0452e260 100644
--- a/security/landlock/domain.h
+++ b/security/landlock/domain.h
@@ -10,6 +10,7 @@
#ifndef _SECURITY_LANDLOCK_DOMAIN_H
#define _SECURITY_LANDLOCK_DOMAIN_H
+#include <linux/cleanup.h>
#include <linux/limits.h>
#include <linux/mm.h>
#include <linux/path.h>
@@ -17,9 +18,11 @@
#include <linux/refcount.h>
#include <linux/sched.h>
#include <linux/slab.h>
+#include <linux/workqueue.h>
#include "access.h"
#include "audit.h"
+#include "ruleset.h"
enum landlock_log_status {
LANDLOCK_LOG_PENDING = 0,
@@ -176,4 +179,70 @@ static inline void landlock_put_hierarchy(struct landlock_hierarchy *hierarchy)
}
}
+/**
+ * struct landlock_domain - Immutable Landlock domain
+ *
+ * A domain is created from a ruleset by landlock_merge_ruleset() and enforced
+ * on a task. Once created, its rules and access masks are immutable. Unlike
+ * &struct landlock_ruleset, a domain has no lock field.
+ */
+struct landlock_domain {
+ /**
+ * @rules: Red-black tree storage for rules.
+ */
+ struct landlock_rules rules;
+ /**
+ * @hierarchy: Enables hierarchy identification even when a parent
+ * domain vanishes. This is needed for the ptrace and scope
+ * restrictions.
+ */
+ struct landlock_hierarchy *hierarchy;
+ union {
+ /**
+ * @work_free: Enables to free a domain within a lockless
+ * section. This is only used by landlock_put_domain_deferred()
+ * when @usage reaches zero. The fields @usage, @num_layers and
+ * @access_masks are then unused.
+ */
+ struct work_struct work_free;
+ struct {
+ /**
+ * @usage: Number of credentials referencing this
+ * domain.
+ */
+ refcount_t usage;
+ /**
+ * @num_layers: Number of layers that are used in this
+ * domain. This enables to check that all the layers
+ * allow an access request.
+ */
+ u32 num_layers;
+ /**
+ * @access_masks: Contains the subset of filesystem and
+ * network actions that are restricted by a domain. A
+ * domain saves all layers of merged rulesets in a stack
+ * (FAM), starting from the first layer to the last one.
+ * These layers are used when merging rulesets, for user
+ * space backward compatibility (i.e. future-proof), and
+ * to properly handle merged rulesets without
+ * overlapping access rights. These layers are set once
+ * and never changed for the lifetime of the domain.
+ */
+ struct access_masks access_masks[];
+ };
+ };
+};
+
+void landlock_put_domain(struct landlock_domain *const domain);
+void landlock_put_domain_deferred(struct landlock_domain *const domain);
+
+DEFINE_FREE(landlock_put_domain, struct landlock_domain *,
+ if (!IS_ERR_OR_NULL(_T)) landlock_put_domain(_T))
+
+static inline void landlock_get_domain(struct landlock_domain *const domain)
+{
+ if (domain)
+ refcount_inc(&domain->usage);
+}
+
#endif /* _SECURITY_LANDLOCK_DOMAIN_H */
diff --git a/security/landlock/ruleset.c b/security/landlock/ruleset.c
index 4dd09ea22c84..9e560aed64b6 100644
--- a/security/landlock/ruleset.c
+++ b/security/landlock/ruleset.c
@@ -39,16 +39,16 @@ static struct landlock_ruleset *create_ruleset(const u32 num_layers)
return ERR_PTR(-ENOMEM);
refcount_set(&new_ruleset->usage, 1);
mutex_init(&new_ruleset->lock);
- new_ruleset->root_inode = RB_ROOT;
+ new_ruleset->rules.root_inode = RB_ROOT;
#if IS_ENABLED(CONFIG_INET)
- new_ruleset->root_net_port = RB_ROOT;
+ new_ruleset->rules.root_net_port = RB_ROOT;
#endif /* IS_ENABLED(CONFIG_INET) */
new_ruleset->num_layers = num_layers;
/*
* hierarchy = NULL
- * num_rules = 0
+ * rules.num_rules = 0
* access_masks[] = 0
*/
return new_ruleset;
@@ -148,19 +148,7 @@ create_rule(const struct landlock_id id,
static struct rb_root *get_root(struct landlock_ruleset *const ruleset,
const enum landlock_key_type key_type)
{
- switch (key_type) {
- case LANDLOCK_KEY_INODE:
- return &ruleset->root_inode;
-
-#if IS_ENABLED(CONFIG_INET)
- case LANDLOCK_KEY_NET_PORT:
- return &ruleset->root_net_port;
-#endif /* IS_ENABLED(CONFIG_INET) */
-
- default:
- WARN_ON_ONCE(1);
- return ERR_PTR(-EINVAL);
- }
+ return landlock_get_rule_root(&ruleset->rules, key_type);
}
static void free_rule(struct landlock_rule *const rule,
@@ -176,19 +164,24 @@ static void free_rule(struct landlock_rule *const rule,
static void build_check_ruleset(void)
{
- const struct landlock_ruleset ruleset = {
+ const struct landlock_rules rules = {
.num_rules = ~0,
+ };
+ const struct landlock_ruleset ruleset = {
.num_layers = ~0,
};
- BUILD_BUG_ON(ruleset.num_rules < LANDLOCK_MAX_NUM_RULES);
+ BUILD_BUG_ON(rules.num_rules < LANDLOCK_MAX_NUM_RULES);
BUILD_BUG_ON(ruleset.num_layers < LANDLOCK_MAX_NUM_LAYERS);
}
/**
- * insert_rule - Create and insert a rule in a ruleset
+ * insert_rule - Create and insert a rule into the rule storage
*
- * @ruleset: The ruleset to be updated.
+ * @rules: The rule storage to be updated. The caller is responsible for
+ * any required locking. For rulesets, this means holding
+ * &landlock_ruleset.lock. For domains under construction, no lock is
+ * needed because the domain is not yet visible to other tasks.
* @id: The ID to build the new rule with. The underlying kernel object, if
* any, must be held by the caller.
* @layers: One or multiple layers to be copied into the new rule.
@@ -196,16 +189,16 @@ static void build_check_ruleset(void)
*
* When user space requests to add a new rule to a ruleset, @layers only
* contains one entry and this entry is not assigned to any level. In this
- * case, the new rule will extend @ruleset, similarly to a boolean OR between
+ * case, the new rule will extend @rules, similarly to a boolean OR between
* access rights.
*
* When merging a ruleset in a domain, or copying a domain, @layers will be
- * added to @ruleset as new constraints, similarly to a boolean AND between
- * access rights.
+ * added to @rules as new constraints, similarly to a boolean AND between access
+ * rights.
*
* Return: 0 on success, -errno on failure.
*/
-static int insert_rule(struct landlock_ruleset *const ruleset,
+static int insert_rule(struct landlock_rules *const rules,
const struct landlock_id id,
const struct landlock_layer (*layers)[],
const size_t num_layers)
@@ -216,14 +209,13 @@ static int insert_rule(struct landlock_ruleset *const ruleset,
struct rb_root *root;
might_sleep();
- lockdep_assert_held(&ruleset->lock);
if (WARN_ON_ONCE(!layers))
return -ENOENT;
if (is_object_pointer(id.type) && WARN_ON_ONCE(!id.key.object))
return -ENOENT;
- root = get_root(ruleset, id.type);
+ root = landlock_get_rule_root(rules, id.type);
if (IS_ERR(root))
return PTR_ERR(root);
@@ -249,7 +241,7 @@ static int insert_rule(struct landlock_ruleset *const ruleset,
if ((*layers)[0].level == 0) {
/*
* Extends access rights when the request comes from
- * landlock_add_rule(2), i.e. @ruleset is not a domain.
+ * landlock_add_rule(2), i.e. contained by a ruleset.
*/
if (WARN_ON_ONCE(this->num_layers != 1))
return -EINVAL;
@@ -278,14 +270,14 @@ static int insert_rule(struct landlock_ruleset *const ruleset,
/* There is no match for @id. */
build_check_ruleset();
- if (ruleset->num_rules >= LANDLOCK_MAX_NUM_RULES)
+ if (rules->num_rules >= LANDLOCK_MAX_NUM_RULES)
return -E2BIG;
new_rule = create_rule(id, layers, num_layers, NULL);
if (IS_ERR(new_rule))
return PTR_ERR(new_rule);
rb_link_node(&new_rule->node, parent_node, walker_node);
rb_insert_color(&new_rule->node, root);
- ruleset->num_rules++;
+ rules->num_rules++;
return 0;
}
@@ -319,7 +311,8 @@ int landlock_insert_rule(struct landlock_ruleset *const ruleset,
} };
build_check_layer();
- return insert_rule(ruleset, id, &layers, ARRAY_SIZE(layers));
+ lockdep_assert_held(&ruleset->lock);
+ return insert_rule(&ruleset->rules, id, &layers, ARRAY_SIZE(layers));
}
static int merge_tree(struct landlock_ruleset *const dst,
@@ -358,7 +351,7 @@ static int merge_tree(struct landlock_ruleset *const dst,
layers[0].access = walker_rule->layers[0].access;
layers[0].flags = walker_rule->layers[0].flags;
- err = insert_rule(dst, id, &layers, ARRAY_SIZE(layers));
+ err = insert_rule(&dst->rules, id, &layers, ARRAY_SIZE(layers));
if (err)
return err;
}
@@ -432,7 +425,7 @@ static int inherit_tree(struct landlock_ruleset *const parent,
.type = key_type,
};
- err = insert_rule(child, id, &walker_rule->layers,
+ err = insert_rule(&child->rules, id, &walker_rule->layers,
walker_rule->num_layers);
if (err)
return err;
@@ -486,21 +479,26 @@ static int inherit_ruleset(struct landlock_ruleset *const parent,
return err;
}
-static void free_ruleset(struct landlock_ruleset *const ruleset)
+void landlock_free_rules(struct landlock_rules *const rules)
{
struct landlock_rule *freeme, *next;
might_sleep();
- rbtree_postorder_for_each_entry_safe(freeme, next, &ruleset->root_inode,
+ rbtree_postorder_for_each_entry_safe(freeme, next, &rules->root_inode,
node)
free_rule(freeme, LANDLOCK_KEY_INODE);
#if IS_ENABLED(CONFIG_INET)
rbtree_postorder_for_each_entry_safe(freeme, next,
- &ruleset->root_net_port, node)
+ &rules->root_net_port, node)
free_rule(freeme, LANDLOCK_KEY_NET_PORT);
#endif /* IS_ENABLED(CONFIG_INET) */
+}
+static void free_ruleset(struct landlock_ruleset *const ruleset)
+{
+ might_sleep();
+ landlock_free_rules(&ruleset->rules);
landlock_put_hierarchy(ruleset->hierarchy);
kfree(ruleset);
}
@@ -604,7 +602,8 @@ landlock_find_rule(const struct landlock_ruleset *const ruleset,
const struct rb_root *root;
const struct rb_node *node;
- root = get_root((struct landlock_ruleset *)ruleset, id.type);
+ root = landlock_get_rule_root((struct landlock_rules *)&ruleset->rules,
+ id.type);
if (IS_ERR(root))
return NULL;
node = root->rb_node;
diff --git a/security/landlock/ruleset.h b/security/landlock/ruleset.h
index 0437adf17428..c8c83efff4c5 100644
--- a/security/landlock/ruleset.h
+++ b/security/landlock/ruleset.h
@@ -68,13 +68,12 @@ union landlock_key {
*/
enum landlock_key_type {
/**
- * @LANDLOCK_KEY_INODE: Type of &landlock_ruleset.root_inode's node
- * keys.
+ * @LANDLOCK_KEY_INODE: Type of &landlock_rules.root_inode's node keys.
*/
LANDLOCK_KEY_INODE = 1,
/**
- * @LANDLOCK_KEY_NET_PORT: Type of &landlock_ruleset.root_net_port's
- * node keys.
+ * @LANDLOCK_KEY_NET_PORT: Type of &landlock_rules.root_net_port's node
+ * keys.
*/
LANDLOCK_KEY_NET_PORT,
};
@@ -122,30 +121,44 @@ struct landlock_rule {
};
/**
- * struct landlock_ruleset - Landlock ruleset
+ * struct landlock_rules - Red-black tree storage for Landlock rules
*
- * This data structure must contain unique entries, be updatable, and quick to
- * match an object.
+ * This structure holds the rule trees shared by both rulesets and domains.
*/
-struct landlock_ruleset {
+struct landlock_rules {
/**
* @root_inode: Root of a red-black tree containing &struct
- * landlock_rule nodes with inode object. Once a ruleset is tied to a
- * process (i.e. as a domain), this tree is immutable until @usage
- * reaches zero.
+ * landlock_rule nodes with inode object. Immutable for domains.
*/
struct rb_root root_inode;
#if IS_ENABLED(CONFIG_INET)
/**
* @root_net_port: Root of a red-black tree containing &struct
- * landlock_rule nodes with network port. Once a ruleset is tied to a
- * process (i.e. as a domain), this tree is immutable until @usage
- * reaches zero.
+ * landlock_rule nodes with network port. Immutable for domains.
*/
struct rb_root root_net_port;
#endif /* IS_ENABLED(CONFIG_INET) */
+ /**
+ * @num_rules: Number of non-overlapping (i.e. not for the same object)
+ * rules in this tree storage.
+ */
+ u32 num_rules;
+};
+
+/**
+ * struct landlock_ruleset - Landlock ruleset
+ *
+ * This data structure must contain unique entries, be updatable, and quick to
+ * match an object.
+ */
+struct landlock_ruleset {
+ /**
+ * @rules: Red-black tree storage for rules.
+ */
+ struct landlock_rules rules;
+
/**
* @hierarchy: Enables hierarchy identification even when a parent
* domain vanishes. This is needed for the ptrace protection.
@@ -156,8 +169,8 @@ struct landlock_ruleset {
* @work_free: Enables to free a ruleset within a lockless
* section. This is only used by
* landlock_put_ruleset_deferred() when @usage reaches zero.
- * The fields @lock, @usage, @num_rules, @num_layers,
- * @quiet_masks and @access_masks are then unused.
+ * The fields @lock, @usage, @num_layers, @quiet_masks and
+ * @access_masks are then unused.
*/
struct work_struct work_free;
struct {
@@ -171,11 +184,6 @@ struct landlock_ruleset {
* descriptors referencing this ruleset.
*/
refcount_t usage;
- /**
- * @num_rules: Number of non-overlapping (i.e. not for
- * the same object) rules in this ruleset.
- */
- u32 num_rules;
/**
* @num_layers: Number of layers that are used in this
* ruleset. This enables to check that all the layers
@@ -221,6 +229,8 @@ int landlock_insert_rule(struct landlock_ruleset *const ruleset,
const struct landlock_id id,
const access_mask_t access, const u32 flags);
+void landlock_free_rules(struct landlock_rules *const rules);
+
struct landlock_ruleset *
landlock_merge_ruleset(struct landlock_ruleset *const parent,
struct landlock_ruleset *const ruleset);
@@ -229,6 +239,33 @@ const struct landlock_rule *
landlock_find_rule(const struct landlock_ruleset *const ruleset,
const struct landlock_id id);
+/**
+ * landlock_get_rule_root - Get the root of a rule tree by key type
+ *
+ * @rules: The rules storage to look up.
+ * @key_type: The type of key to select the tree for.
+ *
+ * Return: A pointer to the rb_root, or ERR_PTR(-EINVAL) on unknown type.
+ */
+static inline struct rb_root *
+landlock_get_rule_root(struct landlock_rules *const rules,
+ const enum landlock_key_type key_type)
+{
+ switch (key_type) {
+ case LANDLOCK_KEY_INODE:
+ return &rules->root_inode;
+
+#if IS_ENABLED(CONFIG_INET)
+ case LANDLOCK_KEY_NET_PORT:
+ return &rules->root_net_port;
+#endif /* IS_ENABLED(CONFIG_INET) */
+
+ default:
+ WARN_ON_ONCE(1);
+ return ERR_PTR(-EINVAL);
+ }
+}
+
static inline void landlock_get_ruleset(struct landlock_ruleset *const ruleset)
{
if (ruleset)
--
2.54.0
^ permalink raw reply related
* [PATCH v3 06/20] landlock: Consolidate access-right and scope names in a shared header
From: Mickaël Salaün @ 2026-07-22 17:11 UTC (permalink / raw)
To: Günther Noack, Steven Rostedt
Cc: Mickaël Salaün, Christian Brauner, Jann Horn, Jeff Xu,
Justin Suess, Kees Cook, Masami Hiramatsu, Mathieu Desnoyers,
Matthieu Buffet, Mikhail Ivanov, Tingmao Wang, kernel-team,
linux-security-module, linux-trace-kernel
In-Reply-To: <20260722171159.2776765-1-mic@digikod.net>
Audit formats denial records with per-right name strings. A following
commit adds trace events that print the same access and scope masks with
__print_flags() and need the same names, but a trace event header cannot
include Landlock-internal headers, so the names cannot be shared from
the logging unit.
Define the filesystem, network, and scope names once, as the
_LANDLOCK_ACCESS_FS_NAMES, _LANDLOCK_ACCESS_NET_NAMES, and
_LANDLOCK_SCOPE_NAMES lists in the public Landlock header. Each entry
is a _LANDLOCK_NAME_ENTRY() the consumer expands: audit maps it to a
"[bit] = name" array slot for an O(1) lookup, the trace events map it to
a __print_flags() { mask, name } pair. The bit value comes only from
the LANDLOCK_* UAPI constant each entry references, so every bit-to-name
mapping has a single source and does not depend on entry order.
The shared names are unprefixed; blocker_prefix() prepends the
fs./net./scope. category for audit records, so the scope names move from
inline literals to the shared table too. Audit records are unchanged.
No functional change.
Cc: Günther Noack <gnoack@google.com>
Cc: Tingmao Wang <m@maowtm.org>
Signed-off-by: Mickaël Salaün <mic@digikod.net>
---
Changes since v2:
- New patch.
---
MAINTAINERS | 1 +
include/linux/landlock.h | 55 +++++++++++++++++++++++++
security/landlock/audit.c | 84 ++++++++++++++++++++++++---------------
3 files changed, 109 insertions(+), 31 deletions(-)
create mode 100644 include/linux/landlock.h
diff --git a/MAINTAINERS b/MAINTAINERS
index a674e36529f7..91de7e3c2836 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -14598,6 +14598,7 @@ F: Documentation/admin-guide/LSM/landlock.rst
F: Documentation/security/landlock.rst
F: Documentation/userspace-api/landlock.rst
F: fs/ioctl.c
+F: include/linux/landlock.h
F: include/uapi/linux/landlock.h
F: samples/landlock/
F: security/landlock/
diff --git a/include/linux/landlock.h b/include/linux/landlock.h
new file mode 100644
index 000000000000..cabe6c784833
--- /dev/null
+++ b/include/linux/landlock.h
@@ -0,0 +1,55 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * Landlock - Public types and definitions
+ *
+ * Copyright © 2016-2026 Mickaël Salaün <mic@digikod.net>
+ * Copyright © 2026 Cloudflare, Inc.
+ */
+
+#ifndef _LINUX_LANDLOCK_H
+#define _LINUX_LANDLOCK_H
+
+#include <uapi/linux/landlock.h>
+
+/*
+ * Access-right and scope names, shared between the audit records (get_blocker()
+ * in security/landlock/audit.c) and the trace events
+ * (include/trace/events/landlock.h). A consumer defines
+ * _LANDLOCK_NAME_ENTRY(mask, name) before expanding a list and undefines it
+ * afterwards: audit maps each entry to a "[bit] = name" slot for O(1) lookup,
+ * the trace events map it to a __print_flags() { mask, name } pair. The bit
+ * value lives only in the LANDLOCK_* UAPI constant each entry references.
+ * Names are unprefixed; audit prepends the "fs."/"net."/"scope." category.
+ */
+#define _LANDLOCK_ACCESS_FS_NAMES \
+ _LANDLOCK_NAME_ENTRY(LANDLOCK_ACCESS_FS_EXECUTE, "execute"), \
+ _LANDLOCK_NAME_ENTRY(LANDLOCK_ACCESS_FS_WRITE_FILE, "write_file"), \
+ _LANDLOCK_NAME_ENTRY(LANDLOCK_ACCESS_FS_READ_FILE, "read_file"), \
+ _LANDLOCK_NAME_ENTRY(LANDLOCK_ACCESS_FS_READ_DIR, "read_dir"), \
+ _LANDLOCK_NAME_ENTRY(LANDLOCK_ACCESS_FS_REMOVE_DIR, "remove_dir"), \
+ _LANDLOCK_NAME_ENTRY(LANDLOCK_ACCESS_FS_REMOVE_FILE, "remove_file"), \
+ _LANDLOCK_NAME_ENTRY(LANDLOCK_ACCESS_FS_MAKE_CHAR, "make_char"), \
+ _LANDLOCK_NAME_ENTRY(LANDLOCK_ACCESS_FS_MAKE_DIR, "make_dir"), \
+ _LANDLOCK_NAME_ENTRY(LANDLOCK_ACCESS_FS_MAKE_REG, "make_reg"), \
+ _LANDLOCK_NAME_ENTRY(LANDLOCK_ACCESS_FS_MAKE_SOCK, "make_sock"), \
+ _LANDLOCK_NAME_ENTRY(LANDLOCK_ACCESS_FS_MAKE_FIFO, "make_fifo"), \
+ _LANDLOCK_NAME_ENTRY(LANDLOCK_ACCESS_FS_MAKE_BLOCK, "make_block"), \
+ _LANDLOCK_NAME_ENTRY(LANDLOCK_ACCESS_FS_MAKE_SYM, "make_sym"), \
+ _LANDLOCK_NAME_ENTRY(LANDLOCK_ACCESS_FS_REFER, "refer"), \
+ _LANDLOCK_NAME_ENTRY(LANDLOCK_ACCESS_FS_TRUNCATE, "truncate"), \
+ _LANDLOCK_NAME_ENTRY(LANDLOCK_ACCESS_FS_IOCTL_DEV, "ioctl_dev"), \
+ _LANDLOCK_NAME_ENTRY(LANDLOCK_ACCESS_FS_RESOLVE_UNIX, "resolve_unix")
+
+#define _LANDLOCK_ACCESS_NET_NAMES \
+ _LANDLOCK_NAME_ENTRY(LANDLOCK_ACCESS_NET_BIND_TCP, "bind_tcp"), \
+ _LANDLOCK_NAME_ENTRY(LANDLOCK_ACCESS_NET_CONNECT_TCP, "connect_tcp"), \
+ _LANDLOCK_NAME_ENTRY(LANDLOCK_ACCESS_NET_BIND_UDP, "bind_udp"), \
+ _LANDLOCK_NAME_ENTRY(LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP, \
+ "connect_send_udp")
+
+#define _LANDLOCK_SCOPE_NAMES \
+ _LANDLOCK_NAME_ENTRY(LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET, \
+ "abstract_unix_socket"), \
+ _LANDLOCK_NAME_ENTRY(LANDLOCK_SCOPE_SIGNAL, "signal")
+
+#endif /* _LINUX_LANDLOCK_H */
diff --git a/security/landlock/audit.c b/security/landlock/audit.c
index 32260e7cbfe9..e02963834e48 100644
--- a/security/landlock/audit.c
+++ b/security/landlock/audit.c
@@ -7,6 +7,7 @@
#include <linux/audit.h>
#include <linux/bitops.h>
+#include <linux/landlock.h>
#include <linux/lsm_audit.h>
#include <linux/pid.h>
#include <uapi/linux/landlock.h>
@@ -19,38 +20,28 @@
#include "limits.h"
#include "log.h"
-static const char *const fs_access_strings[] = {
- [BIT_INDEX(LANDLOCK_ACCESS_FS_EXECUTE)] = "fs.execute",
- [BIT_INDEX(LANDLOCK_ACCESS_FS_WRITE_FILE)] = "fs.write_file",
- [BIT_INDEX(LANDLOCK_ACCESS_FS_READ_FILE)] = "fs.read_file",
- [BIT_INDEX(LANDLOCK_ACCESS_FS_READ_DIR)] = "fs.read_dir",
- [BIT_INDEX(LANDLOCK_ACCESS_FS_REMOVE_DIR)] = "fs.remove_dir",
- [BIT_INDEX(LANDLOCK_ACCESS_FS_REMOVE_FILE)] = "fs.remove_file",
- [BIT_INDEX(LANDLOCK_ACCESS_FS_MAKE_CHAR)] = "fs.make_char",
- [BIT_INDEX(LANDLOCK_ACCESS_FS_MAKE_DIR)] = "fs.make_dir",
- [BIT_INDEX(LANDLOCK_ACCESS_FS_MAKE_REG)] = "fs.make_reg",
- [BIT_INDEX(LANDLOCK_ACCESS_FS_MAKE_SOCK)] = "fs.make_sock",
- [BIT_INDEX(LANDLOCK_ACCESS_FS_MAKE_FIFO)] = "fs.make_fifo",
- [BIT_INDEX(LANDLOCK_ACCESS_FS_MAKE_BLOCK)] = "fs.make_block",
- [BIT_INDEX(LANDLOCK_ACCESS_FS_MAKE_SYM)] = "fs.make_sym",
- [BIT_INDEX(LANDLOCK_ACCESS_FS_REFER)] = "fs.refer",
- [BIT_INDEX(LANDLOCK_ACCESS_FS_TRUNCATE)] = "fs.truncate",
- [BIT_INDEX(LANDLOCK_ACCESS_FS_IOCTL_DEV)] = "fs.ioctl_dev",
- [BIT_INDEX(LANDLOCK_ACCESS_FS_RESOLVE_UNIX)] = "fs.resolve_unix",
-};
+/*
+ * Access-right and scope names are built from the lists shared with the trace
+ * events (see <linux/landlock.h>). The designated initializer places each name
+ * at its bit index, so the lookup stays O(1) and does not depend on the entry
+ * order. log_blockers() adds the "fs."/"net."/"scope." category prefix.
+ */
+#define _LANDLOCK_NAME_ENTRY(mask, name) [BIT_INDEX(mask)] = name
+
+static const char *const fs_access_strings[] = { _LANDLOCK_ACCESS_FS_NAMES };
static_assert(ARRAY_SIZE(fs_access_strings) == LANDLOCK_NUM_ACCESS_FS);
-static const char *const net_access_strings[] = {
- [BIT_INDEX(LANDLOCK_ACCESS_NET_BIND_TCP)] = "net.bind_tcp",
- [BIT_INDEX(LANDLOCK_ACCESS_NET_CONNECT_TCP)] = "net.connect_tcp",
- [BIT_INDEX(LANDLOCK_ACCESS_NET_BIND_UDP)] = "net.bind_udp",
- [BIT_INDEX(LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP)] =
- "net.connect_send_udp",
-};
+static const char *const net_access_strings[] = { _LANDLOCK_ACCESS_NET_NAMES };
static_assert(ARRAY_SIZE(net_access_strings) == LANDLOCK_NUM_ACCESS_NET);
+static const char *const scope_strings[] = { _LANDLOCK_SCOPE_NAMES };
+
+static_assert(ARRAY_SIZE(scope_strings) == LANDLOCK_NUM_SCOPE);
+
+#undef _LANDLOCK_NAME_ENTRY
+
static __attribute_const__ const char *
get_blocker(const enum landlock_request_type type,
const unsigned long access_bit)
@@ -62,7 +53,7 @@ get_blocker(const enum landlock_request_type type,
case LANDLOCK_REQUEST_FS_CHANGE_TOPOLOGY:
WARN_ON_ONCE(access_bit != -1);
- return "fs.change_topology";
+ return "change_topology";
case LANDLOCK_REQUEST_FS_ACCESS:
if (WARN_ON_ONCE(access_bit >= ARRAY_SIZE(fs_access_strings)))
@@ -76,32 +67,63 @@ get_blocker(const enum landlock_request_type type,
case LANDLOCK_REQUEST_SCOPE_ABSTRACT_UNIX_SOCKET:
WARN_ON_ONCE(access_bit != -1);
- return "scope.abstract_unix_socket";
+ return scope_strings[BIT_INDEX(
+ LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET)];
case LANDLOCK_REQUEST_SCOPE_SIGNAL:
WARN_ON_ONCE(access_bit != -1);
- return "scope.signal";
+ return scope_strings[BIT_INDEX(LANDLOCK_SCOPE_SIGNAL)];
}
WARN_ON_ONCE(1);
return "unknown";
}
+/*
+ * Returns the audit category prefix prepended to the unprefixed blocker name
+ * returned by get_blocker() (filesystem and network access rights,
+ * change_topology, and scopes). The ptrace blocker is standalone and carries
+ * its full name in get_blocker(), so it uses no prefix.
+ */
+static __attribute_const__ const char *
+blocker_prefix(const enum landlock_request_type type)
+{
+ switch (type) {
+ case LANDLOCK_REQUEST_PTRACE:
+ return "";
+
+ case LANDLOCK_REQUEST_FS_CHANGE_TOPOLOGY:
+ case LANDLOCK_REQUEST_FS_ACCESS:
+ return "fs.";
+
+ case LANDLOCK_REQUEST_NET_ACCESS:
+ return "net.";
+
+ case LANDLOCK_REQUEST_SCOPE_ABSTRACT_UNIX_SOCKET:
+ case LANDLOCK_REQUEST_SCOPE_SIGNAL:
+ return "scope.";
+ }
+
+ WARN_ON_ONCE(1);
+ return "";
+}
+
static void log_blockers(struct audit_buffer *const ab,
const enum landlock_request_type type,
const access_mask_t access)
{
const unsigned long access_mask = access;
+ const char *const prefix = blocker_prefix(type);
unsigned long access_bit;
bool is_first = true;
for_each_set_bit(access_bit, &access_mask, BITS_PER_TYPE(access)) {
- audit_log_format(ab, "%s%s", is_first ? "" : ",",
+ audit_log_format(ab, "%s%s%s", is_first ? "" : ",", prefix,
get_blocker(type, access_bit));
is_first = false;
}
if (is_first)
- audit_log_format(ab, "%s", get_blocker(type, -1));
+ audit_log_format(ab, "%s%s", prefix, get_blocker(type, -1));
}
static void log_domain(struct landlock_hierarchy *const hierarchy)
--
2.54.0
^ permalink raw reply related
* [PATCH v3 02/20] landlock: Move domain query functions to domain.c
From: Mickaël Salaün @ 2026-07-22 17:11 UTC (permalink / raw)
To: Günther Noack, Steven Rostedt
Cc: Mickaël Salaün, Christian Brauner, Jann Horn, Jeff Xu,
Justin Suess, Kees Cook, Masami Hiramatsu, Mathieu Desnoyers,
Matthieu Buffet, Mikhail Ivanov, Tingmao Wang, kernel-team,
linux-security-module, linux-trace-kernel
In-Reply-To: <20260722171159.2776765-1-mic@digikod.net>
Grouping domain-specific code in one compilation unit reduces coupling
between domain and ruleset implementations.
Move the access-check functions that only operate on a domain (rule
lookup, layer unmasking, layer-mask init, access-mask union) from
ruleset.[ch] to domain.[ch]. They evaluate whether a domain grants a
requested access during the pathwalk and network checks and do not
modify the domain.
The merge and inherit chain stays in ruleset.c for now because it calls
the static create_ruleset() allocator; a following commit moves it once
the domain type switch eliminates that dependency.
No behavioral change. The functions move with unchanged signatures and
bodies.
Cc: Günther Noack <gnoack@google.com>
Cc: Tingmao Wang <m@maowtm.org>
Signed-off-by: Mickaël Salaün <mic@digikod.net>
---
Changes since v2:
https://patch.msgid.link/20260406143717.1815792-3-mic@digikod.net
- Adapt the moved access-check helpers to the base's struct layer_masks
(renamed from struct layer_access_masks), including the per-layer
quiet flag.
Changes since v1:
- New patch.
---
security/landlock/domain.c | 162 ++++++++++++++++++++++++++++++++++++
security/landlock/domain.h | 38 +++++++++
security/landlock/net.c | 1 +
security/landlock/ruleset.c | 150 ---------------------------------
security/landlock/ruleset.h | 38 ---------
5 files changed, 201 insertions(+), 188 deletions(-)
diff --git a/security/landlock/domain.c b/security/landlock/domain.c
index c5efb7635a7b..8fb74107c2ef 100644
--- a/security/landlock/domain.c
+++ b/security/landlock/domain.c
@@ -10,11 +10,15 @@
#include <kunit/test.h>
#include <linux/bitops.h>
#include <linux/bits.h>
+#include <linux/cleanup.h>
#include <linux/cred.h>
+#include <linux/err.h>
#include <linux/file.h>
#include <linux/mm.h>
+#include <linux/overflow.h>
#include <linux/path.h>
#include <linux/pid.h>
+#include <linux/rbtree.h>
#include <linux/refcount.h>
#include <linux/sched.h>
#include <linux/signal.h>
@@ -26,6 +30,7 @@
#include "common.h"
#include "domain.h"
#include "id.h"
+#include "limits.h"
#include "ruleset.h"
static void free_domain(struct landlock_domain *const domain)
@@ -59,6 +64,163 @@ void landlock_put_domain_deferred(struct landlock_domain *const domain)
}
}
+/* The returned access has the same lifetime as the domain. */
+const struct landlock_rule *
+landlock_find_rule(const struct landlock_ruleset *const ruleset,
+ const struct landlock_id id)
+{
+ const struct rb_root *root;
+ const struct rb_node *node;
+
+ root = landlock_get_rule_root((struct landlock_rules *)&ruleset->rules,
+ id.type);
+ if (IS_ERR(root))
+ return NULL;
+ node = root->rb_node;
+
+ while (node) {
+ struct landlock_rule *this =
+ rb_entry(node, struct landlock_rule, node);
+
+ if (this->key.data == id.key.data)
+ return this;
+ if (this->key.data < id.key.data)
+ node = node->rb_right;
+ else
+ node = node->rb_left;
+ }
+ return NULL;
+}
+
+/**
+ * landlock_unmask_layers - Remove the access rights in @masks which are
+ * granted in @rule
+ *
+ * Updates the set of (per-layer) unfulfilled access rights @masks so that all
+ * the access rights granted in @rule are removed from it (because they are now
+ * fulfilled).
+ *
+ * @rule: A rule that grants a set of access rights for each layer.
+ * @masks: A matrix of unfulfilled access rights for each layer.
+ *
+ * Return: True if the request is allowed (i.e. the access rights granted all
+ * remaining unfulfilled access rights and masks has no leftover set bits).
+ */
+bool landlock_unmask_layers(const struct landlock_rule *const rule,
+ struct layer_masks *masks)
+{
+ if (!masks)
+ return true;
+ if (!rule)
+ return false;
+
+ /*
+ * An access is granted if, for each policy layer, at least one rule
+ * encountered on the pathwalk grants the requested access, regardless
+ * of its position in the layer stack. We must then check the remaining
+ * layers for each inode, from the first added layer to the last one.
+ * When there are multiple requested accesses, for each policy layer,
+ * the full set of requested accesses may not be granted by only one
+ * rule, but by the union (binary OR) of multiple rules. For example,
+ * /a/b <execute> + /a <read> grants /a/b <execute + read>.
+ *
+ * This function is called once per matching rule during the pathwalk,
+ * progressively clearing bits in @masks. The overall access decision
+ * is per-layer: access is granted iff masks->layers[l].access == 0 for
+ * all layers l. When two independent mechanisms can each grant access
+ * within a layer (e.g. a path rule OR a scope exception), the
+ * composition must evaluate per-layer: FOR-ALL l (A(l) OR B(l)), not
+ * (FOR-ALL l A(l)) OR (FOR-ALL l B(l)), to prevent bypass when
+ * different layers grant via different mechanisms.
+ */
+ for (size_t i = 0; i < rule->num_layers; i++) {
+ const struct landlock_layer *const layer = &rule->layers[i];
+
+ /* Clear the bits where the layer in the rule grants access. */
+ masks->layers[layer->level - 1].access &= ~layer->access;
+
+#ifdef CONFIG_AUDIT
+ /* Collect rule flags for each layer. */
+ if (layer->flags.quiet)
+ masks->layers[layer->level - 1].quiet = true;
+#endif /* CONFIG_AUDIT */
+ }
+
+ for (size_t i = 0; i < ARRAY_SIZE(masks->layers); i++) {
+ if (masks->layers[i].access)
+ return false;
+ }
+ return true;
+}
+
+typedef access_mask_t
+get_access_mask_t(const struct landlock_ruleset *const ruleset,
+ const u16 layer_level);
+
+/**
+ * landlock_init_layer_masks - Initialize layer masks from an access request
+ *
+ * Populates @masks such that for each access right in @access_request, the bits
+ * for all the layers are set where this access right is handled. Rule flags
+ * are also zeroed.
+ *
+ * @domain: The domain that defines the current restrictions.
+ * @access_request: The requested access rights to check.
+ * @masks: Layer access masks to populate.
+ * @key_type: The key type to switch between access masks of different types.
+ *
+ * Return: An access mask where each access right bit is set which is handled in
+ * any of the active layers in @domain.
+ */
+access_mask_t
+landlock_init_layer_masks(const struct landlock_ruleset *const domain,
+ const access_mask_t access_request,
+ struct layer_masks *const masks,
+ const enum landlock_key_type key_type)
+{
+ access_mask_t handled_accesses = 0;
+ get_access_mask_t *get_access_mask;
+
+ switch (key_type) {
+ case LANDLOCK_KEY_INODE:
+ get_access_mask = landlock_get_fs_access_mask;
+ break;
+
+#if IS_ENABLED(CONFIG_INET)
+ case LANDLOCK_KEY_NET_PORT:
+ get_access_mask = landlock_get_net_access_mask;
+ break;
+#endif /* IS_ENABLED(CONFIG_INET) */
+
+ default:
+ WARN_ON_ONCE(1);
+ return 0;
+ }
+
+ /* An empty access request can happen because of O_WRONLY | O_RDWR. */
+ if (!access_request)
+ return 0;
+
+ for (size_t i = 0; i < domain->num_layers; i++) {
+ const access_mask_t handled = get_access_mask(domain, i);
+
+ masks->layers[i].access = access_request & handled;
+ handled_accesses |= masks->layers[i].access;
+#ifdef CONFIG_AUDIT
+ masks->layers[i].quiet = false;
+#endif /* CONFIG_AUDIT */
+ }
+ for (size_t i = domain->num_layers; i < ARRAY_SIZE(masks->layers);
+ i++) {
+ masks->layers[i].access = 0;
+#ifdef CONFIG_AUDIT
+ masks->layers[i].quiet = false;
+#endif /* CONFIG_AUDIT */
+ }
+
+ return handled_accesses;
+}
+
#ifdef CONFIG_AUDIT
/**
diff --git a/security/landlock/domain.h b/security/landlock/domain.h
index bcfd0452e260..9f9b892ab17d 100644
--- a/security/landlock/domain.h
+++ b/security/landlock/domain.h
@@ -233,12 +233,50 @@ struct landlock_domain {
};
};
+/**
+ * landlock_union_access_masks - Return all access rights handled in the
+ * domain
+ *
+ * @domain: Landlock ruleset (used as a domain)
+ *
+ * Return: An access_masks result of the OR of all the domain's access masks.
+ */
+static inline struct access_masks
+landlock_union_access_masks(const struct landlock_ruleset *const domain)
+{
+ union access_masks_all matches = {};
+ size_t layer_level;
+
+ for (layer_level = 0; layer_level < domain->num_layers; layer_level++) {
+ union access_masks_all layer = {
+ .masks = domain->access_masks[layer_level],
+ };
+
+ matches.all |= layer.all;
+ }
+
+ return matches.masks;
+}
+
void landlock_put_domain(struct landlock_domain *const domain);
void landlock_put_domain_deferred(struct landlock_domain *const domain);
DEFINE_FREE(landlock_put_domain, struct landlock_domain *,
if (!IS_ERR_OR_NULL(_T)) landlock_put_domain(_T))
+const struct landlock_rule *
+landlock_find_rule(const struct landlock_ruleset *const ruleset,
+ const struct landlock_id id);
+
+bool landlock_unmask_layers(const struct landlock_rule *const rule,
+ struct layer_masks *masks);
+
+access_mask_t
+landlock_init_layer_masks(const struct landlock_ruleset *const domain,
+ const access_mask_t access_request,
+ struct layer_masks *masks,
+ const enum landlock_key_type key_type);
+
static inline void landlock_get_domain(struct landlock_domain *const domain)
{
if (domain)
diff --git a/security/landlock/net.c b/security/landlock/net.c
index 46c17116fcf4..feecc8c77ab4 100644
--- a/security/landlock/net.c
+++ b/security/landlock/net.c
@@ -15,6 +15,7 @@
#include "audit.h"
#include "common.h"
#include "cred.h"
+#include "domain.h"
#include "limits.h"
#include "net.h"
#include "ruleset.h"
diff --git a/security/landlock/ruleset.c b/security/landlock/ruleset.c
index 9e560aed64b6..96e65804d2ff 100644
--- a/security/landlock/ruleset.c
+++ b/security/landlock/ruleset.c
@@ -591,153 +591,3 @@ landlock_merge_ruleset(struct landlock_ruleset *const parent,
return no_free_ptr(new_dom);
}
-
-/*
- * The returned access has the same lifetime as @ruleset.
- */
-const struct landlock_rule *
-landlock_find_rule(const struct landlock_ruleset *const ruleset,
- const struct landlock_id id)
-{
- const struct rb_root *root;
- const struct rb_node *node;
-
- root = landlock_get_rule_root((struct landlock_rules *)&ruleset->rules,
- id.type);
- if (IS_ERR(root))
- return NULL;
- node = root->rb_node;
-
- while (node) {
- struct landlock_rule *this =
- rb_entry(node, struct landlock_rule, node);
-
- if (this->key.data == id.key.data)
- return this;
- if (this->key.data < id.key.data)
- node = node->rb_right;
- else
- node = node->rb_left;
- }
- return NULL;
-}
-
-/**
- * landlock_unmask_layers - Remove the access rights in @masks
- * which are granted in @rule
- *
- * Updates the set of (per-layer) unfulfilled access rights @masks
- * so that all the access rights granted in @rule are removed from it
- * (because they are now fulfilled).
- *
- * @rule: A rule that grants a set of access rights for each layer
- * @masks: A matrix of unfulfilled access rights for each layer
- *
- * Return: True if the request is allowed (i.e. the access rights granted all
- * remaining unfulfilled access rights and masks has no leftover set bits).
- */
-bool landlock_unmask_layers(const struct landlock_rule *const rule,
- struct layer_masks *masks)
-{
- if (!masks)
- return true;
- if (!rule)
- return false;
-
- /*
- * An access is granted if, for each policy layer, at least one rule
- * encountered on the pathwalk grants the requested access,
- * regardless of its position in the layer stack. We must then check
- * the remaining layers for each inode, from the first added layer to
- * the last one. When there is multiple requested accesses, for each
- * policy layer, the full set of requested accesses may not be granted
- * by only one rule, but by the union (binary OR) of multiple rules.
- * E.g. /a/b <execute> + /a <read> => /a/b <execute + read>
- */
- for (size_t i = 0; i < rule->num_layers; i++) {
- const struct landlock_layer *const layer = &rule->layers[i];
-
- /* Clear the bits where the layer in the rule grants access. */
- masks->layers[layer->level - 1].access &= ~layer->access;
-
-#ifdef CONFIG_AUDIT
- /* Collect rule flags for each layer. */
- if (layer->flags.quiet)
- masks->layers[layer->level - 1].quiet = true;
-#endif /* CONFIG_AUDIT */
- }
-
- for (size_t i = 0; i < ARRAY_SIZE(masks->layers); i++) {
- if (masks->layers[i].access)
- return false;
- }
- return true;
-}
-
-typedef access_mask_t
-get_access_mask_t(const struct landlock_ruleset *const ruleset,
- const u16 layer_level);
-
-/**
- * landlock_init_layer_masks - Initialize layer masks from an access request
- *
- * Populates @masks such that for each access right in @access_request, the bits
- * for all the layers are set where this access right is handled. Rule flags
- * are also zeroed.
- *
- * @domain: The domain that defines the current restrictions.
- * @access_request: The requested access rights to check.
- * @masks: Layer access masks to populate.
- * @key_type: The key type to switch between access masks of different types.
- *
- * Return: An access mask where each access right bit is set which is handled
- * in any of the active layers in @domain.
- */
-access_mask_t
-landlock_init_layer_masks(const struct landlock_ruleset *const domain,
- const access_mask_t access_request,
- struct layer_masks *const masks,
- const enum landlock_key_type key_type)
-{
- access_mask_t handled_accesses = 0;
- get_access_mask_t *get_access_mask;
-
- switch (key_type) {
- case LANDLOCK_KEY_INODE:
- get_access_mask = landlock_get_fs_access_mask;
- break;
-
-#if IS_ENABLED(CONFIG_INET)
- case LANDLOCK_KEY_NET_PORT:
- get_access_mask = landlock_get_net_access_mask;
- break;
-#endif /* IS_ENABLED(CONFIG_INET) */
-
- default:
- WARN_ON_ONCE(1);
- return 0;
- }
-
- /* An empty access request can happen because of O_WRONLY | O_RDWR. */
- if (!access_request)
- return 0;
-
- for (size_t i = 0; i < domain->num_layers; i++) {
- const access_mask_t handled = get_access_mask(domain, i);
-
- masks->layers[i].access = access_request & handled;
- handled_accesses |= masks->layers[i].access;
-#ifdef CONFIG_AUDIT
- masks->layers[i].quiet = false;
-#endif /* CONFIG_AUDIT */
- }
- for (size_t i = domain->num_layers; i < ARRAY_SIZE(masks->layers);
- i++) {
- masks->layers[i].access = 0;
-#ifdef CONFIG_AUDIT
- masks->layers[i].quiet = false;
-#endif /* CONFIG_AUDIT */
- }
-
- return handled_accesses;
-}
diff --git a/security/landlock/ruleset.h b/security/landlock/ruleset.h
index c8c83efff4c5..f9486c06b7ec 100644
--- a/security/landlock/ruleset.h
+++ b/security/landlock/ruleset.h
@@ -235,10 +235,6 @@ struct landlock_ruleset *
landlock_merge_ruleset(struct landlock_ruleset *const parent,
struct landlock_ruleset *const ruleset);
-const struct landlock_rule *
-landlock_find_rule(const struct landlock_ruleset *const ruleset,
- const struct landlock_id id);
-
/**
* landlock_get_rule_root - Get the root of a rule tree by key type
*
@@ -272,31 +268,6 @@ static inline void landlock_get_ruleset(struct landlock_ruleset *const ruleset)
refcount_inc(&ruleset->usage);
}
-/**
- * landlock_union_access_masks - Return all access rights handled in the
- * domain
- *
- * @domain: Landlock ruleset (used as a domain)
- *
- * Return: An access_masks result of the OR of all the domain's access masks.
- */
-static inline struct access_masks
-landlock_union_access_masks(const struct landlock_ruleset *const domain)
-{
- union access_masks_all matches = {};
- size_t layer_level;
-
- for (layer_level = 0; layer_level < domain->num_layers; layer_level++) {
- union access_masks_all layer = {
- .masks = domain->access_masks[layer_level],
- };
-
- matches.all |= layer.all;
- }
-
- return matches.masks;
-}
-
static inline void
landlock_add_fs_access_mask(struct landlock_ruleset *const ruleset,
const access_mask_t fs_access_mask,
@@ -355,13 +326,4 @@ landlock_get_scope_mask(const struct landlock_ruleset *const ruleset,
return ruleset->access_masks[layer_level].scope;
}
-bool landlock_unmask_layers(const struct landlock_rule *const rule,
- struct layer_masks *masks);
-
-access_mask_t
-landlock_init_layer_masks(const struct landlock_ruleset *const domain,
- const access_mask_t access_request,
- struct layer_masks *masks,
- const enum landlock_key_type key_type);
-
#endif /* _SECURITY_LANDLOCK_RULESET_H */
--
2.54.0
^ permalink raw reply related
* [PATCH v3 08/20] landlock: Add create_ruleset and free_ruleset tracepoints
From: Mickaël Salaün @ 2026-07-22 17:11 UTC (permalink / raw)
To: Günther Noack, Steven Rostedt
Cc: Mickaël Salaün, Christian Brauner, Jann Horn, Jeff Xu,
Justin Suess, Kees Cook, Masami Hiramatsu, Mathieu Desnoyers,
Matthieu Buffet, Mikhail Ivanov, Tingmao Wang, kernel-team,
linux-security-module, linux-trace-kernel
In-Reply-To: <20260722171159.2776765-1-mic@digikod.net>
Add the first Landlock tracepoints, for ruleset lifecycle:
landlock_create_ruleset fires from the landlock_create_ruleset() syscall
handler, and landlock_free_ruleset fires in free_ruleset() before the
ruleset is freed.
These tracepoints, and the ones added by the following commits, share a
common design. Rather than one polymorphic event distinguished by a
status field (as audit uses a shared record type with a "status="
field), each lifecycle transition and denial type gets its own event
with a type-safe TP_PROTO, giving precise ftrace filtering by event name
and type-safe eBPF access. TP_PROTO passes the object pointer and the
fields are read from it in TP_fast_assign, so an eBPF program reads the
full object state (rules, access masks, hierarchy) via BTF from a single
pointer rather than from the flattened TP_STRUCT__entry fields. The
whole cost is paid only when a tracer is attached; the static branch is
not taken otherwise. Trace fields carry the bare access-right and scope
names (read_file), reusing the audit name tables; audit prepends the
category (fs.read_file), which the trace event name already conveys.
The trace header's DOC comment documents the consistency and locking
guarantees these events share.
create_ruleset needs no lock because the ruleset is not yet shared (its
file descriptor is not yet installed). The deallocation events use the
"free_" prefix, not "drop_", because they fire when the object is
actually freed.
Add trace.c, built for CONFIG_TRACEPOINTS, which defines
CREATE_TRACE_POINTS, and extend CONFIG_SECURITY_LANDLOCK_LOG to also be
selected by CONFIG_TRACEPOINTS so the common log framework is available
to a tracepoints-only build.
Add an id field to struct landlock_ruleset, gated on CONFIG_TRACEPOINTS
and assigned from landlock_get_id_range() at creation. Only the
tracepoints consume it (audit identifies domains, not rulesets), so it
does not exist in an audit-only build. The Landlock ID is a stable u64
that names the ruleset across the trace stream and uses the same scheme
as audit, so a ruleset can be correlated between trace and audit
records.
Cc: Günther Noack <gnoack@google.com>
Cc: Justin Suess <utilityemal77@gmail.com>
Cc: Masami Hiramatsu <mhiramat@kernel.org>
Cc: Mathieu Desnoyers <mathieu.desnoyers@efficios.com>
Cc: Steven Rostedt <rostedt@goodmis.org>
Cc: Tingmao Wang <m@maowtm.org>
Signed-off-by: Mickaël Salaün <mic@digikod.net>
---
Changes since v2:
https://patch.msgid.link/20260406143717.1815792-7-mic@digikod.net
- Reformatted TP_STRUCT__entry and TP_fast_assign to kernel tracing
convention (requested by Steven Rostedt).
- Render the handled access masks as symbolic names with
__print_flags(), shared with the audit blocker names.
- Move the id.h declaration guard widening (landlock_init_id,
landlock_get_id_range) to the earlier commit that builds id.o under
CONFIG_SECURITY_LANDLOCK_LOG, so intermediate patches build without
CONFIG_AUDIT.
- Refine the trace-event consistency-guarantee DOC comment (observable
from user space, balanced creation and deallocation events) and
document why create_ruleset emits before the file descriptor is
installed.
- Define CREATE_TRACE_POINTS in a dedicated trace.c built for
CONFIG_TRACEPOINTS, and extend CONFIG_SECURITY_LANDLOCK_LOG to be
selected by CONFIG_TRACEPOINTS here (the first tracepoint consumer),
rather than placing CREATE_TRACE_POINTS in the audit and log
translation unit.
- Gate the struct landlock_ruleset id field on CONFIG_TRACEPOINTS (only
tracing uses it) instead of CONFIG_SECURITY_LANDLOCK_LOG.
Changes since v1:
- New patch (split from the v1 add_rule_fs tracepoint patch).
---
MAINTAINERS | 1 +
include/linux/landlock.h | 1 +
include/trace/events/landlock.h | 139 ++++++++++++++++++++++++++++++++
security/landlock/Kconfig | 2 +-
security/landlock/Makefile | 2 +
security/landlock/ruleset.c | 8 ++
security/landlock/ruleset.h | 9 +++
security/landlock/syscalls.c | 11 +++
security/landlock/trace.c | 17 ++++
9 files changed, 189 insertions(+), 1 deletion(-)
create mode 100644 include/trace/events/landlock.h
create mode 100644 security/landlock/trace.c
diff --git a/MAINTAINERS b/MAINTAINERS
index 91de7e3c2836..ca41aec9993c 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -14599,6 +14599,7 @@ F: Documentation/security/landlock.rst
F: Documentation/userspace-api/landlock.rst
F: fs/ioctl.c
F: include/linux/landlock.h
+F: include/trace/events/landlock.h
F: include/uapi/linux/landlock.h
F: samples/landlock/
F: security/landlock/
diff --git a/include/linux/landlock.h b/include/linux/landlock.h
index cabe6c784833..004cbd0b9298 100644
--- a/include/linux/landlock.h
+++ b/include/linux/landlock.h
@@ -9,6 +9,7 @@
#ifndef _LINUX_LANDLOCK_H
#define _LINUX_LANDLOCK_H
+#include <linux/types.h>
#include <uapi/linux/landlock.h>
/*
diff --git a/include/trace/events/landlock.h b/include/trace/events/landlock.h
new file mode 100644
index 000000000000..2fb717055cc8
--- /dev/null
+++ b/include/trace/events/landlock.h
@@ -0,0 +1,139 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Copyright © 2025 Microsoft Corporation
+ * Copyright © 2026 Cloudflare, Inc.
+ */
+
+#undef TRACE_SYSTEM
+#define TRACE_SYSTEM landlock
+
+#if !defined(_TRACE_LANDLOCK_H) || defined(TRACE_HEADER_MULTI_READ)
+#define _TRACE_LANDLOCK_H
+
+#include <linux/landlock.h>
+#include <linux/tracepoint.h>
+
+struct landlock_ruleset;
+
+/* Maps a shared _LANDLOCK_*_NAMES entry to a __print_flags() pair. */
+#define _LANDLOCK_NAME_ENTRY(mask, name) { mask, name }
+
+/**
+ * DOC: Landlock trace events
+ *
+ * These guarantees and constraints hold for every Landlock tracepoint.
+ * A new tracepoint must uphold them, and an eBPF consumer can rely on
+ * them.
+ *
+ * Lifecycle consistency
+ * ~~~~~~~~~~~~~~~~~~~~~~
+ *
+ * Lifecycle events are balanced: a creation event always has a matching
+ * deallocation event and vice versa, so an eBPF program can model object
+ * lifetimes from the trace stream without reconciliation logic. A creation
+ * event fires while the object is still private to the calling thread
+ * (landlock_create_ruleset fires before the ruleset's file descriptor is
+ * installed, so it cannot race a concurrent :manpage:`close(2)`); if fd
+ * installation later fails and the ruleset is freed, free_ruleset still
+ * fires, keeping the pair balanced. The domain pair (create_domain and
+ * free_domain) is balanced the same way: create_domain fires when the
+ * domain is created (under the ruleset lock, before thread-sync), and
+ * free_domain fires when it is freed. A rare thread-sync failure aborts
+ * the just-created domain, which then emits both events (its creation, then
+ * an immediate free). Denial events fire only for denials that actually
+ * happen.
+ *
+ * Pointer access
+ * ~~~~~~~~~~~~~~
+ *
+ * All pointer arguments in TP_PROTO are guaranteed non-NULL by the
+ * caller, but pointers reached through them may still be NULL (e.g.,
+ * hierarchy->parent at a root domain) and must be checked. eBPF programs
+ * read these pointers via BTF for richer introspection than the
+ * TP_STRUCT__entry fields, which serve TP_printk display only.
+ *
+ * Mutable object pointers are passed while the caller holds the object's
+ * lock, so TP_fast_assign and a BTF reader see the exact object the event
+ * reports, a snapshot no concurrent writer can change: add_rule holds the
+ * modified ruleset's lock, and create_domain holds the ruleset lock across
+ * the emission (before the thread-sync wait) so the inspected ruleset is
+ * the one merged into the domain. Objects immutable at the emission site
+ * (a domain after creation, a hierarchy at its last reference) need no
+ * lock. A few values that no held lock protects are a best-effort
+ * lockless snapshot instead: a task's comm, and the deny_access_net struct
+ * sock (whose network hook holds no socket lock), matching how the sched
+ * and signal trace events sample comm.
+ */
+
+/**
+ * landlock_create_ruleset - New ruleset created
+ *
+ * @ruleset: Newly created ruleset (never NULL); not yet shared via an fd,
+ * so no lock is needed.
+ *
+ * Emitted by sys_landlock_create_ruleset() while the new ruleset is still
+ * private to the calling thread, before its file descriptor is installed,
+ * so it cannot race a concurrent :manpage:`close(2)`. Balanced by a
+ * matching landlock_free_ruleset event.
+ */
+TRACE_EVENT(landlock_create_ruleset,
+
+ TP_PROTO(const struct landlock_ruleset *ruleset),
+
+ TP_ARGS(ruleset),
+
+ TP_STRUCT__entry(
+ __field( __u64, ruleset_id )
+ __field( access_mask_t, handled_fs )
+ __field( access_mask_t, handled_net )
+ __field( access_mask_t, scoped )
+ ),
+
+ TP_fast_assign(
+ __entry->ruleset_id = ruleset->id;
+ __entry->handled_fs = ruleset->handled_masks.fs;
+ __entry->handled_net = ruleset->handled_masks.net;
+ __entry->scoped = ruleset->handled_masks.scope;
+ ),
+
+ TP_printk("ruleset=%llx handled_fs=%s handled_net=%s scoped=%s",
+ __entry->ruleset_id,
+ __print_flags(__entry->handled_fs, "|", _LANDLOCK_ACCESS_FS_NAMES),
+ __print_flags(__entry->handled_net, "|", _LANDLOCK_ACCESS_NET_NAMES),
+ __print_flags(__entry->scoped, "|", _LANDLOCK_SCOPE_NAMES))
+);
+
+/**
+ * landlock_free_ruleset - Ruleset freed
+ *
+ * @ruleset: Ruleset being freed (never NULL); at its last reference, so no
+ * lock is needed.
+ *
+ * Emitted when a ruleset's last reference is dropped (typically when
+ * the creating process closes the ruleset file descriptor). Fires even
+ * when file-descriptor installation failed after creation, keeping the
+ * create/free pair balanced.
+ */
+TRACE_EVENT(landlock_free_ruleset,
+
+ TP_PROTO(const struct landlock_ruleset *ruleset),
+
+ TP_ARGS(ruleset),
+
+ TP_STRUCT__entry(
+ __field( __u64, ruleset_id )
+ ),
+
+ TP_fast_assign(
+ __entry->ruleset_id = ruleset->id;
+ ),
+
+ TP_printk("ruleset=%llx", __entry->ruleset_id)
+);
+
+#undef _LANDLOCK_NAME_ENTRY
+
+#endif /* _TRACE_LANDLOCK_H */
+
+/* This part must be outside protection */
+#include <trace/define_trace.h>
diff --git a/security/landlock/Kconfig b/security/landlock/Kconfig
index b4e7f0e9ba9b..7aeac29160e8 100644
--- a/security/landlock/Kconfig
+++ b/security/landlock/Kconfig
@@ -24,7 +24,7 @@ config SECURITY_LANDLOCK
config SECURITY_LANDLOCK_LOG
bool
depends on SECURITY_LANDLOCK
- default y if AUDIT
+ default y if AUDIT || TRACEPOINTS
config SECURITY_LANDLOCK_KUNIT_TEST
bool "KUnit tests for Landlock" if !KUNIT_ALL_TESTS
diff --git a/security/landlock/Makefile b/security/landlock/Makefile
index 2462e6c5921e..2711f4876939 100644
--- a/security/landlock/Makefile
+++ b/security/landlock/Makefile
@@ -18,3 +18,5 @@ landlock-$(CONFIG_SECURITY_LANDLOCK_LOG) += \
log.o
landlock-$(CONFIG_AUDIT) += audit.o
+
+landlock-$(CONFIG_TRACEPOINTS) += trace.o
diff --git a/security/landlock/ruleset.c b/security/landlock/ruleset.c
index 13edb77f07a0..3bfee53177d8 100644
--- a/security/landlock/ruleset.c
+++ b/security/landlock/ruleset.c
@@ -23,10 +23,13 @@
#include <uapi/linux/landlock.h>
#include "access.h"
+#include "id.h"
#include "limits.h"
#include "object.h"
#include "ruleset.h"
+#include <trace/events/landlock.h>
+
struct landlock_ruleset *
landlock_create_ruleset(const access_mask_t fs_access_mask,
const access_mask_t net_access_mask,
@@ -50,6 +53,10 @@ landlock_create_ruleset(const access_mask_t fs_access_mask,
new_ruleset->rules.root_net_port = RB_ROOT;
#endif /* IS_ENABLED(CONFIG_INET) */
+#ifdef CONFIG_TRACEPOINTS
+ new_ruleset->id = landlock_get_id_range(1);
+#endif /* CONFIG_TRACEPOINTS */
+
/* Should already be checked in landlock_create_ruleset(). */
if (fs_access_mask) {
const access_mask_t mask = fs_access_mask &
@@ -325,6 +332,7 @@ void landlock_free_rules(struct landlock_rules *const rules)
static void free_ruleset(struct landlock_ruleset *const ruleset)
{
might_sleep();
+ trace_landlock_free_ruleset(ruleset);
landlock_free_rules(&ruleset->rules);
kfree(ruleset);
}
diff --git a/security/landlock/ruleset.h b/security/landlock/ruleset.h
index da6a12a8e066..ca1fd5f4c417 100644
--- a/security/landlock/ruleset.h
+++ b/security/landlock/ruleset.h
@@ -4,6 +4,7 @@
*
* Copyright © 2016-2020 Mickaël Salaün <mic@digikod.net>
* Copyright © 2018-2020 ANSSI
+ * Copyright © 2026 Cloudflare, Inc.
*/
#ifndef _SECURITY_LANDLOCK_RULESET_H
@@ -164,6 +165,14 @@ struct landlock_ruleset {
* @usage: Number of file descriptors referencing this ruleset.
*/
refcount_t usage;
+
+#ifdef CONFIG_TRACEPOINTS
+ /**
+ * @id: Unique identifier for this ruleset, used for tracing.
+ */
+ u64 id;
+#endif /* CONFIG_TRACEPOINTS */
+
/**
* @quiet_masks: Stores the quiet flags for an unmerged ruleset. For a
* merged domain, this is stored in each layer's struct
diff --git a/security/landlock/syscalls.c b/security/landlock/syscalls.c
index 0ad20a6f9564..dbc4facf00b6 100644
--- a/security/landlock/syscalls.c
+++ b/security/landlock/syscalls.c
@@ -38,6 +38,8 @@
#include "setup.h"
#include "tsync.h"
+#include <trace/events/landlock.h>
+
static bool is_initialized(void)
{
if (likely(landlock_initialized))
@@ -281,6 +283,15 @@ SYSCALL_DEFINE3(landlock_create_ruleset,
ruleset->quiet_masks.net = ruleset_attr.quiet_access_net;
ruleset->quiet_masks.scope = ruleset_attr.quiet_scoped;
+ /*
+ * Emits before anon_inode_getfd() installs the file descriptor, while
+ * the ruleset is still private to this thread: no lock is needed, and
+ * the event cannot race a concurrent close() freeing the ruleset under
+ * the tracepoint's BTF read. This is the last point at which the
+ * ruleset is guaranteed alive and unshared.
+ */
+ trace_landlock_create_ruleset(ruleset);
+
/* Creates anonymous FD referring to the ruleset. */
ruleset_fd = anon_inode_getfd("[landlock-ruleset]", &ruleset_fops,
ruleset, O_RDWR | O_CLOEXEC);
diff --git a/security/landlock/trace.c b/security/landlock/trace.c
new file mode 100644
index 000000000000..aeb6eeebe42a
--- /dev/null
+++ b/security/landlock/trace.c
@@ -0,0 +1,17 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Landlock - Tracepoint helpers
+ *
+ * Copyright © 2025 Microsoft Corporation
+ * Copyright © 2026 Cloudflare, Inc.
+ */
+
+#include "ruleset.h"
+
+/*
+ * Generates the tracepoint definitions in this translation unit. The trace
+ * event header dereferences the traced objects in TP_fast_assign, so the full
+ * struct definitions (e.g. ruleset.h) must be included before it.
+ */
+#define CREATE_TRACE_POINTS
+#include <trace/events/landlock.h>
--
2.54.0
^ permalink raw reply related
* [PATCH v3 05/20] landlock: Decouple the per-denial logging decision from CONFIG_AUDIT
From: Mickaël Salaün @ 2026-07-22 17:11 UTC (permalink / raw)
To: Günther Noack, Steven Rostedt
Cc: Mickaël Salaün, Christian Brauner, Jann Horn, Jeff Xu,
Justin Suess, Kees Cook, Masami Hiramatsu, Mathieu Desnoyers,
Matthieu Buffet, Mikhail Ivanov, Tingmao Wang, kernel-team,
linux-security-module, linux-trace-kernel
In-Reply-To: <20260722171159.2776765-1-mic@digikod.net>
Until now, whether a denial is logged was decided inside
landlock_audit_denial(): a per-execution flag check (log_same_exec or
log_new_exec, selected by the credential's domain_exec bitmask),
preceded by a LANDLOCK_LOG_DISABLED early return in
landlock_log_denial() for domains an ancestor fully quieted.
Factor that decision into a single is_denial_logged() helper called once
by landlock_log_denial(), and pass its result to landlock_audit_denial()
as a "logged" boolean. A following commit passes the same boolean to
the deny tracepoints, so audit and tracing share one decision that stays
correct as new log state is added, and a tracepoints-only build
(CONFIG_AUDIT=n) computes it identically. Computing the logged verdict
once in the shared helper makes audit and tracing apply identical
filtering, so they cannot report different logged= values for the same
denial as log controls grow.
Move the LANDLOCK_LOG_DISABLED gate out of landlock_log_denial() into
the decision so num_denials counts every denial, including those a
domain quiets. This was previously masked: the only reader of
num_denials is the audit "domain deallocated" record, emitted only for
domains that reached LANDLOCK_LOG_RECORDED; a fully quieted domain never
records, so its undercount was never observable. A following commit
adds a free_domain tracepoint that reports num_denials, which needs the
full count.
This is not a functional change for audit: the logged decision and the
audit_enabled gate are preserved, so the emitted records are identical.
Cc: Günther Noack <gnoack@google.com>
Cc: Tingmao Wang <m@maowtm.org>
Signed-off-by: Mickaël Salaün <mic@digikod.net>
---
Changes since v2:
- New patch.
---
security/landlock/audit.c | 90 ++++-----------------------------------
security/landlock/audit.h | 14 ++----
security/landlock/log.c | 88 ++++++++++++++++++++++++++++++++++++--
3 files changed, 97 insertions(+), 95 deletions(-)
diff --git a/security/landlock/audit.c b/security/landlock/audit.c
index dfa1e5a64aac..32260e7cbfe9 100644
--- a/security/landlock/audit.c
+++ b/security/landlock/audit.c
@@ -136,104 +136,32 @@ static void log_domain(struct landlock_hierarchy *const hierarchy)
WRITE_ONCE(hierarchy->log_status, LANDLOCK_LOG_RECORDED);
}
-static access_mask_t
-pick_access_mask_for_request_type(const enum landlock_request_type type,
- const struct access_masks access_masks)
-{
- switch (type) {
- case LANDLOCK_REQUEST_FS_ACCESS:
- return access_masks.fs;
- case LANDLOCK_REQUEST_NET_ACCESS:
- return access_masks.net;
- default:
- WARN_ONCE(1, "Invalid request type %d passed to %s", type,
- __func__);
- return 0;
- }
-}
-
/**
* landlock_audit_denial - Create an audit record for a denied access request
*
- * @subject: The Landlock subject's credential denying an action.
* @request: Detail of the user space request.
* @youngest_denied: The youngest hierarchy node that denied the access.
- * @youngest_layer: The layer index of @youngest_denied.
* @missing: The set of denied access rights.
- * @object_quiet_flag: Whether the object denied by @youngest_denied is
- * covered by a quiet rule in that layer.
+ * @logged: Whether the denial is selected for logging, as computed by
+ * landlock_log_denial() (domain policy and quiet rules).
*
- * Called from landlock_log_denial() with the same arguments.
+ * Emits the record when audit is enabled and the denial is selected for
+ * logging.
*/
-void landlock_audit_denial(const struct landlock_cred_security *const subject,
- const struct landlock_request *const request,
+void landlock_audit_denial(const struct landlock_request *const request,
struct landlock_hierarchy *const youngest_denied,
- const size_t youngest_layer,
- const access_mask_t missing,
- const bool object_quiet_flag)
+ const access_mask_t missing, const bool logged)
{
struct audit_buffer *ab;
- bool quiet_applicable_to_access = false;
if (!audit_enabled)
return;
- /* Checks if the current exec was restricting itself. */
- if (subject->domain_exec & BIT(youngest_layer)) {
- /* Ignores denials for the same execution. */
- if (!youngest_denied->log_same_exec)
- return;
- } else {
- /* Ignores denials after a new execution. */
- if (!youngest_denied->log_new_exec)
- return;
- }
-
/*
- * Checks if the object is marked quiet by the layer that denied the
- * request. If it's a different layer that marked it as quiet, but that
- * layer is not the one that denied the request, we should still audit
- * log the denial.
+ * Skips denials the domain's policy or a quiet rule excludes from
+ * logging (folded into @logged by landlock_log_denial()).
*/
- if (object_quiet_flag) {
- /*
- * We now check if the denied requests are all covered by the
- * layer's quiet access bits.
- */
- const access_mask_t quiet_mask =
- pick_access_mask_for_request_type(
- request->type, youngest_denied->quiet_masks);
-
- quiet_applicable_to_access = (quiet_mask & missing) == missing;
- } else {
- /*
- * Either the object is not quiet, or this is a scope request.
- * We check request->type to distinguish between the two cases.
- */
- const access_mask_t quiet_mask =
- youngest_denied->quiet_masks.scope;
-
- switch (request->type) {
- case LANDLOCK_REQUEST_SCOPE_SIGNAL:
- quiet_applicable_to_access =
- !!(quiet_mask & LANDLOCK_SCOPE_SIGNAL);
- break;
- case LANDLOCK_REQUEST_SCOPE_ABSTRACT_UNIX_SOCKET:
- quiet_applicable_to_access =
- !!(quiet_mask &
- LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET);
- break;
- /*
- * Leave LANDLOCK_REQUEST_PTRACE and
- * LANDLOCK_REQUEST_FS_CHANGE_TOPOLOGY unhandled for now - they
- * are never quiet.
- */
- default:
- break;
- }
- }
-
- if (quiet_applicable_to_access)
+ if (!logged)
return;
/* Uses consistent allocation flags wrt common_lsm_audit(). */
diff --git a/security/landlock/audit.h b/security/landlock/audit.h
index 1b36bb3c6cfd..14a514065e08 100644
--- a/security/landlock/audit.h
+++ b/security/landlock/audit.h
@@ -12,18 +12,14 @@
#include "access.h"
-struct landlock_cred_security;
struct landlock_hierarchy;
struct landlock_request;
#ifdef CONFIG_AUDIT
-void landlock_audit_denial(const struct landlock_cred_security *const subject,
- const struct landlock_request *const request,
+void landlock_audit_denial(const struct landlock_request *const request,
struct landlock_hierarchy *const youngest_denied,
- const size_t youngest_layer,
- const access_mask_t missing,
- const bool object_quiet_flag);
+ const access_mask_t missing, const bool logged);
void landlock_audit_free_domain(
const struct landlock_hierarchy *const hierarchy);
@@ -31,11 +27,9 @@ void landlock_audit_free_domain(
#else /* CONFIG_AUDIT */
static inline void
-landlock_audit_denial(const struct landlock_cred_security *const subject,
- const struct landlock_request *const request,
+landlock_audit_denial(const struct landlock_request *const request,
struct landlock_hierarchy *const youngest_denied,
- const size_t youngest_layer, const access_mask_t missing,
- const bool object_quiet_flag)
+ const access_mask_t missing, const bool logged)
{
}
diff --git a/security/landlock/log.c b/security/landlock/log.c
index 6dd3373c4ba4..f42e6dc4de5c 100644
--- a/security/landlock/log.c
+++ b/security/landlock/log.c
@@ -405,6 +405,86 @@ static bool is_valid_request(const struct landlock_request *const request)
return true;
}
+static access_mask_t
+pick_access_mask_for_request_type(const enum landlock_request_type type,
+ const struct access_masks access_masks)
+{
+ switch (type) {
+ case LANDLOCK_REQUEST_FS_ACCESS:
+ return access_masks.fs;
+ case LANDLOCK_REQUEST_NET_ACCESS:
+ return access_masks.net;
+ default:
+ WARN_ONCE(1, "Invalid request type %d passed to %s", type,
+ __func__);
+ return 0;
+ }
+}
+
+/*
+ * Whether a quiet rule silences the denial: the rule must cover the whole
+ * denied access in the layer that denied it (a quiet rule in a non-denying
+ * layer does not suppress the denial).
+ */
+static bool
+is_denial_quieted(const struct landlock_request *const request,
+ const struct landlock_hierarchy *const youngest_denied,
+ const access_mask_t missing, const bool object_quiet_flag)
+{
+ if (object_quiet_flag) {
+ const access_mask_t quiet_mask =
+ pick_access_mask_for_request_type(
+ request->type, youngest_denied->quiet_masks);
+
+ return (quiet_mask & missing) == missing;
+ }
+
+ /*
+ * Either the object is not quiet, or this is a scope request. We check
+ * request->type to distinguish between the two cases.
+ */
+ switch (request->type) {
+ case LANDLOCK_REQUEST_SCOPE_SIGNAL:
+ return !!(youngest_denied->quiet_masks.scope &
+ LANDLOCK_SCOPE_SIGNAL);
+ case LANDLOCK_REQUEST_SCOPE_ABSTRACT_UNIX_SOCKET:
+ return !!(youngest_denied->quiet_masks.scope &
+ LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET);
+ /*
+ * Leave LANDLOCK_REQUEST_PTRACE and LANDLOCK_REQUEST_FS_CHANGE_TOPOLOGY
+ * unhandled for now - they are never quiet.
+ */
+ default:
+ return false;
+ }
+}
+
+/*
+ * Computes whether a denial from youngest_denied is selected for logging by the
+ * domain's policy: its logging must not be disabled (by both per-execution
+ * flags being off, or by an ancestor's
+ * LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF), the per-execution flag matching
+ * same_exec must be set, and no quiet rule may cover the denied access.
+ * landlock_log_denial() computes this once and passes it to
+ * landlock_audit_denial(), which additionally requires audit_enabled.
+ */
+static bool
+is_denial_logged(const struct landlock_request *const request,
+ const struct landlock_hierarchy *const youngest_denied,
+ const access_mask_t missing, const bool same_exec,
+ const bool object_quiet_flag)
+{
+ if (READ_ONCE(youngest_denied->log_status) == LANDLOCK_LOG_DISABLED)
+ return false;
+
+ if (!(same_exec ? youngest_denied->log_same_exec :
+ youngest_denied->log_new_exec))
+ return false;
+
+ return !is_denial_quieted(request, youngest_denied, missing,
+ object_quiet_flag);
+}
+
/**
* landlock_log_denial - Log a denied access
*
@@ -451,8 +531,9 @@ void landlock_log_denial(const struct landlock_cred_security *const subject,
get_hierarchy(subject->domain, youngest_layer);
}
- if (READ_ONCE(youngest_denied->log_status) == LANDLOCK_LOG_DISABLED)
- return;
+ const bool same_exec = !!(subject->domain_exec & BIT(youngest_layer));
+ const bool logged = is_denial_logged(request, youngest_denied, missing,
+ same_exec, object_quiet_flag);
/*
* Consistently keeps track of the number of denied access requests even
@@ -461,8 +542,7 @@ void landlock_log_denial(const struct landlock_cred_security *const subject,
*/
atomic64_inc(&youngest_denied->num_denials);
- landlock_audit_denial(subject, request, youngest_denied, youngest_layer,
- missing, object_quiet_flag);
+ landlock_audit_denial(request, youngest_denied, missing, logged);
}
/**
--
2.54.0
^ permalink raw reply related
* [PATCH v3 10/20] landlock: Add create_domain and free_domain tracepoints
From: Mickaël Salaün @ 2026-07-22 17:11 UTC (permalink / raw)
To: Günther Noack, Steven Rostedt
Cc: Mickaël Salaün, Christian Brauner, Jann Horn, Jeff Xu,
Justin Suess, Kees Cook, Masami Hiramatsu, Mathieu Desnoyers,
Matthieu Buffet, Mikhail Ivanov, Tingmao Wang, kernel-team,
linux-security-module, linux-trace-kernel
In-Reply-To: <20260722171159.2776765-1-mic@digikod.net>
Add a landlock_create_domain tracepoint emitted from
landlock_restrict_self() after the new domain is created, so a consumer
can correlate the source ruleset with the resulting domain. The
flags-only path (ruleset_fd == -1) creates no domain and emits no event.
Move the ruleset lock acquisition from landlock_merge_ruleset() to the
caller so the lock is held across both the merge and the tracepoint
emission, giving an eBPF program a consistent ruleset snapshot. Release
it before the thread-sync: holding ruleset->lock across
landlock_restrict_sibling_threads() would deadlock a sibling blocked on
the same lock. The event therefore fires before the (rare) thread-sync
failure path; when that path aborts the just-created domain, the
matching free_domain event fires so the create/free pair stays balanced.
Add a landlock_free_domain tracepoint that fires when a domain's
hierarchy node is freed. The hierarchy node is the lifecycle boundary
because it represents the domain's identity and outlives the domain's
access masks, which may still be active in descendant domains.
A domain freed without ever being committed to a credential was never
visible to user space, so free_domain is suppressed for it. This is
tracked by a new landlock_log_status value, LANDLOCK_LOG_UNCOMMITTED,
which is also the zero value so a hierarchy whose initialization failed
defaults to not observable. A hierarchy is born UNCOMMITTED and is
promoted to LANDLOCK_LOG_PENDING (or LANDLOCK_LOG_DISABLED when logging
is off) right after its create_domain event fires; a thread-sync failure
does not reset it, so an aborted domain that already emitted
create_domain still emits the matching free_domain. Promoting right
after the event, rather than at commit_creds() time, avoids a race: on a
successful thread-sync the sibling threads commit the new domain in
lockstep before landlock_restrict_self() returns, so the shared domain
may already have moved to LANDLOCK_LOG_RECORDED through a plain store,
and a late promotion would race that store and could unbalance the
domain allocation and deallocation audit records.
Cc: Günther Noack <gnoack@google.com>
Cc: Justin Suess <utilityemal77@gmail.com>
Cc: Masami Hiramatsu <mhiramat@kernel.org>
Cc: Mathieu Desnoyers <mathieu.desnoyers@efficios.com>
Cc: Steven Rostedt <rostedt@goodmis.org>
Cc: Tingmao Wang <m@maowtm.org>
Signed-off-by: Mickaël Salaün <mic@digikod.net>
---
Changes since v2:
https://patch.msgid.link/20260406143717.1815792-9-mic@digikod.net
- Renamed the landlock_restrict_self tracepoint to
landlock_create_domain and reordered its arguments to (domain,
ruleset) for domain-first consistency with the other
domain-lifecycle events.
- Reformat the TRACE_EVENT body (TP_PROTO, TP_STRUCT__entry,
TP_fast_assign, TP_printk) to the kernel tracing convention with one
field per line and tab-aligned columns (requested by Steven Rostedt).
- Emit the create_domain creation event under the ruleset lock, just
after the merge and before the thread-sync wait, so an observer reads
the exact ruleset frozen into the domain and the lock is not held
across thread-sync (where a sibling blocked in landlock_add_rule() on
the same lock would stall it).
- Gate free_domain on a new LANDLOCK_LOG_UNCOMMITTED status so a domain
freed before its creation event fired emits no free_domain;
create_domain marks the domain committed right after emitting the
event, so a later thread-sync failure still emits the matching
free_domain and the create/free pair stays balanced.
- Emit free_domain from landlock_trace_free_domain() in a dedicated
tracing translation unit, and move the decoupling of the log state and
landlock_log_free_domain() from CONFIG_AUDIT to the commit that
separates the common log framework from audit.
- Clarify in the create_domain emission comment that the event fires
before the infallible commit_creds().
- Clarify the free_domain kdoc: the event fires when the hierarchy
node's refcount reaches zero, after all child domains release their
parent reference (the earlier "all descendants freed" phrasing could
be misread).
Changes since v1:
- New patch.
---
include/trace/events/landlock.h | 82 +++++++++++++++++++++++++++++++++
security/landlock/domain.c | 28 ++++++-----
security/landlock/domain.h | 16 ++++++-
security/landlock/log.c | 6 ++-
security/landlock/syscalls.c | 45 +++++++++++++++++-
security/landlock/trace.c | 31 ++++++++++++-
security/landlock/trace.h | 28 +++++++++++
7 files changed, 219 insertions(+), 17 deletions(-)
create mode 100644 security/landlock/trace.h
diff --git a/include/trace/events/landlock.h b/include/trace/events/landlock.h
index 69a75cf47f65..cb0e21a2fa1f 100644
--- a/include/trace/events/landlock.h
+++ b/include/trace/events/landlock.h
@@ -13,6 +13,8 @@
#include <linux/landlock.h>
#include <linux/tracepoint.h>
+struct landlock_domain;
+struct landlock_hierarchy;
struct landlock_ruleset;
struct path;
@@ -240,6 +242,86 @@ TRACE_EVENT(landlock_add_rule_net,
__entry->port)
);
+/**
+ * landlock_create_domain - New domain created
+ *
+ * @domain: Newly created domain (never NULL, immutable after creation).
+ * @domain->hierarchy->id is its unique ID, shared with the
+ * landlock_enforce_domain and landlock_free_domain events;
+ * @domain->hierarchy->details holds the requesting process.
+ * @ruleset: Source ruleset frozen into the domain (never NULL). The
+ * ruleset lock is held across the emission, so a BPF program
+ * reading it via BTF sees the exact merged ruleset;
+ * @ruleset->id / @ruleset->version identify it.
+ *
+ * Emitted by sys_landlock_restrict_self() once, in the requesting
+ * thread's context, right after the merge and before thread-sync. The
+ * flags-only path (ruleset_fd == -1) creates no domain and does not
+ * emit this event. Paired with the per-thread landlock_enforce_domain
+ * (join on @domain->hierarchy->id) and balanced by a matching
+ * landlock_free_domain event.
+ */
+TRACE_EVENT(landlock_create_domain,
+
+ TP_PROTO(const struct landlock_domain *domain,
+ const struct landlock_ruleset *ruleset),
+
+ TP_ARGS(domain, ruleset),
+
+ TP_STRUCT__entry(
+ __field( __u64, domain_id )
+ __field( __u64, parent_id )
+ __field( __u64, ruleset_id )
+ __field( __u32, ruleset_version )
+ ),
+
+ TP_fast_assign(
+ lockdep_assert_held(&ruleset->lock);
+ __entry->domain_id = domain->hierarchy->id;
+ __entry->parent_id = domain->hierarchy->parent ?
+ domain->hierarchy->parent->id : 0;
+ __entry->ruleset_id = ruleset->id;
+ __entry->ruleset_version = ruleset->version;
+ ),
+
+ TP_printk("domain=%llx parent=%llx ruleset=%llx.%u",
+ __entry->domain_id, __entry->parent_id,
+ __entry->ruleset_id, __entry->ruleset_version)
+);
+
+/**
+ * landlock_free_domain - Domain freed
+ *
+ * @hierarchy: Hierarchy node being freed (never NULL).
+ *
+ * Emitted when the hierarchy node's last reference is dropped: its
+ * refcount reaches zero after all child domains have released their
+ * parent reference. A committed domain is
+ * freed from a kworker via landlock_put_domain_deferred() (the credential
+ * free path runs in RCU context, where sleeping is forbidden), so the
+ * current task is not the sandboxed task that triggered the free. Balanced
+ * by a matching landlock_create_domain event.
+ */
+TRACE_EVENT(landlock_free_domain,
+
+ TP_PROTO(const struct landlock_hierarchy *hierarchy),
+
+ TP_ARGS(hierarchy),
+
+ TP_STRUCT__entry(
+ __field( __u64, domain_id )
+ __field( __u64, denials )
+ ),
+
+ TP_fast_assign(
+ __entry->domain_id = hierarchy->id;
+ __entry->denials = atomic64_read(&hierarchy->num_denials);
+ ),
+
+ TP_printk("domain=%llx denials=%llu",
+ __entry->domain_id, __entry->denials)
+);
+
#undef _LANDLOCK_NAME_ENTRY
#endif /* _TRACE_LANDLOCK_H */
diff --git a/security/landlock/domain.c b/security/landlock/domain.c
index 6444cf27bda2..082c4da68536 100644
--- a/security/landlock/domain.c
+++ b/security/landlock/domain.c
@@ -309,31 +309,28 @@ static int merge_ruleset(struct landlock_domain *const dst,
if (WARN_ON_ONCE(!dst || !dst->hierarchy))
return -EINVAL;
- mutex_lock(&src->lock);
+ lockdep_assert_held(&src->lock);
/* Stacks the new layer. */
- if (WARN_ON_ONCE(dst->num_layers < 1)) {
- err = -EINVAL;
- goto out_unlock;
- }
+ if (WARN_ON_ONCE(dst->num_layers < 1))
+ return -EINVAL;
+
dst->handled_masks[dst->num_layers - 1] =
landlock_upgrade_handled_access_masks(src->handled_masks);
/* Merges the @src inode tree. */
err = merge_tree(dst, src, LANDLOCK_KEY_INODE);
if (err)
- goto out_unlock;
+ return err;
#if IS_ENABLED(CONFIG_INET)
/* Merges the @src network port tree. */
err = merge_tree(dst, src, LANDLOCK_KEY_NET_PORT);
if (err)
- goto out_unlock;
+ return err;
#endif /* IS_ENABLED(CONFIG_INET) */
-out_unlock:
- mutex_unlock(&src->lock);
- return err;
+ return 0;
}
static int inherit_tree(struct landlock_domain *const parent,
@@ -415,6 +412,8 @@ static int inherit_ruleset(struct landlock_domain *const parent,
* The current task is requesting to be restricted. The subjective credentials
* must not be in an overridden state. cf. landlock_init_hierarchy_log().
*
+ * The caller must hold @ruleset->lock.
+ *
* Return: A new domain merging @parent and @ruleset on success, or ERR_PTR() on
* failure. If @parent is NULL, the new domain duplicates @ruleset.
*/
@@ -427,6 +426,7 @@ landlock_merge_ruleset(struct landlock_domain *const parent,
int err;
might_sleep();
+ lockdep_assert_held(&ruleset->lock);
if (WARN_ON_ONCE(!ruleset))
return ERR_PTR(-EINVAL);
@@ -575,7 +575,13 @@ int landlock_init_hierarchy_log(struct landlock_hierarchy *const hierarchy)
hierarchy->details = details;
hierarchy->id = landlock_get_id_range(1);
- hierarchy->log_status = LANDLOCK_LOG_PENDING;
+ /*
+ * The hierarchy is born unobservable: landlock_restrict_self() moves it
+ * out of LANDLOCK_LOG_UNCOMMITTED once it has emitted the creation
+ * event, so the matching free_domain event fires for it and not for a
+ * hierarchy whose creation was never observed.
+ */
+ hierarchy->log_status = LANDLOCK_LOG_UNCOMMITTED;
hierarchy->log_same_exec = true;
hierarchy->log_new_exec = false;
atomic64_set(&hierarchy->num_denials, 0);
diff --git a/security/landlock/domain.h b/security/landlock/domain.h
index 1237e3e25240..8351e22016fe 100644
--- a/security/landlock/domain.h
+++ b/security/landlock/domain.h
@@ -25,7 +25,21 @@
#include "ruleset.h"
enum landlock_log_status {
- LANDLOCK_LOG_PENDING = 0,
+ /*
+ * Hierarchy whose creation event has not been emitted, so it is not yet
+ * observable from user space. A hierarchy is born in this state (the
+ * zero value, so a partially initialized hierarchy defaults to "not
+ * observable") and leaves it when landlock_restrict_self() emits its
+ * creation event, right after the merge and before the thread-sync
+ * wait. No trace free_domain event (and no audit deallocation record)
+ * fires while a hierarchy is in this state, so a hierarchy that never
+ * became observable (e.g. its initialization failed) is freed silently.
+ * A domain aborted by a thread-sync failure already emitted its
+ * creation event, so it is no longer UNCOMMITTED and does fire
+ * free_domain.
+ */
+ LANDLOCK_LOG_UNCOMMITTED = 0,
+ LANDLOCK_LOG_PENDING,
LANDLOCK_LOG_RECORDED,
LANDLOCK_LOG_DISABLED,
};
diff --git a/security/landlock/log.c b/security/landlock/log.c
index f42e6dc4de5c..13033808bdbd 100644
--- a/security/landlock/log.c
+++ b/security/landlock/log.c
@@ -17,6 +17,7 @@
#include "limits.h"
#include "log.h"
#include "ruleset.h"
+#include "trace.h"
static struct landlock_hierarchy *
get_hierarchy(const struct landlock_domain *const domain, const size_t layer)
@@ -550,14 +551,15 @@ void landlock_log_denial(const struct landlock_cred_security *const subject,
*
* @hierarchy: The domain's hierarchy being deallocated.
*
- * Called in a work queue scheduled by landlock_put_domain_deferred() called by
- * hook_cred_free().
+ * Called from landlock_put_domain_deferred() (via a work queue scheduled by
+ * hook_cred_free()) or directly from landlock_put_domain().
*/
void landlock_log_free_domain(const struct landlock_hierarchy *const hierarchy)
{
if (WARN_ON_ONCE(!hierarchy))
return;
+ landlock_trace_free_domain(hierarchy);
landlock_audit_free_domain(hierarchy);
}
diff --git a/security/landlock/syscalls.c b/security/landlock/syscalls.c
index dbc4facf00b6..7a1ec140cf14 100644
--- a/security/landlock/syscalls.c
+++ b/security/landlock/syscalls.c
@@ -536,6 +536,7 @@ SYSCALL_DEFINE2(landlock_restrict_self, const int, ruleset_fd, const __u32,
flags)
{
struct landlock_ruleset *ruleset __free(landlock_put_ruleset) = NULL;
+ struct landlock_domain *new_dom = NULL;
struct cred *new_cred;
struct landlock_cred_security *new_llcred;
bool __maybe_unused log_same_exec, log_new_exec, log_subdomains,
@@ -604,18 +605,50 @@ SYSCALL_DEFINE2(landlock_restrict_self, const int, ruleset_fd, const __u32,
* manipulating the current credentials because they are
* dedicated per thread.
*/
- struct landlock_domain *const new_dom =
- landlock_merge_ruleset(new_llcred->domain, ruleset);
+ mutex_lock(&ruleset->lock);
+ new_dom = landlock_merge_ruleset(new_llcred->domain, ruleset);
if (IS_ERR(new_dom)) {
+ mutex_unlock(&ruleset->lock);
abort_creds(new_cred);
return PTR_ERR(new_dom);
}
+ /*
+ * Emits the domain-creation event while @ruleset->lock is still
+ * held, right after the merge, so an eBPF program attached to
+ * the tracepoint reads the exact ruleset that was merged into
+ * the domain: a consistent snapshot that a concurrent
+ * landlock_add_rule() (which holds the same lock) cannot
+ * modify.
+ *
+ * This must come before the thread-sync wait below. Holding
+ * @ruleset->lock across landlock_restrict_sibling_threads()
+ * would hang: a sibling thread blocked in landlock_add_rule()
+ * on the same @ruleset->lock cannot run the task_work that
+ * thread-sync waits for (the lock wait is uninterruptible).
+ * Emitting here keeps the lock off the thread-sync path.
+ *
+ * The trade-off is that the event fires for a domain that a
+ * later (rare) thread-sync failure aborts. That path emits the
+ * matching free_domain event so the create/free pair stays
+ * balanced (see the thread-sync error path below).
+ */
+ trace_landlock_create_domain(new_dom, ruleset);
+ mutex_unlock(&ruleset->lock);
#ifdef CONFIG_SECURITY_LANDLOCK_LOG
new_dom->hierarchy->log_same_exec = log_same_exec;
new_dom->hierarchy->log_new_exec = log_new_exec;
+ /*
+ * The creation event fired above, so move the domain out of
+ * LANDLOCK_LOG_UNCOMMITTED: its free_domain event must fire
+ * too, even if a thread-sync failure aborts it below. Audit
+ * logging may still be disabled (DISABLED); tracing observes it
+ * anyway.
+ */
if ((!log_same_exec && !log_new_exec) || !prev_log_subdomains)
new_dom->hierarchy->log_status = LANDLOCK_LOG_DISABLED;
+ else
+ new_dom->hierarchy->log_status = LANDLOCK_LOG_PENDING;
#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
/* Replaces the old (prepared) domain. */
@@ -631,6 +664,14 @@ SYSCALL_DEFINE2(landlock_restrict_self, const int, ruleset_fd, const __u32,
const int err = landlock_restrict_sibling_threads(
current_cred(), new_cred);
if (err) {
+ /*
+ * Thread-sync failed (rare), so the new domain is
+ * aborted instead of committed. Its creation event
+ * already fired above, so the imminent free must emit
+ * the matching free_domain event to keep the
+ * create/free pair balanced; no special log_status is
+ * set here.
+ */
abort_creds(new_cred);
return err;
}
diff --git a/security/landlock/trace.c b/security/landlock/trace.c
index aeb6eeebe42a..5e6df313c7d2 100644
--- a/security/landlock/trace.c
+++ b/security/landlock/trace.c
@@ -6,12 +6,41 @@
* Copyright © 2026 Cloudflare, Inc.
*/
+#include "trace.h"
+#include "domain.h"
#include "ruleset.h"
/*
* Generates the tracepoint definitions in this translation unit. The trace
* event header dereferences the traced objects in TP_fast_assign, so the full
- * struct definitions (e.g. ruleset.h) must be included before it.
+ * struct definitions (e.g. ruleset.h, domain.h) must be included before it.
*/
#define CREATE_TRACE_POINTS
#include <trace/events/landlock.h>
+
+/**
+ * landlock_trace_free_domain - Emit a tracepoint on domain deallocation
+ *
+ * @hierarchy: The domain's hierarchy being deallocated.
+ *
+ * Fires only for a hierarchy whose creation event was emitted, i.e. one that
+ * left LANDLOCK_LOG_UNCOMMITTED in landlock_restrict_self(). This keeps the
+ * create/free pair balanced: a hierarchy that never became observable is freed
+ * silently, while a domain that landlock_restrict_self() created and a
+ * thread-sync failure then aborted still fires free_domain, because its
+ * creation event already fired.
+ *
+ * Called from landlock_log_free_domain().
+ */
+void landlock_trace_free_domain(const struct landlock_hierarchy *const hierarchy)
+{
+ /*
+ * The log_status read is a correctness guard (keep the create/free pair
+ * balanced), not a cost guard, so this cold path needs no
+ * trace_..._enabled() check: the tracepoint is a static-branch no-op
+ * when disabled. The denial path guards trace_..._enabled() instead
+ * because it does expensive __getname()/path work before emitting.
+ */
+ if (READ_ONCE(hierarchy->log_status) != LANDLOCK_LOG_UNCOMMITTED)
+ trace_landlock_free_domain(hierarchy);
+}
diff --git a/security/landlock/trace.h b/security/landlock/trace.h
new file mode 100644
index 000000000000..59c8ea348625
--- /dev/null
+++ b/security/landlock/trace.h
@@ -0,0 +1,28 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * Landlock - Tracepoint helpers
+ *
+ * Copyright © 2025 Microsoft Corporation
+ * Copyright © 2026 Cloudflare, Inc.
+ */
+
+#ifndef _SECURITY_LANDLOCK_TRACE_H
+#define _SECURITY_LANDLOCK_TRACE_H
+
+struct landlock_hierarchy;
+
+#ifdef CONFIG_TRACEPOINTS
+
+void landlock_trace_free_domain(
+ const struct landlock_hierarchy *const hierarchy);
+
+#else /* CONFIG_TRACEPOINTS */
+
+static inline void
+landlock_trace_free_domain(const struct landlock_hierarchy *const hierarchy)
+{
+}
+
+#endif /* CONFIG_TRACEPOINTS */
+
+#endif /* _SECURITY_LANDLOCK_TRACE_H */
--
2.54.0
^ permalink raw reply related
* [PATCH v3 11/20] landlock: Add landlock_enforce_domain tracepoint
From: Mickaël Salaün @ 2026-07-22 17:11 UTC (permalink / raw)
To: Günther Noack, Steven Rostedt
Cc: Mickaël Salaün, Christian Brauner, Jann Horn, Jeff Xu,
Justin Suess, Kees Cook, Masami Hiramatsu, Mathieu Desnoyers,
Matthieu Buffet, Mikhail Ivanov, Tingmao Wang, kernel-team,
linux-security-module, linux-trace-kernel
In-Reply-To: <20260722171159.2776765-1-mic@digikod.net>
The landlock_create_domain event records that a domain was created,
once, before thread-sync. It cannot tell which threads end up enforcing
it: a successful landlock_restrict_self(2) with
LANDLOCK_RESTRICT_SELF_TSYNC applies the domain to the caller and every
eligible sibling. Creation (the operation) and enforcement (the
per-thread outcome) are distinct.
Add landlock_enforce_domain(domain, complete, process_wide), emitted
once per thread the domain is applied to, strictly after that thread's
commit_creds(), so it fires only for a thread that is enforcing the
domain, never speculatively; an aborted operation emits none. The
lifecycle now reads create -> enforce* -> free.
The two booleans name properties, not the implementation:
- complete: marks the single event that concludes the operation. It
names the outcome, the set is now enforced, not which thread
finishes, which the contract leaves unspecified.
- process_wide: means every eligible thread of the process is
covered. It is set race-free by either establishing path,
thread-sync or a single-threaded process, so
complete && process_wide is the whole-process-enforced guarantee.
The requesting thread and source ruleset are not repeated here: they are
on create_domain (joined via domain->hierarchy->id) and on the immutable
domain->hierarchy->details. Source ruleset means the ruleset_id and
ruleset_version recorded on create_domain, not the ruleset object, which
the caller may close before enforcement.
Cc: Günther Noack <gnoack@google.com>
Cc: Masami Hiramatsu <mhiramat@kernel.org>
Cc: Mathieu Desnoyers <mathieu.desnoyers@efficios.com>
Cc: Steven Rostedt <rostedt@goodmis.org>
Cc: Tingmao Wang <m@maowtm.org>
Signed-off-by: Mickaël Salaün <mic@digikod.net>
---
Changes since v2:
- New patch.
---
include/trace/events/landlock.h | 52 +++++++++++++++++++++++++++++++++
security/landlock/syscalls.c | 13 ++++++++-
security/landlock/tsync.c | 15 ++++++++++
3 files changed, 79 insertions(+), 1 deletion(-)
diff --git a/include/trace/events/landlock.h b/include/trace/events/landlock.h
index cb0e21a2fa1f..7f221d8fff38 100644
--- a/include/trace/events/landlock.h
+++ b/include/trace/events/landlock.h
@@ -289,6 +289,58 @@ TRACE_EVENT(landlock_create_domain,
__entry->ruleset_id, __entry->ruleset_version)
);
+/**
+ * landlock_enforce_domain - Domain enforced on a thread
+ *
+ * @domain: Domain now enforced on the current thread (never NULL,
+ * immutable; read locklessly). Correlate to
+ * landlock_create_domain via @domain->hierarchy->id for the
+ * source ruleset and requesting thread, or read
+ * @domain->hierarchy->details for the requesting process.
+ * @complete: Set on the single event that concludes the operation, after
+ * all its other enforcements; filter on it for one event per
+ * operation.
+ * @process_wide: The enforcement covers every eligible (non-exiting)
+ * thread of the process: set when the caller used
+ * %LANDLOCK_RESTRICT_SELF_TSYNC or the process is
+ * single-threaded. A lone thread whose group still
+ * holds a zombie leader is not counted single-threaded,
+ * so process_wide == 0 never proves the opposite.
+ *
+ * Emitted for each thread sys_landlock_restrict_self() enforces the
+ * domain on, in that thread's own context, right after its
+ * commit_creds(), so it fires only once the thread is irreversibly
+ * enforcing the domain (aborted operations emit none). Not
+ * balanced; every enforcement falls between the domain's
+ * landlock_create_domain and landlock_free_domain events.
+ *
+ * @complete == 1 && @process_wide == 1 means the whole process is
+ * sandboxed by @domain, durably (Landlock domains are monotonic and
+ * inherited on :manpage:`clone(2)`).
+ */
+TRACE_EVENT(landlock_enforce_domain,
+
+ TP_PROTO(const struct landlock_domain *domain, bool complete,
+ bool process_wide),
+
+ TP_ARGS(domain, complete, process_wide),
+
+ TP_STRUCT__entry(
+ __field( __u64, domain_id )
+ __field( bool, complete )
+ __field( bool, process_wide )
+ ),
+
+ TP_fast_assign(
+ __entry->domain_id = domain->hierarchy->id;
+ __entry->complete = complete;
+ __entry->process_wide = process_wide;
+ ),
+
+ TP_printk("domain=%llx complete=%d process_wide=%d",
+ __entry->domain_id, __entry->complete, __entry->process_wide)
+);
+
/**
* landlock_free_domain - Domain freed
*
diff --git a/security/landlock/syscalls.c b/security/landlock/syscalls.c
index 7a1ec140cf14..bf838f65b060 100644
--- a/security/landlock/syscalls.c
+++ b/security/landlock/syscalls.c
@@ -22,6 +22,7 @@
#include <linux/mount.h>
#include <linux/path.h>
#include <linux/sched.h>
+#include <linux/sched/signal.h>
#include <linux/security.h>
#include <linux/stddef.h>
#include <linux/syscalls.h>
@@ -539,6 +540,7 @@ SYSCALL_DEFINE2(landlock_restrict_self, const int, ruleset_fd, const __u32,
struct landlock_domain *new_dom = NULL;
struct cred *new_cred;
struct landlock_cred_security *new_llcred;
+ bool process_wide;
bool __maybe_unused log_same_exec, log_new_exec, log_subdomains,
prev_log_subdomains;
@@ -677,5 +679,14 @@ SYSCALL_DEFINE2(landlock_restrict_self, const int, ruleset_fd, const __u32,
}
}
- return commit_creds(new_cred);
+ /* Whole process: thread-sync swept siblings, or single-threaded. */
+ process_wide = (flags & LANDLOCK_RESTRICT_SELF_TSYNC) ||
+ get_nr_threads(current) == 1;
+ commit_creds(new_cred);
+
+ /* The caller commits last, so its event concludes the operation. */
+ if (ruleset)
+ trace_landlock_enforce_domain(new_dom, true, process_wide);
+
+ return 0;
}
diff --git a/security/landlock/tsync.c b/security/landlock/tsync.c
index c5730bbd9ed3..b7de4f9624c7 100644
--- a/security/landlock/tsync.c
+++ b/security/landlock/tsync.c
@@ -21,6 +21,8 @@
#include "cred.h"
#include "tsync.h"
+#include <trace/events/landlock.h>
+
/*
* Shared state between multiple threads which are enforcing Landlock rulesets
* in lockstep with each other.
@@ -78,6 +80,8 @@ struct tsync_work {
*/
static void restrict_one_thread(struct tsync_shared_context *ctx)
{
+ const struct landlock_domain *new_dom =
+ landlock_cred(ctx->new_cred)->domain;
int err;
struct cred *cred = NULL;
@@ -146,6 +150,17 @@ static void restrict_one_thread(struct tsync_shared_context *ctx)
commit_creds(cred);
+ /*
+ * Emitted strictly after commit_creds() and before the out: label, so
+ * it fires only for a thread now enforcing new_dom, and every
+ * non-concluding (complete == false) event happens-before the
+ * operation's single concluding one. Skipped on the flags-only path,
+ * where old_cred and new_cred carry the same domain. A sibling never
+ * concludes the operation and its enforcement is always process-wide.
+ */
+ if (new_dom != landlock_cred(ctx->old_cred)->domain)
+ trace_landlock_enforce_domain(new_dom, false, true);
+
out:
/* Notify the calling thread once all threads are done */
if (atomic_dec_return(&ctx->num_unfinished) == 0)
--
2.54.0
^ permalink raw reply related
* [PATCH v3 09/20] landlock: Add landlock_add_rule_fs and landlock_add_rule_net tracepoints
From: Mickaël Salaün @ 2026-07-22 17:11 UTC (permalink / raw)
To: Günther Noack, Steven Rostedt
Cc: Mickaël Salaün, Christian Brauner, Jann Horn, Jeff Xu,
Justin Suess, Kees Cook, Masami Hiramatsu, Mathieu Desnoyers,
Matthieu Buffet, Mikhail Ivanov, Tingmao Wang, kernel-team,
linux-security-module, linux-trace-kernel
In-Reply-To: <20260722171159.2776765-1-mic@digikod.net>
Add tracepoints for Landlock rule addition, landlock_add_rule_fs for
filesystem rules and landlock_add_rule_net for network rules, so trace
consumers can correlate filesystem objects and network ports with their
rulesets. Both are emitted under the ruleset lock (asserted in
TP_fast_assign) so an eBPF program reads the ruleset, including the rule
just inserted, in a consistent snapshot.
Add a version field to struct landlock_ruleset, gated on
CONFIG_TRACEPOINTS like the id field and incremented under the ruleset
lock on each successful landlock_add_rule(2), including when it only
extends an existing rule's access rights. It fills the existing 4-byte
hole after usage, so the struct does not grow. Pairing the ruleset ID
with the version lets a later restrict_self event record the exact
ruleset revision merged into a domain.
Resolve the filesystem rule's absolute path with d_absolute_path()
rather than the d_path() audit uses: d_absolute_path() produces
namespace-independent paths that do not depend on the tracer's chroot
state, making trace output deterministic regardless of mount namespace
configuration. Distinguish the error cases as "<too_long>"
(-ENAMETOOLONG) and "<unreachable>" (anonymous files or detached
mounts).
Cc: Christian Brauner <brauner@kernel.org>
Cc: Günther Noack <gnoack@google.com>
Cc: Justin Suess <utilityemal77@gmail.com>
Cc: Masami Hiramatsu <mhiramat@kernel.org>
Cc: Mathieu Desnoyers <mathieu.desnoyers@efficios.com>
Cc: Steven Rostedt <rostedt@goodmis.org>
Cc: Tingmao Wang <m@maowtm.org>
Signed-off-by: Mickaël Salaün <mic@digikod.net>
---
Changes since v2:
https://patch.msgid.link/20260406143717.1815792-8-mic@digikod.net
- Order the add_rule_net tracepoint arguments access_rights before
port, matching add_rule_fs.
- Reformatted TP_STRUCT__entry and TP_fast_assign to kernel tracing
convention (requested by Steven Rostedt).
- Render access_rights as symbolic names with __print_flags(), shared
with the audit blocker names, instead of raw hex.
- Drop the tautological version static assertion (the counter tracks
add-rule operations, not rule count) and clarify that @version is
incremented on access-right extensions too.
- Gate the version field and its writer on CONFIG_TRACEPOINTS (only
tracing uses it) instead of CONFIG_SECURITY_LANDLOCK_LOG, matching the
id field.
- Adapt rule insertion to the base's quiet flag (landlock_insert_rule()
flags argument).
Changes since v1:
https://patch.msgid.link/20250523165741.693976-5-mic@digikod.net
- Added landlock_add_rule_net tracepoint for network rules.
- Dropped key=inode:0x%lx from add_rule_fs printk, using dev/ino
instead.
- Used ruleset Landlock ID instead of kernel pointer in printk.
- Differentiated d_absolute_path() error cases (suggested by
Tingmao Wang).
- Moved DEFINE_FREE(__putname) to include/linux/fs.h (noticed by
Tingmao Wang).
- Added version field to struct landlock_ruleset.
- Added version to add_rule trace events (format:
ruleset=<id>.<version>).
- Added d_absolute_path() vs d_path() rationale to commit message.
---
include/linux/fs.h | 1 +
include/trace/events/landlock.h | 115 +++++++++++++++++++++++++++++++-
security/landlock/fs.c | 19 ++++++
security/landlock/fs.h | 30 +++++++++
security/landlock/net.c | 11 +++
security/landlock/ruleset.c | 13 +++-
security/landlock/ruleset.h | 7 ++
7 files changed, 191 insertions(+), 5 deletions(-)
diff --git a/include/linux/fs.h b/include/linux/fs.h
index 50ce731a2b78..925517c672f3 100644
--- a/include/linux/fs.h
+++ b/include/linux/fs.h
@@ -2587,6 +2587,7 @@ extern void __init vfs_caches_init(void);
#define __getname() kmalloc(PATH_MAX, GFP_KERNEL)
#define __putname(name) kfree(name)
+DEFINE_FREE(__putname, char *, if (_T) __putname(_T))
void emergency_thaw_all(void);
extern int sync_filesystem(struct super_block *);
diff --git a/include/trace/events/landlock.h b/include/trace/events/landlock.h
index 2fb717055cc8..69a75cf47f65 100644
--- a/include/trace/events/landlock.h
+++ b/include/trace/events/landlock.h
@@ -14,6 +14,7 @@
#include <linux/tracepoint.h>
struct landlock_ruleset;
+struct path;
/* Maps a shared _LANDLOCK_*_NAMES entry to a __print_flags() pair. */
#define _LANDLOCK_NAME_ENTRY(mask, name) { mask, name }
@@ -63,6 +64,14 @@ struct landlock_ruleset;
* lockless snapshot instead: a task's comm, and the deny_access_net struct
* sock (whose network hook holds no socket lock), matching how the sched
* and signal trace events sample comm.
+ *
+ * Field encoding
+ * ~~~~~~~~~~~~~~
+ *
+ * Fields that mirror the Landlock UAPI use the same C types and endianness
+ * (e.g. network ports are __u64 in host endianness, like
+ * landlock_net_port_attr.port). Per-event details, such as where a value
+ * is byte-swapped, live in the field's own kdoc.
*/
/**
@@ -84,6 +93,7 @@ TRACE_EVENT(landlock_create_ruleset,
TP_STRUCT__entry(
__field( __u64, ruleset_id )
+ __field( __u32, ruleset_version )
__field( access_mask_t, handled_fs )
__field( access_mask_t, handled_net )
__field( access_mask_t, scoped )
@@ -91,13 +101,14 @@ TRACE_EVENT(landlock_create_ruleset,
TP_fast_assign(
__entry->ruleset_id = ruleset->id;
+ __entry->ruleset_version = ruleset->version;
__entry->handled_fs = ruleset->handled_masks.fs;
__entry->handled_net = ruleset->handled_masks.net;
__entry->scoped = ruleset->handled_masks.scope;
),
- TP_printk("ruleset=%llx handled_fs=%s handled_net=%s scoped=%s",
- __entry->ruleset_id,
+ TP_printk("ruleset=%llx.%u handled_fs=%s handled_net=%s scoped=%s",
+ __entry->ruleset_id, __entry->ruleset_version,
__print_flags(__entry->handled_fs, "|", _LANDLOCK_ACCESS_FS_NAMES),
__print_flags(__entry->handled_net, "|", _LANDLOCK_ACCESS_NET_NAMES),
__print_flags(__entry->scoped, "|", _LANDLOCK_SCOPE_NAMES))
@@ -122,13 +133,111 @@ TRACE_EVENT(landlock_free_ruleset,
TP_STRUCT__entry(
__field( __u64, ruleset_id )
+ __field( __u32, ruleset_version )
+ ),
+
+ TP_fast_assign(
+ __entry->ruleset_id = ruleset->id;
+ __entry->ruleset_version = ruleset->version;
+ ),
+
+ TP_printk("ruleset=%llx.%u",
+ __entry->ruleset_id, __entry->ruleset_version)
+);
+
+/**
+ * landlock_add_rule_fs - Filesystem rule added to a ruleset
+ *
+ * @ruleset: Source ruleset (never NULL).
+ * @access_rights: Effective access mask stored in the rule, not the raw
+ * sys_landlock_add_rule() argument (unhandled rights
+ * added).
+ * @path: Filesystem path for the rule (never NULL).
+ * @pathname: Resolved absolute path string (never NULL; error placeholder
+ * on resolution failure).
+ *
+ * Emitted by sys_landlock_add_rule() under the modified ruleset's lock, so
+ * the reported ruleset is a stable snapshot that no concurrent writer can
+ * change.
+ */
+TRACE_EVENT(landlock_add_rule_fs,
+
+ TP_PROTO(const struct landlock_ruleset *ruleset,
+ access_mask_t access_rights, const struct path *path,
+ const char *pathname),
+
+ TP_ARGS(ruleset, access_rights, path, pathname),
+
+ TP_STRUCT__entry(
+ __field( __u64, ruleset_id )
+ __field( __u32, ruleset_version )
+ __field( access_mask_t, access_rights )
+ __field( dev_t, dev )
+ __field( ino_t, ino )
+ __string( pathname, pathname )
+ ),
+
+ TP_fast_assign(
+ lockdep_assert_held(&ruleset->lock);
+ __entry->ruleset_id = ruleset->id;
+ __entry->ruleset_version = ruleset->version;
+ __entry->access_rights = access_rights;
+ __entry->dev = path->dentry->d_sb->s_dev;
+ /*
+ * The inode number may not be the user-visible one,
+ * but it will be the same used by audit.
+ */
+ __entry->ino = d_backing_inode(path->dentry)->i_ino;
+ __assign_str(pathname);
+ ),
+
+ TP_printk("ruleset=%llx.%u access_rights=%s dev=%u:%u ino=%lu path=%s",
+ __entry->ruleset_id, __entry->ruleset_version,
+ __print_flags(__entry->access_rights, "|", _LANDLOCK_ACCESS_FS_NAMES),
+ MAJOR(__entry->dev), MINOR(__entry->dev), __entry->ino,
+ __print_untrusted_str(pathname))
+);
+
+/**
+ * landlock_add_rule_net - Network port rule added to a ruleset
+ *
+ * @ruleset: Source ruleset (never NULL).
+ * @access_rights: Effective access mask stored in the rule, not the raw
+ * sys_landlock_add_rule() argument (unhandled rights
+ * added).
+ * @port: Network port, the landlock_net_port_attr.port UAPI value
+ * forwarded directly.
+ *
+ * Emitted by sys_landlock_add_rule() under the modified ruleset's lock, so
+ * the reported ruleset is a stable snapshot that no concurrent writer can
+ * change.
+ */
+TRACE_EVENT(landlock_add_rule_net,
+
+ TP_PROTO(const struct landlock_ruleset *ruleset,
+ access_mask_t access_rights, __u64 port),
+
+ TP_ARGS(ruleset, access_rights, port),
+
+ TP_STRUCT__entry(
+ __field( __u64, ruleset_id )
+ __field( __u32, ruleset_version )
+ __field( access_mask_t, access_rights )
+ __field( __u64, port )
),
TP_fast_assign(
+ lockdep_assert_held(&ruleset->lock);
__entry->ruleset_id = ruleset->id;
+ __entry->ruleset_version = ruleset->version;
+ __entry->access_rights = access_rights;
+ __entry->port = port;
),
- TP_printk("ruleset=%llx", __entry->ruleset_id)
+ TP_printk("ruleset=%llx.%u access_rights=%s port=%llu",
+ __entry->ruleset_id, __entry->ruleset_version,
+ __print_flags(__entry->access_rights, "|", _LANDLOCK_ACCESS_NET_NAMES),
+ __entry->port)
);
#undef _LANDLOCK_NAME_ENTRY
diff --git a/security/landlock/fs.c b/security/landlock/fs.c
index d39da6e9fa8c..48744a21d0a3 100644
--- a/security/landlock/fs.c
+++ b/security/landlock/fs.c
@@ -52,6 +52,8 @@
#include "ruleset.h"
#include "setup.h"
+#include <trace/events/landlock.h>
+
/* Underlying object management */
static void release_inode(struct landlock_object *const object)
@@ -346,7 +348,24 @@ int landlock_append_fs_rule(struct landlock_ruleset *const ruleset,
return PTR_ERR(id.key.object);
mutex_lock(&ruleset->lock);
err = landlock_insert_rule(ruleset, id, access_rights, flags);
+
+ /*
+ * Emit after the rule insertion succeeds, so every event corresponds to
+ * a rule that is actually in the ruleset. The ruleset lock is still
+ * held for BTF consistency (enforced by lockdep_assert_held in
+ * TP_fast_assign).
+ */
+ if (!err && trace_landlock_add_rule_fs_enabled()) {
+ char *buffer __free(__putname) = __getname();
+ const char *pathname =
+ buffer ? resolve_path_for_trace(path, buffer) :
+ "<no_mem>";
+
+ trace_landlock_add_rule_fs(ruleset, access_rights, path,
+ pathname);
+ }
mutex_unlock(&ruleset->lock);
+
/*
* No need to check for an error because landlock_insert_rule()
* increments the refcount for the new object if needed.
diff --git a/security/landlock/fs.h b/security/landlock/fs.h
index c16f24e30bd5..4e3d7cbd7e1e 100644
--- a/security/landlock/fs.h
+++ b/security/landlock/fs.h
@@ -11,6 +11,7 @@
#define _SECURITY_LANDLOCK_FS_H
#include <linux/build_bug.h>
+#include <linux/cleanup.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/rcupdate.h>
@@ -153,4 +154,33 @@ int landlock_append_fs_rule(struct landlock_ruleset *const ruleset,
const struct path *const path,
access_mask_t access_hierarchy, const u32 flags);
+/**
+ * resolve_path_for_trace - Resolve a path for tracepoint display
+ *
+ * @path: The path to resolve.
+ * @buf: A buffer of at least PATH_MAX bytes for the resolved path.
+ *
+ * Uses d_absolute_path() to produce a namespace-independent absolute path,
+ * unlike d_path() which resolves relative to the process's chroot. This
+ * ensures trace output is deterministic regardless of the tracer's mount
+ * namespace.
+ *
+ * Return: A pointer into @buf with the resolved path, or an error string
+ * ("<too_long>", "<unreachable>").
+ */
+static inline const char *resolve_path_for_trace(const struct path *path,
+ char *buf)
+{
+ const char *p;
+
+ p = d_absolute_path(path, buf, PATH_MAX);
+ if (!IS_ERR_OR_NULL(p))
+ return p;
+
+ if (PTR_ERR(p) == -ENAMETOOLONG)
+ return "<too_long>";
+
+ return "<unreachable>";
+}
+
#endif /* _SECURITY_LANDLOCK_FS_H */
diff --git a/security/landlock/net.c b/security/landlock/net.c
index e27b3ba15664..ead97fcfdcff 100644
--- a/security/landlock/net.c
+++ b/security/landlock/net.c
@@ -20,6 +20,8 @@
#include "net.h"
#include "ruleset.h"
+#include <trace/events/landlock.h>
+
int landlock_append_net_rule(struct landlock_ruleset *const ruleset,
const u16 port, access_mask_t access_rights,
const u32 flags)
@@ -37,6 +39,15 @@ int landlock_append_net_rule(struct landlock_ruleset *const ruleset,
mutex_lock(&ruleset->lock);
err = landlock_insert_rule(ruleset, id, access_rights, flags);
+
+ /*
+ * Emit after the rule insertion succeeds, so every event corresponds to
+ * a rule that is actually in the ruleset. The ruleset lock is still
+ * held for BTF consistency (enforced by lockdep_assert_held in
+ * TP_fast_assign).
+ */
+ if (!err)
+ trace_landlock_add_rule_net(ruleset, access_rights, port);
mutex_unlock(&ruleset->lock);
return err;
diff --git a/security/landlock/ruleset.c b/security/landlock/ruleset.c
index 3bfee53177d8..b78714047ddf 100644
--- a/security/landlock/ruleset.c
+++ b/security/landlock/ruleset.c
@@ -4,6 +4,7 @@
*
* Copyright © 2016-2020 Mickaël Salaün <mic@digikod.net>
* Copyright © 2018-2020 ANSSI
+ * Copyright © 2026 Cloudflare, Inc.
*/
#include <linux/bits.h>
@@ -306,11 +307,19 @@ int landlock_insert_rule(struct landlock_ruleset *const ruleset,
.quiet = !!(flags & LANDLOCK_ADD_RULE_QUIET),
},
} };
+ int err;
build_check_layer();
lockdep_assert_held(&ruleset->lock);
- return landlock_rule_insert(&ruleset->rules, id, &layers,
- ARRAY_SIZE(layers));
+ err = landlock_rule_insert(&ruleset->rules, id, &layers,
+ ARRAY_SIZE(layers));
+
+#ifdef CONFIG_TRACEPOINTS
+ if (!err)
+ ruleset->version++;
+#endif /* CONFIG_TRACEPOINTS */
+
+ return err;
}
void landlock_free_rules(struct landlock_rules *const rules)
diff --git a/security/landlock/ruleset.h b/security/landlock/ruleset.h
index ca1fd5f4c417..799d9b3cc205 100644
--- a/security/landlock/ruleset.h
+++ b/security/landlock/ruleset.h
@@ -167,6 +167,13 @@ struct landlock_ruleset {
refcount_t usage;
#ifdef CONFIG_TRACEPOINTS
+ /**
+ * @version: Counter incremented on each successful
+ * landlock_add_rule(2), including when it only extends an existing
+ * rule's access rights. Used by tracepoints to correlate a domain with
+ * the exact ruleset state it was created from. Protected by @lock.
+ */
+ u32 version;
/**
* @id: Unique identifier for this ruleset, used for tracing.
*/
--
2.54.0
^ permalink raw reply related
* [PATCH v3 14/20] landlock: Add tracepoints for ptrace and scope denials
From: Mickaël Salaün @ 2026-07-22 17:11 UTC (permalink / raw)
To: Günther Noack, Steven Rostedt
Cc: Mickaël Salaün, Christian Brauner, Jann Horn, Jeff Xu,
Justin Suess, Kees Cook, Masami Hiramatsu, Mathieu Desnoyers,
Matthieu Buffet, Mikhail Ivanov, Tingmao Wang, kernel-team,
linux-security-module, linux-trace-kernel
In-Reply-To: <20260722171159.2776765-1-mic@digikod.net>
Scope and ptrace denials follow a different code path (a domain
hierarchy check) than access-right denials, so they need dedicated
tracepoints with type-specific TP_PROTO arguments. Complete the denial
coverage with:
- landlock_deny_ptrace: ptrace access denied by a domain hierarchy
mismatch.
- landlock_deny_scope_signal: signal delivery denied by
LANDLOCK_SCOPE_SIGNAL.
- landlock_deny_scope_abstract_unix_socket: abstract unix socket access
denied by LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET.
TP_PROTO passes the raw kernel object (struct task_struct or struct
sock) for eBPF BTF access; the comm and sun_path string fields use
__print_untrusted_str() because they hold untrusted input. Unlike the
deny_access events, these omit the blockers field: each maps to exactly
one denial type named by the event, so the bitmask would always be zero.
Like the deny_access events they carry same_exec and logged.
Audit logs the task-targeted denials with generic field names (opid,
ocomm), but a strongly typed trace event can use role-prefixed names
(tracee_pid/tracee_comm, target_pid/target_comm) that match the mainline
task-name convention (sched_process_fork's parent_comm/child_comm) and
say whose name each field holds; a bare comm= would collide across
events. The abstract-unix-socket event reports peer_pid instead, a
tracepoint-only field with no audit counterpart.
A scope or ptrace verdict compares the subject domain against the other
party's domain, so each event also reports that other party's Landlock
domain (tracee_domain=, target_domain=, or peer_domain=); the subject
domain= alone does not let a consumer redo domain_is_scoped() or
domain_ptrace(). It is reported as a scalar ID rather than a domain
pointer: a domain object is immutable, but the other task can replace
its credential and free the domain that credential referenced, so a
stored foreign pointer could dangle before the event is consumed. The
scalar ID also honors the tracepoint no-nullable-pointer rule, since the
other party is frequently unsandboxed. Passing the foreign domain
hierarchy object so an eBPF consumer could walk the other party's
ancestry live would lengthen the RCU section on the shared denial path
and needs a deferred refcount put, so it is left as a future
enhancement. The relational domain-ID field (tracee_domain,
target_domain, or peer_domain) is trace-only and is not added to audit
records, so audit's denial format is unchanged by this series.
Cc: Günther Noack <gnoack@google.com>
Cc: Justin Suess <utilityemal77@gmail.com>
Cc: Masami Hiramatsu <mhiramat@kernel.org>
Cc: Mathieu Desnoyers <mathieu.desnoyers@efficios.com>
Cc: Steven Rostedt <rostedt@goodmis.org>
Cc: Tingmao Wang <m@maowtm.org>
Signed-off-by: Mickaël Salaün <mic@digikod.net>
---
Changes since v2:
https://patch.msgid.link/20260406143717.1815792-13-mic@digikod.net
- Reformatted TRACE_EVENT bodies to the kernel tracing convention with
one field per line and tab-aligned columns (requested by Steven
Rostedt).
- Replaced the log_same_exec and log_new_exec fields with the single
logged boolean, matching the deny_access events.
- Assert unix_state_lock(peer) in the abstract-unix-socket event (held
by the caller) and document why unix_sk(peer)->addr is stable.
Document peer_pid as a best-effort READ_ONCE read: its canonical lock
is sk->sk_peer_lock, not unix_state_lock, but the target peer's
peercred is not updated concurrently in these hooks, and it is 0 for a
datagram peer.
- Renamed the deny_ptrace and deny_scope_signal printk label comm= to
the role-prefixed tracee_comm= and target_comm=, matching each event's
sibling *_pid field and the sched_process_fork task-name convention;
label equals field so it works directly as an ftrace filter.
- Record the other party's Landlock domain as a verdict-time scalar ID
(tracee_domain=/target_domain=/peer_domain=, 0 when unsandboxed) so a
trace consumer can reproduce a relational scope or ptrace verdict; the
ID is captured under the RCU read lock or the unix socket lock, and
audit records are unchanged.
- Turn the request-type switch default in landlock_trace_denial() into a
WARN_ONCE() canary reporting the offending type, now that this patch
completes the switch with the ptrace and scope cases (the default is
unreachable once every request type is handled).
Changes since v1:
- New patch.
---
include/trace/events/landlock.h | 195 ++++++++++++++++++++++++++++++++
security/landlock/log.h | 9 ++
security/landlock/task.c | 52 +++++++--
security/landlock/trace.c | 22 ++++
4 files changed, 271 insertions(+), 7 deletions(-)
diff --git a/include/trace/events/landlock.h b/include/trace/events/landlock.h
index e70dcc9a94b9..94de20fbe908 100644
--- a/include/trace/events/landlock.h
+++ b/include/trace/events/landlock.h
@@ -13,6 +13,7 @@
#include <linux/landlock.h>
#include <linux/trace_seq.h>
#include <linux/tracepoint.h>
+#include <net/af_unix.h>
struct dentry;
struct landlock_domain;
@@ -21,6 +22,7 @@ struct landlock_rule;
struct landlock_ruleset;
struct path;
struct sock;
+struct task_struct;
/* Maps a shared _LANDLOCK_*_NAMES entry to a __print_flags() pair. */
#define _LANDLOCK_NAME_ENTRY(mask, name) { mask, name }
@@ -32,6 +34,18 @@ struct sock;
* A new tracepoint must uphold them, and an eBPF consumer can rely on
* them.
*
+ * Decision context
+ * ~~~~~~~~~~~~~~~~
+ *
+ * A denial event, together with the lifecycle events, exposes the full
+ * set of inputs the verdict consumed, so a consumer that tracked domain
+ * creation (landlock_create_ruleset, landlock_create_domain) can verify
+ * or reproduce the Landlock decision rather than merely observe it
+ * happened. In who/what/why terms: who is the denying domain (the domain
+ * field, always the subject that enforced the policy, never the current
+ * task), what is the operation and its object, and why is every other
+ * input the verdict weighed.
+ *
* Lifecycle consistency
* ~~~~~~~~~~~~~~~~~~~~~~
*
@@ -100,6 +114,18 @@ struct sock;
* log flags. Denial events order their fields as domain, same_exec,
* logged, then blockers (deny_access events only), then the type-specific
* object fields, then any variable-length field.
+ *
+ * Relational referents
+ * ~~~~~~~~~~~~~~~~~~~~~
+ *
+ * A scope or ptrace verdict compares two domains, so the other party's
+ * domain is part of the decision context. It is exposed as a scalar
+ * domain ID (0 when that party is unsandboxed): target_domain (signal),
+ * peer_domain (abstract unix socket), tracee_domain (ptrace). With both
+ * IDs in the stream, a consumer that tracked domain creation can relate
+ * the two parties without kernel-internal state. The ID is a scalar
+ * snapshot, not a live domain pointer that could dangle: an optional
+ * relational referent is a scalar (0 sentinel), not a nullable pointer.
*/
#ifdef CREATE_TRACE_POINTS
@@ -705,6 +731,175 @@ TRACE_EVENT(landlock_deny_access_net,
__entry->sport, __entry->dport)
);
+/**
+ * landlock_deny_ptrace - Ptrace access denied by a Landlock domain
+ *
+ * @hierarchy: Denying domain's hierarchy node (never NULL); its id is the
+ * domain field.
+ * @same_exec: Whether the current task entered the denying domain itself.
+ * @logged: The domain's audit-logging decision for this denial.
+ * @tracee_domain_id: The tracee's Landlock domain ID, or 0 if the tracee
+ * is unsandboxed.
+ * @tracee: The target task ptrace acted on (never NULL). tracee_pid is
+ * the init-namespace TGID (like audit's opid).
+ *
+ * Emitted when a Landlock domain denies a ptrace operation.
+ */
+TRACE_EVENT(landlock_deny_ptrace,
+
+ TP_PROTO(const struct landlock_hierarchy *hierarchy, bool same_exec,
+ bool logged, u64 tracee_domain_id,
+ const struct task_struct *tracee),
+
+ TP_ARGS(hierarchy, same_exec, logged, tracee_domain_id, tracee),
+
+ TP_STRUCT__entry(
+ __field( __u64, domain_id )
+ __field( bool, same_exec )
+ __field( bool, logged )
+ __field( __u64, tracee_domain_id)
+ __field( pid_t, tracee_pid )
+ __string( tracee_comm, tracee->comm )
+ ),
+
+ TP_fast_assign(
+ __entry->domain_id = hierarchy->id;
+ __entry->same_exec = same_exec;
+ __entry->logged = logged;
+ __entry->tracee_domain_id = tracee_domain_id;
+ __entry->tracee_pid = task_tgid_nr((struct task_struct *)tracee);
+ __assign_str(tracee_comm);
+ ),
+
+ TP_printk("domain=%llx same_exec=%d logged=%d tracee_domain=%llx tracee_pid=%d tracee_comm=%s",
+ __entry->domain_id, __entry->same_exec, __entry->logged,
+ __entry->tracee_domain_id, __entry->tracee_pid,
+ __print_untrusted_str(tracee_comm))
+);
+
+/**
+ * landlock_deny_scope_signal - Signal delivery denied by
+ * LANDLOCK_SCOPE_SIGNAL
+ *
+ * @hierarchy: Denying domain's hierarchy node (never NULL); its id is the
+ * domain field.
+ * @same_exec: Whether the current task entered the denying domain itself.
+ * @logged: The domain's audit-logging decision for this denial.
+ * @target_domain_id: The target's Landlock domain ID, or 0 if the target
+ * is unsandboxed.
+ * @target: The task the signal was aimed at (never NULL). target_pid is
+ * the init-namespace TGID (like audit's opid).
+ *
+ * Emitted when a Landlock domain denies signal delivery to a scoped-out
+ * target.
+ */
+TRACE_EVENT(landlock_deny_scope_signal,
+
+ TP_PROTO(const struct landlock_hierarchy *hierarchy, bool same_exec,
+ bool logged, u64 target_domain_id,
+ const struct task_struct *target),
+
+ TP_ARGS(hierarchy, same_exec, logged, target_domain_id, target),
+
+ TP_STRUCT__entry(
+ __field( __u64, domain_id )
+ __field( bool, same_exec )
+ __field( bool, logged )
+ __field( __u64, target_domain_id)
+ __field( pid_t, target_pid )
+ __string( target_comm, target->comm )
+ ),
+
+ TP_fast_assign(
+ __entry->domain_id = hierarchy->id;
+ __entry->same_exec = same_exec;
+ __entry->logged = logged;
+ __entry->target_domain_id = target_domain_id;
+ __entry->target_pid = task_tgid_nr((struct task_struct *)target);
+ __assign_str(target_comm);
+ ),
+
+ TP_printk("domain=%llx same_exec=%d logged=%d target_domain=%llx target_pid=%d target_comm=%s",
+ __entry->domain_id, __entry->same_exec, __entry->logged,
+ __entry->target_domain_id, __entry->target_pid,
+ __print_untrusted_str(target_comm))
+);
+
+/**
+ * landlock_deny_scope_abstract_unix_socket - Abstract unix socket access
+ * denied by LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET
+ *
+ * @hierarchy: Denying domain's hierarchy node (never NULL); its id is the
+ * domain field.
+ * @same_exec: Whether the current task entered the denying domain itself.
+ * @logged: The domain's audit-logging decision for this denial.
+ * @peer_domain_id: The peer's Landlock domain ID, or 0 if the peer is
+ * unsandboxed.
+ * @peer: Peer socket (never NULL). peer_pid is best-effort: it is 0 for
+ * a datagram peer (no SO_PEERCRED), so sun_path is the reliable
+ * peer identifier.
+ *
+ * Emitted when a Landlock domain denies access to a scoped-out abstract
+ * unix socket.
+ */
+TRACE_EVENT(landlock_deny_scope_abstract_unix_socket,
+
+ TP_PROTO(const struct landlock_hierarchy *hierarchy, bool same_exec,
+ bool logged, u64 peer_domain_id, const struct sock *peer),
+
+ TP_ARGS(hierarchy, same_exec, logged, peer_domain_id, peer),
+
+ TP_STRUCT__entry(
+ __field( __u64, domain_id )
+ __field( bool, same_exec )
+ __field( bool, logged )
+ __field( __u64, peer_domain_id )
+ __field( pid_t, peer_pid )
+ /*
+ * Abstract socket names are untrusted binary data from
+ * user space. Use __string_len because abstract names
+ * are not NUL-terminated; their length is determined by
+ * addr->len. unix_sk(peer)->addr is stable here because
+ * the caller (hook_unix_stream_connect or
+ * hook_unix_may_send) holds unix_state_lock(peer).
+ */
+ __string_len( sun_path,
+ unix_sk(peer)->addr ?
+ unix_sk(peer)->addr->name->sun_path + 1 :
+ "",
+ unix_sk(peer)->addr ?
+ unix_sk(peer)->addr->len -
+ offsetof(struct sockaddr_un,
+ sun_path) - 1 :
+ 0)
+ ),
+
+ TP_fast_assign(
+ struct pid *peer_pid;
+
+ lockdep_assert_held(&unix_sk(peer)->lock);
+ __entry->domain_id = hierarchy->id;
+ __entry->same_exec = same_exec;
+ __entry->logged = logged;
+ __entry->peer_domain_id = peer_domain_id;
+ /*
+ * Best-effort (0 for a datagram peer). sk_peer_pid is
+ * canonically guarded by sk->sk_peer_lock, but the target
+ * peer's peercred is set once and not updated concurrently in
+ * these hooks, so this READ_ONCE() is safe; sun_path is the
+ * reliable identifier.
+ */
+ peer_pid = READ_ONCE(peer->sk_peer_pid);
+ __entry->peer_pid = peer_pid ? pid_nr(peer_pid) : 0;
+ __assign_str(sun_path);
+ ),
+
+ TP_printk("domain=%llx same_exec=%d logged=%d peer_domain=%llx peer_pid=%d sun_path=%s",
+ __entry->domain_id, __entry->same_exec, __entry->logged,
+ __entry->peer_domain_id, __entry->peer_pid,
+ __print_untrusted_str(sun_path))
+);
+
#undef _LANDLOCK_NAME_ENTRY
#endif /* _TRACE_LANDLOCK_H */
diff --git a/security/landlock/log.h b/security/landlock/log.h
index 25afc17cf055..e0a6e44f3ddd 100644
--- a/security/landlock/log.h
+++ b/security/landlock/log.h
@@ -3,6 +3,7 @@
* Landlock - Log helpers
*
* Copyright © 2023-2025 Microsoft Corporation
+ * Copyright © 2026 Cloudflare, Inc.
*/
#ifndef _SECURITY_LANDLOCK_LOG_H
@@ -50,6 +51,14 @@ struct landlock_request {
const access_mask_t all_existing_optional_access;
deny_masks_t deny_masks;
optional_access_t quiet_optional_accesses;
+
+ /*
+ * Other-party domain ID for a relational (scope/ptrace) denial, or 0 if
+ * that party is unsandboxed. An ID, not a pointer: the other task can
+ * replace its credential and free the domain it referenced. Trace path
+ * only; audit ignores it.
+ */
+ u64 other_domain_id;
};
#ifdef CONFIG_SECURITY_LANDLOCK_LOG
diff --git a/security/landlock/task.c b/security/landlock/task.c
index c0da22736ea0..cbc7c1ef33cb 100644
--- a/security/landlock/task.c
+++ b/security/landlock/task.c
@@ -88,6 +88,7 @@ static int hook_ptrace_access_check(struct task_struct *const child,
const unsigned int mode)
{
const struct landlock_cred_security *parent_subject;
+ u64 tracee_domain_id = 0;
int err;
/* Quick return for non-landlocked tasks. */
@@ -99,6 +100,10 @@ static int hook_ptrace_access_check(struct task_struct *const child,
const struct landlock_domain *const child_dom =
landlock_get_task_domain(child);
err = domain_ptrace(parent_subject->domain, child_dom);
+#ifdef CONFIG_SECURITY_LANDLOCK_LOG
+ if (child_dom)
+ tracee_domain_id = child_dom->hierarchy->id;
+#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
}
if (!err)
@@ -116,6 +121,7 @@ static int hook_ptrace_access_check(struct task_struct *const child,
.u.tsk = child,
},
.layer_plus_one = parent_subject->domain->num_layers,
+ .other_domain_id = tracee_domain_id,
});
return err;
@@ -136,6 +142,7 @@ static int hook_ptrace_traceme(struct task_struct *const parent)
{
const struct landlock_cred_security *parent_subject;
const struct landlock_domain *child_dom;
+ u64 tracee_domain_id = 0;
int err;
child_dom = landlock_get_current_domain();
@@ -147,6 +154,12 @@ static int hook_ptrace_traceme(struct task_struct *const parent)
if (!err)
return 0;
+#ifdef CONFIG_SECURITY_LANDLOCK_LOG
+ /* The tracee is the current task; its domain is stable here. */
+ if (child_dom)
+ tracee_domain_id = child_dom->hierarchy->id;
+#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
+
/*
* For the ptrace_traceme case, we log the domain which is the cause of
* the denial, which means the parent domain instead of the current
@@ -161,6 +174,7 @@ static int hook_ptrace_traceme(struct task_struct *const parent)
.u.tsk = current,
},
.layer_plus_one = parent_subject->domain->num_layers,
+ .other_domain_id = tracee_domain_id,
});
return err;
}
@@ -236,13 +250,17 @@ static bool domain_is_scoped(const struct landlock_domain *const client,
}
static bool sock_is_scoped(struct sock *const other,
- const struct landlock_domain *const domain)
+ const struct landlock_domain *const domain,
+ u64 *const peer_domain_id)
{
const struct landlock_domain *dom_other;
/* The credentials will not change. */
lockdep_assert_held(&unix_sk(other)->lock);
dom_other = landlock_cred(other->sk_socket->file->f_cred)->domain;
+#ifdef CONFIG_SECURITY_LANDLOCK_LOG
+ *peer_domain_id = dom_other ? dom_other->hierarchy->id : 0;
+#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
return domain_is_scoped(domain, dom_other,
LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET);
}
@@ -270,6 +288,7 @@ static int hook_unix_stream_connect(struct sock *const sock,
struct sock *const newsk)
{
size_t handle_layer;
+ u64 peer_domain_id = 0;
const struct landlock_cred_security *const subject =
landlock_get_applicable_subject(current_cred(), unix_scope,
&handle_layer);
@@ -281,7 +300,7 @@ static int hook_unix_stream_connect(struct sock *const sock,
if (!is_abstract_socket(other))
return 0;
- if (!sock_is_scoped(other, subject->domain))
+ if (!sock_is_scoped(other, subject->domain, &peer_domain_id))
return 0;
landlock_log_denial(subject, &(struct landlock_request) {
@@ -293,6 +312,7 @@ static int hook_unix_stream_connect(struct sock *const sock,
},
},
.layer_plus_one = handle_layer + 1,
+ .other_domain_id = peer_domain_id,
});
return -EPERM;
}
@@ -301,6 +321,7 @@ static int hook_unix_may_send(struct socket *const sock,
struct socket *const other)
{
size_t handle_layer;
+ u64 peer_domain_id = 0;
const struct landlock_cred_security *const subject =
landlock_get_applicable_subject(current_cred(), unix_scope,
&handle_layer);
@@ -318,7 +339,7 @@ static int hook_unix_may_send(struct socket *const sock,
if (!is_abstract_socket(other->sk))
return 0;
- if (!sock_is_scoped(other->sk, subject->domain))
+ if (!sock_is_scoped(other->sk, subject->domain, &peer_domain_id))
return 0;
landlock_log_denial(subject, &(struct landlock_request) {
@@ -330,6 +351,7 @@ static int hook_unix_may_send(struct socket *const sock,
},
},
.layer_plus_one = handle_layer + 1,
+ .other_domain_id = peer_domain_id,
});
return -EPERM;
}
@@ -344,6 +366,7 @@ static int hook_task_kill(struct task_struct *const p,
{
bool is_scoped;
size_t handle_layer;
+ u64 target_domain_id = 0;
const struct landlock_cred_security *subject;
if (!cred) {
@@ -370,9 +393,15 @@ static int hook_task_kill(struct task_struct *const p,
return 0;
scoped_guard(rcu) {
- is_scoped = domain_is_scoped(subject->domain,
- landlock_get_task_domain(p),
+ const struct landlock_domain *const other =
+ landlock_get_task_domain(p);
+
+ is_scoped = domain_is_scoped(subject->domain, other,
signal_scope.scope);
+#ifdef CONFIG_SECURITY_LANDLOCK_LOG
+ if (other)
+ target_domain_id = other->hierarchy->id;
+#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
}
if (!is_scoped)
@@ -385,6 +414,7 @@ static int hook_task_kill(struct task_struct *const p,
.u.tsk = p,
},
.layer_plus_one = handle_layer + 1,
+ .other_domain_id = target_domain_id,
});
return -EPERM;
}
@@ -394,6 +424,7 @@ static int hook_file_send_sigiotask(struct task_struct *tsk,
{
const struct landlock_cred_security *subject;
bool is_scoped = false;
+ u64 target_domain_id = 0;
/* Lock already held by send_sigio() and send_sigurg(). */
lockdep_assert_held(&fown->lock);
@@ -421,9 +452,15 @@ static int hook_file_send_sigiotask(struct task_struct *tsk,
return 0;
scoped_guard(rcu) {
- is_scoped = domain_is_scoped(subject->domain,
- landlock_get_task_domain(tsk),
+ const struct landlock_domain *const other =
+ landlock_get_task_domain(tsk);
+
+ is_scoped = domain_is_scoped(subject->domain, other,
signal_scope.scope);
+#ifdef CONFIG_SECURITY_LANDLOCK_LOG
+ if (other)
+ target_domain_id = other->hierarchy->id;
+#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
}
if (!is_scoped)
@@ -438,6 +475,7 @@ static int hook_file_send_sigiotask(struct task_struct *tsk,
#ifdef CONFIG_SECURITY_LANDLOCK_LOG
.layer_plus_one = landlock_file(fown->file)->fown_layer + 1,
#endif /* CONFIG_SECURITY_LANDLOCK_LOG */
+ .other_domain_id = target_domain_id,
});
return -EPERM;
}
diff --git a/security/landlock/trace.c b/security/landlock/trace.c
index 4c4229d4ffdf..2ea7aac8d75d 100644
--- a/security/landlock/trace.c
+++ b/security/landlock/trace.c
@@ -157,7 +157,29 @@ void landlock_trace_denial(
ntohs(request->audit.u.net->sport),
ntohs(request->audit.u.net->dport));
break;
+ case LANDLOCK_REQUEST_PTRACE:
+ if (trace_landlock_deny_ptrace_enabled())
+ trace_landlock_deny_ptrace(youngest_denied, same_exec,
+ logged,
+ request->other_domain_id,
+ request->audit.u.tsk);
+ break;
+ case LANDLOCK_REQUEST_SCOPE_SIGNAL:
+ if (trace_landlock_deny_scope_signal_enabled())
+ trace_landlock_deny_scope_signal(
+ youngest_denied, same_exec, logged,
+ request->other_domain_id, request->audit.u.tsk);
+ break;
+ case LANDLOCK_REQUEST_SCOPE_ABSTRACT_UNIX_SOCKET:
+ if (trace_landlock_deny_scope_abstract_unix_socket_enabled())
+ trace_landlock_deny_scope_abstract_unix_socket(
+ youngest_denied, same_exec, logged,
+ request->other_domain_id,
+ request->audit.u.net->sk);
+ break;
default:
+ WARN_ONCE(1, "Unhandled Landlock request type %d",
+ request->type);
break;
}
}
--
2.54.0
^ permalink raw reply related
* [PATCH v3 12/20] landlock: Add tracepoints for rule checking
From: Mickaël Salaün @ 2026-07-22 17:11 UTC (permalink / raw)
To: Günther Noack, Steven Rostedt
Cc: Mickaël Salaün, Christian Brauner, Jann Horn, Jeff Xu,
Justin Suess, Kees Cook, Masami Hiramatsu, Mathieu Desnoyers,
Matthieu Buffet, Mikhail Ivanov, Tingmao Wang, kernel-team,
linux-security-module, linux-trace-kernel
In-Reply-To: <20260722171159.2776765-1-mic@digikod.net>
Merge landlock_find_rule() into landlock_unmask_layers() so rule
pointers stay inside the domain implementation while unmask checking
gets the matched rule it needs for the check_rule tracepoint.
landlock_unmask_layers() now takes a landlock_id and the domain instead
of a rule pointer. A rename or link evaluates the same dentry against
both renamed parents, so this path now looks the rule up once per
parent; collapsing that back to a single lookup is left to a follow-up.
Emit, via the per-type wrappers unmask_layers_fs() and
unmask_layers_net(), the rights each matching rule grants at every
domain layer. The events carry this as a dynamic per-layer array (up to
LANDLOCK_MAX_NUM_LAYERS entries) reserved from the trace ring buffer,
not the caller's stack, and rendered symbolically per layer. A
WARN_ON_ONCE() in __trace_landlock_fill_layers() flags a rule whose
layer levels fall outside the domain range or are unsorted, a
cannot-happen case; the zero-filled slots keep the rendered output and
the array bounds safe regardless.
Setting allowed_parent2 to true for non-dom-check requests when
get_inode_id() returns false preserves the pre-refactoring behavior: a
negative dentry (no backing inode) has no matching rule, so the access
is allowed at this path component. Before the refactoring,
landlock_unmask_layers() with a NULL rule produced this result as a side
effect; now the caller must set it explicitly.
Name the trace-only check_rule fields so each printk label equals its
ring-buffer field name and works directly as an ftrace filter: the
request field is labelled access_request= and the per-layer array is
named grants. Values audit also logs keep audit's label (domain=,
ruleset=) so a single filter works across trace and audit.
Cc: Günther Noack <gnoack@google.com>
Cc: Justin Suess <utilityemal77@gmail.com>
Cc: Masami Hiramatsu <mhiramat@kernel.org>
Cc: Mathieu Desnoyers <mathieu.desnoyers@efficios.com>
Cc: Steven Rostedt <rostedt@goodmis.org>
Cc: Tingmao Wang <m@maowtm.org>
Signed-off-by: Mickaël Salaün <mic@digikod.net>
---
Changes since v2:
https://patch.msgid.link/20260406143717.1815792-10-mic@digikod.net
- Order the check_rule tracepoint arguments with the common part
(domain, rule) before the event-specific access_request and object.
- Reformatted TP_STRUCT__entry and TP_fast_assign to kernel tracing
convention (requested by Steven Rostedt).
- Render the request access right as symbolic names with
__print_flags(), shared with the audit blocker names.
- Rename the per-layer field label allowed= to grants= and render it
symbolically via __print_landlock_layers(), intersected with the
request to show the granted requested rights per layer (was a raw hex
value).
- Rename struct layer_access_masks to the base's struct layer_masks.
- Rename the check_rule printk label request= to access_request= and the
per-layer ring-buffer field layers to grants, so each trace-only field
and its printk label share one name (usable directly as an ftrace
filter); audit-shared labels (domain=, ruleset=) are unchanged.
Changes since v1:
https://patch.msgid.link/20250523165741.693976-6-mic@digikod.net
- Merged find-rule consolidation (v1 2/5) into this patch.
- Added check_rule_net tracepoint for network rules.
- Added get_inode_id() helper with rcu_access_pointer().
- Added allowed_parent2 behavioral fix.
---
include/trace/events/landlock.h | 203 ++++++++++++++++++++++++++++++++
security/landlock/domain.c | 32 +++--
security/landlock/domain.h | 10 +-
security/landlock/fs.c | 149 +++++++++++++++--------
security/landlock/net.c | 21 +++-
5 files changed, 350 insertions(+), 65 deletions(-)
diff --git a/include/trace/events/landlock.h b/include/trace/events/landlock.h
index 7f221d8fff38..c693248afe23 100644
--- a/include/trace/events/landlock.h
+++ b/include/trace/events/landlock.h
@@ -11,10 +11,13 @@
#define _TRACE_LANDLOCK_H
#include <linux/landlock.h>
+#include <linux/trace_seq.h>
#include <linux/tracepoint.h>
+struct dentry;
struct landlock_domain;
struct landlock_hierarchy;
+struct landlock_rule;
struct landlock_ruleset;
struct path;
@@ -74,7 +77,110 @@ struct path;
* (e.g. network ports are __u64 in host endianness, like
* landlock_net_port_attr.port). Per-event details, such as where a value
* is byte-swapped, live in the field's own kdoc.
+ *
+ * Rule-check fields
+ * ~~~~~~~~~~~~~~~~~
+ *
+ * The check_rule events fire during an access check, once per matching
+ * rule, before the final allow-or-deny verdict. They share domain (the
+ * enforcing domain being evaluated), access_request (the access mask being
+ * checked), and rule (the matching rule, with per-layer access masks).
+ */
+
+#ifdef CREATE_TRACE_POINTS
+
+/*
+ * Fills the dense per-domain-layer array layers (one access mask per layer,
+ * indexed by level - 1) from rule's sparse layer stack, keeping only the
+ * requested rights (access_request). Layers with no matching rule entry get
+ * a zero mask. Shared by the check_rule_fs and check_rule_net events.
+ *
+ * rule->layers is sorted by ascending level, with levels in the domain's
+ * [1, num_layers] range (see landlock_merge_ruleset()), so every entry maps
+ * to a slot. A leftover entry would be a malformed rule; the zero-filled
+ * slots keep the output and the array bounds safe regardless.
*/
+static inline void
+__trace_landlock_fill_layers(access_mask_t *const layers,
+ const size_t num_layers,
+ const struct landlock_rule *const rule,
+ const access_mask_t access_request)
+{
+ size_t i = 0;
+
+ for (size_t level = 1; level <= num_layers; level++) {
+ access_mask_t grants = 0;
+
+ if (i < rule->num_layers && level == rule->layers[i].level) {
+ grants = rule->layers[i].access & access_request;
+ i++;
+ }
+ layers[level - 1] = grants;
+ }
+
+ /* A leftover entry means an out-of-range or unsorted rule level. */
+ WARN_ON_ONCE(i < rule->num_layers);
+}
+
+/*
+ * Renders the dense per-domain-layer access array as symbolic flag names for
+ * the grants field: layers wrapped in "{}", flags within a layer joined by
+ * "|", layers separated by ",", an empty layer rendered as nothing.
+ * Open-codes the flag walk because trace_print_flags_seq() NUL-terminates per
+ * call and so cannot be chained into a single field. The shared names table
+ * covers every access right, so masked bits are always named. Returns the
+ * trace_seq position like __print_flags().
+ */
+static inline const char *
+__trace_landlock_print_layers(struct trace_seq *p,
+ const access_mask_t *const layers,
+ const size_t num_layers,
+ const struct trace_print_flags *const names,
+ const size_t names_size)
+{
+ const char *const ret = trace_seq_buffer_ptr(p);
+
+ trace_seq_putc(p, '{');
+ for (size_t i = 0; i < num_layers; i++) {
+ access_mask_t mask = layers[i];
+ bool first = true;
+
+ if (i)
+ trace_seq_putc(p, ',');
+ for (size_t j = 0; mask && j < names_size; j++) {
+ if ((mask & names[j].mask) != names[j].mask)
+ continue;
+ if (!first)
+ trace_seq_putc(p, '|');
+ trace_seq_puts(p, names[j].name);
+ mask &= ~names[j].mask;
+ first = false;
+ }
+ }
+ trace_seq_putc(p, '}');
+ trace_seq_putc(p, 0);
+ return ret;
+}
+
+#endif /* CREATE_TRACE_POINTS */
+
+/*
+ * Prints a per-layer access mask array (the dynamic array @array) as symbolic
+ * flag names using the shared @flag_names list (a _LANDLOCK_*_NAMES macro).
+ * Stays outside CREATE_TRACE_POINTS: TP_printk is expanded in the print-output
+ * pass where that macro is undefined.
+ */
+#define __print_landlock_layers(array, flag_names...) \
+ ({ \
+ static const struct trace_print_flags __layer_names[] = { \
+ flag_names \
+ }; \
+ __trace_landlock_print_layers( \
+ p, __get_dynamic_array(array), \
+ __get_dynamic_array_len(array) / \
+ sizeof(access_mask_t), \
+ __layer_names, ARRAY_SIZE(__layer_names)); \
+ })
/**
* landlock_create_ruleset - New ruleset created
@@ -374,6 +480,103 @@ TRACE_EVENT(landlock_free_domain,
__entry->domain_id, __entry->denials)
);
+/**
+ * landlock_check_rule_fs - Filesystem rule evaluated during access check
+ *
+ * @domain: Enforcing domain (never NULL).
+ * @rule: Matching rule with per-layer access masks (never NULL).
+ * @access_request: Access mask evaluated against the rule (the domain's
+ * handled mask during rename/link double-checks).
+ * @dentry: Filesystem dentry being checked (never NULL).
+ *
+ * Emitted for each rule that matches during a filesystem access check.
+ * The grants array shows the requested rights the rule grants at each
+ * domain layer. See Documentation/trace/events-landlock.rst for how to
+ * interpret it.
+ */
+TRACE_EVENT(landlock_check_rule_fs,
+
+ TP_PROTO(const struct landlock_domain *domain,
+ const struct landlock_rule *rule,
+ access_mask_t access_request, const struct dentry *dentry),
+
+ TP_ARGS(domain, rule, access_request, dentry),
+
+ TP_STRUCT__entry(
+ __field( __u64, domain_id )
+ __field( access_mask_t, access_request )
+ __field( dev_t, dev )
+ __field( ino_t, ino )
+ __dynamic_array(access_mask_t, grants,
+ domain->num_layers)
+ ),
+
+ TP_fast_assign(
+ __entry->domain_id = domain->hierarchy->id;
+ __entry->access_request = access_request;
+ __entry->dev = dentry->d_sb->s_dev;
+ __entry->ino = d_backing_inode(dentry)->i_ino;
+
+ __trace_landlock_fill_layers(__get_dynamic_array(grants),
+ __get_dynamic_array_len(grants) /
+ sizeof(access_mask_t),
+ rule, access_request);
+ ),
+
+ TP_printk("domain=%llx access_request=%s dev=%u:%u ino=%lu grants=%s",
+ __entry->domain_id,
+ __print_flags(__entry->access_request, "|", _LANDLOCK_ACCESS_FS_NAMES),
+ MAJOR(__entry->dev), MINOR(__entry->dev), __entry->ino,
+ __print_landlock_layers(grants, _LANDLOCK_ACCESS_FS_NAMES))
+);
+
+/**
+ * landlock_check_rule_net - Network port rule evaluated during access check
+ *
+ * @domain: Enforcing domain (never NULL).
+ * @rule: Matching rule with per-layer access masks (never NULL).
+ * @access_request: Access mask being requested.
+ * @port: Network port being checked (host endianness).
+ *
+ * Emitted for each rule that matches during a network access check. The
+ * grants array shows the requested rights the rule grants at each domain
+ * layer. See Documentation/trace/events-landlock.rst for how to
+ * interpret it.
+ */
+TRACE_EVENT(landlock_check_rule_net,
+
+ TP_PROTO(const struct landlock_domain *domain,
+ const struct landlock_rule *rule,
+ access_mask_t access_request, __u64 port),
+
+ TP_ARGS(domain, rule, access_request, port),
+
+ TP_STRUCT__entry(
+ __field( __u64, domain_id )
+ __field( access_mask_t, access_request )
+ __field( __u64, port )
+ __dynamic_array(access_mask_t, grants,
+ domain->num_layers)
+ ),
+
+ TP_fast_assign(
+ __entry->domain_id = domain->hierarchy->id;
+ __entry->access_request = access_request;
+ __entry->port = port;
+
+ __trace_landlock_fill_layers(__get_dynamic_array(grants),
+ __get_dynamic_array_len(grants) /
+ sizeof(access_mask_t),
+ rule, access_request);
+ ),
+
+ TP_printk("domain=%llx access_request=%s port=%llu grants=%s",
+ __entry->domain_id,
+ __print_flags(__entry->access_request, "|", _LANDLOCK_ACCESS_NET_NAMES),
+ __entry->port,
+ __print_landlock_layers(grants, _LANDLOCK_ACCESS_NET_NAMES))
+);
+
#undef _LANDLOCK_NAME_ENTRY
#endif /* _TRACE_LANDLOCK_H */
diff --git a/security/landlock/domain.c b/security/landlock/domain.c
index 082c4da68536..c20020024de4 100644
--- a/security/landlock/domain.c
+++ b/security/landlock/domain.c
@@ -97,9 +97,9 @@ void landlock_put_domain_deferred(struct landlock_domain *const domain)
}
/* The returned access has the same lifetime as the domain. */
-const struct landlock_rule *
-landlock_find_rule(const struct landlock_domain *const domain,
- const struct landlock_id id)
+static const struct landlock_rule *
+find_rule(const struct landlock_domain *const domain,
+ const struct landlock_id id)
{
const struct rb_root *root;
const struct rb_node *node;
@@ -126,26 +126,38 @@ landlock_find_rule(const struct landlock_domain *const domain,
/**
* landlock_unmask_layers - Remove the access rights in @masks which are
- * granted in @rule
+ * granted by a matching rule
*
- * Updates the set of (per-layer) unfulfilled access rights @masks so that all
- * the access rights granted in @rule are removed from it (because they are now
- * fulfilled).
+ * Looks up the rule matching @id in @domain, then updates the set of
+ * (per-layer) unfulfilled access rights @masks so that all the access rights
+ * granted by that rule are removed (because they are now fulfilled).
*
- * @rule: A rule that grants a set of access rights for each layer.
+ * @domain: The Landlock domain to search for a matching rule.
+ * @id: Identifier for the rule target (e.g. inode, port).
* @masks: A matrix of unfulfilled access rights for each layer.
+ * @matched_rule: Optional output for the matched rule (for tracing); set to
+ * the matching rule when non-NULL, unchanged otherwise.
*
* Return: True if the request is allowed (i.e. the access rights granted all
* remaining unfulfilled access rights and masks has no leftover set bits).
*/
-bool landlock_unmask_layers(const struct landlock_rule *const rule,
- struct layer_masks *masks)
+bool landlock_unmask_layers(const struct landlock_domain *const domain,
+ const struct landlock_id id,
+ struct layer_masks *masks,
+ const struct landlock_rule **matched_rule)
{
+ const struct landlock_rule *rule;
+
if (!masks)
return true;
+
+ rule = find_rule(domain, id);
if (!rule)
return false;
+ if (matched_rule)
+ *matched_rule = rule;
+
/*
* An access is granted if, for each policy layer, at least one rule
* encountered on the pathwalk grants the requested access, regardless
diff --git a/security/landlock/domain.h b/security/landlock/domain.h
index 8351e22016fe..5baa4a73b446 100644
--- a/security/landlock/domain.h
+++ b/security/landlock/domain.h
@@ -305,12 +305,10 @@ struct landlock_domain *
landlock_merge_ruleset(struct landlock_domain *const parent,
struct landlock_ruleset *const ruleset);
-const struct landlock_rule *
-landlock_find_rule(const struct landlock_domain *const domain,
- const struct landlock_id id);
-
-bool landlock_unmask_layers(const struct landlock_rule *const rule,
- struct layer_masks *masks);
+bool landlock_unmask_layers(const struct landlock_domain *const domain,
+ const struct landlock_id id,
+ struct layer_masks *masks,
+ const struct landlock_rule **matched_rule);
access_mask_t
landlock_init_layer_masks(const struct landlock_domain *const domain,
diff --git a/security/landlock/fs.c b/security/landlock/fs.c
index 48744a21d0a3..fe028aac26ae 100644
--- a/security/landlock/fs.c
+++ b/security/landlock/fs.c
@@ -376,31 +376,55 @@ int landlock_append_fs_rule(struct landlock_ruleset *const ruleset,
/* Access-control management */
-/*
- * The lifetime of the returned rule is tied to @domain.
+/**
+ * get_inode_id - Look up the Landlock object for a dentry
+ * @dentry: The dentry to look up.
+ * @id: Filled with the inode's Landlock object pointer on success.
+ *
+ * Extracts the Landlock object pointer from @dentry's inode security blob and
+ * stores it in @id for use as a rule-tree lookup key.
*
- * Returns NULL if no rule is found or if @dentry is negative.
+ * When this returns false (negative dentry or no Landlock object), no rule can
+ * match this inode, so landlock_unmask_layers() need not be called. Callers
+ * that gate landlock_unmask_layers() on this function must handle the NULL
+ * masks case independently, since the !masks-returns-true early-return in
+ * landlock_unmask_layers() will not be reached. See the allowed_parent2
+ * initialization in is_access_to_paths_allowed().
+ *
+ * Return: True if a Landlock object exists for @dentry, false otherwise.
*/
-static const struct landlock_rule *
-find_rule(const struct landlock_domain *const domain,
- const struct dentry *const dentry)
+static bool get_inode_id(const struct dentry *const dentry,
+ struct landlock_id *id)
{
- const struct landlock_rule *rule;
- const struct inode *inode;
- struct landlock_id id = {
- .type = LANDLOCK_KEY_INODE,
- };
-
/* Ignores nonexistent leafs. */
if (d_is_negative(dentry))
- return NULL;
+ return false;
- inode = d_backing_inode(dentry);
- rcu_read_lock();
- id.key.object = rcu_dereference(landlock_inode(inode)->object);
- rule = landlock_find_rule(domain, id);
- rcu_read_unlock();
- return rule;
+ /*
+ * rcu_access_pointer() is sufficient: the pointer is used only as a
+ * numeric comparison key for rule lookup, not dereferenced. The object
+ * cannot be freed while the domain exists because the domain's rule
+ * tree holds its own reference to it.
+ */
+ id->key.object = rcu_access_pointer(
+ landlock_inode(d_backing_inode(dentry))->object);
+ return !!id->key.object;
+}
+
+static bool unmask_layers_fs(const struct landlock_domain *const domain,
+ const struct landlock_id id,
+ const access_mask_t access_request,
+ struct layer_masks *masks,
+ const struct dentry *const dentry)
+{
+ const struct landlock_rule *rule = NULL;
+ bool ret;
+
+ ret = landlock_unmask_layers(domain, id, masks, &rule);
+ if (rule)
+ trace_landlock_check_rule_fs(domain, rule, access_request,
+ dentry);
+ return ret;
}
/*
@@ -781,6 +805,9 @@ is_access_to_paths_allowed(const struct landlock_domain *const domain,
bool allowed_parent1 = false, allowed_parent2 = false, is_dom_check,
child1_is_directory = true, child2_is_directory = true;
struct path walker_path;
+ struct landlock_id id = {
+ .type = LANDLOCK_KEY_INODE,
+ };
access_mask_t access_masked_parent1, access_masked_parent2;
struct layer_masks _layer_masks_child1, _layer_masks_child2;
struct layer_masks *layer_masks_child1 = NULL,
@@ -820,28 +847,46 @@ is_access_to_paths_allowed(const struct landlock_domain *const domain,
/* For a simple request, only check for requested accesses. */
access_masked_parent1 = access_request_parent1;
access_masked_parent2 = access_request_parent2;
+ /*
+ * Simple requests have no parent2 to check, so parent2 is
+ * trivially allowed. This must be set explicitly because the
+ * get_inode_id() gate in the pathwalk loop may prevent
+ * landlock_unmask_layers() from being called (which would
+ * otherwise return true for NULL masks as a side effect).
+ */
+ allowed_parent2 = true;
is_dom_check = false;
}
if (unlikely(dentry_child1)) {
- /*
- * Get the layer masks for the child dentries for use by domain
- * check later.
- */
- if (landlock_init_layer_masks(domain, LANDLOCK_MASK_ACCESS_FS,
- &_layer_masks_child1,
- LANDLOCK_KEY_INODE))
- landlock_unmask_layers(find_rule(domain, dentry_child1),
- &_layer_masks_child1);
+ struct landlock_id id = {
+ .type = LANDLOCK_KEY_INODE,
+ };
+ access_mask_t handled;
+
+ handled = landlock_init_layer_masks(domain,
+ LANDLOCK_MASK_ACCESS_FS,
+ &_layer_masks_child1,
+ LANDLOCK_KEY_INODE);
+ if (handled && get_inode_id(dentry_child1, &id))
+ unmask_layers_fs(domain, id, handled,
+ &_layer_masks_child1, dentry_child1);
layer_masks_child1 = &_layer_masks_child1;
child1_is_directory = d_is_dir(dentry_child1);
}
if (unlikely(dentry_child2)) {
- if (landlock_init_layer_masks(domain, LANDLOCK_MASK_ACCESS_FS,
- &_layer_masks_child2,
- LANDLOCK_KEY_INODE))
- landlock_unmask_layers(find_rule(domain, dentry_child2),
- &_layer_masks_child2);
+ struct landlock_id id = {
+ .type = LANDLOCK_KEY_INODE,
+ };
+ access_mask_t handled;
+
+ handled = landlock_init_layer_masks(domain,
+ LANDLOCK_MASK_ACCESS_FS,
+ &_layer_masks_child2,
+ LANDLOCK_KEY_INODE);
+ if (handled && get_inode_id(dentry_child2, &id))
+ unmask_layers_fs(domain, id, handled,
+ &_layer_masks_child2, dentry_child2);
layer_masks_child2 = &_layer_masks_child2;
child2_is_directory = d_is_dir(dentry_child2);
}
@@ -853,8 +898,6 @@ is_access_to_paths_allowed(const struct landlock_domain *const domain,
* restriction.
*/
while (true) {
- const struct landlock_rule *rule;
-
/*
* If at least all accesses allowed on the destination are
* already allowed on the source, respectively if there is at
@@ -895,13 +938,20 @@ is_access_to_paths_allowed(const struct landlock_domain *const domain,
break;
}
- rule = find_rule(domain, walker_path.dentry);
- allowed_parent1 =
- allowed_parent1 ||
- landlock_unmask_layers(rule, layer_masks_parent1);
- allowed_parent2 =
- allowed_parent2 ||
- landlock_unmask_layers(rule, layer_masks_parent2);
+ if (get_inode_id(walker_path.dentry, &id)) {
+ allowed_parent1 =
+ allowed_parent1 ||
+ unmask_layers_fs(domain, id,
+ access_masked_parent1,
+ layer_masks_parent1,
+ walker_path.dentry);
+ allowed_parent2 =
+ allowed_parent2 ||
+ unmask_layers_fs(domain, id,
+ access_masked_parent2,
+ layer_masks_parent2,
+ walker_path.dentry);
+ }
/* Stops when a rule from each layer grants access. */
if (allowed_parent1 && allowed_parent2)
@@ -1064,23 +1114,30 @@ static bool collect_domain_accesses(const struct landlock_domain *const domain,
struct layer_masks *layer_masks_dom)
{
bool ret = false;
+ access_mask_t access_masked_dom;
if (WARN_ON_ONCE(!domain || !mnt_root || !dir || !layer_masks_dom))
return true;
if (is_nouser_or_private(dir))
return true;
- if (!landlock_init_layer_masks(domain, LANDLOCK_MASK_ACCESS_FS,
- layer_masks_dom, LANDLOCK_KEY_INODE))
+ access_masked_dom =
+ landlock_init_layer_masks(domain, LANDLOCK_MASK_ACCESS_FS,
+ layer_masks_dom, LANDLOCK_KEY_INODE);
+ if (!access_masked_dom)
return true;
dget(dir);
while (true) {
struct dentry *parent_dentry;
+ struct landlock_id id = {
+ .type = LANDLOCK_KEY_INODE,
+ };
/* Gets all layers allowing all domain accesses. */
- if (landlock_unmask_layers(find_rule(domain, dir),
- layer_masks_dom)) {
+ if (get_inode_id(dir, &id) &&
+ unmask_layers_fs(domain, id, access_masked_dom,
+ layer_masks_dom, dir)) {
/*
* Stops when all handled accesses are allowed by at
* least one rule in each layer.
diff --git a/security/landlock/net.c b/security/landlock/net.c
index ead97fcfdcff..8f2aaac54b33 100644
--- a/security/landlock/net.c
+++ b/security/landlock/net.c
@@ -53,6 +53,22 @@ int landlock_append_net_rule(struct landlock_ruleset *const ruleset,
return err;
}
+static bool unmask_layers_net(const struct landlock_domain *const domain,
+ const struct landlock_id id,
+ struct layer_masks *masks,
+ access_mask_t access_request)
+{
+ const struct landlock_rule *rule = NULL;
+ bool ret;
+
+ ret = landlock_unmask_layers(domain, id, masks, &rule);
+ if (rule)
+ trace_landlock_check_rule_net(
+ domain, rule, access_request,
+ ntohs((__force __be16)id.key.data));
+ return ret;
+}
+
static int current_check_access_socket(struct socket *const sock,
struct sockaddr *const address,
const int addrlen,
@@ -62,7 +78,6 @@ static int current_check_access_socket(struct socket *const sock,
unsigned short sock_family;
__be16 port;
struct layer_masks layer_masks = {};
- const struct landlock_rule *rule;
struct landlock_id id = {
.type = LANDLOCK_KEY_NET_PORT,
};
@@ -248,14 +263,14 @@ static int current_check_access_socket(struct socket *const sock,
id.key.data = (__force uintptr_t)port;
BUILD_BUG_ON(sizeof(port) > sizeof(id.key.data));
- rule = landlock_find_rule(subject->domain, id);
access_request = landlock_init_layer_masks(subject->domain,
access_request, &layer_masks,
LANDLOCK_KEY_NET_PORT);
if (!access_request)
return 0;
- if (landlock_unmask_layers(rule, &layer_masks))
+ if (unmask_layers_net(subject->domain, id, &layer_masks,
+ access_request))
return 0;
audit_net.family = address->sa_family;
--
2.54.0
^ permalink raw reply related
* [PATCH v3 15/20] selftests/landlock: Add trace event test infrastructure and tests
From: Mickaël Salaün @ 2026-07-22 17:11 UTC (permalink / raw)
To: Günther Noack, Steven Rostedt
Cc: Mickaël Salaün, Christian Brauner, Jann Horn, Jeff Xu,
Justin Suess, Kees Cook, Masami Hiramatsu, Mathieu Desnoyers,
Matthieu Buffet, Mikhail Ivanov, Tingmao Wang, kernel-team,
linux-security-module, linux-trace-kernel
In-Reply-To: <20260722171159.2776765-1-mic@digikod.net>
Add tracefs test infrastructure in trace.h: helpers for mounting
tracefs, enabling/disabling events, reading the trace buffer, counting
regex matches, and extracting field values, plus per-event regex
patterns. The patterns are anchored with ^ and $, verify every
TP_printk field, and use no unescaped dot characters; TRACE_PREFIX
matches the ftrace line format with either the expected task name
(truncated to TASK_COMM_LEN - 1) or "<...>" for an evicted comm cache
entry.
Add trace_test.c with the trace fixture (setup enables all available
events with a PID filter, teardown disables and clears) and the
lifecycle, API, denial-field, and log-flag tests. Extend the existing
true helper to open its working directory before exiting, triggering a
read_dir denial inside a sandbox, so the exec-based tests can verify
same_exec and the logged decision across an exec. Move regex_escape()
from audit.h to common.h for shared use by the audit and trace tests.
Enable CONFIG_ENABLE_DEFAULT_TRACERS alongside CONFIG_FTRACE in the
selftest config: CONFIG_FTRACE alone only enables the tracer menu
without activating any tracer, while CONFIG_ENABLE_DEFAULT_TRACERS
selects TRACING (and thus TRACEPOINTS and event tracing) without
depending on architecture-specific syscall tracepoints. When
CONFIG_FTRACE is disabled it cannot be set, so TRACEPOINTS is correctly
disabled too.
Cc: Günther Noack <gnoack@google.com>
Cc: Tingmao Wang <m@maowtm.org>
Signed-off-by: Mickaël Salaün <mic@digikod.net>
---
Changes since v2:
https://patch.msgid.link/20260406143717.1815792-14-mic@digikod.net
- Renamed the restrict_self trace matchers (REGEX_RESTRICT_SELF,
TRACEFS_RESTRICT_SELF_ENABLE) and the restrict_self tests to
create_domain.
- Trim the intentionally-elided-coverage comment: drop the
check_rule_net and ptrace-TRACEME notes (both now have dedicated
trace tests) and the stale claim that TRACEME routes through
hook_ptrace_access_check.
- Updated add_rule_net_fields test expected access mask to include the
new UDP access bits (LANDLOCK_ACCESS_NET_BIND_UDP,
LANDLOCK_ACCESS_NET_CONNECT_SEND_UDP).
- Assert the symbolic access-right names in the trace field tests
instead of hex masks.
- Switched the selftest config from CONFIG_FTRACE_SYSCALLS to
CONFIG_ENABLE_DEFAULT_TRACERS, which selects TRACING without an
architecture-specific syscall-tracepoint dependency.
- Updated the denial field tests to assert the single logged field
instead of log_same_exec and log_new_exec; log_flags_subdomains_off
now checks logged==0 (the case the raw flags could not express).
- Follow the check_rule_fs printk label rename request= to
access_request= (the trace-only field and its label now share one
name): define REGEX_CHECK_RULE_FS with the final access_request=
label, since restrict_self already matches check_rule_fs events
here and the kernel emits the renamed label.
- Define REGEX_CHECK_RULE_NET with the final access_request= label
(same check_rule request= to access_request= rename), so the shared
matcher carries its final form from the patch that introduces trace.h
rather than being updated in a later test patch.
- Define REGEX_DENY_PTRACE and REGEX_DENY_SCOPE_SIGNAL with the final
role-prefixed tracee_comm= and target_comm= labels (deny_ptrace and
deny_scope_signal printk label rename comm= to *_comm=, each label now
matching its sibling *_pid field), so both matchers carry their final
form from the patch that introduces trace.h; the per-layer domain
fields and the tests that consume the labels are added later.
- Reuse a shared scope-based ruleset helper (build_enforce_ruleset) in
the trace tests: define it here and call it from create_domain_nested
and create_domain_invalid, which only need a domain.
Changes since v1:
- New patch.
---
tools/testing/selftests/landlock/audit.h | 35 -
tools/testing/selftests/landlock/common.h | 47 +
tools/testing/selftests/landlock/config | 2 +
tools/testing/selftests/landlock/trace.h | 634 ++++++++++
tools/testing/selftests/landlock/trace_test.c | 1101 +++++++++++++++++
tools/testing/selftests/landlock/true.c | 10 +
6 files changed, 1794 insertions(+), 35 deletions(-)
create mode 100644 tools/testing/selftests/landlock/trace.h
create mode 100644 tools/testing/selftests/landlock/trace_test.c
diff --git a/tools/testing/selftests/landlock/audit.h b/tools/testing/selftests/landlock/audit.h
index f45fdef35681..d428ce802f49 100644
--- a/tools/testing/selftests/landlock/audit.h
+++ b/tools/testing/selftests/landlock/audit.h
@@ -214,41 +214,6 @@ static int audit_set_status(int fd, __u32 key, __u32 val)
return audit_request(fd, &msg, NULL);
}
-/* Returns a pointer to the last filled character of @dst, which is `\0`. */
-static __maybe_unused char *regex_escape(const char *const src, char *dst,
- size_t dst_size)
-{
- char *d = dst;
-
- for (const char *s = src; *s; s++) {
- switch (*s) {
- case '$':
- case '*':
- case '.':
- case '[':
- case '\\':
- case ']':
- case '^':
- if (d >= dst + dst_size - 2)
- return (char *)-ENOMEM;
-
- *d++ = '\\';
- *d++ = *s;
- break;
- default:
- if (d >= dst + dst_size - 1)
- return (char *)-ENOMEM;
-
- *d++ = *s;
- }
- }
- if (d >= dst + dst_size - 1)
- return (char *)-ENOMEM;
-
- *d = '\0';
- return d;
-}
-
/*
* @domain_id: The domain ID extracted from the audit message (if the first part
* of @pattern is REGEX_LANDLOCK_PREFIX). It is set to 0 if the domain ID is
diff --git a/tools/testing/selftests/landlock/common.h b/tools/testing/selftests/landlock/common.h
index 7206d5105d66..c5124de68a51 100644
--- a/tools/testing/selftests/landlock/common.h
+++ b/tools/testing/selftests/landlock/common.h
@@ -253,3 +253,50 @@ static void __maybe_unused set_unix_address(struct service_fixture *const srv,
srv->unix_addr_len = SUN_LEN(&srv->unix_addr);
srv->unix_addr.sun_path[0] = '\0';
}
+
+/**
+ * regex_escape - Escape BRE metacharacters in a string
+ *
+ * @src: Source string to escape.
+ * @dst: Destination buffer for the escaped string.
+ * @dst_size: Size of the destination buffer.
+ *
+ * Escapes characters that have special meaning in POSIX Basic Regular
+ * Expressions: $ * . [ \ ] ^
+ *
+ * Returns a pointer to the NUL terminator in @dst (cursor-style API for
+ * chaining), or (char *)-ENOMEM if the buffer is too small.
+ */
+static __maybe_unused char *regex_escape(const char *const src, char *dst,
+ size_t dst_size)
+{
+ char *d = dst;
+
+ for (const char *s = src; *s; s++) {
+ switch (*s) {
+ case '$':
+ case '*':
+ case '.':
+ case '[':
+ case '\\':
+ case ']':
+ case '^':
+ if (d >= dst + dst_size - 2)
+ return (char *)-ENOMEM;
+
+ *d++ = '\\';
+ *d++ = *s;
+ break;
+ default:
+ if (d >= dst + dst_size - 1)
+ return (char *)-ENOMEM;
+
+ *d++ = *s;
+ }
+ }
+ if (d >= dst + dst_size - 1)
+ return (char *)-ENOMEM;
+
+ *d = '\0';
+ return d;
+}
diff --git a/tools/testing/selftests/landlock/config b/tools/testing/selftests/landlock/config
index 8fe9b461b1fd..d86321936fd8 100644
--- a/tools/testing/selftests/landlock/config
+++ b/tools/testing/selftests/landlock/config
@@ -2,6 +2,8 @@ CONFIG_AF_UNIX_OOB=y
CONFIG_AUDIT=y
CONFIG_CGROUPS=y
CONFIG_CGROUP_SCHED=y
+CONFIG_ENABLE_DEFAULT_TRACERS=y
+CONFIG_FTRACE=y
CONFIG_INET=y
CONFIG_IPV6=y
CONFIG_KEYS=y
diff --git a/tools/testing/selftests/landlock/trace.h b/tools/testing/selftests/landlock/trace.h
new file mode 100644
index 000000000000..31f17b43e9f4
--- /dev/null
+++ b/tools/testing/selftests/landlock/trace.h
@@ -0,0 +1,634 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Landlock trace test helpers
+ *
+ * Copyright © 2026 Cloudflare, Inc.
+ */
+
+#define _GNU_SOURCE
+#include <errno.h>
+#include <fcntl.h>
+#include <regex.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/mount.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#include "kselftest_harness.h"
+
+#define TRACEFS_ROOT "/sys/kernel/tracing"
+#define TRACEFS_LANDLOCK_DIR TRACEFS_ROOT "/events/landlock"
+#define TRACEFS_CREATE_RULESET_ENABLE \
+ TRACEFS_LANDLOCK_DIR "/landlock_create_ruleset/enable"
+#define TRACEFS_CREATE_DOMAIN_ENABLE \
+ TRACEFS_LANDLOCK_DIR "/landlock_create_domain/enable"
+#define TRACEFS_ADD_RULE_FS_ENABLE \
+ TRACEFS_LANDLOCK_DIR "/landlock_add_rule_fs/enable"
+#define TRACEFS_ADD_RULE_NET_ENABLE \
+ TRACEFS_LANDLOCK_DIR "/landlock_add_rule_net/enable"
+#define TRACEFS_CHECK_RULE_FS_ENABLE \
+ TRACEFS_LANDLOCK_DIR "/landlock_check_rule_fs/enable"
+#define TRACEFS_CHECK_RULE_NET_ENABLE \
+ TRACEFS_LANDLOCK_DIR "/landlock_check_rule_net/enable"
+#define TRACEFS_DENY_ACCESS_FS_ENABLE \
+ TRACEFS_LANDLOCK_DIR "/landlock_deny_access_fs/enable"
+#define TRACEFS_DENY_ACCESS_NET_ENABLE \
+ TRACEFS_LANDLOCK_DIR "/landlock_deny_access_net/enable"
+#define TRACEFS_DENY_PTRACE_ENABLE \
+ TRACEFS_LANDLOCK_DIR "/landlock_deny_ptrace/enable"
+#define TRACEFS_DENY_SCOPE_SIGNAL_ENABLE \
+ TRACEFS_LANDLOCK_DIR "/landlock_deny_scope_signal/enable"
+#define TRACEFS_DENY_SCOPE_ABSTRACT_UNIX_SOCKET_ENABLE \
+ TRACEFS_LANDLOCK_DIR \
+ "/landlock_deny_scope_abstract_unix_socket/enable"
+#define TRACEFS_FREE_DOMAIN_ENABLE \
+ TRACEFS_LANDLOCK_DIR "/landlock_free_domain/enable"
+#define TRACEFS_FREE_RULESET_ENABLE \
+ TRACEFS_LANDLOCK_DIR "/landlock_free_ruleset/enable"
+#define TRACEFS_TRACE TRACEFS_ROOT "/trace"
+#define TRACEFS_SET_EVENT_PID TRACEFS_ROOT "/set_event_pid"
+#define TRACEFS_OPTIONS_EVENT_FORK TRACEFS_ROOT "/options/event-fork"
+
+#define TRACE_BUFFER_SIZE (64 * 1024)
+
+/*
+ * Trace line prefix: matches the ftrace "trace" file format. Format: "
+ * <task>-<pid> [<cpu>] <flags> <timestamp>: "
+ *
+ * The task parameter must be a string literal truncated to 15 chars
+ * (TASK_COMM_LEN - 1), matching what the kernel stores in task->comm. The
+ * pattern accepts either the expected task name or "<...>" because the ftrace
+ * comm cache may evict short-lived processes (e.g., forked children that exit
+ * before the trace buffer is read).
+ *
+ * No unescaped '.' in any REGEX macro; literal dots use '\\.'.
+ */
+#define TRACE_PREFIX(task) \
+ "^ *\\(<\\.\\.\\.>" \
+ "\\|" task "\\)" \
+ "-[0-9]\\+ *\\[[0-9]\\+\\] [^ ]\\+ \\+[0-9]\\+\\.[0-9]\\+: "
+
+/*
+ * Task name for events emitted by kworker threads (e.g., free_domain fires from
+ * a work queue, not from the test process).
+ */
+#define KWORKER_TASK "kworker/[0-9]\\+:[0-9]\\+"
+
+#define REGEX_ADD_RULE_FS(task) \
+ TRACE_PREFIX(task) \
+ "landlock_add_rule_fs: " \
+ "ruleset=[0-9a-f]\\+\\.[0-9]\\+ " \
+ "access_rights=[a-z_|]* " \
+ "dev=[0-9]\\+:[0-9]\\+ " \
+ "ino=[0-9]\\+ " \
+ "path=[^ ]\\+$"
+
+#define REGEX_ADD_RULE_NET(task) \
+ TRACE_PREFIX(task) \
+ "landlock_add_rule_net: " \
+ "ruleset=[0-9a-f]\\+\\.[0-9]\\+ " \
+ "access_rights=[a-z_|]* " \
+ "port=[0-9]\\+$"
+
+#define REGEX_CREATE_RULESET(task) \
+ TRACE_PREFIX(task) \
+ "landlock_create_ruleset: " \
+ "ruleset=[0-9a-f]\\+\\.[0-9]\\+ " \
+ "handled_fs=[a-z_|]* " \
+ "handled_net=[a-z_|]* " \
+ "scoped=[a-z_|]*$"
+
+#define REGEX_CREATE_DOMAIN(task) \
+ TRACE_PREFIX(task) \
+ "landlock_create_domain: " \
+ "domain=[0-9a-f]\\+ " \
+ "parent=[0-9a-f]\\+ " \
+ "ruleset=[0-9a-f]\\+\\.[0-9]\\+$"
+
+#define REGEX_CHECK_RULE_FS(task) \
+ TRACE_PREFIX(task) \
+ "landlock_check_rule_fs: " \
+ "domain=[0-9a-f]\\+ " \
+ "access_request=[a-z_|]* " \
+ "dev=[0-9]\\+:[0-9]\\+ " \
+ "ino=[0-9]\\+ " \
+ "grants={[a-z_|,]*}$"
+
+#define REGEX_CHECK_RULE_NET(task) \
+ TRACE_PREFIX(task) \
+ "landlock_check_rule_net: " \
+ "domain=[0-9a-f]\\+ " \
+ "access_request=[a-z_|]* " \
+ "port=[0-9]\\+ " \
+ "grants={[a-z_|,]*}$"
+
+#define REGEX_DENY_ACCESS_FS(task) \
+ TRACE_PREFIX(task) \
+ "landlock_deny_access_fs: " \
+ "domain=[0-9a-f]\\+ " \
+ "same_exec=[01] " \
+ "logged=[01] " \
+ "blockers=[a-z_|]* " \
+ "dev=[0-9]\\+:[0-9]\\+ " \
+ "ino=[0-9]\\+ " \
+ "path=[^ ]*$"
+
+#define REGEX_DENY_ACCESS_NET(task) \
+ TRACE_PREFIX(task) \
+ "landlock_deny_access_net: " \
+ "domain=[0-9a-f]\\+ " \
+ "same_exec=[01] " \
+ "logged=[01] " \
+ "blockers=[a-z_|]* " \
+ "sport=[0-9]\\+ " \
+ "dport=[0-9]\\+$"
+
+#define REGEX_DENY_PTRACE(task) \
+ TRACE_PREFIX(task) \
+ "landlock_deny_ptrace: " \
+ "domain=[0-9a-f]\\+ " \
+ "same_exec=[01] " \
+ "logged=[01] " \
+ "tracee_pid=[0-9]\\+ " \
+ "tracee_comm=[^ ]*$"
+
+#define REGEX_DENY_SCOPE_SIGNAL(task) \
+ TRACE_PREFIX(task) \
+ "landlock_deny_scope_signal: " \
+ "domain=[0-9a-f]\\+ " \
+ "same_exec=[01] " \
+ "logged=[01] " \
+ "target_pid=[0-9]\\+ " \
+ "target_comm=[^ ]*$"
+
+#define REGEX_DENY_SCOPE_ABSTRACT_UNIX_SOCKET(task) \
+ TRACE_PREFIX(task) \
+ "landlock_deny_scope_abstract_unix_socket: " \
+ "domain=[0-9a-f]\\+ " \
+ "same_exec=[01] " \
+ "logged=[01] " \
+ "peer_pid=[0-9]\\+ " \
+ "sun_path=[^ ]*$"
+
+#define REGEX_FREE_DOMAIN(task) \
+ TRACE_PREFIX(task) \
+ "landlock_free_domain: " \
+ "domain=[0-9a-f]\\+ " \
+ "denials=[0-9]\\+$"
+
+#define REGEX_FREE_RULESET(task) \
+ TRACE_PREFIX(task) \
+ "landlock_free_ruleset: " \
+ "ruleset=[0-9a-f]\\+\\.[0-9]\\+$"
+
+static int __maybe_unused tracefs_write(const char *path, const char *value)
+{
+ int fd;
+ ssize_t ret;
+ size_t len = strlen(value);
+
+ fd = open(path, O_WRONLY | O_TRUNC | O_CLOEXEC);
+ if (fd < 0)
+ return -errno;
+
+ ret = write(fd, value, len);
+ close(fd);
+ if (ret < 0)
+ return -errno;
+ if ((size_t)ret != len)
+ return -EIO;
+
+ return 0;
+}
+
+static int __maybe_unused tracefs_write_int(const char *path, int value)
+{
+ char buf[32];
+
+ snprintf(buf, sizeof(buf), "%d", value);
+ return tracefs_write(path, buf);
+}
+
+static int __maybe_unused tracefs_setup(void)
+{
+ struct stat st;
+
+ /* Mount tracefs if not already mounted. */
+ if (stat(TRACEFS_ROOT, &st) != 0) {
+ int ret = mount("tracefs", TRACEFS_ROOT, "tracefs", 0, NULL);
+
+ if (ret)
+ return -errno;
+ }
+
+ /* Verify landlock events are available. */
+ if (stat(TRACEFS_LANDLOCK_DIR, &st) != 0)
+ return -ENOENT;
+
+ return 0;
+}
+
+/*
+ * Set up PID-based event filtering so only events from the current process and
+ * its children are recorded. This is analogous to audit's AUDIT_EXE filter: it
+ * prevents events from unrelated processes from polluting the trace buffer.
+ */
+static int __maybe_unused tracefs_set_pid_filter(pid_t pid)
+{
+ int ret;
+
+ /* Enable event-fork so children inherit the PID filter. */
+ ret = tracefs_write(TRACEFS_OPTIONS_EVENT_FORK, "1");
+ if (ret)
+ return ret;
+
+ return tracefs_write_int(TRACEFS_SET_EVENT_PID, pid);
+}
+
+/* Clear the PID filter to stop filtering by PID. */
+static int __maybe_unused tracefs_clear_pid_filter(void)
+{
+ return tracefs_write(TRACEFS_SET_EVENT_PID, "");
+}
+
+static int __maybe_unused tracefs_enable_event(const char *enable_path,
+ bool enable)
+{
+ return tracefs_write(enable_path, enable ? "1" : "0");
+}
+
+static int __maybe_unused tracefs_clear(void)
+{
+ return tracefs_write(TRACEFS_TRACE, "");
+}
+
+/*
+ * Reads the trace buffer content into a newly allocated buffer. The caller is
+ * responsible for freeing the returned buffer. Returns NULL on error.
+ */
+static char __maybe_unused *tracefs_read_trace(void)
+{
+ char *buf;
+ int fd;
+ ssize_t total = 0, ret;
+
+ buf = malloc(TRACE_BUFFER_SIZE);
+ if (!buf)
+ return NULL;
+
+ fd = open(TRACEFS_TRACE, O_RDONLY | O_CLOEXEC);
+ if (fd < 0) {
+ free(buf);
+ return NULL;
+ }
+
+ while (total < TRACE_BUFFER_SIZE - 1) {
+ ret = read(fd, buf + total, TRACE_BUFFER_SIZE - 1 - total);
+ if (ret <= 0)
+ break;
+ total += ret;
+ }
+ close(fd);
+ buf[total] = '\0';
+ return buf;
+}
+
+/* Counts the number of lines in @buf matching the basic regex @pattern. */
+static int __maybe_unused tracefs_count_matches(const char *buf,
+ const char *pattern)
+{
+ regex_t regex;
+ int count = 0;
+ const char *line, *end;
+
+ if (regcomp(®ex, pattern, 0) != 0)
+ return -EINVAL;
+
+ line = buf;
+ while (*line) {
+ end = strchr(line, '\n');
+ if (!end)
+ end = line + strlen(line);
+
+ /* Create a temporary null-terminated line. */
+ size_t len = end - line;
+ char *tmp = malloc(len + 1);
+
+ if (tmp) {
+ memcpy(tmp, line, len);
+ tmp[len] = '\0';
+ if (regexec(®ex, tmp, 0, NULL, 0) == 0)
+ count++;
+ free(tmp);
+ }
+
+ if (*end == '\n')
+ line = end + 1;
+ else
+ break;
+ }
+
+ regfree(®ex);
+ return count;
+}
+
+/*
+ * Extracts the value of a named field from a trace line in @buf. Searches for
+ * the first line matching @line_pattern, then extracts the value after
+ * "@field_name=" into @out. Stops at space or newline.
+ *
+ * Returns 0 on success, -ENOENT if no match.
+ */
+static int __maybe_unused tracefs_extract_field(const char *buf,
+ const char *line_pattern,
+ const char *field_name,
+ char *out, size_t out_size)
+{
+ regex_t regex;
+ const char *line, *end;
+
+ if (regcomp(®ex, line_pattern, 0) != 0)
+ return -EINVAL;
+
+ line = buf;
+ while (*line) {
+ end = strchr(line, '\n');
+ if (!end)
+ end = line + strlen(line);
+
+ size_t len = end - line;
+ char *tmp = malloc(len + 1);
+
+ if (tmp) {
+ const char *field, *val_start;
+ size_t field_len, val_len;
+
+ memcpy(tmp, line, len);
+ tmp[len] = '\0';
+
+ if (regexec(®ex, tmp, 0, NULL, 0) != 0) {
+ free(tmp);
+ goto next;
+ }
+
+ /*
+ * Find "field_name=" in the line, ensuring a word
+ * boundary before the field name to avoid substring
+ * matches (e.g., "port" in "sport").
+ */
+ field_len = strlen(field_name);
+ field = tmp;
+ while ((field = strstr(field, field_name))) {
+ if (field[field_len] == '=' &&
+ (field == tmp || field[-1] == ' '))
+ break;
+ field++;
+ }
+ if (!field) {
+ free(tmp);
+ regfree(®ex);
+ return -ENOENT;
+ }
+
+ val_start = field + field_len + 1;
+ val_len = 0;
+ while (val_start[val_len] &&
+ val_start[val_len] != ' ' &&
+ val_start[val_len] != '\n')
+ val_len++;
+
+ if (val_len >= out_size)
+ val_len = out_size - 1;
+ memcpy(out, val_start, val_len);
+ out[val_len] = '\0';
+
+ free(tmp);
+ regfree(®ex);
+ return 0;
+ }
+next:
+ if (*end == '\n')
+ line = end + 1;
+ else
+ break;
+ }
+
+ regfree(®ex);
+ return -ENOENT;
+}
+
+/*
+ * Common fixture setup for trace tests. Mounts tracefs if needed and sets a
+ * PID filter. The caller must create a mount namespace first
+ * (unshare(CLONE_NEWNS) + mount(MS_REC | MS_PRIVATE)) to isolate the tracefs
+ * mount; the trace buffer, per-event enable flags, and PID filter are global
+ * kernel state, scoped to the test by the PID filter.
+ *
+ * Returns 0 on success, -errno on failure (caller should SKIP).
+ */
+static int __maybe_unused tracefs_fixture_setup(void)
+{
+ int ret;
+
+ ret = tracefs_setup();
+ if (ret)
+ return ret;
+
+ return tracefs_set_pid_filter(getpid());
+}
+
+static void __maybe_unused tracefs_fixture_teardown(void)
+{
+ tracefs_clear_pid_filter();
+}
+
+/*
+ * Temporarily raises CAP_SYS_ADMIN effective capability, calls @func, then
+ * drops the capability. Returns the value from @func, or -EPERM if the
+ * capability manipulation fails.
+ */
+static int __maybe_unused tracefs_priv_call(int (*func)(void))
+{
+ const cap_value_t admin = CAP_SYS_ADMIN;
+ cap_t cap_p;
+ int ret;
+
+ cap_p = cap_get_proc();
+ if (!cap_p)
+ return -EPERM;
+
+ if (cap_set_flag(cap_p, CAP_EFFECTIVE, 1, &admin, CAP_SET) ||
+ cap_set_proc(cap_p)) {
+ cap_free(cap_p);
+ return -EPERM;
+ }
+
+ ret = func();
+
+ cap_set_flag(cap_p, CAP_EFFECTIVE, 1, &admin, CAP_CLEAR);
+ cap_set_proc(cap_p);
+ cap_free(cap_p);
+ return ret;
+}
+
+/* Read the trace buffer with elevated privileges. Returns NULL on failure. */
+static char __maybe_unused *tracefs_read_buf(void)
+{
+ /* Cannot use tracefs_priv_call() because the return type is char *. */
+ cap_t cap_p;
+ char *buf;
+ const cap_value_t admin = CAP_SYS_ADMIN;
+
+ cap_p = cap_get_proc();
+ if (!cap_p)
+ return NULL;
+
+ if (cap_set_flag(cap_p, CAP_EFFECTIVE, 1, &admin, CAP_SET) ||
+ cap_set_proc(cap_p)) {
+ cap_free(cap_p);
+ return NULL;
+ }
+
+ buf = tracefs_read_trace();
+
+ cap_set_flag(cap_p, CAP_EFFECTIVE, 1, &admin, CAP_CLEAR);
+ cap_set_proc(cap_p);
+ cap_free(cap_p);
+ return buf;
+}
+
+/* Clear the trace buffer with elevated privileges. Returns 0 on success. */
+static int __maybe_unused tracefs_clear_buf(void)
+{
+ return tracefs_priv_call(tracefs_clear);
+}
+
+/*
+ * Forks a child that creates a Landlock sandbox and performs an FS access. The
+ * parent waits for the child, then reads the trace buffer.
+ *
+ * Requires common.h and wrappers.h to be included before trace.h.
+ */
+static void __maybe_unused sandbox_child_fs_access(
+ struct __test_metadata *const _metadata, const char *rule_path,
+ __u64 handled_access, __u64 allowed_access, const char *access_path)
+{
+ pid_t pid;
+ int status;
+
+ pid = fork();
+ ASSERT_LE(0, pid);
+
+ if (pid == 0) {
+ struct landlock_ruleset_attr ruleset_attr = {
+ .handled_access_fs = handled_access,
+ };
+ struct landlock_path_beneath_attr path_beneath = {
+ .allowed_access = allowed_access,
+ };
+ int ruleset_fd, fd;
+
+ ruleset_fd = landlock_create_ruleset(&ruleset_attr,
+ sizeof(ruleset_attr), 0);
+ if (ruleset_fd < 0)
+ _exit(1);
+
+ path_beneath.parent_fd =
+ open(rule_path, O_PATH | O_DIRECTORY | O_CLOEXEC);
+ if (path_beneath.parent_fd < 0) {
+ close(ruleset_fd);
+ _exit(1);
+ }
+
+ if (landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH,
+ &path_beneath, 0)) {
+ close(path_beneath.parent_fd);
+ close(ruleset_fd);
+ _exit(1);
+ }
+ close(path_beneath.parent_fd);
+
+ prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
+ if (landlock_restrict_self(ruleset_fd, 0)) {
+ close(ruleset_fd);
+ _exit(1);
+ }
+ close(ruleset_fd);
+
+ fd = open(access_path, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
+ if (fd >= 0)
+ close(fd);
+
+ _exit(0);
+ }
+
+ ASSERT_EQ(pid, waitpid(pid, &status, 0));
+ ASSERT_TRUE(WIFEXITED(status));
+ EXPECT_EQ(0, WEXITSTATUS(status));
+}
+
+/*
+ * Forks a child that creates a Landlock sandbox allowing execute+read_dir for
+ * /usr and execute-only for ".", then execs ./true. The true binary opens "."
+ * on startup, triggering a read_dir denial with same_exec=0. The parent waits
+ * for the child to exit.
+ */
+static void __maybe_unused sandbox_child_exec_true(
+ struct __test_metadata *const _metadata, __u32 restrict_flags)
+{
+ pid_t pid;
+ int status;
+
+ pid = fork();
+ ASSERT_LE(0, pid);
+
+ if (pid == 0) {
+ struct landlock_ruleset_attr attr = {
+ .handled_access_fs = LANDLOCK_ACCESS_FS_READ_DIR |
+ LANDLOCK_ACCESS_FS_EXECUTE,
+ };
+ struct landlock_path_beneath_attr path_beneath = {
+ .allowed_access = LANDLOCK_ACCESS_FS_EXECUTE |
+ LANDLOCK_ACCESS_FS_READ_DIR,
+ };
+ int ruleset_fd;
+
+ ruleset_fd = landlock_create_ruleset(&attr, sizeof(attr), 0);
+ if (ruleset_fd < 0)
+ _exit(1);
+
+ path_beneath.parent_fd =
+ open("/usr", O_PATH | O_DIRECTORY | O_CLOEXEC);
+ if (path_beneath.parent_fd >= 0) {
+ landlock_add_rule(ruleset_fd,
+ LANDLOCK_RULE_PATH_BENEATH,
+ &path_beneath, 0);
+ close(path_beneath.parent_fd);
+ }
+
+ path_beneath.allowed_access = LANDLOCK_ACCESS_FS_EXECUTE;
+ path_beneath.parent_fd =
+ open(".", O_PATH | O_DIRECTORY | O_CLOEXEC);
+ if (path_beneath.parent_fd >= 0) {
+ landlock_add_rule(ruleset_fd,
+ LANDLOCK_RULE_PATH_BENEATH,
+ &path_beneath, 0);
+ close(path_beneath.parent_fd);
+ }
+
+ prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
+ if (landlock_restrict_self(ruleset_fd, restrict_flags))
+ _exit(1);
+ close(ruleset_fd);
+
+ execl("./true", "./true", NULL);
+ _exit(1);
+ }
+
+ ASSERT_EQ(pid, waitpid(pid, &status, 0));
+ ASSERT_TRUE(WIFEXITED(status));
+ EXPECT_EQ(0, WEXITSTATUS(status));
+}
diff --git a/tools/testing/selftests/landlock/trace_test.c b/tools/testing/selftests/landlock/trace_test.c
new file mode 100644
index 000000000000..a141f22ad98f
--- /dev/null
+++ b/tools/testing/selftests/landlock/trace_test.c
@@ -0,0 +1,1101 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Landlock tests - Tracepoints
+ *
+ * Copyright © 2026 Cloudflare, Inc.
+ */
+
+#define _GNU_SOURCE
+#include <errno.h>
+#include <fcntl.h>
+#include <linux/landlock.h>
+#include <sched.h>
+#include <stdio.h>
+#include <string.h>
+#include <sys/mount.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include "common.h"
+#include "trace.h"
+
+#define TRACE_TASK "trace_test"
+
+/* clang-format off */
+FIXTURE(trace) {
+ /* clang-format on */
+ int tracefs_ok;
+};
+
+FIXTURE_SETUP(trace)
+{
+ int ret;
+
+ set_cap(_metadata, CAP_SYS_ADMIN);
+ ASSERT_EQ(0, unshare(CLONE_NEWNS));
+ ASSERT_EQ(0, mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL));
+
+ ret = tracefs_fixture_setup();
+ if (ret) {
+ clear_cap(_metadata, CAP_SYS_ADMIN);
+ self->tracefs_ok = 0;
+ SKIP(return, "tracefs not available");
+ }
+ self->tracefs_ok = 1;
+
+ ASSERT_EQ(0, tracefs_enable_event(TRACEFS_CREATE_RULESET_ENABLE, true));
+ ASSERT_EQ(0, tracefs_enable_event(TRACEFS_CREATE_DOMAIN_ENABLE, true));
+ ASSERT_EQ(0, tracefs_enable_event(TRACEFS_ADD_RULE_FS_ENABLE, true));
+ ASSERT_EQ(0, tracefs_enable_event(TRACEFS_ADD_RULE_NET_ENABLE, true));
+ ASSERT_EQ(0, tracefs_enable_event(TRACEFS_CHECK_RULE_FS_ENABLE, true));
+ ASSERT_EQ(0, tracefs_enable_event(TRACEFS_CHECK_RULE_NET_ENABLE, true));
+ ASSERT_EQ(0, tracefs_enable_event(TRACEFS_DENY_ACCESS_FS_ENABLE, true));
+ ASSERT_EQ(0,
+ tracefs_enable_event(TRACEFS_DENY_ACCESS_NET_ENABLE, true));
+ ASSERT_EQ(0, tracefs_enable_event(TRACEFS_FREE_DOMAIN_ENABLE, true));
+ ASSERT_EQ(0, tracefs_enable_event(TRACEFS_FREE_RULESET_ENABLE, true));
+ ASSERT_EQ(0, tracefs_clear());
+ clear_cap(_metadata, CAP_SYS_ADMIN);
+}
+
+FIXTURE_TEARDOWN(trace)
+{
+ if (!self->tracefs_ok)
+ return;
+
+ /* Disables landlock events and clears PID filter. */
+ set_cap(_metadata, CAP_SYS_ADMIN);
+ tracefs_enable_event(TRACEFS_CREATE_RULESET_ENABLE, false);
+ tracefs_enable_event(TRACEFS_CREATE_DOMAIN_ENABLE, false);
+ tracefs_enable_event(TRACEFS_ADD_RULE_FS_ENABLE, false);
+ tracefs_enable_event(TRACEFS_ADD_RULE_NET_ENABLE, false);
+ tracefs_enable_event(TRACEFS_CHECK_RULE_FS_ENABLE, false);
+ tracefs_enable_event(TRACEFS_CHECK_RULE_NET_ENABLE, false);
+ tracefs_enable_event(TRACEFS_DENY_ACCESS_FS_ENABLE, false);
+ tracefs_enable_event(TRACEFS_DENY_ACCESS_NET_ENABLE, false);
+ tracefs_enable_event(TRACEFS_FREE_DOMAIN_ENABLE, false);
+ tracefs_enable_event(TRACEFS_FREE_RULESET_ENABLE, false);
+ tracefs_clear_pid_filter();
+ clear_cap(_metadata, CAP_SYS_ADMIN);
+
+ /*
+ * The mount namespace is cleaned up automatically when the test process
+ * (harness child) exits.
+ */
+}
+
+/*
+ * Verifies that no trace events are emitted when the tracepoints are disabled.
+ */
+TEST_F(trace, no_trace_when_disabled)
+{
+ char *buf;
+
+ /* Disable all landlock events. */
+ set_cap(_metadata, CAP_SYS_ADMIN);
+ ASSERT_EQ(0,
+ tracefs_enable_event(TRACEFS_CREATE_RULESET_ENABLE, false));
+ ASSERT_EQ(0, tracefs_enable_event(TRACEFS_CREATE_DOMAIN_ENABLE, false));
+ ASSERT_EQ(0, tracefs_enable_event(TRACEFS_ADD_RULE_FS_ENABLE, false));
+ ASSERT_EQ(0, tracefs_enable_event(TRACEFS_ADD_RULE_NET_ENABLE, false));
+ ASSERT_EQ(0, tracefs_enable_event(TRACEFS_CHECK_RULE_FS_ENABLE, false));
+ ASSERT_EQ(0,
+ tracefs_enable_event(TRACEFS_CHECK_RULE_NET_ENABLE, false));
+ ASSERT_EQ(0,
+ tracefs_enable_event(TRACEFS_DENY_ACCESS_FS_ENABLE, false));
+ ASSERT_EQ(0,
+ tracefs_enable_event(TRACEFS_DENY_ACCESS_NET_ENABLE, false));
+ ASSERT_EQ(0, tracefs_enable_event(TRACEFS_DENY_PTRACE_ENABLE, false));
+ ASSERT_EQ(0, tracefs_enable_event(TRACEFS_DENY_SCOPE_SIGNAL_ENABLE,
+ false));
+ ASSERT_EQ(0, tracefs_enable_event(
+ TRACEFS_DENY_SCOPE_ABSTRACT_UNIX_SOCKET_ENABLE,
+ false));
+ ASSERT_EQ(0, tracefs_enable_event(TRACEFS_FREE_DOMAIN_ENABLE, false));
+ ASSERT_EQ(0, tracefs_enable_event(TRACEFS_FREE_RULESET_ENABLE, false));
+ ASSERT_EQ(0, tracefs_clear());
+ clear_cap(_metadata, CAP_SYS_ADMIN);
+
+ /*
+ * Trigger both allowed and denied accesses to verify neither check_rule
+ * nor check_access events fire when disabled.
+ */
+ sandbox_child_fs_access(_metadata, "/usr", LANDLOCK_ACCESS_FS_READ_DIR,
+ LANDLOCK_ACCESS_FS_READ_DIR, "/tmp");
+
+ /* Read trace buffer and verify no landlock events at all. */
+ buf = tracefs_read_buf();
+ ASSERT_NE(NULL, buf);
+
+ EXPECT_EQ(0, tracefs_count_matches(buf, "landlock_"))
+ {
+ TH_LOG("Expected 0 landlock events when disabled\n%s", buf);
+ }
+
+ free(buf);
+}
+
+/*
+ * Verifies that landlock_create_ruleset emits a trace event with the correct
+ * handled access masks.
+ */
+TEST_F(trace, create_ruleset)
+{
+ struct landlock_ruleset_attr ruleset_attr = {
+ .handled_access_fs = LANDLOCK_ACCESS_FS_READ_FILE,
+ .handled_access_net = LANDLOCK_ACCESS_NET_BIND_TCP,
+ };
+ int ruleset_fd;
+ char *buf, *dot;
+ char field[64];
+
+ ruleset_fd =
+ landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
+ ASSERT_LE(0, ruleset_fd);
+ ASSERT_EQ(0, close(ruleset_fd));
+
+ buf = tracefs_read_buf();
+ ASSERT_NE(NULL, buf);
+
+ EXPECT_EQ(1,
+ tracefs_count_matches(buf, REGEX_CREATE_RULESET(TRACE_TASK)))
+ {
+ TH_LOG("Expected 1 create_ruleset event\n%s", buf);
+ }
+
+ /* Verify handled_fs matches what we requested. */
+ EXPECT_EQ(0,
+ tracefs_extract_field(buf, REGEX_CREATE_RULESET(TRACE_TASK),
+ "handled_fs", field, sizeof(field)));
+ EXPECT_STREQ("read_file", field);
+
+ /* Verify handled_net matches. */
+ EXPECT_EQ(0,
+ tracefs_extract_field(buf, REGEX_CREATE_RULESET(TRACE_TASK),
+ "handled_net", field, sizeof(field)));
+ EXPECT_STREQ("bind_tcp", field);
+
+ /* Verify version is 0 at creation (no rules added yet). */
+ EXPECT_EQ(0,
+ tracefs_extract_field(buf, REGEX_CREATE_RULESET(TRACE_TASK),
+ "ruleset", field, sizeof(field)));
+ /* Format is <hex>.<dec>; version is after the dot. */
+ dot = strchr(field, '.');
+ ASSERT_NE(0, !!dot);
+ EXPECT_STREQ("0", dot + 1);
+
+ free(buf);
+}
+
+/*
+ * Verifies that the ruleset version increments with each add_rule call and that
+ * create_domain records the correct version.
+ */
+TEST_F(trace, ruleset_version)
+{
+ pid_t pid;
+ int status;
+ char *buf;
+ const char *dot;
+ char field[64];
+
+ ASSERT_EQ(0, tracefs_clear_buf());
+
+ pid = fork();
+ ASSERT_LE(0, pid);
+
+ if (pid == 0) {
+ struct landlock_ruleset_attr ruleset_attr = {
+ .handled_access_fs = LANDLOCK_ACCESS_FS_READ_DIR,
+ };
+ struct landlock_path_beneath_attr path_beneath = {
+ .allowed_access = LANDLOCK_ACCESS_FS_READ_DIR,
+ };
+ int ruleset_fd;
+
+ ruleset_fd = landlock_create_ruleset(&ruleset_attr,
+ sizeof(ruleset_attr), 0);
+ if (ruleset_fd < 0)
+ _exit(1);
+
+ /* First rule: version becomes 1. */
+ path_beneath.parent_fd =
+ open("/usr", O_PATH | O_DIRECTORY | O_CLOEXEC);
+ if (path_beneath.parent_fd < 0)
+ _exit(1);
+ landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH,
+ &path_beneath, 0);
+ close(path_beneath.parent_fd);
+
+ /* Second rule: version becomes 2. */
+ path_beneath.parent_fd =
+ open("/tmp", O_PATH | O_DIRECTORY | O_CLOEXEC);
+ if (path_beneath.parent_fd < 0)
+ _exit(1);
+ landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH,
+ &path_beneath, 0);
+ close(path_beneath.parent_fd);
+
+ prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
+ if (landlock_restrict_self(ruleset_fd, 0))
+ _exit(1);
+ close(ruleset_fd);
+ _exit(0);
+ }
+
+ ASSERT_EQ(pid, waitpid(pid, &status, 0));
+ ASSERT_TRUE(WIFEXITED(status));
+ EXPECT_EQ(0, WEXITSTATUS(status));
+
+ buf = tracefs_read_buf();
+ ASSERT_NE(NULL, buf);
+
+ /* Verify create_ruleset has version=0. */
+ ASSERT_EQ(0,
+ tracefs_extract_field(buf, REGEX_CREATE_RULESET(TRACE_TASK),
+ "ruleset", field, sizeof(field)));
+ dot = strchr(field, '.');
+ ASSERT_NE(0, !!dot);
+ EXPECT_STREQ("0", dot + 1);
+
+ /* Verify 2 add_rule_fs events were emitted. */
+ EXPECT_EQ(2, tracefs_count_matches(buf, REGEX_ADD_RULE_FS(TRACE_TASK)))
+ {
+ TH_LOG("Expected 2 add_rule_fs events\n%s", buf);
+ }
+
+ /*
+ * Verify create_domain records version=2 (after 2 add_rule calls). The
+ * ruleset field format is <hex_id>.<dec_version>.
+ */
+ ASSERT_EQ(0, tracefs_extract_field(buf, REGEX_CREATE_DOMAIN(TRACE_TASK),
+ "ruleset", field, sizeof(field)));
+ dot = strchr(field, '.');
+ ASSERT_NE(0, !!dot);
+ EXPECT_STREQ("2", dot + 1);
+
+ free(buf);
+}
+
+/*
+ * Verifies that landlock_create_domain emits a trace event linking the ruleset
+ * ID to the new domain ID.
+ */
+TEST_F(trace, create_domain)
+{
+ pid_t pid;
+ int status, check_count;
+ char *buf;
+ char parent_id[64], domain_id[64], check_domain[64];
+
+ /* Clear before the sandboxed child. */
+ ASSERT_EQ(0, tracefs_clear_buf());
+
+ pid = fork();
+ ASSERT_LE(0, pid);
+
+ if (pid == 0) {
+ struct landlock_ruleset_attr ruleset_attr = {
+ .handled_access_fs = LANDLOCK_ACCESS_FS_READ_DIR,
+ };
+ struct landlock_path_beneath_attr path_beneath = {
+ .allowed_access = LANDLOCK_ACCESS_FS_READ_DIR,
+ };
+ int ruleset_fd, fd;
+
+ ruleset_fd = landlock_create_ruleset(&ruleset_attr,
+ sizeof(ruleset_attr), 0);
+ if (ruleset_fd < 0)
+ _exit(1);
+
+ path_beneath.parent_fd =
+ open("/usr", O_PATH | O_DIRECTORY | O_CLOEXEC);
+ if (path_beneath.parent_fd < 0)
+ _exit(1);
+
+ landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH,
+ &path_beneath, 0);
+ close(path_beneath.parent_fd);
+
+ prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
+ if (landlock_restrict_self(ruleset_fd, 0))
+ _exit(1);
+ close(ruleset_fd);
+
+ /* Trigger a check_rule to verify domain_id correlation. */
+ fd = open("/usr", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
+ if (fd >= 0)
+ close(fd);
+
+ _exit(0);
+ }
+
+ ASSERT_EQ(pid, waitpid(pid, &status, 0));
+ ASSERT_TRUE(WIFEXITED(status));
+ EXPECT_EQ(0, WEXITSTATUS(status));
+
+ buf = tracefs_read_buf();
+ ASSERT_NE(NULL, buf);
+
+ /* Verify create_domain event exists. */
+ EXPECT_EQ(1,
+ tracefs_count_matches(buf, REGEX_CREATE_DOMAIN(TRACE_TASK)))
+ {
+ TH_LOG("Expected 1 create_domain event\n%s", buf);
+ }
+
+ /* Extract the domain ID from create_domain. */
+ EXPECT_EQ(0, tracefs_extract_field(buf, REGEX_CREATE_DOMAIN(TRACE_TASK),
+ "domain", domain_id,
+ sizeof(domain_id)));
+
+ /* Verify domain ID is non-zero. */
+ EXPECT_NE(0, strcmp(domain_id, "0"));
+
+ /* Verify parent=0 (first restriction, no prior domain). */
+ EXPECT_EQ(0, tracefs_extract_field(buf, REGEX_CREATE_DOMAIN(TRACE_TASK),
+ "parent", parent_id,
+ sizeof(parent_id)));
+ EXPECT_STREQ("0", parent_id);
+
+ /*
+ * Verify the same domain ID appears in the check_rule event, confirming
+ * end-to-end correlation.
+ */
+ check_count =
+ tracefs_count_matches(buf, REGEX_CHECK_RULE_FS(TRACE_TASK));
+ ASSERT_LE(1, check_count)
+ {
+ TH_LOG("Expected check_rule_fs events\n%s", buf);
+ }
+
+ EXPECT_EQ(0, tracefs_extract_field(buf, REGEX_CHECK_RULE_FS(TRACE_TASK),
+ "domain", check_domain,
+ sizeof(check_domain)));
+ EXPECT_STREQ(domain_id, check_domain);
+
+ free(buf);
+}
+
+/* Builds a rule-less scope-based ruleset; returns the fd or -1. */
+static int build_enforce_ruleset(void)
+{
+ const struct landlock_ruleset_attr attr = {
+ .scoped = LANDLOCK_SCOPE_SIGNAL,
+ };
+
+ return landlock_create_ruleset(&attr, sizeof(attr), 0);
+}
+
+/*
+ * Verifies that nested landlock_restrict_self calls produce trace events with
+ * correct parent domain IDs: the second create_domain's parent should be the
+ * first domain's ID.
+ */
+TEST_F(trace, create_domain_nested)
+{
+ pid_t pid;
+ int status;
+ char *buf;
+ const char *after_first;
+ char first_domain[64], first_parent[64], second_parent[64];
+
+ ASSERT_EQ(0, tracefs_clear_buf());
+
+ pid = fork();
+ ASSERT_LE(0, pid);
+
+ if (pid == 0) {
+ int ruleset_fd;
+
+ /* First restriction. */
+ ruleset_fd = build_enforce_ruleset();
+ if (ruleset_fd < 0)
+ _exit(1);
+ prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
+ if (landlock_restrict_self(ruleset_fd, 0))
+ _exit(1);
+ close(ruleset_fd);
+
+ /* Second restriction (nested). */
+ ruleset_fd = build_enforce_ruleset();
+ if (ruleset_fd < 0)
+ _exit(1);
+ if (landlock_restrict_self(ruleset_fd, 0))
+ _exit(1);
+ close(ruleset_fd);
+
+ _exit(0);
+ }
+
+ ASSERT_EQ(pid, waitpid(pid, &status, 0));
+ ASSERT_TRUE(WIFEXITED(status));
+ EXPECT_EQ(0, WEXITSTATUS(status));
+
+ buf = tracefs_read_buf();
+ ASSERT_NE(NULL, buf);
+
+ /* Should have 2 create_domain events. */
+ EXPECT_EQ(2,
+ tracefs_count_matches(buf, REGEX_CREATE_DOMAIN(TRACE_TASK)))
+ {
+ TH_LOG("Expected 2 create_domain events\n%s", buf);
+ }
+
+ /*
+ * Extract domain and parent from each create_domain event. The first
+ * event (parent=0) is the outer domain; the second (parent!=0) is the
+ * nested domain whose parent should match the first domain's ID.
+ */
+ ASSERT_EQ(0, tracefs_extract_field(buf, REGEX_CREATE_DOMAIN(TRACE_TASK),
+ "domain", first_domain,
+ sizeof(first_domain)));
+ ASSERT_EQ(0, tracefs_extract_field(buf, REGEX_CREATE_DOMAIN(TRACE_TASK),
+ "parent", first_parent,
+ sizeof(first_parent)));
+ EXPECT_STREQ("0", first_parent);
+
+ /*
+ * Find the second create_domain by scanning past the first.
+ * tracefs_extract_field returns the first match, so search in the
+ * buffer after the first event.
+ *
+ * Skip past the first create_domain line. tracefs_extract_field matches
+ * the first line that matches the regex, so passing the buffer after
+ * the first matching line gives us the second event.
+ */
+ after_first = strstr(buf, "landlock_create_domain:");
+ ASSERT_NE(NULL, after_first);
+ after_first = strchr(after_first, '\n');
+ ASSERT_NE(NULL, after_first);
+
+ ASSERT_EQ(0, tracefs_extract_field(
+ after_first + 1, REGEX_CREATE_DOMAIN(TRACE_TASK),
+ "parent", second_parent, sizeof(second_parent)));
+
+ /* The second domain's parent should be the first domain's ID. */
+ EXPECT_STREQ(first_domain, second_parent);
+
+ free(buf);
+}
+
+/*
+ * Verifies that landlock_add_rule does not emit a trace event when the syscall
+ * fails (e.g., invalid ruleset fd).
+ */
+TEST_F(trace, add_rule_invalid_fd)
+{
+ struct landlock_path_beneath_attr path_beneath = {
+ .allowed_access = LANDLOCK_ACCESS_FS_READ_FILE,
+ };
+ char *buf;
+
+ path_beneath.parent_fd = open("/usr", O_PATH | O_DIRECTORY | O_CLOEXEC);
+ ASSERT_LE(0, path_beneath.parent_fd);
+
+ /* Invalid ruleset fd (-1). */
+ ASSERT_EQ(-1, landlock_add_rule(-1, LANDLOCK_RULE_PATH_BENEATH,
+ &path_beneath, 0));
+ ASSERT_EQ(0, close(path_beneath.parent_fd));
+
+ buf = tracefs_read_buf();
+ ASSERT_NE(NULL, buf);
+
+ EXPECT_EQ(0, tracefs_count_matches(buf, REGEX_ADD_RULE_FS(TRACE_TASK)))
+ {
+ TH_LOG("No add_rule_fs event expected on invalid fd\n%s", buf);
+ }
+
+ free(buf);
+}
+
+/*
+ * Verifies that landlock_create_domain does not emit a trace event when the
+ * syscall fails (e.g., invalid ruleset fd or unknown flags).
+ */
+TEST_F(trace, create_domain_invalid)
+{
+ int ruleset_fd;
+ char *buf;
+
+ ruleset_fd = build_enforce_ruleset();
+ ASSERT_LE(0, ruleset_fd);
+
+ /* Clear the trace buffer after create_ruleset event. */
+ ASSERT_EQ(0, tracefs_clear_buf());
+
+ /* Invalid fd. */
+ ASSERT_EQ(-1, landlock_restrict_self(-1, 0));
+
+ /* Unknown flags. */
+ ASSERT_EQ(-1, landlock_restrict_self(ruleset_fd, -1));
+
+ ASSERT_EQ(0, close(ruleset_fd));
+
+ buf = tracefs_read_buf();
+ ASSERT_NE(NULL, buf);
+
+ EXPECT_EQ(0,
+ tracefs_count_matches(buf, REGEX_CREATE_DOMAIN(TRACE_TASK)))
+ {
+ TH_LOG("No create_domain event expected on error\n%s", buf);
+ }
+
+ free(buf);
+}
+
+/*
+ * Verifies that trace_landlock_free_domain fires when a domain is deallocated,
+ * with the correct denials count.
+ */
+TEST_F(trace, free_domain)
+{
+ char *buf;
+ int count;
+ char denials_field[32];
+
+ ASSERT_EQ(0, tracefs_clear_buf());
+
+ /*
+ * The domain is freed via a work queue (kworker), so the free_domain
+ * trace event is emitted from a different PID. Clear the PID filter
+ * BEFORE the child exits, so the kworker event passes the filter when
+ * it fires.
+ */
+ set_cap(_metadata, CAP_SYS_ADMIN);
+ tracefs_clear_pid_filter();
+ clear_cap(_metadata, CAP_SYS_ADMIN);
+
+ sandbox_child_fs_access(_metadata, "/usr", LANDLOCK_ACCESS_FS_READ_DIR,
+ LANDLOCK_ACCESS_FS_READ_DIR, "/tmp");
+
+ /*
+ * Wait for the deferred deallocation work to run. The domain is freed
+ * asynchronously from a kworker; poll until the event appears or a
+ * timeout is reached.
+ */
+ for (int retry = 0; retry < 10; retry++) {
+ usleep(100000);
+
+ set_cap(_metadata, CAP_SYS_ADMIN);
+ buf = tracefs_read_trace();
+ clear_cap(_metadata, CAP_SYS_ADMIN);
+ ASSERT_NE(NULL, buf);
+
+ count = tracefs_count_matches(buf,
+ REGEX_FREE_DOMAIN(KWORKER_TASK));
+ if (count >= 1)
+ break;
+ free(buf);
+ buf = NULL;
+ }
+
+ set_cap(_metadata, CAP_SYS_ADMIN);
+ ASSERT_EQ(0, tracefs_set_pid_filter(getpid()));
+ clear_cap(_metadata, CAP_SYS_ADMIN);
+
+ ASSERT_NE(NULL, buf);
+ EXPECT_LE(1, count)
+ {
+ TH_LOG("Expected free_domain event, got %d\n%s", count, buf);
+ }
+
+ /* Verify denials count matches the single denial we triggered. */
+ EXPECT_EQ(0, tracefs_extract_field(buf, REGEX_FREE_DOMAIN(KWORKER_TASK),
+ "denials", denials_field,
+ sizeof(denials_field)));
+ EXPECT_STREQ("1", denials_field);
+
+ free(buf);
+}
+
+/*
+ * Verifies that deny_access_fs includes the enriched fields: same_exec and
+ * logged.
+ */
+TEST_F(trace, deny_access_fs_fields)
+{
+ char *buf;
+ char field_buf[64];
+
+ ASSERT_EQ(0, tracefs_clear_buf());
+
+ /* Trigger a denial: rule for /usr, access /tmp. */
+ sandbox_child_fs_access(_metadata, "/usr", LANDLOCK_ACCESS_FS_READ_DIR,
+ LANDLOCK_ACCESS_FS_READ_DIR, "/tmp");
+
+ buf = tracefs_read_buf();
+ ASSERT_NE(NULL, buf);
+
+ /* Verify the enriched fields are present and have valid values. */
+ ASSERT_EQ(0, tracefs_extract_field(
+ buf, REGEX_DENY_ACCESS_FS(TRACE_TASK), "same_exec",
+ field_buf, sizeof(field_buf)));
+ /* Child is the same exec that restricted itself. */
+ EXPECT_STREQ("1", field_buf);
+
+ /* Same exec with default flags: audit would log this denial. */
+ ASSERT_EQ(0, tracefs_extract_field(
+ buf, REGEX_DENY_ACCESS_FS(TRACE_TASK), "logged",
+ field_buf, sizeof(field_buf)));
+ EXPECT_STREQ("1", field_buf);
+
+ free(buf);
+}
+
+/*
+ * Verifies that same_exec is 1 (true) for denials from the same executable that
+ * called landlock_restrict_self().
+ */
+TEST_F(trace, same_exec_before_exec)
+{
+ pid_t pid;
+ int status;
+ char *buf;
+ char field[64];
+
+ ASSERT_EQ(0, tracefs_clear_buf());
+
+ pid = fork();
+ ASSERT_LE(0, pid);
+
+ if (pid == 0) {
+ struct landlock_ruleset_attr attr = {
+ .handled_access_fs = LANDLOCK_ACCESS_FS_READ_DIR,
+ };
+ int ruleset_fd, dir_fd;
+
+ ruleset_fd = landlock_create_ruleset(&attr, sizeof(attr), 0);
+ if (ruleset_fd < 0)
+ _exit(1);
+
+ /* No rules: all read_dir access is denied. */
+ prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
+ if (landlock_restrict_self(ruleset_fd, 0))
+ _exit(1);
+ close(ruleset_fd);
+
+ /* Trigger denial without exec (same executable). */
+ dir_fd = open(".", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
+ if (dir_fd >= 0)
+ close(dir_fd);
+ _exit(0);
+ }
+
+ ASSERT_EQ(pid, waitpid(pid, &status, 0));
+ ASSERT_TRUE(WIFEXITED(status));
+ EXPECT_EQ(0, WEXITSTATUS(status));
+
+ buf = tracefs_read_buf();
+ ASSERT_NE(NULL, buf);
+
+ /* Should have at least one deny_access_fs denial. */
+ EXPECT_LE(1,
+ tracefs_count_matches(buf, REGEX_DENY_ACCESS_FS(TRACE_TASK)));
+
+ /* Verify same_exec=1 (same executable, no exec). */
+ ASSERT_EQ(0,
+ tracefs_extract_field(buf, REGEX_DENY_ACCESS_FS(TRACE_TASK),
+ "same_exec", field, sizeof(field)));
+ EXPECT_STREQ("1", field);
+
+ /* Same exec with default flags: audit would log this denial. */
+ ASSERT_EQ(0,
+ tracefs_extract_field(buf, REGEX_DENY_ACCESS_FS(TRACE_TASK),
+ "logged", field, sizeof(field)));
+ EXPECT_STREQ("1", field);
+
+ free(buf);
+}
+
+/*
+ * Verifies that same_exec is 0 (false) for denials from a process that has
+ * exec'd a new binary after landlock_restrict_self(). The sandboxed child
+ * exec's true which opens "." and triggers a read_dir denial. Covers the
+ * "trace-only" visibility condition: with same_exec=0 and the default
+ * log_new_exec=0, audit suppresses the denial (logged=0) but the trace event
+ * still fires.
+ */
+TEST_F(trace, same_exec_after_exec)
+{
+ char *buf;
+ char field[64];
+
+ ASSERT_EQ(0, tracefs_clear_buf());
+
+ sandbox_child_exec_true(_metadata, 0);
+
+ buf = tracefs_read_buf();
+ ASSERT_NE(NULL, buf);
+
+ EXPECT_LE(1, tracefs_count_matches(buf, REGEX_DENY_ACCESS_FS("true")));
+
+ /* Verify same_exec=0 (different executable after exec). */
+ ASSERT_EQ(0, tracefs_extract_field(buf, REGEX_DENY_ACCESS_FS("true"),
+ "same_exec", field, sizeof(field)));
+ EXPECT_STREQ("0", field);
+
+ /*
+ * same_exec=0 with default log_new_exec=0: audit suppresses (logged=0).
+ */
+ ASSERT_EQ(0, tracefs_extract_field(buf, REGEX_DENY_ACCESS_FS("true"),
+ "logged", field, sizeof(field)));
+ EXPECT_STREQ("0", field);
+
+ free(buf);
+}
+
+/*
+ * Verifies that LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF suppresses logging
+ * (logged=0) for a denial from the same executable.
+ */
+TEST_F(trace, log_flags_same_exec_off)
+{
+ pid_t pid;
+ int status;
+ char *buf;
+ char field[64];
+
+ ASSERT_EQ(0, tracefs_clear_buf());
+
+ pid = fork();
+ ASSERT_LE(0, pid);
+
+ if (pid == 0) {
+ struct landlock_ruleset_attr attr = {
+ .handled_access_fs = LANDLOCK_ACCESS_FS_READ_DIR,
+ };
+ int ruleset_fd, dir_fd;
+
+ ruleset_fd = landlock_create_ruleset(&attr, sizeof(attr), 0);
+ if (ruleset_fd < 0)
+ _exit(1);
+
+ prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
+ if (landlock_restrict_self(
+ ruleset_fd,
+ LANDLOCK_RESTRICT_SELF_LOG_SAME_EXEC_OFF))
+ _exit(1);
+ close(ruleset_fd);
+
+ dir_fd = open(".", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
+ if (dir_fd >= 0)
+ close(dir_fd);
+ _exit(0);
+ }
+
+ ASSERT_EQ(pid, waitpid(pid, &status, 0));
+ ASSERT_TRUE(WIFEXITED(status));
+ EXPECT_EQ(0, WEXITSTATUS(status));
+
+ buf = tracefs_read_buf();
+ ASSERT_NE(NULL, buf);
+
+ EXPECT_LE(1,
+ tracefs_count_matches(buf, REGEX_DENY_ACCESS_FS(TRACE_TASK)));
+
+ /* Same-exec denial with LOG_SAME_EXEC_OFF: audit suppresses it. */
+ ASSERT_EQ(0,
+ tracefs_extract_field(buf, REGEX_DENY_ACCESS_FS(TRACE_TASK),
+ "logged", field, sizeof(field)));
+ EXPECT_STREQ("0", field);
+
+ free(buf);
+}
+
+/*
+ * Verifies that LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON causes a post-exec
+ * denial to be logged (logged=1). The child exec's true so that the denial
+ * comes from a new executable (same_exec=0).
+ */
+TEST_F(trace, log_flags_new_exec_on)
+{
+ char *buf;
+ char field[64];
+
+ ASSERT_EQ(0, tracefs_clear_buf());
+
+ sandbox_child_exec_true(_metadata,
+ LANDLOCK_RESTRICT_SELF_LOG_NEW_EXEC_ON);
+
+ buf = tracefs_read_buf();
+ ASSERT_NE(NULL, buf);
+
+ EXPECT_LE(1, tracefs_count_matches(buf, REGEX_DENY_ACCESS_FS("true")));
+
+ ASSERT_EQ(0, tracefs_extract_field(buf, REGEX_DENY_ACCESS_FS("true"),
+ "same_exec", field, sizeof(field)));
+ EXPECT_STREQ("0", field);
+
+ /* LOG_NEW_EXEC_ON: the post-exec denial (same_exec=0) is logged. */
+ ASSERT_EQ(0, tracefs_extract_field(buf, REGEX_DENY_ACCESS_FS("true"),
+ "logged", field, sizeof(field)));
+ EXPECT_STREQ("1", field);
+
+ free(buf);
+}
+
+/*
+ * Verifies that denials suppressed by audit log flags are still counted in
+ * num_denials. The child restricts itself with default flags (log_same_exec=1,
+ * log_new_exec=0), then execs true which attempts to read a denied directory.
+ * After exec, same_exec=0 and log_new_exec=0, so audit suppresses the denial.
+ * But the trace event fires unconditionally and free_domain must report the
+ * correct denials count.
+ */
+TEST_F(trace, non_audit_visible_denial_counting)
+{
+ char *buf = NULL;
+ char denials_field[32];
+ int count;
+
+ set_cap(_metadata, CAP_SYS_ADMIN);
+ ASSERT_EQ(0, tracefs_clear());
+ tracefs_clear_pid_filter();
+ clear_cap(_metadata, CAP_SYS_ADMIN);
+
+ sandbox_child_exec_true(_metadata, 0);
+
+ /* Wait for free_domain event with retry. */
+ for (int retry = 0; retry < 10; retry++) {
+ usleep(100000);
+
+ set_cap(_metadata, CAP_SYS_ADMIN);
+ buf = tracefs_read_trace();
+ clear_cap(_metadata, CAP_SYS_ADMIN);
+ if (!buf)
+ break;
+
+ count = tracefs_count_matches(buf,
+ REGEX_FREE_DOMAIN(KWORKER_TASK));
+ if (count >= 1)
+ break;
+ free(buf);
+ buf = NULL;
+ }
+
+ set_cap(_metadata, CAP_SYS_ADMIN);
+ ASSERT_EQ(0, tracefs_set_pid_filter(getpid()));
+ clear_cap(_metadata, CAP_SYS_ADMIN);
+
+ /*
+ * The denial happened after exec (same_exec=0), so audit would suppress
+ * it. But num_denials counts all denials regardless.
+ */
+ ASSERT_NE(NULL, buf)
+ {
+ TH_LOG("free_domain event not found after 10 retries");
+ }
+ EXPECT_EQ(0, tracefs_extract_field(buf, REGEX_FREE_DOMAIN(KWORKER_TASK),
+ "denials", denials_field,
+ sizeof(denials_field)));
+ EXPECT_STREQ("1", denials_field);
+
+ free(buf);
+}
+
+/*
+ * Verifies that landlock_add_rule_net emits a trace event with the correct port
+ * and allowed access mask fields.
+ */
+TEST_F(trace, add_rule_net_fields)
+{
+ struct landlock_ruleset_attr ruleset_attr = {
+ .handled_access_net = LANDLOCK_ACCESS_NET_BIND_TCP,
+ };
+ struct landlock_net_port_attr net_port = {
+ .allowed_access = LANDLOCK_ACCESS_NET_BIND_TCP,
+ .port = 8080,
+ };
+ int ruleset_fd;
+ char *buf;
+ char field[64];
+
+ ruleset_fd =
+ landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
+ ASSERT_LE(0, ruleset_fd);
+
+ ASSERT_EQ(0, tracefs_clear_buf());
+
+ ASSERT_EQ(0, landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT,
+ &net_port, 0));
+ close(ruleset_fd);
+
+ buf = tracefs_read_buf();
+ ASSERT_NE(NULL, buf);
+
+ EXPECT_EQ(1, tracefs_count_matches(buf, REGEX_ADD_RULE_NET(TRACE_TASK)))
+ {
+ TH_LOG("Expected 1 add_rule_net event\n%s", buf);
+ }
+
+ /*
+ * Verify the port is in host endianness, matching the UAPI convention
+ * (landlock_net_port_attr.port). On little-endian, htons(8080) is
+ * 36895, so this comparison catches byte-order bugs.
+ */
+ EXPECT_EQ(0, tracefs_extract_field(buf, REGEX_ADD_RULE_NET(TRACE_TASK),
+ "port", field, sizeof(field)));
+ EXPECT_STREQ("8080", field);
+ /*
+ * The allowed mask is the absolute value after transformation: the
+ * user-requested BIND_TCP plus all unhandled access rights (the other
+ * net access bits are unhandled because the ruleset only handles
+ * BIND_TCP).
+ */
+ EXPECT_EQ(0,
+ tracefs_extract_field(buf, REGEX_ADD_RULE_NET(TRACE_TASK),
+ "access_rights", field, sizeof(field)));
+ EXPECT_STREQ("bind_tcp|connect_tcp|bind_udp|connect_send_udp", field);
+
+ free(buf);
+}
+
+/*
+ * Verifies that LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF suppresses audit
+ * logging for child domains (logged=0) even though the child's own
+ * per-execution flags are the defaults, while the trace event still fires
+ * (tracing is unconditional). The parent creates a domain with
+ * LOG_SUBDOMAINS_OFF, then the child creates a sub-domain and triggers a
+ * denial.
+ */
+TEST_F(trace, log_flags_subdomains_off)
+{
+ pid_t pid;
+ int status;
+ char *buf;
+ char field[64];
+
+ ASSERT_EQ(0, tracefs_clear_buf());
+
+ pid = fork();
+ ASSERT_LE(0, pid);
+
+ if (pid == 0) {
+ struct landlock_ruleset_attr attr = {
+ .handled_access_fs = LANDLOCK_ACCESS_FS_READ_DIR,
+ };
+ int parent_fd, child_fd, dir_fd;
+
+ /* Parent domain with LOG_SUBDOMAINS_OFF. */
+ parent_fd = landlock_create_ruleset(&attr, sizeof(attr), 0);
+ if (parent_fd < 0)
+ _exit(1);
+
+ prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
+ if (landlock_restrict_self(
+ parent_fd,
+ LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF))
+ _exit(1);
+ close(parent_fd);
+
+ /* Child sub-domain with default flags. */
+ child_fd = landlock_create_ruleset(&attr, sizeof(attr), 0);
+ if (child_fd < 0)
+ _exit(1);
+
+ if (landlock_restrict_self(child_fd, 0))
+ _exit(1);
+ close(child_fd);
+
+ /* Trigger a denial from the child domain. */
+ dir_fd = open(".", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
+ if (dir_fd >= 0)
+ close(dir_fd);
+ _exit(0);
+ }
+
+ ASSERT_EQ(pid, waitpid(pid, &status, 0));
+ ASSERT_TRUE(WIFEXITED(status));
+ EXPECT_EQ(0, WEXITSTATUS(status));
+
+ buf = tracefs_read_buf();
+ ASSERT_NE(NULL, buf);
+
+ /*
+ * Trace fires unconditionally even though audit is disabled for the
+ * child domain (parent had LOG_SUBDOMAINS_OFF).
+ */
+ EXPECT_LE(1,
+ tracefs_count_matches(buf, REGEX_DENY_ACCESS_FS(TRACE_TASK)))
+ {
+ TH_LOG("Expected deny_access_fs event despite "
+ "LOG_SUBDOMAINS_OFF\n%s",
+ buf);
+ }
+
+ /*
+ * The child's per-execution flags default to logging, but the
+ * ancestor's LOG_SUBDOMAINS_OFF disables it, so audit suppresses this
+ * denial (logged=0). This is exactly the case the single logged field
+ * captures and the raw per-execution flags could not.
+ */
+ ASSERT_EQ(0,
+ tracefs_extract_field(buf, REGEX_DENY_ACCESS_FS(TRACE_TASK),
+ "logged", field, sizeof(field)));
+ EXPECT_STREQ("0", field);
+
+ free(buf);
+}
+
+/* Verifies that landlock_free_ruleset fires when a ruleset FD is closed. */
+TEST_F(trace, free_ruleset_on_close)
+{
+ struct landlock_ruleset_attr ruleset_attr = {
+ .handled_access_fs = LANDLOCK_ACCESS_FS_READ_DIR,
+ };
+ int ruleset_fd;
+ char *buf;
+
+ ruleset_fd =
+ landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
+ ASSERT_LE(0, ruleset_fd);
+
+ ASSERT_EQ(0, tracefs_clear_buf());
+
+ /* Closing the FD should trigger free_ruleset. */
+ close(ruleset_fd);
+
+ buf = tracefs_read_buf();
+ ASSERT_NE(NULL, buf);
+
+ EXPECT_EQ(1, tracefs_count_matches(buf, REGEX_FREE_RULESET(TRACE_TASK)))
+ {
+ TH_LOG("Expected 1 free_ruleset event\n%s", buf);
+ }
+
+ free(buf);
+}
+
+/*
+ * The following tests are intentionally elided because the underlying kernel
+ * mechanisms are already validated by audit tests:
+ *
+ * - Domain ID monotonicity: validated by audit_test.c:layers. The same
+ * landlock_get_id_range() function serves both audit and trace.
+ *
+ * - Domain deallocation order (LIFO): validated by audit_test.c:layers. Trace
+ * events fire from the same free_domain_work() code path.
+ *
+ * - Max-layer stacking (16 domains): validated by audit_test.c:layers.
+ *
+ * - IPv6 network tests: IPv6 hook dispatch uses the same
+ * current_check_access_socket() as IPv4, validated by net_test.c:audit tests.
+ *
+ * - Per-access-right full matrix (all 16 FS rights): hook dispatch is validated
+ * by fs_test.c:audit tests. Trace tests verify representative samples to
+ * ensure bitmask encoding is correct.
+ *
+ * - Combined log flag variants (e.g., LOG_SUBDOMAINS_OFF + LOG_NEW_EXEC_ON):
+ * individual flag tests above cover each flag's effect on trace fields. Flag
+ * combination logic is validated by audit_test.c:audit_flags tests.
+ *
+ * - fs.refer multi-record denials and fs.change_topology (mount):
+ * trace_denial() uses the same code path for all FS request types. The
+ * DENTRY union member is validated by the deny_access_fs_fields
+ * test. Audit tests in fs_test.c cover refer and mount denial specifics.
+ */
+
+TEST_HARNESS_MAIN
diff --git a/tools/testing/selftests/landlock/true.c b/tools/testing/selftests/landlock/true.c
index 3f9ccbf52783..1e39b664512d 100644
--- a/tools/testing/selftests/landlock/true.c
+++ b/tools/testing/selftests/landlock/true.c
@@ -1,5 +1,15 @@
// SPDX-License-Identifier: GPL-2.0
+/*
+ * Minimal helper for Landlock selftests. Opens its own working directory
+ * before exiting, which may trigger access denials depending on the sandbox
+ * configuration.
+ */
+
+#include <fcntl.h>
+#include <unistd.h>
+
int main(void)
{
+ close(open(".", O_RDONLY | O_DIRECTORY | O_CLOEXEC));
return 0;
}
--
2.54.0
^ permalink raw reply related
page: next (older) | prev (newer) | latest
- recent:[subjects (threaded)|topics (new)|topics (active)]
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox