* [PATCH 0/3] audit: Measure and reduce syscall filtering overhead
@ 2026-08-07 1:01 Stanislav Kinsburskii
2026-08-07 1:01 ` [PATCH 1/3] selftests/audit: Add syscall overhead benchmark Stanislav Kinsburskii
` (2 more replies)
0 siblings, 3 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
This series adds a repeatable microbenchmark for audit's fixed syscall
overhead and uses it to address two cases where audit continues doing work
which cannot produce a record.
Patch 1 adds audit_bench, a manually run getpid(2) microbenchmark under
tools/testing/selftests/audit. It leaves policy management to the caller
so the same workload can measure different rule configurations without
silently changing the system policy.
Patch 2 fixes audit_n_rules and audit_signals accounting when rules are
removed automatically with a watch or tree, or after an LSM rule update
fails. These paths could leave the counters nonzero after the last
applicable rule had disappeared, causing every subsequent syscall to
allocate a non-dummy audit context. It also centralizes rule accounting
so all rule removal paths share the same bookkeeping.
The median getpid latency in the same unpinned VM was:
no rules stale state fixed
automatically removed watch 38 ns 55 ns 38 ns
automatically removed tree 38 ns 59 ns 38 ns
Patch 3 builds on those lifecycle helpers. It maintains an aggregate mask
of the syscall numbers present in exit rules and checks that mask before
walking the exit filter list. The mask is architecture-independent and
therefore conservative: overlapping syscall numbers may cause an
unnecessary scan, but cannot suppress a match.
For an unrelated getpid workload, the median latency scaled as follows:
exit rules 1 32 128 256
before 55 ns 71 ns 428 ns 791 ns
after 55 ns 55 ns 55 ns 55 ns
The aggregate mask is updated through the centralized accounting helpers.
Insertion sets the relevant bits before publishing the rule with
list_add_rcu(); removal unlinks the rule before clearing them. This keeps
the lockless rejection test conservative during concurrent rule changes.
The series does not change the audit userspace ABI or rule matching
semantics. The benchmark and complete reproduction procedures are
documented in the individual patches.
---
Stanislav Kinsburskii (3):
selftests/audit: Add syscall overhead benchmark
audit: Fix filter rule accounting after automatic removal
audit: Skip exit filtering for syscalls without rules
MAINTAINERS | 1 +
kernel/audit.h | 7 +
kernel/audit_tree.c | 1 +
kernel/audit_watch.c | 2 +
kernel/auditfilter.c | 140 +++++++++++------
kernel/auditsc.c | 13 ++
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 ++++++++++++++++++++++++++++
11 files changed, 389 insertions(+), 44 deletions(-)
---
base-commit: ea2bff00da89d7767d677bb68470130ba96f4928
change-id: 20260806-audit-96a1e71d38b1
Best regards,
--
Stanislav Kinsburskii <skinsburskii@gmail.com>
^ permalink raw reply [flat|nested] 4+ messages in thread
* [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
end of thread, other threads:[~2026-08-07 1:01 UTC | newest]
Thread overview: 4+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
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 ` [PATCH 3/3] audit: Skip exit filtering for syscalls without rules Stanislav Kinsburskii
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox