[PATCH RFC,-next] audit: add syscall fastpath to skip audit for uncovered syscalls
Gaosheng Cui <[email protected]> Wed, 8 Jul 2026 15:29:57 +0000
| Newsgroups | org.kernel.vger.audit |
|---|---|
| Message-ID | <[email protected]> |
When audit rules are configured, syscalls not covered by any rule
(e.g., futex, nanosleep, epoll_pwait) still incur full audit
overhead.
Add a bitmap fastpath: at syscall entry, audit_fastpath_skip()
checks a precomputed bitmap; if the syscall is not covered, set
dummy=1 so the exit path skips both filter traversal and context
reset.
The bitmap is the OR of:
- All EXIT rules' masks (not including watch/tree rules, ORed
under audit_filter_mutex during rebuild)
- audit_inode_syscalls[]: native syscalls that may trigger
__audit_inode()/__audit_getname(), built at init from the
file-operation audit classes plus audit_classify_syscall().
Compat syscalls are excluded from the fastpath because native and
compat syscall numbers share the same flat bitmap but belong to
different namespaces. in_compat_syscall() guards the fastpath
so compat paths always take the original full audit processing.
Accordingly, audit_inode_syscalls[] only includes native audit
classes, not their _32 compat variants.
The bitmap is read lock-free via READ_ONCE on syscall hot paths
and written under audit_filter_mutex with WRITE_ONCE per word —
the same no-guarantee-during-rule-change policy as audit_n_rules.
Signed-off-by: Gaosheng Cui <[email protected]>
---
kernel/audit.h | 6 ++
kernel/auditfilter.c | 140 +++++++++++++++++++++++++++++++++++++++++++
kernel/auditsc.c | 3 +
3 files changed, 149 insertions(+)
diff --git a/kernel/audit.h b/kernel/audit.h
index 92d5e723d570..87fb5e570651 100644
--- a/kernel/audit.h
+++ b/kernel/audit.h
@@ -218,6 +218,12 @@ struct audit_context {
extern bool audit_ever_enabled;
+#ifdef CONFIG_AUDITSYSCALL
+extern bool audit_fastpath_skip(int major);
+#else
+static inline bool audit_fastpath_skip(int major) { return false; }
+#endif
+
extern void audit_log_session_info(struct audit_buffer *ab);
extern int auditd_test_task(struct task_struct *task);
diff --git a/kernel/auditfilter.c b/kernel/auditfilter.c
index 4401119b5275..9b497b7b3196 100644
--- a/kernel/auditfilter.c
+++ b/kernel/auditfilter.c
@@ -10,6 +10,10 @@
#include <linux/kernel.h>
#include <linux/audit.h>
+#ifdef CONFIG_AUDITSYSCALL
+#include <linux/audit_arch.h>
+#include <asm/syscall.h>
+#endif
#include <linux/kthread.h>
#include <linux/mutex.h>
#include <linux/fs.h>
@@ -165,6 +169,81 @@ static inline int audit_to_inode(struct audit_krule *krule,
static __u32 *classes[AUDIT_SYSCALL_CLASSES];
+#ifdef CONFIG_AUDITSYSCALL
+static u32 audit_filter_syscalls[AUDIT_BITMASK_SIZE];
+static u32 audit_inode_syscalls[AUDIT_BITMASK_SIZE];
+
+/**
+ * audit_fastpath_skip - check if a syscall can skip audit processing
+ * @major: syscall number
+ *
+ * Determines whether the syscall identified by @major is not covered by
+ * any audit rule and can thus safely skip the full audit entry/exit path.
+ *
+ * The fastpath bitmap is a merge of two sources:
+ * 1) All EXIT rules' masks (not including watch/tree rules) ORed into
+ * audit_filter_syscalls.
+ * 2) audit_inode_syscalls is always ORed in, covering syscalls that may
+ * trigger __audit_inode()/__audit_getname() (open, execve, etc.).
+ *
+ * The bitmap is read lock-free via READ_ONCE on syscall hot paths, and
+ * written under audit_filter_mutex during rule changes with WRITE_ONCE
+ * per word, following the same no-guarantee-during-rule-change policy
+ * as audit_n_rules.
+ *
+ * Return: true if the syscall is not covered by any rule (can skip),
+ * false otherwise.
+ */
+bool audit_fastpath_skip(int major)
+{
+ u32 filter_word;
+
+ if (in_compat_syscall())
+ return false;
+
+ if (major < 0 || major >= AUDIT_BITMASK_SIZE * 32)
+ return false;
+
+ filter_word = READ_ONCE(audit_filter_syscalls[AUDIT_WORD(major)]);
+
+ return !(filter_word & AUDIT_BIT(major));
+}
+
+static void audit_rebuild_filter_syscalls(void)
+{
+ struct audit_entry *e;
+
+ lockdep_assert_held(&audit_filter_mutex);
+
+ for (int i = 0; i < AUDIT_BITMASK_SIZE; i++)
+ WRITE_ONCE(audit_filter_syscalls[i], 0);
+
+ list_for_each_entry(e, &audit_filter_list[AUDIT_FILTER_EXIT], list) {
+ struct audit_krule *r = &e->rule;
+
+ if (r->tree)
+ continue;
+
+ for (int i = 0; i < AUDIT_BITMASK_SIZE; i++)
+ WRITE_ONCE(audit_filter_syscalls[i],
+ audit_filter_syscalls[i] | r->mask[i]);
+ }
+
+ /*
+ * Always OR audit_inode_syscalls into the filter bitmap so that
+ * syscalls which may trigger __audit_inode()/__audit_getname()
+ * (open, execve, etc.) are never skipped, regardless of whether
+ * watch/tree rules currently exist. These syscalls must always
+ * run the normal entry path to set up the audit context; skipping
+ * them would leave the context uninitialized if a later part of
+ * the kernel (e.g., audit_log_start) needs to emit a record.
+ */
+ for (int i = 0; i < AUDIT_BITMASK_SIZE; i++)
+ WRITE_ONCE(audit_filter_syscalls[i],
+ audit_filter_syscalls[i] | audit_inode_syscalls[i]);
+}
+#endif
+
int __init audit_register_class(int class, unsigned int *list)
{
__u32 *p = kcalloc(AUDIT_BITMASK_SIZE, sizeof(__u32), GFP_KERNEL);
@@ -183,9 +262,59 @@ int __init audit_register_class(int class, unsigned int *list)
return -EINVAL;
}
classes[class] = p;
+
+#ifdef CONFIG_AUDITSYSCALL
+ switch (class) {
+ case AUDIT_CLASS_WRITE:
+ case AUDIT_CLASS_READ:
+ case AUDIT_CLASS_DIR_WRITE:
+ case AUDIT_CLASS_CHATTR:
+ for (int i = 0; i < AUDIT_BITMASK_SIZE; i++)
+ audit_inode_syscalls[i] |= p[i];
+ break;
+ }
+#endif
+
return 0;
}
+#ifdef CONFIG_AUDITSYSCALL
+static int __init audit_build_inode_syscalls(void)
+{
+ unsigned int arch = syscall_get_arch(current);
+
+ static const struct {
+ unsigned int nr;
+ int classify_result;
+ } table[] = {
+#ifdef __NR_open
+ { __NR_open, AUDITSC_OPEN },
+#endif
+#ifdef __NR_openat
+ { __NR_openat, AUDITSC_OPENAT },
+#endif
+#ifdef __NR_openat2
+ { __NR_openat2, AUDITSC_OPENAT2 },
+#endif
+#ifdef __NR_execve
+ { __NR_execve, AUDITSC_EXECVE },
+#endif
+#ifdef __NR_execveat
+ { __NR_execveat, AUDITSC_EXECVE },
+#endif
+ };
+
+ for (int i = 0; i < ARRAY_SIZE(table); i++) {
+ if (audit_classify_syscall(arch, table[i].nr) == table[i].classify_result)
+ audit_inode_syscalls[AUDIT_WORD(table[i].nr)] |=
+ AUDIT_BIT(table[i].nr);
+ }
+
+ return 0;
+}
+late_initcall(audit_build_inode_syscalls);
+#endif
+
int audit_match_class(int class, unsigned int syscall)
{
if (unlikely(syscall >= AUDIT_BITMASK_SIZE * 32))
@@ -1013,7 +1142,11 @@ static inline int audit_add_rule(struct audit_entry *entry)
if (!audit_match_signal(entry))
audit_signals++;
+
+ if (entry->rule.listnr == AUDIT_FILTER_EXIT)
+ audit_rebuild_filter_syscalls();
#endif
+
mutex_unlock(&audit_filter_mutex);
return err;
@@ -1060,6 +1193,9 @@ int audit_del_rule(struct audit_entry *entry)
if (!audit_match_signal(entry))
audit_signals--;
+
+ if (e->rule.listnr == AUDIT_FILTER_EXIT)
+ audit_rebuild_filter_syscalls();
#endif
list_del_rcu(&e->list);
@@ -1458,6 +1594,10 @@ int audit_update_lsm_rules(void)
err = res;
}
}
+
+#ifdef CONFIG_AUDITSYSCALL
+ audit_rebuild_filter_syscalls();
+#endif
mutex_unlock(&audit_filter_mutex);
return err;
diff --git a/kernel/auditsc.c b/kernel/auditsc.c
index 6610e667c728..4471fa9b4101 100644
--- a/kernel/auditsc.c
+++ b/kernel/auditsc.c
@@ -2010,6 +2010,9 @@ void __audit_syscall_entry(int major, unsigned long a1, unsigned long a2,
return;
}
+ if (!context->dummy)
+ context->dummy = audit_fastpath_skip(major);
+
context->arch = syscall_get_arch(current);
context->major = major;
context->argv[0] = a1;
--
2.43.0