[RFC PATCH v4 03/15] landlock: landlock_find/insert_rule refactoring
Mickaël Salaün
mic at digikod.net
Fri Mar 18 18:33:26 UTC 2022
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).
>>> + }
>>> +
>>> 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.
>>> 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().
>>
>>> - 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.
>>
>>> + 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.
[...]
>>> @@ -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().
>>
>>
>>> 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.
[...]
More information about the Linux-security-module-archive
mailing list