aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMickaël Salaün <mic@digikod.net>2024-02-09 18:20:16 +0100
committerMickaël Salaün <mic@digikod.net>2024-02-09 18:26:08 +0100
commit87033059ff9ee7214eabf93e7cacf1d0ec5e9454 (patch)
tree1a6dd7ccdcd49f175c0144bbce004dcb9bea5ab2
parent305aea29fd67a4942038a761139a1676086cd782 (diff)
downloadlinux-wip-audit.tar.gz
WIP: Add audit support for Landlockwip-audit
This is WIP squashed patch for the second patch series. See https://lore.kernel.org/all/20230921061641.273654-1-mic@digikod.net/ To test: * tail -F /var/log/audit/audit.log & * ./sandboxer ... Signed-off-by: Mickaël Salaün <mic@digikod.net>
-rw-r--r--include/linux/lsm_audit.h22
-rw-r--r--include/uapi/linux/audit.h7
-rw-r--r--security/Makefile2
-rw-r--r--security/landlock/.kunitconfig2
-rw-r--r--security/landlock/Makefile2
-rw-r--r--security/landlock/audit.c681
-rw-r--r--security/landlock/audit.h139
-rw-r--r--security/landlock/fs.c194
-rw-r--r--security/landlock/fs.h46
-rw-r--r--security/landlock/limits.h1
-rw-r--r--security/landlock/net.c75
-rw-r--r--security/landlock/ptrace.c61
-rw-r--r--security/landlock/ruleset.c30
-rw-r--r--security/landlock/ruleset.h26
-rw-r--r--security/landlock/syscalls.c31
-rw-r--r--security/lsm_audit.c27
-rw-r--r--tools/testing/kunit/configs/all_tests.config2
17 files changed, 1284 insertions, 64 deletions
diff --git a/include/linux/lsm_audit.h b/include/linux/lsm_audit.h
index 97a8b21eb03339..b62769a7c5fa04 100644
--- a/include/linux/lsm_audit.h
+++ b/include/linux/lsm_audit.h
@@ -116,14 +116,36 @@ struct common_audit_data {
#define v4info fam.v4
#define v6info fam.v6
+#ifdef CONFIG_AUDIT
+
int ipv4_skb_to_auditdata(struct sk_buff *skb,
struct common_audit_data *ad, u8 *proto);
+#if IS_ENABLED(CONFIG_IPV6)
int ipv6_skb_to_auditdata(struct sk_buff *skb,
struct common_audit_data *ad, u8 *proto);
+#endif /* IS_ENABLED(CONFIG_IPV6) */
void common_lsm_audit(struct common_audit_data *a,
void (*pre_audit)(struct audit_buffer *, void *),
void (*post_audit)(struct audit_buffer *, void *));
+void audit_log_lsm_data(struct audit_buffer *ab,
+ const struct common_audit_data *a);
+
+#else /* CONFIG_AUDIT */
+
+static inline void common_lsm_audit(struct common_audit_data *a,
+ void (*pre_audit)(struct audit_buffer *, void *),
+ void (*post_audit)(struct audit_buffer *, void *))
+{
+}
+
+static inline void audit_log_lsm_data(struct audit_buffer *ab,
+ const struct common_audit_data *a)
+{
+}
+
+#endif /* CONFIG_AUDIT */
+
#endif
diff --git a/include/uapi/linux/audit.h b/include/uapi/linux/audit.h
index d676ed2b246ec2..44a3ebd73abc58 100644
--- a/include/uapi/linux/audit.h
+++ b/include/uapi/linux/audit.h
@@ -122,6 +122,13 @@
#define AUDIT_OPENAT2 1337 /* Record showing openat2 how args */
#define AUDIT_DM_CTRL 1338 /* Device Mapper target control */
#define AUDIT_DM_EVENT 1339 /* Device Mapper events */
+#define AUDIT_LANDLOCK_RULESET 1340 /* Landlock ruleset */
+#define AUDIT_LANDLOCK_DROP 1341 /* Landlock ruleset or domain release */
+#define AUDIT_LANDLOCK_DOMAIN 1342 /* Landlock domain */
+#define AUDIT_LANDLOCK_DENIAL 1343 /* Landlock denial */
+#define AUDIT_LANDLOCK_RULE 1344 /* Landlock generic rule */
+#define AUDIT_LANDLOCK_PATH_BENEATH 1345 /* Landlock path_beneath rule */
+#define AUDIT_LANDLOCK_NET_PORT 1346 /* Landlock net_port rule */
#define AUDIT_AVC 1400 /* SE Linux avc denial or grant */
#define AUDIT_SELINUX_ERR 1401 /* Internal SE Linux Errors */
diff --git a/security/Makefile b/security/Makefile
index 59f23849066500..9a6f8e0e697085 100644
--- a/security/Makefile
+++ b/security/Makefile
@@ -15,7 +15,7 @@ obj-$(CONFIG_SECURITY) += security.o
obj-$(CONFIG_SECURITYFS) += inode.o
obj-$(CONFIG_SECURITY_SELINUX) += selinux/
obj-$(CONFIG_SECURITY_SMACK) += smack/
-obj-$(CONFIG_SECURITY) += lsm_audit.o
+obj-$(CONFIG_AUDIT) += lsm_audit.o
obj-$(CONFIG_SECURITY_TOMOYO) += tomoyo/
obj-$(CONFIG_SECURITY_APPARMOR) += apparmor/
obj-$(CONFIG_SECURITY_YAMA) += yama/
diff --git a/security/landlock/.kunitconfig b/security/landlock/.kunitconfig
index 03e11946660429..f9423f01ac5b07 100644
--- a/security/landlock/.kunitconfig
+++ b/security/landlock/.kunitconfig
@@ -1,4 +1,6 @@
+CONFIG_AUDIT=y
CONFIG_KUNIT=y
+CONFIG_NET=y
CONFIG_SECURITY=y
CONFIG_SECURITY_LANDLOCK=y
CONFIG_SECURITY_LANDLOCK_KUNIT_TEST=y
diff --git a/security/landlock/Makefile b/security/landlock/Makefile
index c2e116f2a299b9..a1dfad1b05b33a 100644
--- a/security/landlock/Makefile
+++ b/security/landlock/Makefile
@@ -4,3 +4,5 @@ landlock-y := setup.o syscalls.o object.o ruleset.o \
cred.o ptrace.o fs.o
landlock-$(CONFIG_INET) += net.o
+
+landlock-$(CONFIG_AUDIT) += audit.o
diff --git a/security/landlock/audit.c b/security/landlock/audit.c
new file mode 100644
index 00000000000000..1803fb63ee1894
--- /dev/null
+++ b/security/landlock/audit.c
@@ -0,0 +1,681 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Landlock LSM - Audit helpers
+ *
+ * Copyright © 2023-2024 Microsoft Corporation
+ */
+
+#include <kunit/test.h>
+#include <linux/atomic.h>
+#include <linux/audit.h>
+#include <linux/lsm_audit.h>
+#include <linux/path.h>
+#include <uapi/linux/landlock.h>
+
+#include "audit.h"
+#include "common.h"
+#include "cred.h"
+#include "fs.h"
+#include "ruleset.h"
+
+static atomic64_t ruleset_and_domain_counter = ATOMIC64_INIT(0);
+
+static const char *const op_strings[] = {
+ [0] = "",
+ [LANDLOCK_OP_PTRACE_ACCESS] = "ptrace_access",
+ [LANDLOCK_OP_PTRACE_TRACEME] = "ptrace_traceme",
+ [LANDLOCK_OP_MOUNT] = "mount",
+ [LANDLOCK_OP_MOVE_MOUNT] = "move_mount",
+ [LANDLOCK_OP_UMOUNT] = "umount",
+ [LANDLOCK_OP_REMOUNT] = "remount",
+ [LANDLOCK_OP_PIVOT_ROOT] = "pivot_root",
+ [LANDLOCK_OP_MKDIR] = "mkdir",
+ [LANDLOCK_OP_MKNOD] = "mknod",
+ [LANDLOCK_OP_SYMLINK] = "symlink",
+ [LANDLOCK_OP_UNLINK] = "unlink",
+ [LANDLOCK_OP_RMDIR] = "rmdir",
+ [LANDLOCK_OP_TRUNCATE] = "truncate",
+ [LANDLOCK_OP_OPEN] = "open",
+ [LANDLOCK_OP_IOCTL] = "ioctl",
+ [LANDLOCK_OP_SOCKET_BIND] = "socket_bind",
+ [LANDLOCK_OP_SOCKET_CONNECT] = "socket_connect",
+};
+
+static const char *op_to_string(enum landlock_operation operation)
+{
+ if (WARN_ON_ONCE(operation < 0 || operation > ARRAY_SIZE(op_strings)))
+ return "unknown";
+
+ return op_strings[operation];
+}
+
+static const char *const fs_access_strings[] = {
+ [BIT_INDEX(LANDLOCK_ACCESS_FS_EXECUTE)] = "execute",
+ [BIT_INDEX(LANDLOCK_ACCESS_FS_WRITE_FILE)] = "write_file",
+ [BIT_INDEX(LANDLOCK_ACCESS_FS_READ_FILE)] = "read_file",
+ [BIT_INDEX(LANDLOCK_ACCESS_FS_READ_DIR)] = "read_dir",
+ [BIT_INDEX(LANDLOCK_ACCESS_FS_REMOVE_DIR)] = "remove_dir",
+ [BIT_INDEX(LANDLOCK_ACCESS_FS_REMOVE_FILE)] = "remove_file",
+ [BIT_INDEX(LANDLOCK_ACCESS_FS_MAKE_CHAR)] = "make_char",
+ [BIT_INDEX(LANDLOCK_ACCESS_FS_MAKE_DIR)] = "make_dir",
+ [BIT_INDEX(LANDLOCK_ACCESS_FS_MAKE_REG)] = "make_reg",
+ [BIT_INDEX(LANDLOCK_ACCESS_FS_MAKE_SOCK)] = "make_sock",
+ [BIT_INDEX(LANDLOCK_ACCESS_FS_MAKE_FIFO)] = "make_fifo",
+ [BIT_INDEX(LANDLOCK_ACCESS_FS_MAKE_BLOCK)] = "make_block",
+ [BIT_INDEX(LANDLOCK_ACCESS_FS_MAKE_SYM)] = "make_sym",
+ [BIT_INDEX(LANDLOCK_ACCESS_FS_REFER)] = "refer",
+ [BIT_INDEX(LANDLOCK_ACCESS_FS_TRUNCATE)] = "truncate",
+ [BIT_INDEX(LANDLOCK_ACCESS_FS_IOCTL)] = "ioctl",
+};
+static_assert(ARRAY_SIZE(fs_access_strings) == LANDLOCK_NUM_PUBLIC_ACCESS_FS);
+
+static void log_fs_accesses(struct audit_buffer *const ab,
+ const access_mask_t access_rights)
+{
+ unsigned long access_bit;
+ bool is_first = true;
+ const unsigned long access_mask = access_rights &
+ LANDLOCK_MASK_PUBLIC_ACCESS_FS;
+
+ for_each_set_bit(access_bit, &access_mask,
+ ARRAY_SIZE(fs_access_strings)) {
+ audit_log_format(ab, "%s%s", is_first ? "" : ",",
+ fs_access_strings[access_bit]);
+ is_first = false;
+ }
+}
+
+static const char *const net_access_strings[] = {
+ [BIT_INDEX(LANDLOCK_ACCESS_NET_BIND_TCP)] = "bind_tcp",
+ [BIT_INDEX(LANDLOCK_ACCESS_NET_CONNECT_TCP)] = "connect_tcp",
+};
+static_assert(ARRAY_SIZE(net_access_strings) == LANDLOCK_NUM_ACCESS_NET);
+
+static void log_net_accesses(struct audit_buffer *const ab,
+ const access_mask_t access_rights)
+{
+ unsigned long access_bit;
+ bool is_first = true;
+ const unsigned long access_mask = access_rights;
+
+ for_each_set_bit(access_bit, &access_mask,
+ ARRAY_SIZE(net_access_strings)) {
+ audit_log_format(ab, "%s%s", is_first ? "" : ",",
+ net_access_strings[access_bit]);
+ is_first = false;
+ }
+}
+
+void landlock_log_create_ruleset(struct landlock_ruleset *const ruleset)
+{
+ struct audit_buffer *ab;
+
+ WARN_ON_ONCE(ruleset->id);
+
+ /* Always set an ID. */
+ ruleset->id = atomic64_inc_return(&ruleset_and_domain_counter);
+
+ if (!audit_enabled)
+ return;
+
+ ab = audit_log_start(audit_context(), GFP_ATOMIC,
+ AUDIT_LANDLOCK_RULESET);
+ if (!ab)
+ /* audit_log_lost() call */
+ return;
+
+ /* Saves log state for this domain creation. */
+ ruleset->logged = true;
+
+ /*
+ * Artificially print 0 as the ruleset's version. In practice another
+ * thread could concurrently use the newly created ruleset and add a
+ * new rule before landlock_create_ruleset() returns. This makes sure
+ * the initial version is not missed.
+ */
+ audit_log_format(ab,
+ "op=create_ruleset ruleset=%llu.0 handled_access_fs=",
+ ruleset->id);
+ log_fs_accesses(ab, landlock_get_fs_access_mask(ruleset, 0));
+ audit_log_end(ab);
+}
+
+void landlock_log_restrict_self(struct landlock_ruleset *const domain,
+ struct landlock_ruleset *const ruleset,
+ const u32 ruleset_version)
+{
+ struct audit_buffer *ab;
+
+ WARN_ON_ONCE(domain->id);
+ WARN_ON_ONCE(!ruleset->id);
+
+ if (WARN_ON_ONCE(!domain->hierarchy))
+ return;
+
+ /* Always set an ID. */
+ domain->hierarchy->id =
+ atomic64_inc_return(&ruleset_and_domain_counter);
+
+ if (!audit_enabled)
+ return;
+
+ ab = audit_log_start(audit_context(), GFP_ATOMIC,
+ AUDIT_LANDLOCK_DOMAIN);
+ if (!ab)
+ /* audit_log_lost() call */
+ return;
+
+ /* Saves log state for this domain creation. */
+ domain->hierarchy->logged = true;
+
+ audit_log_format(ab, "domain=%llu ruleset=%llu.%u",
+ domain->hierarchy->id, ruleset->id, ruleset_version);
+ audit_log_format(
+ ab, " parent=%llu",
+ domain->hierarchy->parent ? domain->hierarchy->parent->id : 0);
+ audit_log_end(ab);
+}
+
+/*
+ * This is useful to know when a ruleset will never show again in the audit
+ * log.
+ *
+ * This is the only record that is not directly tied to a syscall entry.
+ */
+void landlock_log_drop_ruleset(const struct landlock_ruleset *const ruleset)
+{
+ struct audit_buffer *ab;
+
+ if (!audit_enabled)
+ return;
+
+#if 0
+ /* Ignores rulesets that were not logged at creation time. */
+ if (!ruleset->logged)
+ return;
+#endif
+
+ // TODO: Replace with AUDIT_LANDLOCK_DROP_RULESET
+ ab = audit_log_start(audit_context(), GFP_ATOMIC, AUDIT_LANDLOCK_DROP);
+ if (!ab)
+ /* audit_log_lost() call */
+ return;
+
+ audit_log_format(ab, "id=%llu id_type=ruleset", ruleset->id);
+ audit_log_end(ab);
+}
+
+/*
+ * This is useful to know when a domain will never show again in the audit log.
+ *
+ * This is the only record that is not directly tied to a syscall entry.
+ */
+void landlock_log_drop_domain(const struct landlock_hierarchy *const hierarchy)
+{
+ struct audit_buffer *ab;
+
+ if (!audit_enabled)
+ return;
+
+#if 0
+ /* Ignores domains that were not logged at creation time. */
+ if (!hierarchy->logged)
+ return;
+#endif
+
+ // TODO: Replace with AUDIT_LANDLOCK_DROP_DOMAIN
+ ab = audit_log_start(audit_context(), GFP_ATOMIC, AUDIT_LANDLOCK_DROP);
+ if (!ab)
+ /* audit_log_lost() call */
+ return;
+
+ audit_log_format(ab, "id=%llu id_type=domain", hierarchy->id);
+ audit_log_end(ab);
+}
+
+static u64 get_domain_id(const struct landlock_ruleset *const domain,
+ const size_t layer)
+{
+ const struct landlock_hierarchy *node = domain->hierarchy;
+ size_t i;
+
+ if (WARN_ON_ONCE(layer >= domain->num_layers))
+ return node->id;
+
+ for (i = domain->num_layers - layer - 1; i > 0; i--) {
+ if (WARN_ON_ONCE(!node->parent))
+ break;
+
+ node = node->parent;
+ }
+ return node->id;
+}
+
+static size_t
+get_level_from_layer_masks(const struct landlock_ruleset *const domain,
+ const access_mask_t access_request,
+ const layer_mask_t (*const layer_masks)[],
+ const size_t layer_masks_size)
+{
+ const unsigned long access_req = access_request;
+ size_t youngest_denied_level = 0;
+ unsigned long access_bit;
+
+ for_each_set_bit(access_bit, &access_req, layer_masks_size) {
+ size_t level;
+
+ if (!(*layer_masks)[access_bit])
+ continue;
+
+ level = __fls((*layer_masks)[access_bit]);
+ if (level > youngest_denied_level)
+ youngest_denied_level = level;
+ }
+ return youngest_denied_level;
+}
+
+static size_t
+get_level_from_deny_masks(const struct landlock_ruleset *const domain,
+ const access_mask_t access_request,
+ const deny_masks_t deny_masks)
+{
+ const unsigned long access_opt = ACCESS_OPTIONAL;
+ size_t youngest_denied_level = 0;
+ size_t access_index = 0;
+ unsigned long access_bit;
+
+ /* Gets the greatest/closest domain ID. */
+ for_each_set_bit(access_bit, &access_opt, LANDLOCK_NUM_ACCESS_FS) {
+ if (access_request & BIT_ULL(access_bit)) {
+ const size_t level =
+ (deny_masks >> (access_index * 4)) & 0xf;
+
+ if (level > youngest_denied_level)
+ youngest_denied_level = level;
+ }
+ access_index++;
+ }
+ return youngest_denied_level;
+}
+
+void landlock_log_denial(const struct landlock_ruleset *const domain,
+ const struct landlock_request *const request)
+{
+ struct audit_buffer *ab;
+ size_t youngest_denied_level;
+
+ if (WARN_ON_ONCE(!domain || !domain->hierarchy || !request))
+ return;
+
+ /* An access request comes either with layer_masks or file_security. */
+ if (WARN_ON_ONCE(request->access &&
+ (!!request->layer_masks == !!request->file_security)))
+ return;
+
+ if (WARN_ON_ONCE(request->layer_masks &&
+ request->layer_masks_size == 0))
+ return;
+
+ if (!audit_enabled)
+ /* audit_log_lost() call */
+ return;
+
+#if 0
+ /*
+ * Domains can only log their denials if their creation was logged as
+ * well. This is a balance to have a consistent scoped view of
+ * domain's denials without performance impact (e.g. deny_masks_t
+ * computation) when audit was disabled at domain creation time. This
+ * way, we know if we can identify domains restricting a resource (e.g.
+ * future opened file descriptor) thanks to its saved layer levels.
+ * See landlock_get_deny_masks().
+ */
+ if (!domain->hierarchy->logged)
+ return;
+#endif
+
+ /* Uses GFP_ATOMIC to not sleep. */
+ ab = audit_log_start(audit_context(), GFP_ATOMIC | __GFP_NOWARN,
+ AUDIT_LANDLOCK_DENIAL);
+ if (!ab)
+ /* audit_log_lost() call */
+ return;
+
+ if (request->access) {
+ /* Gets the nearest domain that denies the request. */
+ if (request->layer_masks) {
+ youngest_denied_level = get_level_from_layer_masks(
+ domain, request->access, request->layer_masks,
+ request->layer_masks_size);
+ } else {
+ youngest_denied_level = get_level_from_deny_masks(
+ domain, request->access,
+ request->file_security->deny_masks);
+ }
+ } else {
+ /* No missing accesses (e.g. mount scope restriction). */
+ youngest_denied_level = 0;
+ }
+
+ audit_log_format(ab, "op=%s domain=%llu",
+ op_to_string(request->operation),
+ get_domain_id(domain, youngest_denied_level));
+ audit_log_lsm_data(ab, &request->audit);
+ audit_log_end(ab);
+}
+
+void landlock_log_add_path_beneath(struct landlock_ruleset *const ruleset,
+ const u32 ruleset_version,
+ const struct path *const path,
+ const access_mask_t access_rights)
+{
+ struct common_audit_data audit_data = {
+ .type = LSM_AUDIT_DATA_PATH,
+ .u.path = *path,
+ };
+ struct audit_buffer *ab;
+
+ if (WARN_ON_ONCE(!ruleset || !path))
+ return;
+ if (WARN_ON_ONCE(!access_rights))
+ return;
+
+ if (!audit_enabled)
+ return;
+
+#if 0
+ if (!ruleset->logged)
+ return;
+#endif
+
+ /* Logs new rule. */
+ ab = audit_log_start(audit_context(), __GFP_NOWARN,
+ AUDIT_LANDLOCK_RULE);
+ if (!ab)
+ /* audit_log_lost() call */
+ return;
+
+ audit_log_format(ab, "ruleset=%llu.%u type=path_beneath", ruleset->id,
+ ruleset_version);
+ audit_log_end(ab);
+
+ /* Logs rule details. */
+ ab = audit_log_start(audit_context(), __GFP_NOWARN,
+ AUDIT_LANDLOCK_PATH_BENEATH);
+ if (!ab)
+ /* audit_log_lost() call */
+ return;
+
+ audit_log_format(ab, "allowed_access=");
+ log_fs_accesses(ab, access_rights);
+ // TODO: Use an AUDIT_PATH record instead (which avoid relying on prefix)
+ audit_log_lsm_data(ab, &audit_data);
+ audit_log_end(ab);
+}
+
+void landlock_log_add_net_port(struct landlock_ruleset *const ruleset,
+ const u32 ruleset_version, const u16 port,
+ const access_mask_t access_rights)
+{
+ struct audit_buffer *ab;
+
+ if (WARN_ON_ONCE(!ruleset))
+ return;
+ if (WARN_ON_ONCE(!access_rights))
+ return;
+
+ if (!audit_enabled)
+ return;
+
+#if 0
+ if (!ruleset->logged)
+ return;
+#endif
+
+ /* Logs new rule. */
+ ab = audit_log_start(audit_context(), __GFP_NOWARN,
+ AUDIT_LANDLOCK_RULE);
+ if (!ab)
+ /* audit_log_lost() call */
+ return;
+
+ // TODO: Factor out with landlock_log_add_path_beneath()
+ audit_log_format(ab, "ruleset=%llu.%u type=net_port", ruleset->id,
+ ruleset_version);
+ audit_log_end(ab);
+
+ /* Logs rule details. */
+ ab = audit_log_start(audit_context(), __GFP_NOWARN,
+ AUDIT_LANDLOCK_NET_PORT);
+ if (!ab)
+ /* audit_log_lost() call */
+ return;
+
+ audit_log_format(ab, "allowed_access=");
+ // TODO: Keep numeric values thanks to the dedicated
+ // AUDIT_LANDLOCK_PATH_BENEATH and AUDIT_LANDLOCK_NET_PORT types?
+ log_net_accesses(ab, access_rights);
+ audit_log_format(ab, " port=%u", port);
+ audit_log_end(ab);
+}
+
+#if 0
+static void debug_print_denying_domains(
+ const struct landlock_audit_domains *const audit_domains)
+{
+ size_t i;
+
+ if (WARN_ON_ONCE(!audit_domains))
+ return;
+
+ pr_warn("XXX %s: begin\n", __func__);
+ for (i = 0; i < audit_domains->num_domains; i++) {
+ pr_warn("XXX %s: layer:%u denied_access:%x\n", __func__,
+ audit_domains->domains[i].layer,
+ audit_domains->domains[i].denied_access);
+ }
+ pr_warn("XXX %s: end\n", __func__);
+}
+#endif
+
+static deny_masks_t build_deny_masks(const size_t layer_level,
+ const access_mask_t access_mask)
+{
+ const unsigned long access_opt = ACCESS_OPTIONAL;
+ size_t access_index = 0;
+ deny_masks_t deny_masks = 0;
+ unsigned long access_bit;
+
+ if (WARN_ON_ONCE(layer_level >= LANDLOCK_MAX_NUM_LAYERS))
+ return 0;
+
+ for_each_set_bit(access_bit, &access_opt, LANDLOCK_NUM_ACCESS_FS) {
+ if (access_mask & BIT_ULL(access_bit))
+ deny_masks |= layer_level << (access_index * 4);
+ access_index++;
+ }
+ return deny_masks;
+}
+
+#ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST
+
+static void test_build_deny_masks(struct kunit *const test)
+{
+ /* clang-format off */
+ KUNIT_EXPECT_EQ(test, 0, build_deny_masks(0, LANDLOCK_ACCESS_FS_EXECUTE));
+ KUNIT_EXPECT_EQ(test, 0, build_deny_masks(0, LANDLOCK_ACCESS_FS_TRUNCATE));
+
+ KUNIT_EXPECT_EQ(test, 0, build_deny_masks(1, LANDLOCK_ACCESS_FS_EXECUTE));
+ KUNIT_EXPECT_EQ(test, 0x1, build_deny_masks(1, LANDLOCK_ACCESS_FS_TRUNCATE));
+
+ KUNIT_EXPECT_EQ(test, 0, build_deny_masks(2, LANDLOCK_ACCESS_FS_READ_FILE));
+ KUNIT_EXPECT_EQ(test, 0x20, build_deny_masks(2, LANDLOCK_ACCESS_FS_IOCTL));
+
+ KUNIT_EXPECT_EQ(test, 0x300, build_deny_masks(3, LANDLOCK_ACCESS_FS_IOCTL_GROUP1));
+ KUNIT_EXPECT_EQ(test, 0x303, build_deny_masks(3,
+ LANDLOCK_ACCESS_FS_TRUNCATE | LANDLOCK_ACCESS_FS_IOCTL_GROUP1));
+
+ KUNIT_EXPECT_EQ(test, 0x4000, build_deny_masks(4, LANDLOCK_ACCESS_FS_IOCTL_GROUP2));
+ KUNIT_EXPECT_EQ(test, 0x4000, build_deny_masks(4,
+ LANDLOCK_ACCESS_FS_IOCTL_GROUP2 | LANDLOCK_ACCESS_FS_WRITE_FILE));
+
+ KUNIT_EXPECT_EQ(test, 0x50000, build_deny_masks(5, LANDLOCK_ACCESS_FS_IOCTL_GROUP3));
+
+ KUNIT_EXPECT_EQ(test, 0xf00000, build_deny_masks(15, LANDLOCK_ACCESS_FS_IOCTL_GROUP4));
+ /* clang-format on */
+}
+
+#endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */
+
+#if 0
+// TODO: Move to fs.c
+static layer_mask_t get_denying_layers(const deny_masks_t deny_masks,
+ const access_mask_t access_mask)
+{
+ unsigned long access_bit;
+ const unsigned long access_rights = access_mask;
+ size_t i;
+ layer_mask_t layer_mask = 0;
+
+ //if (access_mask ACCESS_OPTIONAL)
+
+ //for_each_set_bit(access_bit, &access_rights, LANDLOCK_NUM_ACCESS_FS) {
+ for (i = 0; i < HWEIGHT(ACCESS_OPTIONAL); i++) {
+ const int level = (deny_masks >> i) & 0xf;
+
+ if (level)
+ layer_mask |= 1 << level;
+ }
+ return layer_mask;
+}
+#endif
+
+static deny_masks_t
+get_deny_masks(const access_mask_t optional_access,
+ const layer_mask_t (*const layer_masks)[LANDLOCK_NUM_ACCESS_FS])
+{
+ const unsigned long access_opt = optional_access & ACCESS_OPTIONAL;
+ access_mask_t access_masks[LANDLOCK_MAX_NUM_LAYERS] = {};
+ unsigned long access_bit;
+ size_t layer_level, num_domains;
+ deny_masks_t deny_masks;
+
+ // TODO: remove this check because ACCESS_OPTIONAL should be defined by
+ // the caller, i.e. fs.c
+ WARN_ON_ONCE(optional_access != access_opt);
+
+ if (WARN_ON_ONCE(!layer_masks))
+ return 0;
+
+ if (WARN_ON_ONCE(!access_opt))
+ return 0;
+
+ /* Fills access_masks with potential denied accesses. */
+ num_domains = 0;
+ for_each_set_bit(access_bit, &access_opt, ARRAY_SIZE(*layer_masks)) {
+ if (!(*layer_masks)[access_bit])
+ continue;
+
+ layer_level = __fls((*layer_masks)[access_bit]);
+ if (WARN_ON_ONCE(layer_level > ARRAY_SIZE(access_masks)))
+ return 0;
+
+ if (!access_masks[layer_level])
+ num_domains++;
+ access_masks[layer_level] |= BIT_ULL(access_bit);
+ }
+
+ if (num_domains == 0)
+ return 0;
+
+ /* Converts access_masks to deny_masks. */
+ deny_masks = 0;
+ for (layer_level = 0; layer_level < ARRAY_SIZE(access_masks);
+ layer_level++) {
+ if (!access_masks[layer_level])
+ continue;
+
+ deny_masks |= build_deny_masks(layer_level,
+ access_masks[layer_level]);
+ }
+ //debug_print_denying_domains(audit_domains);
+ return deny_masks;
+}
+
+#ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST
+
+static void test_get_deny_masks(struct kunit *const test)
+{
+ const access_mask_t optional_access = ACCESS_OPTIONAL;
+ const layer_mask_t layers1[LANDLOCK_NUM_ACCESS_FS] = {
+ [BIT_INDEX(LANDLOCK_ACCESS_FS_EXECUTE)] = BIT_ULL(0) |
+ BIT_ULL(9),
+ [BIT_INDEX(LANDLOCK_ACCESS_FS_TRUNCATE)] = BIT_ULL(1),
+ [BIT_INDEX(LANDLOCK_ACCESS_FS_IOCTL)] = BIT_ULL(2) | BIT_ULL(0),
+ [BIT_INDEX(LANDLOCK_ACCESS_FS_IOCTL_GROUP2)] =
+ BIT_ULL(5) | BIT_ULL(4) | BIT_ULL(3),
+ };
+
+ KUNIT_EXPECT_EQ(test, 0x5021,
+ get_deny_masks(optional_access, &layers1));
+}
+
+// TODO: remove
+static void test_fls(struct kunit *const test)
+{
+ KUNIT_EXPECT_EQ(test, -1, __fls(0));
+ KUNIT_EXPECT_EQ(test, 0, __fls(1));
+ KUNIT_EXPECT_EQ(test, 1, __fls(3));
+}
+
+#endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */
+
+/*
+ * Set the layer levels that deny @optional_access.
+ *
+ * TODO: Minimize computing at open-time, and defer most of it at denial-time
+ * thanks to landlock_cred(f_cred)->domain.
+ *
+ * Goal: We don't want to store in each file the full layer_masks[] required by
+ * update_request().
+ */
+void landlock_set_deny_masks(
+ struct landlock_file_security *const file_security,
+ const struct landlock_ruleset *const domain,
+ const access_mask_t optional_access,
+ const layer_mask_t (*const layer_masks)[LANDLOCK_NUM_ACCESS_FS])
+{
+ if (WARN_ON_ONCE(!file_security || !domain || !domain->hierarchy))
+ return;
+
+ /*
+ * To keep a valid deny_masks_t, always set it if the domain's creation
+ * was logged. We must not rely on audit_enabled because it can be
+ * unset. This is consistent with landlock_log_request()'s checks.
+ */
+ if (!domain->hierarchy->logged)
+ return;
+
+ file_security->deny_masks =
+ get_deny_masks(optional_access, layer_masks);
+}
+
+#ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST
+
+/* clang-format off */
+static struct kunit_case test_cases[] = {
+ KUNIT_CASE(test_fls),
+ KUNIT_CASE(test_build_deny_masks),
+ KUNIT_CASE(test_get_deny_masks),
+ {}
+};
+/* clang-format on */
+
+static struct kunit_suite test_suite = {
+ .name = "landlock_audit",
+ .test_cases = test_cases,
+};
+
+kunit_test_suite(test_suite);
+
+#endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */
diff --git a/security/landlock/audit.h b/security/landlock/audit.h
new file mode 100644
index 00000000000000..fc140b9cff18d0
--- /dev/null
+++ b/security/landlock/audit.h
@@ -0,0 +1,139 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * Landlock LSM - Audit helpers
+ *
+ * Copyright © 2023 Microsoft Corporation
+ */
+
+#ifndef _SECURITY_LANDLOCK_AUDIT_H
+#define _SECURITY_LANDLOCK_AUDIT_H
+
+#include <linux/audit.h>
+#include <linux/lsm_audit.h>
+#include <linux/path.h>
+
+#include "fs.h"
+#include "ruleset.h"
+
+enum landlock_operation {
+ LANDLOCK_OP_PTRACE_ACCESS = 1,
+ LANDLOCK_OP_PTRACE_TRACEME,
+ LANDLOCK_OP_MOUNT,
+ LANDLOCK_OP_MOVE_MOUNT,
+ LANDLOCK_OP_UMOUNT,
+ LANDLOCK_OP_REMOUNT,
+ LANDLOCK_OP_PIVOT_ROOT,
+ LANDLOCK_OP_MKDIR,
+ LANDLOCK_OP_MKNOD,
+ LANDLOCK_OP_SYMLINK,
+ LANDLOCK_OP_UNLINK,
+ LANDLOCK_OP_RMDIR,
+ LANDLOCK_OP_TRUNCATE,
+ LANDLOCK_OP_OPEN,
+ LANDLOCK_OP_IOCTL,
+ LANDLOCK_OP_SOCKET_BIND,
+ LANDLOCK_OP_SOCKET_CONNECT,
+};
+
+/*
+ * We should be careful to only use a variable of this type for
+ * landlock_log_denial(). This way, the compiler can remove it entirely if
+ * CONFIG_AUDIT is not set.
+ */
+struct landlock_request {
+ /* Mandatory fields. */
+ const enum landlock_operation operation;
+ struct common_audit_data audit;
+
+ /* Required field for configurable access control. */
+ access_mask_t access;
+
+ /* Required fields for requests with layer masks. */
+ const layer_mask_t (*layer_masks)[];
+ size_t layer_masks_size;
+
+ /* Required field for requests with deny masks. */
+ const struct landlock_file_security *file_security;
+};
+
+#ifdef CONFIG_AUDIT
+
+void landlock_log_create_ruleset(struct landlock_ruleset *const ruleset);
+void landlock_log_drop_ruleset(const struct landlock_ruleset *const ruleset);
+void landlock_log_drop_domain(const struct landlock_hierarchy *const hierarchy);
+void landlock_log_restrict_self(struct landlock_ruleset *const domain,
+ struct landlock_ruleset *const ruleset,
+ const u32 ruleset_version);
+
+void landlock_log_denial(const struct landlock_ruleset *const domain,
+ const struct landlock_request *const request);
+
+void landlock_log_add_path_beneath(struct landlock_ruleset *const ruleset,
+ const u32 ruleset_version,
+ const struct path *const path,
+ const access_mask_t access_rights);
+
+void landlock_log_add_net_port(struct landlock_ruleset *const ruleset,
+ const u32 ruleset_version, const u16 port,
+ const access_mask_t access_rights);
+
+void landlock_set_deny_masks(
+ struct landlock_file_security *const file_security,
+ const struct landlock_ruleset *const domain,
+ const access_mask_t optional_access,
+ const layer_mask_t (*const layer_masks)[LANDLOCK_NUM_ACCESS_FS]);
+
+#else /* CONFIG_AUDIT */
+
+static inline void
+landlock_log_create_ruleset(struct landlock_ruleset *const ruleset)
+{
+}
+
+static inline void
+landlock_log_drop_ruleset(const struct landlock_ruleset *const ruleset)
+{
+}
+
+static inline void
+landlock_log_drop_domain(const struct landlock_hierarchy *const hierarchy)
+{
+}
+
+static inline void
+landlock_log_restrict_self(struct landlock_ruleset *const domain,
+ struct landlock_ruleset *const ruleset,
+ const u32 ruleset_version)
+{
+}
+
+static inline void
+landlock_log_denial(const struct landlock_ruleset *const domain,
+ const struct landlock_request *const request)
+{
+}
+
+static inline void landlock_log_add_path_beneath(
+ struct landlock_ruleset *const ruleset, const u32 ruleset_version,
+ const struct path *const path, const access_mask_t access_rights)
+{
+}
+
+static inline void
+landlock_log_add_net_port(struct landlock_ruleset *const ruleset,
+ const u32 ruleset_version, const u16 port,
+ const access_mask_t access_rights)
+{
+}
+
+static inline void landlock_set_deny_masks(
+ struct landlock_file_security *const file_security,
+ const struct landlock_ruleset *const domain,
+ const access_mask_t optional_access,
+ const layer_mask_t (*const layer_masks)[LANDLOCK_NUM_ACCESS_FS])
+{
+}
+
+#endif /* CONFIG_AUDIT */
+
+#endif /* _SECURITY_LANDLOCK_AUDIT_H */
diff --git a/security/landlock/fs.c b/security/landlock/fs.c
index 90f7f6db1e87dc..9286db550ce123 100644
--- a/security/landlock/fs.c
+++ b/security/landlock/fs.c
@@ -21,6 +21,7 @@
#include <linux/kernel.h>
#include <linux/limits.h>
#include <linux/list.h>
+#include <linux/lsm_audit.h>
#include <linux/lsm_hooks.h>
#include <linux/mount.h>
#include <linux/namei.h>
@@ -34,6 +35,7 @@
#include <uapi/linux/fiemap.h>
#include <uapi/linux/landlock.h>
+#include "audit.h"
#include "common.h"
#include "cred.h"
#include "fs.h"
@@ -89,6 +91,8 @@ static const struct landlock_object_underops landlock_fs_underops = {
/* IOCTL helpers */
+// FIXME:
+#ifndef LANDLOCK_ACCESS_FS_IOCTL_GROUP1
/*
* These are synthetic access rights, which are only used within the kernel, but
* not exposed to callers in userspace. The mapping between these access rights
@@ -107,6 +111,7 @@ static const struct landlock_object_underops landlock_fs_underops = {
LANDLOCK_ACCESS_FS_IOCTL_GROUP3 | \
LANDLOCK_ACCESS_FS_IOCTL_GROUP4)
/* clang-format on */
+#endif
static_assert((IOCTL_GROUPS & LANDLOCK_MASK_ACCESS_FS) == IOCTL_GROUPS);
@@ -303,6 +308,7 @@ int landlock_append_fs_rule(struct landlock_ruleset *const ruleset,
{
access_mask_t handled;
int err;
+ u32 ruleset_version;
struct landlock_id id = {
.type = LANDLOCK_KEY_INODE,
};
@@ -324,12 +330,19 @@ 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);
+ ruleset_version = ruleset->num_rules;
mutex_unlock(&ruleset->lock);
+
/*
* No need to check for an error because landlock_insert_rule()
* increments the refcount for the new object if needed.
*/
landlock_put_object(id.key.object);
+
+ if (!err)
+ landlock_log_add_path_beneath(ruleset, ruleset_version, path,
+ access_rights);
+
return err;
}
@@ -907,28 +920,30 @@ jump_up:
return allowed_parent1 && allowed_parent2;
}
-static int check_access_path(const struct landlock_ruleset *const domain,
- const struct path *const path,
- access_mask_t access_request)
-{
- layer_mask_t layer_masks[LANDLOCK_NUM_ACCESS_FS] = {};
-
- access_request = landlock_init_layer_masks(
- domain, access_request, &layer_masks, LANDLOCK_KEY_INODE);
- if (is_access_to_paths_allowed(domain, path, access_request,
- &layer_masks, NULL, 0, NULL, NULL))
- return 0;
- return -EACCES;
-}
-
static int current_check_access_path(const struct path *const path,
- const access_mask_t access_request)
+ access_mask_t access_request,
+ // TODO: constify request?
+ struct landlock_request *const request)
{
const struct landlock_ruleset *const dom = get_current_fs_domain();
+ layer_mask_t layer_masks[LANDLOCK_NUM_ACCESS_FS] = {};
if (!dom)
return 0;
- return check_access_path(dom, path, access_request);
+
+ access_request = landlock_init_layer_masks(
+ dom, access_request, &layer_masks, LANDLOCK_KEY_INODE);
+ if (is_access_to_paths_allowed(dom, path, access_request, &layer_masks,
+ NULL, 0, NULL, NULL))
+ return 0;
+
+ request->audit.type = LSM_AUDIT_DATA_PATH;
+ request->audit.u.path = *path;
+ request->access = access_request;
+ request->layer_masks = &layer_masks;
+ request->layer_masks_size = ARRAY_SIZE(layer_masks);
+ landlock_log_denial(dom, request);
+ return -EACCES;
}
static access_mask_t get_mode_access(const umode_t mode)
@@ -1323,16 +1338,38 @@ static int hook_sb_mount(const char *const dev_name,
const struct path *const path, const char *const type,
const unsigned long flags, void *const data)
{
- if (!get_current_fs_domain())
+ const struct landlock_ruleset *const dom = get_current_fs_domain();
+ struct landlock_request request = {
+ .operation = LANDLOCK_OP_MOUNT,
+ .audit = {
+ .type = LSM_AUDIT_DATA_PATH,
+ .u.path = *path,
+ },
+ };
+
+ if (!dom)
return 0;
+
+ landlock_log_denial(dom, &request);
return -EPERM;
}
static int hook_move_mount(const struct path *const from_path,
const struct path *const to_path)
{
- if (!get_current_fs_domain())
+ const struct landlock_ruleset *const dom = get_current_fs_domain();
+ struct landlock_request request = {
+ .operation = LANDLOCK_OP_MOVE_MOUNT,
+ .audit = {
+ .type = LSM_AUDIT_DATA_PATH,
+ .u.path = *to_path,
+ },
+ };
+
+ if (!dom)
return 0;
+
+ landlock_log_denial(dom, &request);
return -EPERM;
}
@@ -1342,15 +1379,39 @@ static int hook_move_mount(const struct path *const from_path,
*/
static int hook_sb_umount(struct vfsmount *const mnt, const int flags)
{
- if (!get_current_fs_domain())
+ const struct landlock_ruleset *const dom = get_current_fs_domain();
+ struct landlock_request request = {
+ .operation = LANDLOCK_OP_UMOUNT,
+ .audit = {
+ // TODO: try to print the mounted path
+ // cf. dentry_path()
+ .type = LSM_AUDIT_DATA_DENTRY,
+ .u.dentry = mnt->mnt_root,
+ },
+ };
+
+ if (!dom)
return 0;
+
+ landlock_log_denial(dom, &request);
return -EPERM;
}
static int hook_sb_remount(struct super_block *const sb, void *const mnt_opts)
{
- if (!get_current_fs_domain())
+ const struct landlock_ruleset *const dom = get_current_fs_domain();
+ struct landlock_request request = {
+ .operation = LANDLOCK_OP_REMOUNT,
+ .audit = {
+ .type = LSM_AUDIT_DATA_DENTRY,
+ .u.dentry = sb->s_root,
+ },
+ };
+
+ if (!dom)
return 0;
+
+ landlock_log_denial(dom, &request);
return -EPERM;
}
@@ -1365,8 +1426,19 @@ static int hook_sb_remount(struct super_block *const sb, void *const mnt_opts)
static int hook_sb_pivotroot(const struct path *const old_path,
const struct path *const new_path)
{
- if (!get_current_fs_domain())
+ const struct landlock_ruleset *const dom = get_current_fs_domain();
+ struct landlock_request request = {
+ .operation = LANDLOCK_OP_PIVOT_ROOT,
+ .audit = {
+ .type = LSM_AUDIT_DATA_PATH,
+ .u.path = *new_path,
+ },
+ };
+
+ if (!dom)
return 0;
+
+ landlock_log_denial(dom, &request);
return -EPERM;
}
@@ -1376,6 +1448,7 @@ static int hook_path_link(struct dentry *const old_dentry,
const struct path *const new_dir,
struct dentry *const new_dentry)
{
+ // TODO: Implement fine-grained audit
return current_check_refer_path(old_dentry, new_dir, new_dentry, false,
false);
}
@@ -1394,42 +1467,68 @@ static int hook_path_rename(const struct path *const old_dir,
static int hook_path_mkdir(const struct path *const dir,
struct dentry *const dentry, const umode_t mode)
{
- return current_check_access_path(dir, LANDLOCK_ACCESS_FS_MAKE_DIR);
+ struct landlock_request request = {
+ .operation = LANDLOCK_OP_MKDIR,
+ };
+ // TODO: log dentry?
+
+ return current_check_access_path(dir, LANDLOCK_ACCESS_FS_MAKE_DIR,
+ &request);
}
static int hook_path_mknod(const struct path *const dir,
struct dentry *const dentry, const umode_t mode,
const unsigned int dev)
{
- const struct landlock_ruleset *const dom = get_current_fs_domain();
+ struct landlock_request request = {
+ .operation = LANDLOCK_OP_MKNOD,
+ };
- if (!dom)
- return 0;
- return check_access_path(dom, dir, get_mode_access(mode));
+ return current_check_access_path(dir, get_mode_access(mode), &request);
}
static int hook_path_symlink(const struct path *const dir,
struct dentry *const dentry,
const char *const old_name)
{
- return current_check_access_path(dir, LANDLOCK_ACCESS_FS_MAKE_SYM);
+ struct landlock_request request = {
+ .operation = LANDLOCK_OP_SYMLINK,
+ };
+
+ return current_check_access_path(dir, LANDLOCK_ACCESS_FS_MAKE_SYM,
+ &request);
}
static int hook_path_unlink(const struct path *const dir,
struct dentry *const dentry)
{
- return current_check_access_path(dir, LANDLOCK_ACCESS_FS_REMOVE_FILE);
+ struct landlock_request request = {
+ .operation = LANDLOCK_OP_UNLINK,
+ };
+
+ return current_check_access_path(dir, LANDLOCK_ACCESS_FS_REMOVE_FILE,
+ &request);
}
static int hook_path_rmdir(const struct path *const dir,
struct dentry *const dentry)
{
- return current_check_access_path(dir, LANDLOCK_ACCESS_FS_REMOVE_DIR);
+ struct landlock_request request = {
+ .operation = LANDLOCK_OP_RMDIR,
+ };
+
+ return current_check_access_path(dir, LANDLOCK_ACCESS_FS_REMOVE_DIR,
+ &request);
}
static int hook_path_truncate(const struct path *const path)
{
- return current_check_access_path(path, LANDLOCK_ACCESS_FS_TRUNCATE);
+ struct landlock_request request = {
+ .operation = LANDLOCK_OP_TRUNCATE,
+ };
+
+ return current_check_access_path(path, LANDLOCK_ACCESS_FS_TRUNCATE,
+ &request);
}
/* File hooks */
@@ -1483,6 +1582,13 @@ static int hook_file_open(struct file *const file)
LANDLOCK_ACCESS_FS_IOCTL |
IOCTL_GROUPS;
const struct landlock_ruleset *const dom = get_current_fs_domain();
+ struct landlock_request request = {
+ .operation = LANDLOCK_OP_OPEN,
+ .audit = {
+ .type = LSM_AUDIT_DATA_PATH,
+ .u.path = file->f_path,
+ },
+ };
if (!dom)
return 0;
@@ -1530,15 +1636,29 @@ static int hook_file_open(struct file *const file)
* file access rights in the opened struct file.
*/
landlock_file(file)->allowed_access = allowed_access;
+ landlock_set_deny_masks(landlock_file(file), dom, optional_access,
+ &layer_masks);
if ((open_access_request & allowed_access) == open_access_request)
return 0;
+ request.access = open_access_request;
+ request.layer_masks = &layer_masks;
+ request.layer_masks_size = ARRAY_SIZE(layer_masks);
+ landlock_log_denial(dom, &request);
return -EACCES;
}
static int hook_file_truncate(struct file *const file)
{
+ struct landlock_request request = {
+ .operation = LANDLOCK_OP_TRUNCATE,
+ .audit = {
+ .type = LSM_AUDIT_DATA_FILE,
+ .u.file = file,
+ },
+ };
+
/*
* Allows truncation if the truncate right was available at the time of
* opening the file, to get a consistent access check as for read, write
@@ -1551,12 +1671,23 @@ static int hook_file_truncate(struct file *const file)
*/
if (landlock_file(file)->allowed_access & LANDLOCK_ACCESS_FS_TRUNCATE)
return 0;
+
+ request.access = LANDLOCK_ACCESS_FS_TRUNCATE;
+ request.file_security = landlock_file(file);
+ landlock_log_denial(landlock_cred(file->f_cred)->domain, &request);
return -EACCES;
}
static int hook_file_ioctl(struct file *file, unsigned int cmd,
unsigned long arg)
{
+ struct landlock_request request = {
+ .operation = LANDLOCK_OP_IOCTL,
+ .audit = {
+ .type = LSM_AUDIT_DATA_FILE,
+ .u.file = file,
+ },
+ };
const access_mask_t required_access = required_ioctl_access(cmd);
const access_mask_t allowed_access =
landlock_file(file)->allowed_access;
@@ -1570,6 +1701,9 @@ static int hook_file_ioctl(struct file *file, unsigned int cmd,
if ((allowed_access & required_access) == required_access)
return 0;
+ request.access = LANDLOCK_ACCESS_FS_IOCTL;
+ request.file_security = landlock_file(file);
+ landlock_log_denial(landlock_cred(file->f_cred)->domain, &request);
return -EACCES;
}
diff --git a/security/landlock/fs.h b/security/landlock/fs.h
index c88fe7bda37b69..5b9210ad52b4a9 100644
--- a/security/landlock/fs.h
+++ b/security/landlock/fs.h
@@ -13,9 +13,51 @@
#include <linux/init.h>
#include <linux/rcupdate.h>
+#include "limits.h"
#include "ruleset.h"
#include "setup.h"
+// FIXME: Only use one definition, cf. fs.c
+/*
+ * These are synthetic access rights, which are only used within the kernel, but
+ * not exposed to callers in userspace. The mapping between these access rights
+ * and IOCTL commands is defined in the required_ioctl_access() helper function.
+ */
+#define LANDLOCK_ACCESS_FS_IOCTL_GROUP1 (LANDLOCK_LAST_PUBLIC_ACCESS_FS << 1)
+#define LANDLOCK_ACCESS_FS_IOCTL_GROUP2 (LANDLOCK_LAST_PUBLIC_ACCESS_FS << 2)
+#define LANDLOCK_ACCESS_FS_IOCTL_GROUP3 (LANDLOCK_LAST_PUBLIC_ACCESS_FS << 3)
+#define LANDLOCK_ACCESS_FS_IOCTL_GROUP4 (LANDLOCK_LAST_PUBLIC_ACCESS_FS << 4)
+
+/* ioctl_groups - all synthetic access rights for IOCTL command groups */
+/* clang-format off */
+#define IOCTL_GROUPS ( \
+ LANDLOCK_ACCESS_FS_IOCTL_GROUP1 | \
+ LANDLOCK_ACCESS_FS_IOCTL_GROUP2 | \
+ LANDLOCK_ACCESS_FS_IOCTL_GROUP3 | \
+ LANDLOCK_ACCESS_FS_IOCTL_GROUP4)
+/* clang-format on */
+
+/* clang-format off */
+#define ACCESS_OPTIONAL ( \
+ LANDLOCK_ACCESS_FS_TRUNCATE | \
+ LANDLOCK_ACCESS_FS_IOCTL | \
+ IOCTL_GROUPS)
+/* clang-format on */
+
+/* LANDLOCK_MAX_NUM_LAYERS must be a power of two (cf. deny_masks_t assert). */
+static_assert(HWEIGHT(LANDLOCK_MAX_NUM_LAYERS) == 1);
+
+/* Tracks domains responsible of a denied access. */
+typedef u32 deny_masks_t;
+
+/*
+ * Makes sure all optional access rights can be tied to a layer index (cf.
+ * get_deny_mask).
+ */
+static_assert(BITS_PER_TYPE(deny_masks_t) >=
+ (HWEIGHT(LANDLOCK_MAX_NUM_LAYERS - 1) *
+ HWEIGHT(ACCESS_OPTIONAL)));
+
/**
* struct landlock_inode_security - Inode security blob
*
@@ -52,6 +94,10 @@ struct landlock_file_security {
* needed to authorize later operations on the open file.
*/
access_mask_t allowed_access;
+
+#ifdef CONFIG_AUDIT
+ deny_masks_t deny_masks;
+#endif /* CONFIG_AUDIT */
};
/**
diff --git a/security/landlock/limits.h b/security/landlock/limits.h
index 296795f8a5c127..1d2b79c8bf7764 100644
--- a/security/landlock/limits.h
+++ b/security/landlock/limits.h
@@ -26,6 +26,7 @@
*/
#define LANDLOCK_LAST_PUBLIC_ACCESS_FS LANDLOCK_ACCESS_FS_IOCTL
#define LANDLOCK_MASK_PUBLIC_ACCESS_FS ((LANDLOCK_LAST_PUBLIC_ACCESS_FS << 1) - 1)
+#define LANDLOCK_NUM_PUBLIC_ACCESS_FS __const_hweight64(LANDLOCK_MASK_PUBLIC_ACCESS_FS)
#define LANDLOCK_LAST_ACCESS_FS (LANDLOCK_LAST_PUBLIC_ACCESS_FS << 4)
#define LANDLOCK_MASK_ACCESS_FS ((LANDLOCK_LAST_ACCESS_FS << 1) - 1)
diff --git a/security/landlock/net.c b/security/landlock/net.c
index efa1b644a4afaf..be90504349b8d7 100644
--- a/security/landlock/net.c
+++ b/security/landlock/net.c
@@ -11,6 +11,7 @@
#include <linux/socket.h>
#include <net/ipv6.h>
+#include "audit.h"
#include "common.h"
#include "cred.h"
#include "limits.h"
@@ -21,6 +22,7 @@ int landlock_append_net_rule(struct landlock_ruleset *const ruleset,
const u16 port, access_mask_t access_rights)
{
int err;
+ u32 ruleset_version;
const struct landlock_id id = {
.key.data = (__force uintptr_t)htons(port),
.type = LANDLOCK_KEY_NET_PORT,
@@ -34,8 +36,13 @@ int landlock_append_net_rule(struct landlock_ruleset *const ruleset,
mutex_lock(&ruleset->lock);
err = landlock_insert_rule(ruleset, id, access_rights);
+ ruleset_version = ruleset->num_rules;
mutex_unlock(&ruleset->lock);
+ if (!err)
+ landlock_log_add_net_port(ruleset, ruleset_version, port,
+ access_rights);
+
return err;
}
@@ -64,20 +71,22 @@ static const struct landlock_ruleset *get_current_net_domain(void)
static int current_check_access_socket(struct socket *const sock,
struct sockaddr *const address,
const int addrlen,
- const access_mask_t access_request)
+ access_mask_t access_request,
+ struct landlock_request *const request)
{
__be16 port;
layer_mask_t layer_masks[LANDLOCK_NUM_ACCESS_NET] = {};
const struct landlock_rule *rule;
- access_mask_t handled_access;
struct landlock_id id = {
.type = LANDLOCK_KEY_NET_PORT,
};
+ struct lsm_network_audit audit_net = {};
const struct landlock_ruleset *const dom = get_current_net_domain();
if (!dom)
return 0;
if (WARN_ON_ONCE(dom->num_layers < 1))
+ // FIXME: Should we log this?
return -EACCES;
/* Checks if it's a (potential) TCP socket. */
@@ -90,18 +99,45 @@ static int current_check_access_socket(struct socket *const sock,
switch (address->sa_family) {
case AF_UNSPEC:
- case AF_INET:
+ case AF_INET: {
+ const struct sockaddr_in *addr4;
+
if (addrlen < sizeof(struct sockaddr_in))
return -EINVAL;
- port = ((struct sockaddr_in *)address)->sin_port;
+
+ addr4 = (struct sockaddr_in *)address;
+ port = addr4->sin_port;
+
+ if (access_request == LANDLOCK_ACCESS_NET_CONNECT_TCP) {
+ audit_net.dport = port;
+ audit_net.v4info.daddr = addr4->sin_addr.s_addr;
+ } else {
+ audit_net.sport = port;
+ audit_net.v4info.saddr = addr4->sin_addr.s_addr;
+ }
break;
+ }
#if IS_ENABLED(CONFIG_IPV6)
- case AF_INET6:
+ case AF_INET6: {
+ const struct sockaddr_in6 *addr6;
+
if (addrlen < SIN6_LEN_RFC2133)
return -EINVAL;
- port = ((struct sockaddr_in6 *)address)->sin6_port;
+
+ addr6 = (struct sockaddr_in6 *)address;
+ port = addr6->sin6_port;
+ audit_net.v6info.saddr = addr6->sin6_addr;
+
+ if (access_request == LANDLOCK_ACCESS_NET_CONNECT_TCP) {
+ audit_net.dport = port;
+ audit_net.v6info.daddr = addr6->sin6_addr;
+ } else {
+ audit_net.sport = port;
+ audit_net.v6info.saddr = addr6->sin6_addr;
+ }
break;
+ }
#endif /* IS_ENABLED(CONFIG_IPV6) */
default:
@@ -164,28 +200,45 @@ static int current_check_access_socket(struct socket *const sock,
BUILD_BUG_ON(sizeof(port) > sizeof(id.key.data));
rule = landlock_find_rule(dom, id);
- handled_access = landlock_init_layer_masks(
+ access_request = landlock_init_layer_masks(
dom, access_request, &layer_masks, LANDLOCK_KEY_NET_PORT);
- if (landlock_unmask_layers(rule, handled_access, &layer_masks,
+ if (landlock_unmask_layers(rule, access_request, &layer_masks,
ARRAY_SIZE(layer_masks)))
return 0;
+ audit_net.family = address->sa_family;
+ request->audit.type = LSM_AUDIT_DATA_NET;
+ request->audit.u.net = &audit_net;
+ request->access = access_request;
+ request->layer_masks = &layer_masks;
+ request->layer_masks_size = ARRAY_SIZE(layer_masks);
+ landlock_log_denial(dom, request);
return -EACCES;
}
static int hook_socket_bind(struct socket *const sock,
struct sockaddr *const address, const int addrlen)
{
- return current_check_access_socket(sock, address, addrlen,
- LANDLOCK_ACCESS_NET_BIND_TCP);
+ // TODO: Move to current_check_access_socket() ?
+ struct landlock_request request = {
+ .operation = LANDLOCK_OP_SOCKET_BIND,
+ };
+
+ return current_check_access_socket(
+ sock, address, addrlen, LANDLOCK_ACCESS_NET_BIND_TCP, &request);
}
static int hook_socket_connect(struct socket *const sock,
struct sockaddr *const address,
const int addrlen)
{
+ struct landlock_request request = {
+ .operation = LANDLOCK_OP_SOCKET_CONNECT,
+ };
+
return current_check_access_socket(sock, address, addrlen,
- LANDLOCK_ACCESS_NET_CONNECT_TCP);
+ LANDLOCK_ACCESS_NET_CONNECT_TCP,
+ &request);
}
static struct security_hook_list landlock_hooks[] __ro_after_init = {
diff --git a/security/landlock/ptrace.c b/security/landlock/ptrace.c
index 2bfc533d36e429..84e86e0a805a46 100644
--- a/security/landlock/ptrace.c
+++ b/security/landlock/ptrace.c
@@ -10,10 +10,12 @@
#include <linux/cred.h>
#include <linux/errno.h>
#include <linux/kernel.h>
+#include <linux/lsm_audit.h>
#include <linux/lsm_hooks.h>
#include <linux/rcupdate.h>
#include <linux/sched.h>
+#include "audit.h"
#include "common.h"
#include "cred.h"
#include "ptrace.h"
@@ -64,11 +66,9 @@ static bool task_is_scoped(const struct task_struct *const parent,
static int task_ptrace(const struct task_struct *const parent,
const struct task_struct *const child)
{
- /* Quick return for non-landlocked tasks. */
- if (!landlocked(parent))
- return 0;
if (task_is_scoped(parent, child))
return 0;
+
return -EPERM;
}
@@ -88,7 +88,33 @@ static int task_ptrace(const struct task_struct *const parent,
static int hook_ptrace_access_check(struct task_struct *const child,
const unsigned int mode)
{
- return task_ptrace(current, child);
+ const struct landlock_ruleset *const dom =
+ landlock_get_current_domain();
+ struct landlock_request request = {
+ .operation = LANDLOCK_OP_PTRACE_ACCESS,
+ .audit = {
+ .type = LSM_AUDIT_DATA_TASK,
+ .u.tsk = child,
+ // TODO: log the child domain as well, if any
+ },
+ };
+ int err;
+
+ if (!dom)
+ return 0;
+
+ err = task_ptrace(current, child);
+ if (!err)
+ return 0;
+
+ /*
+ * For the ptrace_access_check case, we log the current/parent domain
+ * and the child task.
+ */
+ if (!(mode & PTRACE_MODE_NOAUDIT))
+ landlock_log_denial(dom, &request);
+
+ return err;
}
/**
@@ -105,7 +131,32 @@ static int hook_ptrace_access_check(struct task_struct *const child,
*/
static int hook_ptrace_traceme(struct task_struct *const parent)
{
- return task_ptrace(parent, current);
+ const struct landlock_ruleset *const parent_dom =
+ landlock_get_task_domain(parent);
+ struct landlock_request request = {
+ .operation = LANDLOCK_OP_PTRACE_TRACEME,
+ .audit = {
+ .type = LSM_AUDIT_DATA_TASK,
+ .u.tsk = parent,
+ // TODO: log the parent domain as well, if any
+ },
+ };
+ int err;
+
+ if (!parent_dom)
+ return 0;
+
+ err = task_ptrace(parent, current);
+ if (!err)
+ return 0;
+
+ /*
+ * For the ptrace_traceme case, we log the parent domain and the parent
+ * task (instead of the current/child ones). Indeed, the requester is
+ * the current task, but the action is performed by the parent one.
+ */
+ landlock_log_denial(parent_dom, &request);
+ return err;
}
static struct security_hook_list landlock_hooks[] __ro_after_init = {
diff --git a/security/landlock/ruleset.c b/security/landlock/ruleset.c
index e0a5fbf9201ade..b4bcd3e6ce810b 100644
--- a/security/landlock/ruleset.c
+++ b/security/landlock/ruleset.c
@@ -20,6 +20,7 @@
#include <linux/spinlock.h>
#include <linux/workqueue.h>
+#include "audit.h"
#include "limits.h"
#include "object.h"
#include "ruleset.h"
@@ -43,6 +44,7 @@ static struct landlock_ruleset *create_ruleset(const u32 num_layers)
new_ruleset->num_layers = num_layers;
/*
+ * id = 0
* hierarchy = NULL
* num_rules = 0
* access_masks[] = 0
@@ -316,6 +318,8 @@ static void put_hierarchy(struct landlock_hierarchy *hierarchy)
while (hierarchy && refcount_dec_and_test(&hierarchy->usage)) {
const struct landlock_hierarchy *const freeme = hierarchy;
+ /* Only a domain has a hierarchy. */
+ landlock_log_drop_domain(hierarchy);
hierarchy = hierarchy->parent;
kfree(freeme);
}
@@ -364,7 +368,8 @@ static int merge_tree(struct landlock_ruleset *const dst,
}
static int merge_ruleset(struct landlock_ruleset *const dst,
- struct landlock_ruleset *const src)
+ struct landlock_ruleset *const src,
+ u32 *const src_version)
{
int err = 0;
@@ -400,6 +405,9 @@ static int merge_ruleset(struct landlock_ruleset *const dst,
#endif /* IS_ENABLED(CONFIG_INET) */
out_unlock:
+ /* Atomically copy the ruleset's version. */
+ *src_version = src->num_rules;
+
mutex_unlock(&src->lock);
mutex_unlock(&dst->lock);
return err;
@@ -498,7 +506,14 @@ static void free_ruleset(struct landlock_ruleset *const ruleset)
free_rule(freeme, LANDLOCK_KEY_NET_PORT);
#endif /* IS_ENABLED(CONFIG_INET) */
- put_hierarchy(ruleset->hierarchy);
+ /* Only a domain has a hierarchy. */
+ if (ruleset->hierarchy) {
+ /* Logs domain release, if any. */
+ put_hierarchy(ruleset->hierarchy);
+ } else {
+ /* Logs ruleset release. */
+ landlock_log_drop_ruleset(ruleset);
+ }
kfree(ruleset);
}
@@ -514,6 +529,10 @@ static void free_ruleset_work(struct work_struct *const work)
struct landlock_ruleset *ruleset;
ruleset = container_of(work, struct landlock_ruleset, work_free);
+
+ /* Only called by hook_cred_free(), hence for a domain. */
+ WARN_ON_ONCE(!ruleset->hierarchy);
+
free_ruleset(ruleset);
}
@@ -530,13 +549,16 @@ void landlock_put_ruleset_deferred(struct landlock_ruleset *const ruleset)
*
* @parent: Parent domain.
* @ruleset: New ruleset to be merged.
+ * @ruleset_version: Returned @ruleset version (i.e. number of rules for now),
+ * if the merge was successful.
*
* Returns the intersection of @parent and @ruleset, or returns @parent if
* @ruleset is empty, or returns a duplicate of @ruleset if @parent is empty.
*/
struct landlock_ruleset *
landlock_merge_ruleset(struct landlock_ruleset *const parent,
- struct landlock_ruleset *const ruleset)
+ struct landlock_ruleset *const ruleset,
+ u32 *const ruleset_version)
{
struct landlock_ruleset *new_dom;
u32 num_layers;
@@ -572,7 +594,7 @@ landlock_merge_ruleset(struct landlock_ruleset *const parent,
goto out_put_dom;
/* ...and including @ruleset. */
- err = merge_ruleset(new_dom, ruleset);
+ err = merge_ruleset(new_dom, ruleset, ruleset_version);
if (err)
goto out_put_dom;
diff --git a/security/landlock/ruleset.h b/security/landlock/ruleset.h
index 5a28ea8e1c3d50..6cfcc8fbfa250f 100644
--- a/security/landlock/ruleset.h
+++ b/security/landlock/ruleset.h
@@ -140,6 +140,16 @@ struct landlock_rule {
* struct landlock_hierarchy - Node in a ruleset hierarchy
*/
struct landlock_hierarchy {
+#ifdef CONFIG_AUDIT
+ /* domain's ID */
+ u64 id;
+ /**
+ * @logged: Set to true when the domain creation was logged. This is
+ * then used to limit domain denial events.
+ */
+ bool logged;
+#endif /* CONFIG_AUDIT */
+
/**
* @parent: Pointer to the parent node, or NULL if it is a root
* Landlock domain.
@@ -159,6 +169,16 @@ struct landlock_hierarchy {
* match an object.
*/
struct landlock_ruleset {
+#ifdef CONFIG_AUDIT
+ /* ruleset's ID, must be 0 for a domain */
+ u64 id;
+ /**
+ * @logged: Set to true when the ruleset creation was logged. This is
+ * then used to limit rules addition events.
+ */
+ bool logged;
+#endif /* CONFIG_AUDIT */
+
/**
* @root_inode: Root of a red-black tree containing &struct
* landlock_rule nodes with inode object. Once a ruleset is tied to a
@@ -204,7 +224,8 @@ struct landlock_ruleset {
refcount_t usage;
/**
* @num_rules: Number of non-overlapping (i.e. not for
- * the same object) rules in this ruleset.
+ * the same object) rules in this ruleset. This is also
+ * used as the ruleset's version.
*/
u32 num_rules;
/**
@@ -244,7 +265,8 @@ int landlock_insert_rule(struct landlock_ruleset *const ruleset,
struct landlock_ruleset *
landlock_merge_ruleset(struct landlock_ruleset *const parent,
- struct landlock_ruleset *const ruleset);
+ struct landlock_ruleset *const ruleset,
+ u32 *const ruleset_version);
const struct landlock_rule *
landlock_find_rule(const struct landlock_ruleset *const ruleset,
diff --git a/security/landlock/syscalls.c b/security/landlock/syscalls.c
index f0bc50003b4684..111d86203bf380 100644
--- a/security/landlock/syscalls.c
+++ b/security/landlock/syscalls.c
@@ -26,6 +26,7 @@
#include <linux/uaccess.h>
#include <uapi/linux/landlock.h>
+#include "audit.h"
#include "cred.h"
#include "fs.h"
#include "limits.h"
@@ -106,6 +107,9 @@ static int fop_ruleset_release(struct inode *const inode,
{
struct landlock_ruleset *ruleset = filp->private_data;
+ /* Only called by ruleset_fops, hence for a ruleset. */
+ WARN_ON_ONCE(ruleset->hierarchy);
+
landlock_put_ruleset(ruleset);
return 0;
}
@@ -214,8 +218,21 @@ SYSCALL_DEFINE3(landlock_create_ruleset,
/* Creates anonymous FD referring to the ruleset. */
ruleset_fd = anon_inode_getfd("[landlock-ruleset]", &ruleset_fops,
ruleset, O_RDWR | O_CLOEXEC);
- if (ruleset_fd < 0)
+ if (ruleset_fd < 0) {
landlock_put_ruleset(ruleset);
+ } else {
+ /*
+ * Doesn't ensure sequential logs because of potential
+ * race-condition caused by another thread using the file
+ * descriptor before it is returned by this syscall. Log
+ * parsers should not imply that audit entries are strictly
+ * sequential, even if they should be in most cases. The
+ * provided guarantee is that rulesets' ID and version always
+ * reflect their state, and domain creation logs are tied to
+ * such state.
+ */
+ landlock_log_create_ruleset(ruleset);
+ }
return ruleset_fd;
}
@@ -465,6 +482,7 @@ SYSCALL_DEFINE2(landlock_restrict_self, const int, ruleset_fd, const __u32,
struct landlock_ruleset *new_dom, *ruleset;
struct cred *new_cred;
struct landlock_cred_security *new_llcred;
+ u32 ruleset_version = 0;
int err;
if (!landlock_initialized)
@@ -499,7 +517,8 @@ SYSCALL_DEFINE2(landlock_restrict_self, const int, ruleset_fd, const __u32,
* There is no possible race condition while copying and manipulating
* the current credentials because they are dedicated per thread.
*/
- new_dom = landlock_merge_ruleset(new_llcred->domain, ruleset);
+ new_dom = landlock_merge_ruleset(new_llcred->domain, ruleset,
+ &ruleset_version);
if (IS_ERR(new_dom)) {
err = PTR_ERR(new_dom);
goto out_put_creds;
@@ -509,6 +528,14 @@ SYSCALL_DEFINE2(landlock_restrict_self, const int, ruleset_fd, const __u32,
landlock_put_ruleset(new_llcred->domain);
new_llcred->domain = new_dom;
+ /*
+ * Because audit entries could be printed concurrently, and to avoid
+ * race-condition between concurrent ruleset update logs, we can rely
+ * on this ruleset version to log the exact state of a ruleset. This
+ * enables a lockless approach to still log states atomically, but not
+ * necessarily sequentially.
+ */
+ landlock_log_restrict_self(new_dom, ruleset, ruleset_version);
landlock_put_ruleset(ruleset);
return commit_creds(new_cred);
diff --git a/security/lsm_audit.c b/security/lsm_audit.c
index 849e832719e215..de29ce8ff7088f 100644
--- a/security/lsm_audit.c
+++ b/security/lsm_audit.c
@@ -189,16 +189,13 @@ static inline void print_ipv4_addr(struct audit_buffer *ab, __be32 addr,
}
/**
- * dump_common_audit_data - helper to dump common audit data
+ * audit_log_lsm_data - helper to log common LSM audit data
* @ab : the audit buffer
* @a : common audit data
- *
*/
-static void dump_common_audit_data(struct audit_buffer *ab,
- struct common_audit_data *a)
+void audit_log_lsm_data(struct audit_buffer *ab,
+ const struct common_audit_data *a)
{
- char comm[sizeof(current->comm)];
-
/*
* To keep stack sizes in check force programmers to notice if they
* start making this union too large! See struct lsm_network_audit
@@ -206,9 +203,6 @@ static void dump_common_audit_data(struct audit_buffer *ab,
*/
BUILD_BUG_ON(sizeof(a->u) > sizeof(void *)*2);
- audit_log_format(ab, " pid=%d comm=", task_tgid_nr(current));
- audit_log_untrustedstring(ab, memcpy(comm, current->comm, sizeof(comm)));
-
switch (a->type) {
case LSM_AUDIT_DATA_NONE:
return;
@@ -429,6 +423,21 @@ static void dump_common_audit_data(struct audit_buffer *ab,
}
/**
+ * dump_common_audit_data - helper to dump common audit data
+ * @ab : the audit buffer
+ * @a : common audit data
+ */
+static void dump_common_audit_data(struct audit_buffer *ab,
+ const struct common_audit_data *a)
+{
+ char comm[sizeof(current->comm)];
+
+ audit_log_format(ab, " pid=%d comm=", task_tgid_nr(current));
+ audit_log_untrustedstring(ab, memcpy(comm, current->comm, sizeof(comm)));
+ audit_log_lsm_data(ab, a);
+}
+
+/**
* common_lsm_audit - generic LSM auditing function
* @a: auxiliary audit data
* @pre_audit: lsm-specific pre-audit callback
diff --git a/tools/testing/kunit/configs/all_tests.config b/tools/testing/kunit/configs/all_tests.config
index 1b8f1abfedf041..2a324d1e23593e 100644
--- a/tools/testing/kunit/configs/all_tests.config
+++ b/tools/testing/kunit/configs/all_tests.config
@@ -35,6 +35,8 @@ CONFIG_DAMON_DBGFS=y
CONFIG_REGMAP_BUILD=y
+CONFIG_AUDIT=y
+
CONFIG_SECURITY=y
CONFIG_SECURITY_APPARMOR=y
CONFIG_SECURITY_LANDLOCK=y