* [PATCH 1/3] selftests/audit: Add syscall overhead benchmark
2026-08-07 1:01 [PATCH 0/3] audit: Measure and reduce syscall filtering overhead Stanislav Kinsburskii
@ 2026-08-07 1:01 ` Stanislav Kinsburskii
2026-08-07 1:01 ` [PATCH 2/3] audit: Fix filter rule accounting after automatic removal Stanislav Kinsburskii
2026-08-07 1:01 ` [PATCH 3/3] audit: Skip exit filtering for syscalls without rules Stanislav Kinsburskii
2 siblings, 0 replies; 4+ messages in thread
From: Stanislav Kinsburskii @ 2026-08-07 1:01 UTC (permalink / raw)
To: Shuah Khan, Paul Moore, Eric Paris, Al Viro, Amy Griffis
Cc: Stanislav Kinsburskii, Frank Hofmann, Noah Orlando, linux-kernel,
linux-kselftest, audit
Add a microbenchmark which repeatedly invokes getpid(2) and reports the
per-operation latency across multiple repetitions. The workload avoids
filesystem and other syscall-specific work so the fixed audit syscall
overhead remains visible.
The benchmark deliberately leaves audit policy management to the caller.
This permits comparisons with increasing numbers of unrelated exit rules
and with automatically removed watch or tree rules without modifying an
existing policy unexpectedly.
Signed-off-by: Stanislav Kinsburskii <skinsburskii@gmail.com>
---
MAINTAINERS | 1 +
tools/testing/selftests/Makefile | 1 +
tools/testing/selftests/audit/.gitignore | 2 +
tools/testing/selftests/audit/Makefile | 9 ++
tools/testing/selftests/audit/README | 30 ++++
tools/testing/selftests/audit/audit_bench.c | 227 ++++++++++++++++++++++++++++
6 files changed, 270 insertions(+)
diff --git a/MAINTAINERS b/MAINTAINERS
index d52c224eabaf..6d87387de0cf 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -4365,6 +4365,7 @@ F: include/linux/audit_arch.h
F: include/uapi/linux/audit.h
F: kernel/audit*
F: lib/*audit.c
+F: tools/testing/selftests/audit/
K: \baudit_[a-z_0-9]\+\b
AUTOFDO BUILD
diff --git a/tools/testing/selftests/Makefile b/tools/testing/selftests/Makefile
index 84343fd1e354..fb2dae9d4018 100644
--- a/tools/testing/selftests/Makefile
+++ b/tools/testing/selftests/Makefile
@@ -1,5 +1,6 @@
# SPDX-License-Identifier: GPL-2.0
TARGETS += acct
+TARGETS += audit
TARGETS += alloc_tag
TARGETS += alsa
TARGETS += amd-pstate
diff --git a/tools/testing/selftests/audit/.gitignore b/tools/testing/selftests/audit/.gitignore
new file mode 100644
index 000000000000..1138c94bdce5
--- /dev/null
+++ b/tools/testing/selftests/audit/.gitignore
@@ -0,0 +1,2 @@
+# SPDX-License-Identifier: GPL-2.0-only
+audit_bench
diff --git a/tools/testing/selftests/audit/Makefile b/tools/testing/selftests/audit/Makefile
new file mode 100644
index 000000000000..ce7e06725fd0
--- /dev/null
+++ b/tools/testing/selftests/audit/Makefile
@@ -0,0 +1,9 @@
+# SPDX-License-Identifier: GPL-2.0
+
+CFLAGS += -O2 -Wall -Wextra
+LDLIBS += -lm
+
+TEST_GEN_PROGS_EXTENDED := audit_bench
+TEST_FILES := README
+
+include ../lib.mk
diff --git a/tools/testing/selftests/audit/README b/tools/testing/selftests/audit/README
new file mode 100644
index 000000000000..b40155808dcf
--- /dev/null
+++ b/tools/testing/selftests/audit/README
@@ -0,0 +1,30 @@
+Audit syscall overhead benchmark
+================================
+
+Build it with:
+
+ make -C tools/testing/selftests/audit
+
+The benchmark repeatedly invokes getpid(2). It does not install or remove
+audit rules. Configure the policy explicitly with auditctl, then run the same
+workload for each policy.
+
+For example:
+
+ sudo auditctl -a always,exit -F arch=b64 -S openat
+ sudo ./tools/testing/selftests/audit/audit_bench
+ sudo auditctl -d always,exit -F arch=b64 -S openat
+
+The getpid workload exposes the fixed per-syscall audit overhead without
+adding filesystem work. Useful comparisons are no rules, increasing numbers
+of unrelated syscall rules, and a clean state versus one where a watch or
+tree rule was removed automatically. Keep the machine idle, pin with --cpu
+when possible, and collect profiles with perf stat and perf record. Report
+the kernel commit, CPU model, audit status, policy and auditd state with every
+comparison.
+
+After printing each repetition, the benchmark reports the median, arithmetic
+mean, sample standard deviation, coefficient of variation and observed range
+of per-operation latency across repetitions. The coefficient of variation
+makes noisy runs easy to identify; increase the iteration count or investigate
+system noise when it is high.
diff --git a/tools/testing/selftests/audit/audit_bench.c b/tools/testing/selftests/audit/audit_bench.c
new file mode 100644
index 000000000000..89c19c03817c
--- /dev/null
+++ b/tools/testing/selftests/audit/audit_bench.c
@@ -0,0 +1,227 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Microbenchmark for Linux audit syscall overhead.
+ *
+ * This program does not configure audit. Install rules manually to compare
+ * the same workload under different policies.
+ */
+
+#define _GNU_SOURCE
+#include <err.h>
+#include <errno.h>
+#include <getopt.h>
+#include <inttypes.h>
+#include <limits.h>
+#include <math.h>
+#include <sched.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/syscall.h>
+#include <time.h>
+#include <unistd.h>
+
+#define DEFAULT_ITERATIONS 10000000ULL
+#define DEFAULT_REPETITIONS 10
+
+static int compare_double(const void *left, const void *right)
+{
+ const double a = *(const double *)left;
+ const double b = *(const double *)right;
+
+ return (a > b) - (a < b);
+}
+
+static void print_summary(double *samples, unsigned int repetitions)
+{
+ double mean = 0.0;
+ double squared_deviations = 0.0;
+ double median;
+ double stddev;
+ unsigned int i;
+
+ for (i = 0; i < repetitions; i++)
+ mean += samples[i];
+ mean /= repetitions;
+
+ for (i = 0; i < repetitions; i++) {
+ double deviation = samples[i] - mean;
+
+ squared_deviations += deviation * deviation;
+ }
+ stddev = repetitions > 1 ?
+ sqrt(squared_deviations / (repetitions - 1)) : 0.0;
+
+ qsort(samples, repetitions, sizeof(*samples), compare_double);
+ if (repetitions % 2)
+ median = samples[repetitions / 2];
+ else
+ median = (samples[repetitions / 2 - 1] +
+ samples[repetitions / 2]) / 2.0;
+
+ printf("==========================================\n");
+ printf("summary (ns/op): median=%.f", median);
+ printf(" mean=%.f", mean);
+ printf(" stddev=%.f (%.f%%)", stddev,
+ mean ? stddev * 100.0 / mean : 0.0);
+ printf(" range=%.f..%.f\n",
+ samples[0], samples[repetitions - 1]);
+}
+
+static void usage(const char *program)
+{
+ printf("Usage: %s [OPTIONS]\n", program);
+ printf("\n");
+ printf("Options:\n");
+ printf(" -c, --cpu CPU pin the benchmark to CPU\n");
+ printf(" -h, --help show this help\n");
+ printf(" -n, --iterations N measured operations per repetition\n");
+ printf(" (default: %llu)\n",
+ DEFAULT_ITERATIONS);
+ printf(" -r, --repetitions N number of measured repetitions\n");
+ printf(" (default: %d)\n",
+ DEFAULT_REPETITIONS);
+ printf(" -w, --warmup N warm-up operations (default N/10)\n");
+}
+
+static uint64_t parse_u64(const char *value, const char *name)
+{
+ uint64_t parsed;
+ char *end;
+
+ if (*value < '0' || *value > '9')
+ errx(EXIT_FAILURE, "invalid %s: %s", name, value);
+
+ errno = 0;
+ parsed = strtoull(value, &end, 0);
+ if (errno || *value == '\0' || *end != '\0')
+ errx(EXIT_FAILURE, "invalid %s: %s", name, value);
+
+ return parsed;
+}
+
+static unsigned int parse_uint(const char *value, const char *name)
+{
+ uint64_t parsed = parse_u64(value, name);
+
+ if (parsed > UINT_MAX)
+ errx(EXIT_FAILURE, "%s is too large: %s", name, value);
+
+ return parsed;
+}
+
+static void pin_to_cpu(unsigned int cpu)
+{
+ cpu_set_t set;
+
+ if (cpu >= CPU_SETSIZE)
+ errx(EXIT_FAILURE, "CPU must be less than %d", CPU_SETSIZE);
+
+ CPU_ZERO(&set);
+ CPU_SET(cpu, &set);
+ if (sched_setaffinity(0, sizeof(set), &set))
+ err(EXIT_FAILURE, "sched_setaffinity(%u)", cpu);
+}
+
+static uint64_t elapsed_ns(const struct timespec *start,
+ const struct timespec *end)
+{
+ return (end->tv_sec - start->tv_sec) * 1000000000ULL +
+ end->tv_nsec - start->tv_nsec;
+}
+
+static void run_getpid(uint64_t iterations)
+{
+ uint64_t i;
+
+ for (i = 0; i < iterations; i++)
+ syscall(SYS_getpid);
+}
+
+int main(int argc, char **argv)
+{
+ static const struct option options[] = {
+ { "cpu", required_argument, NULL, 'c' },
+ { "help", no_argument, NULL, 'h' },
+ { "iterations", required_argument, NULL, 'n' },
+ { "repetitions", required_argument, NULL, 'r' },
+ { "warmup", required_argument, NULL, 'w' },
+ { }
+ };
+ uint64_t iterations = DEFAULT_ITERATIONS;
+ uint64_t warmup = 0;
+ unsigned int repetitions = DEFAULT_REPETITIONS;
+ unsigned int cpu = 0;
+ bool warmup_set = false;
+ bool cpu_set = false;
+ double *samples;
+ int option;
+ unsigned int repetition;
+
+ while ((option = getopt_long(argc, argv, "c:hn:r:w:", options,
+ NULL)) != -1) {
+ switch (option) {
+ case 'c':
+ cpu = parse_uint(optarg, "CPU");
+ cpu_set = true;
+ break;
+ case 'h':
+ usage(argv[0]);
+ return EXIT_SUCCESS;
+ case 'n':
+ iterations = parse_u64(optarg, "iteration count");
+ break;
+ case 'r':
+ repetitions = parse_uint(optarg, "repetition count");
+ break;
+ case 'w':
+ warmup = parse_u64(optarg, "warm-up count");
+ warmup_set = true;
+ break;
+ default:
+ usage(argv[0]);
+ return EXIT_FAILURE;
+ }
+ }
+
+ if (optind != argc)
+ errx(EXIT_FAILURE, "unexpected positional argument: %s",
+ argv[optind]);
+ if (!iterations || !repetitions)
+ errx(EXIT_FAILURE, "iterations and repetitions must be nonzero");
+ if (!warmup_set)
+ warmup = iterations / 10;
+ if (cpu_set)
+ pin_to_cpu(cpu);
+
+ samples = calloc(repetitions, sizeof(*samples));
+ if (!samples)
+ err(EXIT_FAILURE, "calloc(samples)");
+
+ printf("getpid iterations=%" PRIu64 " warmup=%" PRIu64
+ " repetitions=%u\n", iterations, warmup, repetitions);
+
+ run_getpid(warmup);
+ for (repetition = 0; repetition < repetitions; repetition++) {
+ struct timespec start, end;
+ uint64_t duration;
+ double ns_per_operation;
+
+ if (clock_gettime(CLOCK_MONOTONIC_RAW, &start))
+ err(EXIT_FAILURE, "clock_gettime(start)");
+ run_getpid(iterations);
+ if (clock_gettime(CLOCK_MONOTONIC_RAW, &end))
+ err(EXIT_FAILURE, "clock_gettime(end)");
+
+ duration = elapsed_ns(&start, &end);
+ ns_per_operation = (double)duration / iterations;
+ samples[repetition] = ns_per_operation;
+ printf("%u: %.2f ns/op\n", repetition + 1,
+ ns_per_operation);
+ }
+
+ print_summary(samples, repetitions);
+ free(samples);
+ return EXIT_SUCCESS;
+}
--
2.43.0
^ permalink raw reply related [flat|nested] 4+ messages in thread* [PATCH 2/3] audit: Fix filter rule accounting after automatic removal
2026-08-07 1:01 [PATCH 0/3] audit: Measure and reduce syscall filtering overhead Stanislav Kinsburskii
2026-08-07 1:01 ` [PATCH 1/3] selftests/audit: Add syscall overhead benchmark Stanislav Kinsburskii
@ 2026-08-07 1:01 ` Stanislav Kinsburskii
2026-08-07 1:01 ` [PATCH 3/3] audit: Skip exit filtering for syscalls without rules Stanislav Kinsburskii
2 siblings, 0 replies; 4+ messages in thread
From: Stanislav Kinsburskii @ 2026-08-07 1:01 UTC (permalink / raw)
To: Shuah Khan, Paul Moore, Eric Paris, Al Viro, Amy Griffis
Cc: Stanislav Kinsburskii, Frank Hofmann, Noah Orlando, linux-kernel,
linux-kselftest, audit
The audit_n_rules and audit_signals counters are incremented when filter
rules are installed and decremented by the explicit rule deletion path.
Rules can also disappear when a watch or tree is removed, or when an LSM
rule cannot be reconstructed, but those paths do not update the counters.
As a result, audit_n_rules can remain nonzero after the last applicable
rule has gone away, causing subsequent syscalls to allocate non-dummy
audit contexts unnecessarily. A stale audit_signals value similarly
causes unnecessary signal auditing work.
This can be reproduced for an inode watch with:
mkdir /tmp/audit-n-rules-bench
touch /tmp/audit-n-rules-bench/watched
auditctl -w /tmp/audit-n-rules-bench/watched -p r \
-k audit_n_rules_bench
rm /tmp/audit-n-rules-bench/watched
rmdir /tmp/audit-n-rules-bench
The rm updates the watch after its inode disappears, and the rmdir causes
audit_remove_parent_watches() to remove the rule. For an audit tree, the
kill_rules() path can be reproduced with:
mkdir /tmp/audit-kill-rules
auditctl -a always,exit -F arch=b64 \
-F dir=/tmp/audit-kill-rules -F perm=r \
-k audit_kill_rules_test
rmdir /tmp/audit-kill-rules
In both cases, auditctl -l reports no rules after the directory is
removed. Run the following before installing the rule and again after it
has disappeared:
audit_bench --iterations 10000000 --repetitions 10
For the inode watch, the same VM produced:
no rules:
median=38 mean=39 stddev=4 (10%) range=38..53 ns/op
automatically removed, before this fix:
median=55 mean=56 stddev=3 (5%) range=55..65 ns/op
automatically removed, with this fix:
median=38 mean=39 stddev=4 (10%) range=38..52 ns/op
For the audit tree, it produced:
no rules:
median=38 mean=39 stddev=4 (9%) range=38..52 ns/op
automatically removed, before this fix:
median=59 mean=60 stddev=2 (3%) range=59..67 ns/op
automatically removed, with this fix:
median=38 mean=39 stddev=4 (9%) range=38..52 ns/op
Reboot between the unpatched and patched tests because an already stale
counter cannot be repaired by deleting rules which are no longer present.
Factor the existing counter updates into common rule insertion and removal
helpers and call the removal helper from every automatic removal path. All
of these updates remain serialized by audit_filter_mutex.
Fixes: 471a5c7c8391 ("[PATCH] introduce audit rules counter")
Fixes: e54dc2431d74 ("[PATCH] audit signal recipients")
Signed-off-by: Stanislav Kinsburskii <skinsburskii@gmail.com>
---
kernel/audit.h | 5 +++
kernel/audit_tree.c | 1 +
kernel/audit_watch.c | 2 ++
kernel/auditfilter.c | 86 +++++++++++++++++++++++++---------------------------
4 files changed, 50 insertions(+), 44 deletions(-)
diff --git a/kernel/audit.h b/kernel/audit.h
index 92d5e723d570..3176da464843 100644
--- a/kernel/audit.h
+++ b/kernel/audit.h
@@ -272,6 +272,9 @@ extern void audit_put_tty(struct tty_struct *tty);
/* audit watch/mark/tree functions */
extern unsigned int audit_serial(void);
#ifdef CONFIG_AUDITSYSCALL
+void audit_rule_account(const struct audit_krule *rule);
+void audit_rule_unaccount(const struct audit_krule *rule);
+
extern int auditsc_get_stamp(struct audit_context *ctx,
struct audit_stamp *stamp);
@@ -315,6 +318,8 @@ extern void audit_filter_inodes(struct task_struct *tsk,
struct audit_context *ctx);
extern struct list_head *audit_killed_trees(void);
#else /* CONFIG_AUDITSYSCALL */
+#define audit_rule_account(...) do { } while (0)
+#define audit_rule_unaccount(...) do { } while (0)
#define auditsc_get_stamp(c, s) 0
#define audit_put_watch(w) do { } while (0)
#define audit_get_watch(w) do { } while (0)
diff --git a/kernel/audit_tree.c b/kernel/audit_tree.c
index 1ed19b775912..2d68d2ec2b2a 100644
--- a/kernel/audit_tree.c
+++ b/kernel/audit_tree.c
@@ -558,6 +558,7 @@ static void kill_rules(struct audit_context *context, struct audit_tree *tree)
rule->tree = NULL;
list_del_rcu(&entry->list);
list_del(&entry->rule.list);
+ audit_rule_unaccount(rule);
call_rcu(&entry->rcu, audit_free_rule_rcu);
}
}
diff --git a/kernel/audit_watch.c b/kernel/audit_watch.c
index 4ac8a91e9ba8..28fab822ca0c 100644
--- a/kernel/audit_watch.c
+++ b/kernel/audit_watch.c
@@ -284,6 +284,7 @@ static void audit_update_watch(struct audit_parent *parent,
nentry = audit_dupe_rule(&oentry->rule, ctx);
if (IS_ERR(nentry)) {
list_del(&oentry->rule.list);
+ audit_rule_unaccount(r);
audit_panic("error updating watch, removing");
} else {
int h = audit_hash_ino(ino);
@@ -336,6 +337,7 @@ static void audit_remove_parent_watches(struct audit_parent *parent)
list_del(&r->rlist);
list_del(&r->list);
list_del_rcu(&e->list);
+ audit_rule_unaccount(r);
call_rcu(&e->rcu, audit_free_rule_rcu);
}
audit_remove_watch(w);
diff --git a/kernel/auditfilter.c b/kernel/auditfilter.c
index 7f791afe5791..38a56278ae0b 100644
--- a/kernel/auditfilter.c
+++ b/kernel/auditfilter.c
@@ -196,7 +196,7 @@ int audit_match_class(int class, unsigned int syscall)
}
#ifdef CONFIG_AUDITSYSCALL
-static inline int audit_match_class_bits(int class, u32 *mask)
+static inline int audit_match_class_bits(int class, const u32 *mask)
{
int i;
@@ -208,30 +208,62 @@ static inline int audit_match_class_bits(int class, u32 *mask)
return 1;
}
-static int audit_match_signal(struct audit_entry *entry)
+static int audit_match_signal(const struct audit_krule *rule)
{
- struct audit_field *arch = entry->rule.arch_f;
+ struct audit_field *arch = rule->arch_f;
if (!arch) {
/* When arch is unspecified, we must check both masks on biarch
* as syscall number alone is ambiguous. */
return (audit_match_class_bits(AUDIT_CLASS_SIGNAL,
- entry->rule.mask) &&
+ rule->mask) &&
audit_match_class_bits(AUDIT_CLASS_SIGNAL_32,
- entry->rule.mask));
+ rule->mask));
}
switch (audit_classify_arch(arch->val)) {
case 0: /* native */
return (audit_match_class_bits(AUDIT_CLASS_SIGNAL,
- entry->rule.mask));
+ rule->mask));
case 1: /* 32bit on biarch */
return (audit_match_class_bits(AUDIT_CLASS_SIGNAL_32,
- entry->rule.mask));
+ rule->mask));
default:
return 1;
}
}
+
+static bool audit_rule_counts_syscalls(const struct audit_krule *rule)
+{
+ switch (rule->listnr) {
+ case AUDIT_FILTER_USER:
+ case AUDIT_FILTER_EXCLUDE:
+ case AUDIT_FILTER_FS:
+ return false;
+ default:
+ return true;
+ }
+}
+
+void audit_rule_account(const struct audit_krule *rule)
+{
+ lockdep_assert_held(&audit_filter_mutex);
+
+ if (audit_rule_counts_syscalls(rule))
+ audit_n_rules++;
+ if (!audit_match_signal(rule))
+ audit_signals++;
+}
+
+void audit_rule_unaccount(const struct audit_krule *rule)
+{
+ lockdep_assert_held(&audit_filter_mutex);
+
+ if (audit_rule_counts_syscalls(rule))
+ audit_n_rules--;
+ if (!audit_match_signal(rule))
+ audit_signals--;
+}
#endif
/* Common user-space to kernel rule translation. */
@@ -943,17 +975,6 @@ static inline int audit_add_rule(struct audit_entry *entry)
struct audit_tree *tree = entry->rule.tree;
struct list_head *list;
int err = 0;
-#ifdef CONFIG_AUDITSYSCALL
- int dont_count = 0;
-
- /* If any of these, don't count towards total */
- switch (entry->rule.listnr) {
- case AUDIT_FILTER_USER:
- case AUDIT_FILTER_EXCLUDE:
- case AUDIT_FILTER_FS:
- dont_count = 1;
- }
-#endif
mutex_lock(&audit_filter_mutex);
e = audit_find_rule(entry, &list);
@@ -1007,13 +1028,7 @@ static inline int audit_add_rule(struct audit_entry *entry)
&audit_rules_list[entry->rule.listnr]);
list_add_tail_rcu(&entry->list, list);
}
-#ifdef CONFIG_AUDITSYSCALL
- if (!dont_count)
- audit_n_rules++;
-
- if (!audit_match_signal(entry))
- audit_signals++;
-#endif
+ audit_rule_account(&entry->rule);
mutex_unlock(&audit_filter_mutex);
return err;
@@ -1026,17 +1041,6 @@ int audit_del_rule(struct audit_entry *entry)
struct audit_tree *tree = entry->rule.tree;
struct list_head *list;
int ret = 0;
-#ifdef CONFIG_AUDITSYSCALL
- int dont_count = 0;
-
- /* If any of these, don't count towards total */
- switch (entry->rule.listnr) {
- case AUDIT_FILTER_USER:
- case AUDIT_FILTER_EXCLUDE:
- case AUDIT_FILTER_FS:
- dont_count = 1;
- }
-#endif
mutex_lock(&audit_filter_mutex);
e = audit_find_rule(entry, &list);
@@ -1058,14 +1062,7 @@ int audit_del_rule(struct audit_entry *entry)
if (e->rule.exe)
audit_remove_mark_rule(&e->rule);
-#ifdef CONFIG_AUDITSYSCALL
- if (!dont_count)
- audit_n_rules--;
-
- if (!audit_match_signal(entry))
- audit_signals--;
-#endif
-
+ audit_rule_unaccount(&e->rule);
call_rcu(&e->rcu, audit_free_rule_rcu);
out:
@@ -1429,6 +1426,7 @@ static int update_lsm_rule(struct audit_krule *r)
list_del(&r->rlist);
list_del_rcu(&entry->list);
list_del(&r->list);
+ audit_rule_unaccount(r);
} else {
if (r->watch || r->tree)
list_replace_init(&r->rlist, &nentry->rule.rlist);
--
2.43.0
^ permalink raw reply related [flat|nested] 4+ messages in thread* [PATCH 3/3] audit: Skip exit filtering for syscalls without rules
2026-08-07 1:01 [PATCH 0/3] audit: Measure and reduce syscall filtering overhead Stanislav Kinsburskii
2026-08-07 1:01 ` [PATCH 1/3] selftests/audit: Add syscall overhead benchmark Stanislav Kinsburskii
2026-08-07 1:01 ` [PATCH 2/3] audit: Fix filter rule accounting after automatic removal Stanislav Kinsburskii
@ 2026-08-07 1:01 ` Stanislav Kinsburskii
2 siblings, 0 replies; 4+ messages in thread
From: Stanislav Kinsburskii @ 2026-08-07 1:01 UTC (permalink / raw)
To: Shuah Khan, Paul Moore, Eric Paris, Al Viro, Amy Griffis
Cc: Stanislav Kinsburskii, Frank Hofmann, Noah Orlando, linux-kernel,
linux-kselftest, audit
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 <skinsburskii@gmail.com>
---
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
^ permalink raw reply related [flat|nested] 4+ messages in thread