diff options
Diffstat (limited to 'tools/testing/selftests')
| -rw-r--r-- | tools/testing/selftests/exec/Makefile | 11 | ||||
| -rw-r--r-- | tools/testing/selftests/exec/binfmt_bind_interp.c | 14 | ||||
| -rw-r--r-- | tools/testing/selftests/exec/binfmt_misc_bpf.c | 306 | ||||
| -rw-r--r-- | tools/testing/selftests/exec/binfmt_misc_common.h | 45 | ||||
| -rw-r--r-- | tools/testing/selftests/exec/binfmt_misc_disabled.c | 172 | ||||
| -rw-r--r-- | tools/testing/selftests/exec/binfmt_misc_transparent.c | 2 | ||||
| -rw-r--r-- | tools/testing/selftests/exec/interp_bind.bpf.c | 76 | ||||
| -rw-r--r-- | tools/testing/selftests/kvm/Makefile.kvm | 1 | ||||
| -rw-r--r-- | tools/testing/selftests/kvm/arm64/debug-exceptions.c | 41 | ||||
| -rw-r--r-- | tools/testing/selftests/kvm/arm64/get-reg-list.c | 1 | ||||
| -rw-r--r-- | tools/testing/selftests/kvm/arm64/stage2_block_transitions.c | 226 | ||||
| -rw-r--r-- | tools/testing/selftests/mm/memory-failure.c | 44 | ||||
| -rwxr-xr-x | tools/testing/selftests/net/fib_nexthops.sh | 83 | ||||
| -rwxr-xr-x | tools/testing/selftests/net/test_vxlan_nh.sh | 40 | ||||
| -rwxr-xr-x | tools/testing/selftests/zram/zram_lib.sh | 2 |
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 |
