From: Stanislav Kinsburskii <skinsburskii@gmail.com>
To: Shuah Khan <shuah@kernel.org>, Paul Moore <paul@paul-moore.com>,
Eric Paris <eparis@redhat.com>,
Al Viro <viro@zeniv.linux.org.uk>,
Amy Griffis <amy.griffis@hp.com>
Cc: Stanislav Kinsburskii <skinsburskii@gmail.com>,
Frank Hofmann <hofmann@deshaw.com>,
Noah Orlando <orlandon@deshaw.com>,
linux-kernel@vger.kernel.org, linux-kselftest@vger.kernel.org,
audit@vger.kernel.org
Subject: [PATCH 1/3] selftests/audit: Add syscall overhead benchmark
Date: Thu, 06 Aug 2026 18:01:19 -0700 [thread overview]
Message-ID: <20260806-audit-v1-1-ddd0d94ff0b6@gmail.com> (raw)
In-Reply-To: <20260806-audit-v1-0-ddd0d94ff0b6@gmail.com>
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
next prev parent reply other threads:[~2026-08-07 1:01 UTC|newest]
Thread overview: 4+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-08-07 1:01 [PATCH 0/3] audit: Measure and reduce syscall filtering overhead Stanislav Kinsburskii
2026-08-07 1:01 ` Stanislav Kinsburskii [this message]
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
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260806-audit-v1-1-ddd0d94ff0b6@gmail.com \
--to=skinsburskii@gmail.com \
--cc=amy.griffis@hp.com \
--cc=audit@vger.kernel.org \
--cc=eparis@redhat.com \
--cc=hofmann@deshaw.com \
--cc=linux-kernel@vger.kernel.org \
--cc=linux-kselftest@vger.kernel.org \
--cc=orlandon@deshaw.com \
--cc=paul@paul-moore.com \
--cc=shuah@kernel.org \
--cc=viro@zeniv.linux.org.uk \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox