summaryrefslogtreecommitdiff
path: root/tools/testing/selftests
diff options
context:
space:
mode:
Diffstat (limited to 'tools/testing/selftests')
-rw-r--r--tools/testing/selftests/exec/Makefile11
-rw-r--r--tools/testing/selftests/exec/binfmt_bind_interp.c14
-rw-r--r--tools/testing/selftests/exec/binfmt_misc_bpf.c306
-rw-r--r--tools/testing/selftests/exec/binfmt_misc_common.h45
-rw-r--r--tools/testing/selftests/exec/binfmt_misc_disabled.c172
-rw-r--r--tools/testing/selftests/exec/binfmt_misc_transparent.c2
-rw-r--r--tools/testing/selftests/exec/interp_bind.bpf.c76
-rw-r--r--tools/testing/selftests/kvm/Makefile.kvm1
-rw-r--r--tools/testing/selftests/kvm/arm64/debug-exceptions.c41
-rw-r--r--tools/testing/selftests/kvm/arm64/get-reg-list.c1
-rw-r--r--tools/testing/selftests/kvm/arm64/stage2_block_transitions.c226
-rw-r--r--tools/testing/selftests/mm/memory-failure.c44
-rwxr-xr-xtools/testing/selftests/net/fib_nexthops.sh83
-rwxr-xr-xtools/testing/selftests/net/test_vxlan_nh.sh40
-rwxr-xr-xtools/testing/selftests/zram/zram_lib.sh2
15 files changed, 1009 insertions, 55 deletions
diff --git a/tools/testing/selftests/exec/Makefile b/tools/testing/selftests/exec/Makefile
index 390fe11a7bed..410c93606a0c 100644
--- a/tools/testing/selftests/exec/Makefile
+++ b/tools/testing/selftests/exec/Makefile
@@ -25,6 +25,10 @@ TEST_GEN_PROGS += check-exec
# or an 'F' entry can pin the instance that owns it. Unprivileged, no bpf.
TEST_GEN_PROGS += binfmt_misc_selfpin
+# 'D' (register disabled) binfmt_misc test: an entry that exists but does
+# not dispatch until it is enabled. Static magic entry, no bpf toolchain.
+TEST_GEN_PROGS += binfmt_misc_disabled
+
# Static ('T' flag) transparent binfmt_misc test; the asserting interpreter
# is shared with the bpf harness's transparent case. No bpf toolchain needed.
TEST_GEN_PROGS += binfmt_misc_transparent
@@ -52,8 +56,8 @@ HAVE_BPF_TOOLCHAIN ?= $(shell command -v $(CLANG) >/dev/null 2>&1 && \
ifeq ($(HAVE_BPF_TOOLCHAIN),y)
TEST_GEN_PROGS += binfmt_misc_bpf
TEST_GEN_FILES += bpf_interp.bpf.o nix_origin.bpf.o transparent.bpf.o
-TEST_GEN_FILES += loader.bpf.o
-TEST_GEN_FILES += binfmt_bpf_interp binfmt_bpf_app
+TEST_GEN_FILES += loader.bpf.o interp_bind.bpf.o
+TEST_GEN_FILES += binfmt_bpf_interp binfmt_bpf_app binfmt_bind_interp
else
$(info exec selftests: skipping binfmt_misc_bpf, needs clang, bpftool, vmlinux BTF and libbpf)
endif
@@ -123,6 +127,9 @@ $(OUTPUT)/binfmt_misc_bpf: binfmt_misc_bpf.c binfmt_misc_common.h
$(OUTPUT)/binfmt_bpf_interp: binfmt_bpf_interp.c
$(CC) $(CFLAGS) $(LDFLAGS) $< -o $@
+$(OUTPUT)/binfmt_bind_interp: binfmt_bind_interp.c
+ $(CC) $(CFLAGS) $(LDFLAGS) $< -o $@
+
$(OUTPUT)/binfmt_loader_payload: binfmt_loader_payload.c binfmt_misc_common.h
$(CC) $(CFLAGS) $(LDFLAGS) -fPIE -pie $< -o $@
diff --git a/tools/testing/selftests/exec/binfmt_bind_interp.c b/tools/testing/selftests/exec/binfmt_bind_interp.c
new file mode 100644
index 000000000000..06d65062856b
--- /dev/null
+++ b/tools/testing/selftests/exec/binfmt_bind_interp.c
@@ -0,0 +1,14 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Test interpreter for the bound-interpreter case of the binfmt_misc_bpf
+ * selftest. Two copies are installed at different paths and bound to one
+ * entry under different names; printing argv[0] - the path the kernel ran
+ * this copy under - tells the harness which of them the load program picked.
+ */
+#include <stdio.h>
+
+int main(int argc, char **argv)
+{
+ printf("BIND_RAN %s\n", argc > 0 ? argv[0] : "");
+ return 0;
+}
diff --git a/tools/testing/selftests/exec/binfmt_misc_bpf.c b/tools/testing/selftests/exec/binfmt_misc_bpf.c
index 069768a66ba0..2c7b63075f1d 100644
--- a/tools/testing/selftests/exec/binfmt_misc_bpf.c
+++ b/tools/testing/selftests/exec/binfmt_misc_bpf.c
@@ -9,7 +9,7 @@
*
* echo ':name:B::::<handler>:' > /proc/sys/fs/binfmt_misc/register
*
- * Three self-contained cases are exercised:
+ * Five self-contained cases are exercised:
*
* 1. bpf_interp: the match program matches a synthetic aarch64 ELF header
* from the prefetched bprm->buf and the load program routes it to a
@@ -26,6 +26,11 @@
* (binfmt_loader_payload) runs as the main image with the selected
* interpreter substituted for its PT_INTERP and asserts the native
* identity from inside.
+ * 5. interp_bind: an entry registered disabled with 'D' is given its
+ * interpreters one write at a time, and the load program picks one by
+ * name per exec. Replacing what the path holds afterwards changes
+ * nothing, which is the point of binding a file rather than resolving
+ * a name at exec time. Enabling the entry seals it.
*
* The first two route to a test interpreter that prints BPF_INTERP_RAN,
* proving the program's chosen interpreter actually ran.
@@ -54,6 +59,12 @@
#define TRANS_EXPECT "TRANSPARENT_OK"
#define LOADER_INTERP "/tmp/binfmt_loader_interp"
#define LOADER_PATH "/tmp/binfmt_bpf_loader.ldrtest"
+#define BIND_FIRST "/tmp/binfmt_bind_first"
+#define BIND_SECOND "/tmp/binfmt_bind_second"
+#define BIND_ARM_PATH "/tmp/binfmt_bind_arm"
+#define BIND_RISCV_PATH "/tmp/binfmt_bind_riscv"
+#define BIND_EXPECT "BIND_RAN "
+#define BIND_MAX 100
/* A minimal 64-bit little-endian ELF header, padded to the read size. */
static int create_fake_elf(const char *path, unsigned short machine)
@@ -82,11 +93,17 @@ static int create_fake_elf(const char *path, unsigned short machine)
return 0;
}
-static int register_entry(const char *name, const char *handler)
+/*
+ * Register a 'B' entry for @handler. With @flags "D" the entry is created
+ * disabled, which is what leaves it open to being given interpreters.
+ */
+static int register_entry(const char *name, const char *handler,
+ const char *flags)
{
char rule[PATH_MAX];
- snprintf(rule, sizeof(rule), ":%s:B::::%s:", name, handler);
+ snprintf(rule, sizeof(rule), ":%s:B::::%s:%s", name, handler,
+ flags ? flags : "");
return write_reg(rule);
}
@@ -106,6 +123,30 @@ static int check_output(const char *cmd, const char *expected)
return strncmp(buf, expected, strlen(expected)) ? -1 : 0;
}
+/* Does the kernel BTF know struct binfmt_misc_ops (CONFIG_BINFMT_MISC_BPF)? */
+static bool have_binfmt_misc_ops(void)
+{
+ struct btf *btf = btf__load_vmlinux_btf();
+ bool have;
+
+ have = btf && btf__find_by_name_kind(btf, "binfmt_misc_ops",
+ BTF_KIND_STRUCT) >= 0;
+ btf__free(btf);
+ return have;
+}
+
+/* The reason bpf handler cases cannot run here, NULL if they can. */
+static const char *bpf_handler_unsupported(void)
+{
+ if (getuid() != 0)
+ return "test must be run as root";
+ if (!have_binfmt_misc_ops())
+ return "no struct binfmt_misc_ops in the kernel BTF (CONFIG_BINFMT_MISC_BPF)";
+ if (!binfmt_misc_available())
+ return "no binfmt_misc";
+ return NULL;
+}
+
/* An attached handler with its 'B' entry activated. */
struct bpf_case {
struct bpf_object *obj;
@@ -115,10 +156,12 @@ struct bpf_case {
/*
* Load @objfile, attach its struct_ops map @handler (which publishes the
- * handler) and activate a 'B' entry named @entry that references it.
+ * handler) and register a 'B' entry named @entry that references it, with
+ * @flags as the entry's register-string flags.
*/
-static int bpf_case_start(struct bpf_case *c, const char *objfile,
- const char *handler, const char *entry)
+static int bpf_case_start_flags(struct bpf_case *c, const char *objfile,
+ const char *handler, const char *entry,
+ const char *flags)
{
struct bpf_map *map;
@@ -148,7 +191,7 @@ static int bpf_case_start(struct bpf_case *c, const char *objfile,
c->link = NULL;
goto fail;
}
- if (register_entry(entry, handler)) {
+ if (register_entry(entry, handler, flags)) {
fprintf(stderr, "register 'B' entry '%s' failed\n", entry);
goto fail;
}
@@ -162,6 +205,12 @@ fail:
return -1;
}
+static int bpf_case_start(struct bpf_case *c, const char *objfile,
+ const char *handler, const char *entry)
+{
+ return bpf_case_start_flags(c, objfile, handler, entry, NULL);
+}
+
static void bpf_case_stop(struct bpf_case *c)
{
unregister(c->entry);
@@ -190,23 +239,10 @@ FIXTURE(bpf_handler) {
FIXTURE_SETUP(bpf_handler)
{
char src[PATH_MAX];
- struct btf *btf;
-
- if (getuid() != 0)
- SKIP(return, "test must be run as root");
-
- /* The kernel must know struct binfmt_misc_ops (CONFIG_BINFMT_MISC_BPF). */
- btf = btf__load_vmlinux_btf();
- if (!btf || btf__find_by_name_kind(btf, "binfmt_misc_ops",
- BTF_KIND_STRUCT) < 0) {
- btf__free(btf);
- SKIP(return,
- "no struct binfmt_misc_ops in the kernel BTF (CONFIG_BINFMT_MISC_BPF)");
- }
- btf__free(btf);
+ const char *why = bpf_handler_unsupported();
- if (!binfmt_misc_available())
- SKIP(return, "no binfmt_misc");
+ if (why)
+ SKIP(return, "%s", why);
/* Shared test interpreter. */
ASSERT_EQ(artifact_path(src, sizeof(src), "binfmt_bpf_interp"), 0);
@@ -258,7 +294,7 @@ TEST_F(bpf_handler, transparent_dispatch)
char src[PATH_MAX], cmd[PATH_MAX + 16];
/* Probe for transparent-mode support via its static counterpart. */
- if (binfmt_flag_supported('T'))
+ if (!binfmt_flag_supported('T'))
SKIP(return, "kernel without transparent mode");
ASSERT_EQ(artifact_path(src, sizeof(src), "binfmt_transparent_interp"), 0);
@@ -307,4 +343,226 @@ TEST_F(bpf_handler, loader_substitution)
unlink(LOADER_INTERP);
}
+/* The errno an exec of @path fails with, 0 if it succeeded. */
+static int exec_errno(const char *path)
+{
+ int status;
+ pid_t pid;
+
+ pid = fork();
+ if (pid == 0) {
+ execl(path, path, (char *)NULL);
+ _exit(errno);
+ }
+ if (pid < 0 || waitpid(pid, &status, 0) != pid || !WIFEXITED(status))
+ return -1;
+ return WEXITSTATUS(status);
+}
+
+/* Install a copy of the bound-interpreter test binary at @path. */
+static int install_interp(const char *path)
+{
+ char src[PATH_MAX];
+
+ if (artifact_path(src, sizeof(src), "binfmt_bind_interp"))
+ return -1;
+ return copy_file(src, path);
+}
+
+/* Bind @path to @entry under @name, the '+' command of a disabled entry. */
+static int entry_bind(const char *entry, const char *name, const char *path)
+{
+ char cmd[PATH_MAX];
+
+ snprintf(cmd, sizeof(cmd), "+%s %s\n", name, path);
+ return entry_command(entry, cmd);
+}
+
+FIXTURE(bound_interp) {
+ char obj[PATH_MAX];
+ struct bpf_case c;
+ bool started;
+};
+
+FIXTURE_SETUP(bound_interp)
+{
+ const char *why = bpf_handler_unsupported();
+
+ if (why)
+ SKIP(return, "%s", why);
+ if (!binfmt_flag_supported('D')) {
+ ASSERT_EQ(errno, EINVAL);
+ SKIP(return, "kernel without the 'D' flag");
+ }
+
+ ASSERT_EQ(install_interp(BIND_FIRST), 0);
+ ASSERT_EQ(install_interp(BIND_SECOND), 0);
+
+ ASSERT_EQ(artifact_path(self->obj, sizeof(self->obj),
+ "interp_bind.bpf.o"), 0);
+
+ /*
+ * Registered disabled, so it cannot be matched yet and can still be
+ * given interpreters. Each path is resolved once, by its write(2);
+ * from here on the entry holds the files themselves.
+ */
+ ASSERT_EQ(bpf_case_start_flags(&self->c, self->obj, "interp_bind",
+ "test_interp_bind", "D"), 0);
+ self->started = true;
+
+ ASSERT_EQ(entry_bind("test_interp_bind", "first", BIND_FIRST), 0);
+ ASSERT_EQ(entry_bind("test_interp_bind", "second", BIND_SECOND), 0);
+}
+
+FIXTURE_TEARDOWN(bound_interp)
+{
+ if (self->started)
+ bpf_case_stop(&self->c);
+ unlink(BIND_FIRST);
+ unlink(BIND_SECOND);
+ unlink(AARCH64_PATH);
+ unlink(BIND_RISCV_PATH);
+ unlink(BIND_ARM_PATH);
+}
+
+/* Enabling is what makes the configured entry matchable. */
+static int activate(const char *entry)
+{
+ return entry_command(entry, "1\n");
+}
+
+/* One entry, one interpreter per guest architecture, picked per exec. */
+TEST_F(bound_interp, selects_by_name)
+{
+ ASSERT_EQ(create_fake_elf(AARCH64_PATH, EM_AARCH64), 0);
+ ASSERT_EQ(create_fake_elf(BIND_RISCV_PATH, EM_RISCV), 0);
+
+ /* Disabled, so it does not match and no format claims the binary. */
+ EXPECT_EQ(exec_errno(AARCH64_PATH), ENOEXEC);
+
+ ASSERT_EQ(activate("test_interp_bind"), 0);
+ EXPECT_EQ(check_output(AARCH64_PATH, BIND_EXPECT BIND_FIRST), 0);
+ EXPECT_EQ(check_output(BIND_RISCV_PATH, BIND_EXPECT BIND_SECOND), 0);
+}
+
+/* What was bound is what runs, whatever the path holds afterwards. */
+TEST_F(bound_interp, path_no_longer_decides)
+{
+ char other[PATH_MAX];
+
+ ASSERT_EQ(create_fake_elf(AARCH64_PATH, EM_AARCH64), 0);
+ ASSERT_EQ(activate("test_interp_bind"), 0);
+
+ /* Bound interpreters are pinned against writes, exactly like 'F'. */
+ EXPECT_TRUE(write_denied(BIND_FIRST));
+
+ /* Replace the path with a different binary: a new file, new inode. */
+ ASSERT_EQ(artifact_path(other, sizeof(other), "binfmt_bpf_interp"), 0);
+ ASSERT_EQ(unlink(BIND_FIRST), 0);
+ ASSERT_EQ(copy_file(other, BIND_FIRST), 0);
+
+ EXPECT_EQ(check_output(AARCH64_PATH, BIND_EXPECT BIND_FIRST), 0);
+}
+
+/* The entry reports what it bound, under the names it bound them as. */
+TEST_F(bound_interp, entry_reports_bindings)
+{
+ EXPECT_TRUE(entry_shows("test_interp_bind",
+ "bpf-interpreter first " BIND_FIRST));
+ EXPECT_TRUE(entry_shows("test_interp_bind",
+ "bpf-interpreter second " BIND_SECOND));
+}
+
+/* Selecting a name the entry did not bind fails the exec. */
+TEST_F(bound_interp, unbound_name_fails)
+{
+ ASSERT_EQ(create_fake_elf(BIND_ARM_PATH, EM_ARM), 0);
+ ASSERT_EQ(activate("test_interp_bind"), 0);
+
+ EXPECT_EQ(exec_errno(BIND_ARM_PATH), ENOENT);
+}
+
+/* Activating seals it: what can be matched cannot be changed. */
+TEST_F(bound_interp, sealed_once_active)
+{
+ ASSERT_EQ(activate("test_interp_bind"), 0);
+
+ EXPECT_EQ(entry_bind("test_interp_bind", "third", BIND_SECOND), -EBUSY);
+ EXPECT_FALSE(entry_shows("test_interp_bind",
+ "bpf-interpreter third " BIND_SECOND));
+}
+
+/* The seal is for good: disabling the entry again reopens nothing. */
+TEST_F(bound_interp, disable_does_not_unseal)
+{
+ ASSERT_EQ(activate("test_interp_bind"), 0);
+ ASSERT_EQ(entry_command("test_interp_bind", "0\n"), 0);
+
+ EXPECT_EQ(entry_bind("test_interp_bind", "third", BIND_SECOND), -EBUSY);
+}
+
+/* An entry registered without 'D' is sealed from the start. */
+TEST_F(bound_interp, born_sealed)
+{
+ /* A second entry for the handler the fixture already published. */
+ ASSERT_EQ(register_entry("test_born_sealed", "interp_bind", NULL), 0);
+
+ EXPECT_EQ(entry_bind("test_born_sealed", "first", BIND_FIRST), -EBUSY);
+ unregister("test_born_sealed");
+}
+
+/* A name is bound once; a second use of it is refused. */
+TEST_F(bound_interp, duplicate_name_refused)
+{
+ EXPECT_EQ(entry_bind("test_interp_bind", "first", BIND_SECOND), -EEXIST);
+}
+
+/* A name is a printable word: the entry file reports 'name path' lines. */
+TEST_F(bound_interp, name_must_be_printable)
+{
+ /* A control character would forge a line into the entry file. */
+ EXPECT_EQ(entry_bind("test_interp_bind", "a\tb", BIND_FIRST), -EINVAL);
+ EXPECT_EQ(entry_bind("test_interp_bind", "a\nb", BIND_FIRST), -EINVAL);
+
+ /* A space cannot even be spelled: the path starts after the first one. */
+ EXPECT_EQ(entry_bind("test_interp_bind", "a b", BIND_FIRST), -EINVAL);
+}
+
+/* The command ends at the write: bytes past an embedded nul are refused. */
+TEST_F(bound_interp, trailing_bytes_refused)
+{
+ char cmd[PATH_MAX];
+ size_t len;
+ int fd;
+
+ /* entry_command() cannot spell a nul, so write the buffer raw. */
+ snprintf(cmd, sizeof(cmd), "+nul %s", BIND_FIRST);
+ len = strlen(cmd) + 1;
+ memcpy(cmd + len, "junk", sizeof("junk"));
+ len += sizeof("junk");
+
+ fd = open(BINFMT_DIR "/test_interp_bind", O_WRONLY | O_CLOEXEC);
+ ASSERT_GE(fd, 0);
+ EXPECT_EQ(write(fd, cmd, len), -1);
+ EXPECT_EQ(errno, EINVAL);
+ close(fd);
+
+ EXPECT_FALSE(entry_shows("test_interp_bind",
+ "bpf-interpreter nul " BIND_FIRST));
+}
+
+/* An entry binds at most BIND_MAX interpreters. */
+TEST_F(bound_interp, capped_bindings)
+{
+ char name[16];
+ int i;
+
+ /* The fixture bound "first" and "second" already. */
+ for (i = 2; i < BIND_MAX; i++) {
+ snprintf(name, sizeof(name), "n%d", i);
+ ASSERT_EQ(entry_bind("test_interp_bind", name, BIND_FIRST), 0);
+ }
+ EXPECT_EQ(entry_bind("test_interp_bind", "over", BIND_FIRST), -ENOSPC);
+}
+
TEST_HARNESS_MAIN
diff --git a/tools/testing/selftests/exec/binfmt_misc_common.h b/tools/testing/selftests/exec/binfmt_misc_common.h
index c6900ded019f..745aff84dc78 100644
--- a/tools/testing/selftests/exec/binfmt_misc_common.h
+++ b/tools/testing/selftests/exec/binfmt_misc_common.h
@@ -93,6 +93,45 @@ static inline void unregister(const char *name)
}
}
+/* Write @line to @entry's file, reporting the errno it was refused with. */
+static inline int entry_command(const char *entry, const char *line)
+{
+ char path[PATH_MAX];
+ int fd, retval = 0;
+ size_t len = strlen(line);
+
+ snprintf(path, sizeof(path), BINFMT_DIR "/%s", entry);
+ fd = open(path, O_WRONLY | O_CLOEXEC);
+ if (fd < 0)
+ return -errno;
+ if (write(fd, line, len) != (ssize_t)len)
+ retval = -errno;
+ close(fd);
+ return retval;
+}
+
+/* Does @entry's file report @line? */
+static inline bool entry_shows(const char *entry, const char *line)
+{
+ char path[PATH_MAX], buf[PATH_MAX];
+ bool found = false;
+ FILE *fp;
+
+ snprintf(path, sizeof(path), BINFMT_DIR "/%s", entry);
+ fp = fopen(path, "r");
+ if (!fp)
+ return false;
+ while (fgets(buf, sizeof(buf), fp)) {
+ buf[strcspn(buf, "\n")] = '\0';
+ if (!strcmp(buf, line)) {
+ found = true;
+ break;
+ }
+ }
+ fclose(fp);
+ return found;
+}
+
/* Mount binfmt_misc unless it already is, and report whether it is usable. */
static inline bool binfmt_misc_available(void)
{
@@ -117,16 +156,16 @@ static inline int artifact_path(char *out, size_t sz, const char *name)
}
/* Probe kernel support for a registration flag with a throwaway entry. */
-static inline int binfmt_flag_supported(char flag)
+static inline bool binfmt_flag_supported(char flag)
{
char rule[64];
snprintf(rule, sizeof(rule), ":bm_flag_probe:E::bmprobe::/bin/true:%c",
flag);
if (write_reg(rule))
- return -1;
+ return false;
unregister("bm_flag_probe");
- return 0;
+ return true;
}
/*
diff --git a/tools/testing/selftests/exec/binfmt_misc_disabled.c b/tools/testing/selftests/exec/binfmt_misc_disabled.c
new file mode 100644
index 000000000000..47c9e8a4ee42
--- /dev/null
+++ b/tools/testing/selftests/exec/binfmt_misc_disabled.c
@@ -0,0 +1,172 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Test the 'D' (register disabled) flag of binfmt_misc. An entry
+ * registered with it exists but cannot be matched until userspace enables
+ * it, which splits a registration into create and activate.
+ *
+ * Needs root for the registration; no bpf toolchain involved.
+ */
+#define _GNU_SOURCE
+#include <stdio.h>
+#include <stdlib.h>
+
+#include "binfmt_misc_common.h"
+#include "kselftest_harness.h"
+
+#define MAGIC "#DISABLED-SELFTEST#"
+#define TARGET_PATH "/tmp/binfmt_disabled_target"
+#define INTERP_PATH "/tmp/binfmt_disabled_interp.sh"
+#define ENTRY "test_disabled"
+#define RULE(flags) ":" ENTRY ":M:0:" MAGIC "::" INTERP_PATH ":" flags
+
+/* The interpreter exits with a code the harness can recognise. */
+#define EXIT_INTERP 7
+
+/* The target only has to carry the magic; it is never actually loaded. */
+static int create_target(void)
+{
+ char buf[128] = MAGIC "\n";
+ int fd;
+
+ unlink(TARGET_PATH);
+ fd = open(TARGET_PATH, O_WRONLY | O_CREAT | O_EXCL, 0755);
+ if (fd < 0)
+ return -1;
+ if (write(fd, buf, sizeof(buf)) != (ssize_t)sizeof(buf)) {
+ close(fd);
+ return -1;
+ }
+ close(fd);
+ return 0;
+}
+
+static int create_interp(void)
+{
+ char buf[64];
+ int fd;
+
+ unlink(INTERP_PATH);
+ fd = open(INTERP_PATH, O_WRONLY | O_CREAT | O_EXCL, 0755);
+ if (fd < 0)
+ return -1;
+ snprintf(buf, sizeof(buf), "#!/bin/sh\nexit %d\n", EXIT_INTERP);
+ if (write(fd, buf, strlen(buf)) != (ssize_t)strlen(buf)) {
+ close(fd);
+ return -1;
+ }
+ return close(fd);
+}
+
+FIXTURE(disabled) {
+};
+
+FIXTURE_SETUP(disabled)
+{
+ if (getuid() != 0)
+ SKIP(return, "test must be run as root");
+ if (!binfmt_misc_available())
+ SKIP(return, "no binfmt_misc");
+
+ /* Skip the whole suite on a kernel that does not know 'D'. */
+ if (!binfmt_flag_supported('D')) {
+ ASSERT_EQ(errno, EINVAL);
+ SKIP(return, "kernel without the 'D' flag");
+ }
+
+ ASSERT_EQ(create_interp(), 0);
+ ASSERT_EQ(create_target(), 0);
+}
+
+FIXTURE_TEARDOWN(disabled)
+{
+ unregister(ENTRY);
+ unlink(TARGET_PATH);
+ unlink(INTERP_PATH);
+}
+
+/* The entry exists but does not dispatch until it is enabled. */
+TEST_F(disabled, inert_until_enabled)
+{
+ ASSERT_EQ(write_reg(RULE("D")), 0);
+ EXPECT_TRUE(entry_shows(ENTRY, "disabled"));
+
+ /* Nothing matches it, so no binary format claims the target. */
+ EXPECT_EQ(run_payload(TARGET_PATH), RUN_ENOEXEC);
+
+ ASSERT_EQ(entry_command(ENTRY, "1\n"), 0);
+ EXPECT_TRUE(entry_shows(ENTRY, "enabled"));
+ EXPECT_EQ(run_payload(TARGET_PATH), EXIT_INTERP);
+}
+
+/* Without 'D' an entry is matchable the moment it is registered. */
+TEST_F(disabled, enabled_without_the_flag)
+{
+ ASSERT_EQ(write_reg(RULE("")), 0);
+ EXPECT_TRUE(entry_shows(ENTRY, "enabled"));
+ EXPECT_EQ(run_payload(TARGET_PATH), EXIT_INTERP);
+}
+
+/* 'D' is spent on the registration: the entry does not report it back. */
+TEST_F(disabled, flag_not_reported)
+{
+ ASSERT_EQ(write_reg(RULE("D")), 0);
+ EXPECT_FALSE(entry_shows(ENTRY, "flags: D"));
+ EXPECT_TRUE(entry_shows(ENTRY, "flags: "));
+}
+
+/* A disabled entry can be disabled and enabled like any other. */
+TEST_F(disabled, toggles_like_any_entry)
+{
+ ASSERT_EQ(write_reg(RULE("D")), 0);
+
+ ASSERT_EQ(entry_command(ENTRY, "1\n"), 0);
+ ASSERT_EQ(run_payload(TARGET_PATH), EXIT_INTERP);
+ ASSERT_EQ(entry_command(ENTRY, "0\n"), 0);
+ EXPECT_EQ(run_payload(TARGET_PATH), RUN_ENOEXEC);
+ ASSERT_EQ(entry_command(ENTRY, "1\n"), 0);
+ EXPECT_EQ(run_payload(TARGET_PATH), EXIT_INTERP);
+}
+
+/* 'D' composes with the invocation flags a static entry can carry. */
+TEST_F(disabled, composes_with_invocation_flags)
+{
+ ASSERT_EQ(write_reg(RULE("PD")), 0);
+ EXPECT_TRUE(entry_shows(ENTRY, "disabled"));
+ EXPECT_TRUE(entry_shows(ENTRY, "flags: P"));
+}
+
+/* '-1' to the status file sweeps a staged entry with everything else. */
+TEST_F(disabled, removed_by_remove_all)
+{
+ int fd;
+
+ ASSERT_EQ(write_reg(RULE("D")), 0);
+ EXPECT_TRUE(entry_shows(ENTRY, "disabled"));
+
+ fd = open(BINFMT_DIR "/status", O_WRONLY | O_CLOEXEC);
+ ASSERT_GE(fd, 0);
+ ASSERT_EQ(write(fd, "-1", 2), 2);
+ close(fd);
+
+ EXPECT_NE(access(BINFMT_DIR "/" ENTRY, F_OK), 0);
+}
+
+/* A file handle held across a removal cannot resurrect the entry. */
+TEST_F(disabled, no_resurrection_after_remove)
+{
+ int fd;
+
+ ASSERT_EQ(write_reg(RULE("D")), 0);
+ fd = open(BINFMT_DIR "/" ENTRY, O_WRONLY | O_CLOEXEC);
+ ASSERT_GE(fd, 0);
+
+ ASSERT_EQ(write(fd, "-1", 2), 2);
+ EXPECT_NE(access(BINFMT_DIR "/" ENTRY, F_OK), 0);
+
+ /* Accepted like any toggle of a removed entry, but publishes nothing. */
+ EXPECT_EQ(write(fd, "1", 1), 1);
+ EXPECT_EQ(run_payload(TARGET_PATH), RUN_ENOEXEC);
+ close(fd);
+}
+
+TEST_HARNESS_MAIN
diff --git a/tools/testing/selftests/exec/binfmt_misc_transparent.c b/tools/testing/selftests/exec/binfmt_misc_transparent.c
index d0cb845df1d3..2ebf73de8018 100644
--- a/tools/testing/selftests/exec/binfmt_misc_transparent.c
+++ b/tools/testing/selftests/exec/binfmt_misc_transparent.c
@@ -56,7 +56,7 @@ FIXTURE_SETUP(transparent)
ASSERT_EQ(create_target(), 0);
/* Skip the whole suite on a kernel that does not know 'T'. */
- if (binfmt_flag_supported('T')) {
+ if (!binfmt_flag_supported('T')) {
ASSERT_EQ(errno, EINVAL);
SKIP(return, "kernel without the 'T' flag");
}
diff --git a/tools/testing/selftests/exec/interp_bind.bpf.c b/tools/testing/selftests/exec/interp_bind.bpf.c
new file mode 100644
index 000000000000..1ce45cca215f
--- /dev/null
+++ b/tools/testing/selftests/exec/interp_bind.bpf.c
@@ -0,0 +1,76 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * binfmt_misc_ops handler for the selftest's bound-interpreter case: one
+ * handler, one entry, an interpreter per guest architecture - each bound to
+ * a file when the entry was registered rather than to a path resolved at
+ * exec time. The load program names the one it wants; a name the entry did
+ * not bind fails the exec, which the harness checks too.
+ */
+#include "vmlinux.h"
+#include <bpf/bpf_helpers.h>
+#include <bpf/bpf_tracing.h>
+
+char _license[] SEC("license") = "GPL";
+
+#define EI_CLASS 4
+#define ELFCLASS64 2
+#define E_MACHINE_OFF 18
+#define EM_ARM 40
+#define EM_AARCH64 183
+#define EM_RISCV 243
+
+extern int bpf_binprm_select_interp(struct linux_binprm *bprm,
+ const char *name, size_t name__sz) __ksym;
+
+/* The guest architecture of a 64-bit ELF, or zero if it is not one. */
+static __u16 elf_machine(struct linux_binprm *bprm)
+{
+ if (bprm->buf[0] != 0x7f || bprm->buf[1] != 'E' ||
+ bprm->buf[2] != 'L' || bprm->buf[3] != 'F' ||
+ bprm->buf[EI_CLASS] != ELFCLASS64)
+ return 0;
+
+ /* Little-endian 16-bit field, read byte-wise for the verifier. */
+ return (__u8)bprm->buf[E_MACHINE_OFF] |
+ ((__u16)(__u8)bprm->buf[E_MACHINE_OFF + 1] << 8);
+}
+
+SEC("struct_ops.s/match")
+bool BPF_PROG(interp_bind_match, struct linux_binprm *bprm)
+{
+ __u16 machine = elf_machine(bprm);
+
+ return machine == EM_AARCH64 || machine == EM_RISCV ||
+ machine == EM_ARM;
+}
+
+SEC("struct_ops.s/load")
+int BPF_PROG(interp_bind_load, struct linux_binprm *bprm)
+{
+ /*
+ * Names, not paths: each one selects a file the entry pre-opened, so
+ * nothing is resolved here or later, in any namespace. The buffers
+ * are on the stack because the verifier rejects .rodata for a sized
+ * memory argument.
+ */
+ char first[] = "first";
+ char second[] = "second";
+ char unbound[] = "unbound";
+
+ switch (elf_machine(bprm)) {
+ case EM_AARCH64:
+ return bpf_binprm_select_interp(bprm, first, sizeof(first));
+ case EM_RISCV:
+ return bpf_binprm_select_interp(bprm, second, sizeof(second));
+ }
+
+ /* The entry bound nothing under this name: -ENOENT fails the exec. */
+ return bpf_binprm_select_interp(bprm, unbound, sizeof(unbound));
+}
+
+SEC(".struct_ops.link")
+struct binfmt_misc_ops interp_bind = {
+ .match = (void *)interp_bind_match,
+ .load = (void *)interp_bind_load,
+ .name = "interp_bind",
+};
diff --git a/tools/testing/selftests/kvm/Makefile.kvm b/tools/testing/selftests/kvm/Makefile.kvm
index eb8af7b8a29a..3b67feae4764 100644
--- a/tools/testing/selftests/kvm/Makefile.kvm
+++ b/tools/testing/selftests/kvm/Makefile.kvm
@@ -183,6 +183,7 @@ TEST_GEN_PROGS_arm64 += arm64/psci_test
TEST_GEN_PROGS_arm64 += arm64/sea_to_user
TEST_GEN_PROGS_arm64 += arm64/set_id_regs
TEST_GEN_PROGS_arm64 += arm64/smccc_filter
+TEST_GEN_PROGS_arm64 += arm64/stage2_block_transitions
TEST_GEN_PROGS_arm64 += arm64/vcpu_width_config
TEST_GEN_PROGS_arm64 += arm64/vgic_init
TEST_GEN_PROGS_arm64 += arm64/vgic_irq
diff --git a/tools/testing/selftests/kvm/arm64/debug-exceptions.c b/tools/testing/selftests/kvm/arm64/debug-exceptions.c
index 3eb4b1b6682d..7dc5f0b4f6ad 100644
--- a/tools/testing/selftests/kvm/arm64/debug-exceptions.c
+++ b/tools/testing/selftests/kvm/arm64/debug-exceptions.c
@@ -527,6 +527,46 @@ void test_single_step_from_userspace(int test_cnt)
kvm_vm_free(vm);
}
+static void guest_code_wp(void)
+{
+ write_data = 'x';
+ GUEST_DONE();
+}
+
+/*
+ * A userspace hardware watchpoint (KVM_GUESTDBG_USE_HW) must fire and report
+ * the accessed address in debug.arch.far, exercising the watchpoint exit path.
+ */
+static void test_watchpoint_from_userspace(void)
+{
+ struct kvm_guest_debug debug = {};
+ struct kvm_vcpu *vcpu;
+ struct kvm_run *run;
+ struct kvm_vm *vm;
+
+ vm = vm_create_with_one_vcpu(&vcpu, guest_code_wp);
+ run = vcpu->run;
+
+ debug.control = KVM_GUESTDBG_ENABLE | KVM_GUESTDBG_USE_HW;
+ debug.arch.dbg_wcr[0] = DBGWCR_LEN8 | DBGWCR_RD | DBGWCR_WR |
+ DBGWCR_EL1 | DBGWCR_E;
+ /*
+ * BAS = 0xff (LEN8) requires a doubleword-aligned DBGWVR; FAR still
+ * reports the exact accessed byte.
+ */
+ debug.arch.dbg_wvr[0] = PC(write_data) & ~7UL;
+ vcpu_guest_debug_set(vcpu, &debug);
+
+ vcpu_run(vcpu);
+ TEST_ASSERT(run->exit_reason == KVM_EXIT_DEBUG,
+ "Expected KVM_EXIT_DEBUG, got %u", run->exit_reason);
+ TEST_ASSERT((u64)run->debug.arch.far == PC(write_data),
+ "Watchpoint FAR 0x%lx != accessed address 0x%lx",
+ (u64)run->debug.arch.far, PC(write_data));
+
+ kvm_vm_free(vm);
+}
+
/*
* Run debug testing using the various breakpoint#, watchpoint# and
* context-aware breakpoint# with the given ID_AA64DFR0_EL1 configuration.
@@ -600,6 +640,7 @@ int main(int argc, char *argv[])
test_guest_debug_exceptions_all(aa64dfr0);
test_single_step_from_userspace(ss_iteration);
+ test_watchpoint_from_userspace();
return 0;
}
diff --git a/tools/testing/selftests/kvm/arm64/get-reg-list.c b/tools/testing/selftests/kvm/arm64/get-reg-list.c
index 0a3a94c4cca1..cfa99979d57c 100644
--- a/tools/testing/selftests/kvm/arm64/get-reg-list.c
+++ b/tools/testing/selftests/kvm/arm64/get-reg-list.c
@@ -532,6 +532,7 @@ static __u64 base_regs[] = {
static __u64 pmu_regs[] = {
ARM64_SYS_REG(3, 0, 9, 14, 1), /* PMINTENSET_EL1 */
ARM64_SYS_REG(3, 0, 9, 14, 2), /* PMINTENCLR_EL1 */
+ ARM64_SYS_REG(3, 0, 9, 14, 6), /* PMMIR_EL1 */
ARM64_SYS_REG(3, 3, 9, 12, 0), /* PMCR_EL0 */
ARM64_SYS_REG(3, 3, 9, 12, 1), /* PMCNTENSET_EL0 */
ARM64_SYS_REG(3, 3, 9, 12, 2), /* PMCNTENCLR_EL0 */
diff --git a/tools/testing/selftests/kvm/arm64/stage2_block_transitions.c b/tools/testing/selftests/kvm/arm64/stage2_block_transitions.c
new file mode 100644
index 000000000000..5fd47f4ada1f
--- /dev/null
+++ b/tools/testing/selftests/kvm/arm64/stage2_block_transitions.c
@@ -0,0 +1,226 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (c) 2026 Google LLC
+ * Author: Fuad Tabba <fuad.tabba@linux.dev>
+ *
+ * stage2_block_transitions - Exercise stage-2 block/page granularity changes
+ * that dirty logging forces at fault time, and assert the guest completes.
+ *
+ * Both scenarios need the fault handler to allocate at fault time (a fresh
+ * mapping and/or page-table pages while holding mmu_lock), so a fault path
+ * that fails to stage that memory manifests as a KVM_RUN error or, worse, a
+ * host crash. The asserted property is host-agnostic: the guest runs the
+ * sequence to completion and every KVM_RUN succeeds. On a pKVM host, where a
+ * non-protected guest's stage-2 faults are serviced by the pkvm_pgtable_*()
+ * backend, the same sequences also guard that backend's fault-time staging.
+ *
+ * Scenario 1 - block collapse on dirty-logging disable:
+ * A write under dirty logging installs a 4K page; GET_DIRTY_LOG
+ * re-write-protects it; logging is disabled; a second write takes a
+ * permission fault that collapses the page into a hugetlb-backed block,
+ * which requires a fresh mapping object under mmu_lock.
+ *
+ * Scenario 2 - block split under dirty logging:
+ * Several hugetlb-backed blocks are faulted in as non-executable blocks,
+ * dirty logging is enabled (write-protect only), then the guest executes
+ * into each block. Each instruction fetch takes an execute permission
+ * fault that must split the block into pages during logging, draining
+ * page-table pages. Skipped on CTR_EL0.DIC hardware, where mappings are
+ * made executable eagerly and the execute fault never occurs.
+ */
+#include <linux/bitfield.h>
+#include <linux/bitmap.h>
+#include <linux/mman.h>
+#include <linux/sizes.h>
+#include <sys/mman.h>
+
+#include <asm/sysreg.h>
+
+#include "kvm_util.h"
+#include "processor.h"
+#include "test_util.h"
+#include "ucall.h"
+
+#define DATA_SLOT 1
+#define TEST_GVA 0xc0000000UL
+#define BLOCK_SIZE SZ_2M
+
+/* AArch64 "ret" (ret x30): a self-contained, returnable executable payload. */
+#define RET_INSN 0xd65f03c0U
+
+/*
+ * A non-protected guest's per-VM stage-2 pool is seeded only with the PGD
+ * donation, which stage-2 init immediately consumes, so the page-table budget
+ * for a fault that does not top up is just the handful (~2x the stage-2 min
+ * pages) of memcache leftovers. Executing into this many distinct blocks
+ * demands far more than that budget: a fault path that tops up on every fault
+ * completes all of them, one that skips non-write faults runs out mid-sequence.
+ */
+#define NR_BLOCKS 16
+
+/* Scenario 2 guest -> host sync stages. */
+#define STAGE_SKIP_DIC 1
+#define STAGE_BLOCKS_READY 2
+
+static void collapse_guest_code(u64 gva)
+{
+ u64 *data = (u64 *)gva;
+
+ /* Under dirty logging: install a 4K writable page. */
+ WRITE_ONCE(*data, 0x1);
+ GUEST_SYNC(1);
+
+ /* Logging disabled: a permission fault collapses the page into a block. */
+ WRITE_ONCE(*data, 0x2);
+ GUEST_SYNC(2);
+
+ GUEST_DONE();
+}
+
+static void test_block_collapse(void)
+{
+ struct kvm_vcpu *vcpu;
+ unsigned long *bmap;
+ struct kvm_vm *vm;
+ struct ucall uc;
+ size_t npages;
+ u64 gpa;
+
+ vm = vm_create_with_one_vcpu(&vcpu, collapse_guest_code);
+ npages = BLOCK_SIZE / vm->page_size;
+
+ gpa = (vm_compute_max_gfn(vm) * vm->page_size) - BLOCK_SIZE;
+ gpa = align_down(gpa, BLOCK_SIZE);
+
+ vm_userspace_mem_region_add(vm, VM_MEM_SRC_ANONYMOUS_HUGETLB_2MB, gpa,
+ DATA_SLOT, npages, KVM_MEM_LOG_DIRTY_PAGES);
+ virt_map(vm, TEST_GVA, gpa, npages);
+ vcpu_args_set(vcpu, 1, TEST_GVA);
+
+ bmap = bitmap_zalloc(BLOCK_SIZE / getpagesize());
+
+ vcpu_run(vcpu);
+ TEST_ASSERT(get_ucall(vcpu, &uc) == UCALL_SYNC && uc.args[1] == 1,
+ "Expected first sync, got cmd %lu arg %lu", uc.cmd, uc.args[1]);
+
+ /* GET_DIRTY_LOG re-write-protects the dirtied page; then stop logging. */
+ kvm_vm_get_dirty_log(vm, DATA_SLOT, bmap);
+ vm_mem_region_set_flags(vm, DATA_SLOT, 0);
+
+ /* The collapsing permission fault: a broken fault path faults here. */
+ vcpu_run(vcpu);
+ TEST_ASSERT(get_ucall(vcpu, &uc) == UCALL_SYNC && uc.args[1] == 2,
+ "Expected second sync, got cmd %lu arg %lu", uc.cmd, uc.args[1]);
+
+ vcpu_run(vcpu);
+ TEST_ASSERT(get_ucall(vcpu, &uc) == UCALL_DONE,
+ "Expected done, got cmd %lu", uc.cmd);
+
+ free(bmap);
+ kvm_vm_free(vm);
+}
+
+static void guest_sync_insn(u64 va)
+{
+ /* Make the just-written instruction coherent for execution (!DIC). */
+ asm volatile("dc cvau, %0\n"
+ "dsb ish\n"
+ "ic ivau, %0\n"
+ "dsb ish\n"
+ "isb\n"
+ :: "r" (va) : "memory");
+}
+
+static void split_guest_code(u64 base_gva, u64 nblocks)
+{
+ u64 i, va;
+
+ if (FIELD_GET(CTR_EL0_DIC_MASK, read_sysreg(ctr_el0))) {
+ GUEST_SYNC(STAGE_SKIP_DIC);
+ GUEST_DONE();
+ return;
+ }
+
+ /* Fault in each block (non-executable) and stage an executable payload. */
+ for (i = 0; i < nblocks; i++) {
+ va = base_gva + i * BLOCK_SIZE;
+ WRITE_ONCE(*(u32 *)va, RET_INSN);
+ guest_sync_insn(va);
+ }
+ GUEST_SYNC(STAGE_BLOCKS_READY);
+
+ /* Logging is now on: executing into each block splits it into pages. */
+ for (i = 0; i < nblocks; i++) {
+ va = base_gva + i * BLOCK_SIZE;
+ ((void (*)(void))va)();
+ }
+
+ GUEST_DONE();
+}
+
+static void test_exec_split_drain(void)
+{
+ struct kvm_vcpu *vcpu;
+ struct kvm_vm *vm;
+ struct ucall uc;
+ size_t npages;
+ u64 gpa;
+
+ vm = vm_create_with_one_vcpu(&vcpu, split_guest_code);
+ npages = NR_BLOCKS * (BLOCK_SIZE / vm->page_size);
+
+ gpa = (vm_compute_max_gfn(vm) * vm->page_size) - NR_BLOCKS * BLOCK_SIZE;
+ gpa = align_down(gpa, BLOCK_SIZE);
+
+ vm_userspace_mem_region_add(vm, VM_MEM_SRC_ANONYMOUS_HUGETLB_2MB, gpa,
+ DATA_SLOT, npages, 0);
+ virt_map(vm, TEST_GVA, gpa, npages);
+ vcpu_args_set(vcpu, 2, TEST_GVA, (u64)NR_BLOCKS);
+
+ vcpu_run(vcpu);
+ TEST_ASSERT(get_ucall(vcpu, &uc) == UCALL_SYNC,
+ "Expected sync, got cmd %lu", uc.cmd);
+ if (uc.args[1] == STAGE_SKIP_DIC) {
+ ksft_print_msg("SKIP block split: CTR_EL0.DIC == 1\n");
+ kvm_vm_free(vm);
+ return;
+ }
+ TEST_ASSERT(uc.args[1] == STAGE_BLOCKS_READY,
+ "Expected blocks-ready sync, got arg %lu", uc.args[1]);
+
+ /* Write-protect the blocks; the guest then splits them by executing. */
+ vm_mem_region_set_flags(vm, DATA_SLOT, KVM_MEM_LOG_DIRTY_PAGES);
+
+ vcpu_run(vcpu);
+ TEST_ASSERT(get_ucall(vcpu, &uc) == UCALL_DONE,
+ "Expected done, got cmd %lu", uc.cmd);
+
+ kvm_vm_free(vm);
+}
+
+/*
+ * The explicit-size hugetlb backing hard-fails region creation if the pages
+ * are not already reserved, so probe here and skip rather than abort. The
+ * peak reservation is scenario 2's; the two scenarios run and free in turn.
+ */
+static void require_hugepages(size_t bytes)
+{
+ void *mem = mmap(NULL, bytes, PROT_READ | PROT_WRITE,
+ MAP_PRIVATE | MAP_ANONYMOUS | MAP_HUGETLB | MAP_HUGE_2MB,
+ -1, 0);
+
+ if (mem == MAP_FAILED)
+ ksft_exit_skip("Need %zu bytes of reserved 2M hugepages\n", bytes);
+ munmap(mem, bytes);
+}
+
+int main(void)
+{
+ require_hugepages(NR_BLOCKS * BLOCK_SIZE);
+
+ test_block_collapse();
+ test_exec_split_drain();
+
+ ksft_print_msg("All ok!\n");
+ return 0;
+}
diff --git a/tools/testing/selftests/mm/memory-failure.c b/tools/testing/selftests/mm/memory-failure.c
index 032ed952057c..1a5a32e22cce 100644
--- a/tools/testing/selftests/mm/memory-failure.c
+++ b/tools/testing/selftests/mm/memory-failure.c
@@ -46,7 +46,7 @@ FIXTURE(memory_failure)
unsigned long pfn;
int pagemap_fd;
int kpageflags_fd;
- bool triggered;
+ bool injection_attempted;
};
FIXTURE_VARIANT(memory_failure)
@@ -122,13 +122,6 @@ static void teardown_sighandler(void)
sigaction(SIGBUS, &sa, NULL);
}
-FIXTURE_TEARDOWN(memory_failure)
-{
- close(self->kpageflags_fd);
- close(self->pagemap_fd);
- teardown_sighandler();
-}
-
static void prepare(struct __test_metadata *_metadata, FIXTURE_DATA(memory_failure) * self,
void *vaddr)
{
@@ -200,8 +193,7 @@ static void check(struct __test_metadata *_metadata, FIXTURE_DATA(memory_failure
ASSERT_EQ(pfn_flags & KPF_HWPOISON, KPF_HWPOISON);
}
-static void cleanup(struct __test_metadata *_metadata, FIXTURE_DATA(memory_failure) * self,
- void *vaddr)
+static void cleanup(struct __test_metadata *_metadata, FIXTURE_DATA(memory_failure) * self)
{
unsigned long size;
uint64_t pfn_flags;
@@ -217,6 +209,20 @@ static void cleanup(struct __test_metadata *_metadata, FIXTURE_DATA(memory_failu
ASSERT_EQ(size, self->corrupted_size);
}
+FIXTURE_TEARDOWN(memory_failure)
+{
+ /*
+ * Injection may poison the page before failing or delivering SIGBUS, so
+ * clean up after every injection attempt.
+ */
+ if (self->injection_attempted)
+ cleanup(_metadata, self);
+
+ close(self->kpageflags_fd);
+ close(self->pagemap_fd);
+ teardown_sighandler();
+}
+
TEST_F(memory_failure, anon)
{
char *addr;
@@ -231,8 +237,8 @@ TEST_F(memory_failure, anon)
prepare(_metadata, self, addr);
ret = sigsetjmp(signal_jmp_buf, 1);
- if (!self->triggered) {
- self->triggered = true;
+ if (!self->injection_attempted) {
+ self->injection_attempted = true;
ASSERT_EQ(variant->inject(self, addr), 0);
FORCE_READ(*addr);
}
@@ -242,8 +248,6 @@ TEST_F(memory_failure, anon)
else
check(_metadata, self, addr, MADV_SOFT_ANON, ret);
- cleanup(_metadata, self, addr);
-
ASSERT_EQ(munmap(addr, self->page_size), 0);
}
@@ -296,8 +300,8 @@ TEST_F(memory_failure, clean_pagecache)
prepare(_metadata, self, addr);
ret = sigsetjmp(signal_jmp_buf, 1);
- if (!self->triggered) {
- self->triggered = true;
+ if (!self->injection_attempted) {
+ self->injection_attempted = true;
ASSERT_EQ(variant->inject(self, addr), 0);
FORCE_READ(*addr);
}
@@ -307,8 +311,6 @@ TEST_F(memory_failure, clean_pagecache)
else
check(_metadata, self, addr, MADV_SOFT_CLEAN_PAGECACHE, ret);
- cleanup(_metadata, self, addr);
-
ASSERT_EQ(munmap(addr, self->page_size), 0);
ASSERT_EQ(close(fd), 0);
@@ -337,8 +339,8 @@ TEST_F(memory_failure, dirty_pagecache)
prepare(_metadata, self, addr);
ret = sigsetjmp(signal_jmp_buf, 1);
- if (!self->triggered) {
- self->triggered = true;
+ if (!self->injection_attempted) {
+ self->injection_attempted = true;
ASSERT_EQ(variant->inject(self, addr), 0);
FORCE_READ(*addr);
}
@@ -348,8 +350,6 @@ TEST_F(memory_failure, dirty_pagecache)
else
check(_metadata, self, addr, MADV_SOFT_DIRTY_PAGECACHE, ret);
- cleanup(_metadata, self, addr);
-
ASSERT_EQ(munmap(addr, self->page_size), 0);
ASSERT_EQ(close(fd), 0);
diff --git a/tools/testing/selftests/net/fib_nexthops.sh b/tools/testing/selftests/net/fib_nexthops.sh
index ac868a731694..3d347126730a 100755
--- a/tools/testing/selftests/net/fib_nexthops.sh
+++ b/tools/testing/selftests/net/fib_nexthops.sh
@@ -30,6 +30,7 @@ IPV4_TESTS="
ipv4_large_res_grp
ipv4_compat_mode
ipv4_fdb_grp_fcnal
+ ipv4_fdb_port_fcnal
ipv4_mpath_select
ipv4_torture
ipv4_res_torture
@@ -44,6 +45,7 @@ IPV6_TESTS="
ipv6_large_res_grp
ipv6_compat_mode
ipv6_fdb_grp_fcnal
+ ipv6_fdb_port_fcnal
ipv6_mpath_select
ipv6_torture
ipv6_res_torture
@@ -432,6 +434,15 @@ check_nexthop_fdb_support()
fi
}
+check_nexthop_fdb_port_support()
+{
+ $IP nexthop help 2>&1 | grep -q "dst_port"
+ if [ $? -ne 0 ]; then
+ echo "SKIP: iproute2 too old, missing nexthop dst_port support"
+ return $ksft_skip
+ fi
+}
+
check_nexthop_res_support()
{
$IP nexthop help 2>&1 | grep -q resilient
@@ -541,6 +552,42 @@ ipv6_fdb_grp_fcnal()
$IP link del dev vx10
}
+ipv6_fdb_port_fcnal()
+{
+ echo
+ echo "IPv6 fdb nexthop dst_port functional"
+ echo "------------------------------------"
+
+ check_nexthop_fdb_port_support
+ if [ $? -eq $ksft_skip ]; then
+ return $ksft_skip
+ fi
+
+ # NHA_DST_PORT: optional per-nexthop VXLAN destination UDP port,
+ # letting an fdb nexthop group balance a flow across legs that share
+ # an underlay IP but listen on different UDP ports.
+ run_cmd "$IP nexthop add id 80 via 2001:db8:91::2 fdb dst_port 4790"
+ check_nexthop "id 80" \
+ "id 80 via 2001:db8:91::2 scope link fdb dst_port 4790"
+ log_test $? 0 "Fdb nexthop with dst_port"
+
+ run_cmd "$IP nexthop add id 81 fdb dst_port 4790"
+ log_test $? 2 "Fdb nexthop with dst_port but no gateway"
+
+ run_cmd "$IP nexthop add id 81 via 2001:db8:91::2 fdb dst_port 0"
+ log_test $? 2 "Fdb nexthop with dst_port 0"
+
+ run_cmd "$IP nexthop add id 82 via 2001:db8:91::2 fdb dst_port 4789"
+ run_cmd "$IP nexthop add id 83 via 2001:db8:91::3 fdb dst_port 5789"
+ run_cmd "$IP nexthop add id 106 group 82/83 fdb"
+ check_nexthop "id 106" "id 106 group 82/83 fdb"
+ log_test $? 0 "Fdb nexthop group with legs differing in dst_port"
+
+ run_cmd "$IP nexthop add id 84 via 2001:db8:91::2 fdb"
+ check_nexthop "id 84" "id 84 via 2001:db8:91::2 scope link fdb"
+ log_test $? 0 "Fdb nexthop without dst_port omits dst_port"
+}
+
ipv4_fdb_grp_fcnal()
{
local rc
@@ -641,6 +688,42 @@ ipv4_fdb_grp_fcnal()
$IP link del dev vx10
}
+ipv4_fdb_port_fcnal()
+{
+ echo
+ echo "IPv4 fdb nexthop dst_port functional"
+ echo "------------------------------------"
+
+ check_nexthop_fdb_port_support
+ if [ $? -eq $ksft_skip ]; then
+ return $ksft_skip
+ fi
+
+ # NHA_DST_PORT: optional per-nexthop VXLAN destination UDP port,
+ # letting an fdb nexthop group balance a flow across legs that share
+ # an underlay IP but listen on different UDP ports.
+ run_cmd "$IP nexthop add id 30 via 172.16.1.2 fdb dst_port 4790"
+ check_nexthop "id 30" \
+ "id 30 via 172.16.1.2 scope link fdb dst_port 4790"
+ log_test $? 0 "Fdb nexthop with dst_port"
+
+ run_cmd "$IP nexthop add id 31 fdb dst_port 4790"
+ log_test $? 2 "Fdb nexthop with dst_port but no gateway"
+
+ run_cmd "$IP nexthop add id 31 via 172.16.1.2 fdb dst_port 0"
+ log_test $? 2 "Fdb nexthop with dst_port 0"
+
+ run_cmd "$IP nexthop add id 32 via 172.16.1.2 fdb dst_port 4789"
+ run_cmd "$IP nexthop add id 33 via 172.16.1.3 fdb dst_port 5789"
+ run_cmd "$IP nexthop add id 105 group 32/33 fdb"
+ check_nexthop "id 105" "id 105 group 32/33 fdb"
+ log_test $? 0 "Fdb nexthop group with legs differing in dst_port"
+
+ run_cmd "$IP nexthop add id 34 via 172.16.1.2 fdb"
+ check_nexthop "id 34" "id 34 via 172.16.1.2 scope link fdb"
+ log_test $? 0 "Fdb nexthop without dst_port omits dst_port"
+}
+
ipv4_mpath_select()
{
local rc dev match h addr
diff --git a/tools/testing/selftests/net/test_vxlan_nh.sh b/tools/testing/selftests/net/test_vxlan_nh.sh
index 20f3369f776b..5ce6f27f6cf4 100755
--- a/tools/testing/selftests/net/test_vxlan_nh.sh
+++ b/tools/testing/selftests/net/test_vxlan_nh.sh
@@ -56,6 +56,17 @@ tc_stats_get()
tc_rule_handle_stats_get "dev dummy1 egress" 101 ".packets" "-n $ns1"
}
+nh_stats_get_port()
+{
+ ip -n "$ns1" -s -j nexthop show id 20 | \
+ jq ".[][\"group_stats\"][][\"packets\"]"
+}
+
+tc_stats_get_port()
+{
+ tc_rule_handle_stats_get "dev dummy1 egress" 102 ".packets" "-n $ns1"
+}
+
basic_tx_common()
{
local af_str=$1; shift
@@ -90,6 +101,31 @@ basic_tx_common()
busywait "$BUSYWAIT_TIMEOUT" until_counter_is "== 1" tc_stats_get > /dev/null
check_err $? "tc filter stats did not increase"
+ # Add a second FDB nexthop group whose nexthop carries a per-nexthop
+ # destination port (NHA_DST_PORT) that differs from the VXLAN device
+ # default. Matching outer traffic must egress with that port, so a
+ # separate flower filter keyed on the new port catches it.
+ run_cmd "tc -n $ns1 filter add dev dummy1 egress proto $proto \
+ pref 1 handle 102 flower ip_proto udp dst_ip $remote_addr \
+ dst_port 4790 action pass"
+
+ run_cmd "ip -n $ns1 nexthop add id 2 via $remote_addr fdb dst_port 4790"
+ run_cmd "ip -n $ns1 nexthop add id 20 group 2 fdb"
+
+ run_cmd "bridge -n $ns1 fdb add 00:11:22:33:44:66 dev vx0 \
+ self static nhid 20"
+
+ run_cmd "ip netns exec $ns1 mausezahn vx0 -a own \
+ -b 00:11:22:33:44:66 -c 1 -q"
+
+ busywait "$BUSYWAIT_TIMEOUT" until_counter_is "== 1" \
+ nh_stats_get_port > /dev/null
+ check_err $? "FDB nexthop group stats did not increase (with port)"
+
+ busywait "$BUSYWAIT_TIMEOUT" until_counter_is "== 1" \
+ tc_stats_get_port > /dev/null
+ check_err $? "tc filter stats did not increase (with port)"
+
log_test "VXLAN FDB nexthop: $af_str basic Tx"
}
@@ -210,8 +246,8 @@ require_command arping
require_command ndisc6
require_command jq
-if ! ip nexthop help 2>&1 | grep -q "stats"; then
- echo "SKIP: iproute2 ip too old, missing nexthop stats support"
+if ! ip nexthop help 2>&1 | grep -q "dst_port"; then
+ echo "SKIP: iproute2 ip too old, missing nexthop dst_port support"
exit "$ksft_skip"
fi
diff --git a/tools/testing/selftests/zram/zram_lib.sh b/tools/testing/selftests/zram/zram_lib.sh
index 21ec1966de76..0d44d83888f9 100755
--- a/tools/testing/selftests/zram/zram_lib.sh
+++ b/tools/testing/selftests/zram/zram_lib.sh
@@ -37,7 +37,7 @@ kernel_gte()
if [ $kernel_major -gt $major ]; then
return 0
- elif [[ $kernel_major -eq $major && $kernel_minor -ge $minor ]]; then
+ elif [ $kernel_major -eq $major ] && [ $kernel_minor -ge $minor ]; then
return 0
fi