On 22 September 2026 14:46:55 BST, "Jérémy Jean"
<[email protected]> wrote:
>On 2026-09-22 13:26, Ricardo Robaina wrote:
>> On Mon, Sep 21, 2026 at 5:07 PM Jérémy Jean
>> <[email protected]> wrote:
>>> 
>>> kill_rules() removes mixed AUDIT_DIR and AUDIT_EXE rules when an audit
>>> tree is pruned. It drops entry->rule.exe before removing the rule from
>>> the RCU-visible filter lists.
>>> 
>>> After a rule has been installed with AUDIT_ADD_RULE, which requires
>>> CAP_AUDIT_CONTROL, removing or moving the watched directory can race
>>> with another task that is still evaluating the rule. In that case,
>>> fsnotify can free the executable mark before the reader reaches
>>> audit_mark_compare(), causing a use-after-free.
>>> 
>>> KASAN reports:
>>> 
>>>     BUG: KASAN: slab-use-after-free in audit_mark_compare+0x8d/0xa0
>>> 
>>> Remove the rule from the RCU-visible filter lists first, wait for a
>grace
>>> period, and only then drop the executable mark. audit_del_rule()
>already
>>> uses this ordering.
>>> 
>>> Fixes: 34d99af52ad4 ("audit: implement audit by executable")
>>> Assisted-by: Codex:gpt-5
>>> Signed-off-by: Jérémy Jean <[email protected]>
>>> ---
>>>  kernel/audit_tree.c | 5 +++--
>>>  1 file changed, 3 insertions(+), 2 deletions(-)
>>> 
>>> diff --git a/kernel/audit_tree.c b/kernel/audit_tree.c
>>> index 1ed19b7..864a58b 100644
>>> --- a/kernel/audit_tree.c
>>> +++ b/kernel/audit_tree.c
>>> @@ -553,11 +553,12 @@ static void kill_rules(struct audit_context
>*context, struct audit_tree *tree)
>>>                 if (rule->tree) {
>>>                         /* not a half-baked one */
>>>                         audit_tree_log_remove_rule(context, rule);
>>> -                       if (entry->rule.exe)
>>> -                               audit_remove_mark(entry->rule.exe);
>>>                         rule->tree = NULL;
>>>                         list_del_rcu(&entry->list);
>>>                         list_del(&entry->rule.list);
>>> +                       synchronize_rcu();
>>> +                       if (entry->rule.exe)
>>> +                               audit_remove_mark(entry->rule.exe);
>>>                         call_rcu(&entry->rcu, audit_free_rule_rcu);
>>>                 }
>>>         }
>>> --
>>> 2.47.3
>>> 
>>> 
>> 
>> Hi Jérémy,
>> 
>> Thanks for troubleshooting and fixing this bug.
>> 
>> sashiko's review makes sense, though: synchronize_rcu() inside the
>> loop waits a full grace period per rule, and kill_rules() runs with
>> audit_filter_mutex held, so a tree with many rules stalls other audit
>> config operations. Below is an untested suggestion to use a single
>> grace period instead.
>> 
>> diff --git a/kernel/audit_tree.c b/kernel/audit_tree.c
>> index 1ed19b775912..5a3d7c7ba5ce 100644
>> --- a/kernel/audit_tree.c
>> +++ b/kernel/audit_tree.c
>> @@ -549,18 +549,30 @@ static void kill_rules(struct audit_context
>> *context, struct audit_tree *tree)
>>         list_for_each_entry_safe(rule, next, &tree->rules, rlist) {
>>                 entry = container_of(rule, struct audit_entry, rule);
>> 
>> -               list_del_init(&rule->rlist);
>>                 if (rule->tree) {
>>                         /* not a half-baked one */
>>                         audit_tree_log_remove_rule(context, rule);
>> -                       if (entry->rule.exe)
>> -                               audit_remove_mark(entry->rule.exe);
>>                         rule->tree = NULL;
>>                         list_del_rcu(&entry->list);
>>                         list_del(&entry->rule.list);
>> -                       call_rcu(&entry->rcu, audit_free_rule_rcu);
>> +               } else {
>> +                       list_del_init(&rule->rlist);
>>                 }
>>         }
>> +
>> +       if (list_empty(&tree->rules))
>> +               return;
>> +
>> +       synchronize_rcu();
>> +
>> +       list_for_each_entry_safe(rule, next, &tree->rules, rlist) {
>> +               entry = container_of(rule, struct audit_entry, rule);
>> +
>> +               list_del_init(&rule->rlist);
>> +               if (entry->rule.exe)
>> +                       audit_remove_mark(entry->rule.exe);
>> +               call_rcu(&entry->rcu, audit_free_rule_rcu);
>> +       }
>>  }
>> 
>>  /*
>
>Hello Ricardo,
>
>Many thanks for your nice and constructive answer.
>
>Indeed, sashiko's review makes sense and your suggested v2 is a good fix.
>I have replayed my reproducer to ensure that it indeeds fix the reported
>bug.
>
>However, I wonder whether the wait should still happen in the case where
>all removed rules have rule->exe == NULL. Doesn't call_rcu() already
>free (kernel/auditfilter.c:96)?  If yes, could we skip synchronize_rcu()
>in
>that case? If this question is valid, how about this fix based on yours,
>which
>simply adds a bool to detect whether synchronize_rcu() should be called.
>
>If you think this addition is good, I can send a v2 with that.
>
>diff --git a/kernel/audit_tree.c b/kernel/audit_tree.c
>index 1ed19b775912..e2cd27b17590 100644
>--- a/kernel/audit_tree.c
>+++ b/kernel/audit_tree.c
>@@ -545,22 +545,38 @@ static void kill_rules(struct audit_context *context, 
>struct audit_tree *tree)
> {
>        struct audit_krule *rule, *next;
>        struct audit_entry *entry;
>+       bool need_sync = false;
>
>        list_for_each_entry_safe(rule, next, &tree->rules, rlist) {
>                entry = container_of(rule, struct audit_entry, rule);
>
>-               list_del_init(&rule->rlist);
>                if (rule->tree) {
>                        /* not a half-baked one */
>                        audit_tree_log_remove_rule(context, rule);
>-                       if (entry->rule.exe)
>-                               audit_remove_mark(entry->rule.exe);
>                        rule->tree = NULL;
>                        list_del_rcu(&entry->list);
>                        list_del(&entry->rule.list);
>-                       call_rcu(&entry->rcu, audit_free_rule_rcu);
>+                       if (rule->exe)
>+                               need_sync = true;
>+               } else {
>+                       list_del_init(&rule->rlist);
>                }
>        }
>+
>+       if (list_empty(&tree->rules))
>+               return;
>+
>+       if (need_sync)
>+               synchronize_rcu();
>+
>+       list_for_each_entry_safe(rule, next, &tree->rules, rlist) {
>+               entry = container_of(rule, struct audit_entry, rule);
>+
>+               list_del_init(&rule->rlist);
>+               if (entry->rule.exe)
>+                       audit_remove_mark(entry->rule.exe);
>+               call_rcu(&entry->rcu, audit_free_rule_rcu);
>+       }
> }
>
> /*

LGTM!

Reviewed-by: Bradley Morgan <[email protected]>


What do you reckon Ricardo?


>
>Regards,
>Jérémy

--- Thanks!
"I'm not a very positive person" - Linus torvalds

Reply via email to