[RFC PATCH v4 03/15] landlock: landlock_find/insert_rule refactoring

Konstantin Meskhidze konstantin.meskhidze at huawei.com
Tue Mar 22 12:33:17 UTC 2022



3/18/2022 9:33 PM, Mickaël Salaün пишет:
> 
> On 17/03/2022 15:29, Konstantin Meskhidze wrote:
>>
>>
>> 3/16/2022 11:27 AM, Mickaël Salaün пишет:
>>>
>>> On 09/03/2022 14:44, Konstantin Meskhidze wrote:
>>>> A new object union added to support a socket port
>>>> rule type. To support it landlock_insert_rule() and
>>>> landlock_find_rule() were refactored. Now adding
>>>> or searching a rule in a ruleset depends on a
>>>> rule_type argument provided in refactored
>>>> functions mentioned above.
>>>>
>>>> Signed-off-by: Konstantin Meskhidze <konstantin.meskhidze at huawei.com>
>>>> ---
>>>>
>>>> Changes since v3:
>>>> * Split commit.
>>>> * Refactoring landlock_insert_rule and landlock_find_rule functions.
>>>> * Rename new_ruleset->root_inode.
>>>>
>>>> ---
>>>>   security/landlock/fs.c      |   5 +-
>>>>   security/landlock/ruleset.c | 108 
>>>> +++++++++++++++++++++++++-----------
>>>>   security/landlock/ruleset.h |  26 +++++----
>>>>   3 files changed, 94 insertions(+), 45 deletions(-)
>>>>
>>>> diff --git a/security/landlock/fs.c b/security/landlock/fs.c
>>>> index 97f5c455f5a7..1497948d754f 100644
>>>> --- a/security/landlock/fs.c
>>>> +++ b/security/landlock/fs.c
>>>> @@ -168,7 +168,7 @@ int landlock_append_fs_rule(struct 
>>>> landlock_ruleset *const ruleset,
>>>>       if (IS_ERR(object))
>>>>           return PTR_ERR(object);
>>>>       mutex_lock(&ruleset->lock);
>>>> -    err = landlock_insert_rule(ruleset, object, access_rights);
>>>> +    err = landlock_insert_rule(ruleset, object, 0, access_rights, 
>>>> LANDLOCK_RULE_PATH_BENEATH);
>>>
>>> For consistency, please use 80 columns everywhere.
>>
>>    Ok. I got it.
>>>
>>>>       mutex_unlock(&ruleset->lock);
>>>>       /*
>>>>        * No need to check for an error because landlock_insert_rule()
>>>> @@ -195,7 +195,8 @@ static inline u64 unmask_layers(
>>>>       inode = d_backing_inode(path->dentry);
>>>>       rcu_read_lock();
>>>>       rule = landlock_find_rule(domain,
>>>> -            rcu_dereference(landlock_inode(inode)->object));
>>>> +            (uintptr_t)rcu_dereference(landlock_inode(inode)->object),
>>>> +            LANDLOCK_RULE_PATH_BENEATH);
>>>>       rcu_read_unlock();
>>>>       if (!rule)
>>>>           return layer_mask;
>>>> diff --git a/security/landlock/ruleset.c b/security/landlock/ruleset.c
>>>> index a6212b752549..971685c48641 100644
>>>> --- a/security/landlock/ruleset.c
>>>> +++ b/security/landlock/ruleset.c
>>>> @@ -34,7 +34,7 @@ static struct landlock_ruleset 
>>>> *create_ruleset(const u32 num_layers)
>>>>           return ERR_PTR(-ENOMEM);
>>>>       refcount_set(&new_ruleset->usage, 1);
>>>>       mutex_init(&new_ruleset->lock);
>>>> -    new_ruleset->root = RB_ROOT;
>>>> +    new_ruleset->root_inode = RB_ROOT;
>>>>       new_ruleset->num_layers = num_layers;
>>>>       /*
>>>>        * hierarchy = NULL
>>>> @@ -81,10 +81,12 @@ static void build_check_rule(void)
>>>>   }
>>>>
>>>>   static struct landlock_rule *create_rule(
>>>> -        struct landlock_object *const object,
>>>> +        struct landlock_object *const object_ptr,
>>>> +        const uintptr_t object_data,
>>>>           const struct landlock_layer (*const layers)[],
>>>>           const u32 num_layers,
>>>> -        const struct landlock_layer *const new_layer)
>>>> +        const struct landlock_layer *const new_layer,
>>>> +        const u16 rule_type)
>>>>   {
>>>>       struct landlock_rule *new_rule;
>>>>       u32 new_num_layers;
>>>> @@ -103,8 +105,16 @@ static struct landlock_rule *create_rule(
>>>>       if (!new_rule)
>>>>           return ERR_PTR(-ENOMEM);
>>>>       RB_CLEAR_NODE(&new_rule->node);
>>>> -    landlock_get_object(object);
>>>> -    new_rule->object = object;
>>>> +
>>>> +    switch (rule_type) {
>>>> +    case LANDLOCK_RULE_PATH_BENEATH:
>>>> +        landlock_get_object(object_ptr);
>>>> +        new_rule->object.ptr = object_ptr;
>>>> +        break;
>>>> +    default:
>>>> +        return ERR_PTR(-EINVAL);
>>>
>>> This would lead to memory leak. You should at least add a 
>>> WARN_ON_ONCE(1) here, but a proper solution would be to remove the 
>>> use of rule_type and only rely on object_ptr and object_data values. 
>>> You can also add a WARN_ON_ONCE(object_ptr && object_data).
>>>
>>>
>>    But rule_type is needed here in coming commits to support network
>>    rules. For LANDLOCK_RULE_PATH_BENEATH rule type 
>> landlock_get_object() is used but for LANDLOCK_RULE_NET_SERVICE is 
>> not. Using rule type is convenient for distinguising between fs and 
>> network rules.
> 
> rule_type is not required to infer if the rule use a pointer or raw 
> data, even with the following commits, because you can rely on 
> object_ptr being NULL or not. This would make create_rule() generic for 
> pointer-based and data-based object, even if not-yet-existing rule 
> types. It is less error-prone to only be able to infer something from 
> one source (i.e. object_ptr and not rule_type).
> 
  Ok. I got you. Will be refactored.
> 
>>>> +    }
>>>> +
>>>>       new_rule->num_layers = new_num_layers;
>>>>       /* Copies the original layer stack. */
>>>>       memcpy(new_rule->layers, layers,
>>>> @@ -120,7 +130,7 @@ static void free_rule(struct landlock_rule 
>>>> *const rule)
>>>>       might_sleep();
>>>>       if (!rule)
>>>>           return;
>>>> -    landlock_put_object(rule->object);
>>>> +    landlock_put_object(rule->object.ptr);
>>>>       kfree(rule);
>>>>   }
>>>>
>>>> @@ -156,26 +166,38 @@ static void build_check_ruleset(void)
>>>>    * access rights.
>>>>    */
>>>>   static int insert_rule(struct landlock_ruleset *const ruleset,
>>>> -        struct landlock_object *const object,
>>>> +        struct landlock_object *const object_ptr,
>>>> +        const uintptr_t object_data,
> 
> Can you move rule_type here for this function and similar ones? It makes 
> sense to group object-related arguments.

  Just to group them together, not putting rule_type in the end?
> 
> 
>>>>           const struct landlock_layer (*const layers)[],
>>>> -        size_t num_layers)
>>>> +        size_t num_layers, u16 rule_type)
>>>>   {
>>>>       struct rb_node **walker_node;
>>>>       struct rb_node *parent_node = NULL;
>>>>       struct landlock_rule *new_rule;
>>>> +    uintptr_t object;
>>>> +    struct rb_root *root;
>>>>
>>>>       might_sleep();
>>>>       lockdep_assert_held(&ruleset->lock);
>>>> -    if (WARN_ON_ONCE(!object || !layers))
>>>> -        return -ENOENT;
>>>
>>> You can leave this code here.
>>
>>   But anyway in coming commits with network rules this code will be 
>> moved into case LANDLOCK_RULE_PATH_BENEATH: ....
> 
> Yes, but without rule_type you don't need to duplicate this check, just 
> to remove object_ptr from WARN_ON_ONCE() and replace the rule_type 
> switch/case with if (object_ptr).
> 
> You can change to this:
> 
> --- a/security/landlock/ruleset.c
> +++ b/security/landlock/ruleset.c
> @@ -194,43 +194,49 @@ static void build_check_ruleset(void)
>    */
>   static int insert_rule(struct landlock_ruleset *const ruleset,
>           struct landlock_object *const object_ptr,
> -        const uintptr_t object_data,
> +        uintptr_t object_data, /* move @rule_type here */
>           const struct landlock_layer (*const layers)[],
> -        size_t num_layers, u16 rule_type)
> +        size_t num_layers, const enum landlock_rule_type rule_type)
>   {
>       struct rb_node **walker_node;
>       struct rb_node *parent_node = NULL;
>       struct landlock_rule *new_rule;
> -    uintptr_t object;
>       struct rb_root *root;
> 
>       might_sleep();
>       lockdep_assert_held(&ruleset->lock);
> -    /* Choose rb_tree structure depending on a rule type */
> +
> +    if (WARN_ON_ONCE(!layers))
> +        return -ENOENT;
> +    if (WARN_ON_ONCE(object_ptr && object_data))
> +        return -EINVAL;
> +
> +    /* Chooses the rb_tree according to the rule type. */
>       switch (rule_type) {
>       case LANDLOCK_RULE_PATH_BENEATH:
> -        if (WARN_ON_ONCE(!object_ptr || !layers))
> +        if (WARN_ON_ONCE(!object_ptr))
>               return -ENOENT;
> -        object = (uintptr_t)object_ptr;
> +        object_data = (uintptr_t)object_ptr;
>           root = &ruleset->root_inode;
>           break;
>       case LANDLOCK_RULE_NET_SERVICE:
> -        if (WARN_ON_ONCE(!object_data || !layers))
> -            return -ENOENT;
> -        object = object_data;
> +        if (WARN_ON_ONCE(object_ptr))
> +            return -EINVAL;
>           root = &ruleset->root_net_port;
>           break;
>       default:
> +        WARN_ON_ONCE(1);
>           return -EINVAL;
>       }
> +
>       walker_node = &root->rb_node;
>       while (*walker_node) {
>           struct landlock_rule *const this = rb_entry(*walker_node,
>                   struct landlock_rule, node);
> 
> -        if (this->object.data != object) {
> +        if (this->object.data != object_data) {
>               parent_node = *walker_node;
> -            if (this->object.data < object)
> +            if (this->object.data < object_data)
>                   walker_node = &((*walker_node)->rb_right);
>               else
>                   walker_node = &((*walker_node)->rb_left);
> 
> 
> This highlight an implicit error handling for a port value of 0. I'm not 
> sure if this should be allowed or not though. If not, it should be an 
> explicit service_port check in add_rule_net_service(). A data value of 
> zero might be legitimate for this use case or not-yet-existing 
> data-based rule types. Anyway, this kind of check is specific to the use 
> case and should not be part of insert_rule().
> 
  Ok. I got it.
> 
> 
>>>
>>>> -    walker_node = &(ruleset->root.rb_node);
>>>> +    /* Choose rb_tree structure depending on a rule type */
>>>> +    switch (rule_type) {
>>>> +    case LANDLOCK_RULE_PATH_BENEATH:
>>>> +        if (WARN_ON_ONCE(!object_ptr || !layers))
>>>> +            return -ENOENT;
>>>> +        object = (uintptr_t)object_ptr;
>>>> +        root = &ruleset->root_inode;
>>>> +        break;
>>>> +    default:
>>>> +        return -EINVAL;
>>>> +    }
>>>> +    walker_node = &root->rb_node;
>>>>       while (*walker_node) {
>>>>           struct landlock_rule *const this = rb_entry(*walker_node,
>>>>                   struct landlock_rule, node);
>>>>
>>>> -        if (this->object != object) {
>>>> +        if (this->object.data != object) {
>>>>               parent_node = *walker_node;
>>>> -            if (this->object < object)
>>>> +            if (this->object.data < object)
>>>>                   walker_node = &((*walker_node)->rb_right);
>>>>               else
>>>>                   walker_node = &((*walker_node)->rb_left);
>>>> @@ -207,11 +229,15 @@ static int insert_rule(struct landlock_ruleset 
>>>> *const ruleset,
>>>>            * Intersects access rights when it is a merge between a
>>>>            * ruleset and a domain.
>>>>            */
>>>> -        new_rule = create_rule(object, &this->layers, 
>>>> this->num_layers,
>>>> -                &(*layers)[0]);
>>>> +        switch (rule_type) {
>>>> +        case LANDLOCK_RULE_PATH_BENEATH:
>>>
>>> Same here and for the following code, you should replace such 
>>> switch/case with an if (object_ptr).
>>>    What about coming commits with network rule_type support?
> 
> This will still works.
> 
   Yep. Ok.
> 
>>>
>>>> +            new_rule = create_rule(object_ptr, 0, &this->layers, 
>>>> this->num_layers,
>>>> +                           &(*layers)[0], rule_type);
>>>> +            break;
>>>> +        }
>>>>           if (IS_ERR(new_rule))
>>>>               return PTR_ERR(new_rule);
>>>> -        rb_replace_node(&this->node, &new_rule->node, &ruleset->root);
>>>> +        rb_replace_node(&this->node, &new_rule->node, 
>>>> &ruleset->root_inode);
>>>
>>> Use the root variable here. Same for the following code and patches.
>>
>>   What about your suggestion to use 2 rb_tress to support different 
>> rule_types:
>>       1. root_inode - for filesystem objects
>>           2. root_net_port - for network port objects
>> ????
> 
> I was talking about the root variable you declared a few line before. 
> The conversion from ruleset->root to ruleset->root_inode is fine.
> 
  Sorry. It was a misunderstanding. Got your point.
> 
> [...]
> 
>>>> @@ -465,20 +501,28 @@ struct landlock_ruleset *landlock_merge_ruleset(
>>>>    */
>>>>   const struct landlock_rule *landlock_find_rule(
>>>>           const struct landlock_ruleset *const ruleset,
>>>> -        const struct landlock_object *const object)
>>>> +        const uintptr_t object_data, const u16 rule_type)
>>>>   {
>>>>       const struct rb_node *node;
>>>>
>>>> -    if (!object)
>>>> +    if (!object_data)
>>>
>>> object_data can be 0. You need to add a test with such value.
>>>
>>> We need to be sure that this change cannot affect the current FS code.
>>
>>   I got it. I will refactor it.
> 
> Well, 0 means a port 0, which might not be correct, but this check 
> should not be performed by landlock_merge_ruleset().
> 
  Do you mean landlock_find_rule()?? Cause this check is not
  performed in landlock_merge_ruleset().

> 
>>>
>>>
>>>>           return NULL;
>>>> -    node = ruleset->root.rb_node;
>>>> +
>>>> +    switch (rule_type) {
>>>> +    case LANDLOCK_RULE_PATH_BENEATH:
>>>> +        node = ruleset->root_inode.rb_node;
>>>> +        break;
>>>> +    default:
>>>> +        return ERR_PTR(-EINVAL);
>>>
>>> This is a bug. There is no check for such value. You need to check 
>>> and update all call sites to catch such errors. Same for all new use 
>>> of ERR_PTR().
>>
>> Sorry, I did not get your point.
>> Do you mean I should check the correctness of rule_type in above 
>> function which calls landlock_find_rule() ??? Why can't I add such 
>> check here?
> 
> landlock_find_rule() only returns NULL or a valid pointer, not an error.

   What about incorrect rule_type?? Return NULL? Or final rule_checl 
must be in upper function?
> 
> [...]
> .



More information about the Linux-security-module-archive mailing list