diff options
Diffstat (limited to 'security/landlock')
-rw-r--r-- | security/landlock/.kunitconfig | 4 | ||||
-rw-r--r-- | security/landlock/Kconfig | 15 | ||||
-rw-r--r-- | security/landlock/Makefile | 2 | ||||
-rw-r--r-- | security/landlock/common.h | 2 | ||||
-rw-r--r-- | security/landlock/fs.c | 252 | ||||
-rw-r--r-- | security/landlock/net.c | 7 | ||||
-rw-r--r-- | security/landlock/setup.c | 4 | ||||
-rw-r--r-- | security/landlock/syscalls.c | 18 | ||||
-rw-r--r-- | security/landlock/task.c (renamed from security/landlock/ptrace.c) | 4 | ||||
-rw-r--r-- | security/landlock/task.h (renamed from security/landlock/ptrace.h) | 8 |
10 files changed, 293 insertions, 23 deletions
diff --git a/security/landlock/.kunitconfig b/security/landlock/.kunitconfig new file mode 100644 index 000000000000..03e119466604 --- /dev/null +++ b/security/landlock/.kunitconfig @@ -0,0 +1,4 @@ +CONFIG_KUNIT=y +CONFIG_SECURITY=y +CONFIG_SECURITY_LANDLOCK=y +CONFIG_SECURITY_LANDLOCK_KUNIT_TEST=y diff --git a/security/landlock/Kconfig b/security/landlock/Kconfig index c4bf0d5eff39..3f1493402052 100644 --- a/security/landlock/Kconfig +++ b/security/landlock/Kconfig @@ -20,3 +20,18 @@ config SECURITY_LANDLOCK If you are unsure how to answer this question, answer N. Otherwise, you should also prepend "landlock," to the content of CONFIG_LSM to enable Landlock at boot time. + +config SECURITY_LANDLOCK_KUNIT_TEST + bool "KUnit tests for Landlock" if !KUNIT_ALL_TESTS + depends on KUNIT=y + depends on SECURITY_LANDLOCK + default KUNIT_ALL_TESTS + help + Build KUnit tests for Landlock. + + See the KUnit documentation in Documentation/dev-tools/kunit + + Run all KUnit tests for Landlock with: + ./tools/testing/kunit/kunit.py run --kunitconfig security/landlock + + If you are unsure how to answer this question, answer N. diff --git a/security/landlock/Makefile b/security/landlock/Makefile index c2e116f2a299..b4538b7cf7d2 100644 --- a/security/landlock/Makefile +++ b/security/landlock/Makefile @@ -1,6 +1,6 @@ obj-$(CONFIG_SECURITY_LANDLOCK) := landlock.o landlock-y := setup.o syscalls.o object.o ruleset.o \ - cred.o ptrace.o fs.o + cred.o task.o fs.o landlock-$(CONFIG_INET) += net.o diff --git a/security/landlock/common.h b/security/landlock/common.h index 5dc0fe15707d..0eb1d34c2eae 100644 --- a/security/landlock/common.h +++ b/security/landlock/common.h @@ -17,4 +17,6 @@ #define pr_fmt(fmt) LANDLOCK_NAME ": " fmt +#define BIT_INDEX(bit) HWEIGHT(bit - 1) + #endif /* _SECURITY_LANDLOCK_COMMON_H */ diff --git a/security/landlock/fs.c b/security/landlock/fs.c index 0171f7eb6ee1..c15559432d3d 100644 --- a/security/landlock/fs.c +++ b/security/landlock/fs.c @@ -7,6 +7,7 @@ * Copyright © 2021-2022 Microsoft Corporation */ +#include <kunit/test.h> #include <linux/atomic.h> #include <linux/bitops.h> #include <linux/bits.h> @@ -247,15 +248,18 @@ get_handled_fs_accesses(const struct landlock_ruleset *const domain) LANDLOCK_ACCESS_FS_INITIALLY_DENIED; } -static const struct landlock_ruleset *get_current_fs_domain(void) +static const struct landlock_ruleset * +get_fs_domain(const struct landlock_ruleset *const domain) { - const struct landlock_ruleset *const dom = - landlock_get_current_domain(); - - if (!dom || !get_raw_handled_fs_accesses(dom)) + if (!domain || !get_raw_handled_fs_accesses(domain)) return NULL; - return dom; + return domain; +} + +static const struct landlock_ruleset *get_current_fs_domain(void) +{ + return get_fs_domain(landlock_get_current_domain()); } /* @@ -311,6 +315,119 @@ static bool no_more_access( return true; } +#define NMA_TRUE(...) KUNIT_EXPECT_TRUE(test, no_more_access(__VA_ARGS__)) +#define NMA_FALSE(...) KUNIT_EXPECT_FALSE(test, no_more_access(__VA_ARGS__)) + +#ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST + +static void test_no_more_access(struct kunit *const test) +{ + const layer_mask_t rx0[LANDLOCK_NUM_ACCESS_FS] = { + [BIT_INDEX(LANDLOCK_ACCESS_FS_EXECUTE)] = BIT_ULL(0), + [BIT_INDEX(LANDLOCK_ACCESS_FS_READ_FILE)] = BIT_ULL(0), + }; + const layer_mask_t mx0[LANDLOCK_NUM_ACCESS_FS] = { + [BIT_INDEX(LANDLOCK_ACCESS_FS_EXECUTE)] = BIT_ULL(0), + [BIT_INDEX(LANDLOCK_ACCESS_FS_MAKE_REG)] = BIT_ULL(0), + }; + const layer_mask_t x0[LANDLOCK_NUM_ACCESS_FS] = { + [BIT_INDEX(LANDLOCK_ACCESS_FS_EXECUTE)] = BIT_ULL(0), + }; + const layer_mask_t x1[LANDLOCK_NUM_ACCESS_FS] = { + [BIT_INDEX(LANDLOCK_ACCESS_FS_EXECUTE)] = BIT_ULL(1), + }; + const layer_mask_t x01[LANDLOCK_NUM_ACCESS_FS] = { + [BIT_INDEX(LANDLOCK_ACCESS_FS_EXECUTE)] = BIT_ULL(0) | + BIT_ULL(1), + }; + const layer_mask_t allows_all[LANDLOCK_NUM_ACCESS_FS] = {}; + + /* Checks without restriction. */ + NMA_TRUE(&x0, &allows_all, false, &allows_all, NULL, false); + NMA_TRUE(&allows_all, &x0, false, &allows_all, NULL, false); + NMA_FALSE(&x0, &x0, false, &allows_all, NULL, false); + + /* + * Checks that we can only refer a file if no more access could be + * inherited. + */ + NMA_TRUE(&x0, &x0, false, &rx0, NULL, false); + NMA_TRUE(&rx0, &rx0, false, &rx0, NULL, false); + NMA_FALSE(&rx0, &rx0, false, &x0, NULL, false); + NMA_FALSE(&rx0, &rx0, false, &x1, NULL, false); + + /* Checks allowed referring with different nested domains. */ + NMA_TRUE(&x0, &x1, false, &x0, NULL, false); + NMA_TRUE(&x1, &x0, false, &x0, NULL, false); + NMA_TRUE(&x0, &x01, false, &x0, NULL, false); + NMA_TRUE(&x0, &x01, false, &rx0, NULL, false); + NMA_TRUE(&x01, &x0, false, &x0, NULL, false); + NMA_TRUE(&x01, &x0, false, &rx0, NULL, false); + NMA_FALSE(&x01, &x01, false, &x0, NULL, false); + + /* Checks that file access rights are also enforced for a directory. */ + NMA_FALSE(&rx0, &rx0, true, &x0, NULL, false); + + /* Checks that directory access rights don't impact file referring... */ + NMA_TRUE(&mx0, &mx0, false, &x0, NULL, false); + /* ...but only directory referring. */ + NMA_FALSE(&mx0, &mx0, true, &x0, NULL, false); + + /* Checks directory exchange. */ + NMA_TRUE(&mx0, &mx0, true, &mx0, &mx0, true); + NMA_TRUE(&mx0, &mx0, true, &mx0, &x0, true); + NMA_FALSE(&mx0, &mx0, true, &x0, &mx0, true); + NMA_FALSE(&mx0, &mx0, true, &x0, &x0, true); + NMA_FALSE(&mx0, &mx0, true, &x1, &x1, true); + + /* Checks file exchange with directory access rights... */ + NMA_TRUE(&mx0, &mx0, false, &mx0, &mx0, false); + NMA_TRUE(&mx0, &mx0, false, &mx0, &x0, false); + NMA_TRUE(&mx0, &mx0, false, &x0, &mx0, false); + NMA_TRUE(&mx0, &mx0, false, &x0, &x0, false); + /* ...and with file access rights. */ + NMA_TRUE(&rx0, &rx0, false, &rx0, &rx0, false); + NMA_TRUE(&rx0, &rx0, false, &rx0, &x0, false); + NMA_FALSE(&rx0, &rx0, false, &x0, &rx0, false); + NMA_FALSE(&rx0, &rx0, false, &x0, &x0, false); + NMA_FALSE(&rx0, &rx0, false, &x1, &x1, false); + + /* + * Allowing the following requests should not be a security risk + * because domain 0 denies execute access, and domain 1 is always + * nested with domain 0. However, adding an exception for this case + * would mean to check all nested domains to make sure none can get + * more privileges (e.g. processes only sandboxed by domain 0). + * Moreover, this behavior (i.e. composition of N domains) could then + * be inconsistent compared to domain 1's ruleset alone (e.g. it might + * be denied to link/rename with domain 1's ruleset, whereas it would + * be allowed if nested on top of domain 0). Another drawback would be + * to create a cover channel that could enable sandboxed processes to + * infer most of the filesystem restrictions from their domain. To + * make it simple, efficient, safe, and more consistent, this case is + * always denied. + */ + NMA_FALSE(&x1, &x1, false, &x0, NULL, false); + NMA_FALSE(&x1, &x1, false, &rx0, NULL, false); + NMA_FALSE(&x1, &x1, true, &x0, NULL, false); + NMA_FALSE(&x1, &x1, true, &rx0, NULL, false); + + /* Checks the same case of exclusive domains with a file... */ + NMA_TRUE(&x1, &x1, false, &x01, NULL, false); + NMA_FALSE(&x1, &x1, false, &x01, &x0, false); + NMA_FALSE(&x1, &x1, false, &x01, &x01, false); + NMA_FALSE(&x1, &x1, false, &x0, &x0, false); + /* ...and with a directory. */ + NMA_FALSE(&x1, &x1, false, &x0, &x0, true); + NMA_FALSE(&x1, &x1, true, &x0, &x0, false); + NMA_FALSE(&x1, &x1, true, &x0, &x0, true); +} + +#endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */ + +#undef NMA_TRUE +#undef NMA_FALSE + /* * Removes @layer_masks accesses that are not requested. * @@ -331,6 +448,57 @@ scope_to_request(const access_mask_t access_request, return !memchr_inv(layer_masks, 0, sizeof(*layer_masks)); } +#ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST + +static void test_scope_to_request_with_exec_none(struct kunit *const test) +{ + /* Allows everything. */ + layer_mask_t layer_masks[LANDLOCK_NUM_ACCESS_FS] = {}; + + /* Checks and scopes with execute. */ + KUNIT_EXPECT_TRUE(test, scope_to_request(LANDLOCK_ACCESS_FS_EXECUTE, + &layer_masks)); + KUNIT_EXPECT_EQ(test, 0, + layer_masks[BIT_INDEX(LANDLOCK_ACCESS_FS_EXECUTE)]); + KUNIT_EXPECT_EQ(test, 0, + layer_masks[BIT_INDEX(LANDLOCK_ACCESS_FS_WRITE_FILE)]); +} + +static void test_scope_to_request_with_exec_some(struct kunit *const test) +{ + /* Denies execute and write. */ + layer_mask_t layer_masks[LANDLOCK_NUM_ACCESS_FS] = { + [BIT_INDEX(LANDLOCK_ACCESS_FS_EXECUTE)] = BIT_ULL(0), + [BIT_INDEX(LANDLOCK_ACCESS_FS_WRITE_FILE)] = BIT_ULL(1), + }; + + /* Checks and scopes with execute. */ + KUNIT_EXPECT_FALSE(test, scope_to_request(LANDLOCK_ACCESS_FS_EXECUTE, + &layer_masks)); + KUNIT_EXPECT_EQ(test, BIT_ULL(0), + layer_masks[BIT_INDEX(LANDLOCK_ACCESS_FS_EXECUTE)]); + KUNIT_EXPECT_EQ(test, 0, + layer_masks[BIT_INDEX(LANDLOCK_ACCESS_FS_WRITE_FILE)]); +} + +static void test_scope_to_request_without_access(struct kunit *const test) +{ + /* Denies execute and write. */ + layer_mask_t layer_masks[LANDLOCK_NUM_ACCESS_FS] = { + [BIT_INDEX(LANDLOCK_ACCESS_FS_EXECUTE)] = BIT_ULL(0), + [BIT_INDEX(LANDLOCK_ACCESS_FS_WRITE_FILE)] = BIT_ULL(1), + }; + + /* Checks and scopes without access request. */ + KUNIT_EXPECT_TRUE(test, scope_to_request(0, &layer_masks)); + KUNIT_EXPECT_EQ(test, 0, + layer_masks[BIT_INDEX(LANDLOCK_ACCESS_FS_EXECUTE)]); + KUNIT_EXPECT_EQ(test, 0, + layer_masks[BIT_INDEX(LANDLOCK_ACCESS_FS_WRITE_FILE)]); +} + +#endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */ + /* * Returns true if there is at least one access right different than * LANDLOCK_ACCESS_FS_REFER. @@ -354,6 +522,51 @@ is_eacces(const layer_mask_t (*const layer_masks)[LANDLOCK_NUM_ACCESS_FS], return false; } +#define IE_TRUE(...) KUNIT_EXPECT_TRUE(test, is_eacces(__VA_ARGS__)) +#define IE_FALSE(...) KUNIT_EXPECT_FALSE(test, is_eacces(__VA_ARGS__)) + +#ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST + +static void test_is_eacces_with_none(struct kunit *const test) +{ + const layer_mask_t layer_masks[LANDLOCK_NUM_ACCESS_FS] = {}; + + IE_FALSE(&layer_masks, 0); + IE_FALSE(&layer_masks, LANDLOCK_ACCESS_FS_REFER); + IE_FALSE(&layer_masks, LANDLOCK_ACCESS_FS_EXECUTE); + IE_FALSE(&layer_masks, LANDLOCK_ACCESS_FS_WRITE_FILE); +} + +static void test_is_eacces_with_refer(struct kunit *const test) +{ + const layer_mask_t layer_masks[LANDLOCK_NUM_ACCESS_FS] = { + [BIT_INDEX(LANDLOCK_ACCESS_FS_REFER)] = BIT_ULL(0), + }; + + IE_FALSE(&layer_masks, 0); + IE_FALSE(&layer_masks, LANDLOCK_ACCESS_FS_REFER); + IE_FALSE(&layer_masks, LANDLOCK_ACCESS_FS_EXECUTE); + IE_FALSE(&layer_masks, LANDLOCK_ACCESS_FS_WRITE_FILE); +} + +static void test_is_eacces_with_write(struct kunit *const test) +{ + const layer_mask_t layer_masks[LANDLOCK_NUM_ACCESS_FS] = { + [BIT_INDEX(LANDLOCK_ACCESS_FS_WRITE_FILE)] = BIT_ULL(0), + }; + + IE_FALSE(&layer_masks, 0); + IE_FALSE(&layer_masks, LANDLOCK_ACCESS_FS_REFER); + IE_FALSE(&layer_masks, LANDLOCK_ACCESS_FS_EXECUTE); + + IE_TRUE(&layer_masks, LANDLOCK_ACCESS_FS_WRITE_FILE); +} + +#endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */ + +#undef IE_TRUE +#undef IE_FALSE + /** * is_access_to_paths_allowed - Check accesses for requests with a common path * @@ -1124,7 +1337,8 @@ static int hook_file_open(struct file *const file) layer_mask_t layer_masks[LANDLOCK_NUM_ACCESS_FS] = {}; access_mask_t open_access_request, full_access_request, allowed_access; const access_mask_t optional_access = LANDLOCK_ACCESS_FS_TRUNCATE; - const struct landlock_ruleset *const dom = get_current_fs_domain(); + const struct landlock_ruleset *const dom = + get_fs_domain(landlock_cred(file->f_cred)->domain); if (!dom) return 0; @@ -1225,3 +1439,27 @@ __init void landlock_add_fs_hooks(void) security_add_hooks(landlock_hooks, ARRAY_SIZE(landlock_hooks), &landlock_lsmid); } + +#ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST + +/* clang-format off */ +static struct kunit_case test_cases[] = { + KUNIT_CASE(test_no_more_access), + KUNIT_CASE(test_scope_to_request_with_exec_none), + KUNIT_CASE(test_scope_to_request_with_exec_some), + KUNIT_CASE(test_scope_to_request_without_access), + KUNIT_CASE(test_is_eacces_with_none), + KUNIT_CASE(test_is_eacces_with_refer), + KUNIT_CASE(test_is_eacces_with_write), + {} +}; +/* clang-format on */ + +static struct kunit_suite test_suite = { + .name = "landlock_fs", + .test_cases = test_cases, +}; + +kunit_test_suite(test_suite); + +#endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */ diff --git a/security/landlock/net.c b/security/landlock/net.c index efa1b644a4af..c8bcd29bde09 100644 --- a/security/landlock/net.c +++ b/security/landlock/net.c @@ -64,12 +64,11 @@ 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) { __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, }; @@ -164,9 +163,9 @@ 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; diff --git a/security/landlock/setup.c b/security/landlock/setup.c index f6dd33143b7f..28519a45b11f 100644 --- a/security/landlock/setup.c +++ b/security/landlock/setup.c @@ -14,8 +14,8 @@ #include "cred.h" #include "fs.h" #include "net.h" -#include "ptrace.h" #include "setup.h" +#include "task.h" bool landlock_initialized __ro_after_init = false; @@ -34,7 +34,7 @@ const struct lsm_id landlock_lsmid = { static int __init landlock_init(void) { landlock_add_cred_hooks(); - landlock_add_ptrace_hooks(); + landlock_add_task_hooks(); landlock_add_fs_hooks(); landlock_add_net_hooks(); landlock_initialized = true; diff --git a/security/landlock/syscalls.c b/security/landlock/syscalls.c index 898358f57fa0..6788e73b6681 100644 --- a/security/landlock/syscalls.c +++ b/security/landlock/syscalls.c @@ -33,6 +33,18 @@ #include "ruleset.h" #include "setup.h" +static bool is_initialized(void) +{ + if (likely(landlock_initialized)) + return true; + + pr_warn_once( + "Disabled but requested by user space. " + "You should enable Landlock at boot time: " + "https://docs.kernel.org/userspace-api/landlock.html#boot-time-configuration\n"); + return false; +} + /** * copy_min_struct_from_user - Safe future-proof argument copying * @@ -173,7 +185,7 @@ SYSCALL_DEFINE3(landlock_create_ruleset, /* Build-time checks. */ build_check_abi(); - if (!landlock_initialized) + if (!is_initialized()) return -EOPNOTSUPP; if (flags) { @@ -398,7 +410,7 @@ SYSCALL_DEFINE4(landlock_add_rule, const int, ruleset_fd, struct landlock_ruleset *ruleset; int err; - if (!landlock_initialized) + if (!is_initialized()) return -EOPNOTSUPP; /* No flag for now. */ @@ -458,7 +470,7 @@ SYSCALL_DEFINE2(landlock_restrict_self, const int, ruleset_fd, const __u32, struct landlock_cred_security *new_llcred; int err; - if (!landlock_initialized) + if (!is_initialized()) return -EOPNOTSUPP; /* diff --git a/security/landlock/ptrace.c b/security/landlock/task.c index 2bfc533d36e4..849f5123610b 100644 --- a/security/landlock/ptrace.c +++ b/security/landlock/task.c @@ -16,9 +16,9 @@ #include "common.h" #include "cred.h" -#include "ptrace.h" #include "ruleset.h" #include "setup.h" +#include "task.h" /** * domain_scope_le - Checks domain ordering for scoped ptrace @@ -113,7 +113,7 @@ static struct security_hook_list landlock_hooks[] __ro_after_init = { LSM_HOOK_INIT(ptrace_traceme, hook_ptrace_traceme), }; -__init void landlock_add_ptrace_hooks(void) +__init void landlock_add_task_hooks(void) { security_add_hooks(landlock_hooks, ARRAY_SIZE(landlock_hooks), &landlock_lsmid); diff --git a/security/landlock/ptrace.h b/security/landlock/task.h index 265b220ae3bf..7c00360219a2 100644 --- a/security/landlock/ptrace.h +++ b/security/landlock/task.h @@ -6,9 +6,9 @@ * Copyright © 2019 ANSSI */ -#ifndef _SECURITY_LANDLOCK_PTRACE_H -#define _SECURITY_LANDLOCK_PTRACE_H +#ifndef _SECURITY_LANDLOCK_TASK_H +#define _SECURITY_LANDLOCK_TASK_H -__init void landlock_add_ptrace_hooks(void); +__init void landlock_add_task_hooks(void); -#endif /* _SECURITY_LANDLOCK_PTRACE_H */ +#endif /* _SECURITY_LANDLOCK_TASK_H */ |