diff options
author | Mickaël Salaün <mic@digikod.net> | 2024-02-09 18:20:16 +0100 |
---|---|---|
committer | Mickaël Salaün <mic@digikod.net> | 2024-02-09 18:26:08 +0100 |
commit | 87033059ff9ee7214eabf93e7cacf1d0ec5e9454 (patch) | |
tree | 1a6dd7ccdcd49f175c0144bbce004dcb9bea5ab2 | |
parent | 305aea29fd67a4942038a761139a1676086cd782 (diff) | |
download | linux-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.h | 22 | ||||
-rw-r--r-- | include/uapi/linux/audit.h | 7 | ||||
-rw-r--r-- | security/Makefile | 2 | ||||
-rw-r--r-- | security/landlock/.kunitconfig | 2 | ||||
-rw-r--r-- | security/landlock/Makefile | 2 | ||||
-rw-r--r-- | security/landlock/audit.c | 681 | ||||
-rw-r--r-- | security/landlock/audit.h | 139 | ||||
-rw-r--r-- | security/landlock/fs.c | 194 | ||||
-rw-r--r-- | security/landlock/fs.h | 46 | ||||
-rw-r--r-- | security/landlock/limits.h | 1 | ||||
-rw-r--r-- | security/landlock/net.c | 75 | ||||
-rw-r--r-- | security/landlock/ptrace.c | 61 | ||||
-rw-r--r-- | security/landlock/ruleset.c | 30 | ||||
-rw-r--r-- | security/landlock/ruleset.h | 26 | ||||
-rw-r--r-- | security/landlock/syscalls.c | 31 | ||||
-rw-r--r-- | security/lsm_audit.c | 27 | ||||
-rw-r--r-- | tools/testing/kunit/configs/all_tests.config | 2 |
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 |