Audit walks every exit filter rule for each audited syscall, even when no
rule contains the current syscall number. Policies with many unrelated
rules therefore add linear overhead to otherwise uninteresting syscalls.
Maintain a reference count for each syscall bit present in exit filter
rules and derive an aggregate interest mask. Update the mask through the
centralized rule lifecycle helpers, which cover explicit and automatic
rule removal. Use the mask as a lockless rejection test before entering
the exit filter RCU traversal.
The mask is architecture-independent. Syscall number overlap between
architectures can cause an unnecessary scan but cannot suppress a match.
The aggregate bit must be set before list_add_rcu() publishes a new rule.
Otherwise, a reader could observe the rule after publication while the
aggregate mask still rejects its syscall. Move audit_rule_account()
before the list insertion to provide this ordering. Rule removal already
uses the inverse safe ordering: it unlinks the rule before clearing the
aggregate bit, so a concurrent reader can only perform an unnecessary
scan, not miss a rule.
To measure the effect, install increasing numbers of distinct statx rules
in a disposable VM and benchmark the unrelated getpid syscall after each
set is installed:
for nr_rules in 1 32 128 256; do
auditctl -D
for uid in $(seq 1 $nr_rules); do
auditctl -a always,exit -F arch=b64 -S statx \
-F uid=$uid
done
audit_bench
done
Without this change, the same unpinned VM produced:
1 rule:
median=55 ns/op
32 rules:
median=71 ns/op
128 rules:
median=428 ns/op
256 rules:
median=791 ns/op
With this change, it produced:
1 rule:
median=55 ns/op
32 rules:
median=55 ns/op
128 rules:
median=55 ns/op
256 rules:
median=55 ns/op
Signed-off-by: Stanislav Kinsburskii <[email protected]>
---
kernel/audit.h | 2 ++
kernel/auditfilter.c | 56 +++++++++++++++++++++++++++++++++++++++++++++++++++-
kernel/auditsc.c | 13 ++++++++++++
3 files changed, 70 insertions(+), 1 deletion(-)
diff --git a/kernel/audit.h b/kernel/audit.h
index 3176da464843..afcbdecc917c 100644
--- a/kernel/audit.h
+++ b/kernel/audit.h
@@ -272,6 +272,8 @@ extern void audit_put_tty(struct tty_struct *tty);
/* audit watch/mark/tree functions */
extern unsigned int audit_serial(void);
#ifdef CONFIG_AUDITSYSCALL
+extern u32 audit_exit_filter_mask[AUDIT_BITMASK_SIZE];
+
void audit_rule_account(const struct audit_krule *rule);
void audit_rule_unaccount(const struct audit_krule *rule);
diff --git a/kernel/auditfilter.c b/kernel/auditfilter.c
index 38a56278ae0b..55ab9d05fafd 100644
--- a/kernel/auditfilter.c
+++ b/kernel/auditfilter.c
@@ -196,6 +196,54 @@ int audit_match_class(int class, unsigned int syscall)
}
#ifdef CONFIG_AUDITSYSCALL
+/*
+ * The mask provides a quick rejection test for syscalls which cannot match an
+ * exit filter rule. The counters and mask updates are protected by
+ * audit_filter_mutex; the mask is read locklessly in the syscall exit path.
+ *
+ * The mask is intentionally architecture-independent. Syscall number
+ * overlap between architectures can only cause an unnecessary filter scan.
+ */
+u32 audit_exit_filter_mask[AUDIT_BITMASK_SIZE] __read_mostly;
+static unsigned int audit_exit_filter_count[AUDIT_BITMASK_SIZE * 32];
+
+static void audit_exit_mask_update(const struct audit_krule *rule, bool add)
+{
+ unsigned int bit, index, word;
+ u32 mask, rule_mask;
+
+ lockdep_assert_held(&audit_filter_mutex);
+
+ for (word = 0; word < AUDIT_BITMASK_SIZE; word++) {
+ mask = READ_ONCE(audit_exit_filter_mask[word]);
+ rule_mask = rule->mask[word];
+ if (!rule_mask)
+ continue;
+ while (rule_mask) {
+ bit = __ffs(rule_mask);
+ index = word * 32 + bit;
+ if (add) {
+ if (!audit_exit_filter_count[index]++)
+ mask |= BIT(bit);
+ } else if (!--audit_exit_filter_count[index]) {
+ mask &= ~BIT(bit);
+ }
+ rule_mask &= ~BIT(bit);
+ }
+ WRITE_ONCE(audit_exit_filter_mask[word], mask);
+ }
+}
+
+static void audit_exit_mask_add(const struct audit_krule *rule)
+{
+ audit_exit_mask_update(rule, true);
+}
+
+static void audit_exit_mask_remove(const struct audit_krule *rule)
+{
+ audit_exit_mask_update(rule, false);
+}
+
static inline int audit_match_class_bits(int class, const u32 *mask)
{
int i;
@@ -249,6 +297,9 @@ void audit_rule_account(const struct audit_krule *rule)
{
lockdep_assert_held(&audit_filter_mutex);
+ if (rule->listnr == AUDIT_FILTER_EXIT)
+ audit_exit_mask_add(rule);
+
if (audit_rule_counts_syscalls(rule))
audit_n_rules++;
if (!audit_match_signal(rule))
@@ -259,6 +310,9 @@ void audit_rule_unaccount(const struct audit_krule *rule)
{
lockdep_assert_held(&audit_filter_mutex);
+ if (rule->listnr == AUDIT_FILTER_EXIT)
+ audit_exit_mask_remove(rule);
+
if (audit_rule_counts_syscalls(rule))
audit_n_rules--;
if (!audit_match_signal(rule))
@@ -1018,6 +1072,7 @@ static inline int audit_add_rule(struct audit_entry
*entry)
entry->rule.prio = --prio_low;
}
+ audit_rule_account(&entry->rule);
if (entry->rule.flags & AUDIT_FILTER_PREPEND) {
list_add(&entry->rule.list,
&audit_rules_list[entry->rule.listnr]);
@@ -1028,7 +1083,6 @@ static inline int audit_add_rule(struct audit_entry
*entry)
&audit_rules_list[entry->rule.listnr]);
list_add_tail_rcu(&entry->list, list);
}
- audit_rule_account(&entry->rule);
mutex_unlock(&audit_filter_mutex);
return err;
diff --git a/kernel/auditsc.c b/kernel/auditsc.c
index 2b9ce0b52511..ff1809df63df 100644
--- a/kernel/auditsc.c
+++ b/kernel/auditsc.c
@@ -861,6 +861,16 @@ static void audit_filter_uring(struct task_struct *tsk,
rcu_read_unlock();
}
+static inline bool audit_exit_filter_may_match(unsigned long syscall)
+{
+ u32 word;
+
+ if (syscall >= AUDIT_BITMASK_SIZE * 32)
+ return false;
+ word = AUDIT_WORD(syscall);
+ return READ_ONCE(audit_exit_filter_mask[word]) & AUDIT_BIT(syscall);
+}
+
/* At syscall exit time, this filter is called if the audit_state is
* not low enough that auditing cannot take place, but is also not
* high enough that we already know we have to write an audit record
@@ -872,6 +882,9 @@ static void audit_filter_syscall(struct task_struct *tsk,
if (auditd_test_task(tsk))
return;
+ if (!audit_exit_filter_may_match(ctx->major))
+ return;
+
rcu_read_lock();
__audit_filter_op(tsk, ctx, &audit_filter_list[AUDIT_FILTER_EXIT],
NULL, ctx->major);
--
2.43.0