| // SPDX-License-Identifier: GPL-2.0-only | |
| /* | |
| * Landlock - Domain management | |
| * | |
| * Copyright © 2016-2020 Mickaël Salaün <mic@digikod.net> | |
| * Copyright © 2018-2020 ANSSI | |
| * Copyright © 2024-2025 Microsoft Corporation | |
| * Copyright © 2026 Cloudflare, Inc. | |
| */ | |
| #include <kunit/test.h> | |
| #include <linux/bitops.h> | |
| #include <linux/bits.h> | |
| #include <linux/cleanup.h> | |
| #include <linux/cred.h> | |
| #include <linux/err.h> | |
| #include <linux/file.h> | |
| #include <linux/lockdep.h> | |
| #include <linux/mm.h> | |
| #include <linux/mutex.h> | |
| #include <linux/overflow.h> | |
| #include <linux/path.h> | |
| #include <linux/pid.h> | |
| #include <linux/rbtree.h> | |
| #include <linux/refcount.h> | |
| #include <linux/sched.h> | |
| #include <linux/signal.h> | |
| #include <linux/slab.h> | |
| #include <linux/uidgid.h> | |
| #include <linux/workqueue.h> | |
| #include "access.h" | |
| #include "common.h" | |
| #include "domain.h" | |
| #include "id.h" | |
| #include "limits.h" | |
| #include "ruleset.h" | |
| static void build_check_domain(void) | |
| { | |
| const struct landlock_domain domain = { | |
| .num_layers = ~0, | |
| }; | |
| BUILD_BUG_ON(domain.num_layers < LANDLOCK_MAX_NUM_LAYERS); | |
| } | |
| static struct landlock_domain *create_domain(const u32 num_layers) | |
| { | |
| struct landlock_domain *new_domain; | |
| build_check_domain(); | |
| new_domain = kzalloc_flex(*new_domain, handled_masks, num_layers, | |
| GFP_KERNEL_ACCOUNT); | |
| if (!new_domain) | |
| return ERR_PTR(-ENOMEM); | |
| refcount_set(&new_domain->usage, 1); | |
| new_domain->rules.root_inode = RB_ROOT; | |
| #if IS_ENABLED(CONFIG_INET) | |
| new_domain->rules.root_net_port = RB_ROOT; | |
| #endif /* IS_ENABLED(CONFIG_INET) */ | |
| new_domain->num_layers = num_layers; | |
| return new_domain; | |
| } | |
| static void free_domain(struct landlock_domain *const domain) | |
| { | |
| might_sleep(); | |
| landlock_free_rules(&domain->rules); | |
| landlock_put_hierarchy(domain->hierarchy); | |
| kfree(domain); | |
| } | |
| void landlock_put_domain(struct landlock_domain *const domain) | |
| { | |
| might_sleep(); | |
| if (domain && refcount_dec_and_test(&domain->usage)) | |
| free_domain(domain); | |
| } | |
| static void free_domain_work(struct work_struct *const work) | |
| { | |
| struct landlock_domain *domain; | |
| domain = container_of(work, struct landlock_domain, work_free); | |
| free_domain(domain); | |
| } | |
| void landlock_put_domain_deferred(struct landlock_domain *const domain) | |
| { | |
| if (domain && refcount_dec_and_test(&domain->usage)) { | |
| INIT_WORK(&domain->work_free, free_domain_work); | |
| schedule_work(&domain->work_free); | |
| } | |
| } | |
| /* The returned access has the same lifetime as the domain. */ | |
| static const struct landlock_rule * | |
| find_rule(const struct landlock_domain *const domain, | |
| const struct landlock_id id) | |
| { | |
| const struct rb_root *root; | |
| const struct rb_node *node; | |
| root = landlock_get_rule_root((struct landlock_rules *)&domain->rules, | |
| id.type); | |
| if (IS_ERR(root)) | |
| return NULL; | |
| node = root->rb_node; | |
| while (node) { | |
| struct landlock_rule *this = | |
| rb_entry(node, struct landlock_rule, node); | |
| if (this->key.data == id.key.data) | |
| return this; | |
| if (this->key.data < id.key.data) | |
| node = node->rb_right; | |
| else | |
| node = node->rb_left; | |
| } | |
| return NULL; | |
| } | |
| /** | |
| * landlock_unmask_layers - Remove the access rights in @masks which are | |
| * granted by a matching rule | |
| * | |
| * Looks up the rule matching @id in @domain, then updates the set of | |
| * (per-layer) unfulfilled access rights @masks so that all the access rights | |
| * granted by that rule are removed (because they are now fulfilled). | |
| * | |
| * @domain: The Landlock domain to search for a matching rule. | |
| * @id: Identifier for the rule target (e.g. inode, port). | |
| * @masks: A matrix of unfulfilled access rights for each layer. | |
| * @matched_rule: Optional output for the matched rule (for tracing); set to | |
| * the matching rule when non-NULL, unchanged otherwise. | |
| * | |
| * Return: True if the request is allowed (i.e. the access rights granted all | |
| * remaining unfulfilled access rights and masks has no leftover set bits). | |
| */ | |
| bool landlock_unmask_layers(const struct landlock_domain *const domain, | |
| const struct landlock_id id, | |
| struct layer_masks *masks, | |
| const struct landlock_rule **matched_rule) | |
| { | |
| const struct landlock_rule *rule; | |
| if (!masks) | |
| return true; | |
| rule = find_rule(domain, id); | |
| if (!rule) | |
| return false; | |
| if (matched_rule) | |
| *matched_rule = rule; | |
| /* | |
| * An access is granted if, for each policy layer, at least one rule | |
| * encountered on the pathwalk grants the requested access, regardless | |
| * of its position in the layer stack. We must then check the remaining | |
| * layers for each inode, from the first added layer to the last one. | |
| * When there are multiple requested accesses, for each policy layer, | |
| * the full set of requested accesses may not be granted by only one | |
| * rule, but by the union (binary OR) of multiple rules. For example, | |
| * /a/b <execute> + /a <read> grants /a/b <execute + read>. | |
| * | |
| * This function is called once per matching rule during the pathwalk, | |
| * progressively clearing bits in @masks. The overall access decision | |
| * is per-layer: access is granted iff masks->layers[l].access == 0 for | |
| * all layers l. When two independent mechanisms can each grant access | |
| * within a layer (e.g. a path rule OR a scope exception), the | |
| * composition must evaluate per-layer: FOR-ALL l (A(l) OR B(l)), not | |
| * (FOR-ALL l A(l)) OR (FOR-ALL l B(l)), to prevent bypass when | |
| * different layers grant via different mechanisms. | |
| */ | |
| for (size_t i = 0; i < rule->num_layers; i++) { | |
| const struct landlock_layer *const layer = &rule->layers[i]; | |
| /* Clear the bits where the layer in the rule grants access. */ | |
| masks->layers[layer->level - 1].access &= ~layer->access; | |
| #ifdef CONFIG_SECURITY_LANDLOCK_LOG | |
| /* Collect rule flags for each layer. */ | |
| if (layer->flags.quiet) | |
| masks->layers[layer->level - 1].quiet = true; | |
| #endif /* CONFIG_SECURITY_LANDLOCK_LOG */ | |
| } | |
| for (size_t i = 0; i < ARRAY_SIZE(masks->layers); i++) { | |
| if (masks->layers[i].access) | |
| return false; | |
| } | |
| return true; | |
| } | |
| typedef access_mask_t | |
| get_access_mask_t(const struct landlock_domain *const domain, | |
| const u16 layer_level); | |
| /** | |
| * landlock_init_layer_masks - Initialize layer masks from an access request | |
| * | |
| * Populates @masks such that for each access right in @access_request, the bits | |
| * for all the layers are set where this access right is handled. Rule flags | |
| * are also zeroed. | |
| * | |
| * @domain: The domain that defines the current restrictions. | |
| * @access_request: The requested access rights to check. | |
| * @masks: Layer access masks to populate. | |
| * @key_type: The key type to switch between access masks of different types. | |
| * | |
| * Return: An access mask where each access right bit is set which is handled in | |
| * any of the active layers in @domain. | |
| */ | |
| access_mask_t | |
| landlock_init_layer_masks(const struct landlock_domain *const domain, | |
| const access_mask_t access_request, | |
| struct layer_masks *const masks, | |
| const enum landlock_key_type key_type) | |
| { | |
| access_mask_t handled_accesses = 0; | |
| get_access_mask_t *get_access_mask; | |
| switch (key_type) { | |
| case LANDLOCK_KEY_INODE: | |
| get_access_mask = landlock_get_fs_access_mask; | |
| break; | |
| #if IS_ENABLED(CONFIG_INET) | |
| case LANDLOCK_KEY_NET_PORT: | |
| get_access_mask = landlock_get_net_access_mask; | |
| break; | |
| #endif /* IS_ENABLED(CONFIG_INET) */ | |
| default: | |
| WARN_ON_ONCE(1); | |
| return 0; | |
| } | |
| /* An empty access request can happen because of O_WRONLY | O_RDWR. */ | |
| if (!access_request) | |
| return 0; | |
| for (size_t i = 0; i < domain->num_layers; i++) { | |
| const access_mask_t handled = get_access_mask(domain, i); | |
| masks->layers[i].access = access_request & handled; | |
| handled_accesses |= masks->layers[i].access; | |
| #ifdef CONFIG_SECURITY_LANDLOCK_LOG | |
| masks->layers[i].quiet = false; | |
| #endif /* CONFIG_SECURITY_LANDLOCK_LOG */ | |
| } | |
| for (size_t i = domain->num_layers; i < ARRAY_SIZE(masks->layers); | |
| i++) { | |
| masks->layers[i].access = 0; | |
| #ifdef CONFIG_SECURITY_LANDLOCK_LOG | |
| masks->layers[i].quiet = false; | |
| #endif /* CONFIG_SECURITY_LANDLOCK_LOG */ | |
| } | |
| return handled_accesses; | |
| } | |
| static int merge_tree(struct landlock_domain *const dst, | |
| struct landlock_ruleset *const src, | |
| const enum landlock_key_type key_type) | |
| { | |
| struct landlock_rule *walker_rule, *next_rule; | |
| struct rb_root *src_root; | |
| int err = 0; | |
| might_sleep(); | |
| lockdep_assert_held(&src->lock); | |
| src_root = landlock_get_rule_root(&src->rules, key_type); | |
| if (IS_ERR(src_root)) | |
| return PTR_ERR(src_root); | |
| /* Merges the @src tree. */ | |
| rbtree_postorder_for_each_entry_safe(walker_rule, next_rule, src_root, | |
| node) { | |
| struct landlock_layer layers[] = { { | |
| .level = dst->num_layers, | |
| } }; | |
| const struct landlock_id id = { | |
| .key = walker_rule->key, | |
| .type = key_type, | |
| }; | |
| if (WARN_ON_ONCE(walker_rule->num_layers != 1)) | |
| return -EINVAL; | |
| if (WARN_ON_ONCE(walker_rule->layers[0].level != 0)) | |
| return -EINVAL; | |
| layers[0].access = walker_rule->layers[0].access; | |
| layers[0].flags = walker_rule->layers[0].flags; | |
| err = landlock_store_rule(&dst->rules, id, &layers, | |
| ARRAY_SIZE(layers)); | |
| if (err) | |
| return err; | |
| } | |
| return err; | |
| } | |
| static int merge_ruleset(struct landlock_domain *const dst, | |
| struct landlock_ruleset *const src) | |
| { | |
| int err = 0; | |
| might_sleep(); | |
| /* Should already be checked by landlock_merge_ruleset() */ | |
| if (WARN_ON_ONCE(!src)) | |
| return 0; | |
| /* Only merge into a domain. */ | |
| if (WARN_ON_ONCE(!dst || !dst->hierarchy)) | |
| return -EINVAL; | |
| lockdep_assert_held(&src->lock); | |
| /* Stacks the new layer. */ | |
| if (WARN_ON_ONCE(dst->num_layers < 1)) | |
| return -EINVAL; | |
| dst->handled_masks[dst->num_layers - 1] = | |
| landlock_upgrade_handled_access_masks(src->handled_masks); | |
| /* Merges the @src inode tree. */ | |
| err = merge_tree(dst, src, LANDLOCK_KEY_INODE); | |
| if (err) | |
| return err; | |
| #if IS_ENABLED(CONFIG_INET) | |
| /* Merges the @src network port tree. */ | |
| err = merge_tree(dst, src, LANDLOCK_KEY_NET_PORT); | |
| if (err) | |
| return err; | |
| #endif /* IS_ENABLED(CONFIG_INET) */ | |
| return 0; | |
| } | |
| static int inherit_tree(struct landlock_domain *const parent, | |
| struct landlock_domain *const child, | |
| const enum landlock_key_type key_type) | |
| { | |
| struct landlock_rule *walker_rule, *next_rule; | |
| struct rb_root *parent_root; | |
| int err = 0; | |
| might_sleep(); | |
| parent_root = landlock_get_rule_root(&parent->rules, key_type); | |
| if (IS_ERR(parent_root)) | |
| return PTR_ERR(parent_root); | |
| /* Copies the @parent inode or network tree. */ | |
| rbtree_postorder_for_each_entry_safe(walker_rule, next_rule, | |
| parent_root, node) { | |
| const struct landlock_id id = { | |
| .key = walker_rule->key, | |
| .type = key_type, | |
| }; | |
| err = landlock_store_rule(&child->rules, id, | |
| &walker_rule->layers, | |
| walker_rule->num_layers); | |
| if (err) | |
| return err; | |
| } | |
| return err; | |
| } | |
| static int inherit_ruleset(struct landlock_domain *const parent, | |
| struct landlock_domain *const child) | |
| { | |
| int err = 0; | |
| might_sleep(); | |
| if (!parent) | |
| return 0; | |
| /* Copies the @parent inode tree. */ | |
| err = inherit_tree(parent, child, LANDLOCK_KEY_INODE); | |
| if (err) | |
| return err; | |
| #if IS_ENABLED(CONFIG_INET) | |
| /* Copies the @parent network port tree. */ | |
| err = inherit_tree(parent, child, LANDLOCK_KEY_NET_PORT); | |
| if (err) | |
| return err; | |
| #endif /* IS_ENABLED(CONFIG_INET) */ | |
| if (WARN_ON_ONCE(child->num_layers <= parent->num_layers)) | |
| return -EINVAL; | |
| /* | |
| * Copies the parent layer stack and leaves a space for the new layer. | |
| */ | |
| memcpy(child->handled_masks, parent->handled_masks, | |
| flex_array_size(parent, handled_masks, parent->num_layers)); | |
| if (WARN_ON_ONCE(!parent->hierarchy)) | |
| return -EINVAL; | |
| landlock_get_hierarchy(parent->hierarchy); | |
| child->hierarchy->parent = parent->hierarchy; | |
| return 0; | |
| } | |
| /** | |
| * landlock_merge_ruleset - Merge a ruleset with a domain | |
| * | |
| * @parent: Parent domain. | |
| * @ruleset: New ruleset to be merged. | |
| * | |
| * The current task is requesting to be restricted. The subjective credentials | |
| * must not be in an overridden state. cf. landlock_init_hierarchy_log(). | |
| * | |
| * The caller must hold @ruleset->lock. | |
| * | |
| * Return: A new domain merging @parent and @ruleset on success, or ERR_PTR() on | |
| * failure. If @parent is NULL, the new domain duplicates @ruleset. | |
| */ | |
| struct landlock_domain * | |
| landlock_merge_ruleset(struct landlock_domain *const parent, | |
| struct landlock_ruleset *const ruleset) | |
| { | |
| struct landlock_domain *new_dom __free(landlock_put_domain) = NULL; | |
| u32 num_layers; | |
| int err; | |
| might_sleep(); | |
| if (WARN_ON_ONCE(!ruleset)) | |
| return ERR_PTR(-EINVAL); | |
| lockdep_assert_held(&ruleset->lock); | |
| if (parent) { | |
| if (parent->num_layers >= LANDLOCK_MAX_NUM_LAYERS) | |
| return ERR_PTR(-E2BIG); | |
| num_layers = parent->num_layers + 1; | |
| } else { | |
| num_layers = 1; | |
| } | |
| /* Creates a new domain... */ | |
| new_dom = create_domain(num_layers); | |
| if (IS_ERR(new_dom)) | |
| return new_dom; | |
| new_dom->hierarchy = | |
| kzalloc_obj(*new_dom->hierarchy, GFP_KERNEL_ACCOUNT); | |
| if (!new_dom->hierarchy) | |
| return ERR_PTR(-ENOMEM); | |
| refcount_set(&new_dom->hierarchy->usage, 1); | |
| /* ...as a child of @parent... */ | |
| err = inherit_ruleset(parent, new_dom); | |
| if (err) | |
| return ERR_PTR(err); | |
| /* ...and including @ruleset. */ | |
| err = merge_ruleset(new_dom, ruleset); | |
| if (err) | |
| return ERR_PTR(err); | |
| err = landlock_init_hierarchy_log(new_dom->hierarchy); | |
| if (err) | |
| return ERR_PTR(err); | |
| #ifdef CONFIG_SECURITY_LANDLOCK_LOG | |
| new_dom->hierarchy->quiet_masks = ruleset->quiet_masks; | |
| #endif /* CONFIG_SECURITY_LANDLOCK_LOG */ | |
| return no_free_ptr(new_dom); | |
| } | |
| #ifdef CONFIG_SECURITY_LANDLOCK_LOG | |
| /** | |
| * get_current_exe - Get the current's executable path, if any | |
| * | |
| * @exe_str: Returned pointer to a path string with a lifetime tied to the | |
| * returned buffer, if any. | |
| * @exe_size: Returned size of @exe_str (including the trailing null | |
| * character), if any. | |
| * | |
| * Return: A pointer to an allocated buffer where @exe_str point to, %NULL if | |
| * there is no executable path, or an error otherwise. | |
| */ | |
| static const void *get_current_exe(const char **const exe_str, | |
| size_t *const exe_size) | |
| { | |
| const size_t buffer_size = LANDLOCK_PATH_MAX_SIZE; | |
| struct mm_struct *mm = current->mm; | |
| struct file *file __free(fput) = NULL; | |
| char *buffer __free(kfree) = NULL; | |
| const char *exe; | |
| ssize_t size; | |
| if (!mm) | |
| return NULL; | |
| file = get_mm_exe_file(mm); | |
| if (!file) | |
| return NULL; | |
| buffer = kmalloc(buffer_size, GFP_KERNEL); | |
| if (!buffer) | |
| return ERR_PTR(-ENOMEM); | |
| exe = d_path(&file->f_path, buffer, buffer_size); | |
| if (WARN_ON_ONCE(IS_ERR(exe))) | |
| /* Should never happen according to LANDLOCK_PATH_MAX_SIZE. */ | |
| return ERR_CAST(exe); | |
| size = buffer + buffer_size - exe; | |
| if (WARN_ON_ONCE(size <= 0)) | |
| return ERR_PTR(-ENAMETOOLONG); | |
| *exe_size = size; | |
| *exe_str = exe; | |
| return no_free_ptr(buffer); | |
| } | |
| /* | |
| * Return: A newly allocated object describing a domain, or an error | |
| * otherwise. | |
| */ | |
| static struct landlock_details *get_current_details(void) | |
| { | |
| /* Cf. audit_log_d_path_exe() */ | |
| static const char null_path[] = "(null)"; | |
| const char *path_str = null_path; | |
| size_t path_size = sizeof(null_path); | |
| const void *buffer __free(kfree) = NULL; | |
| struct landlock_details *details; | |
| buffer = get_current_exe(&path_str, &path_size); | |
| if (IS_ERR(buffer)) | |
| return ERR_CAST(buffer); | |
| /* | |
| * Create the new details according to the path's length. Account to | |
| * the calling task's memcg, like the other Landlock per-domain | |
| * allocations, even if it may not control the related size. | |
| */ | |
| details = | |
| kzalloc_flex(*details, exe_path, path_size, GFP_KERNEL_ACCOUNT); | |
| if (!details) | |
| return ERR_PTR(-ENOMEM); | |
| memcpy(details->exe_path, path_str, path_size); | |
| details->pid = get_pid(task_tgid(current)); | |
| details->uid = from_kuid(&init_user_ns, current_uid()); | |
| get_task_comm(details->comm, current); | |
| return details; | |
| } | |
| /** | |
| * landlock_init_hierarchy_log - Partially initialize landlock_hierarchy | |
| * | |
| * @hierarchy: The hierarchy to initialize. | |
| * | |
| * The current task is referenced as the domain that is enforcing the | |
| * restriction. The subjective credentials must not be in an overridden state. | |
| * | |
| * @hierarchy->parent and @hierarchy->usage should already be set. | |
| * | |
| * Return: 0 on success, -errno on failure. | |
| */ | |
| int landlock_init_hierarchy_log(struct landlock_hierarchy *const hierarchy) | |
| { | |
| struct landlock_details *details; | |
| details = get_current_details(); | |
| if (IS_ERR(details)) | |
| return PTR_ERR(details); | |
| hierarchy->details = details; | |
| hierarchy->id = landlock_get_id_range(1); | |
| /* | |
| * The hierarchy is born unobservable: landlock_restrict_self() moves it | |
| * out of LANDLOCK_LOG_UNCOMMITTED once it has emitted the creation | |
| * event, so the matching free_domain event fires for it and not for a | |
| * hierarchy whose creation was never observed. | |
| */ | |
| hierarchy->log_status = LANDLOCK_LOG_UNCOMMITTED; | |
| hierarchy->log_same_exec = true; | |
| hierarchy->log_new_exec = false; | |
| atomic64_set(&hierarchy->num_denials, 0); | |
| return 0; | |
| } | |
| static deny_masks_t | |
| get_layer_deny_mask(const access_mask_t all_existing_optional_access, | |
| const unsigned long access_bit, const size_t layer) | |
| { | |
| unsigned long access_weight; | |
| /* This may require change with new object types. */ | |
| WARN_ON_ONCE(all_existing_optional_access != | |
| _LANDLOCK_ACCESS_FS_OPTIONAL); | |
| if (WARN_ON_ONCE(layer >= LANDLOCK_MAX_NUM_LAYERS)) | |
| return 0; | |
| access_weight = hweight_long(all_existing_optional_access & | |
| GENMASK(access_bit, 0)); | |
| if (WARN_ON_ONCE(access_weight < 1)) | |
| return 0; | |
| return layer | |
| << ((access_weight - 1) * HWEIGHT(LANDLOCK_MAX_NUM_LAYERS - 1)); | |
| } | |
| /** | |
| * landlock_get_quiet_optional_accesses - Get optional accesses which are | |
| * covered by quiet rule flags. | |
| * | |
| * @all_existing_optional_access: Bitmask of valid optional accesses. | |
| * @deny_masks: Domain layer levels that denied each optional access (the | |
| * deny_masks field on struct landlock_file_security). | |
| * @masks: The struct layer_masks collected during the path walk. | |
| * | |
| * Return: a bitmask of which optional accesses are denied by layers for which | |
| * the quiet flag was collected during the path walk. | |
| */ | |
| optional_access_t landlock_get_quiet_optional_accesses( | |
| const access_mask_t all_existing_optional_access, | |
| const deny_masks_t deny_masks, const struct layer_masks *const masks) | |
| { | |
| const unsigned long access_opt = all_existing_optional_access; | |
| size_t access_index = 0; | |
| unsigned long access_bit; | |
| optional_access_t quiet_optional_accesses = 0; | |
| /* This will require change with new object types. */ | |
| WARN_ON_ONCE(access_opt != _LANDLOCK_ACCESS_FS_OPTIONAL); | |
| for_each_set_bit(access_bit, &access_opt, | |
| BITS_PER_TYPE(access_mask_t)) { | |
| const u8 layer = | |
| (deny_masks >> (access_index * | |
| HWEIGHT(LANDLOCK_MAX_NUM_LAYERS - 1))) & | |
| (LANDLOCK_MAX_NUM_LAYERS - 1); | |
| if (masks->layers[layer].quiet) | |
| quiet_optional_accesses |= BIT(access_index); | |
| access_index++; | |
| } | |
| return quiet_optional_accesses; | |
| } | |
| #ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST | |
| static void test_get_layer_deny_mask(struct kunit *const test) | |
| { | |
| const unsigned long truncate = BIT_INDEX(LANDLOCK_ACCESS_FS_TRUNCATE); | |
| const unsigned long ioctl_dev = BIT_INDEX(LANDLOCK_ACCESS_FS_IOCTL_DEV); | |
| KUNIT_EXPECT_EQ(test, 0, | |
| get_layer_deny_mask(_LANDLOCK_ACCESS_FS_OPTIONAL, | |
| truncate, 0)); | |
| KUNIT_EXPECT_EQ(test, 0x3, | |
| get_layer_deny_mask(_LANDLOCK_ACCESS_FS_OPTIONAL, | |
| truncate, 3)); | |
| KUNIT_EXPECT_EQ(test, 0, | |
| get_layer_deny_mask(_LANDLOCK_ACCESS_FS_OPTIONAL, | |
| ioctl_dev, 0)); | |
| KUNIT_EXPECT_EQ(test, 0xf0, | |
| get_layer_deny_mask(_LANDLOCK_ACCESS_FS_OPTIONAL, | |
| ioctl_dev, 15)); | |
| } | |
| #endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */ | |
| deny_masks_t | |
| landlock_get_deny_masks(const access_mask_t all_existing_optional_access, | |
| const access_mask_t optional_access, | |
| const struct layer_masks *const masks) | |
| { | |
| const unsigned long access_opt = optional_access; | |
| unsigned long access_bit; | |
| deny_masks_t deny_masks = 0; | |
| access_mask_t all_denied = 0; | |
| /* This may require change with new object types. */ | |
| WARN_ON_ONCE(!access_mask_subset(optional_access, | |
| all_existing_optional_access)); | |
| if (WARN_ON_ONCE(!masks)) | |
| return 0; | |
| if (WARN_ON_ONCE(!access_opt)) | |
| return 0; | |
| for (ssize_t i = ARRAY_SIZE(masks->layers) - 1; i >= 0; i--) { | |
| const access_mask_t denied = masks->layers[i].access & | |
| optional_access; | |
| const unsigned long newly_denied = denied & ~all_denied; | |
| if (!newly_denied) | |
| continue; | |
| for_each_set_bit(access_bit, &newly_denied, | |
| 8 * sizeof(access_mask_t)) { | |
| deny_masks |= get_layer_deny_mask( | |
| all_existing_optional_access, access_bit, i); | |
| } | |
| all_denied |= denied; | |
| } | |
| return deny_masks; | |
| } | |
| #ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST | |
| static void test_landlock_get_deny_masks(struct kunit *const test) | |
| { | |
| const struct layer_masks layers1 = { | |
| .layers[0].access = LANDLOCK_ACCESS_FS_EXECUTE | | |
| LANDLOCK_ACCESS_FS_IOCTL_DEV, | |
| .layers[1].access = LANDLOCK_ACCESS_FS_TRUNCATE, | |
| .layers[2].access = LANDLOCK_ACCESS_FS_IOCTL_DEV, | |
| .layers[9].access = LANDLOCK_ACCESS_FS_EXECUTE, | |
| }; | |
| KUNIT_EXPECT_EQ(test, 0x1, | |
| landlock_get_deny_masks(_LANDLOCK_ACCESS_FS_OPTIONAL, | |
| LANDLOCK_ACCESS_FS_TRUNCATE, | |
| &layers1)); | |
| KUNIT_EXPECT_EQ(test, 0x20, | |
| landlock_get_deny_masks(_LANDLOCK_ACCESS_FS_OPTIONAL, | |
| LANDLOCK_ACCESS_FS_IOCTL_DEV, | |
| &layers1)); | |
| KUNIT_EXPECT_EQ( | |
| test, 0x21, | |
| landlock_get_deny_masks(_LANDLOCK_ACCESS_FS_OPTIONAL, | |
| LANDLOCK_ACCESS_FS_TRUNCATE | | |
| LANDLOCK_ACCESS_FS_IOCTL_DEV, | |
| &layers1)); | |
| } | |
| #endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */ | |
| #ifdef CONFIG_SECURITY_LANDLOCK_KUNIT_TEST | |
| static struct kunit_case test_cases[] = { | |
| /* clang-format off */ | |
| KUNIT_CASE(test_get_layer_deny_mask), | |
| KUNIT_CASE(test_landlock_get_deny_masks), | |
| {} | |
| /* clang-format on */ | |
| }; | |
| static struct kunit_suite test_suite = { | |
| .name = "landlock_domain", | |
| .test_cases = test_cases, | |
| }; | |
| kunit_test_suite(test_suite); | |
| #endif /* CONFIG_SECURITY_LANDLOCK_KUNIT_TEST */ | |
| #endif /* CONFIG_SECURITY_LANDLOCK_LOG */ |