diff --git a/tools/testing/selftests/Makefile b/tools/testing/selftests/Makefile
index bc8fe9e8f7f2..5ee2fc668f39 100644
--- a/tools/testing/selftests/Makefile
+++ b/tools/testing/selftests/Makefile
@@ -47,6 +47,7 @@ TARGETS += kcmp
 TARGETS += kexec
 TARGETS += kvm
 TARGETS += landlock
+TARGETS += landlock/bench
 TARGETS += lib
 TARGETS += livepatch
 TARGETS += lkdtm
diff --git a/tools/testing/selftests/landlock/Makefile b/tools/testing/selftests/landlock/Makefile
index e149ef66841a..348e2dbdb4e0 100644
--- a/tools/testing/selftests/landlock/Makefile
+++ b/tools/testing/selftests/landlock/Makefile
@@ -10,9 +10,7 @@ src_test := $(wildcard *_test.c)
 
 TEST_GEN_PROGS := $(src_test:.c=)
 
-TEST_GEN_PROGS_EXTENDED := true bench/sandboxer bench/microbench
-
-TEST_PROGS_EXTENDED := bench/run.sh
+TEST_GEN_PROGS_EXTENDED := true
 
 # Short targets:
 $(TEST_GEN_PROGS): LDLIBS += -lcap
@@ -20,11 +18,6 @@ $(TEST_GEN_PROGS_EXTENDED): LDFLAGS += -static
 
 include ../lib.mk
 
-$(OUTPUT)/bench/microbench: bench/microbench.c bench/common.c
-$(OUTPUT)/bench/sandboxer: bench/sandboxer.c bench/common.c
-
 # Targets with $(OUTPUT)/ prefix:
 $(TEST_GEN_PROGS): LDLIBS += -lcap
 $(TEST_GEN_PROGS_EXTENDED): LDFLAGS += -static
-
-EXTRA_CLEAN = $(OUTPUT)/common.o
diff --git a/tools/testing/selftests/landlock/bench/Makefile b/tools/testing/selftests/landlock/bench/Makefile
new file mode 100644
index 000000000000..4187b4983b9c
--- /dev/null
+++ b/tools/testing/selftests/landlock/bench/Makefile
@@ -0,0 +1,179 @@
+# SPDX-License-Identifier: GPL-2.0
+
+# based on tools/testing/selftest/bpf/Makefile
+include ../../../../build/Build.include
+include ../../../../scripts/Makefile.arch
+include ../../../../scripts/Makefile.include
+
+CFLAGS += -g -rdynamic -Wall -Werror -I$(OUTPUT)
+# CFLAGS += -I$(OUTPUT)/tools/include
+
+LDLIBS += -lelf -lz -lrt -lpthread -lm
+
+# Silence some warnings when compiled with clang
+ifneq ($(LLVM),)
+CFLAGS += -Wno-unused-command-line-argument
+endif
+
+LOCAL_HDRS += common.h tracer_common.h
+
+# Order correspond to 'make run_tests' order
+TEST_GEN_PROGS_EXTENDED := tracer sandboxer microbench
+
+TEST_PROGS_EXTENDED := run.sh
+
+# Emit succinct information message describing current building step
+# $1 - generic step name (e.g., CC, LINK, etc);
+# $2 - optional "flavor" specifier; if provided, will be emitted as [flavor];
+# $3 - target (assumed to be file); only file name will be emitted;
+# $4 - optional extra arg, emitted as-is, if provided.
+ifeq ($(V),1)
+Q =
+msg =
+else
+Q = @
+msg = @printf '  %-8s%s %s%s\n' "$(1)" "$(if $(2), [$(2)])" "$(notdir $(3))" "$(if $(4), $(4))";
+MAKEFLAGS += --no-print-directory
+submake_extras := feature_display=0
+endif
+
+# override lib.mk's default rules
+OVERRIDE_TARGETS := 1
+override define CLEAN
+	$(call msg,CLEAN)
+	$(Q)$(RM) -r $(TEST_GEN_PROGS)
+	$(Q)$(RM) -r $(EXTRA_CLEAN)
+endef
+
+include ../../lib.mk
+
+TOOLSDIR := $(top_srcdir)/tools
+LIBDIR := $(TOOLSDIR)/lib
+BPFDIR := $(LIBDIR)/bpf
+BPFTOOLDIR := $(TOOLSDIR)/bpf/bpftool
+SCRATCH_DIR := $(OUTPUT)/tools
+BUILD_DIR := $(SCRATCH_DIR)/build
+INCLUDE_DIR := $(SCRATCH_DIR)/include
+BPFOBJ := $(BUILD_DIR)/libbpf/libbpf.a
+
+VMLINUX_BTF_PATHS ?= $(if $(O),$(O)/vmlinux)				\
+		     $(if $(KBUILD_OUTPUT),$(KBUILD_OUTPUT)/vmlinux)	\
+		     /sys/kernel/btf/vmlinux				\
+		     ../../../../../vmlinux				\
+		     /boot/vmlinux-$(shell uname -r)
+VMLINUX_BTF ?= $(abspath $(firstword $(wildcard $(VMLINUX_BTF_PATHS))))
+ifeq ($(VMLINUX_BTF),)
+$(error Cannot find a vmlinux for VMLINUX_BTF at any of "$(VMLINUX_BTF_PATHS)")
+endif
+
+# Define simple and short `make test_progs`, `make test_sysctl`, etc targets
+# to build individual tests.
+# NOTE: Semicolon at the end is critical to override lib.mk's default static
+# rule for binaries.
+$(notdir $(TEST_GEN_PROGS)): %: $(OUTPUT)/% ;
+
+# sort removes libbpf duplicates when not cross-building
+MAKE_DIRS := $(sort $(BUILD_DIR)/libbpf 		\
+	       $(BUILD_DIR)/bpftool  $(INCLUDE_DIR))
+
+$(MAKE_DIRS):
+	$(call msg,MKDIR,,$@)
+	$(Q)mkdir -p $@
+
+DEFAULT_BPFTOOL := $(SCRATCH_DIR)/sbin/bpftool
+
+TEST_GEN_PROGS_EXTENDED += $(DEFAULT_BPFTOOL)
+
+$(TEST_GEN_PROGS_EXTENDED): $(BPFOBJ)
+
+BPFTOOL ?= $(DEFAULT_BPFTOOL)
+$(DEFAULT_BPFTOOL): $(wildcard $(BPFTOOLDIR)/*.[ch] $(BPFTOOLDIR)/Makefile)    \
+		    $(BPFOBJ) | $(BUILD_DIR)/bpftool
+	$(Q)$(MAKE) $(submake_extras)  -C $(BPFTOOLDIR)			       \
+		    ARCH= CROSS_COMPILE= CC="$(HOSTCC)" LD="$(HOSTLD)" 	       \
+		    EXTRA_CFLAGS='-g'			       \
+		    OUTPUT=$(BUILD_DIR)/bpftool/			       \
+		    LIBBPF_OUTPUT=$(BUILD_DIR)/libbpf/		       \
+		    LIBBPF_DESTDIR=$(SCRATCH_DIR)/			       \
+		    prefix= DESTDIR=$(SCRATCH_DIR)/ install-bin
+
+$(BPFOBJ): $(wildcard $(BPFDIR)/*.[ch] $(BPFDIR)/Makefile)		       \
+	   | $(BUILD_DIR)/libbpf
+	$(Q)$(MAKE) $(submake_extras) -C $(BPFDIR) OUTPUT=$(BUILD_DIR)/libbpf/ \
+		    EXTRA_CFLAGS='-g'	       \
+		    DESTDIR=$(SCRATCH_DIR) prefix= all install_headers
+
+$(INCLUDE_DIR)/vmlinux.h: $(VMLINUX_BTF) $(BPFTOOL) | $(INCLUDE_DIR)
+ifeq ($(VMLINUX_H),)
+	$(call msg,GEN,,$@)
+	$(Q)$(BPFTOOL) btf dump file $(VMLINUX_BTF) format c > $@
+else
+	$(call msg,CP,,$@)
+	$(Q)cp "$(VMLINUX_H)" $@
+endif
+
+# Get Clang's default includes on this system, as opposed to those seen by
+# '--target=bpf'. This fixes "missing" files on some architectures/distros,
+# such as asm/byteorder.h, asm/socket.h, asm/sockios.h, sys/cdefs.h etc.
+#
+# Use '-idirafter': Don't interfere with include mechanics except where the
+# build would have failed anyways.
+define get_sys_includes
+$(shell $(1) -v -E - </dev/null 2>&1 \
+	| sed -n '/<...> search starts here:/,/End of search list./{ s| \(/.*\)|-idirafter \1|p }') \
+$(shell $(1) -dM -E - </dev/null | grep '__riscv_xlen ' | awk '{printf("-D__riscv_xlen=%d -D__BITS_PER_LONG=%d", $$3, $$3)}')
+endef
+
+# Determine target endianness.
+IS_LITTLE_ENDIAN = $(shell $(CC) -dM -E - </dev/null | \
+			grep 'define __BYTE_ORDER__ __ORDER_LITTLE_ENDIAN__')
+MENDIAN=$(if $(IS_LITTLE_ENDIAN),-mlittle-endian,-mbig-endian)
+
+CLANG_SYS_INCLUDES = $(call get_sys_includes,$(CLANG))
+BPF_CFLAGS = -g -Werror -D__TARGET_ARCH_$(SRCARCH) $(MENDIAN) 		\
+	     -I$(INCLUDE_DIR)
+
+CLANG_CFLAGS = $(CLANG_SYS_INCLUDES) \
+	       -Wno-compare-distinct-pointer-types
+
+# Build BPF object using Clang
+# $1 - input .c file
+# $2 - output .o file
+# $3 - CFLAGS
+define CLANG_BPF_BUILD_RULE
+	$(call msg,CLNG-BPF,,$2)
+	$(Q)$(CLANG) $3 -O2 --target=bpf -c $1 -mcpu=v3 -o $2
+endef
+
+BPF_PROGS_DIR := progs
+BPF_BUILD_RULE := CLANG_BPF_BUILD_RULE
+BPF_SRCS := $(notdir $(wildcard $(BPF_PROGS_DIR)/*.c))
+BPF_OBJS := $(patsubst %.c,$(OUTPUT)/%.bpf.o, $(BPF_SRCS))
+BPF_SKELS := $(patsubst %.c,$(OUTPUT)/%.skel.h, $(BPF_SRCS))
+
+TEST_GEN_FILES += $(BPF_OBJS)
+
+$(BPF_PROGS_DIR)-bpfobjs := y
+$(BPF_OBJS): $(OUTPUT)/%.bpf.o:				\
+	     $(BPF_PROGS_DIR)/%.c			\
+	     $(wildcard $(BPF_PROGS_DIR)/*.h)		\
+	     $(INCLUDE_DIR)/vmlinux.h				\
+	     $(wildcard $(BPFDIR)/*.bpf.h)			\
+	     | $(OUTPUT) $(BPFOBJ)
+	$(call $(BPF_BUILD_RULE),$<,$@, $(BPF_CFLAGS))
+
+$(BPF_SKELS): %.skel.h: %.bpf.o $(BPFTOOL) | $(OUTPUT)
+	$(call msg,GEN-SKEL,$(BINARY),$@)
+	$(Q)$(BPFTOOL) gen object $(<:.o=.linked1.o) $<
+	$(Q)$(BPFTOOL) gen skeleton $(<:.o=.linked1.o) name $(notdir $(<:.bpf.o=)) > $@
+
+$(OUTPUT)/%.o: %.c $(BPF_SKELS)
+	$(call msg,CC,,$@)
+	$(Q)$(CC) $(CFLAGS) -c $(filter %.c,$^) $(LDLIBS) -o $@
+
+$(OUTPUT)/%: $(OUTPUT)/%.o common.c
+	$(call msg,BINARY,,$@)
+	$(Q)$(LINK.c) $^ $(LDLIBS) -o $@
+
+EXTRA_CLEAN := $(SCRATCH_DIR) feature bpftool	\
+	$(addprefix $(OUTPUT)/,*.o *.skel.h) common.o
diff --git a/tools/testing/selftests/landlock/bench/config b/tools/testing/selftests/landlock/bench/config
new file mode 100644
index 000000000000..d7353ef77ff2
--- /dev/null
+++ b/tools/testing/selftests/landlock/bench/config
@@ -0,0 +1,10 @@
+CONFIG_BPF=y
+CONFIG_BPF_EVENTS=y
+CONFIG_BPF_SYSCALL=y
+CONFIG_CMDLINE="isolcpus=0 nohz_full=0 nosmt cpufreq.off=1"
+CONFIG_CMDLINE_BOOL=y
+CONFIG_DEBUG_INFO=y
+CONFIG_DEBUG_INFO_BTF=y
+CONFIG_DEBUG_INFO_DWARF4=y
+CONFIG_SECURITY=y
+CONFIG_SECURITY_LANDLOCK=y
diff --git a/tools/testing/selftests/landlock/bench/progs/tracer.c b/tools/testing/selftests/landlock/bench/progs/tracer.c
new file mode 100644
index 000000000000..3bd71d541b03
--- /dev/null
+++ b/tools/testing/selftests/landlock/bench/progs/tracer.c
@@ -0,0 +1,126 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * BPF programs for benchmarking data collection.
+ *
+ * Copyright © 2024 Huawei Tech. Co., Ltd.
+ */
+
+#include <vmlinux.h>
+#include <bpf/bpf_helpers.h>
+#include <bpf/bpf_tracing.h>
+#include <bpf/bpf_core_read.h>
+
+#include "../tracer_common.h"
+
+void bpf_rcu_read_lock(void) __ksym;
+void bpf_rcu_read_unlock(void) __ksym;
+
+char _license[] SEC("license") = "GPL";
+
+struct syscall_enter_args {
+	unsigned long long common_tp_fields;
+	long syscall_nr;
+	unsigned long args[6];
+};
+
+struct syscall_exit_args {
+	unsigned long long common_tp_fields;
+	long syscall_nr;
+	long ret;
+};
+
+struct {
+	__uint(type, BPF_MAP_TYPE_ARRAY);
+	__type(key, int);
+	__type(value, struct tracer_stat_data);
+	__uint(max_entries, NR_SYSCALLS);
+} sys_stats SEC(".maps");
+
+struct {
+	__uint(type, BPF_MAP_TYPE_HASH);
+	__type(key, pid_t);
+	__type(value, unsigned long long);
+	__uint(max_entries, MAX_TASKS);
+} sys_enter_timestamp SEC(".maps");
+
+int target_pid;
+
+static inline bool is_target(struct task_struct *task)
+{
+	bool res = false;
+
+	bpf_rcu_read_lock();
+
+	/* Accumulates data only for target process or it's child. */
+	while (task && task->pid != target_pid)
+		task = task->real_parent;
+	if (task)
+		res = true;
+
+	bpf_rcu_read_unlock();
+	return res;
+}
+
+SEC("tp/raw_syscalls/sys_enter")
+int sys_enter(struct syscall_enter_args *args)
+{
+	pid_t pid;
+	unsigned long long timestamp;
+	struct task_struct *task;
+
+	task = bpf_get_current_task_btf();
+
+	if (!is_target(task))
+		return 0;
+
+	pid = task->pid;
+	timestamp = bpf_ktime_get_ns();
+	bpf_map_update_elem(&sys_enter_timestamp, &pid, &timestamp, 0);
+	return 0;
+}
+
+SEC("tp/raw_syscalls/sys_exit")
+int sys_exit(struct syscall_exit_args *args)
+{
+	pid_t pid;
+	struct tracer_stat_data *stat;
+	unsigned long long *enter_timestamp, exit_timestamp, duration;
+	long long delta;
+	long syscall_nr;
+	struct task_struct *task;
+
+	exit_timestamp = bpf_ktime_get_ns();
+
+	task = bpf_get_current_task_btf();
+	if (!is_target(task))
+		return 0;
+
+	pid = task->pid;
+	enter_timestamp = bpf_map_lookup_elem(&sys_enter_timestamp, &pid);
+	if (!enter_timestamp)
+		return 0;
+
+	syscall_nr = args->syscall_nr;
+
+	stat = bpf_map_lookup_elem(&sys_stats, &syscall_nr);
+	if (stat) {
+		duration = exit_timestamp - *enter_timestamp;
+
+		/* Cf. tools/perf/util/stat.c */
+		stat->samples++;
+
+		delta = (long long)duration - stat->mean;
+
+		stat->duration += duration;
+
+		/* EBPF doesn't support signed division */
+		if (delta > 0)
+			stat->mean += (unsigned long long)delta / stat->samples;
+		else
+			stat->mean -= (unsigned long long)(-delta) / stat->samples;
+
+		stat->M2 += delta * (duration - stat->mean);
+		bpf_map_update_elem(&sys_stats, &syscall_nr, stat, 0);
+	}
+	return 0;
+}
diff --git a/tools/testing/selftests/landlock/bench/run.sh b/tools/testing/selftests/landlock/bench/run.sh
index 582313f689ad..6a8022b44ece 100755
--- a/tools/testing/selftests/landlock/bench/run.sh
+++ b/tools/testing/selftests/landlock/bench/run.sh
@@ -31,6 +31,7 @@ ACCESS=
 TRACED_SYSCALLS=
 MICROBENCH=false
 WORKLOAD=
+CUSTOM_TRACER=false
 SILENCE=false
 TRACE_CMD=
 
@@ -55,6 +56,7 @@ help()
 	echo "Options:"
 	echo "  -e TRACED_SYSCALLS  specify syscalls which would be traced while benchmarking"
 	echo "  -m                  run microbenchmark workload for TRACED_SYSCALLS"
+	echo "  -b                  trace via custom tracer"
 	echo "  -p PERF_BINARY      use PERF_BINARY instead of /usr/bin/perf"
 	echo "  -D MSECS            wait MSECS msecs before tracing sandboxed workload"
 	echo "                      (default: $SANDBOX_DELAY)"
@@ -95,6 +97,7 @@ parse_and_check_arguments()
 		case $arg in
 			e) TRACED_SYSCALLS=$OPTARG ;;
 			m) MICROBENCH=true ;;
+			b) CUSTOM_TRACER=true ;;
 			t) add_sandboxer_args $OPTARG ;;
 			p) PERF_BIN=`realpath $OPTARG` ;;
 			D) SANDBOX_DELAY=$OPTARG ;;
@@ -132,6 +135,10 @@ parse_and_check_arguments()
 	if [ ! -z "$WORKLOAD" ] && [ -z "$SANDBOXER_ARGS" ]; then
 		err Landlock topology is not specified
 	fi
+
+	if [ ! -f "$CUSTOM_TRACER_BIN" ] && $CUSTOM_TRACER ; then
+		err Tracer binary does not exist
+	fi
 }
 
 # perf trace
@@ -142,6 +149,9 @@ header+=';/^                                     (msec)    (msec)    (msec)    (
 header+=';/^   --------------- --------  ------ -------- --------- --------- ---------     ------$/d'
 header+=';/^ Summary of events:$/d'
 
+# custom tracer
+header+=';/^                                           samples  duration AVG(ns)  duration(ms)     stddev(%)$/d'
+
 rm_headers()
 {
 	sed -i "$header" $1
@@ -327,6 +337,12 @@ run_traced_microbench()
 		sandbox_opt=-s
 	fi
 
+	if $CUSTOM_TRACER; then
+		stddev_col=5
+	else
+		stddev_col=9
+	fi
+
 	echo '' > $output
 
 	syscalls_to_parse=$(echo $TRACED_SYSCALLS | sed 's/,/\n/g')
diff --git a/tools/testing/selftests/landlock/bench/tracer.c b/tools/testing/selftests/landlock/bench/tracer.c
new file mode 100644
index 000000000000..fe48e1b6db45
--- /dev/null
+++ b/tools/testing/selftests/landlock/bench/tracer.c
@@ -0,0 +1,278 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Attach BPF programs to syscall tracepoints, launch workload and show
+ * gathered statistics.
+ *
+ * Copyright © 2024 Huawei Tech. Co., Ltd.
+ */
+
+#include <bpf/libbpf.h>
+#define _GNU_SOURCE
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/types.h>
+#include <sys/syscall.h>
+#include <sys/stat.h>
+#include <sys/wait.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <assert.h>
+#include <getopt.h>
+#include <time.h>
+#include <linux/bpf.h>
+#include <math.h>
+
+#include "tracer.skel.h"
+#include "common.h"
+#include "tracer_common.h"
+
+#define ARG_MAX 32
+
+#define max(x, y) ((x) > (y) ? (x) : (y))
+#define ns2ms(ns) ((long double)(ns) / (1000 * 1000))
+
+#define pr_warn(fmt, ...) fprintf(stderr, fmt, ##__VA_ARGS__)
+
+#define SYS_ENTER_PREFIX "sys_enter_"
+#define SYS_EXIT_PREFIX "sys_exit_"
+#define SYS_PREFIX_MAXLEN sizeof(SYS_ENTER_PREFIX)
+
+#define SYS_DELIM ","
+
+static int attach_bpf_progs(struct tracer *skel, const char *strsys)
+{
+	struct bpf_link *bpf_link;
+	char strsys_buf[STRBUF_MAXLEN], *strsys_parsed, *strsys_next;
+	char str_tracepoint[STRBUF_MAXLEN + SYS_PREFIX_MAXLEN];
+
+	assert(strlen(strsys) < STRBUF_MAXLEN);
+	strcpy(strsys_buf, strsys);
+	strsys_parsed = strsys_buf;
+
+	while ((strsys_next = strsep(&strsys_parsed, SYS_DELIM))) {
+		strcpy(str_tracepoint, SYS_ENTER_PREFIX);
+		strcat(str_tracepoint, strsys_next);
+		bpf_link = bpf_program__attach_tracepoint(
+			skel->progs.sys_enter, "syscalls", str_tracepoint);
+		if (!bpf_link) {
+			pr_warn("BPF attaching failed for syscall \"%s\": %s\n",
+				strsys_next, strerror(errno));
+			goto err_out;
+		}
+
+		strcpy(str_tracepoint, SYS_EXIT_PREFIX);
+		strcat(str_tracepoint, strsys_next);
+		bpf_link = bpf_program__attach_tracepoint(
+			skel->progs.sys_exit, "syscalls", str_tracepoint);
+		if (!bpf_link) {
+			pr_warn("BPF attaching failed for syscall \"%s\": %s\n",
+				strsys_next, strerror(errno));
+			goto err_out;
+		}
+	}
+
+	return 0;
+
+err_out:
+	return 1;
+}
+
+static const char col_entity_name[] = "";
+static const char col_samples_name[] = "samples";
+static const char col_duration_avg_name[] = "duration AVG(ns)";
+static const char col_duration_name[] = "duration(ms)";
+static const char col_stddev_name[] = "stddev(%)";
+
+static const int col_entity_pad = 35;
+static const int col_samples_pad = max(14, sizeof(col_samples_name));
+static const int col_duration_avg_pad = max(12, sizeof(col_duration_avg_name));
+static const int col_duration_pad = max(13, sizeof(col_duration_name));
+static const int col_stddev_pad = max(10, sizeof(col_duration_name));
+
+void show_stat_header(FILE *output)
+{
+	fprintf(output, "%*s %*s %*s %*s %*s\n", col_entity_pad,
+		col_entity_name, col_samples_pad, col_samples_name,
+		col_duration_avg_pad, col_duration_avg_name, col_duration_pad,
+		col_duration_name, col_stddev_pad, col_stddev_name);
+}
+
+void show_syscall_stat(FILE *output, long syscall_nr,
+		       struct tracer_stat_data *stat)
+{
+	char name[STRBUF_MAXLEN];
+	double variance, variance_mean;
+	double stddev = 0;
+
+	assert(snprintf(name, sizeof(name), "syscall-%ld", syscall_nr) >= 0);
+
+	/* Cf. tools/perf/util/stat.c */
+	if (stat->samples >= 2 && stat->mean) {
+		variance = (double)stat->M2 / (stat->samples - 1);
+		variance_mean = variance / stat->samples;
+
+		stddev = 100.0 * sqrt(variance_mean) / stat->mean;
+	}
+
+	fprintf(output, "%-*s %*u %*llu %*.3Lf %*.2lf%%\n", col_entity_pad,
+		name, col_samples_pad, stat->samples, col_duration_avg_pad,
+		stat->mean, col_duration_pad - 1, ns2ms(stat->duration),
+		col_stddev_pad, stddev);
+}
+
+static int dump_bench_results(struct tracer *skel, const char *output)
+{
+	int err = 1;
+	int sys_map_fd;
+	struct tracer_stat_data sys_stat;
+	FILE *output_file;
+
+	if (!output)
+		output_file = stderr;
+	else {
+		output_file = fopen(output, "w");
+		if (!output_file) {
+			pr_warn("Failed to open output file \"%s\": %s\n",
+				output, strerror(errno));
+			goto out;
+		}
+	}
+
+	sys_map_fd = bpf_map__fd(skel->maps.sys_stats);
+	if (sys_map_fd < 0) {
+		pr_warn("Failed to get fd from BPF map \"sys_stats\"\n");
+		goto out;
+	}
+
+	show_stat_header(output_file);
+	for (int syscall_nr = 0; syscall_nr < NR_SYSCALLS; syscall_nr++) {
+		err = bpf_map__lookup_elem(skel->maps.sys_stats, &syscall_nr,
+					   sizeof(syscall_nr), &sys_stat,
+					   sizeof(sys_stat), 0);
+		if (err) {
+			pr_warn("Failed to extract stat from BPF map \"sys_stat\" for syscall[%d]\n",
+				syscall_nr);
+			goto out;
+		}
+		if (!sys_stat.samples)
+			continue;
+
+		show_syscall_stat(output_file, syscall_nr, &sys_stat);
+	}
+	err = 0;
+out:
+	if (output_file && output_file != stderr)
+		fclose(output_file);
+	return err;
+}
+
+int msleep(int msec)
+{
+	int err;
+	struct timespec ts;
+
+	ts.tv_sec = msec / 1000;
+	ts.tv_nsec = (msec % 1000) * 1000000;
+
+	do {
+		err = nanosleep(&ts, &ts);
+	} while (err && errno == EINTR);
+
+	if (err)
+		pr_warn("nanosleep failed: %s\n", strerror(errno));
+	return err;
+}
+
+// TODO: syscalls to string
+
+int main(const int argc, char *const argv[])
+{
+	int c, child, status;
+	const char *strsys = NULL, *output = NULL, *cmd;
+	struct tracer *skel = NULL;
+	int init_duration = 0;
+
+	while ((c = getopt(argc, argv, "e:o:D:h")) != -1) {
+		switch (c) {
+		case 'e':
+			strsys = optarg;
+			break;
+		case 'o':
+			output = optarg;
+			break;
+		case 'D':
+			errno = 0;
+			init_duration = atoi(optarg);
+			if (errno) {
+				pr_warn("atoi() failed on %s\n", optarg);
+				goto cleanup;
+			}
+			break;
+		case 'h':
+			pr_warn("Usage: %s [OPTIONS] [WORKLOAD_CMD]\n"
+				"Run WORKLOAD_CMD and trace number and duration of syscalls\n"
+				"\n"
+				"Options:\n"
+				"  -e TRACED_SYSCALLS  specify syscalls which would be traced while benchmarking\n"
+				"  -o FILE             redirect result output into FILE\n"
+				"  -D MSECS            wait MSECS msecs before tracing sandboxed workload\n",
+				argv[0]);
+			break;
+		}
+
+		/* Next argument is workload. */
+		if (optind < argc && *argv[optind] != '-')
+			break;
+	}
+
+	if (optind >= argc) {
+		pr_warn("Command is not specified\n");
+		goto cleanup;
+	}
+
+	if (!strsys) {
+		pr_warn("Syscall list is not specified\n");
+		goto cleanup;
+	}
+
+	skel = tracer__open_and_load();
+	if (!skel) {
+		pr_warn("BPF skeleton loading failed: %s\n", strerror(errno));
+		goto cleanup;
+	}
+
+	cmd = argv[optind];
+
+	child = fork();
+	if (child < 0) {
+		pr_warn("Failed to fork child workload process: \"%s\"\n",
+			strerror(errno));
+		goto cleanup;
+	}
+
+	if (child == 0) {
+		skel->bss->target_pid = getpid();
+
+		execv(cmd, argv + optind);
+
+		pr_warn("Failed to execute \"%s\": %s\n", cmd, strerror(errno));
+		_exit(EXIT_FAILURE);
+	}
+
+	if (msleep(init_duration))
+		goto cleanup;
+
+	if (attach_bpf_progs(skel, strsys))
+		goto cleanup;
+
+	if (waitpid(child, &status, 0) != child)
+		pr_warn("\"%s\" execution failed: %s\n", cmd, strerror(errno));
+
+	if (dump_bench_results(skel, output))
+		goto cleanup;
+	return 0;
+cleanup:
+	tracer__destroy(skel);
+	return 1;
+}
diff --git a/tools/testing/selftests/landlock/bench/tracer_common.h b/tools/testing/selftests/landlock/bench/tracer_common.h
new file mode 100644
index 000000000000..e5c9ae72632e
--- /dev/null
+++ b/tools/testing/selftests/landlock/bench/tracer_common.h
@@ -0,0 +1,15 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+#ifndef LANDLOCK_BENCH_TRACER_COMMON_H
+#define LANDLOCK_BENCH_TRACER_COMMON_H
+
+#define NR_SYSCALLS 1024
+#define MAX_TASKS 100000
+
+struct tracer_stat_data {
+	long long M2;
+	long long mean;
+	unsigned long long duration;
+	unsigned int samples;
+};
+
+#endif
diff --git a/tools/testing/selftests/landlock/config b/tools/testing/selftests/landlock/config
index e4bf29cf935f..29af19c4e9f9 100644
--- a/tools/testing/selftests/landlock/config
+++ b/tools/testing/selftests/landlock/config
@@ -1,7 +1,5 @@
 CONFIG_CGROUPS=y
 CONFIG_CGROUP_SCHED=y
-CONFIG_CMDLINE="isolcpus=0 nohz_full=0 nosmt cpufreq.off=1"
-CONFIG_CMDLINE_BOOL=y
 CONFIG_INET=y
 CONFIG_IPV6=y
 CONFIG_KEYS=y
