[PATCH 08/25] test/bpf_validate: add setup and basic tests
Marat Khalili
marat.khalili at huawei.com
Wed May 6 19:38:26 CEST 2026
Introduce tests for validation of specific eBPF instructions, generating
a sample eBPF program setting specified pre-conditions for the
instruction, then validating both pre- and post-conditions using step
execution of the validation over the validate debug interface.
Signed-off-by: Marat Khalili <marat.khalili at huawei.com>
---
app/test/meson.build | 1 +
app/test/test_bpf_validate.c | 908 +++++++++++++++++++++++++++++++++++
2 files changed, 909 insertions(+)
create mode 100644 app/test/test_bpf_validate.c
diff --git a/app/test/meson.build b/app/test/meson.build
index 7d458f9c079a..45a18ee68bb7 100644
--- a/app/test/meson.build
+++ b/app/test/meson.build
@@ -35,6 +35,7 @@ source_file_deps = {
'test_bitset.c': [],
'test_bitratestats.c': ['metrics', 'bitratestats', 'ethdev'] + sample_packet_forward_deps,
'test_bpf.c': ['bpf', 'net'],
+ 'test_bpf_validate.c': ['bpf'],
'test_byteorder.c': [],
'test_cfgfile.c': ['cfgfile'],
'test_cksum.c': ['net'],
diff --git a/app/test/test_bpf_validate.c b/app/test/test_bpf_validate.c
new file mode 100644
index 000000000000..20b0dfaf87b2
--- /dev/null
+++ b/app/test/test_bpf_validate.c
@@ -0,0 +1,908 @@
+/* SPDX-License-Identifier: BSD-3-Clause
+ * Copyright(c) 2025 Huawei Technologies Co., Ltd
+ */
+
+#include "test.h"
+
+#include <bpf_def.h>
+#include <rte_bpf.h>
+#include <rte_bpf_validate_debug.h>
+#include <rte_errno.h>
+
+/*
+ * Tests of BPF validation.
+ */
+
+extern int test_bpf_validate_logtype;
+#define RTE_LOGTYPE_TEST_BPF_VALIDATE test_bpf_validate_logtype
+#define TEST_LOG_LINE(level, ...) \
+ RTE_LOG_LINE(level, TEST_BPF_VALIDATE, "" __VA_ARGS__)
+
+RTE_LOG_REGISTER(test_bpf_validate_logtype, test.bpf_validate, NOTICE);
+
+/* Special value indicating that program counter variable is not being used. */
+#define NO_PROGRAM_COUNTER UINT32_MAX
+
+/* Special value indicating that register variable is not being used. */
+#define NO_REGISTER UINT8_MAX
+
+/* Sizes of text buffers used for formatting various debug outputs. */
+#define VALUE_FORMAT_BUFFER_SIZE 24
+#define INTERVAL_FORMAT_BUFFER_SIZE 64
+#define REGISTER_FORMAT_BUFFER_SIZE 256
+#define DISASSEMBLY_FORMAT_BUFFER_SIZE 64
+
+/* Interval bounded by two signed values, inclusive; min <= max. */
+struct signed_interval {
+ int64_t min;
+ int64_t max;
+};
+
+/* Interval bounded by two unsigned values, inclusive; min <= max. */
+struct unsigned_interval {
+ uint64_t min;
+ uint64_t max;
+};
+
+/*
+ * Expected interval of register values.
+ *
+ * If `is_defined` is not set, domain is considered to be unused in verification
+ * parameters (instruction is not accessing corresponding register).
+ * It's not the same as `unknown` domain which describes register that is being
+ * used but can hold any value.
+ */
+struct domain {
+ bool is_defined;
+ struct signed_interval s;
+ struct unsigned_interval u;
+};
+
+/* Expected validation state at certain point. */
+struct state {
+ /* Specifies that the branch is dynamically unreachable. */
+ bool is_unreachable;
+ struct domain dst;
+ struct domain src;
+};
+
+/* Instruction verification parameters. */
+struct verify_instruction_param {
+ struct ebpf_insn tested_instruction;
+ size_t area_size;
+ /* States just before the tested instruction, just after, or if jumped. */
+ struct state pre;
+ struct state post;
+ struct state jump;
+};
+
+/* Point (pre/post/jump) specific verification context. */
+struct point_context {
+ uint32_t program_counter;
+ uint32_t hit_count;
+ char formatted_dst[REGISTER_FORMAT_BUFFER_SIZE];
+ char formatted_src[REGISTER_FORMAT_BUFFER_SIZE];
+};
+
+/* Verification context. */
+struct verify_instruction_context {
+ struct verify_instruction_param prm;
+ /* Allocation of registers in the generated program. */
+ uint8_t base_reg;
+ uint8_t dst_reg;
+ uint8_t src_reg;
+ uint8_t tmp_reg;
+ /* Number of times invalid state callback was called. */
+ uint32_t invalid_state_count;
+ /* Contexts just before the tested instruction, just after, or if jumped. */
+ struct point_context pre;
+ struct point_context post;
+ struct point_context jump;
+};
+
+/* Domain with both signed and unsigned interval having maximum size. */
+static const struct domain unknown = {
+ .is_defined = true,
+ .s = { .min = INT64_MIN, .max = INT64_MAX },
+ .u = { .min = 0, .max = UINT64_MAX },
+};
+
+
+/* BUILDING DOMAINS */
+
+/* Create domain from singleton interval. */
+static struct domain
+make_singleton_domain(uint64_t value)
+{
+ return (struct domain){
+ .is_defined = true,
+ .s = { .min = value, .max = value },
+ .u = { .min = value, .max = value },
+ };
+}
+
+/* Create domain from signed interval. */
+static struct domain
+make_signed_domain(int64_t min, int64_t max)
+{
+ RTE_VERIFY(min <= max);
+ return (struct domain){
+ .is_defined = true,
+ .s = { .min = min, .max = max },
+ .u = (min ^ max) >= 0 ?
+ (struct unsigned_interval){ .min = min, .max = max } :
+ unknown.u,
+ };
+}
+
+/* Create domain from unsigned interval. */
+static struct domain
+make_unsigned_domain(uint64_t min, uint64_t max)
+{
+ RTE_VERIFY(min <= max);
+ return (struct domain){
+ .is_defined = true,
+ .s = (int64_t)(min ^ max) >= 0 ?
+ (struct signed_interval){ .min = min, .max = max } :
+ unknown.s,
+ .u = { .min = min, .max = max },
+ };
+}
+
+/* Return true if domain is a singleton. */
+static bool
+domain_is_singleton(const struct domain *domain)
+{
+ return domain->s.min == domain->s.max &&
+ (uint64_t)domain->s.max == domain->u.min &&
+ domain->u.min == domain->u.max;
+}
+
+/* Print error message into buffer if rc signifies error or overflow. */
+static void
+handle_format_errors(char *buffer, size_t bufsz, int rc)
+{
+ if (rc < 0)
+ snprintf(buffer, bufsz, "FORMAT ERROR %d!", -rc);
+ else if ((unsigned int)rc >= bufsz)
+ snprintf(buffer, bufsz, "FORMAT OVERFLOW!");
+}
+
+/* Format register information into provided buffer and return the buffer. */
+static const char *
+format_value(char *buffer, size_t bufsz, char format, uint64_t value)
+{
+ handle_format_errors(buffer, bufsz,
+ rte_bpf_validate_debug_format_value(buffer, bufsz, format, value));
+ return buffer;
+}
+
+/* Format register information into provided buffer and return the buffer. */
+static const char *
+format_interval(char *buffer, size_t bufsz, char format, uint64_t min, uint64_t max)
+{
+ handle_format_errors(buffer, bufsz,
+ rte_bpf_validate_debug_format_interval(buffer, bufsz, format, min, max));
+ return buffer;
+}
+
+/* Format domain information into provided buffer and return the buffer. */
+static const char *
+format_domain(char *buffer, size_t bufsz, const struct domain *domain)
+{
+ char signed_buffer[INTERVAL_FORMAT_BUFFER_SIZE];
+ char unsigned_buffer[INTERVAL_FORMAT_BUFFER_SIZE];
+
+ const int rc = !domain->is_defined ?
+ snprintf(buffer, bufsz, "UNDEFINED") :
+ snprintf(buffer, bufsz, "%s INTERSECT %s",
+ format_interval(signed_buffer, sizeof(signed_buffer), 'd',
+ domain->s.min, domain->s.max),
+ format_interval(unsigned_buffer, sizeof(unsigned_buffer), 'x',
+ domain->u.min, domain->u.max));
+
+ handle_format_errors(buffer, bufsz, rc < 0 ? -errno : rc);
+
+ return buffer;
+}
+
+/* Format register information into provided buffer and return the buffer. */
+static const char *
+format_register(struct rte_bpf_validate_debug *debug, char *buffer, size_t bufsz, uint8_t reg)
+{
+ handle_format_errors(buffer, bufsz,
+ rte_bpf_validate_debug_format_register_info(debug, buffer, bufsz, reg));
+ return buffer;
+}
+
+
+/* CHECKING REGISTER ACTUAL DOMAINS */
+
+/* Return true the specified conditional jump _may_ occur at current state. */
+static bool
+may_jump(const struct rte_bpf_validate_debug *debug,
+ const struct ebpf_insn *jump, uint64_t imm64)
+{
+ const int result = rte_bpf_validate_debug_may_jump(debug, jump, imm64);
+ RTE_VERIFY(result >= 0);
+ return (result & RTE_BPF_VALIDATE_DEBUG_MAY_BE_TRUE) != 0;
+}
+
+/* Check interval of the register interpreted as signed. */
+static int
+check_signed_interval(struct rte_bpf_validate_debug *debug,
+ uint8_t reg, struct signed_interval interval)
+{
+ char buffer[VALUE_FORMAT_BUFFER_SIZE];
+
+ TEST_ASSERT_EQUAL(may_jump(debug,
+ &(struct ebpf_insn){
+ .code = (BPF_JMP | EBPF_JSLT | BPF_K),
+ .dst_reg = reg,
+ }, interval.min),
+ false,
+ "r%hhu s< %s is impossible", reg,
+ format_value(buffer, sizeof(buffer), 'd', interval.min));
+
+ TEST_ASSERT_EQUAL(may_jump(debug,
+ &(struct ebpf_insn){
+ .code = (BPF_JMP | BPF_JEQ | BPF_K),
+ .dst_reg = reg,
+ }, interval.min),
+ true,
+ "r%hhu == %s is possible", reg,
+ format_value(buffer, sizeof(buffer), 'd', interval.min));
+
+ TEST_ASSERT_EQUAL(may_jump(debug,
+ &(struct ebpf_insn){
+ .code = (BPF_JMP | BPF_JEQ | BPF_K),
+ .dst_reg = reg,
+ }, interval.max),
+ true,
+ "r%hhu == %s is possible", reg,
+ format_value(buffer, sizeof(buffer), 'd', interval.max));
+
+ TEST_ASSERT_EQUAL(may_jump(debug,
+ &(struct ebpf_insn){
+ .code = (BPF_JMP | EBPF_JSGT | BPF_K),
+ .dst_reg = reg,
+ }, interval.max),
+ false,
+ "r%hhu s> %s is impossible", reg,
+ format_value(buffer, sizeof(buffer), 'd', interval.max));
+
+ return TEST_SUCCESS;
+}
+
+/* Check interval of the register interpreted as unsigned. */
+static int
+check_unsigned_interval(struct rte_bpf_validate_debug *debug,
+ uint8_t reg, struct unsigned_interval interval)
+{
+ char buffer[VALUE_FORMAT_BUFFER_SIZE];
+
+ TEST_ASSERT_EQUAL(may_jump(debug,
+ &(struct ebpf_insn){
+ .code = (BPF_JMP | EBPF_JLT | BPF_K),
+ .dst_reg = reg,
+ }, interval.min),
+ false,
+ "r%hhu u< %s is impossible", reg,
+ format_value(buffer, sizeof(buffer), 'x', interval.min));
+
+ TEST_ASSERT_EQUAL(may_jump(debug,
+ &(struct ebpf_insn){
+ .code = (BPF_JMP | BPF_JEQ | BPF_K),
+ .dst_reg = reg,
+ }, interval.min),
+ true,
+ "r%hhu == %s is possible", reg,
+ format_value(buffer, sizeof(buffer), 'x', interval.min));
+
+ TEST_ASSERT_EQUAL(may_jump(debug,
+ &(struct ebpf_insn){
+ .code = (BPF_JMP | BPF_JEQ | BPF_K),
+ .dst_reg = reg,
+ }, interval.max),
+ true,
+ "r%hhu == %s is possible", reg,
+ format_value(buffer, sizeof(buffer), 'x', interval.max));
+
+ TEST_ASSERT_EQUAL(may_jump(debug,
+ &(struct ebpf_insn){
+ .code = (BPF_JMP | BPF_JGT | BPF_K),
+ .dst_reg = reg,
+ }, interval.max),
+ false,
+ "r%hhu u> %s is impossible", reg,
+ format_value(buffer, sizeof(buffer), 'x', interval.max));
+
+ return TEST_SUCCESS;
+}
+
+/* Check domain of the register interpreted as value. */
+static int
+check_domain_impl(struct rte_bpf_validate_debug *debug, uint8_t reg,
+ const struct domain *domain)
+{
+ TEST_ASSERT_SUCCESS(
+ check_signed_interval(debug, reg, domain->s),
+ "signed interval check");
+
+ TEST_ASSERT_SUCCESS(
+ check_unsigned_interval(debug, reg, domain->u),
+ "unsigned interval check");
+
+ return TEST_SUCCESS;
+}
+
+/* Check domain of the register and format the values in case of an error. */
+static int
+check_domain(struct rte_bpf_validate_debug *debug, uint8_t reg,
+ const struct domain *domain)
+{
+ char buffer[REGISTER_FORMAT_BUFFER_SIZE];
+
+ const int rc = check_domain_impl(debug, reg, domain);
+
+ if (rc != TEST_SUCCESS) {
+ TEST_LOG_LINE(WARNING, "\tExpected: r%hhu = %s", reg,
+ format_domain(buffer, sizeof(buffer), domain));
+
+ TEST_LOG_LINE(WARNING, "\tFound: r%hhu = %s", reg,
+ format_register(debug, buffer, sizeof(buffer), reg));
+ }
+
+ return rc;
+}
+
+
+/* GENERATING TEST PROGRAM */
+
+static bool
+fits_in_imm32(int64_t value)
+{
+ return value >= INT32_MIN && value <= INT32_MAX;
+}
+
+/* Load constant into the register. */
+static void
+load_constant(struct ebpf_insn **ins, uint8_t reg, int64_t value)
+{
+ if (fits_in_imm32(value)) {
+ *(*ins)++ = (struct ebpf_insn){
+ .code = (EBPF_ALU64 | EBPF_MOV | BPF_K),
+ .dst_reg = reg,
+ .imm = (int32_t)value,
+ };
+ } else {
+ /* Load imm64 into tmp_reg using wide load, lower bits first... */
+ *(*ins)++ = (struct ebpf_insn){
+ .code = (BPF_LD | BPF_IMM | EBPF_DW),
+ .dst_reg = reg,
+ .imm = (uint32_t)value,
+ };
+ /* ... then higher bits. */
+ *(*ins)++ = (struct ebpf_insn){
+ .imm = (uint32_t)(value >> 32),
+ };
+ }
+}
+
+/*
+ * Compare specified register to value and jump.
+ *
+ * Jump offset is not filled and should be patched in by the caller.
+ */
+static void
+compare_and_jump(struct ebpf_insn **ins, uint8_t op, uint8_t reg,
+ int64_t value, uint8_t tmp_reg)
+{
+ if (fits_in_imm32(value)) {
+ /* Jump on specified condition between reg and immediate. */
+ *(*ins)++ = (struct ebpf_insn){
+ .code = (BPF_JMP | op | BPF_K),
+ .dst_reg = reg,
+ .imm = value,
+ };
+ } else {
+ /* Load value into tmp_reg. */
+ load_constant(ins, tmp_reg, value);
+
+ /* Jump on specified condition between reg and tmp_reg. */
+ *(*ins)++ = (struct ebpf_insn){
+ .code = (BPF_JMP | op | BPF_X),
+ .dst_reg = reg,
+ .src_reg = tmp_reg,
+ };
+ }
+}
+
+/*
+ * Prepare register to be in the specified domain.
+ *
+ * Unless singleton, load unknown value into it and clamp it with conditional jumps.
+ * (Jump offsets are not filled and should be patched in by the caller.)
+ */
+static void
+prepare_domain(struct ebpf_insn **ins, uint8_t reg,
+ const struct domain *domain, uint8_t base_reg, int *service_cell_count,
+ uint8_t tmp_reg)
+{
+ if (domain_is_singleton(domain)) {
+ /* Don't need any uncertainty for a singleton. */
+ load_constant(ins, reg, domain->s.min);
+ return;
+ }
+
+ /* Load value from memory area into the register. */
+ *(*ins)++ = (struct ebpf_insn){
+ .code = (BPF_LDX | EBPF_DW | BPF_MEM),
+ .dst_reg = reg,
+ .src_reg = base_reg,
+ .off = sizeof(uint64_t) * (*service_cell_count)++,
+ };
+
+ /*
+ * Use both signed and unsigned conditions, even if redundant.
+ * It makes it more robust if conditional jump verification itself
+ * contains bugs like not updating the other type of interval.
+ * Jump instructions themselves can be tested separately to catch
+ * these bugs, this preparation phase is not a test for them.
+ */
+ if (domain->u.min > unknown.u.min)
+ compare_and_jump(ins, EBPF_JLT, reg, domain->u.min, tmp_reg);
+ if (domain->u.max < unknown.u.max)
+ compare_and_jump(ins, BPF_JGT, reg, domain->u.max, tmp_reg);
+ if (domain->s.min > unknown.s.min)
+ compare_and_jump(ins, EBPF_JSLT, reg, domain->s.min, tmp_reg);
+ if (domain->s.max < unknown.s.max)
+ compare_and_jump(ins, EBPF_JSGT, reg, domain->s.max, tmp_reg);
+}
+
+static void
+fill_verify_instruction_defaults(struct verify_instruction_param *prm)
+{
+
+ if (BPF_CLASS(prm->tested_instruction.code) != BPF_JMP)
+ prm->jump.is_unreachable = true;
+
+ RTE_VERIFY(!prm->pre.is_unreachable);
+ if (prm->post.is_unreachable) {
+ RTE_VERIFY(!prm->post.dst.is_defined);
+ RTE_VERIFY(!prm->post.src.is_defined);
+ } else {
+ if (!prm->post.dst.is_defined)
+ prm->post.dst = prm->pre.dst;
+ if (!prm->post.src.is_defined)
+ prm->post.src = prm->pre.src;
+ }
+
+ if (prm->jump.is_unreachable) {
+ RTE_VERIFY(!prm->jump.dst.is_defined);
+ RTE_VERIFY(!prm->jump.src.is_defined);
+ } else {
+ if (!prm->jump.dst.is_defined)
+ prm->jump.dst = prm->pre.dst;
+ if (!prm->jump.src.is_defined)
+ prm->jump.src = prm->pre.src;
+ }
+}
+
+/* Generate program for the tested instruction and domains from the context.
+ *
+ * Return number of instructions.
+ *
+ * Destination and source registers in tested_instruction should not be specified,
+ * they are filled in by the function as long as domains for them are specified.
+ * Jump offset should not be specified, it is filled in by the function.
+ *
+ * If `pre.dst` or `pre.src` domain is not defined, corresponding register
+ * is not prepared.
+ *
+ * For non-jump instructions `jump.is_unreachable` is always set automatically.
+ *
+ * If any of the post or jump domains are not defined, they are copied from src
+ * unless corresponding branch is unreachable.
+ *
+ * Memory area size is automatically expanded to have enough space for loading
+ * unknown dst and src register values, thus testing sizes less than 16 bytes is
+ * not guaranteed.
+ *
+ * Limitations:
+ * - Support for jump instructions is incomplete (e.g. exit, ja).
+ * - Wide instructions are not supported yet.
+ */
+static uint32_t
+generate_program(struct verify_instruction_context *ctx, struct ebpf_insn *ins)
+{
+ struct ebpf_insn *const ins_buf = ins;
+ /* Number of double words used for service purposes. */
+ int service_cell_count = 0;
+
+ /* Make sure we actually support provided instruction. */
+ switch (BPF_CLASS(ctx->prm.tested_instruction.code)) {
+ case BPF_LD:
+ /* Wide instructions are not supported yet. */
+ RTE_VERIFY(!rte_bpf_insn_is_wide(&ctx->prm.tested_instruction));
+ break;
+ }
+
+ fill_verify_instruction_defaults(&ctx->prm);
+
+ /* Allocate registers, base_reg is received as program argument. */
+ ctx->base_reg = EBPF_REG_1;
+ ctx->dst_reg = (ctx->prm.pre.dst.is_defined || ctx->prm.post.dst.is_defined ||
+ ctx->prm.jump.dst.is_defined) ? EBPF_REG_2 : NO_REGISTER;
+ ctx->src_reg = (ctx->prm.pre.src.is_defined || ctx->prm.post.src.is_defined ||
+ ctx->prm.jump.src.is_defined) ? EBPF_REG_3 : NO_REGISTER;
+ ctx->tmp_reg = EBPF_REG_4;
+
+ /* Clear r0 to make it eligible as a return value. */
+ load_constant(&ins, EBPF_REG_0, 0);
+
+ /* Fill dst register in the instruction if defined anywhere, prepare if needed. */
+ if (ctx->dst_reg != NO_REGISTER) {
+ RTE_VERIFY(ctx->prm.tested_instruction.dst_reg == 0);
+ ctx->prm.tested_instruction.dst_reg = ctx->dst_reg;
+
+ if (ctx->prm.pre.dst.is_defined)
+ prepare_domain(&ins, ctx->dst_reg, &ctx->prm.pre.dst,
+ ctx->base_reg, &service_cell_count, ctx->tmp_reg);
+ else
+ TEST_LOG_LINE(DEBUG, "Not preparing undefined r%hhu", ctx->dst_reg);
+ }
+
+ /* Fill src register in the instruction if defined anywhere, prepare if needed. */
+ if (ctx->src_reg != NO_REGISTER) {
+ RTE_VERIFY(ctx->prm.tested_instruction.src_reg == 0);
+ ctx->prm.tested_instruction.src_reg = ctx->src_reg;
+
+ if (ctx->prm.pre.src.is_defined)
+ prepare_domain(&ins, ctx->src_reg, &ctx->prm.pre.src,
+ ctx->base_reg, &service_cell_count, ctx->tmp_reg);
+ else
+ TEST_LOG_LINE(DEBUG, "Not preparing undefined r%hhu", ctx->src_reg);
+ }
+
+ /* Automatically increase area size if needed. */
+ ctx->prm.area_size = RTE_MAX(ctx->prm.area_size, service_cell_count * sizeof(uint64_t));
+
+ /* Issue tested instruction. */
+ ctx->pre.program_counter = ins - ins_buf;
+ *ins++ = ctx->prm.tested_instruction;
+
+ /* Issue post instruction (for setting post breakpoint). */
+ ctx->post.program_counter = ins - ins_buf;
+ load_constant(&ins, EBPF_REG_0, 1);
+
+ /* Issue jump branch for the jump instruction, even if dynamically unreachable. */
+ if (BPF_CLASS(ctx->prm.tested_instruction.code) != BPF_JMP)
+ ctx->jump.program_counter = NO_PROGRAM_COUNTER;
+ else {
+ /* Finish previous branch by issuing exit. */
+ *ins++ = (struct ebpf_insn){ .code = (BPF_JMP | EBPF_EXIT) };
+
+ /* Issue jump target instruction (for setting jump breakpoint). */
+ ctx->jump.program_counter = ins - ins_buf;
+ load_constant(&ins, EBPF_REG_0, 2);
+
+ /* Patch jump in tested jump instruction. */
+ RTE_VERIFY(ins_buf[ctx->pre.program_counter].off == 0);
+ ins_buf[ctx->pre.program_counter].off =
+ ctx->jump.program_counter - ctx->post.program_counter;
+ }
+
+ /* Issue exit instruction. */
+ const uint32_t exit_pc = ins - ins_buf;
+ *ins++ = (struct ebpf_insn){ .code = (BPF_JMP | EBPF_EXIT) };
+
+ /* Patch all jumps to point to exit. */
+ for (uint32_t pc = 0; pc != ctx->pre.program_counter; ++pc)
+ if (BPF_CLASS(ins_buf[pc].code) == BPF_JMP) {
+ RTE_ASSERT(ins_buf[pc].off == 0);
+ ins_buf[pc].off = exit_pc - (pc + 1);
+ }
+
+ const uint32_t nb_ins = ins - ins_buf;
+ return nb_ins;
+}
+
+
+/* VERIFICATION OF AN ARBITRARY INSTRUCTION */
+
+/* Invoked when invalid state is detected. */
+static int
+invalid_state_cb(struct rte_bpf_validate_debug *debug, void *void_ctx)
+{
+ struct verify_instruction_context *const ctx = void_ctx;
+
+ ++ctx->invalid_state_count;
+
+ TEST_LOG_LINE(WARNING,
+ "Invalid state detected at pc %u",
+ rte_bpf_validate_debug_get_pc(debug));
+
+ RTE_SET_USED(debug);
+
+ return TEST_SUCCESS;
+}
+
+static int
+point_callback(struct rte_bpf_validate_debug *debug, const struct verify_instruction_context *ctx,
+ struct point_context *point_ctx, const struct state *state)
+{
+ TEST_ASSERT_EQUAL(point_ctx->hit_count, 0, "not called before");
+
+ const uint32_t pc = rte_bpf_validate_debug_get_pc(debug);
+ TEST_ASSERT_EQUAL(pc, point_ctx->program_counter,
+ "Expected program counter: %" PRIu32 ", found: %" PRIu32,
+ point_ctx->program_counter, pc);
+
+ if (ctx->dst_reg != NO_REGISTER) {
+ format_register(debug, point_ctx->formatted_dst,
+ sizeof(point_ctx->formatted_dst), ctx->dst_reg);
+
+ if (state->dst.is_defined) {
+ TEST_ASSERT_SUCCESS(
+ check_domain(debug, ctx->dst_reg, &state->dst),
+ "dst domain check");
+ TEST_LOG_LINE(DEBUG, "Successfully checked r%hhu.", ctx->dst_reg);
+ } else
+ TEST_LOG_LINE(DEBUG, "Not checking undefined r%hhu.", ctx->dst_reg);
+ }
+
+ if (ctx->src_reg != NO_REGISTER) {
+ format_register(debug, point_ctx->formatted_src,
+ sizeof(point_ctx->formatted_src), ctx->src_reg);
+
+ if (state->src.is_defined) {
+ TEST_ASSERT_SUCCESS(
+ check_domain(debug, ctx->src_reg, &state->src),
+ "src domain check");
+ TEST_LOG_LINE(DEBUG, "Successfully checked r%hhu.", ctx->src_reg);
+ } else
+ TEST_LOG_LINE(DEBUG, "Not checking undefined r%hhu.", ctx->src_reg);
+ }
+
+ ++point_ctx->hit_count;
+
+ return TEST_SUCCESS;
+}
+
+/*
+ * Invoked before the tested instruction and checks pre-conditions.
+ *
+ * Also formats registers in the pre state for postmortem, if needed.
+ */
+static int
+pre_callback(struct rte_bpf_validate_debug *debug, void *void_ctx)
+{
+ struct verify_instruction_context *const ctx = void_ctx;
+
+ TEST_LOG_LINE(DEBUG, "Pre callback invoked.");
+
+ TEST_ASSERT_SUCCESS(
+ point_callback(debug, ctx, &ctx->pre, &ctx->prm.pre),
+ "pre-state check");
+
+ return TEST_SUCCESS;
+}
+
+/* Invoked after the tested instruction and checks post-conditions. */
+static int
+post_callback(struct rte_bpf_validate_debug *debug, void *void_ctx)
+{
+ struct verify_instruction_context *const ctx = void_ctx;
+
+ TEST_LOG_LINE(DEBUG, "Post callback invoked.");
+
+ TEST_ASSERT_SUCCESS(
+ point_callback(debug, ctx, &ctx->post, &ctx->prm.post),
+ "post-state check");
+
+ return TEST_SUCCESS;
+}
+
+/* Invoked after the tested instruction jumped and checks jump post-conditions. */
+static int
+jump_callback(struct rte_bpf_validate_debug *debug, void *void_ctx)
+{
+ struct verify_instruction_context *const ctx = void_ctx;
+
+ TEST_LOG_LINE(DEBUG, "Jump callback invoked.");
+
+ TEST_ASSERT_SUCCESS(
+ point_callback(debug, ctx, &ctx->jump, &ctx->prm.jump),
+ "jump-state check");
+
+ return TEST_SUCCESS;
+}
+
+static int
+debug_validation(struct verify_instruction_context *ctx, const struct ebpf_insn *ins,
+ uint32_t nb_ins)
+{
+ struct rte_bpf_validate_debug *const debug = rte_bpf_validate_debug_create();
+ TEST_ASSERT_NOT_NULL(debug, "validate debug create error %d", rte_errno);
+
+ const struct rte_bpf_prm_ex prm = {
+ .sz = sizeof(struct rte_bpf_prm_ex),
+ .origin = RTE_BPF_ORIGIN_RAW,
+ .raw.ins = ins,
+ .raw.nb_ins = nb_ins,
+ .prog_arg[0] = {
+ .type = RTE_BPF_ARG_PTR,
+ .size = ctx->prm.area_size,
+ },
+ .nb_prog_arg = 1,
+ .debug = debug,
+ };
+
+ /* Catch invalid states. */
+ TEST_ASSERT_NOT_NULL(rte_bpf_validate_debug_catch(debug,
+ RTE_BPF_VALIDATE_DEBUG_EVENT_INVALID_STATE,
+ &(struct rte_bpf_validate_debug_callback){
+ .fn = invalid_state_cb,
+ .ctx = ctx,
+ }), "add catchpoint error %d", rte_errno);
+
+ /* Break on pre test instruction. */
+ TEST_ASSERT_NOT_NULL(rte_bpf_validate_debug_break(debug, ctx->pre.program_counter,
+ &(struct rte_bpf_validate_debug_callback){
+ .fn = pre_callback,
+ .ctx = ctx,
+ }), "add pre breakpoint error %d", rte_errno);
+
+ /* Break on post test instruction. */
+ TEST_ASSERT_NOT_NULL(rte_bpf_validate_debug_break(debug, ctx->post.program_counter,
+ &(struct rte_bpf_validate_debug_callback){
+ .fn = post_callback,
+ .ctx = ctx,
+ }), "add post breakpoint error %d", rte_errno);
+
+ if (ctx->jump.program_counter != NO_PROGRAM_COUNTER)
+ /* Break on jump test instruction. */
+ TEST_ASSERT_NOT_NULL(rte_bpf_validate_debug_break(debug, ctx->jump.program_counter,
+ &(struct rte_bpf_validate_debug_callback){
+ .fn = jump_callback,
+ .ctx = ctx,
+ }), "add jump breakpoint error %d", rte_errno);
+
+ struct rte_bpf *const bpf = rte_bpf_load_ex(&prm);
+ const int validation_errno = rte_errno;
+
+ rte_bpf_destroy(bpf);
+ rte_bpf_validate_debug_destroy(debug);
+
+ TEST_ASSERT_NOT_NULL(bpf, "validation error %d", validation_errno);
+
+ TEST_ASSERT_EQUAL(ctx->pre.hit_count, !ctx->prm.pre.is_unreachable,
+ "pre hit_count = %d", ctx->pre.hit_count);
+ TEST_ASSERT_EQUAL(ctx->post.hit_count, !ctx->prm.post.is_unreachable,
+ "post hit_count = %d", ctx->post.hit_count);
+ TEST_ASSERT_EQUAL(ctx->jump.hit_count, !ctx->prm.jump.is_unreachable,
+ "jump hit_count = %d", ctx->jump.hit_count);
+
+ return TEST_SUCCESS;
+}
+
+/* Dump whole program to log. */
+static void
+log_program_dump(const struct ebpf_insn *ins, uint32_t nb_ins, uint32_t pre_pc)
+{
+ char hexadecimal[DISASSEMBLY_FORMAT_BUFFER_SIZE];
+ char disassembly[DISASSEMBLY_FORMAT_BUFFER_SIZE];
+
+ TEST_LOG_LINE(NOTICE, "\tTested program:");
+ for (uint32_t pc = 0; pc != nb_ins; ++pc) {
+ rte_bpf_format(hexadecimal, sizeof(hexadecimal), &ins[pc], pc,
+ RTE_BPF_FORMAT_FLAG_HEXADECIMAL |
+ RTE_BPF_FORMAT_FLAG_NEVER_WIDE);
+ rte_bpf_format(disassembly, sizeof(disassembly), &ins[pc], pc,
+ RTE_BPF_FORMAT_FLAG_DISASSEMBLY |
+ RTE_BPF_FORMAT_FLAG_ABSOLUTE_JUMPS);
+ TEST_LOG_LINE(NOTICE, "\t%5u: \t%s \t%s%s",
+ pc, hexadecimal, disassembly,
+ pc != pre_pc ? "" : " ; tested instruction");
+
+ if (!rte_bpf_insn_is_wide(&ins[pc]))
+ continue;
+
+ ++pc;
+
+ rte_bpf_format(hexadecimal, sizeof(hexadecimal), &ins[pc], pc,
+ RTE_BPF_FORMAT_FLAG_HEXADECIMAL |
+ RTE_BPF_FORMAT_FLAG_NEVER_WIDE);
+ TEST_LOG_LINE(NOTICE, "\t%6s \t%s", "", hexadecimal);
+ }
+}
+
+static void
+log_formatted_registers(const char *heading, const struct verify_instruction_context *ctx,
+ const struct point_context *point_ctx)
+{
+ char register_name[8];
+
+ TEST_LOG_LINE(NOTICE, "\t%s", heading);
+ if (ctx->dst_reg != NO_REGISTER) {
+ snprintf(register_name, sizeof(register_name), "r%hhu", ctx->dst_reg);
+ TEST_LOG_LINE(NOTICE, "\t%5s: \t%s", register_name, point_ctx->formatted_dst);
+ }
+ if (ctx->src_reg != NO_REGISTER) {
+ snprintf(register_name, sizeof(register_name), "r%hhu", ctx->src_reg);
+ TEST_LOG_LINE(NOTICE, "\t%5s: \t%s", register_name, point_ctx->formatted_src);
+ }
+}
+
+/*
+ * Verify instruction validation behaviour described by prm.
+ *
+ * Generate the program containing specified instruction on the code path with
+ * specified register pre-domains and verify specified register post-domains.
+ *
+ * See comment to `generate_program` for more requirements and limitations.
+ */
+static int
+verify_instruction(struct verify_instruction_param prm)
+{
+ struct verify_instruction_context ctx = {
+ .prm = prm,
+ };
+ struct ebpf_insn ins_buf[64];
+
+ const uint32_t nb_ins = generate_program(&ctx, ins_buf);
+ RTE_ASSERT(nb_ins <= RTE_DIM(ins_buf));
+
+ const int rc = debug_validation(&ctx, ins_buf, nb_ins);
+
+ /* Log more data at DEBUG level on success, NOTICE on failure. */
+ if (rte_log_can_log(RTE_LOGTYPE_TEST_BPF_VALIDATE, RTE_LOG_DEBUG) ||
+ rc != TEST_SUCCESS) {
+ log_program_dump(ins_buf, nb_ins, ctx.pre.program_counter);
+ log_formatted_registers("Pre-state:", &ctx, &ctx.pre);
+ log_formatted_registers("Post-state:", &ctx, &ctx.post);
+ if (ctx.jump.program_counter != NO_PROGRAM_COUNTER)
+ log_formatted_registers("Jump-state:", &ctx, &ctx.jump);
+ }
+
+ return rc;
+}
+
+
+/* TESTS FOR SPECIFIC INSTRUCTIONS */
+
+/* 64-bit addition of immediate to a range. */
+static int
+test_alu64_add_k(void)
+{
+ return verify_instruction((struct verify_instruction_param){
+ .tested_instruction = {
+ .code = (EBPF_ALU64 | BPF_ADD | BPF_K),
+ .imm = 17,
+ },
+ .pre.dst = make_signed_domain(11, 29),
+ .post.dst = make_signed_domain(11 + 17, 29 + 17),
+ });
+}
+
+REGISTER_FAST_TEST(bpf_validate_alu64_add_k_autotest, NOHUGE_OK, ASAN_OK,
+ test_alu64_add_k);
+
+/* Jump if greater than immediate. */
+static int
+test_jmp64_jeq_k(void)
+{
+ return verify_instruction((struct verify_instruction_param){
+ .tested_instruction = {
+ .code = (BPF_JMP | BPF_JGT | BPF_K),
+ .imm = 0,
+ },
+ .pre.dst = make_unsigned_domain(0, 1),
+ .post.dst = make_singleton_domain(0),
+ .jump.dst = make_singleton_domain(1),
+ });
+}
+
+REGISTER_FAST_TEST(bpf_validate_jmp64_jeq_k_autotest, NOHUGE_OK, ASAN_OK,
+ test_jmp64_jeq_k);
--
2.43.0
More information about the dev
mailing list