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);
+       }
 }

 /*

--Ricardo


Reply via email to