On Tue, Sep 22, 2026 at 10:47 AM 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.
It looks like a nice optimization, good catch! synchronize_rcu() is
usually expensive, so calling it only when necessary is an improvement
for sure.
One minor detail, I'd use 'entry->rule.exe' instead of 'rule->exe' in
the need_sync check just for consistency.
>
> 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);
> + }
> }
>
> /*
>
> Regards,
> Jérémy
>
--Ricardo