diff mbox series

KVM: selftests: Added eBPF program and selftest to collect vmx exit stat

Message ID 20230126004346.4101944-1-chengkev@google.com (mailing list archive)
State New
Headers show
Series KVM: selftests: Added eBPF program and selftest to collect vmx exit stat | expand

Commit Message

Kevin Cheng Jan. 26, 2023, 12:43 a.m. UTC
Introduce a new selftest that loads an eBPF program that stores the
number of vmx exit counts per vcpu per vm. A process is created per
vm_create to load a separate eBPF program to collect its own stats
unique to the pid.

This test aims to serve as a proof-of-concept and example for using eBPF
to collect stats that are not provided by the other stats interfaces
such as kvm_binary_stats. Since there will be no further stats being
added to kvm_binary_stats, developers can use this selftest as a
reference for writing their own eBPF program + selftest to collect
whatever stat they may need for debugging/monitoring.

Signed-off-by: Kevin Cheng <chengkev@google.com>
---
 tools/testing/selftests/kvm/Makefile          |   4 +-
 tools/testing/selftests/kvm/build_ebpf.sh     |   5 +
 .../testing/selftests/kvm/kvm_vmx_exit_ebpf.c | 128 ++++++++++++++++++
 .../selftests/kvm/kvm_vmx_exit_ebpf_kern.c    |  74 ++++++++++
 4 files changed, 210 insertions(+), 1 deletion(-)
 create mode 100644 tools/testing/selftests/kvm/build_ebpf.sh
 create mode 100644 tools/testing/selftests/kvm/kvm_vmx_exit_ebpf.c
 create mode 100644 tools/testing/selftests/kvm/kvm_vmx_exit_ebpf_kern.c

Comments

David Matlack Feb. 6, 2023, 9:23 p.m. UTC | #1
On Thu, Jan 26, 2023 at 12:43:46AM +0000, Kevin Cheng wrote:
> Introduce a new selftest that loads an eBPF program that stores the
> number of vmx exit counts per vcpu per vm. A process is created per
> vm_create to load a separate eBPF program to collect its own stats
> unique to the pid.
> 
> This test aims to serve as a proof-of-concept and example for using eBPF
> to collect stats that are not provided by the other stats interfaces
> such as kvm_binary_stats. Since there will be no further stats being
> added to kvm_binary_stats, developers can use this selftest as a
> reference for writing their own eBPF program + selftest to collect
> whatever stat they may need for debugging/monitoring.
> 
> Signed-off-by: Kevin Cheng <chengkev@google.com>
> ---
>  tools/testing/selftests/kvm/Makefile          |   4 +-
>  tools/testing/selftests/kvm/build_ebpf.sh     |   5 +
>  .../testing/selftests/kvm/kvm_vmx_exit_ebpf.c | 128 ++++++++++++++++++
>  .../selftests/kvm/kvm_vmx_exit_ebpf_kern.c    |  74 ++++++++++

x86-specific tests should go in tools/testing/selftests/kvm/x86_64.

>  4 files changed, 210 insertions(+), 1 deletion(-)
>  create mode 100644 tools/testing/selftests/kvm/build_ebpf.sh
>  create mode 100644 tools/testing/selftests/kvm/kvm_vmx_exit_ebpf.c
>  create mode 100644 tools/testing/selftests/kvm/kvm_vmx_exit_ebpf_kern.c
> 
> diff --git a/tools/testing/selftests/kvm/Makefile b/tools/testing/selftests/kvm/Makefile
> index 1750f91dd936..d9f56ccbc7bb 100644
> --- a/tools/testing/selftests/kvm/Makefile
> +++ b/tools/testing/selftests/kvm/Makefile
> @@ -129,6 +129,7 @@ TEST_GEN_PROGS_x86_64 += set_memory_region_test
>  TEST_GEN_PROGS_x86_64 += steal_time
>  TEST_GEN_PROGS_x86_64 += kvm_binary_stats_test
>  TEST_GEN_PROGS_x86_64 += system_counter_offset_test
> +TEST_GEN_PROGS_x86_64 += kvm_vmx_exit_ebpf
>  
>  # Compiled outputs used by test targets
>  TEST_GEN_PROGS_EXTENDED_x86_64 += x86_64/nx_huge_pages_test
> @@ -176,6 +177,7 @@ TEST_GEN_PROGS_riscv += set_memory_region_test
>  TEST_GEN_PROGS_riscv += kvm_binary_stats_test
>  
>  TEST_PROGS += $(TEST_PROGS_$(ARCH_DIR))
> +TEST_PROGS := build_ebpf.sh

build_ebpf.sh is not be a test program. It should be part of this
Makefile. i.e. running

  make -C tools/testing/selftests/kvm

should build tools/lib/bpf and kvm_vmx_exit_ebpf_kern.o. Developers
can't be expected to run
tools/testing/testing/selftests/kvm/build_ebpf.sh every time they want
to build the KVM selftests.

>  TEST_GEN_PROGS += $(TEST_GEN_PROGS_$(ARCH_DIR))
>  TEST_GEN_PROGS_EXTENDED += $(TEST_GEN_PROGS_EXTENDED_$(ARCH_DIR))
>  LIBKVM += $(LIBKVM_$(ARCH_DIR))
> @@ -208,7 +210,7 @@ no-pie-option := $(call try-run, echo 'int main(void) { return 0; }' | \
>  pgste-option = $(call try-run, echo 'int main(void) { return 0; }' | \
>  	$(CC) -Werror -Wl$(comma)--s390-pgste -x c - -o "$$TMP",-Wl$(comma)--s390-pgste)
>  
> -LDLIBS += -ldl
> +LDLIBS += -ldl -L$(top_srcdir)/tools/lib/bpf -lbpf -lelf -lz

Please add a comment document why the different libraries are needed for
future readers.

>  LDFLAGS += -pthread $(no-pie-option) $(pgste-option)
>  
>  LIBKVM_C := $(filter %.c,$(LIBKVM))
> diff --git a/tools/testing/selftests/kvm/build_ebpf.sh b/tools/testing/selftests/kvm/build_ebpf.sh
> new file mode 100644
> index 000000000000..b8038b0a0da5
> --- /dev/null
> +++ b/tools/testing/selftests/kvm/build_ebpf.sh
> @@ -0,0 +1,5 @@
> +#!/bin/sh
> +# SPDX-License-Identifier: GPL-2.0
> +clang -g -O2 -target bpf -D__TARGET_ARCH_x86_64 -I . -c kvm_vmx_exit_ebpf_kern.c
> +        -o kvm_vmx_exit_ebpf_kern.o
> +make -C ../../../lib/bpf || exit

As mentioned above, this should be part of the Makefile.

> diff --git a/tools/testing/selftests/kvm/kvm_vmx_exit_ebpf.c b/tools/testing/selftests/kvm/kvm_vmx_exit_ebpf.c
> new file mode 100644
> index 000000000000..a4bd2c549207
> --- /dev/null
> +++ b/tools/testing/selftests/kvm/kvm_vmx_exit_ebpf.c
> @@ -0,0 +1,128 @@
> +// SPDX-License-Identifier: GPL-2.0-only
> +
> +#include <stdio.h>
> +#include <stdlib.h>
> +#include <signal.h>
> +#include <sys/types.h>
> +#include <sys/wait.h>
> +#include <unistd.h>
> +#include <bpf/bpf.h>
> +#include <../bpf/libbpf.h>
> +#include <linux/btf.h>
> +
> +#include "test_util.h"
> +
> +#include "kvm_util.h"
> +#include "linux/kvm.h"
> +
> +#define VCPU_ID         0
> +
> +struct stats_map_key {
> +	__u32 pid;
> +	__u32 vcpu_id;
> +	__u32 exit_reason;
> +};
> +
> +static void guest_code(void)
> +{
> +	__asm__ __volatile__("cpuid");
> +}
> +
> +int main(int argc, char **argv)
> +{
> +	if (argc < 2) {
> +		fprintf(stderr, "Expected arguments: <number_of_vms>\n");
> +		return EXIT_FAILURE;

Selftests run by default with no arguments. So please provide a default
number of VMs to run with the test. Otherwise this test will just fail
by default.

It's common (at least for me) to run all KVM selftests when submitting
patches. So having one test that always fails will be annoying to deal
with.

Also, can you provide some details (e.g. in a comment) about why a user
might want to pick a different number of VMs? What is the value of
running this test with 1 VM vs. 2 vs. 3 etc.?

> +	}
> +	int n = atoi(argv[1]);
> +
> +	for (int i = 0; i < n; i++) {
> +		if (fork() == 0) {

Put the implementation of the child process into a helper function to
reduce indentation.

> +			struct kvm_vm *vm;
> +			struct kvm_vcpu *vcpu;
> +
> +			vm = vm_create_with_one_vcpu(&vcpu, guest_code);
> +
> +			// BPF userspace code
> +			struct bpf_object *obj;
> +			struct bpf_program *prog;
> +			struct bpf_map *map_obj;
> +			struct bpf_link *link = NULL;
> +
> +			obj = bpf_object__open_file("kvm_vmx_exit_ebpf_kern.o", NULL);
> +			if (libbpf_get_error(obj)) {
> +				fprintf(stderr, "ERROR: opening BPF object file failed\n");
> +				return 0;

I notice the children and parent always return 0. The test should exit
with a non-0 return code if it fails.

> +			}
> +
> +			map_obj = bpf_object__find_map_by_name(obj, "vmx_exit_map");
> +			if (!map_obj) {
> +				fprintf(stderr, "ERROR: loading of vmx BPF map failed\n");
> +				goto cleanup;
> +			}
> +
> +			struct bpf_map *pid_map = bpf_object__find_map_by_name(obj, "pid_map");
> +
> +			if (!pid_map) {
> +				fprintf(stderr, "ERROR: loading of pid BPF map failed\n");
> +				goto cleanup;
> +			}
> +
> +			/* load BPF program */

No need for this comment. bpf_object__load() is quite obvious already :)

> +			if (bpf_object__load(obj)) {
> +				fprintf(stderr, "ERROR: loading BPF object file failed\n");
> +				goto cleanup;
> +			}
> +
> +			__u32 userspace_pid = (__u32)getpid();
> +			__u32 val = (__u32)getpid();
> +
> +			bpf_map_update_elem(bpf_map__fd(pid_map), &userspace_pid, &val, 0);
> +
> +			prog = bpf_object__find_program_by_name(obj, "bpf_exit_prog");
> +			if (libbpf_get_error(prog)) {
> +				fprintf(stderr, "ERROR: finding a prog in obj file failed\n");
> +				goto cleanup;
> +			}
> +
> +			link = bpf_program__attach(prog);
> +			if (libbpf_get_error(link)) {
> +				fprintf(stderr, "ERROR: bpf_program__attach failed\n");
> +				link = NULL;
> +				goto cleanup;
> +			}
> +
> +			for (int j = 0; j < 10000; j++)
> +				vcpu_run(vcpu);

It might be interesting to (1) add some timing around this loop and (2)
run this loop without any bpf programs attached. i.e. Automatically do
an A/B performance comparison with and without bpf programs.

> +
> +			struct stats_map_key key = {
> +				.pid = 0,
> +				.vcpu_id = 0,
> +				.exit_reason = 18,
> +			};
> +
> +
> +			struct stats_map_key next_key, lookup_key;
> +
> +			lookup_key = key;
> +			while (bpf_map_get_next_key(bpf_map__fd(map_obj), &lookup_key, &next_key)
> +				 == 0) {
> +				int count;
> +
> +				bpf_map_lookup_elem(bpf_map__fd(map_obj), &next_key, &count);
> +				fprintf(stdout, "exit reason: '%d'\ncount: %d\npid: %d\n",
> +						next_key.exit_reason, count, next_key.pid);

Instead of printing ot the count, assert that the count has the right
value.

> +				lookup_key = next_key;
> +			}
> +
> +cleanup:
> +			bpf_link__destroy(link);
> +			bpf_object__close(obj);
> +			kvm_vm_free(vm);

Shouldn't the child process exit here? Otherwise it's going to keep
looping and creating *more* children?

> +		}
> +	}
> +
> +	for (int i = 0; i < n; i++)
> +		wait(NULL);
> +	return 0;
> +}
> diff --git a/tools/testing/selftests/kvm/kvm_vmx_exit_ebpf_kern.c b/tools/testing/selftests/kvm/kvm_vmx_exit_ebpf_kern.c
> new file mode 100644
> index 000000000000..b9c076f93171
> --- /dev/null
> +++ b/tools/testing/selftests/kvm/kvm_vmx_exit_ebpf_kern.c

I think we should carve out a new directory for bpf programs. If we mix
this in with the selftest .c files, it will start to get confusing.

e.g. tools/testing/selftests/kvm/bpf/vmx_exit_count.c

Note I dropped the "kvm_" prefix since it's obvious this is a
KVM-related program since it's under the KVM selftest directory. And I
also dropped "_ebpf_kern" since that's now obvious from the fact that
this is in the bpf/ subdirectory (which should only contain bpf
programs).

> @@ -0,0 +1,73 @@
> +// SPDX-License-Identifier: GPL-2.0-only
> +#include <linux/bpf.h>
> +#include <stdint.h>
> +#include <bpf/bpf_helpers.h>
> +#include <bpf/bpf_tracing.h>
> +#include <bpf/bpf_core_read.h>
> +
> +struct kvm_vcpu {
> +	int vcpu_id;
> +};
> +
> +struct vmx_args {
> +	__u64 pad;
> +	unsigned int exit_reason;
> +	__u32 isa;
> +	struct kvm_vcpu *vcpu;
> +};
> +
> +struct stats_map_key {
> +	__u32 pid;
> +	__u32 vcpu_id;
> +	__u32 exit_reason;
> +};
> +
> +struct {
> +	__uint(type, BPF_MAP_TYPE_HASH);
> +	__uint(max_entries, 1024);
> +	__type(key, struct stats_map_key);
> +	__type(value, int);
> +} vmx_exit_map SEC(".maps");
> +
> +struct {
> +	__uint(type, BPF_MAP_TYPE_HASH);
> +	__uint(max_entries, 1);
> +	__type(key, __u32);
> +	__type(value, __u32);
> +} pid_map SEC(".maps");
> +
> +
> +SEC("tracepoint/kvm/kvm_exit")
> +int bpf_exit_prog(struct vmx_args *ctx)
> +{
> +	__u32 curr_pid = (bpf_get_current_pid_tgid() >> 32);
> +
> +	__u32 *userspace_pid = bpf_map_lookup_elem(&pid_map, &curr_pid);
> +
> +	if (!userspace_pid || *userspace_pid != curr_pid)
> +		return 0;
> +
> +	struct kvm_vcpu *vcpu = ctx->vcpu;
> +	int _vcpu_id = BPF_CORE_READ(vcpu, vcpu_id);
> +
> +	struct stats_map_key key = {
> +		.pid = (bpf_get_current_pid_tgid() >> 32),
> +		.vcpu_id = _vcpu_id,
> +		.exit_reason = ctx->exit_reason,
> +	};
> +
> +	int *value = bpf_map_lookup_elem(&vmx_exit_map, &key);
> +
> +	if (value) {
> +		*value = *value + 1;
> +		bpf_map_update_elem(&vmx_exit_map, &key, value, BPF_ANY);
> +	} else {
> +		int temp = 1;
> +
> +		bpf_map_update_elem(&vmx_exit_map, &key, &temp, BPF_ANY);
> +	}
> +
> +	return 0;
> +}
> +
> +char _license[] SEC("license") = "GPL";
> -- 
> 2.39.1.456.gfc5497dd1b-goog
>
Sean Christopherson Feb. 8, 2023, 5:19 p.m. UTC | #2
On Mon, Feb 06, 2023, David Matlack wrote:
> On Thu, Jan 26, 2023 at 12:43:46AM +0000, Kevin Cheng wrote:
> > +	int n = atoi(argv[1]);
> > +
> > +	for (int i = 0; i < n; i++) {
> > +		if (fork() == 0) {
> 
> Put the implementation of the child process into a helper function to
> reduce indentation.

+1 for the future, but for this sample test I wouldn't bother having the test
spawn multiple VMs.  IIUC, each child loads its own BPF program, i.e. the user
can achieve the same effect by running multiple instances of the test.

> > +			struct kvm_vm *vm;
> > +			struct kvm_vcpu *vcpu;
> > +
> > +			vm = vm_create_with_one_vcpu(&vcpu, guest_code);
> > +
> > +			// BPF userspace code
> > +			struct bpf_object *obj;
> > +			struct bpf_program *prog;
> > +			struct bpf_map *map_obj;
> > +			struct bpf_link *link = NULL;
> > +
> > +			obj = bpf_object__open_file("kvm_vmx_exit_ebpf_kern.o", NULL);

Does the BPF program _have_ to be in an intermediate object file?  E.g. can the
program be embedded in the selftest?

> > +			if (libbpf_get_error(obj)) {
> > +				fprintf(stderr, "ERROR: opening BPF object file failed\n");
> > +				return 0;
> 
> I notice the children and parent always return 0. The test should exit
> with a non-0 return code if it fails.

Just do TEST_ASSERT(), I don't see any reason to gracefully exit on failure.

> > +			}
> > +
> > +			map_obj = bpf_object__find_map_by_name(obj, "vmx_exit_map");
> > +			if (!map_obj) {
> > +				fprintf(stderr, "ERROR: loading of vmx BPF map failed\n");
> > +				goto cleanup;
> > +			}
> > +
> > +			struct bpf_map *pid_map = bpf_object__find_map_by_name(obj, "pid_map");
> > +
> > +			if (!pid_map) {
> > +				fprintf(stderr, "ERROR: loading of pid BPF map failed\n");
> > +				goto cleanup;
> > +			}
> > +
> > +			/* load BPF program */

...

> > +			for (int j = 0; j < 10000; j++)
> > +				vcpu_run(vcpu);
> 
> It might be interesting to (1) add some timing around this loop and (2)
> run this loop without any bpf programs attached. i.e. Automatically do
> an A/B performance comparison with and without bpf programs.

Hmm, I agree that understanding the performance impact of BPF is interesting, but
I don't think this is the right place to implement one-off code.  I would rather
we add infrastructure to allow selftests to gather timing statistics for run loops
like this, e.g. to capture percentiles, outliers, etc., and possibly to try to
mitigate external influences, e.g. pin the task to prevent migration and/or filter
out samples that appeared to be "bad" due to the task getting interrupted.

In other words, I worry that any amount of scope creep beyond "here's a BPF demo"
will snowball quickly :-)

> > diff --git a/tools/testing/selftests/kvm/kvm_vmx_exit_ebpf_kern.c b/tools/testing/selftests/kvm/kvm_vmx_exit_ebpf_kern.c
> > new file mode 100644
> > index 000000000000..b9c076f93171
> > --- /dev/null
> > +++ b/tools/testing/selftests/kvm/kvm_vmx_exit_ebpf_kern.c
> 
> I think we should carve out a new directory for bpf programs. If we mix
> this in with the selftest .c files, it will start to get confusing.
> 
> e.g. tools/testing/selftests/kvm/bpf/vmx_exit_count.c

+1, though it should probably be tools/testing/selftests/kvm/bpf/x86_64/vmx_exit_count.c
Though we should rename the arch directories before we gain more usage of the bad
names[*]

And I suspect we'll also want add lib helpers, e.g. tools/testing/selftests/kvm/lib/bpf.{c,h},
possibly with per-arch files too.  E.g. to wrap bpf_object__open_file() and
one or more bpf_object__find_map_by_name() calls.

[*] https://lore.kernel.org/all/Y5jadzKz6Qi9MiI9@google.com
Sean Christopherson Feb. 8, 2023, 5:21 p.m. UTC | #3
On Thu, Jan 26, 2023, Kevin Cheng wrote:
> diff --git a/tools/testing/selftests/kvm/kvm_vmx_exit_ebpf_kern.c b/tools/testing/selftests/kvm/kvm_vmx_exit_ebpf_kern.c
> new file mode 100644
> index 000000000000..b9c076f93171
> --- /dev/null
> +++ b/tools/testing/selftests/kvm/kvm_vmx_exit_ebpf_kern.c
> @@ -0,0 +1,73 @@
> +// SPDX-License-Identifier: GPL-2.0-only
> +#include <linux/bpf.h>
> +#include <stdint.h>
> +#include <bpf/bpf_helpers.h>
> +#include <bpf/bpf_tracing.h>
> +#include <bpf/bpf_core_read.h>
> +
> +struct kvm_vcpu {
> +	int vcpu_id;
> +};
> +
> +struct vmx_args {
> +	__u64 pad;
> +	unsigned int exit_reason;
> +	__u32 isa;
> +	struct kvm_vcpu *vcpu;
> +};
> +
> +struct stats_map_key {
> +	__u32 pid;
> +	__u32 vcpu_id;
> +	__u32 exit_reason;
> +};
> +
> +struct {
> +	__uint(type, BPF_MAP_TYPE_HASH);
> +	__uint(max_entries, 1024);
> +	__type(key, struct stats_map_key);
> +	__type(value, int);
> +} vmx_exit_map SEC(".maps");
> +
> +struct {
> +	__uint(type, BPF_MAP_TYPE_HASH);
> +	__uint(max_entries, 1);
> +	__type(key, __u32);
> +	__type(value, __u32);
> +} pid_map SEC(".maps");

Can you add comments to explain why maps are used?  From our internal discussions,
I think I know the answer, but it would be super helpful for others (and me) to
explain exactly what this code is doing, and more importantly, why.
diff mbox series

Patch

diff --git a/tools/testing/selftests/kvm/Makefile b/tools/testing/selftests/kvm/Makefile
index 1750f91dd936..d9f56ccbc7bb 100644
--- a/tools/testing/selftests/kvm/Makefile
+++ b/tools/testing/selftests/kvm/Makefile
@@ -129,6 +129,7 @@  TEST_GEN_PROGS_x86_64 += set_memory_region_test
 TEST_GEN_PROGS_x86_64 += steal_time
 TEST_GEN_PROGS_x86_64 += kvm_binary_stats_test
 TEST_GEN_PROGS_x86_64 += system_counter_offset_test
+TEST_GEN_PROGS_x86_64 += kvm_vmx_exit_ebpf
 
 # Compiled outputs used by test targets
 TEST_GEN_PROGS_EXTENDED_x86_64 += x86_64/nx_huge_pages_test
@@ -176,6 +177,7 @@  TEST_GEN_PROGS_riscv += set_memory_region_test
 TEST_GEN_PROGS_riscv += kvm_binary_stats_test
 
 TEST_PROGS += $(TEST_PROGS_$(ARCH_DIR))
+TEST_PROGS := build_ebpf.sh
 TEST_GEN_PROGS += $(TEST_GEN_PROGS_$(ARCH_DIR))
 TEST_GEN_PROGS_EXTENDED += $(TEST_GEN_PROGS_EXTENDED_$(ARCH_DIR))
 LIBKVM += $(LIBKVM_$(ARCH_DIR))
@@ -208,7 +210,7 @@  no-pie-option := $(call try-run, echo 'int main(void) { return 0; }' | \
 pgste-option = $(call try-run, echo 'int main(void) { return 0; }' | \
 	$(CC) -Werror -Wl$(comma)--s390-pgste -x c - -o "$$TMP",-Wl$(comma)--s390-pgste)
 
-LDLIBS += -ldl
+LDLIBS += -ldl -L$(top_srcdir)/tools/lib/bpf -lbpf -lelf -lz
 LDFLAGS += -pthread $(no-pie-option) $(pgste-option)
 
 LIBKVM_C := $(filter %.c,$(LIBKVM))
diff --git a/tools/testing/selftests/kvm/build_ebpf.sh b/tools/testing/selftests/kvm/build_ebpf.sh
new file mode 100644
index 000000000000..b8038b0a0da5
--- /dev/null
+++ b/tools/testing/selftests/kvm/build_ebpf.sh
@@ -0,0 +1,5 @@ 
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0
+clang -g -O2 -target bpf -D__TARGET_ARCH_x86_64 -I . -c kvm_vmx_exit_ebpf_kern.c
+        -o kvm_vmx_exit_ebpf_kern.o
+make -C ../../../lib/bpf || exit
diff --git a/tools/testing/selftests/kvm/kvm_vmx_exit_ebpf.c b/tools/testing/selftests/kvm/kvm_vmx_exit_ebpf.c
new file mode 100644
index 000000000000..a4bd2c549207
--- /dev/null
+++ b/tools/testing/selftests/kvm/kvm_vmx_exit_ebpf.c
@@ -0,0 +1,128 @@ 
+// SPDX-License-Identifier: GPL-2.0-only
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <signal.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <unistd.h>
+#include <bpf/bpf.h>
+#include <../bpf/libbpf.h>
+#include <linux/btf.h>
+
+#include "test_util.h"
+
+#include "kvm_util.h"
+#include "linux/kvm.h"
+
+#define VCPU_ID         0
+
+struct stats_map_key {
+	__u32 pid;
+	__u32 vcpu_id;
+	__u32 exit_reason;
+};
+
+static void guest_code(void)
+{
+	__asm__ __volatile__("cpuid");
+}
+
+int main(int argc, char **argv)
+{
+	if (argc < 2) {
+		fprintf(stderr, "Expected arguments: <number_of_vms>\n");
+		return EXIT_FAILURE;
+	}
+	int n = atoi(argv[1]);
+
+	for (int i = 0; i < n; i++) {
+		if (fork() == 0) {
+			struct kvm_vm *vm;
+			struct kvm_vcpu *vcpu;
+
+			vm = vm_create_with_one_vcpu(&vcpu, guest_code);
+
+			// BPF userspace code
+			struct bpf_object *obj;
+			struct bpf_program *prog;
+			struct bpf_map *map_obj;
+			struct bpf_link *link = NULL;
+
+			obj = bpf_object__open_file("kvm_vmx_exit_ebpf_kern.o", NULL);
+			if (libbpf_get_error(obj)) {
+				fprintf(stderr, "ERROR: opening BPF object file failed\n");
+				return 0;
+			}
+
+			map_obj = bpf_object__find_map_by_name(obj, "vmx_exit_map");
+			if (!map_obj) {
+				fprintf(stderr, "ERROR: loading of vmx BPF map failed\n");
+				goto cleanup;
+			}
+
+			struct bpf_map *pid_map = bpf_object__find_map_by_name(obj, "pid_map");
+
+			if (!pid_map) {
+				fprintf(stderr, "ERROR: loading of pid BPF map failed\n");
+				goto cleanup;
+			}
+
+			/* load BPF program */
+			if (bpf_object__load(obj)) {
+				fprintf(stderr, "ERROR: loading BPF object file failed\n");
+				goto cleanup;
+			}
+
+			__u32 userspace_pid = (__u32)getpid();
+			__u32 val = (__u32)getpid();
+
+			bpf_map_update_elem(bpf_map__fd(pid_map), &userspace_pid, &val, 0);
+
+			prog = bpf_object__find_program_by_name(obj, "bpf_exit_prog");
+			if (libbpf_get_error(prog)) {
+				fprintf(stderr, "ERROR: finding a prog in obj file failed\n");
+				goto cleanup;
+			}
+
+			link = bpf_program__attach(prog);
+			if (libbpf_get_error(link)) {
+				fprintf(stderr, "ERROR: bpf_program__attach failed\n");
+				link = NULL;
+				goto cleanup;
+			}
+
+			for (int j = 0; j < 10000; j++)
+				vcpu_run(vcpu);
+
+			struct stats_map_key key = {
+				.pid = 0,
+				.vcpu_id = 0,
+				.exit_reason = 18,
+			};
+
+
+			struct stats_map_key next_key, lookup_key;
+
+			lookup_key = key;
+			while (bpf_map_get_next_key(bpf_map__fd(map_obj), &lookup_key, &next_key)
+				 == 0) {
+				int count;
+
+				bpf_map_lookup_elem(bpf_map__fd(map_obj), &next_key, &count);
+				fprintf(stdout, "exit reason: '%d'\ncount: %d\npid: %d\n",
+						next_key.exit_reason, count, next_key.pid);
+				lookup_key = next_key;
+			}
+
+cleanup:
+			bpf_link__destroy(link);
+			bpf_object__close(obj);
+			kvm_vm_free(vm);
+		}
+	}
+
+	for (int i = 0; i < n; i++)
+		wait(NULL);
+	return 0;
+}
diff --git a/tools/testing/selftests/kvm/kvm_vmx_exit_ebpf_kern.c b/tools/testing/selftests/kvm/kvm_vmx_exit_ebpf_kern.c
new file mode 100644
index 000000000000..b9c076f93171
--- /dev/null
+++ b/tools/testing/selftests/kvm/kvm_vmx_exit_ebpf_kern.c
@@ -0,0 +1,73 @@ 
+// SPDX-License-Identifier: GPL-2.0-only
+#include <linux/bpf.h>
+#include <stdint.h>
+#include <bpf/bpf_helpers.h>
+#include <bpf/bpf_tracing.h>
+#include <bpf/bpf_core_read.h>
+
+struct kvm_vcpu {
+	int vcpu_id;
+};
+
+struct vmx_args {
+	__u64 pad;
+	unsigned int exit_reason;
+	__u32 isa;
+	struct kvm_vcpu *vcpu;
+};
+
+struct stats_map_key {
+	__u32 pid;
+	__u32 vcpu_id;
+	__u32 exit_reason;
+};
+
+struct {
+	__uint(type, BPF_MAP_TYPE_HASH);
+	__uint(max_entries, 1024);
+	__type(key, struct stats_map_key);
+	__type(value, int);
+} vmx_exit_map SEC(".maps");
+
+struct {
+	__uint(type, BPF_MAP_TYPE_HASH);
+	__uint(max_entries, 1);
+	__type(key, __u32);
+	__type(value, __u32);
+} pid_map SEC(".maps");
+
+
+SEC("tracepoint/kvm/kvm_exit")
+int bpf_exit_prog(struct vmx_args *ctx)
+{
+	__u32 curr_pid = (bpf_get_current_pid_tgid() >> 32);
+
+	__u32 *userspace_pid = bpf_map_lookup_elem(&pid_map, &curr_pid);
+
+	if (!userspace_pid || *userspace_pid != curr_pid)
+		return 0;
+
+	struct kvm_vcpu *vcpu = ctx->vcpu;
+	int _vcpu_id = BPF_CORE_READ(vcpu, vcpu_id);
+
+	struct stats_map_key key = {
+		.pid = (bpf_get_current_pid_tgid() >> 32),
+		.vcpu_id = _vcpu_id,
+		.exit_reason = ctx->exit_reason,
+	};
+
+	int *value = bpf_map_lookup_elem(&vmx_exit_map, &key);
+
+	if (value) {
+		*value = *value + 1;
+		bpf_map_update_elem(&vmx_exit_map, &key, value, BPF_ANY);
+	} else {
+		int temp = 1;
+
+		bpf_map_update_elem(&vmx_exit_map, &key, &temp, BPF_ANY);
+	}
+
+	return 0;
+}
+
+char _license[] SEC("license") = "GPL";