Add a test that verifies that eBPF validator generates callbacks with the number, order and program counter specified in the documentation.
Signed-off-by: Marat Khalili <[email protected]> --- app/test/test_bpf_validate.c | 341 ++++++++++++++++++++++++++++++++++- 1 file changed, 337 insertions(+), 4 deletions(-) diff --git a/app/test/test_bpf_validate.c b/app/test/test_bpf_validate.c index 066f1fa156ae..ea4621b257f4 100644 --- a/app/test/test_bpf_validate.c +++ b/app/test/test_bpf_validate.c @@ -9,10 +9,6 @@ #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, ...) \ @@ -26,6 +22,343 @@ RTE_LOG_REGISTER(test_bpf_validate_logtype, test.bpf_validate, NOTICE); /* Special value indicating that register variable is not being used. */ #define NO_REGISTER UINT8_MAX +static const char *const event_names[RTE_BPF_VALIDATE_DEBUG_EVENT_END] = { + [RTE_BPF_VALIDATE_DEBUG_EVENT_STEP] = "step", + [RTE_BPF_VALIDATE_DEBUG_EVENT_INVALID_STATE] = "invalid-state", + [RTE_BPF_VALIDATE_DEBUG_EVENT_VALIDATION_START] = "validation-start", + [RTE_BPF_VALIDATE_DEBUG_EVENT_VALIDATION_SUCCESS] = "validation-success", + [RTE_BPF_VALIDATE_DEBUG_EVENT_VALIDATION_FAILURE] = "validation-failure", + [RTE_BPF_VALIDATE_DEBUG_EVENT_BRANCH_ENTER] = "branch-enter", + [RTE_BPF_VALIDATE_DEBUG_EVENT_BRANCH_PRUNE] = "branch-prune", + [RTE_BPF_VALIDATE_DEBUG_EVENT_BRANCH_RETURN] = "branch-return", + [RTE_BPF_VALIDATE_DEBUG_EVENT_BRANCH_UNREACHABLE] = "branch-unreachable", + [RTE_BPF_VALIDATE_DEBUG_EVENT_JUMP_ALWAYS] = "jump-always", + [RTE_BPF_VALIDATE_DEBUG_EVENT_JUMP_CONDITIONAL] = "jump-conditional", +}; + +/* + * Self-test of BPF validation debug events. + */ + +#define EVENT_BIT(name) RTE_BIT64(RTE_BPF_VALIDATE_DEBUG_EVENT_ ## name) +#define EVENT_BITS_ALL RTE_LEN2MASK(RTE_BPF_VALIDATE_DEBUG_EVENT_END, uint64_t) + +struct test_events_context { + /* Tested program. */ + const struct ebpf_insn *ins; + uint32_t nb_ins; + /* Number of times each of the events was encountered. */ + int event_counts[RTE_BPF_VALIDATE_DEBUG_EVENT_END]; + /* Events active within the current step. */ + uint64_t active_events; + /* Breakpoint was called within the current step. */ + bool breakpoint_called; + /* Currently active conditional jump pc. */ + uint32_t branch_pc; + /* Program counters of all conditional jumps in the current code path. */ + uint32_t branch_pc_stack[4]; + size_t branch_pc_stack_length; +}; + +/* Push current branch_pc to stack and clear it. */ +static int +test_events_branch_pc_push(struct test_events_context *ctx) +{ + TEST_ASSERT_NOT_EQUAL(ctx->branch_pc, NO_PROGRAM_COUNTER, + "no branch pc to push"); + TEST_ASSERT(ctx->branch_pc_stack_length < RTE_DIM(ctx->branch_pc_stack), + "branch stack overflow"); + ctx->branch_pc_stack[ctx->branch_pc_stack_length++] = ctx->branch_pc; + ctx->branch_pc = NO_PROGRAM_COUNTER; + return TEST_SUCCESS; +} + +/* Pop branch_pc from stack. */ +static int +test_events_branch_pc_pop(struct test_events_context *ctx) +{ + TEST_ASSERT(ctx->branch_pc_stack_length > 0, "branch stack underflow"); + ctx->branch_pc = ctx->branch_pc_stack[--ctx->branch_pc_stack_length]; + return TEST_SUCCESS; +} + +/* Verify breakpoint, checking that it is only called once and remembering it. */ +static int +test_events_break_cb(struct rte_bpf_validate_debug *debug, void *void_ctx) +{ + struct test_events_context *const ctx = void_ctx; + const uint32_t pc = rte_bpf_validate_debug_get_pc(debug); + + TEST_LOG_LINE(DEBUG, "%2u: break", pc); + + TEST_ASSERT(!ctx->breakpoint_called, + "breakpoint called again at pc %u", pc); + ctx->breakpoint_called = true; + + return TEST_SUCCESS; +} + +/* + * Verify event, checking that: + * - validation-start is called at pc 0; + * - branch-return is called at a corresponding conditional jump pc; + * - branch-enter and branch-return are correctly paired; + * - jump-conditional and jump-always are called for corresponding opcodes; + * - jump-conditional and only it is followed by branches events; + * - events and breakpoints are correctly ordered; + * + * Increment event counter to compare against expected counts later. + */ +static int +test_events_catch_cb(struct rte_bpf_validate_debug *debug, void *void_ctx) +{ + /* Bitmask of events allowed to precede each given event within the step. */ + static const uint64_t allowed_preceding_events[] = { + [RTE_BPF_VALIDATE_DEBUG_EVENT_VALIDATION_START] = 0, + [RTE_BPF_VALIDATE_DEBUG_EVENT_BRANCH_PRUNE] = + EVENT_BIT(BRANCH_PRUNE) | + EVENT_BIT(BRANCH_UNREACHABLE) | + EVENT_BIT(BRANCH_RETURN), + [RTE_BPF_VALIDATE_DEBUG_EVENT_BRANCH_UNREACHABLE] = + EVENT_BIT(BRANCH_PRUNE) | + EVENT_BIT(BRANCH_UNREACHABLE) | + EVENT_BIT(BRANCH_RETURN), + [RTE_BPF_VALIDATE_DEBUG_EVENT_BRANCH_ENTER] = + EVENT_BIT(BRANCH_PRUNE) | + EVENT_BIT(BRANCH_UNREACHABLE) | + EVENT_BIT(BRANCH_RETURN), + [RTE_BPF_VALIDATE_DEBUG_EVENT_BRANCH_RETURN] = + EVENT_BIT(BRANCH_PRUNE) | + EVENT_BIT(BRANCH_UNREACHABLE) | + EVENT_BIT(BRANCH_RETURN), + [RTE_BPF_VALIDATE_DEBUG_EVENT_JUMP_CONDITIONAL] = + EVENT_BIT(BRANCH_ENTER) | + EVENT_BIT(BRANCH_PRUNE) | + EVENT_BIT(BRANCH_UNREACHABLE) | + EVENT_BIT(BRANCH_RETURN), + [RTE_BPF_VALIDATE_DEBUG_EVENT_JUMP_ALWAYS] = + EVENT_BIT(BRANCH_ENTER) | + EVENT_BIT(BRANCH_PRUNE) | + EVENT_BIT(BRANCH_UNREACHABLE) | + EVENT_BIT(BRANCH_RETURN), + [RTE_BPF_VALIDATE_DEBUG_EVENT_STEP] = + EVENT_BITS_ALL - EVENT_BIT(VALIDATION_SUCCESS), + [RTE_BPF_VALIDATE_DEBUG_EVENT_VALIDATION_SUCCESS] = + EVENT_BIT(BRANCH_PRUNE) | + EVENT_BIT(BRANCH_UNREACHABLE) | + EVENT_BIT(BRANCH_RETURN), + }; + + struct test_events_context *const ctx = void_ctx; + const enum rte_bpf_validate_debug_event event = + rte_bpf_validate_debug_get_event(debug); + const uint32_t pc = rte_bpf_validate_debug_get_pc(debug); + + TEST_LOG_LINE(DEBUG, "%2u: %s", pc, event_names[event]); + + /* Verify event program counter and underlying instruction. */ + switch (event) { + case RTE_BPF_VALIDATE_DEBUG_EVENT_VALIDATION_START: + TEST_ASSERT_EQUAL(pc, 0, + "event %s called at pc %u", + event_names[event], pc); + break; + case RTE_BPF_VALIDATE_DEBUG_EVENT_BRANCH_ENTER: + TEST_ASSERT_SUCCESS(test_events_branch_pc_push(ctx), + "error pushing branch for event %s at pc %u", + event_names[event], pc); + break; + case RTE_BPF_VALIDATE_DEBUG_EVENT_BRANCH_RETURN: + TEST_ASSERT_SUCCESS(test_events_branch_pc_pop(ctx), + "error popping branch for event %s at pc %u", + event_names[event], pc); + TEST_ASSERT_EQUAL(pc, ctx->branch_pc, + "event %s called at pc %u after jump pc %u", + event_names[event], pc, ctx->branch_pc); + break; + case RTE_BPF_VALIDATE_DEBUG_EVENT_JUMP_ALWAYS: + TEST_ASSERT_EQUAL(BPF_CLASS(ctx->ins[pc].code), BPF_JMP, + "event %s called at non-jump pc %u", + event_names[event], pc); + TEST_ASSERT_EQUAL(BPF_OP(ctx->ins[pc].code), BPF_JA, + "event %s called at non-jump-always pc %u", + event_names[event], pc); + break; + case RTE_BPF_VALIDATE_DEBUG_EVENT_JUMP_CONDITIONAL: + TEST_ASSERT_EQUAL(BPF_CLASS(ctx->ins[pc].code), BPF_JMP, + "event %s called at non-jump pc %u", + event_names[event], pc); + TEST_ASSERT_NOT_EQUAL(BPF_OP(ctx->ins[pc].code), BPF_JA, + "event %s called at jump-always pc %u", + event_names[event], pc); + break; + default: + break; + } + + /* Verify events order. */ + TEST_ASSERT_EQUAL( + (ctx->active_events & ~allowed_preceding_events[event]), 0, + "event %s at pc %u preceded by %#jx", + event_names[event], pc, (uintmax_t)ctx->active_events); + + /* Update events for the current step as well as branch program counter. */ + if (event != RTE_BPF_VALIDATE_DEBUG_EVENT_STEP) { + TEST_ASSERT(!ctx->breakpoint_called, + "event %s called after a breakpoint at pc %u", + event_names[event], pc); + ctx->active_events |= RTE_BIT64(event); + } else { + TEST_ASSERT_EQUAL(ctx->branch_pc, NO_PROGRAM_COUNTER, + "no branch-enter between branch- and next instruction"); + + if (ctx->active_events & EVENT_BIT(JUMP_CONDITIONAL)) + /* Remember branch instruction pointer, branches to follow. */ + ctx->branch_pc = pc; + + TEST_ASSERT(ctx->breakpoint_called, + "step called before or without a breakpoint at pc %u", pc); + + /* Prepare for the next step. */ + ctx->active_events = 0; + ctx->breakpoint_called = false; + } + + /* Update total event counts. */ + ++ctx->event_counts[event]; + + return TEST_SUCCESS; +} + +/* + * This test verifies that eBPF validator generates expected numbers of events + * of each kind in the expected order (e.g. step after jump after branch). + */ +static int +test_events(void) +{ + /* + * Program demonstrating the following properties: + * - prunnable code paths; + * - dynamically unreachable code paths; + * - conditional jumps landing on conditional jumps; + * - conditional jumps landing on unconditional jumps; + */ + static const struct ebpf_insn ins[] = { + /* 0: r0 = 0 */ + { .code = (EBPF_ALU64 | EBPF_MOV | BPF_K), .dst_reg = EBPF_REG_0, .imm = 0 }, + /* 1: if r1 > 0 goto 4 */ + { .code = (BPF_JMP | BPF_JGT | BPF_K), .dst_reg = EBPF_REG_1, .imm = 0, .off = 2 }, + /* 2: r1 = 0 */ + { .code = (EBPF_ALU64 | EBPF_MOV | BPF_K), .dst_reg = EBPF_REG_1, .imm = 0 }, + /* 3: if r0 < 1 goto 8 */ + { .code = (BPF_JMP | EBPF_JLT | BPF_K), .dst_reg = EBPF_REG_0, .imm = 1, .off = 4 }, + /* 4: if r1 > 0 goto 6 */ + { .code = (BPF_JMP | BPF_JGT | BPF_K), .dst_reg = EBPF_REG_1, .imm = 0, .off = 1 }, + /* 5: goto 9 */ + { .code = (BPF_JMP | BPF_JA), .off = 3 }, + /* 6: r1 = 0 */ + { .code = (EBPF_ALU64 | EBPF_MOV | BPF_K), .dst_reg = EBPF_REG_1, .imm = 0 }, + /* 7: if r0 > 0 goto 9 */ + { .code = (BPF_JMP | BPF_JGT | BPF_K), .dst_reg = EBPF_REG_0, .imm = 0, .off = 1 }, + /* 8: goto 10 */ + { .code = (BPF_JMP | BPF_JA), .off = 1 }, + /* 9: r0 = 1 */ + { .code = (EBPF_ALU64 | EBPF_MOV | BPF_K), .dst_reg = EBPF_REG_0, .imm = 1 }, + /* 10: exit */ + { .code = (BPF_JMP | EBPF_EXIT) } + }; + + /* + * Verified code paths (order may vary by library implementation): + * 0 -- 1 -- 4 -- 6 -- 7 -- unreachable 9 + * | | `-- 8 -- 10 -- return 7 -- return 4 + * | `-- unreachable 5 -- return 1 + * `-- 2 -- 3 -- prune 8 + * `-- unreachable 4 -- return 1 -- success 11 + * + * Note that 4 encountered conditional jump instructions generate: + * - 4 pairs of branch-enter/branch-return events; + * - 3 branch-unreachable events; + * - 1 branch-prune event; + * for a total count of 8 (4 instructions * 2 branches/instruction). + */ + static const int expected_counts[RTE_BPF_VALIDATE_DEBUG_EVENT_END] = { + [RTE_BPF_VALIDATE_DEBUG_EVENT_STEP] = 9, + [RTE_BPF_VALIDATE_DEBUG_EVENT_VALIDATION_START] = 1, + [RTE_BPF_VALIDATE_DEBUG_EVENT_VALIDATION_SUCCESS] = 1, + [RTE_BPF_VALIDATE_DEBUG_EVENT_BRANCH_ENTER] = 4, + [RTE_BPF_VALIDATE_DEBUG_EVENT_BRANCH_PRUNE] = 1, + [RTE_BPF_VALIDATE_DEBUG_EVENT_BRANCH_RETURN] = 4, + [RTE_BPF_VALIDATE_DEBUG_EVENT_BRANCH_UNREACHABLE] = 3, + [RTE_BPF_VALIDATE_DEBUG_EVENT_JUMP_ALWAYS] = 1, + [RTE_BPF_VALIDATE_DEBUG_EVENT_JUMP_CONDITIONAL] = 4, + }; + + struct test_events_context ctx = { + .ins = ins, + .nb_ins = RTE_DIM(ins), + .branch_pc = NO_PROGRAM_COUNTER, + }; + + struct rte_bpf_validate_debug *debug = rte_bpf_validate_debug_create(); + TEST_ASSERT_NOT_NULL(debug, "validate debug create error"); + + /* Set breakpoints for all instructions. */ + for (uint32_t pc = 0; pc != RTE_DIM(ins); ++pc) + rte_bpf_validate_debug_break(debug, pc, + &(struct rte_bpf_validate_debug_callback){ + .fn = test_events_break_cb, + .ctx = &ctx, + }); + + /* Set catchpoints for all events except invalid-state. */ + for (enum rte_bpf_validate_debug_event event = 0; + event != RTE_BPF_VALIDATE_DEBUG_EVENT_END; ++event) + if (event != RTE_BPF_VALIDATE_DEBUG_EVENT_INVALID_STATE) + rte_bpf_validate_debug_catch(debug, event, + &(struct rte_bpf_validate_debug_callback){ + .fn = test_events_catch_cb, + .ctx = &ctx, + }); + + struct rte_bpf *const bpf = rte_bpf_load_ex(&(struct rte_bpf_prm_ex){ + .sz = sizeof(struct rte_bpf_prm_ex), + .origin = RTE_BPF_ORIGIN_RAW, + .raw.ins = ins, + .raw.nb_ins = RTE_DIM(ins), + .prog_arg[0] = { .type = RTE_BPF_ARG_RAW }, + .nb_prog_arg = 1, + .debug = debug, + }); + + rte_bpf_destroy(bpf); + rte_bpf_validate_debug_destroy(debug); + + TEST_ASSERT_NOT_NULL(bpf, "validation failed"); + + /* Verify event counts. */ + for (enum rte_bpf_validate_debug_event event = 0; + event != RTE_BPF_VALIDATE_DEBUG_EVENT_END; ++event) + TEST_ASSERT_EQUAL( + ctx.event_counts[event], expected_counts[event], + "expected %d %s events, but got %d", + expected_counts[event], event_names[event], + ctx.event_counts[event]); + + /* Additionally double-check final stack state. */ + TEST_ASSERT_EQUAL(ctx.branch_pc_stack_length, 0, + "unbalanced branch enters and returns"); + + return TEST_SUCCESS; +} + +REGISTER_FAST_TEST(bpf_validate_events_autotest, NOHUGE_OK, ASAN_OK, test_events); + +/* + * Tests of BPF validation. + */ + /* Sizes of text buffers used for formatting various debug outputs. */ #define VALUE_FORMAT_BUFFER_SIZE 32 #define INTERVAL_FORMAT_BUFFER_SIZE 64 -- 2.43.0

