diff mbox series

[v5,2/2] ima: Handle -ESTALE returned by ima_filter_rule_match()

Message ID 20220921125804.59490-3-guozihua@huawei.com (mailing list archive)
State Handled Elsewhere
Headers show
Series ima: Handle -ESTALE returned by ima_filter_rule_match() | expand

Commit Message

Guozihua (Scott) Sept. 21, 2022, 12:58 p.m. UTC
IMA relies on the blocking LSM policy notifier callback to update the
LSM based IMA policy rules.

When SELinux update its policies, IMA would be notified and starts
updating all its lsm rules one-by-one. During this time, -ESTALE would
be returned by ima_filter_rule_match() if it is called with a LSM rule
that has not yet been updated. In ima_match_rules(), -ESTALE is not
handled, and the LSM rule is considered a match, causing extra files
to be measured by IMA.

Fix it by re-initializing a temporary rule if -ESTALE is returned by
ima_filter_rule_match(). The origin rule in the rule list would be
updated by the LSM policy notifier callback.

Fixes: b16942455193 ("ima: use the lsm policy update notifier")
Signed-off-by: GUO Zihua <guozihua@huawei.com>
---
 security/integrity/ima/ima_policy.c | 41 ++++++++++++++++++++++-------
 1 file changed, 32 insertions(+), 9 deletions(-)

Comments

Mimi Zohar Sept. 22, 2022, 11:09 a.m. UTC | #1
Hi Scott,

On Wed, 2022-09-21 at 20:58 +0800, GUO Zihua wrote:
>                 }
> -               if (!rc)
> -                       return false;
> +
> +               if (rc == -ESTALE && !rule_reinitialized) {

Ok, this limits allocating ima_lsm_copy_rule() to the first -ESTALE,

> +                       lsm_rule = ima_lsm_copy_rule(rule);
> +                       if (lsm_rule) {
> +                               rule_reinitialized = true;
> +                               goto retry;

but "retry" is also limited to the first -ESTALE.

> +                       }
> +               }
> +               if (!rc) {
> +                       result = false;
> +                       goto out;
> +               }
>         }
> -       return true;
> +       result = true;
> +
> +out:
> +       if (rule_reinitialized) {
> +               for (i = 0; i < MAX_LSM_RULES; i++)
> +                       ima_filter_rule_free(lsm_rule->lsm[i].rule);
> +               kfree(lsm_rule);
> +       }
> +       return result;
>  }
Guozihua (Scott) Sept. 23, 2022, 4:01 a.m. UTC | #2
On 2022/9/22 19:09, Mimi Zohar wrote:
> Hi Scott,
> 
> On Wed, 2022-09-21 at 20:58 +0800, GUO Zihua wrote:
>>                  }
>> -               if (!rc)
>> -                       return false;
>> +
>> +               if (rc == -ESTALE && !rule_reinitialized) {
> 
> Ok, this limits allocating ima_lsm_copy_rule() to the first -ESTALE,
> 
>> +                       lsm_rule = ima_lsm_copy_rule(rule);
>> +                       if (lsm_rule) {
>> +                               rule_reinitialized = true;
>> +                               goto retry;
> 
> but "retry" is also limited to the first -ESTALE.

Technically we would only need one retry. This loop is looping on all 
the lsm members of one rule, and ima_lsm_copy_rule would update all the 
lsm members of this rule. The "lsm member" here refers to LSM defined 
properties like obj_user, obj_role etc. These members are of AND 
relation, meaning all lsm members together would form one LSM rule.

As of the scenario you mentioned, I think it should be really rare. 
Spending to much time and code on this might not worth it.
> 
>> +                       }
>> +               }
>> +               if (!rc) {
>> +                       result = false;
>> +                       goto out;
>> +               }
>>          }
>> -       return true;
>> +       result = true;
>> +
>> +out:
>> +       if (rule_reinitialized) {
>> +               for (i = 0; i < MAX_LSM_RULES; i++)
>> +                       ima_filter_rule_free(lsm_rule->lsm[i].rule);
>> +               kfree(lsm_rule);
>> +       }
>> +       return result;
>>   }
>
Mimi Zohar Sept. 23, 2022, 11:19 a.m. UTC | #3
On Fri, 2022-09-23 at 12:01 +0800, Guozihua (Scott) wrote:
> On 2022/9/22 19:09, Mimi Zohar wrote:
> > Hi Scott,
> > 
> > On Wed, 2022-09-21 at 20:58 +0800, GUO Zihua wrote:
> >>                  }
> >> -               if (!rc)
> >> -                       return false;
> >> +
> >> +               if (rc == -ESTALE && !rule_reinitialized) {
> > 
> > Ok, this limits allocating ima_lsm_copy_rule() to the first -ESTALE,
> > 
> >> +                       lsm_rule = ima_lsm_copy_rule(rule);
> >> +                       if (lsm_rule) {
> >> +                               rule_reinitialized = true;
> >> +                               goto retry;
> > 
> > but "retry" is also limited to the first -ESTALE.
> 
> Technically we would only need one retry. This loop is looping on all 
> the lsm members of one rule, and ima_lsm_copy_rule would update all the 
> lsm members of this rule. The "lsm member" here refers to LSM defined 
> properties like obj_user, obj_role etc. These members are of AND 
> relation, meaning all lsm members together would form one LSM rule.
> 
> As of the scenario you mentioned, I think it should be really rare. 
> Spending to much time and code on this might not worth it.
> > 
> >> +                       }
> >> +               }

Either there can be multiple LSM fields and the memory is allocated
once and freed once at the end, as you suggested, or the memory should
be freed here and rule_reinitialized reset, minimizing the code change.

> >> +               if (!rc) {
> >> +                       result = false;
> >> +                       goto out;
> >> +               }
> >>          }
> >> -       return true;
> >> +       result = true;
> >> +
> >> +out:
> >> +       if (rule_reinitialized) {
> >> +               for (i = 0; i < MAX_LSM_RULES; i++)
> >> +                       ima_filter_rule_free(lsm_rule->lsm[i].rule);
> >> +               kfree(lsm_rule);
> >> +       }
> >> +       return result;
> >>   }
Guozihua (Scott) Sept. 24, 2022, 6:05 a.m. UTC | #4
On 2022/9/23 19:19, Mimi Zohar wrote:
> On Fri, 2022-09-23 at 12:01 +0800, Guozihua (Scott) wrote:
>> On 2022/9/22 19:09, Mimi Zohar wrote:
>>> Hi Scott,
>>>
>>> On Wed, 2022-09-21 at 20:58 +0800, GUO Zihua wrote:
>>>>                   }
>>>> -               if (!rc)
>>>> -                       return false;
>>>> +
>>>> +               if (rc == -ESTALE && !rule_reinitialized) {
>>>
>>> Ok, this limits allocating ima_lsm_copy_rule() to the first -ESTALE,
>>>
>>>> +                       lsm_rule = ima_lsm_copy_rule(rule);
>>>> +                       if (lsm_rule) {
>>>> +                               rule_reinitialized = true;
>>>> +                               goto retry;
>>>
>>> but "retry" is also limited to the first -ESTALE.
>>
>> Technically we would only need one retry. This loop is looping on all
>> the lsm members of one rule, and ima_lsm_copy_rule would update all the
>> lsm members of this rule. The "lsm member" here refers to LSM defined
>> properties like obj_user, obj_role etc. These members are of AND
>> relation, meaning all lsm members together would form one LSM rule.
>>
>> As of the scenario you mentioned, I think it should be really rare.
>> Spending to much time and code on this might not worth it.
>>>
>>>> +                       }
>>>> +               }
> 
> Either there can be multiple LSM fields and the memory is allocated
> once and freed once at the end, as you suggested, or the memory should
> be freed here and rule_reinitialized reset, minimizing the code change.

I might have overlooked something, but if I understands the code 
correctly, we would be copying the same rule over and over again till 
the loop ends in that case. ima_lsm_update_rule() would replace the rule 
node on the rule list without updating the rule in place. Although 
synchronize_rcu() should prevent a UAF, the rule in ima_match_rules() 
would not be updated. Meaning SELinux would always return -ESTALE before 
we copy and retry as we keep passing in the outdated rule entry.
> 
>>>> +               if (!rc) {
>>>> +                       result = false;
>>>> +                       goto out;
>>>> +               }
>>>>           }
>>>> -       return true;
>>>> +       result = true;
>>>> +
>>>> +out:
>>>> +       if (rule_reinitialized) {
>>>> +               for (i = 0; i < MAX_LSM_RULES; i++)
>>>> +                       ima_filter_rule_free(lsm_rule->lsm[i].rule);
>>>> +               kfree(lsm_rule);
>>>> +       }
>>>> +       return result;
>>>>    }
>
Mimi Zohar Sept. 28, 2022, 2:11 p.m. UTC | #5
On Sat, 2022-09-24 at 14:05 +0800, Guozihua (Scott) wrote:

> I might have overlooked something, but if I understands the code 
> correctly, we would be copying the same rule over and over again till 
> the loop ends in that case. ima_lsm_update_rule() would replace the rule 
> node on the rule list without updating the rule in place. Although 
> synchronize_rcu() should prevent a UAF, the rule in ima_match_rules() 
> would not be updated. Meaning SELinux would always return -ESTALE before 
> we copy and retry as we keep passing in the outdated rule entry.

After reviewing this patch set again, the code looks fine.  The commit
message is still a bit off, but I've pushed the patch set out to next-
integrity-testing, waiting for some Reviewed-by/Tested-by tags.
Roberto Sassu Oct. 4, 2022, 2:19 p.m. UTC | #6
On Wed, 2022-09-28 at 10:11 -0400, Mimi Zohar wrote:
> On Sat, 2022-09-24 at 14:05 +0800, Guozihua (Scott) wrote:
> 
> > I might have overlooked something, but if I understands the code 
> > correctly, we would be copying the same rule over and over again
> > till 
> > the loop ends in that case. ima_lsm_update_rule() would replace the
> > rule 
> > node on the rule list without updating the rule in place. Although 
> > synchronize_rcu() should prevent a UAF, the rule in
> > ima_match_rules() 
> > would not be updated. Meaning SELinux would always return -ESTALE
> > before 
> > we copy and retry as we keep passing in the outdated rule entry.
> 
> After reviewing this patch set again, the code looks fine.  The
> commit
> message is still a bit off, but I've pushed the patch set out to
> next-
> integrity-testing, waiting for some Reviewed-by/Tested-by tags.

The patches look ok for me. For both:

Reviewed-by: Roberto Sassu <roberto.sassu@huawei.com>

Roberto
Guozihua (Scott) Oct. 18, 2022, 8:43 a.m. UTC | #7
On 2022/9/28 22:11, Mimi Zohar wrote:
> 
> After reviewing this patch set again, the code looks fine.  The commit
> message is still a bit off, but I've pushed the patch set out to next-
> integrity-testing, waiting for some Reviewed-by/Tested-by tags.
> 

Hi Mimi,

How's this patch going? I see Roberto is replying with a Reviewed-by.
Mimi Zohar Oct. 19, 2022, 1:07 a.m. UTC | #8
On Tue, 2022-10-18 at 16:43 +0800, Guozihua (Scott) wrote:
> On 2022/9/28 22:11, Mimi Zohar wrote:
> > 
> > After reviewing this patch set again, the code looks fine.  The commit
> > message is still a bit off, but I've pushed the patch set out to next-
> > integrity-testing, waiting for some Reviewed-by/Tested-by tags.
> > 
> 
> Hi Mimi,
> 
> How's this patch going? I see Roberto is replying with a Reviewed-by.

I'd really like to see a "Tested-by" tag as well.

Are you able to force the scenario?
Guozihua (Scott) Oct. 19, 2022, 7:17 a.m. UTC | #9
On 2022/10/19 9:07, Mimi Zohar wrote:
> On Tue, 2022-10-18 at 16:43 +0800, Guozihua (Scott) wrote:
>> On 2022/9/28 22:11, Mimi Zohar wrote:
>>>
>>> After reviewing this patch set again, the code looks fine.  The commit
>>> message is still a bit off, but I've pushed the patch set out to next-
>>> integrity-testing, waiting for some Reviewed-by/Tested-by tags.
>>>
>>
>> Hi Mimi,
>>
>> How's this patch going? I see Roberto is replying with a Reviewed-by.
> 
> I'd really like to see a "Tested-by" tag as well.
> 
> Are you able to force the scenario?
> 

It's a race condition which could be hard to reproduce easily and in a 
stable manner. I'll give it a try.
Guozihua (Scott) Oct. 28, 2022, 8:36 a.m. UTC | #10
On 2022/10/19 15:17, Guozihua (Scott) wrote:
> On 2022/10/19 9:07, Mimi Zohar wrote:
>> On Tue, 2022-10-18 at 16:43 +0800, Guozihua (Scott) wrote:
>>> On 2022/9/28 22:11, Mimi Zohar wrote:
>>>>
>>>> After reviewing this patch set again, the code looks fine.  The commit
>>>> message is still a bit off, but I've pushed the patch set out to next-
>>>> integrity-testing, waiting for some Reviewed-by/Tested-by tags.
>>>>
>>>
>>> Hi Mimi,
>>>
>>> How's this patch going? I see Roberto is replying with a Reviewed-by.
>>
>> I'd really like to see a "Tested-by" tag as well.
>>
>> Are you able to force the scenario?
>>
> 
> It's a race condition which could be hard to reproduce easily and in a 
> stable manner. I'll give it a try.

Hi Mimi,

I managed to re-produce this issue with the help of the following two 
scripts:

read_tmp_measurement.sh:
> #!/bin/bash
> 
> while true
> do
>         cat /root/tmp.txt > /dev/null
>         measurement=`cat /sys/kernel/security/ima/ascii_runtime_measurements | grep "tmp\.txt" | wc -l`
>         if [ "${measurement}" == "1" ]; then
>                 echo "measurement found"
>                 exit 1
>         fi
> done

test.sh:
> #!/bin/bash
> 
> echo "measure obj_user=system_u obj_role=object_r obj_type=unlabeled_t" > /sys/kernel/security/ima/policy
> 
> cat /root/tmp2.txt
> measurement=`cat /sys/kernel/security/ima/ascii_runtime_measurements | grep "tmp2\.txt" | wc -l`
> [ "$measurement" == "1" ] && echo "measurement for tmp2 found"
> 
> cat /root/tmp.txt
> measurement=`cat /sys/kernel/security/ima/ascii_runtime_measurements | grep "tmp\.txt" | wc -l`
> [ "$measurement" == "1" ] && echo "measurement for tmp found, preparation failed!" && exit 1
> 
> ./read_tmp_measurement.sh &
> pid=$!
> 
> cd /usr/share/selinux/default
> semodule -i clock.pp.bz2
> semodule -r clock
> 
> kill ${pid}

I created two files tmp.txt and tmp2.txt, assign them with type 
user_home_t and unlabeled_t respectively and then run test.sh.
On a multi-core environment, I managed to reproduce this issue pretty 
easily and tested that once the solution is merged, the issue stops 
happening.
Mimi Zohar Nov. 1, 2022, 10:15 p.m. UTC | #11
Hi Scott,

On Fri, 2022-10-28 at 16:36 +0800, Guozihua (Scott) wrote:
> 
> I managed to re-produce this issue with the help of the following two 
> scripts:
> 
> read_tmp_measurement.sh:
> > #!/bin/bash
> > 
> > while true
> > do
> >         cat /root/tmp.txt > /dev/null
> >         measurement=`cat /sys/kernel/security/ima/ascii_runtime_measurements | grep "tmp\.txt" | wc -l`
> >         if [ "${measurement}" == "1" ]; then
> >                 echo "measurement found"
> >                 exit 1
> >         fi
> > done
> 
> test.sh:
> > #!/bin/bash
> > 
> > echo "measure obj_user=system_u obj_role=object_r obj_type=unlabeled_t" > /sys/kernel/security/ima/policy
> > 
> > cat /root/tmp2.txt
> > measurement=`cat /sys/kernel/security/ima/ascii_runtime_measurements | grep "tmp2\.txt" | wc -l`
> > [ "$measurement" == "1" ] && echo "measurement for tmp2 found"
> > 
> > cat /root/tmp.txt
> > measurement=`cat /sys/kernel/security/ima/ascii_runtime_measurements | grep "tmp\.txt" | wc -l`
> > [ "$measurement" == "1" ] && echo "measurement for tmp found, preparation failed!" && exit 1
> > 
> > ./read_tmp_measurement.sh &
> > pid=$!
> > 
> > cd /usr/share/selinux/default
> > semodule -i clock.pp.bz2
> > semodule -r clock
> > 
> > kill ${pid}

Are you loading/unloading any selinux policy or specifically clock? If
specifically clock, what is special about it?

> I created two files tmp.txt and tmp2.txt, assign them with type 
> user_home_t and unlabeled_t respectively and then run test.sh.
> On a multi-core environment, I managed to reproduce this issue pretty 
> easily and tested that once the solution is merged, the issue stops 
> happening.

As I only see an IMA measurement policy rule being loaded for
"unlabeled_t" and not "user_home_t", should I assume that an IMA
measurement rule already exists for "user_home_t"?

thanks,

Mimi
Guozihua (Scott) Nov. 2, 2022, 1:42 a.m. UTC | #12
Hi Mimi,

On 2022/11/2 6:15, Mimi Zohar wrote:
> Hi Scott,
> 
> On Fri, 2022-10-28 at 16:36 +0800, Guozihua (Scott) wrote:
>>
>> I managed to re-produce this issue with the help of the following two
>> scripts:
>>
>> read_tmp_measurement.sh:
>>> #!/bin/bash
>>>
>>> while true
>>> do
>>>          cat /root/tmp.txt > /dev/null
>>>          measurement=`cat /sys/kernel/security/ima/ascii_runtime_measurements | grep "tmp\.txt" | wc -l`
>>>          if [ "${measurement}" == "1" ]; then
>>>                  echo "measurement found"
>>>                  exit 1
>>>          fi
>>> done
>>
>> test.sh:
>>> #!/bin/bash
>>>
>>> echo "measure obj_user=system_u obj_role=object_r obj_type=unlabeled_t" > /sys/kernel/security/ima/policy
>>>
>>> cat /root/tmp2.txt
>>> measurement=`cat /sys/kernel/security/ima/ascii_runtime_measurements | grep "tmp2\.txt" | wc -l`
>>> [ "$measurement" == "1" ] && echo "measurement for tmp2 found"
>>>
>>> cat /root/tmp.txt
>>> measurement=`cat /sys/kernel/security/ima/ascii_runtime_measurements | grep "tmp\.txt" | wc -l`
>>> [ "$measurement" == "1" ] && echo "measurement for tmp found, preparation failed!" && exit 1
>>>
>>> ./read_tmp_measurement.sh &
>>> pid=$!
>>>
>>> cd /usr/share/selinux/default
>>> semodule -i clock.pp.bz2
>>> semodule -r clock
>>>
>>> kill ${pid}
> 
> Are you loading/unloading any selinux policy or specifically clock? If
> specifically clock, what is special about it?

No there are nothing special about clock. Any selinux policy should do,.
> 
>> I created two files tmp.txt and tmp2.txt, assign them with type
>> user_home_t and unlabeled_t respectively and then run test.sh.
>> On a multi-core environment, I managed to reproduce this issue pretty
>> easily and tested that once the solution is merged, the issue stops
>> happening.
> 
> As I only see an IMA measurement policy rule being loaded for
> "unlabeled_t" and not "user_home_t", should I assume that an IMA
> measurement rule already exists for "user_home_t"?

There wasn't a rule for user_home_t. These scripts demonstrate that 
during a selinux policy reload, IMA would measure files that is not in 
the range of it's LSM based rules. Which is the issue I am trying to fix.

In this test, we only have one rule for measuring files of type 
unlabeled_t. However, during selinux policy reload, file of user_home_t 
is also measured.
> 
> thanks,
> 
> Mimi
>
Mimi Zohar Nov. 3, 2022, 1:15 p.m. UTC | #13
On Wed, 2022-11-02 at 09:42 +0800, Guozihua (Scott) wrote:
> > As I only see an IMA measurement policy rule being loaded for
> > "unlabeled_t" and not "user_home_t", should I assume that an IMA
> > measurement rule already exists for "user_home_t"?
> 
> There wasn't a rule for user_home_t. These scripts demonstrate that 
> during a selinux policy reload, IMA would measure files that is not in 
> the range of it's LSM based rules. Which is the issue I am trying to fix.
> 
> In this test, we only have one rule for measuring files of type 
> unlabeled_t. However, during selinux policy reload, file of user_home_t 
> is also measured.

Thanks, Scott.  After tweaking the scripts for my system, I was able to
reproduce the bug.  This patch set is now queued in next-integrity.
Guozihua (Scott) Nov. 14, 2022, 3:31 a.m. UTC | #14
On 2022/11/3 21:15, Mimi Zohar wrote:
> On Wed, 2022-11-02 at 09:42 +0800, Guozihua (Scott) wrote:
>>> As I only see an IMA measurement policy rule being loaded for
>>> "unlabeled_t" and not "user_home_t", should I assume that an IMA
>>> measurement rule already exists for "user_home_t"?
>>
>> There wasn't a rule for user_home_t. These scripts demonstrate that
>> during a selinux policy reload, IMA would measure files that is not in
>> the range of it's LSM based rules. Which is the issue I am trying to fix.
>>
>> In this test, we only have one rule for measuring files of type
>> unlabeled_t. However, during selinux policy reload, file of user_home_t
>> is also measured.
> 
> Thanks, Scott.  After tweaking the scripts for my system, I was able to
> reproduce the bug.  This patch set is now queued in next-integrity.
> 

Hi Mimi,

Any chance these patches would be in 6.1?
diff mbox series

Patch

diff --git a/security/integrity/ima/ima_policy.c b/security/integrity/ima/ima_policy.c
index 8040215c0252..2edff7f58c25 100644
--- a/security/integrity/ima/ima_policy.c
+++ b/security/integrity/ima/ima_policy.c
@@ -545,6 +545,9 @@  static bool ima_match_rules(struct ima_rule_entry *rule,
 			    const char *func_data)
 {
 	int i;
+	bool result = false;
+	struct ima_rule_entry *lsm_rule = rule;
+	bool rule_reinitialized = false;
 
 	if ((rule->flags & IMA_FUNC) &&
 	    (rule->func != func && func != POST_SETATTR))
@@ -606,35 +609,55 @@  static bool ima_match_rules(struct ima_rule_entry *rule,
 		int rc = 0;
 		u32 osid;
 
-		if (!rule->lsm[i].rule) {
-			if (!rule->lsm[i].args_p)
+		if (!lsm_rule->lsm[i].rule) {
+			if (!lsm_rule->lsm[i].args_p)
 				continue;
 			else
 				return false;
 		}
+
+retry:
 		switch (i) {
 		case LSM_OBJ_USER:
 		case LSM_OBJ_ROLE:
 		case LSM_OBJ_TYPE:
 			security_inode_getsecid(inode, &osid);
-			rc = ima_filter_rule_match(osid, rule->lsm[i].type,
+			rc = ima_filter_rule_match(osid, lsm_rule->lsm[i].type,
 						   Audit_equal,
-						   rule->lsm[i].rule);
+						   lsm_rule->lsm[i].rule);
 			break;
 		case LSM_SUBJ_USER:
 		case LSM_SUBJ_ROLE:
 		case LSM_SUBJ_TYPE:
-			rc = ima_filter_rule_match(secid, rule->lsm[i].type,
+			rc = ima_filter_rule_match(secid, lsm_rule->lsm[i].type,
 						   Audit_equal,
-						   rule->lsm[i].rule);
+						   lsm_rule->lsm[i].rule);
 			break;
 		default:
 			break;
 		}
-		if (!rc)
-			return false;
+
+		if (rc == -ESTALE && !rule_reinitialized) {
+			lsm_rule = ima_lsm_copy_rule(rule);
+			if (lsm_rule) {
+				rule_reinitialized = true;
+				goto retry;
+			}
+		}
+		if (!rc) {
+			result = false;
+			goto out;
+		}
 	}
-	return true;
+	result = true;
+
+out:
+	if (rule_reinitialized) {
+		for (i = 0; i < MAX_LSM_RULES; i++)
+			ima_filter_rule_free(lsm_rule->lsm[i].rule);
+		kfree(lsm_rule);
+	}
+	return result;
 }
 
 /*