DPDK-dev Archive on lore.kernel.org
 help / color / mirror / Atom feed
* [PATCH v2 11/25] bpf/validate: fix BPF_NEG of INT64_MIN and 0
From: Marat Khalili @ 2026-05-19  9:31 UTC (permalink / raw)
  To: Konstantin Ananyev; +Cc: dev, stable, Claudia Cauli
In-Reply-To: <20260519093131.52022-1-marat.khalili@huawei.com>

Function `eval_neg` did not treat values of INT64_MIN and 0 specially
when calculating negation ranges (e.g.  negated unsigned range 0..2
should turn into 0..UINT64_MAX) producing incorrect results. On top of
this negating signed INT64_MIN caused undefined behaviour.

E.g. consider the following program with the current validation code:

    Tested program:
        0:  mov r0, #0x0
        1:  ldxdw r2, [r1 + 0]
        2:  lddw r4, #0x8000000000000000
        4:  jgt r2, r4, L7
        5:  neg r2, #0x0  ; tested instruction
        6:  mov r0, #0x1
        7:  exit
    Pre-state:
       r2:  0..0x8000000000000000
    Post-state:
       r2:  INT64_MIN..INT64_MIN+1 INTERSECT 0..0x8000000000000000 (!)

After the tested instruction validator considers r2 to be within
INT64_MIN..INT64_MIN+1 if viewed as signed, or within
0..0x8000000000000000 if viewed as unsigned, however if 1 was loaded on
step 1 into r2 it is possible for it to become -1 after the tested
instruction which satisfies neither of the ranges.

With sanitizer the following diagnostic is generated:

    lib/bpf/bpf_validate.c:1120:7: runtime error: negation of
    -9223372036854775808 cannot be represented in type 'long int'; cast
    to an unsigned type to negate
        #0 0x000002747230 in eval_neg lib/bpf/bpf_validate.c:1120
        #1 0x000002748fb6 in eval_alu lib/bpf/bpf_validate.c:1251
        #2 0x000002759dd3 in evaluate lib/bpf/bpf_validate.c:3161
        ...

    SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior
    lib/bpf/bpf_validate.c:1120:7

Add missing handling of special cases, add tests.

Fixes: 8021917293d0 ("bpf: add extra validation for input BPF program")
Cc: stable@dpdk.org

Reported-by: Claudia Cauli <claudiacauli@gmail.com>
Signed-off-by: Marat Khalili <marat.khalili@huawei.com>
Acked-by: Konstantin Ananyev <konstantin.ananyev@huawei.com>
---
Depends-on: series-38149 ("bpf: introduce extensible load API")
 app/test/test_bpf_validate.c | 126 +++++++++++++++++++++++++++++++++++
 lib/bpf/bpf_validate.c       |  55 ++++++++++++---
 2 files changed, 173 insertions(+), 8 deletions(-)

diff --git a/app/test/test_bpf_validate.c b/app/test/test_bpf_validate.c
index aa385ec8c2..c752d86357 100644
--- a/app/test/test_bpf_validate.c
+++ b/app/test/test_bpf_validate.c
@@ -1154,6 +1154,132 @@ test_alu64_add_x_scalar_scalar(void)
 REGISTER_FAST_TEST(bpf_validate_alu64_add_x_scalar_scalar_autotest, NOHUGE_OK, ASAN_OK,
 	test_alu64_add_x_scalar_scalar);
 
+/* 64-bit negation when interval first element is INT64_MIN. */
+static int
+test_alu64_neg_int64min_first(void)
+{
+	static const int64_t other_values[] = {
+		INT64_MIN,
+		INT64_MIN + 1,
+		INT64_MIN + 13,
+		-17,
+		-1,
+		0,
+		1,
+		19,
+		INT64_MAX - 23,
+		INT64_MAX - 1,
+		INT64_MAX,
+	};
+	for (int other_index = 0; other_index != RTE_DIM(other_values); ++other_index) {
+		const int64_t other_value = other_values[other_index];
+		TEST_ASSERT_SUCCESS(verify_instruction((struct verify_instruction_param){
+			.tested_instruction = {
+				.code = (EBPF_ALU64 | BPF_NEG),
+			},
+			.pre.dst = make_signed_domain(INT64_MIN, other_value),
+			.post.dst = other_value > 0 ? unknown :
+				make_unsigned_domain(-(uint64_t)other_value, INT64_MIN),
+		}), "other_index=%d", other_index);
+	}
+	return TEST_SUCCESS;
+}
+
+REGISTER_FAST_TEST(bpf_validate_alu64_neg_int64min_first_autotest, NOHUGE_OK, ASAN_OK,
+	test_alu64_neg_int64min_first);
+
+/* 64-bit negation when interval last element is INT64_MIN. */
+static int
+test_alu64_neg_int64min_last(void)
+{
+	static const uint64_t other_values[] = {
+		0,
+		1,
+		19,
+		INT64_MAX - 23,
+		INT64_MAX - 1,
+		INT64_MAX,
+		INT64_MIN,
+	};
+	for (int other_index = 0; other_index != RTE_DIM(other_values); ++other_index) {
+		const int64_t other_value = other_values[other_index];
+		TEST_ASSERT_SUCCESS(verify_instruction((struct verify_instruction_param){
+			.tested_instruction = {
+				.code = (EBPF_ALU64 | BPF_NEG),
+			},
+			.pre.dst = make_unsigned_domain(other_value, INT64_MIN),
+			.post.dst = make_signed_domain(INT64_MIN, -(uint64_t)other_value),
+		}), "other_index=%d", other_index);
+	}
+	return TEST_SUCCESS;
+}
+
+REGISTER_FAST_TEST(bpf_validate_alu64_neg_int64min_last_autotest, NOHUGE_OK, ASAN_OK,
+	test_alu64_neg_int64min_last);
+
+/* 64-bit negation when interval first element is zero. */
+static int
+test_alu64_neg_zero_first(void)
+{
+	static const uint64_t other_values[] = {
+		0,
+		1,
+		19,
+		INT64_MAX - 23,
+		INT64_MAX - 1,
+		INT64_MAX,
+		INT64_MIN,
+		INT64_MIN + 1,
+		INT64_MIN + 13,
+		-17,
+		-1,
+	};
+	for (int other_index = 0; other_index != RTE_DIM(other_values); ++other_index) {
+		const uint64_t other_value = other_values[other_index];
+		TEST_ASSERT_SUCCESS(verify_instruction((struct verify_instruction_param){
+			.tested_instruction = {
+				.code = (EBPF_ALU64 | BPF_NEG),
+			},
+			.pre.dst = make_unsigned_domain(0, other_value),
+			.post.dst = other_value > (uint64_t)INT64_MIN ? unknown :
+				make_signed_domain(-(uint64_t)other_value, 0),
+		}), "other_index=%d", other_index);
+	}
+	return TEST_SUCCESS;
+}
+
+REGISTER_FAST_TEST(bpf_validate_alu64_neg_zero_first_autotest, NOHUGE_OK, ASAN_OK,
+	test_alu64_neg_zero_first);
+
+/* 64-bit negation when interval last element is zero. */
+static int
+test_alu64_neg_zero_last(void)
+{
+	static const int64_t other_values[] = {
+		INT64_MIN,
+		INT64_MIN + 1,
+		INT64_MIN + 13,
+		-17,
+		-1,
+		0,
+	};
+	for (int other_index = 0; other_index != RTE_DIM(other_values); ++other_index) {
+		const int64_t other_value = other_values[other_index];
+		TEST_ASSERT_SUCCESS(verify_instruction((struct verify_instruction_param){
+			.tested_instruction = {
+				.code = (EBPF_ALU64 | BPF_NEG),
+			},
+			.pre.dst = make_signed_domain(other_value, 0),
+			.post.dst = make_unsigned_domain(0, -(uint64_t)other_value),
+		}), "other_index=%d", other_index);
+	}
+
+	return TEST_SUCCESS;
+}
+
+REGISTER_FAST_TEST(bpf_validate_alu64_neg_zero_last_autotest, NOHUGE_OK, ASAN_OK,
+	test_alu64_neg_zero_last);
+
 /* Jump if greater than immediate. */
 static int
 test_jmp64_jeq_k(void)
diff --git a/lib/bpf/bpf_validate.c b/lib/bpf/bpf_validate.c
index b0d88fe7d2..79c8679ac5 100644
--- a/lib/bpf/bpf_validate.c
+++ b/lib/bpf/bpf_validate.c
@@ -990,6 +990,11 @@ eval_neg(struct bpf_reg_val *rd, size_t opsz, uint64_t msk)
 {
 	uint64_t ux, uy;
 	int64_t sx, sy;
+	/* additional limits imposed by signed on unsigned and back */
+	struct bpf_reg_val cross_limits = {
+		.s = { INT64_MIN, INT64_MAX },
+		.u = { 0, UINT64_MAX },
+	};
 
 	/* if we have 32-bit values - extend them to 64-bit */
 	if (opsz == sizeof(uint32_t) * CHAR_BIT) {
@@ -997,11 +1002,29 @@ eval_neg(struct bpf_reg_val *rd, size_t opsz, uint64_t msk)
 		rd->u.max = (int32_t)rd->u.max;
 	}
 
-	ux = -(int64_t)rd->u.min & msk;
-	uy = -(int64_t)rd->u.max & msk;
+	if (rd->u.min == 0) {
+		/* special case: ranges that include 0 and, possibly, 1 */
+
+		/*
+		 * Calculate requirements on the signed range of negation.
+		 * It is only possible when negated range does not cross from
+		 * INT64_MIN to INT64_MAX, which means our original range does
+		 * not reach (uint64_t)-INT64_MAX.
+		 */
+		if (rd->u.max < (uint64_t)-INT64_MAX) {
+			cross_limits.s.min = -rd->u.max;
+			cross_limits.s.max = -rd->u.min;
+		}
+
+		if (rd->u.max != 0)
+			rd->u.max = UINT64_MAX;
+	} else {
+		ux = -rd->u.min & msk;
+		uy = -rd->u.max & msk;
 
-	rd->u.max = RTE_MAX(ux, uy);
-	rd->u.min = RTE_MIN(ux, uy);
+		rd->u.max = RTE_MAX(ux, uy);
+		rd->u.min = RTE_MIN(ux, uy);
+	}
 
 	/* if we have 32-bit values - extend them to 64-bit */
 	if (opsz == sizeof(uint32_t) * CHAR_BIT) {
@@ -1009,11 +1032,27 @@ eval_neg(struct bpf_reg_val *rd, size_t opsz, uint64_t msk)
 		rd->s.max = (int32_t)rd->s.max;
 	}
 
-	sx = -rd->s.min & msk;
-	sy = -rd->s.max & msk;
+	if (rd->s.min == INT64_MIN) {
+		/* special case: negation of INT64_MIN is INT64_MIN */
+		if (rd->s.max <= 0) {
+			cross_limits.u.min = -(uint64_t)rd->s.max;
+			cross_limits.u.max = -(uint64_t)rd->s.min;
+		}
+		if (rd->s.max != INT64_MIN)
+			rd->s.max = INT64_MAX;
+	} else {
+		/* since max >= min, neither can be INT64_MIN here */
+		sx = -rd->s.min & msk;
+		sy = -rd->s.max & msk;
+
+		rd->s.max = RTE_MAX(sx, sy);
+		rd->s.min = RTE_MIN(sx, sy);
+	}
 
-	rd->s.max = RTE_MAX(sx, sy);
-	rd->s.min = RTE_MIN(sx, sy);
+	rd->s.min = RTE_MAX(rd->s.min, cross_limits.s.min) & msk;
+	rd->s.max = RTE_MIN(rd->s.max, cross_limits.s.max) & msk;
+	rd->u.min = RTE_MAX(rd->u.min, cross_limits.u.min) & msk;
+	rd->u.max = RTE_MIN(rd->u.max, cross_limits.u.max) & msk;
 }
 
 static const char *
-- 
2.43.0


^ permalink raw reply related

* [PATCH v2 10/25] bpf/validate: fix EBPF_JSLT | BPF_X evaluation
From: Marat Khalili @ 2026-05-19  9:31 UTC (permalink / raw)
  To: Konstantin Ananyev, Ferruh Yigit; +Cc: dev, stable
In-Reply-To: <20260519093131.52022-1-marat.khalili@huawei.com>

Function `eval_jcc` was never called for instruction `(BPF_JMP |
EBPF_JSLT | BPF_X)` due to omission from the table `ins_chk`.

E.g. consider the following program with the current validation code:

    Tested program:
        0:  mov r0, #0x0
        1:  ldxdw r2, [r1 + 0]
        2:  jslt r2, #0xfffffffd, L9
        3:  jsgt r2, #0x3, L9
        4:  mov r3, #0x0
        5:  jslt r2, r3, L8  ; tested instruction
        6:  mov r0, #0x1
        7:  exit
        8:  mov r0, #0x2
        9:  exit
    Pre-state:
       r2:
       r3:
    // skip Post-state
    Jump-state:
       r2:  -3..3

Step 8 should only be reachable (jumped to) for values of r2 less than 0
(value assigned to r3 at step 4), but validator still considers r2 to
have same range -3..3 that it had before the step 5. Moreover the
pre-state that should have been saved on step 5 is not filled in the
test DEBUG output at all, demonstrating that evaluation of this state
just did not happen.

Add missing function and change execution logic to not ignore missing
functions. Add test.

Fixes: 6e12ec4c4d6d ("bpf: add more checks")
Cc: stable@dpdk.org

Signed-off-by: Marat Khalili <marat.khalili@huawei.com>
Acked-by: Konstantin Ananyev <konstantin.ananyev@huawei.com>
---
Depends-on: series-38149 ("bpf: introduce extensible load API")
 app/test/test_bpf_validate.c | 18 +++++++++++++++
 lib/bpf/bpf_validate.c       | 45 ++++++++++++++++++++++++------------
 2 files changed, 48 insertions(+), 15 deletions(-)

diff --git a/app/test/test_bpf_validate.c b/app/test/test_bpf_validate.c
index f05f8a2482..aa385ec8c2 100644
--- a/app/test/test_bpf_validate.c
+++ b/app/test/test_bpf_validate.c
@@ -1172,6 +1172,24 @@ test_jmp64_jeq_k(void)
 REGISTER_FAST_TEST(bpf_validate_jmp64_jeq_k_autotest, NOHUGE_OK, ASAN_OK,
 	test_jmp64_jeq_k);
 
+/* Jump if signed less than another register. */
+static int
+test_jmp64_jslt_x(void)
+{
+	return verify_instruction((struct verify_instruction_param){
+		.tested_instruction = {
+			.code = (BPF_JMP | EBPF_JSLT | BPF_X),
+		},
+		.pre.dst = make_signed_domain(-3, 3),
+		.pre.src = make_signed_domain(0, 0),
+		.post.dst = make_signed_domain(0, 3),
+		.jump.dst = make_signed_domain(-3, -1),
+	});
+}
+
+REGISTER_FAST_TEST(bpf_validate_jmp64_jslt_x_autotest, NOHUGE_OK, ASAN_OK,
+	test_jmp64_jslt_x);
+
 /* 64-bit load from heap (should be set to unknown). */
 static int
 test_mem_ldx_dw_heap(void)
diff --git a/lib/bpf/bpf_validate.c b/lib/bpf/bpf_validate.c
index 391be9cbb4..b0d88fe7d2 100644
--- a/lib/bpf/bpf_validate.c
+++ b/lib/bpf/bpf_validate.c
@@ -1372,6 +1372,14 @@ eval_store(struct bpf_verifier *bvf, const struct ebpf_insn *ins)
 	return NULL;
 }
 
+static const char *
+eval_ja(struct bpf_verifier *bvf, const struct ebpf_insn *ins)
+{
+	RTE_SET_USED(bvf);
+	RTE_SET_USED(ins);
+	return NULL;
+}
+
 static const char *
 eval_func_arg(struct bpf_verifier *bvf, const struct rte_bpf_arg *arg,
 	struct bpf_reg_val *rv)
@@ -2023,6 +2031,7 @@ static const struct bpf_ins_check ins_chk[UINT8_MAX + 1] = {
 		.mask = { .dreg = ZERO_REG, .sreg = ZERO_REG},
 		.off = { .min = 0, .max = UINT16_MAX},
 		.imm = { .min = 0, .max = 0},
+		.eval = eval_ja,
 	},
 	/* jcc IMM instructions */
 	[(BPF_JMP | BPF_JEQ | BPF_K)] = {
@@ -2138,6 +2147,7 @@ static const struct bpf_ins_check ins_chk[UINT8_MAX + 1] = {
 		.mask = { .dreg = ALL_REGS, .sreg = ALL_REGS},
 		.off = { .min = 0, .max = UINT16_MAX},
 		.imm = { .min = 0, .max = 0},
+		.eval = eval_jcc,
 	},
 	[(BPF_JMP | EBPF_JSGE | BPF_X)] = {
 		.mask = { .dreg = ALL_REGS, .sreg = ALL_REGS},
@@ -2890,22 +2900,27 @@ evaluate(struct bpf_verifier *bvf)
 				stats.nb_save++;
 			}
 
-			if (ins_chk[op].eval != NULL) {
-				rc = __rte_bpf_validate_debug_evaluate_step(
-					debug, idx, prev_nb_edge > 1 ?
-						RTE_BPF_VALIDATE_DEBUG_EVENT_BRANCH_ENTER :
-						RTE_BPF_VALIDATE_DEBUG_EVENT_STEP);
-				if (rc < 0)
-					break;
+			if (ins_chk[op].eval == NULL) {
+				RTE_BPF_LOG_FUNC_LINE(ERR,
+					"Unrecognized instruction at pc: %u", idx);
+				rc = -EINVAL;
+				break;
+			}
 
-				err = ins_chk[op].eval(bvf, ins + idx);
-				stats.nb_eval++;
-				if (err != NULL) {
-					RTE_BPF_LOG_FUNC_LINE(ERR,
-						"%s at pc: %u", err, idx);
-					rc = -EINVAL;
-					break;
-				}
+			rc = __rte_bpf_validate_debug_evaluate_step(debug, idx,
+				prev_nb_edge > 1 ?
+					RTE_BPF_VALIDATE_DEBUG_EVENT_BRANCH_ENTER :
+					RTE_BPF_VALIDATE_DEBUG_EVENT_STEP);
+			if (rc < 0)
+				break;
+
+			err = ins_chk[op].eval(bvf, ins + idx);
+			stats.nb_eval++;
+			if (err != NULL) {
+				RTE_BPF_LOG_FUNC_LINE(ERR,
+					"%s at pc: %u", err, idx);
+				rc = -EINVAL;
+				break;
 			}
 
 			log_dbg_eval_state(bvf, ins + idx, idx);
-- 
2.43.0


^ permalink raw reply related

* [PATCH v2 09/25] test/bpf_validate: add harness for pointer tests
From: Marat Khalili @ 2026-05-19  9:31 UTC (permalink / raw)
  Cc: dev, Konstantin Ananyev
In-Reply-To: <20260519093131.52022-1-marat.khalili@huawei.com>

Add necessary harness for testing pointer values in the registers and
add basic tests for adding pointers and scalars in various combinations.
These tests cover previously introduced fixes for BPF_ADD and BPF_LDX.

Signed-off-by: Marat Khalili <marat.khalili@huawei.com>
Acked-by: Konstantin Ananyev <konstantin.ananyev@huawei.com>
---
Depends-on: series-38149 ("bpf: introduce extensible load API")
 app/test/test_bpf_validate.c | 311 +++++++++++++++++++++++++++++++++--
 1 file changed, 297 insertions(+), 14 deletions(-)

diff --git a/app/test/test_bpf_validate.c b/app/test/test_bpf_validate.c
index 350847d07a..f05f8a2482 100644
--- a/app/test/test_bpf_validate.c
+++ b/app/test/test_bpf_validate.c
@@ -51,9 +51,12 @@ struct unsigned_interval {
  * 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.
+ *
+ * Flag `is_pointer` tells if the interval is relative to some memory area base.
  */
 struct domain {
 	bool is_defined;
+	bool is_pointer;
 	struct signed_interval s;
 	struct unsigned_interval u;
 };
@@ -149,7 +152,16 @@ make_unsigned_domain(uint64_t min, uint64_t max)
 	};
 }
 
-/* Return true if domain is a singleton. */
+/* Create domain from signed interval. */
+static struct domain
+make_pointer_domain(int64_t min, int64_t max)
+{
+	struct domain result = make_signed_domain(min, max);
+	result.is_pointer = true;
+	return result;
+}
+
+/* Return true if domain is a scalar or pointer singleton. */
 static bool
 domain_is_singleton(const struct domain *domain)
 {
@@ -195,7 +207,8 @@ format_domain(char *buffer, size_t bufsz, const struct domain *domain)
 
 	const int rc = !domain->is_defined ?
 		snprintf(buffer, bufsz, "UNDEFINED") :
-		snprintf(buffer, bufsz, "%s INTERSECT %s",
+		snprintf(buffer, bufsz, "%s %s INTERSECT %s",
+			domain->is_pointer ? "pointer" : "scalar",
 			format_interval(signed_buffer, sizeof(signed_buffer), 'd',
 				domain->s.min, domain->s.max),
 			format_interval(unsigned_buffer, sizeof(unsigned_buffer), 'x',
@@ -228,7 +241,7 @@ may_jump(const struct rte_bpf_validate_debug *debug,
 	return (result & RTE_BPF_VALIDATE_DEBUG_MAY_BE_TRUE) != 0;
 }
 
-/* Check interval of the register interpreted as signed. */
+/* Check interval of the register interpreted as signed scalar. */
 static int
 check_signed_interval(struct rte_bpf_validate_debug *debug,
 	uint8_t reg, struct signed_interval interval)
@@ -274,7 +287,7 @@ check_signed_interval(struct rte_bpf_validate_debug *debug,
 	return TEST_SUCCESS;
 }
 
-/* Check interval of the register interpreted as unsigned. */
+/* Check interval of the register interpreted as unsigned scalar. */
 static int
 check_unsigned_interval(struct rte_bpf_validate_debug *debug,
 	uint8_t reg, struct unsigned_interval interval)
@@ -320,18 +333,154 @@ check_unsigned_interval(struct rte_bpf_validate_debug *debug,
 	return TEST_SUCCESS;
 }
 
-/* Check domain of the register interpreted as value. */
+/* Check interval of the register relative to the base register. */
+static int
+check_relative_interval(struct rte_bpf_validate_debug *debug,
+	uint8_t reg, struct signed_interval interval, uint8_t base_reg)
+{
+	char buffer[VALUE_FORMAT_BUFFER_SIZE];
+
+	TEST_ASSERT_EQUAL(may_jump(debug,
+		&(struct ebpf_insn){
+			.code = (BPF_JMP | EBPF_JLT | BPF_X),
+			.dst_reg = reg,
+			.src_reg = base_reg,
+		}, interval.min),
+		false,
+		"r%hhu u< r%hhu + %s is impossible", reg, base_reg,
+		format_value(buffer, sizeof(buffer), 'd', interval.min));
+
+	TEST_ASSERT_EQUAL(may_jump(debug,
+		&(struct ebpf_insn){
+			.code = (BPF_JMP | BPF_JEQ | BPF_X),
+			.dst_reg = reg,
+			.src_reg = base_reg,
+		}, interval.min),
+		true,
+		"r%hhu == r%hhu + %s is possible", reg, base_reg,
+		format_value(buffer, sizeof(buffer), 'd', interval.min));
+
+	TEST_ASSERT_EQUAL(may_jump(debug,
+		&(struct ebpf_insn){
+			.code = (BPF_JMP | BPF_JEQ | BPF_X),
+			.dst_reg = reg,
+			.src_reg = base_reg,
+		}, interval.max),
+		true,
+		"r%hhu == r%hhu + %s is possible", reg, base_reg,
+		format_value(buffer, sizeof(buffer), 'd', interval.max));
+
+	TEST_ASSERT_EQUAL(may_jump(debug,
+		&(struct ebpf_insn){
+			.code = (BPF_JMP | BPF_JGT | BPF_X),
+			.dst_reg = reg,
+			.src_reg = base_reg,
+		}, interval.max),
+		false,
+		"r%hhu u> r%hhu + %s is impossible", reg, base_reg,
+		format_value(buffer, sizeof(buffer), 'd', interval.max));
+
+	return TEST_SUCCESS;
+}
+
+/*
+ * Check access of the register interpreted as pointer.
+ *
+ * Unlike other similar functions, min > max is not a problem here,
+ * so either signed or unsigned pair can be passed without any issues.
+ *
+ * This is the reason we are not using signed_interval or unsigned_interval here
+ * to avoid confusion.
+ */
 static int
-check_domain_impl(struct rte_bpf_validate_debug *debug, uint8_t reg,
+check_pointer_access(struct rte_bpf_validate_debug *debug, uint8_t reg,
+	uint64_t min, uint64_t max, size_t area_size)
+{
+	char buffer[VALUE_FORMAT_BUFFER_SIZE];
+
+	/* Start and end of the valid offsets window (unless empty). */
+	const uint64_t window_begin = -min;
+	const uint64_t window_end = area_size - max;
+
+	/* Only have accessible bytes if the interval is smaller than the area. */
+	const uint64_t interval_size = max - min;
+	const bool window_empty = (interval_size >= area_size);
+
+	TEST_ASSERT_EQUAL(rte_bpf_validate_debug_can_access(debug,
+		&(struct ebpf_insn){
+			.code = (BPF_LDX | BPF_B | BPF_MEM),
+			.src_reg = reg
+		}, window_begin - 1),
+		false,
+		"r%hhu + %s (before window begin) dereference is invalid", reg,
+		format_value(buffer, sizeof(buffer), 'd', window_begin - 1));
+
+	TEST_ASSERT_EQUAL(rte_bpf_validate_debug_can_access(debug,
+		&(struct ebpf_insn){
+			.code = (BPF_LDX | BPF_B | BPF_MEM),
+			.src_reg = reg
+		}, window_begin),
+		!window_empty,
+		"r%hhu + %s (after window begin) dereference is %s", reg,
+		format_value(buffer, sizeof(buffer), 'd', window_begin),
+		window_empty ? "invalid for empty window" : "valid");
+
+	TEST_ASSERT_EQUAL(rte_bpf_validate_debug_can_access(debug,
+		&(struct ebpf_insn){
+			.code = (BPF_LDX | BPF_B | BPF_MEM),
+			.src_reg = reg
+		}, window_end - 1),
+		!window_empty,
+		"r%hhu + %s (before window end) dereference is %s", reg,
+		format_value(buffer, sizeof(buffer), 'd', window_end - 1),
+		window_empty ? "invalid for empty window" : "valid");
+
+	TEST_ASSERT_EQUAL(rte_bpf_validate_debug_can_access(debug,
+		&(struct ebpf_insn){
+			.code = (BPF_LDX | BPF_B | BPF_MEM),
+			.src_reg = reg
+		}, window_end),
+		false,
+		"r%hhu + %s (after window end) dereference is invalid", reg,
+		format_value(buffer, sizeof(buffer), 'd', window_end));
+
+	return TEST_SUCCESS;
+}
+
+/* Check domain of the register interpreted as absolute value. */
+static int
+check_scalar_domain(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");
+		"absolute signed interval check");
 
 	TEST_ASSERT_SUCCESS(
 		check_unsigned_interval(debug, reg, domain->u),
-		"unsigned interval check");
+		"absolute unsigned interval check");
+
+	return TEST_SUCCESS;
+}
+
+/* Check domain of the register interpreted as relative pointer. */
+static int
+check_pointer_domain(struct rte_bpf_validate_debug *debug, uint8_t reg,
+	const struct domain *domain, uint8_t base_reg, size_t area_size)
+{
+	TEST_ASSERT_SUCCESS(
+		check_relative_interval(debug, reg, domain->s, base_reg),
+		"relative interval check");
+
+	TEST_ASSERT_SUCCESS(
+		check_pointer_access(debug, reg, domain->s.min, domain->s.max,
+			area_size),
+		"pointer signed access check");
+
+	TEST_ASSERT_SUCCESS(
+		check_pointer_access(debug, reg, domain->u.min, domain->u.max,
+			area_size),
+		"pointer unsigned access check");
 
 	return TEST_SUCCESS;
 }
@@ -339,11 +488,13 @@ check_domain_impl(struct rte_bpf_validate_debug *debug, uint8_t reg,
 /* 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)
+	const struct domain *domain, uint8_t base_reg, size_t area_size)
 {
 	char buffer[REGISTER_FORMAT_BUFFER_SIZE];
 
-	const int rc = check_domain_impl(debug, reg, domain);
+	const int rc = domain->is_pointer ?
+		check_pointer_domain(debug, reg, domain, base_reg, area_size) :
+		check_scalar_domain(debug, reg, domain);
 
 	if (rc != TEST_SUCCESS) {
 		TEST_LOG_LINE(WARNING, "\tExpected: r%hhu = %s", reg,
@@ -419,13 +570,13 @@ compare_and_jump(struct ebpf_insn **ins, uint8_t op, uint8_t reg,
 }
 
 /*
- * Prepare register to be in the specified domain.
+ * Prepare register to be in the specified scalar 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,
+prepare_scalar_domain(struct ebpf_insn **ins, uint8_t reg,
 	const struct domain *domain, uint8_t base_reg, int *service_cell_count,
 	uint8_t tmp_reg)
 {
@@ -460,6 +611,28 @@ prepare_domain(struct ebpf_insn **ins, uint8_t reg,
 		compare_and_jump(ins, EBPF_JSGT, reg, domain->s.max, tmp_reg);
 }
 
+/*
+ * Prepare register to be in the specified scalar or pointer domain, if any.
+ *
+ * If `domain` is NULL, do nothing. Otherwise prepare scalar domain,
+ * and then add base register to it to convert it to a pointer, if needed.
+ */
+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)
+{
+	prepare_scalar_domain(ins, reg, domain, base_reg, service_cell_count, tmp_reg);
+
+	if (domain->is_pointer)
+		/* Add base_reg to convert resulting scalar into a pointer. */
+		*(*ins)++ = (struct ebpf_insn){
+			.code = (EBPF_ALU64 | BPF_ADD | BPF_X),
+			.dst_reg = reg,
+			.src_reg = base_reg,
+		};
+}
+
 static void
 fill_verify_instruction_defaults(struct verify_instruction_param *prm)
 {
@@ -645,7 +818,8 @@ point_callback(struct rte_bpf_validate_debug *debug, const struct verify_instruc
 
 		if (state->dst.is_defined) {
 			TEST_ASSERT_SUCCESS(
-				check_domain(debug, ctx->dst_reg, &state->dst),
+				check_domain(debug, ctx->dst_reg, &state->dst,
+					ctx->base_reg, ctx->prm.area_size),
 				"dst domain check");
 			TEST_LOG_LINE(DEBUG, "Successfully checked r%hhu.", ctx->dst_reg);
 		} else
@@ -658,7 +832,8 @@ point_callback(struct rte_bpf_validate_debug *debug, const struct verify_instruc
 
 		if (state->src.is_defined) {
 			TEST_ASSERT_SUCCESS(
-				check_domain(debug, ctx->src_reg, &state->src),
+				check_domain(debug, ctx->src_reg, &state->src,
+					ctx->base_reg, ctx->prm.area_size),
 				"src domain check");
 			TEST_LOG_LINE(DEBUG, "Successfully checked r%hhu.", ctx->src_reg);
 		} else
@@ -889,6 +1064,96 @@ test_alu64_add_k(void)
 REGISTER_FAST_TEST(bpf_validate_alu64_add_k_autotest, NOHUGE_OK, ASAN_OK,
 	test_alu64_add_k);
 
+/* 64-bit addition of immediate to a pointer range. */
+static int
+test_alu64_add_k_pointer(void)
+{
+	return verify_instruction((struct verify_instruction_param){
+		.tested_instruction = {
+			.code = (EBPF_ALU64 | BPF_ADD | BPF_K),
+			.imm = 17,
+		},
+		.area_size = 256,
+		.pre.dst = make_pointer_domain(11, 29),
+		.post.dst = make_pointer_domain(11 + 17, 29 + 17),
+	});
+}
+
+REGISTER_FAST_TEST(bpf_validate_alu64_add_k_pointer_autotest, NOHUGE_OK, ASAN_OK,
+	test_alu64_add_k_pointer);
+
+/* 64-bit addition of pointer to a pointer. */
+static int
+test_alu64_add_x_pointer_pointer(void)
+{
+	return verify_instruction((struct verify_instruction_param){
+		.tested_instruction = {
+			.code = (EBPF_ALU64 | BPF_ADD | BPF_X),
+		},
+		.area_size = 256,
+		.pre.dst = make_pointer_domain(11, 29),
+		.pre.src = make_pointer_domain(17, 23),
+		.post.dst = unknown,
+	});
+}
+
+REGISTER_FAST_TEST(bpf_validate_alu64_add_x_pointer_pointer_autotest, NOHUGE_OK, ASAN_OK,
+	test_alu64_add_x_pointer_pointer);
+
+/* 64-bit addition of scalar to a pointer. */
+static int
+test_alu64_add_x_pointer_scalar(void)
+{
+	return verify_instruction((struct verify_instruction_param){
+		.tested_instruction = {
+			.code = (EBPF_ALU64 | BPF_ADD | BPF_X),
+		},
+		.area_size = 256,
+		.pre.dst = make_pointer_domain(11, 29),
+		.pre.src = make_signed_domain(17, 23),
+		.post.dst = make_pointer_domain(11 + 17, 29 + 23),
+	});
+}
+
+REGISTER_FAST_TEST(bpf_validate_alu64_add_x_pointer_scalar_autotest, NOHUGE_OK, ASAN_OK,
+	test_alu64_add_x_pointer_scalar);
+
+/* 64-bit addition of pointer to a scalar. */
+static int
+test_alu64_add_x_scalar_pointer(void)
+{
+	return verify_instruction((struct verify_instruction_param){
+		.tested_instruction = {
+			.code = (EBPF_ALU64 | BPF_ADD | BPF_X),
+		},
+		.area_size = 256,
+		.pre.dst = make_signed_domain(11, 29),
+		.pre.src = make_pointer_domain(17, 23),
+		.post.dst = make_pointer_domain(11 + 17, 29 + 23),
+	});
+}
+
+REGISTER_FAST_TEST(bpf_validate_alu64_add_x_scalar_pointer_autotest, NOHUGE_OK, ASAN_OK,
+	test_alu64_add_x_scalar_pointer);
+
+/* 64-bit addition of scalar to a scalar. */
+static int
+test_alu64_add_x_scalar_scalar(void)
+{
+	return verify_instruction((struct verify_instruction_param){
+		.tested_instruction = {
+			.code = (EBPF_ALU64 | BPF_ADD | BPF_X),
+		},
+		.area_size = 256,
+		.pre.dst = make_signed_domain(11, 29),
+		.pre.src = make_signed_domain(17, 23),
+		.post.dst = make_signed_domain(11 + 17, 29 + 23),
+	});
+}
+
+REGISTER_FAST_TEST(bpf_validate_alu64_add_x_scalar_scalar_autotest, NOHUGE_OK, ASAN_OK,
+	test_alu64_add_x_scalar_scalar);
+
 /* Jump if greater than immediate. */
 static int
 test_jmp64_jeq_k(void)
@@ -906,3 +1171,21 @@ test_jmp64_jeq_k(void)
 
 REGISTER_FAST_TEST(bpf_validate_jmp64_jeq_k_autotest, NOHUGE_OK, ASAN_OK,
 	test_jmp64_jeq_k);
+
+/* 64-bit load from heap (should be set to unknown). */
+static int
+test_mem_ldx_dw_heap(void)
+{
+	return verify_instruction((struct verify_instruction_param){
+		.tested_instruction = {
+			.code = (BPF_MEM | BPF_LDX | EBPF_DW),
+			.off = 16,
+		},
+		.area_size = 24,
+		.pre.src = make_pointer_domain(0, 0),
+		.post.dst = unknown,
+	});
+}
+
+REGISTER_FAST_TEST(bpf_validate_mem_ldx_dw_heap_autotest, NOHUGE_OK, ASAN_OK,
+	test_mem_ldx_dw_heap);
-- 
2.43.0


^ permalink raw reply related

* [PATCH v2 08/25] test/bpf_validate: add setup and basic tests
From: Marat Khalili @ 2026-05-19  9:31 UTC (permalink / raw)
  Cc: dev, Konstantin Ananyev
In-Reply-To: <20260519093131.52022-1-marat.khalili@huawei.com>

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@huawei.com>
Acked-by: Konstantin Ananyev <konstantin.ananyev@huawei.com>
---
Depends-on: series-38149 ("bpf: introduce extensible load API")
 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 7d458f9c07..45a18ee68b 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 0000000000..350847d07a
--- /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 = (int32_t)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


^ permalink raw reply related

* [PATCH v2 05/25] bpf/validate: introduce debugging interface
From: Marat Khalili @ 2026-05-19  9:31 UTC (permalink / raw)
  To: Konstantin Ananyev; +Cc: dev
In-Reply-To: <20260519093131.52022-1-marat.khalili@huawei.com>

Introduce debugging interface for BPF validator. New API lets one
observe evaluation of the validated BPF program, including step
evaluation, setting break- and catchpoints, inspecting possible jumps
and memory accesses in current state, as well as formatting current
state elements for the user. It can be used to build both automated
tests and interactive validation debuggers without tight coupling to a
specific validator implementation.

Signed-off-by: Marat Khalili <marat.khalili@huawei.com>
Acked-by: Konstantin Ananyev <konstantin.ananyev@huawei.com>
---
Depends-on: series-38149 ("bpf: introduce extensible load API")
 lib/bpf/bpf_validate.c           | 448 ++++++++++++++++++++-
 lib/bpf/bpf_validate.h           |  60 +++
 lib/bpf/bpf_validate_debug.c     | 663 +++++++++++++++++++++++++++++++
 lib/bpf/bpf_validate_debug.h     |  86 ++++
 lib/bpf/bpf_value_set.c          | 403 +++++++++++++++++++
 lib/bpf/bpf_value_set.h          | 126 ++++++
 lib/bpf/meson.build              |   9 +-
 lib/bpf/rte_bpf.h                |   4 +
 lib/bpf/rte_bpf_validate_debug.h | 375 +++++++++++++++++
 9 files changed, 2169 insertions(+), 5 deletions(-)
 create mode 100644 lib/bpf/bpf_validate.h
 create mode 100644 lib/bpf/bpf_validate_debug.c
 create mode 100644 lib/bpf/bpf_validate_debug.h
 create mode 100644 lib/bpf/bpf_value_set.c
 create mode 100644 lib/bpf/bpf_value_set.h
 create mode 100644 lib/bpf/rte_bpf_validate_debug.h

diff --git a/lib/bpf/bpf_validate.c b/lib/bpf/bpf_validate.c
index 362d00c770..8dac908c39 100644
--- a/lib/bpf/bpf_validate.c
+++ b/lib/bpf/bpf_validate.c
@@ -9,9 +9,13 @@
 #include <stdint.h>
 #include <inttypes.h>
 
+#include <rte_bpf_validate_debug.h>
 #include <rte_common.h>
 
 #include "bpf_impl.h"
+#include "bpf_validate.h"
+#include "bpf_validate_debug.h"
+#include "bpf_value_set.h"
 
 #define BPF_ARG_PTR_STACK RTE_BPF_ARG_RESERVED
 
@@ -92,6 +96,7 @@ struct bpf_verifier {
 	struct inst_node *evin;
 	struct evst_pool evst_sr_pool; /* for evst save/restore */
 	struct evst_pool evst_tp_pool; /* for evst track/prune */
+	struct rte_bpf_validate_debug *debug;
 };
 
 struct bpf_ins_check {
@@ -118,6 +123,409 @@ struct bpf_ins_check {
 /* For LD_IND R6 is an implicit CTX register. */
 #define	IND_SRC_REGS	(WRT_REGS ^ 1 << EBPF_REG_6)
 
+/*
+ * Debugging internal interface and helpers.
+ */
+
+static bool
+reg_val_range_is_valid(const struct bpf_reg_val *rv)
+{
+	if (rv->v.type == RTE_BPF_ARG_UNDEF)
+		return true;
+
+	if (rv->s.min > rv->s.max)
+		return false;
+
+	if (rv->u.min > rv->u.max)
+		return false;
+
+	/* If one of the ranges does not change sign, the other should match. */
+	if (rv->s.min >= 0 || rv->s.max < 0 ||
+			rv->u.min > INT64_MAX || rv->u.max <= INT64_MAX)
+		return rv->u.min == (uint64_t)rv->s.min &&
+			rv->u.max == (uint64_t)rv->s.max;
+
+	return true;
+}
+
+int
+__rte_bpf_validate_state_is_valid(const struct bpf_verifier *verifier)
+{
+	const struct bpf_eval_state *const st = verifier->evst;
+
+	for (int reg = 0; reg != RTE_DIM(st->rv); ++reg)
+		if (!reg_val_range_is_valid(st->rv + reg))
+			return false;
+
+	for (int var = 0; var != RTE_DIM(st->sv); ++var)
+		if (!reg_val_range_is_valid(st->sv + var))
+			return false;
+
+	return true;
+}
+
+int
+__rte_bpf_validate_can_access(const struct bpf_verifier *verifier,
+	const struct ebpf_insn *access, uint64_t off64)
+{
+	const struct bpf_eval_state *const st = verifier->evst;
+	const struct bpf_reg_val *rv;
+	/* Set of accessed byte offsets relative to memory area base. */
+	struct value_set access_set;
+	uint32_t opsz;
+
+	switch (BPF_CLASS(access->code)) {
+	case BPF_LDX:
+		rv = &st->rv[access->src_reg];
+		if (rv->v.type == BPF_ARG_PTR_STACK)
+			/* Not supporting stack access queries yet. */
+			return -ENOTSUP;
+		break;
+	case BPF_ST:
+		rv = &st->rv[access->dst_reg];
+		break;
+	case BPF_STX:
+		rv = &st->rv[access->dst_reg];
+		if (st->rv[access->src_reg].v.type == RTE_BPF_ARG_UNDEF)
+			return false;
+		break;
+	default:
+		return -ENOTSUP;
+	}
+
+	if (!RTE_BPF_ARG_PTR_TYPE(rv->v.type) || rv->v.size == 0)
+		return false;
+
+	access_set = value_set_from_pair(rv->s.min, rv->s.max, rv->u.min, rv->u.max);
+	value_set_translate(&access_set, off64);
+	opsz = bpf_size(BPF_SIZE(access->code));
+	value_set_add_contiguous(&access_set, 0, opsz - 1);
+
+	return value_set_is_covered_by_contiguous(&access_set, 0, rv->v.size - 1);
+}
+
+/* Return true if instruction `code` is supported by `may_jump`. */
+static bool
+may_jump_code_is_supported(uint8_t code)
+{
+	if (BPF_CLASS(code) != BPF_JMP)
+		return false;
+
+	switch (BPF_OP(code)) {
+	case BPF_JEQ:
+	case BPF_JGT:
+	case BPF_JGE:
+	case EBPF_JNE:
+	case EBPF_JSGT:
+	case EBPF_JSGE:
+	case EBPF_JLT:
+	case EBPF_JLE:
+	case EBPF_JSLT:
+	case EBPF_JSLE:
+		return true;
+	default:
+		return false;
+	}
+}
+
+/* Return true if instruction `code` corresponds to a signed comparison. */
+static bool
+may_jump_code_is_signed(uint8_t code)
+{
+	switch (BPF_OP(code)) {
+	case EBPF_JSGT:
+	case EBPF_JSGE:
+	case EBPF_JSLT:
+	case EBPF_JSLE:
+		return true;
+	default:
+		return false;
+	}
+}
+
+/* Return true the specified jump condition _may_ be true. */
+static bool
+may_jump(uint8_t code, const struct value_set *origin,
+	const struct value_set *dst_set, const struct value_set *src_set)
+{
+	switch (BPF_OP(code)) {
+	case BPF_JEQ:
+		return value_sets_intersect(dst_set, src_set);
+	case EBPF_JNE:
+		return !(value_set_is_singleton(dst_set) &&
+			value_sets_equal(dst_set, src_set));
+	case BPF_JGT:
+	case EBPF_JSGT:
+		return !value_sets_based_less_or_equal(origin, dst_set, src_set);
+	case BPF_JGE:
+	case EBPF_JSGE:
+		return !value_sets_based_less(origin, dst_set, src_set);
+	case EBPF_JLT:
+	case EBPF_JSLT:
+		return !value_sets_based_less_or_equal(origin, src_set, dst_set);
+	case EBPF_JSLE:
+	case EBPF_JLE:
+		return !value_sets_based_less(origin, src_set, dst_set);
+	}
+	/* may_jump_code_is_supported should have caught this */
+	RTE_ASSERT(false);
+	return false;
+}
+
+/* Return instruction code for jump condition complement (negated result). */
+static uint8_t
+may_jump_code_complement(uint8_t code)
+{
+	switch (BPF_OP(code)) {
+	case BPF_JEQ:
+	case EBPF_JNE:
+		return code ^ BPF_JEQ ^ EBPF_JNE;
+	case BPF_JGT:
+	case EBPF_JLE:
+		return code ^ BPF_JGT ^ EBPF_JLE;
+	case BPF_JGE:
+	case EBPF_JLT:
+		return code ^ BPF_JGE ^ EBPF_JLT;
+	case EBPF_JSGT:
+	case EBPF_JSLE:
+		return code ^ EBPF_JSGT ^ EBPF_JSLE;
+	case EBPF_JSGE:
+	case EBPF_JSLT:
+		return code ^ EBPF_JSGE ^ EBPF_JSLT;
+	}
+	/* may_jump_code_is_supported should have caught this */
+	RTE_ASSERT(false);
+	return 0;
+}
+
+int
+__rte_bpf_validate_may_jump(const struct bpf_verifier *verifier,
+	const struct ebpf_insn *jump, uint64_t imm64)
+{
+	const struct bpf_eval_state *const st = verifier->evst;
+	const struct bpf_reg_val *rd, *rs;
+	struct value_set dst_set, src_set, origin;
+	int result;
+
+	if (!may_jump_code_is_supported(jump->code))
+		return -ENOTSUP;
+
+	rd = &st->rv[jump->dst_reg];
+	dst_set = (rd->v.type == RTE_BPF_ARG_UNDEF) ? value_set_full :
+		value_set_from_pair(rd->s.min, rd->s.max, rd->u.min, rd->u.max);
+
+	rs = BPF_SRC(jump->code) == BPF_X ? &st->rv[jump->src_reg] : NULL;
+	src_set = rs == NULL ? value_set_singleton((int64_t)jump->imm) :
+		rs->v.type == RTE_BPF_ARG_UNDEF ? value_set_full :
+		value_set_from_pair(rs->s.min, rs->s.max, rs->u.min, rs->u.max);
+
+	value_set_translate(&src_set, imm64);
+
+	if (RTE_BPF_ARG_PTR_TYPE(rd->v.type) &&
+			(rs != NULL && RTE_BPF_ARG_PTR_TYPE(rs->v.type)) &&
+			rd->v.size == rs->v.size) {
+		/*
+		 * Both sides are pointers with the same memory area size.
+		 * Until tracking of memory areas is implemented we will consider them
+		 * pointing to the same memory area just because of this.
+		 * In this case our value sets represent offsets from the memory area base,
+		 * which is some unknown distance from the scalar zero (NULL).
+		 * We know however that the memory area cannot cross zero address.
+		 * Thus range of origin relative to memory base starts with 1 byte gap
+		 * after the memory area and ends just before it.
+		 */
+		origin = value_set_contiguous(rd->v.size + 1, -1);
+	} else {
+		/* Scalar value of a pointer depends on the memory area base address. */
+		if (RTE_BPF_ARG_PTR_TYPE(rd->v.type))
+			value_set_add_contiguous(&dst_set, 1, UINT64_MAX - rd->v.size);
+		if (rs != NULL && RTE_BPF_ARG_PTR_TYPE(rs->v.type))
+			value_set_add_contiguous(&dst_set, 1, UINT64_MAX - rs->v.size);
+		origin = value_set_singleton(0);
+	}
+
+	if (may_jump_code_is_signed(jump->code))
+		/* Shift origin to the minimal value for signed comparisons. */
+		value_set_translate(&origin, INT64_MIN);
+
+	result = 0;
+
+	if (may_jump(jump->code, &origin, &dst_set, &src_set))
+		result |= RTE_BPF_VALIDATE_DEBUG_MAY_BE_TRUE;
+
+	if (may_jump(may_jump_code_complement(jump->code), &origin, &dst_set, &src_set))
+		result |= RTE_BPF_VALIDATE_DEBUG_MAY_BE_FALSE;
+
+	return result;
+}
+
+/* Like snprintf, but advances (except for overflow) ptr and reduces szleft. */
+__attribute__((__format__ (__printf__, 3, 4)))
+static int
+buf_printf(char **ptr, ssize_t *szleft, const char *format, ...)
+{
+	va_list args;
+	int rc;
+
+	va_start(args, format);
+	rc = vsnprintf(*ptr, RTE_MAX(0, *szleft), format, args);
+	va_end(args);
+
+	if (rc > 0) {
+		*szleft -= rc;
+		if (*szleft > 0)
+			*ptr += rc;
+	}
+
+	return rc;
+}
+
+static int
+format_memory_area(char **ptr, ssize_t *szleft, const struct bpf_reg_val *rv)
+{
+	switch (rv->v.type) {
+	case RTE_BPF_ARG_RAW:
+		return 0;
+	case RTE_BPF_ARG_PTR:
+		return buf_printf(ptr, szleft, "%%buffer<%zu> + ",
+			(size_t)rv->v.size);
+	case RTE_BPF_ARG_PTR_MBUF:
+		return buf_printf(ptr, szleft, "%%mbuf<%zu, %zu> + ",
+			(size_t)rv->v.size, (size_t)rv->v.buf_size);
+	case BPF_ARG_PTR_STACK:
+		return buf_printf(ptr, szleft, "%%stack + ");
+	default:
+		return -ENOTSUP;
+	}
+}
+
+/* Format min..max interval using validate-debug API and updating ptr and szleft. */
+static int
+buf_print_interval(char **ptr, ssize_t *szleft, char format, uint64_t min, uint64_t max)
+{
+	int rc;
+
+	rc = rte_bpf_validate_debug_format_interval(*ptr, RTE_MAX(0, *szleft),
+		format, min, max);
+
+	if (rc > 0) {
+		*szleft -= rc;
+		if (*szleft > 0)
+			*ptr += rc;
+	}
+
+	return rc;
+}
+
+/* Format rv roughly as "<signed-range> INTERSECT <unsigned-hex-range>" */
+static int
+format_register_range(char **ptr, ssize_t *szleft, const struct bpf_reg_val *rv)
+{
+	int rc;
+	uint64_t expected_unsigned_min, expected_unsigned_max;
+	const bool valid = reg_val_range_is_valid(rv);
+
+	/* Print signed unless trivial. */
+	if (!valid || rv->s.min != INT64_MIN || rv->s.max != INT64_MAX) {
+		rc = buf_print_interval(ptr, szleft, 'd', rv->s.min, rv->s.max);
+		if (rc < 0)
+			return rc;
+
+		if (valid) {
+			/* Skip printing unsigned if it has expected values. */
+			if (rv->s.min >= 0 || rv->s.max < 0) {
+				expected_unsigned_min = (uint64_t)rv->s.min;
+				expected_unsigned_max = (uint64_t)rv->s.max;
+			} else {
+				expected_unsigned_min = 0;
+				expected_unsigned_max = UINT64_MAX;
+			}
+
+			if (rv->u.min == expected_unsigned_min &&
+					rv->u.max == expected_unsigned_max)
+				return 0;
+		}
+
+		rc = buf_printf(ptr, szleft, " INTERSECT ");
+		if (rc < 0)
+			return rc;
+	}
+
+	rc = buf_print_interval(ptr, szleft, 'x', rv->u.min, rv->u.max);
+	if (rc < 0)
+		return rc;
+
+	if (!valid) {
+		rc = buf_printf(ptr, szleft, " (!)");
+		if (rc < 0)
+			return rc;
+	}
+
+	return 0;
+}
+
+/* Format rv roughly as "<memory-object> + <offsets-range>" */
+static int
+format_reg_val(char *buffer, size_t bufsz, const struct bpf_reg_val *rv)
+{
+	char *ptr = buffer;
+	ssize_t szleft = bufsz;
+	int rc;
+
+	if (rv->v.type == RTE_BPF_ARG_UNDEF)
+		return snprintf(buffer, bufsz, "%%undefined");
+
+	/* Print data area info, if any. */
+	rc = format_memory_area(&ptr, &szleft, rv);
+	if (rc < 0)
+		return rc;
+
+	rc = format_register_range(&ptr, &szleft, rv);
+	if (rc < 0)
+		return rc;
+
+	/* At least one snprintf was called and added terminating zero. */
+	RTE_ASSERT(szleft < (ssize_t)bufsz);
+	--szleft;
+
+	return bufsz - szleft;
+}
+
+int
+__rte_bpf_validate_format_register_info(const struct bpf_verifier *verifier,
+	char *buffer, size_t bufsz, uint8_t reg)
+{
+	if (reg >= EBPF_REG_NUM)
+		return -EINVAL;
+
+	return format_reg_val(buffer, bufsz, &verifier->evst->rv[reg]);
+}
+
+int
+__rte_bpf_validate_format_frame_info(const struct bpf_verifier *verifier,
+	char *buffer, size_t bufsz, int32_t offset)
+{
+	if (offset % sizeof(uint64_t) != 0)
+		return -EINVAL;
+
+	if (offset >= 0 || offset < -MAX_BPF_STACK_SIZE)
+		return -ERANGE;
+
+	offset = (MAX_BPF_STACK_SIZE + offset) / sizeof(uint64_t);
+
+	return format_reg_val(buffer, bufsz, &verifier->evst->sv[offset]);
+}
+
+int32_t
+__rte_bpf_validate_get_frame_size(const struct bpf_verifier *verifier)
+{
+	if (verifier->stack_sz > INT32_MAX)
+		return -ERANGE;
+
+	return verifier->stack_sz;
+}
+
+
 /*
  * check and evaluate functions for particular instruction types.
  */
@@ -2405,7 +2813,9 @@ evaluate(struct bpf_verifier *bvf)
 	const char *err;
 	const struct ebpf_insn *ins;
 	struct inst_node *next, *node;
-	int rc = 0;
+	int prev_nb_edge;  /* branching number of the previous instruction */
+	int rc, debug_rc;
+	struct rte_bpf_validate_debug *const debug = bvf->prm->debug;
 
 	struct {
 		uint32_t nb_eval;
@@ -2439,11 +2849,15 @@ evaluate(struct bpf_verifier *bvf)
 	ins = bvf->prm->raw.ins;
 	node = bvf->in;
 	next = node;
+	prev_nb_edge = 1;
 
 	memset(&stats, 0, sizeof(stats));
 
-	while (node != NULL) {
+	rc = __rte_bpf_validate_debug_evaluate_start(debug, bvf, bvf->prm);
+	if (rc < 0)
+		return rc;
 
+	while (node != NULL) {
 		/*
 		 * current node evaluation, make sure we evaluate
 		 * each node only once.
@@ -2464,6 +2878,13 @@ evaluate(struct bpf_verifier *bvf)
 			}
 
 			if (ins_chk[op].eval != NULL) {
+				rc = __rte_bpf_validate_debug_evaluate_step(
+					debug, idx, prev_nb_edge > 1 ?
+						RTE_BPF_VALIDATE_DEBUG_EVENT_BRANCH_ENTER :
+						RTE_BPF_VALIDATE_DEBUG_EVENT_STEP);
+				if (rc < 0)
+					break;
+
 				err = ins_chk[op].eval(bvf, ins + idx);
 				stats.nb_eval++;
 				if (err != NULL) {
@@ -2499,10 +2920,17 @@ evaluate(struct bpf_verifier *bvf)
 			 */
 			if (node->nb_edge > 1 && prune_eval_state(bvf, node,
 					next) == 0) {
+				rc = __rte_bpf_validate_debug_evaluate_step(
+					debug, get_node_idx(bvf, next),
+					RTE_BPF_VALIDATE_DEBUG_EVENT_BRANCH_PRUNE);
+				if (rc < 0)
+					break;
+
 				next = NULL;
 				stats.nb_prune++;
 			} else {
 				next->prev_node = node;
+				prev_nb_edge = node->nb_edge;
 				node = next;
 			}
 		} else {
@@ -2511,8 +2939,18 @@ evaluate(struct bpf_verifier *bvf)
 			 * mark it's @start state as safe for future references,
 			 * and proceed with parent.
 			 */
+
+			if (prev_nb_edge != 0) {
+				rc = __rte_bpf_validate_debug_evaluate_step(
+					debug, get_node_idx(bvf, node) + 1,
+					RTE_BPF_VALIDATE_DEBUG_EVENT_BRANCH_RETURN);
+				if (rc < 0)
+					break;
+			}
+
 			node->cur_edge = 0;
 			save_safe_eval_state(bvf, node);
+			prev_nb_edge = 0;
 			node = node->prev_node;
 
 			/* first node will not have prev, signalling finish */
@@ -2532,7 +2970,11 @@ evaluate(struct bpf_verifier *bvf)
 		__func__, bvf, rc,
 		stats.nb_eval, stats.nb_prune, stats.nb_save, stats.nb_restore);
 
-	return rc;
+	debug_rc = __rte_bpf_validate_debug_evaluate_finish(debug, rc);
+	rc = debug_rc < 0 ? debug_rc : rc;
+
+	/* Caller does not expect positive values. */
+	return RTE_MIN(0, rc);
 }
 
 static bool
diff --git a/lib/bpf/bpf_validate.h b/lib/bpf/bpf_validate.h
new file mode 100644
index 0000000000..9912f4fd5c
--- /dev/null
+++ b/lib/bpf/bpf_validate.h
@@ -0,0 +1,60 @@
+/* SPDX-License-Identifier: BSD-3-Clause
+ * Copyright(c) 2025 Huawei Technologies Co., Ltd
+ */
+
+#ifndef _BPF_VALIDATE_H_
+#define _BPF_VALIDATE_H_
+
+/**
+ * @file bpf_validate.h
+ *
+ * Internal-use headers for eBPF validation observability.
+ */
+
+#include <bpf_def.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct bpf_verifier;
+
+/*
+ * Return 1 if the verifier passes internal self-check,
+ * 0 if it fails, or a negative error code.
+ */
+int
+__rte_bpf_validate_state_is_valid(const struct bpf_verifier *verifier);
+
+/*
+ * Return 1 if the specified access instruction is valid,
+ * 0 if it is invalid, or a negative error code.
+ */
+int
+__rte_bpf_validate_can_access(const struct bpf_verifier *verifier,
+	const struct ebpf_insn *access, uint64_t off64);
+
+/* Get possible truth values of the specified jump condition. */
+int
+__rte_bpf_validate_may_jump(const struct bpf_verifier *verifier,
+	const struct ebpf_insn *jump, uint64_t imm64);
+
+/* Format known information about the register for the user. */
+int
+__rte_bpf_validate_format_register_info(const struct bpf_verifier *verifier,
+	char *buffer, size_t bufsz, uint8_t reg);
+
+/* Format known information about the frame location for the user. */
+int
+__rte_bpf_validate_format_frame_info(const struct bpf_verifier *verifier,
+	char *buffer, size_t bufsz, int32_t offset);
+
+/* Return frame size. */
+int32_t
+__rte_bpf_validate_get_frame_size(const struct bpf_verifier *verifier);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* _BPF_VALIDATE_H_ */
diff --git a/lib/bpf/bpf_validate_debug.c b/lib/bpf/bpf_validate_debug.c
new file mode 100644
index 0000000000..1817324a9f
--- /dev/null
+++ b/lib/bpf/bpf_validate_debug.c
@@ -0,0 +1,663 @@
+/* SPDX-License-Identifier: BSD-3-Clause
+ * Copyright(c) 2025 Huawei Technologies Co., Ltd
+ */
+
+#include "bpf_impl.h"
+#include "bpf_validate.h"
+#include "bpf_validate_debug.h"
+
+#include <eal_export.h>
+#include <rte_bpf_validate_debug.h>
+#include <rte_errno.h>
+#include <rte_per_lcore.h>
+
+#include <errno.h>
+#include <stddef.h>
+#include <stdlib.h>
+
+#ifndef LIST_FOREACH_SAFE
+/* We need this macro which neither Linux nor EAL for Linux include yet. */
+#define	LIST_FOREACH_SAFE(var, head, field, tvar)			\
+	for ((var) = LIST_FIRST((head));				\
+	    (var) && ((tvar) = LIST_NEXT((var), field), 1);		\
+	    (var) = (tvar))
+#else
+#ifdef RTE_EXEC_ENV_LINUX
+#error "Don't need LIST_FOREACH_SAFE in this version of DPDK anymore, remove it."
+#endif
+#endif
+
+#define EVENT_ARRAY_LENGTH RTE_BPF_VALIDATE_DEBUG_EVENT_END
+
+struct rte_bpf_validate_debug_point {
+	LIST_ENTRY(rte_bpf_validate_debug_point) list;
+	struct rte_bpf_validate_debug_callback callback;
+	uint32_t pc;
+};
+
+LIST_HEAD(point_list, rte_bpf_validate_debug_point);
+
+struct rte_bpf_validate_debug {
+	/* Accessible immediately after object creation. */
+	struct point_list pending_breakpoints;
+	struct point_list *catchpoint_lists;
+	struct rte_bpf_validate_debug_callback step_callback;
+
+	/* Accessible only after evaluate start. */
+	const struct bpf_verifier *verifier;
+	const struct rte_bpf_prm_ex *bpf_prm;
+	struct point_list *breakpoint_lists;
+	struct rte_bpf_validate_debug_point *last_point;
+	uint32_t pc;
+	/* Evaluate stage (only tracking `evaluate` part at the moment). */
+	bool evaluate_started;
+	bool evaluate_finished;
+	int evaluate_result;  /* Only valid if `evaluate_finished` is true. */
+};
+
+/* Point lists functions. */
+
+/* Destroy all points in the list. */
+static void
+point_list_destroy(struct point_list *point_list)
+{
+	struct rte_bpf_validate_debug_point *point, *next;
+
+	LIST_FOREACH_SAFE(point, point_list, list, next)
+		rte_bpf_validate_debug_point_destroy(point);
+
+	RTE_ASSERT(LIST_EMPTY(point_list));
+}
+
+/* Destroy all points in all lists in the array and free the array. */
+static void
+point_lists_destroy(struct point_list *point_lists, uint32_t length)
+{
+	if (point_lists == NULL)
+		return;
+
+	for (uint32_t pli = 0; pli != length; ++pli)
+		point_list_destroy(&point_lists[pli]);
+
+	free(point_lists);
+}
+
+/* Dynamically allocate and initialize an array of point lists. */
+static struct point_list *
+point_lists_create(uint32_t length)
+{
+	/* Allocate at least one element to avoid calloc(0, ...) shenanigans. */
+	struct point_list *const array =
+		calloc(RTE_MAX(1u, length), sizeof(*array));
+	if (array == NULL)
+		return NULL;
+
+	for (uint32_t pli = 0; pli != length; ++pli)
+		LIST_INIT(&array[pli]);
+
+	return array;
+}
+
+/* Move point to a different list. */
+static inline void
+point_move(struct rte_bpf_validate_debug_point *point,
+	struct point_list *destination)
+{
+	LIST_REMOVE(point, list);
+	LIST_INSERT_HEAD(destination, point, list);
+}
+
+/* Move all points between lists (the order is inverted). */
+static void
+points_move(struct point_list *source, struct point_list *destination)
+{
+	struct rte_bpf_validate_debug_point *point, *next;
+
+	LIST_FOREACH_SAFE(point, source, list, next)
+		point_move(point, destination);
+	RTE_ASSERT(LIST_EMPTY(source));
+}
+
+/* Pending breakpoints. */
+
+/* Return true if all pending breakpoints have pc less than nb_ins. */
+static bool
+debug_pending_breakpoints_are_valid(const struct rte_bpf_validate_debug *debug,
+	uint32_t nb_ins)
+{
+	const struct rte_bpf_validate_debug_point *breakpoint;
+
+	LIST_FOREACH(breakpoint, &debug->pending_breakpoints, list)
+		if (breakpoint->pc >= nb_ins)
+			return false;
+
+	return true;
+}
+
+/* Move all pending breakpoints to correct per-pc lists. */
+static void
+debug_pending_breakpoints_restore(struct rte_bpf_validate_debug *debug)
+{
+	struct rte_bpf_validate_debug_point *breakpoint, *next;
+	struct point_list breakpoints;
+
+	/* Invert the list first to preserve point order when we move them. */
+	LIST_INIT(&breakpoints);
+	points_move(&debug->pending_breakpoints, &breakpoints);
+
+	LIST_FOREACH_SAFE(breakpoint, &breakpoints, list, next)
+		point_move(breakpoint, &debug->breakpoint_lists[breakpoint->pc]);
+	RTE_ASSERT(LIST_EMPTY(&breakpoints));
+}
+
+/* Move all breakpoints from per-pc lists to the pending one. */
+static void
+debug_pending_breakpoints_save(struct rte_bpf_validate_debug *debug)
+{
+	struct point_list breakpoints;
+
+	LIST_INIT(&breakpoints);
+	for (uint32_t pc = 0; pc != debug->bpf_prm->raw.nb_ins; ++pc)
+		points_move(&debug->breakpoint_lists[pc], &breakpoints);
+
+	/* Invert the list to restore point order after we moved them. */
+	RTE_ASSERT(LIST_EMPTY(&debug->pending_breakpoints));
+	points_move(&breakpoints, &debug->pending_breakpoints);
+}
+
+/* Debug instance creation and destruction. */
+
+RTE_EXPORT_EXPERIMENTAL_SYMBOL(rte_bpf_validate_debug_destroy, 26.07)
+void
+rte_bpf_validate_debug_destroy(struct rte_bpf_validate_debug *debug)
+{
+	if (debug == NULL)
+		return;
+
+	/* Cannot destroy the instance during validation. */
+	RTE_ASSERT(!debug->evaluate_started);
+
+	point_lists_destroy(debug->catchpoint_lists, EVENT_ARRAY_LENGTH);
+	point_list_destroy(&debug->pending_breakpoints);
+	free(debug);
+}
+
+RTE_EXPORT_EXPERIMENTAL_SYMBOL(rte_bpf_validate_debug_create, 26.07)
+struct rte_bpf_validate_debug *
+rte_bpf_validate_debug_create(void)
+{
+	struct rte_bpf_validate_debug *const debug = calloc(1, sizeof(*debug));
+	if (debug == NULL) {
+		rte_errno = ENOMEM;
+		return NULL;
+	}
+
+	LIST_INIT(&debug->pending_breakpoints);
+
+	debug->catchpoint_lists = point_lists_create(EVENT_ARRAY_LENGTH);
+	if (debug->catchpoint_lists == NULL) {
+		free(debug);
+		rte_errno = ENOMEM;
+		return NULL;
+	}
+
+	return debug;
+}
+
+/* Managing callbacks. */
+
+/* Call back the user function with correct arguments for a point. */
+static inline int
+debug_point_call_back(struct rte_bpf_validate_debug *debug,
+	struct rte_bpf_validate_debug_point *point)
+{
+	debug->last_point = point;
+	return point->callback.fn(debug, point->callback.ctx);
+}
+
+/* Call back all points in point_list. */
+static int
+debug_points_call_back(struct rte_bpf_validate_debug *debug,
+	const struct point_list *point_list)
+{
+	struct rte_bpf_validate_debug_point *point, *next;
+	int rc = 0;
+
+	LIST_FOREACH_SAFE(point, point_list, list, next)
+		rc = rc < 0 ? rc : debug_point_call_back(debug, point);
+
+	return rc;
+}
+
+/* Call back all catchpoints for the specified event. */
+static int
+debug_send_event(struct rte_bpf_validate_debug *debug, debug_event_t event)
+{
+	return debug_points_call_back(debug, &debug->catchpoint_lists[event]);
+}
+
+/* Create new point and insert it into the specified list. */
+static struct rte_bpf_validate_debug_point *
+point_list_insert(struct point_list *point_list,
+	const struct rte_bpf_validate_debug_callback *callback, uint32_t pc)
+{
+	struct rte_bpf_validate_debug_point *const point =
+		malloc(sizeof(*point));
+	if (point == NULL) {
+		rte_errno = ENOMEM;
+		return NULL;
+	}
+
+	LIST_INSERT_HEAD(point_list, point, list);
+	point->callback = *callback;
+	point->pc = pc;
+	return point;
+}
+
+RTE_EXPORT_EXPERIMENTAL_SYMBOL(rte_bpf_validate_debug_break, 26.07)
+struct rte_bpf_validate_debug_point *
+rte_bpf_validate_debug_break(struct rte_bpf_validate_debug *debug, uint32_t pc,
+	const struct rte_bpf_validate_debug_callback *callback)
+{
+	if (debug == NULL || callback == NULL || callback->fn == NULL) {
+		rte_errno = EINVAL;
+		return NULL;
+	}
+
+	if (!debug->evaluate_started)
+		return point_list_insert(&debug->pending_breakpoints,
+			callback, pc);
+
+	if (pc >= debug->bpf_prm->raw.nb_ins) {
+		rte_errno = ENOENT;
+		return NULL;
+	}
+
+	return point_list_insert(&debug->breakpoint_lists[pc], callback, pc);
+}
+
+RTE_EXPORT_EXPERIMENTAL_SYMBOL(rte_bpf_validate_debug_catch, 26.07)
+struct rte_bpf_validate_debug_point *
+rte_bpf_validate_debug_catch(struct rte_bpf_validate_debug *debug,
+	debug_event_t event, const struct rte_bpf_validate_debug_callback *callback)
+{
+	if (debug == NULL || callback == NULL || callback->fn == NULL ||
+			event < 0 || event >= RTE_BPF_VALIDATE_DEBUG_EVENT_END) {
+		rte_errno = EINVAL;
+		return NULL;
+	}
+
+	return point_list_insert(&debug->catchpoint_lists[event], callback, 0);
+}
+
+RTE_EXPORT_EXPERIMENTAL_SYMBOL(rte_bpf_validate_debug_point_destroy, 26.07)
+void
+rte_bpf_validate_debug_point_destroy(struct rte_bpf_validate_debug_point *point)
+{
+	if (point == NULL)
+		return;
+
+	LIST_REMOVE(point, list);
+	free(point);
+}
+
+/* Querying execution state. */
+
+RTE_EXPORT_EXPERIMENTAL_SYMBOL(rte_bpf_validate_debug_get_bpf_param, 26.07)
+const struct rte_bpf_prm_ex *
+rte_bpf_validate_debug_get_bpf_param(const struct rte_bpf_validate_debug *debug)
+{
+	if (debug == NULL) {
+		rte_errno = EINVAL;
+		return NULL;
+	}
+
+	if (!debug->evaluate_started) {
+		rte_errno = ECHILD;
+		return NULL;
+	}
+
+	return debug->bpf_prm;
+}
+
+RTE_EXPORT_EXPERIMENTAL_SYMBOL(rte_bpf_validate_debug_get_ins, 26.07)
+int
+rte_bpf_validate_debug_get_ins(const struct rte_bpf_validate_debug *debug,
+	const struct ebpf_insn **ins, uint32_t *nb_ins)
+{
+	if (debug == NULL)
+		return -EINVAL;
+
+	if (!debug->evaluate_started)
+		return -ECHILD;
+
+	if (debug->bpf_prm->origin != RTE_BPF_ORIGIN_RAW)
+		return -ENOTSUP;
+
+	*ins = debug->bpf_prm->raw.ins;
+	*nb_ins = debug->bpf_prm->raw.nb_ins;
+	return 0;
+}
+
+RTE_EXPORT_EXPERIMENTAL_SYMBOL(rte_bpf_validate_debug_get_last_point, 26.07)
+struct rte_bpf_validate_debug_point *
+rte_bpf_validate_debug_get_last_point(const struct rte_bpf_validate_debug *debug)
+{
+	if (debug == NULL) {
+		rte_errno = EINVAL;
+		return NULL;
+	}
+
+	return debug->last_point;
+}
+
+RTE_EXPORT_EXPERIMENTAL_SYMBOL(rte_bpf_validate_debug_get_pc, 26.07)
+uint32_t
+rte_bpf_validate_debug_get_pc(const struct rte_bpf_validate_debug *debug)
+{
+	if (debug == NULL || !debug->evaluate_started)
+		return UINT32_MAX;
+
+	return debug->pc;
+}
+
+RTE_EXPORT_EXPERIMENTAL_SYMBOL(rte_bpf_validate_debug_get_validation_result, 26.07)
+int
+rte_bpf_validate_debug_get_validation_result(const struct rte_bpf_validate_debug *debug,
+	int *result)
+{
+	if (debug == NULL)
+		return -EINVAL;
+
+	if (!debug->evaluate_finished)
+		return -EAGAIN;
+
+	*result = debug->evaluate_result;
+
+	return 0;
+}
+
+/* Querying VM state. */
+
+RTE_EXPORT_EXPERIMENTAL_SYMBOL(rte_bpf_validate_debug_can_access, 26.07)
+int
+rte_bpf_validate_debug_can_access(const struct rte_bpf_validate_debug *debug,
+	const struct ebpf_insn *access, uint64_t off64)
+{
+	if (debug == NULL || access == NULL)
+		return -EINVAL;
+
+	if (!debug->evaluate_started)
+		return -ECHILD;
+
+	return __rte_bpf_validate_can_access(debug->verifier, access, off64);
+}
+
+RTE_EXPORT_EXPERIMENTAL_SYMBOL(rte_bpf_validate_debug_may_jump, 26.07)
+int
+rte_bpf_validate_debug_may_jump(const struct rte_bpf_validate_debug *debug,
+	const struct ebpf_insn *jump, uint64_t imm64)
+{
+	if (debug == NULL || jump == NULL)
+		return -EINVAL;
+
+	if (!debug->evaluate_started)
+		return -ECHILD;
+
+	return __rte_bpf_validate_may_jump(debug->verifier, jump, imm64);
+}
+
+/* Formatting VM state for user. */
+
+RTE_EXPORT_EXPERIMENTAL_SYMBOL(rte_bpf_validate_debug_format_register_info, 26.07)
+int
+rte_bpf_validate_debug_format_register_info(const struct rte_bpf_validate_debug *debug,
+	char *buffer, size_t bufsz, uint8_t reg)
+{
+	if (debug == NULL)
+		return -EINVAL;
+
+	if (!debug->evaluate_started)
+		return -ECHILD;
+
+	return __rte_bpf_validate_format_register_info(debug->verifier, buffer,
+		bufsz, reg);
+}
+
+RTE_EXPORT_EXPERIMENTAL_SYMBOL(rte_bpf_validate_debug_format_frame_info, 26.07)
+int
+rte_bpf_validate_debug_format_frame_info(const struct rte_bpf_validate_debug *debug,
+	char *buffer, size_t bufsz, int32_t offset)
+{
+	if (debug == NULL)
+		return -EINVAL;
+
+	if (!debug->evaluate_started)
+		return -ECHILD;
+
+	return __rte_bpf_validate_format_frame_info(debug->verifier, buffer,
+		bufsz, offset);
+}
+
+RTE_EXPORT_EXPERIMENTAL_SYMBOL(rte_bpf_validate_debug_get_frame_size, 26.07)
+int32_t
+rte_bpf_validate_debug_get_frame_size(const struct rte_bpf_validate_debug *debug)
+{
+	if (debug == NULL)
+		return -EINVAL;
+
+	if (!debug->evaluate_started)
+		return -ECHILD;
+
+	return __rte_bpf_validate_get_frame_size(debug->verifier);
+}
+
+/* Courtesy formatting functions for user-supplied values. */
+
+RTE_EXPORT_EXPERIMENTAL_SYMBOL(rte_bpf_validate_debug_format_value, 26.07)
+int
+rte_bpf_validate_debug_format_value(char *buffer, size_t bufsz, char format,
+	uint64_t value)
+{
+	static const struct {
+		uint64_t value;
+		const char *name;
+	} constants[] = {
+		{ .value = INT64_MIN, .name = "INT64_MIN" },
+		{ .value = INT32_MIN, .name = "INT32_MIN" },
+		{ .value = INT16_MIN, .name = "INT16_MIN" },
+		{ .value = INT8_MIN, .name = "INT8_MIN" },
+		{ .value = INT8_MAX, .name = "INT8_MAX" },
+		{ .value = UINT8_MAX, .name = "UINT8_MAX" },
+		{ .value = INT16_MAX, .name = "INT16_MAX" },
+		{ .value = UINT16_MAX, .name = "UINT16_MAX" },
+		{ .value = INT32_MAX, .name = "INT32_MAX" },
+		{ .value = UINT32_MAX, .name = "UINT32_MAX" },
+		{ .value = INT64_MAX, .name = "INT64_MAX" },
+		/* UINT64_MAX omitted on purpose, it looks better as -1 */
+	};
+
+	switch (format) {
+	case 'd':
+		for (int ci = 0; ci != RTE_DIM(constants); ++ci)
+			if (constants[ci].value == value)
+				return snprintf(buffer, bufsz, "%s", constants[ci].name);
+		/*
+		 * Special case numbers close to int32_t or int64_t range ends,
+		 * since they are hard to recognize in decimal otherwise.
+		 */
+		if (value - INT64_MIN < 1000000)
+			return snprintf(buffer, bufsz, "INT64_MIN+%" PRId64,
+				value - INT64_MIN);
+		if (INT64_MAX - value < 1000000)
+			return snprintf(buffer, bufsz, "INT64_MAX-%" PRId64,
+				INT64_MAX - value);
+		if (value - INT32_MIN < 1000)
+			return snprintf(buffer, bufsz, "INT32_MIN+%" PRId64,
+				value - INT32_MIN);
+		if (INT32_MAX - value < 1000)
+			return snprintf(buffer, bufsz, "INT32_MAX-%" PRId64,
+				INT32_MAX - value);
+		return snprintf(buffer, bufsz, "%" PRId64, value);
+	case 'x':
+		/* Special case only the common case of UINT64_MAX. */
+		if (value == UINT64_MAX)
+			return snprintf(buffer, bufsz, "%s", "UINT64_MAX");
+		return snprintf(buffer, bufsz, "%#" PRIx64, value);
+	default:
+		return -EINVAL;
+	}
+}
+
+RTE_EXPORT_EXPERIMENTAL_SYMBOL(rte_bpf_validate_debug_format_interval, 26.07)
+int
+rte_bpf_validate_debug_format_interval(char *buffer, size_t bufsz, char format,
+	uint64_t min, uint64_t max)
+{
+	char min_buffer[32], max_buffer[32];
+	int rc;
+
+	if (min == max)
+		return rte_bpf_validate_debug_format_value(buffer, bufsz, format, min);
+
+	rc = rte_bpf_validate_debug_format_value(min_buffer, sizeof(min_buffer), format, min);
+	if (rc < 0)
+		return rc;
+
+	rc = rte_bpf_validate_debug_format_value(max_buffer, sizeof(max_buffer), format, max);
+	if (rc < 0)
+		return rc;
+
+	return snprintf(buffer, bufsz, "%s..%s", min_buffer, max_buffer);
+}
+
+/* Evaluation start and finish. */
+
+/* Free all resources associated with current evaluation. */
+static void
+debug_evaluate_close(struct rte_bpf_validate_debug *debug)
+{
+	RTE_ASSERT(debug->evaluate_started);
+	debug_pending_breakpoints_save(debug);
+	free(debug->breakpoint_lists);
+	debug->breakpoint_lists = NULL;
+	debug->evaluate_started = false;
+}
+
+int
+__rte_bpf_validate_debug_evaluate_start(struct rte_bpf_validate_debug *debug,
+	const struct bpf_verifier *verifier, const struct rte_bpf_prm_ex *bpf_prm)
+{
+	if (debug == NULL)
+		return 0;
+
+	if (verifier == NULL || bpf_prm == NULL ||
+			bpf_prm->origin != RTE_BPF_ORIGIN_RAW)
+		return -EINVAL;
+
+	if (debug->evaluate_started) {
+		RTE_BPF_LOG_FUNC_LINE(ERR, "already started");
+		return -EEXIST;
+	}
+
+	if (!debug_pending_breakpoints_are_valid(debug, bpf_prm->raw.nb_ins))
+		return -ENOENT;
+
+	debug->verifier = verifier;
+	debug->bpf_prm = bpf_prm;
+	debug->breakpoint_lists = point_lists_create(bpf_prm->raw.nb_ins);
+	if (debug->breakpoint_lists == NULL)
+		return -ENOMEM;
+	debug_pending_breakpoints_restore(debug);
+	debug->last_point = NULL;
+	debug->pc = 0;
+	debug->evaluate_started = true;
+
+	const int rc = debug_send_event(debug,
+		RTE_BPF_VALIDATE_DEBUG_EVENT_VALIDATION_START);
+	if (rc < 0) {
+		debug_evaluate_close(debug);
+		return rc;
+	}
+
+	RTE_BPF_LOG_FUNC_LINE(DEBUG, "evaluate started");
+	return 0;
+}
+
+int
+__rte_bpf_validate_debug_evaluate_step(struct rte_bpf_validate_debug *debug,
+	uint32_t pc, debug_event_t event)
+{
+	int rc;
+
+	if (debug == NULL)
+		return 0;
+
+	if (!debug->evaluate_started) {
+		RTE_BPF_LOG_FUNC_LINE(ERR, "not started");
+		return -ECHILD;
+	}
+
+	if (pc > debug->bpf_prm->raw.nb_ins || event < 0 ||
+			event >= RTE_BPF_VALIDATE_DEBUG_EVENT_END)
+		return -EINVAL;
+
+	debug->pc = pc;
+
+	rc = __rte_bpf_validate_state_is_valid(debug->verifier);
+	if (rc == 0)
+		rc = debug_send_event(debug,
+			RTE_BPF_VALIDATE_DEBUG_EVENT_INVALID_STATE);
+
+	if (event != RTE_BPF_VALIDATE_DEBUG_EVENT_STEP)
+		rc = rc < 0 ? rc : debug_send_event(debug, event);
+
+	if (event == RTE_BPF_VALIDATE_DEBUG_EVENT_STEP ||
+			event == RTE_BPF_VALIDATE_DEBUG_EVENT_BRANCH_ENTER)
+		/* Stepping into a real instruction to execute. */
+		rc = rc < 0 ? rc : debug_points_call_back(debug,
+			&debug->breakpoint_lists[pc]);
+
+	rc = rc < 0 ? rc : debug_send_event(debug,
+		RTE_BPF_VALIDATE_DEBUG_EVENT_STEP);
+
+	return rc;
+}
+
+int
+__rte_bpf_validate_debug_evaluate_finish(struct rte_bpf_validate_debug *debug,
+	int result)
+{
+	int rc = 0;
+	uint32_t pc;
+	debug_event_t event;
+
+	if (debug == NULL)
+		return 0;
+
+	if (!debug->evaluate_started) {
+		RTE_BPF_LOG_FUNC_LINE(ERR, "not started");
+		return -ECHILD;
+	}
+
+	debug->evaluate_finished = true;
+	debug->evaluate_result = result;
+
+	if (result != -ECANCELED) {
+		if (result < 0) {
+			/* Last known pc is the place we failed. */
+			pc = debug->pc;
+			event = RTE_BPF_VALIDATE_DEBUG_EVENT_VALIDATION_FAILURE;
+		} else {
+			/* Show program end, not particular instruction. */
+			pc = debug->bpf_prm->raw.nb_ins;
+			event = RTE_BPF_VALIDATE_DEBUG_EVENT_VALIDATION_SUCCESS;
+		}
+
+		rc = __rte_bpf_validate_debug_evaluate_step(debug, pc, event);
+	}
+
+	debug_evaluate_close(debug);
+
+	return rc;
+}
diff --git a/lib/bpf/bpf_validate_debug.h b/lib/bpf/bpf_validate_debug.h
new file mode 100644
index 0000000000..a91f3e9c48
--- /dev/null
+++ b/lib/bpf/bpf_validate_debug.h
@@ -0,0 +1,86 @@
+/* SPDX-License-Identifier: BSD-3-Clause
+ * Copyright(c) 2025 Huawei Technologies Co., Ltd
+ */
+
+#ifndef _BPF_VALIDATE_DEBUG_H_
+#define _BPF_VALIDATE_DEBUG_H_
+
+/**
+ * @file bpf_validate_debug.h
+ *
+ * Internal-use headers for eBPF validation debug notifications.
+ */
+
+#include "rte_bpf_validate_debug.h"
+
+#include <stdint.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct rte_bpf_prm_ex;
+struct rte_bpf_validate_debug;
+struct bpf_verifier;
+
+/* Type alias for validation event enum. */
+typedef enum rte_bpf_validate_debug_event debug_event_t;
+
+/*
+ * Signal beginning of evaluation process.
+ *
+ * Immediately return 0 if debug is NULL.
+ *
+ * @param debug
+ *   Validate debug instance configured by user, can be NULL.
+ * @param verifier
+ *   Opaque pointer that can be used for calling bpf_validate.h API.
+ * @param bpf_prm
+ *   Parameters struct of the validated eBPF program, including code with all
+ *   patches and relocations applied.
+ * @return
+ *   Non-negative value on success, negative errno on failure.
+ */
+int
+__rte_bpf_validate_debug_evaluate_start(struct rte_bpf_validate_debug *debug,
+	const struct bpf_verifier *verifier, const struct rte_bpf_prm_ex *bpf_prm);
+
+/*
+ * Signal each instruction, branch end, or evaluation end.
+ *
+ * Immediately return 0 if debug is NULL.
+ *
+ * @param debug
+ *   Validate debug instance configured by user, can be NULL.
+ * @param pc
+ *   Current value of the program counter, or next after last instruction.
+ * @param event
+ *   Specific evaluation event if any, or RTE_BPF_VALIDATE_DEBUG_EVENT_STEP.
+ * @return
+ *   Non-negative value: evaluation should continue;
+ *   -ECANCELED: evaluation should fail without calling this API again;
+ *   Other negative value: evaluation should fail signalling failure;
+ */
+int
+__rte_bpf_validate_debug_evaluate_step(struct rte_bpf_validate_debug *debug,
+	uint32_t pc, debug_event_t event);
+
+/*
+ * Signal end of evaluation process.
+ *
+ * Immediately return 0 if debug is NULL.
+ *
+ * @param debug
+ *   Validate debug instance configured by user, can be NULL.
+ * @return
+ *   Non-negative value on success, negative errno on failure.
+ */
+int
+__rte_bpf_validate_debug_evaluate_finish(struct rte_bpf_validate_debug *debug,
+	int result);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* _BPF_VALIDATE_DEBUG_H_ */
diff --git a/lib/bpf/bpf_value_set.c b/lib/bpf/bpf_value_set.c
new file mode 100644
index 0000000000..86f46de66f
--- /dev/null
+++ b/lib/bpf/bpf_value_set.c
@@ -0,0 +1,403 @@
+/* SPDX-License-Identifier: BSD-3-Clause
+ * Copyright(c) 2026 Huawei Technologies Co., Ltd
+ */
+
+#include "bpf_value_set.h"
+
+#include <rte_debug.h>
+
+/* Helper interval operations and checks.  */
+
+/* One of many possible full intervals. */
+static const struct value_set_interval canonical_full_interval = {
+	.first = 0,
+	.last = UINT64_MAX,
+};
+
+/* Translate ("shift") interval by `offset`. */
+static void
+interval_translate(struct value_set_interval *interval, uint64_t offset)
+{
+	interval->first += offset;
+	interval->last += offset;
+}
+
+/* Return true if the interval includes all possible values. */
+static bool
+interval_is_full(struct value_set_interval interval)
+{
+	return interval.last + 1 == interval.first;
+}
+
+/* Return true if the interval includes `value`. */
+static bool
+interval_contains(struct value_set_interval interval, uint64_t value)
+{
+	return value - interval.first <= interval.last - interval.first;
+}
+
+/* Return true if the interval `lhs` includes all values from `rhs`. */
+static bool
+interval_covers(struct value_set_interval lhs, struct value_set_interval rhs)
+{
+	const uint64_t offset = -lhs.first;
+	interval_translate(&lhs, offset);
+	interval_translate(&rhs, offset);
+	RTE_ASSERT(lhs.first == 0);
+
+	return lhs.last == UINT64_MAX ||
+		(lhs.last >= rhs.last && rhs.last >= rhs.first);
+}
+
+/* Return true if the interval includes step from UINT64_MAX to 0. */
+static bool
+interval_crosses_zero(struct value_set_interval interval)
+{
+	return interval.last < interval.first;
+}
+
+/* Return number of elements in a non-full elements, 0 for full interval. */
+static uint64_t
+interval_size(struct value_set_interval interval)
+{
+	return interval.last - interval.first + 1;
+}
+
+/* Return true if two intervals represent same sets of values. */
+static bool
+intervals_equal(struct value_set_interval lhs, struct value_set_interval rhs)
+{
+	return (interval_is_full(lhs) && interval_is_full(rhs)) ||
+		(lhs.first == rhs.first && lhs.last == rhs.last);
+}
+
+/* Return true if two intervals have common elements. */
+static bool
+intervals_intersect(struct value_set_interval lhs, struct value_set_interval rhs)
+{
+	return interval_contains(lhs, rhs.first) || interval_contains(rhs, lhs.first);
+}
+
+/* Return true if `rhs.first` follows `lhs.last` with some gap. Does not check other ends! */
+static bool
+intervals_follow_with_gap(struct value_set_interval lhs, struct value_set_interval rhs)
+{
+	return lhs.last != UINT64_MAX && rhs.first > lhs.last + 1;
+}
+
+/* Return true if `(l - o) < (r - o)` for all `(o in origin, l in lhs, r in rhs)`. */
+static bool
+intervals_based_less(struct value_set_interval origin, struct value_set_interval lhs,
+	struct value_set_interval rhs)
+{
+	/* Translate all intervals for the origin to start at 0. */
+	const uint64_t offset = -origin.first;
+	interval_translate(&origin, offset);
+	interval_translate(&lhs, offset);
+	interval_translate(&rhs, offset);
+	RTE_ASSERT(origin.first == 0);
+
+	return origin.last <= lhs.first &&
+		lhs.first <= lhs.last &&
+		lhs.last < rhs.first &&
+		rhs.first <= rhs.last;
+}
+
+/* Return true if `(l - o) <= (r - o)` for all `(o in origin, l in lhs, r in rhs)`. */
+static bool
+intervals_based_less_or_equal(struct value_set_interval origin, struct value_set_interval lhs,
+	struct value_set_interval rhs)
+{
+	/* Translate all intervals for the origin to start at 0. */
+	const uint64_t offset = -origin.first;
+	interval_translate(&origin, offset);
+	interval_translate(&lhs, offset);
+	interval_translate(&rhs, offset);
+	RTE_ASSERT(origin.first == 0);
+
+	/* Special cases. */
+	if (origin.last == 0 && lhs.first == 0 && lhs.last == 0)
+		return true;
+	if (origin.last == 0 && rhs.first == UINT64_MAX && rhs.last == UINT64_MAX)
+		return true;
+	if (lhs.first == lhs.last && lhs.last == rhs.first && rhs.first == rhs.last)
+		return true;
+
+	return origin.last <= lhs.first &&
+		lhs.first <= lhs.last &&
+		lhs.last <= rhs.first &&
+		rhs.first <= rhs.last;
+}
+
+/* Append interval rhs to list of intervals in lhs. */
+static void
+value_set_append(struct value_set *lhs, struct value_set_interval rhs)
+{
+	RTE_VERIFY(lhs->nb_interval < VALUE_SET_NB_INTERVAL_MAX);
+	RTE_VERIFY(lhs->nb_interval == 0 ||
+		intervals_follow_with_gap(lhs->interval[lhs->nb_interval - 1], rhs));
+	lhs->interval[lhs->nb_interval++] = rhs;
+}
+
+/*
+ * Helper operations on noncyclic value set and intervals.
+ * Noncyclic means no interval crosses zero,
+ * but in return last value set interval may touch first.
+ */
+
+static struct value_set
+noncyclic_value_set_union_interval(const struct value_set *lhs, const struct value_set_interval rhs)
+{
+	struct value_set result = {};
+	uint32_t index = 0;
+
+	RTE_ASSERT(lhs->nb_interval == 0 ||
+		!interval_crosses_zero(lhs->interval[lhs->nb_interval - 1]));
+	RTE_ASSERT(!interval_crosses_zero(rhs));
+
+	/* Append to result all lhs intervals preceding rhs. */
+	for (; index != lhs->nb_interval; ++index) {
+		const struct value_set_interval lhs_interval = lhs->interval[index];
+		if (!intervals_follow_with_gap(lhs_interval, rhs))
+			break;
+
+		value_set_append(&result, lhs_interval);
+	}
+
+	/* Appendinterval joined from rhs and all lhs intervals intersecting or touching it. */
+	struct value_set_interval joint_interval = rhs;
+	for (; index != lhs->nb_interval; ++index) {
+		const struct value_set_interval lhs_interval = lhs->interval[index];
+		if (intervals_follow_with_gap(rhs, lhs_interval))
+			break;
+
+		joint_interval.first = RTE_MIN(joint_interval.first, lhs_interval.first);
+		joint_interval.last = RTE_MAX(joint_interval.last, lhs_interval.last);
+	}
+	value_set_append(&result, joint_interval);
+
+	/* Append to result all lhs intervals following rhs. */
+	for (; index != lhs->nb_interval; ++index)
+		value_set_append(&result, lhs->interval[index]);
+
+	return result;
+}
+
+/* Make "normal" maximal disjoint interval value set out of noncyclic one. */
+static struct value_set
+value_set_from_noncyclic(const struct value_set *set)
+{
+	struct value_set result = {};
+	uint32_t index = 0;
+
+	if (set->nb_interval <= 1)
+		return *set;
+
+	struct value_set_interval last_interval = set->interval[set->nb_interval - 1];
+	if (last_interval.last == UINT64_MAX && set->interval[0].first == 0) {
+		/* Join first interval with the last one instead of copying it. */
+		last_interval.last = set->interval[0].last;
+		++index;
+	}
+
+	for (; index != set->nb_interval - 1; ++index)
+		value_set_append(&result, set->interval[index]);
+
+	value_set_append(&result, last_interval);
+
+	return result;
+}
+
+/* Make lhs a union of lhs and rhs. */
+static void
+value_set_union_interval(struct value_set *lhs, const struct value_set_interval rhs)
+{
+	struct value_set temp;
+
+	if (value_set_is_empty(lhs)) {
+		value_set_append(lhs, rhs);
+		return;
+	}
+
+	struct value_set_interval *const last_interval = &lhs->interval[lhs->nb_interval - 1];
+	const bool last_interval_crossed_zero = interval_crosses_zero(*last_interval);
+	const uint64_t wrapping_last = last_interval->last;
+
+	if (last_interval_crossed_zero)
+		/* Make value set noncyclic by removing crossing part of last interval. */
+		last_interval->last = UINT64_MAX;
+
+	if (interval_crosses_zero(rhs)) {
+		/* Add parts before and after zero separately. */
+		temp = noncyclic_value_set_union_interval(lhs,
+			(struct value_set_interval){
+				.first = rhs.first,
+				.last = UINT64_MAX,
+			});
+		temp = noncyclic_value_set_union_interval(lhs,
+			(struct value_set_interval){
+				.first = 0,
+				.last = rhs.last,
+			});
+	} else
+		temp = noncyclic_value_set_union_interval(lhs, rhs);
+
+	if (last_interval_crossed_zero)
+		/* Restore previously removed part. */
+		temp = noncyclic_value_set_union_interval(&temp,
+			(struct value_set_interval){
+				.first = 0,
+				.last = wrapping_last,
+			});
+
+	*lhs = value_set_from_noncyclic(&temp);
+}
+
+/* Set `lhs` to the set of possible sums between values from `lhs` and `rhs`. */
+static void
+value_set_add_interval(struct value_set *lhs, struct value_set_interval rhs)
+{
+	const struct value_set temp = *lhs;
+	lhs->nb_interval = 0;
+
+	for (uint32_t index = 0; index != temp.nb_interval; ++index) {
+		const struct value_set_interval interval = temp.interval[index];
+		if (interval_is_full(rhs) || interval_is_full(interval) ||
+				interval_size(interval) > UINT64_MAX - interval_size(rhs)) {
+			value_set_append(lhs, canonical_full_interval);
+			return;
+		}
+	}
+
+	for (uint32_t index = 0; index != temp.nb_interval; ++index)
+		value_set_union_interval(lhs, (struct value_set_interval){
+			/* Checked sizes above, so these interval expansions won't overflow. */
+			.first = temp.interval[index].first + rhs.first,
+			.last = temp.interval[index].last + rhs.last,
+		});
+}
+
+struct value_set
+value_set_singleton(uint64_t value)
+{
+	return value_set_contiguous(value, value);
+}
+
+struct value_set
+value_set_contiguous(uint64_t first, uint64_t last)
+{
+	return (struct value_set){
+		.nb_interval = 1,
+		.interval = {
+			{ .first = first, .last = last },
+		},
+	};
+}
+
+struct value_set
+value_set_from_pair(uint64_t first1, uint64_t last1, uint64_t first2, uint64_t last2)
+{
+	struct value_set result = {};
+
+	if (first1 - first2 <= last2 - first2)
+		/* Interval 1 starts within interval 2. */
+		value_set_union_interval(&result, (struct value_set_interval){
+				.first = first1,
+				.last = first1 + RTE_MIN(last1 - first1, last2 - first1),
+			});
+
+	if (first2 - first1 <= last1 - first1)
+		/* Interval 2 starts within interval 1. */
+		value_set_union_interval(&result, (struct value_set_interval){
+				.first = first2,
+				.last = first2 + RTE_MIN(last2 - first2, last1 - first2),
+			});
+
+	return result;
+}
+
+bool
+value_set_is_empty(const struct value_set *set)
+{
+	return set->nb_interval == 0;
+}
+
+bool
+value_set_is_singleton(const struct value_set *set)
+{
+	return set->nb_interval == 1 && interval_size(set->interval[0]) == 1;
+}
+
+bool
+value_sets_equal(const struct value_set *lhs, const struct value_set *rhs)
+{
+	if (lhs->nb_interval != rhs->nb_interval)
+		return false;
+
+	for (uint32_t index = 0; index != lhs->nb_interval; ++index)
+		if (!intervals_equal(lhs->interval[index], rhs->interval[index]))
+			return false;
+
+	return true;
+}
+
+bool
+value_sets_intersect(const struct value_set *lhs, const struct value_set *rhs)
+{
+	for (uint32_t lhs_index = 0; lhs_index != lhs->nb_interval; ++lhs_index)
+		for (uint32_t rhs_index = 0; rhs_index != rhs->nb_interval; ++rhs_index)
+			if (intervals_intersect(lhs->interval[lhs_index], rhs->interval[rhs_index]))
+				return true;
+
+	return false;
+}
+
+bool
+value_set_is_covered_by_contiguous(const struct value_set *lhs, uint64_t first, uint64_t last)
+{
+	const struct value_set_interval rhs = { .first = first, .last = last };
+	for (uint32_t lhs_index = 0; lhs_index != lhs->nb_interval; ++lhs_index)
+		if (!interval_covers(rhs, lhs->interval[lhs_index]))
+			return false;
+
+	return true;
+}
+
+bool
+value_sets_based_less(const struct value_set *origin, const struct value_set *lhs,
+	const struct value_set *rhs)
+{
+	for (uint32_t origin_index = 0; origin_index != origin->nb_interval; ++origin_index)
+		for (uint32_t lhs_index = 0; lhs_index != lhs->nb_interval; ++lhs_index)
+			for (uint32_t rhs_index = 0; rhs_index != rhs->nb_interval; ++rhs_index)
+				if (!intervals_based_less(origin->interval[origin_index],
+						lhs->interval[lhs_index], rhs->interval[rhs_index]))
+					return false;
+	return true;
+}
+
+bool
+value_sets_based_less_or_equal(const struct value_set *origin, const struct value_set *lhs,
+	const struct value_set *rhs)
+{
+	for (uint32_t origin_index = 0; origin_index != origin->nb_interval; ++origin_index)
+		for (uint32_t lhs_index = 0; lhs_index != lhs->nb_interval; ++lhs_index)
+			for (uint32_t rhs_index = 0; rhs_index != rhs->nb_interval; ++rhs_index)
+				if (!intervals_based_less_or_equal(origin->interval[origin_index],
+						lhs->interval[lhs_index], rhs->interval[rhs_index]))
+					return false;
+	return true;
+}
+
+void
+value_set_translate(struct value_set *set, uint64_t offset)
+{
+	for (uint32_t index = 0; index != set->nb_interval; ++index)
+		interval_translate(&set->interval[index], offset);
+}
+
+void
+value_set_add_contiguous(struct value_set *lhs, uint64_t first, uint64_t last)
+{
+	value_set_add_interval(lhs, (struct value_set_interval){ .first = first, .last = last });
+}
diff --git a/lib/bpf/bpf_value_set.h b/lib/bpf/bpf_value_set.h
new file mode 100644
index 0000000000..5e7f8e521f
--- /dev/null
+++ b/lib/bpf/bpf_value_set.h
@@ -0,0 +1,126 @@
+/* SPDX-License-Identifier: BSD-3-Clause
+ * Copyright(c) 2026 Huawei Technologies Co., Ltd
+ */
+
+#ifndef _BPF_VALUE_SET_H_
+#define _BPF_VALUE_SET_H_
+
+/**
+ * @file value_set.h
+ *
+ * Value set operations for BPF validate debug.
+ *
+ * This is not a general use library, only minimal set of operations is provided
+ * that are necessary for implementing validate debug interface.
+ */
+
+#include <stdbool.h>
+#include <stdint.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define VALUE_SET_NB_INTERVAL_MAX 3
+
+/*
+ * Cyclic interval on uint64_t.
+ *
+ * Cyclic means value of `last` might be numerically smaller than `first`,
+ * that is the interval may cross from UINT64_MAX to 0.
+ *
+ * Contains element `first` and all elements that can be obtained from it by
+ * adding 1 until the result reaches `last`, which is included.
+ * There is thus multiple representations of the full set and no representation
+ * of the empty set.
+ *
+ * When `first` and `last` are accepted separately as function arguments, the
+ * term _contiguous_ is being used. It means that values of `first` and `last`
+ * are used to create a contiguous set composed of a single cyclic interval
+ * defined by these points.
+ */
+struct value_set_interval {
+	uint64_t first;
+	uint64_t last;
+};
+
+/*
+ * Set of values represented as an ordered sequence of maximal disjoint cyclic intervals.
+ *
+ * Condition `maximal disjoint` means intervals do not intersect or touch each other.
+ *
+ * The sequence is ordered by member `first`. Only last interval may thus cross zero.
+ */
+struct value_set {
+	uint32_t nb_interval;
+	struct value_set_interval interval[VALUE_SET_NB_INTERVAL_MAX];
+};
+
+/* Empty value set. */
+static const struct value_set value_set_empty = {
+	.nb_interval = 0,
+};
+
+/* Full (including every possible value) value set. */
+static const struct value_set value_set_full = {
+	.nb_interval = 1,
+	.interval = {
+		{ .first = 0, .last = UINT64_MAX },
+	},
+};
+
+/* Return set containing only `value`. */
+struct value_set
+value_set_singleton(uint64_t value);
+
+/* Return set of all values between and including `first` and `last` (AKA first..last). */
+struct value_set
+value_set_contiguous(uint64_t first, uint64_t last);
+
+/* Return set of all values belonging to _both_ first1..last1 and first2..last. */
+struct value_set
+value_set_from_pair(uint64_t first1, uint64_t last1, uint64_t first2, uint64_t last2);
+
+/* Return true if the set is empty. */
+bool
+value_set_is_empty(const struct value_set *set);
+
+/* Return true if the set only contains one element. */
+bool
+value_set_is_singleton(const struct value_set *set);
+
+/* Return true if lhs and rhs represent the same set. */
+bool
+value_sets_equal(const struct value_set *lhs, const struct value_set *rhs);
+
+/* Return true if sets intersect (contain common elements). */
+bool
+value_sets_intersect(const struct value_set *lhs, const struct value_set *rhs);
+
+/* Return true if all elements in lhs belong to interval first..last */
+bool
+value_set_is_covered_by_contiguous(const struct value_set *lhs, uint64_t first, uint64_t last);
+
+/* Return true if `(l - o) < (r - o)` for all `(o in origin, l in lhs, r in rhs)`. */
+bool
+value_sets_based_less(const struct value_set *origin, const struct value_set *lhs,
+	const struct value_set *rhs);
+
+/* Return true if `(l - o) <= (r - o)` for all `(o in origin, l in lhs, r in rhs)`. */
+bool
+value_sets_based_less_or_equal(const struct value_set *origin, const struct value_set *lhs,
+	const struct value_set *rhs);
+
+/* Translate ("shift") all set elements by `offset`. */
+void
+value_set_translate(struct value_set *lhs, uint64_t rhs);
+
+/* Set `lhs` to the set of possible sums between values from `lhs` and `rhs`. */
+void
+value_set_add_contiguous(struct value_set *lhs, uint64_t first, uint64_t last);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* _BPF_VALUE_SET_H */
diff --git a/lib/bpf/meson.build b/lib/bpf/meson.build
index 7e8a300e3f..b74a5c2321 100644
--- a/lib/bpf/meson.build
+++ b/lib/bpf/meson.build
@@ -24,6 +24,8 @@ sources = files(
         'bpf_load_elf.c',
         'bpf_pkt.c',
         'bpf_validate.c',
+        'bpf_validate_debug.c',
+        'bpf_value_set.c',
 )
 
 if arch_subdir == 'x86' and dpdk_conf.get('RTE_ARCH_64')
@@ -32,9 +34,12 @@ elif dpdk_conf.has('RTE_ARCH_ARM64')
     sources += files('bpf_jit_arm64.c')
 endif
 
-headers = files('bpf_def.h',
+headers = files(
+        'bpf_def.h',
         'rte_bpf.h',
-        'rte_bpf_ethdev.h')
+        'rte_bpf_ethdev.h',
+        'rte_bpf_validate_debug.h',
+)
 
 deps += ['mbuf', 'net', 'ethdev']
 
diff --git a/lib/bpf/rte_bpf.h b/lib/bpf/rte_bpf.h
index b6c232704a..052849945c 100644
--- a/lib/bpf/rte_bpf.h
+++ b/lib/bpf/rte_bpf.h
@@ -118,6 +118,7 @@ enum rte_bpf_origin {
 };
 
 struct bpf_insn;
+struct rte_bpf_validate_debug;
 
 /**
  * Input parameters for loading eBPF code, extensible version.
@@ -158,6 +159,9 @@ struct rte_bpf_prm_ex {
 
 	struct rte_bpf_arg prog_arg[EBPF_FUNC_MAX_ARGS];  /**< program arguments */
 	uint32_t nb_prog_arg;  /**< program argument count */
+
+	/* Validate debug instance. */
+	struct rte_bpf_validate_debug *debug;
 };
 
 /**
diff --git a/lib/bpf/rte_bpf_validate_debug.h b/lib/bpf/rte_bpf_validate_debug.h
new file mode 100644
index 0000000000..89bf587f02
--- /dev/null
+++ b/lib/bpf/rte_bpf_validate_debug.h
@@ -0,0 +1,375 @@
+/* SPDX-License-Identifier: BSD-3-Clause
+ * Copyright(c) 2025 Huawei Technologies Co., Ltd
+ */
+
+#ifndef _RTE_BPF_VALIDATE_DEBUG_H_
+#define _RTE_BPF_VALIDATE_DEBUG_H_
+
+/**
+ * @file rte_bpf_validate_debug.h
+ *
+ * Debugging interface for BPF validation.
+ *
+ * Can be used for debugging BPF validation problems as well as in tests.
+ */
+
+#include <bpf_def.h>
+#include <rte_compat.h>
+
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define RTE_BPF_VALIDATE_DEBUG_MAY_BE_FALSE	RTE_BIT32(0)
+#define RTE_BPF_VALIDATE_DEBUG_MAY_BE_TRUE	RTE_BIT32(1)
+
+/**
+ * Supported validate events.
+ *
+ * Valid events begin from 0 and end before `RTE_BPF_VALIDATE_DEBUG_EVENT_END`.
+ */
+enum rte_bpf_validate_debug_event {
+	/* Just before every instruction, at branch or validation end. */
+	RTE_BPF_VALIDATE_DEBUG_EVENT_STEP,
+	/* Validator has failed its internal self-checks. */
+	RTE_BPF_VALIDATE_DEBUG_EVENT_INVALID_STATE,
+	/* Start of validation. */
+	RTE_BPF_VALIDATE_DEBUG_EVENT_VALIDATION_START,
+	/* Successful finish of validation. */
+	RTE_BPF_VALIDATE_DEBUG_EVENT_VALIDATION_SUCCESS,
+	/* Finish of validation with error. */
+	RTE_BPF_VALIDATE_DEBUG_EVENT_VALIDATION_FAILURE,
+	/* Beginning of a branch just after the jump. */
+	RTE_BPF_VALIDATE_DEBUG_EVENT_BRANCH_ENTER,
+	/* Pruning branch as verified earlier. */
+	RTE_BPF_VALIDATE_DEBUG_EVENT_BRANCH_PRUNE,
+	/* End of branch verification, after the last verified instruction. */
+	RTE_BPF_VALIDATE_DEBUG_EVENT_BRANCH_RETURN,
+	/* Number of valid event values. */
+	RTE_BPF_VALIDATE_DEBUG_EVENT_END,
+};
+
+struct rte_bpf_validate_debug;
+struct rte_bpf_validate_debug_point;
+
+/** User callback description. */
+struct rte_bpf_validate_debug_callback {
+	int (*fn)(struct rte_bpf_validate_debug *debug, void *ctx);
+	void *ctx;
+};
+
+/** Invoked by rte_bpf_validate_debug_for_each_point for each breakpoint and catchpoint. */
+typedef int (*rte_bpf_validate_debug_point_process_t)(struct rte_bpf_validate_debug_point *point,
+	void *ctx);
+
+/**
+ * Create new debug instance.
+ *
+ * @return
+ *   Debug instance in case of success.
+ *   NULL with rte_errno set in case of a failure.
+ */
+__rte_experimental
+struct rte_bpf_validate_debug *
+rte_bpf_validate_debug_create(void);
+
+/**
+ * Destroy debug instance.
+ *
+ * Behavior is undefined if validation with this debug instance is ongoing.
+ *
+ * @param debug
+ *   Debug instance, or NULL.
+ */
+__rte_experimental
+void
+rte_bpf_validate_debug_destroy(struct rte_bpf_validate_debug *debug);
+
+/**
+ * Create new breakpoint at specified location.
+ *
+ * Can be called before the validation has started. If at validation start later
+ * the program will not have the specified instruction, the start will fail.
+ *
+ * It is allowed to create breakpoints for the same location a callback is
+ * currently executing for, but it will not be invoked in the same cycle.
+ *
+ * @param debug
+ *   Debug instance.
+ * @param pc
+ *   Program counter to create breakpoint at.
+ * @param callback
+ *   Callback to invoke.
+ * @return
+ *   New breakpoint on success, NULL with rte_errno set on failure.
+ */
+__rte_experimental
+struct rte_bpf_validate_debug_point *
+rte_bpf_validate_debug_break(struct rte_bpf_validate_debug *debug, uint32_t pc,
+	const struct rte_bpf_validate_debug_callback *callback);
+
+/**
+ * Create new catchpoint for specified event.
+ *
+ * Can be called before the validation has started.
+ *
+ * It is allowed to create catchpoints for the same event a callback is
+ * currently executing for, but it will not be invoked in the same cycle.
+ *
+ * @param debug
+ *   Debug instance.
+ * @param event
+ *   Validation event to create catchpoint for.
+ * @param callback
+ *   Callback to invoke.
+ * @return
+ *   New breakpoint on success, NULL with rte_errno set on failure.
+ */
+__rte_experimental
+struct rte_bpf_validate_debug_point *
+rte_bpf_validate_debug_catch(struct rte_bpf_validate_debug *debug,
+	enum rte_bpf_validate_debug_event event,
+	const struct rte_bpf_validate_debug_callback *callback);
+
+/**
+ * Delete breakpoint or catchpoint and free all associated resources.
+ *
+ * If a callback is currently being executed, calling this API is allowed for:
+ * - breakpoint or catchpoint the callback is executed for;
+ * - breakpoints or catchpoints for other locations or events;
+ * and NOT allowed for:
+ * - other breakpoints or catchpoints for the same location or event.
+ *
+ * @param point
+ *   Breakpoint or catchpoint to destroy, or NULL.
+ */
+__rte_experimental
+void
+rte_bpf_validate_debug_point_destroy(struct rte_bpf_validate_debug_point *point);
+
+/**
+ * Get effective eBPF parameters struct.
+ *
+ * @param debug
+ *   Debug instance.
+ * @return
+ *   Parameters struct of the validated eBPF program, including code with all
+ *   patches and relocations applied.
+ */
+__rte_experimental
+const struct rte_bpf_prm_ex *
+rte_bpf_validate_debug_get_bpf_param(const struct rte_bpf_validate_debug *debug);
+
+/**
+ * Get pointer to effective eBPF program instructions.
+ *
+ * @param debug
+ *   Debug instance.
+ * @param ins
+ *   Upon return, program instructions with all patches and relocations applied.
+ * @param nb_ins
+ *   Upon return, number of program instructions.
+ * @return
+ *   Non-negative value on success, negative errno on failure.
+ */
+__rte_experimental
+int
+rte_bpf_validate_debug_get_ins(const struct rte_bpf_validate_debug *debug,
+	const struct ebpf_insn **ins, uint32_t *nb_ins);
+
+/**
+ * Get last triggered breakpoint or catchpoint.
+ *
+ * Can be used to destroy currently processed breakpoint or catchpoint.
+ *
+ * The pointer may be invalid if the breakpoint or catchpoint has already been
+ * destroyed earlier.
+ *
+ * @param debug
+ *   Debug instance.
+ * @return
+ *   Last triggered breakpoint or callpoint, including one the callback is
+ *   currently executing for.
+ *   NULL of none were triggered in the current validation process.
+ */
+__rte_experimental
+struct rte_bpf_validate_debug_point *
+rte_bpf_validate_debug_get_last_point(const struct rte_bpf_validate_debug *debug);
+
+/**
+ * Get current instruction index, or one after last if finishing.
+ *
+ * @param debug
+ *   Debug instance.
+ * @return
+ *   Current program counter being validated, or one after last.
+ *   UINT32_MAX if no program is being validated.
+ */
+__rte_experimental
+uint32_t
+rte_bpf_validate_debug_get_pc(const struct rte_bpf_validate_debug *debug);
+
+/**
+ * Get the validation result, if it has finished.
+ *
+ * @param debug
+ *   Debug instance.
+ * @param result
+ *   Upon successful return, the validation result (negative if validation failed).
+ * @return
+ *   Non-negative value if validation has finished and result variable was written;
+ *   -EAGAIN if validation is still ongoing;
+ *   other negative errno in case of failure;
+ */
+__rte_experimental
+int
+rte_bpf_validate_debug_get_validation_result(const struct rte_bpf_validate_debug *debug,
+	int *result);
+
+/**
+ * Check if specified memory access instruction is currently valid.
+ *
+ * @param debug
+ *   Debug instance.
+ * @param access
+ *   Memory load or store eBPF instruction.
+ * @param off64
+ *   Additional 64-bit offset added to ins->off.
+ * @return
+ *   1 if specified memory access is currently valid;
+ *   0 if specified memory access is currently invalid;
+ *   negative errno in case of failure;
+ */
+__rte_experimental
+int
+rte_bpf_validate_debug_can_access(const struct rte_bpf_validate_debug *debug,
+	const struct ebpf_insn *access, uint64_t off64);
+
+/**
+ * Get possible truth values of the specified jump condition.
+ *
+ * @param debug
+ *   Debug instance.
+ * @param jump
+ *   Conditional jump instruction specifying the condition.
+ * @param imm64
+ *   Additional 64-bit immediate added to the source.
+ * @return
+ *   in case of success, bitwise combination of:
+ *     RTE_BPF_VALIDATE_DEBUG_MAY_BE_FALSE if the jump condition may be false;
+ *     RTE_BPF_VALIDATE_DEBUG_MAY_BE_TRUE if the jump condition may be true;
+ *   negative errno in case of failure.
+ */
+__rte_experimental
+int
+rte_bpf_validate_debug_may_jump(const struct rte_bpf_validate_debug *debug,
+	const struct ebpf_insn *jump, uint64_t imm64);
+
+/**
+ * Format information about specified register for the user.
+ *
+ * Parameters buffer, bufsz and return value work the same way as for snprintf.
+ *
+ * @param debug
+ *   Debug instance.
+ * @param buffer
+ *   Buffer to fill with register information.
+ * @param bufsz
+ *   Buffer size (including space for terminating zero).
+ * @param reg
+ *   Register to provide information about.
+ * @return
+ *   Number of characters needed _excluding_ terminating zero.
+ */
+__rte_experimental
+int
+rte_bpf_validate_debug_format_register_info(const struct rte_bpf_validate_debug *debug,
+	char *buffer, size_t bufsz, uint8_t reg);
+
+/**
+ * Format information about specified stack frame location for the user.
+ *
+ * Parameters buffer, bufsz and return value work the same way as for snprintf.
+ *
+ * @param debug
+ *   Debug instance.
+ * @param buffer
+ *   Buffer to fill with register information.
+ * @param bufsz
+ *   Buffer size (including space for terminating zero).
+ * @param offset
+ *   Stack frame offset to provide information about, in bytes.
+ *   Typically a negative multiple of 8.
+ * @return
+ *   Number of characters needed _excluding_ terminating zero.
+ */
+__rte_experimental
+int
+rte_bpf_validate_debug_format_frame_info(const struct rte_bpf_validate_debug *debug,
+	char *buffer, size_t bufsz, int32_t offset);
+
+/**
+ * Get program stack frame size.
+ *
+ * @param debug
+ *   Debug instance.
+ * @return
+ *   Program stack frame size in bytes.
+ */
+__rte_experimental
+int32_t
+rte_bpf_validate_debug_get_frame_size(const struct rte_bpf_validate_debug *debug);
+
+/**
+ * Format value following the style of register format function.
+ *
+ * Parameters buffer, bufsz and return value work the same way as for snprintf.
+ *
+ * @param buffer
+ *   Buffer to fill with register information.
+ * @param bufsz
+ *   Buffer size (including space for terminating zero).
+ * @param format
+ *   One of characters 'd' or 'x' for signed or hexadecimal format.
+ * @param value
+ *   Formatted value, can be signed typecast to unsigned.
+ * @return
+ *   Number of characters needed _excluding_ terminating zero.
+ */
+__rte_experimental
+int
+rte_bpf_validate_debug_format_value(char *buffer, size_t bufsz, char format,
+	uint64_t value);
+
+/**
+ * Format interval following the style of register format function.
+ *
+ * Parameters buffer, bufsz and return value work the same way as for snprintf.
+ *
+ * @param buffer
+ *   Buffer to fill with register information.
+ * @param bufsz
+ *   Buffer size (including space for terminating zero).
+ * @param format
+ *   One of characters 'd' or 'x' for signed or hexadecimal format.
+ * @param min
+ *   Minimum value of the interval, can be signed typecast to unsigned.
+ * @param max
+ *   Maximum value of the interval, can be signed typecast to unsigned.
+ * @return
+ *   Number of characters needed _excluding_ terminating zero.
+ */
+__rte_experimental
+int
+rte_bpf_validate_debug_format_interval(char *buffer, size_t bufsz, char format,
+	uint64_t min, uint64_t max);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* _RTE_BPF_VALIDATE_DEBUG_H_ */
-- 
2.43.0


^ permalink raw reply related

* [PATCH v2 07/25] bpf/validate: fix BPF_LDX | EBPF_DW signed range
From: Marat Khalili @ 2026-05-19  9:31 UTC (permalink / raw)
  To: Konstantin Ananyev; +Cc: dev, stable
In-Reply-To: <20260519093131.52022-1-marat.khalili@huawei.com>

Function `eval_max_load` copied signed range from unsigned regardless of
the mask (operation width) producing on 64-bit load nonsensical signed
range 0..-1 that breaks invariant min <= max relied upon in multiple
places (e.g. signed overflow detection in `eval_mul` only checks `s.min`
to make sure the range is non-negative and so on).

E.g. consider the following program with the current validation code:

    Tested program:
        0:  mov r0, #0x0
        1:  mov r3, #0x0
        2:  add r3, r1
        3:  ldxdw r2, [r3 + 16]  ; tested instruction
        4:  mov r0, #0x1
        5:  exit
    Pre-state:
       r2:  %undefined
       r3:  %buffer<24> + 0
    Post-state:
       r2:  0..-1 INTERSECT 0..UINT64_MAX (!)
       r3:  %buffer<24> + 0

Part before INTERSECT represents signed range, part after INTERSECT
represents unsigned range. Unsigned range is correctly set to full range
0..UINT64_MAX, but signed range copied from it becomes 0..-1.

Fix loading logic to only copy unsigned to signed for non-full mask.

The test will be added in subsequent commits since it depends on other
fixes.

Fixes: 8021917293d0 ("bpf: add extra validation for input BPF program")
Cc: stable@dpdk.org

Signed-off-by: Marat Khalili <marat.khalili@huawei.com>
Acked-by: Konstantin Ananyev <konstantin.ananyev@huawei.com>
---
Depends-on: series-38149 ("bpf: introduce extensible load API")
 lib/bpf/bpf_validate.c | 9 +++++----
 1 file changed, 5 insertions(+), 4 deletions(-)

diff --git a/lib/bpf/bpf_validate.c b/lib/bpf/bpf_validate.c
index 41dca2fb76..391be9cbb4 100644
--- a/lib/bpf/bpf_validate.c
+++ b/lib/bpf/bpf_validate.c
@@ -1220,10 +1220,11 @@ eval_max_load(struct bpf_reg_val *rv, uint64_t mask)
 	/* full 64-bit load */
 	if (mask == UINT64_MAX)
 		eval_smax_bound(rv, mask);
-
-	/* zero-extend load */
-	rv->s.min = rv->u.min;
-	rv->s.max = rv->u.max;
+	else {
+		/* zero-extend load */
+		rv->s.min = rv->u.min;
+		rv->s.max = rv->u.max;
+	}
 }
 
 
-- 
2.43.0


^ permalink raw reply related

* [PATCH v2 06/25] bpf/validate: fix BPF_ADD of pointer to a scalar
From: Marat Khalili @ 2026-05-19  9:31 UTC (permalink / raw)
  To: Konstantin Ananyev; +Cc: dev, stable
In-Reply-To: <20260519093131.52022-1-marat.khalili@huawei.com>

Function `eval_add` preserved type of the destination register even when
a pointer was added to it. If it contained scalar, it remained a scalar,
and if it contained pointer, it remained a pointer.

E.g. consider the following program with the current validation code:

    Tested program:
        0:  mov r0, #0x0
        1:  mov r3, #0x0
        2:  add r3, r1  ; tested instruction
        3:  ldxdw r2, [r3 + 16]
        4:  mov r0, #0x1
        5:  exit

After the tested instruction validator considers r3 to be scalar and
fails validation with the error:

    BPF: evaluate(): destination is not a pointer at pc: 3

However, this code is valid as long as program argument points to a
valid memory area at least 24 bytes long which we read at offset 16.

When adding pointer to a scalar set type of the result to pointer of
the same type. When adding pointer to a pointer set type of the result
to scalar and value to unknown.

The test will be added in subsequent commits since it depends on other
fixes.

Fixes: 8021917293d0 ("bpf: add extra validation for input BPF program")
Cc: stable@dpdk.org

Signed-off-by: Marat Khalili <marat.khalili@huawei.com>
Acked-by: Konstantin Ananyev <konstantin.ananyev@huawei.com>
---
Depends-on: series-38149 ("bpf: introduce extensible load API")
 lib/bpf/bpf_validate.c | 12 ++++++++++++
 1 file changed, 12 insertions(+)

diff --git a/lib/bpf/bpf_validate.c b/lib/bpf/bpf_validate.c
index 8dac908c39..41dca2fb76 100644
--- a/lib/bpf/bpf_validate.c
+++ b/lib/bpf/bpf_validate.c
@@ -647,8 +647,20 @@ eval_apply_mask(struct bpf_reg_val *rv, uint64_t mask)
 static void
 eval_add(struct bpf_reg_val *rd, const struct bpf_reg_val *rs, uint64_t msk)
 {
+	struct bpf_reg_val rs_buf;
 	struct bpf_reg_val rv;
 
+	if (RTE_BPF_ARG_PTR_TYPE(rs->v.type) != 0) {
+		if (RTE_BPF_ARG_PTR_TYPE(rd->v.type) != 0) {
+			/* treat sum of pointers as sum of two unknown scalars */
+			eval_fill_max_bound(&rs_buf, msk);
+			*rd = rs_buf;
+			rs = &rs_buf;
+		} else
+			/* scalar + pointer is a pointer of the same type */
+			rd->v = rs->v;
+	}
+
 	rv.u.min = (rd->u.min + rs->u.min) & msk;
 	rv.u.max = (rd->u.max + rs->u.max) & msk;
 	rv.s.min = ((uint64_t)rd->s.min + (uint64_t)rs->s.min) & msk;
-- 
2.43.0


^ permalink raw reply related

* [PATCH v2 04/25] bpf/validate: expand comments in evaluate cycle
From: Marat Khalili @ 2026-05-19  9:31 UTC (permalink / raw)
  To: Konstantin Ananyev; +Cc: dev
In-Reply-To: <20260519093131.52022-1-marat.khalili@huawei.com>

Logic of execution tree traversal is not 100% obvious, and had some bugs
in the past. Add and expand comments to clarify what `next` and `node`
variables are supposed to point to at various points of the cycle.

Signed-off-by: Marat Khalili <marat.khalili@huawei.com>
Acked-by: Konstantin Ananyev <konstantin.ananyev@huawei.com>
---
Depends-on: series-38149 ("bpf: introduce extensible load API")
 lib/bpf/bpf_validate.c | 12 ++++++++++--
 1 file changed, 10 insertions(+), 2 deletions(-)

diff --git a/lib/bpf/bpf_validate.c b/lib/bpf/bpf_validate.c
index 1619faf360..362d00c770 100644
--- a/lib/bpf/bpf_validate.c
+++ b/lib/bpf/bpf_validate.c
@@ -2449,6 +2449,7 @@ evaluate(struct bpf_verifier *bvf)
 		 * each node only once.
 		 */
 		if (next != NULL) {
+			/* just started or stepped down the tree, node == next */
 
 			bvf->evin = node;
 			idx = get_node_idx(bvf, node);
@@ -2481,8 +2482,10 @@ evaluate(struct bpf_verifier *bvf)
 		next = get_next_node(bvf, node);
 
 		if (next != NULL) {
-
-			/* proceed with next child */
+			/*
+			 * proceed with next child
+			 * next points to an unwalked subtree of node
+			 */
 			if (node->cur_edge == node->nb_edge &&
 					node->evst.cur != NULL) {
 				restore_cur_eval_state(bvf, node);
@@ -2514,6 +2517,11 @@ evaluate(struct bpf_verifier *bvf)
 
 			/* first node will not have prev, signalling finish */
 		}
+
+		/*
+		 * next != NULL: stepped down the tree, node == next;
+		 * next == NULL: stepped up after processing or pruning subtree;
+		 */
 	}
 
 	RTE_LOG(DEBUG, BPF, "%s(%p) returns %d, stats:\n"
-- 
2.43.0


^ permalink raw reply related

* [PATCH v2 03/25] bpf/validate: break on error in evaluate
From: Marat Khalili @ 2026-05-19  9:31 UTC (permalink / raw)
  To: Konstantin Ananyev; +Cc: dev
In-Reply-To: <20260519093131.52022-1-marat.khalili@huawei.com>

Evaluation loop previously continued until the cycle end in case of an
evaluation error. It made reasoning about the code difficult since it
might be executing when the evaluation is already in an invalid state.

Change loop logic to break out of the loop immediately after an error.

Signed-off-by: Marat Khalili <marat.khalili@huawei.com>
Acked-by: Konstantin Ananyev <konstantin.ananyev@huawei.com>
---
Depends-on: series-38149 ("bpf: introduce extensible load API")
 lib/bpf/bpf_validate.c | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/lib/bpf/bpf_validate.c b/lib/bpf/bpf_validate.c
index bf8a4abb5a..1619faf360 100644
--- a/lib/bpf/bpf_validate.c
+++ b/lib/bpf/bpf_validate.c
@@ -2401,11 +2401,11 @@ prune_eval_state(struct bpf_verifier *bvf, const struct inst_node *node,
 static int
 evaluate(struct bpf_verifier *bvf)
 {
-	int32_t rc;
 	uint32_t idx, op;
 	const char *err;
 	const struct ebpf_insn *ins;
 	struct inst_node *next, *node;
+	int rc = 0;
 
 	struct {
 		uint32_t nb_eval;
@@ -2439,11 +2439,10 @@ evaluate(struct bpf_verifier *bvf)
 	ins = bvf->prm->raw.ins;
 	node = bvf->in;
 	next = node;
-	rc = 0;
 
 	memset(&stats, 0, sizeof(stats));
 
-	while (node != NULL && rc == 0) {
+	while (node != NULL) {
 
 		/*
 		 * current node evaluation, make sure we evaluate
@@ -2457,17 +2456,20 @@ evaluate(struct bpf_verifier *bvf)
 
 			/* for jcc node make a copy of evaluation state */
 			if (node->nb_edge > 1) {
-				rc |= save_cur_eval_state(bvf, node);
+				rc = save_cur_eval_state(bvf, node);
+				if (rc < 0)
+					break;
 				stats.nb_save++;
 			}
 
-			if (ins_chk[op].eval != NULL && rc == 0) {
+			if (ins_chk[op].eval != NULL) {
 				err = ins_chk[op].eval(bvf, ins + idx);
 				stats.nb_eval++;
 				if (err != NULL) {
 					RTE_BPF_LOG_FUNC_LINE(ERR,
 						"%s at pc: %u", err, idx);
 					rc = -EINVAL;
+					break;
 				}
 			}
 
-- 
2.43.0


^ permalink raw reply related

* [PATCH v2 02/25] bpf: add format instruction function
From: Marat Khalili @ 2026-05-19  9:31 UTC (permalink / raw)
  To: Konstantin Ananyev; +Cc: dev
In-Reply-To: <20260519093131.52022-1-marat.khalili@huawei.com>

BPF library already contains BPF instruction formatting functions, but
they could only be used via `rte_bpf_dump` to dump result into file. Add
new function `rte_bpf_format` to format instruction in various way
(hexadecimal, disassembly) into a user-provided buffer, as well as a
service function `rte_bpf_insn_is_wide` to detect wide instructions.

Signed-off-by: Marat Khalili <marat.khalili@huawei.com>
Acked-by: Konstantin Ananyev <konstantin.ananyev@huawei.com>
---
Depends-on: series-38149 ("bpf: introduce extensible load API")
 lib/bpf/bpf_dump.c | 290 +++++++++++++++++++++++++++------------------
 lib/bpf/rte_bpf.h  |  51 ++++++++
 2 files changed, 226 insertions(+), 115 deletions(-)

diff --git a/lib/bpf/bpf_dump.c b/lib/bpf/bpf_dump.c
index 0abaeef8ae..4fd67ad5a1 100644
--- a/lib/bpf/bpf_dump.c
+++ b/lib/bpf/bpf_dump.c
@@ -46,6 +46,38 @@ static const char *const jump_tbl[16] = {
 	[EBPF_JSLT >> 4] = "jslt", [EBPF_JSLE >> 4] = "jsle",
 };
 
+RTE_EXPORT_EXPERIMENTAL_SYMBOL(rte_bpf_insn_is_wide, 26.07)
+bool
+rte_bpf_insn_is_wide(const struct ebpf_insn *ins)
+{
+	return ins->code == (BPF_LD | BPF_IMM | EBPF_DW);
+}
+
+
+/* Format one (possibly wide) eBPF command as hexadecimal in objdump format. */
+static int
+format_hexadecimal(char *buffer, size_t bufsz, const struct ebpf_insn *ins,
+	uint32_t flags)
+{
+	const char *const b = (const char *)ins;
+
+	RTE_ASSERT((flags & RTE_BPF_FORMAT_FLAG_HEXADECIMAL) != 0);
+
+	RTE_BUILD_BUG_ON(sizeof(*ins) != 8);
+
+	if ((flags & RTE_BPF_FORMAT_FLAG_NEVER_WIDE) == 0 && rte_bpf_insn_is_wide(ins))
+		return snprintf(buffer, bufsz,
+			"%02hhx %02hhx %02hhx %02hhx %02hhx %02hhx %02hhx %02hhx "
+			"%02hhx %02hhx %02hhx %02hhx %02hhx %02hhx %02hhx %02hhx",
+			b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7],
+			b[8], b[9], b[10], b[11], b[12], b[13], b[14], b[15]);
+	else
+		return snprintf(buffer, bufsz,
+			"%02hhx %02hhx %02hhx %02hhx %02hhx %02hhx %02hhx %02hhx",
+			b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7]);
+}
+
+/* Return atomic subcommand mnemonic based on BPF_STX immediate. */
 static inline const char *
 atomic_op(int32_t imm)
 {
@@ -59,130 +91,158 @@ atomic_op(int32_t imm)
 	}
 }
 
-RTE_EXPORT_SYMBOL(rte_bpf_dump)
-void rte_bpf_dump(FILE *f, const struct ebpf_insn *buf, uint32_t len)
+/* Format one (possibly wide) eBPF command as assembler. */
+static int
+format_disassembly(char *buffer, size_t bufsz, const struct ebpf_insn *ins,
+	uint32_t pc, uint32_t flags)
 {
-	uint32_t i;
+	uint8_t cls = BPF_CLASS(ins->code);
+	const char *op, *postfix = "", *warning = "";
+	char jump[16];
 
-	for (i = 0; i < len; ++i) {
-		const struct ebpf_insn *ins = buf + i;
-		uint8_t cls = BPF_CLASS(ins->code);
-		const char *op, *postfix = "", *warning = "";
+	RTE_ASSERT((flags & RTE_BPF_FORMAT_FLAG_HEXADECIMAL) == 0);
 
-		fprintf(f, " L%u:\t", i);
+	switch (cls) {
+	default:
+		return snprintf(buffer, bufsz, "unimp 0x%x // class: %s",
+			ins->code, class_tbl[cls]);
+	case BPF_ALU:
+		postfix = "32";
+		/* fall through */
+	case EBPF_ALU64:
+		op = alu_op_tbl[BPF_OP_INDEX(ins->code)];
+		if (ins->off != 0)
+			/* Not yet supported variation with non-zero offset. */
+			warning = ", off != 0";
+		if (BPF_SRC(ins->code) == BPF_X)
+			return snprintf(buffer, bufsz, "%s%s r%u, r%u%s", op, postfix, ins->dst_reg,
+				ins->src_reg, warning);
+		else
+			return snprintf(buffer, bufsz, "%s%s r%u, #0x%x%s", op, postfix,
+				ins->dst_reg, ins->imm, warning);
+	case BPF_LD:
+		op = "ld";
+		postfix = size_tbl[BPF_SIZE_INDEX(ins->code)];
+		if (ins->code == (BPF_LD | BPF_IMM | EBPF_DW)) {
+			uint64_t val;
 
-		switch (cls) {
-		default:
-			fprintf(f, "unimp 0x%x // class: %s\n",
-				ins->code, class_tbl[cls]);
-			break;
-		case BPF_ALU:
-			postfix = "32";
-			/* fall through */
-		case EBPF_ALU64:
-			op = alu_op_tbl[BPF_OP_INDEX(ins->code)];
-			if (ins->off != 0)
-				/* Not yet supported variation with non-zero offset. */
-				warning = ", off != 0";
-			if (BPF_SRC(ins->code) == BPF_X)
-				fprintf(f, "%s%s r%u, r%u%s\n", op, postfix, ins->dst_reg,
-					ins->src_reg, warning);
-			else
-				fprintf(f, "%s%s r%u, #0x%x%s\n", op, postfix,
-					ins->dst_reg, ins->imm, warning);
-			break;
-		case BPF_LD:
-			op = "ld";
-			postfix = size_tbl[BPF_SIZE_INDEX(ins->code)];
-			if (ins->code == (BPF_LD | BPF_IMM | EBPF_DW)) {
-				uint64_t val;
-
-				if (ins->src_reg != 0)
-					/* Not yet supported variation with non-zero src. */
-					warning = ", src != 0";
-				val = (uint32_t)ins[0].imm |
-					(uint64_t)(uint32_t)ins[1].imm << 32;
-				fprintf(f, "%s%s r%d, #0x%"PRIx64"%s\n",
-					op, postfix, ins->dst_reg, val, warning);
-				i++;
-			} else if (BPF_MODE(ins->code) == BPF_IMM)
-				fprintf(f, "%s%s r%d, #0x%x\n", op, postfix,
-					ins->dst_reg, ins->imm);
-			else if (BPF_MODE(ins->code) == BPF_ABS)
-				fprintf(f, "%s%s r%d, [%d]\n", op, postfix,
-					ins->dst_reg, ins->imm);
-			else if (BPF_MODE(ins->code) == BPF_IND)
-				fprintf(f, "%s%s r%d, [r%u + %d]\n", op, postfix,
-					ins->dst_reg, ins->src_reg, ins->imm);
-			else
-				fprintf(f, "// BUG: LD opcode 0x%02x in eBPF insns\n",
-					ins->code);
-			break;
-		case BPF_LDX:
-			op = "ldx";
-			postfix = size_tbl[BPF_SIZE_INDEX(ins->code)];
-			if (BPF_MODE(ins->code) == BPF_MEM)
-				fprintf(f, "%s%s r%d, [r%u + %d]\n", op, postfix, ins->dst_reg,
-					ins->src_reg, ins->off);
-			else
-				fprintf(f, "// BUG: LDX opcode 0x%02x in eBPF insns\n",
-					ins->code);
-			break;
-		case BPF_ST:
-			op = "st";
-			postfix = size_tbl[BPF_SIZE_INDEX(ins->code)];
-			if (BPF_MODE(ins->code) == BPF_MEM)
-				fprintf(f, "%s%s [r%d + %d], #0x%x\n", op, postfix,
-					ins->dst_reg, ins->off, ins->imm);
-			else
-				fprintf(f, "// BUG: ST opcode 0x%02x in eBPF insns\n",
-					ins->code);
-			break;
-		case BPF_STX:
-			if (BPF_MODE(ins->code) == BPF_MEM)
-				op = "stx";
-			else if (BPF_MODE(ins->code) == EBPF_ATOMIC) {
-				op = atomic_op(ins->imm);
-				if (op == NULL) {
-					fprintf(f, "// BUG: ATOMIC operation 0x%x in eBPF insns\n",
-						ins->imm);
-					break;
-				}
-			} else {
-				fprintf(f, "// BUG: STX opcode 0x%02x in eBPF insns\n",
-					ins->code);
-				break;
-			}
-			postfix = size_tbl[BPF_SIZE_INDEX(ins->code)];
-			fprintf(f, "%s%s [r%d + %d], r%u\n", op, postfix,
-				ins->dst_reg, ins->off, ins->src_reg);
-			break;
-#define L(pc, off) ((int)(pc) + 1 + (off))
-		case BPF_JMP:
-			op = jump_tbl[BPF_OP_INDEX(ins->code)];
 			if (ins->src_reg != 0)
-				/* Not yet supported variation with non-zero src w/o condition. */
+				/* Not yet supported variation with non-zero src. */
 				warning = ", src != 0";
+			val = (uint32_t)ins[0].imm |
+				(uint64_t)(uint32_t)ins[1].imm << 32;
+			return snprintf(buffer, bufsz, "%s%s r%d, #0x%"PRIx64"%s",
+				op, postfix, ins->dst_reg, val, warning);
+		}
+		switch (BPF_MODE(ins->code)) {
+		case BPF_IMM:
+			return snprintf(buffer, bufsz, "%s%s r%d, #0x%x", op, postfix,
+				ins->dst_reg, ins->imm);
+		case BPF_ABS:
+			return snprintf(buffer, bufsz, "%s%s r%d, [%d]", op, postfix,
+				ins->dst_reg, ins->imm);
+		case BPF_IND:
+			return snprintf(buffer, bufsz, "%s%s r%d, [r%u + %d]", op, postfix,
+				ins->dst_reg, ins->src_reg, ins->imm);
+		default:
+			return snprintf(buffer, bufsz, "// BUG: LD opcode 0x%02x in eBPF insns",
+				ins->code);
+		}
+	case BPF_LDX:
+		op = "ldx";
+		postfix = size_tbl[BPF_SIZE_INDEX(ins->code)];
+		if (BPF_MODE(ins->code) == BPF_MEM)
+			return snprintf(buffer, bufsz, "%s%s r%d, [r%u + %d]", op, postfix,
+				ins->dst_reg, ins->src_reg, ins->off);
+		else
+			return snprintf(buffer, bufsz, "// BUG: LDX opcode 0x%02x in eBPF insns",
+				ins->code);
+	case BPF_ST:
+		op = "st";
+		postfix = size_tbl[BPF_SIZE_INDEX(ins->code)];
+		if (BPF_MODE(ins->code) == BPF_MEM)
+			return snprintf(buffer, bufsz, "%s%s [r%d + %d], #0x%x", op, postfix,
+				ins->dst_reg, ins->off, ins->imm);
+		else
+			return snprintf(buffer, bufsz, "// BUG: ST opcode 0x%02x in eBPF insns",
+				ins->code);
+	case BPF_STX:
+		switch (BPF_MODE(ins->code)) {
+		case BPF_MEM:
+			op = "stx";
+			break;
+		case EBPF_ATOMIC:
+			op = atomic_op(ins->imm);
 			if (op == NULL)
-				fprintf(f, "invalid jump opcode: %#x\n", ins->code);
-			else if (BPF_OP(ins->code) == BPF_JA)
-				fprintf(f, "%s L%d%s\n", op, L(i, ins->off), warning);
-			else if (BPF_OP(ins->code) == EBPF_CALL)
-				/* Call of helper function with index in immediate. */
-				fprintf(f, "%s #%u%s\n", op, ins->imm, warning);
-			else if (BPF_OP(ins->code) == EBPF_EXIT)
-				fprintf(f, "%s%s\n", op, warning);
-			else if (BPF_SRC(ins->code) == BPF_X)
-				fprintf(f, "%s r%u, r%u, L%d\n", op, ins->dst_reg,
-					ins->src_reg, L(i, ins->off));
-			else
-				fprintf(f, "%s r%u, #0x%x, L%d\n", op, ins->dst_reg,
-					ins->imm, L(i, ins->off));
+				return snprintf(buffer, bufsz,
+					"// BUG: ATOMIC operation 0x%x in eBPF insns", ins->imm);
 			break;
-		case BPF_RET:
-			fprintf(f, "// BUG: RET opcode 0x%02x in eBPF insns\n",
+		default:
+			return snprintf(buffer, bufsz, "// BUG: STX opcode 0x%02x in eBPF insns",
 				ins->code);
-			break;
 		}
+		postfix = size_tbl[BPF_SIZE_INDEX(ins->code)];
+		return snprintf(buffer, bufsz, "%s%s [r%d + %d], r%u", op, postfix,
+			ins->dst_reg, ins->off, ins->src_reg);
+	case BPF_JMP:
+		op = jump_tbl[BPF_OP_INDEX(ins->code)];
+		if (op == NULL)
+			return snprintf(buffer, bufsz, "invalid jump opcode: %#x", ins->code);
+
+		if ((flags & RTE_BPF_FORMAT_FLAG_ABSOLUTE_JUMPS) != 0)
+			snprintf(jump, sizeof(jump), "L%d", pc + 1 + ins->off);
+		else
+			snprintf(jump, sizeof(jump), "%+d", (int)ins->off);
+
+		if (ins->src_reg != 0)
+			/* Not yet supported variation with non-zero src w/o condition. */
+			warning = ", src != 0";
+		switch (BPF_OP(ins->code)) {
+		case BPF_JA:
+			return snprintf(buffer, bufsz, "%s %s%s", op, jump, warning);
+		case EBPF_CALL:
+			/* Call of helper function with index in immediate. */
+			return snprintf(buffer, bufsz, "%s #%u%s", op, ins->imm, warning);
+		case EBPF_EXIT:
+			return snprintf(buffer, bufsz, "%s%s", op, warning);
+		}
+
+		if (BPF_SRC(ins->code) == BPF_X)
+			return snprintf(buffer, bufsz, "%s r%u, r%u, %s", op, ins->dst_reg,
+				ins->src_reg, jump);
+		else
+			return snprintf(buffer, bufsz, "%s r%u, #0x%x, %s", op, ins->dst_reg,
+				ins->imm, jump);
+	case BPF_RET:
+		return snprintf(buffer, bufsz, "// BUG: RET opcode 0x%02x in eBPF insns",
+			ins->code);
+	}
+}
+
+RTE_EXPORT_EXPERIMENTAL_SYMBOL(rte_bpf_format, 26.07)
+int
+rte_bpf_format(char *buffer, size_t bufsz, const struct ebpf_insn *ins,
+	uint32_t pc, uint32_t flags)
+{
+	if ((flags & RTE_BPF_FORMAT_FLAG_HEXADECIMAL) != 0)
+		return format_hexadecimal(buffer, bufsz, ins, flags);
+	else
+		return format_disassembly(buffer, bufsz, ins, pc, flags);
+}
+
+RTE_EXPORT_SYMBOL(rte_bpf_dump)
+void rte_bpf_dump(FILE *f, const struct ebpf_insn *buf, uint32_t len)
+{
+	uint32_t i;
+	char buffer[256];
+
+	for (i = 0; i < len; ++i) {
+		const struct ebpf_insn *ins = buf + i;
+
+		format_disassembly(buffer, sizeof(buffer), ins, i,
+			RTE_BPF_FORMAT_FLAG_DISASSEMBLY	|
+			RTE_BPF_FORMAT_FLAG_ABSOLUTE_JUMPS);
+		fprintf(f, " L%u:\t%s\n", i, buffer);
+		i += rte_bpf_insn_is_wide(ins);
 	}
 }
diff --git a/lib/bpf/rte_bpf.h b/lib/bpf/rte_bpf.h
index 413ccf0497..b6c232704a 100644
--- a/lib/bpf/rte_bpf.h
+++ b/lib/bpf/rte_bpf.h
@@ -30,6 +30,23 @@ extern "C" {
 /** Mask with all supported `RTE_BPF_EXEC_FLAG_*` flags set. */
 #define RTE_BPF_EXEC_FLAG_MASK  RTE_BPF_EXEC_FLAG_JIT
 
+/* Format instructions as assembler. */
+#define RTE_BPF_FORMAT_FLAG_DISASSEMBLY		0
+/* Format instructions as hexadecimal. */
+#define RTE_BPF_FORMAT_FLAG_HEXADECIMAL		RTE_BIT32(0)
+
+/* Only valid in disassembly mode. */
+/* Format jump offsets relative to the next instruction. */
+#define RTE_BPF_FORMAT_FLAG_RELATIVE_JUMPS	0
+/* Format jump targets relative to the start of the program. */
+#define RTE_BPF_FORMAT_FLAG_ABSOLUTE_JUMPS	RTE_BIT32(1)
+
+/* Only valid in hexadecimal mode. */
+/* Format full hexadecimal representation of wide instructions. */
+#define RTE_BPF_FORMAT_FLAG_AUTO_WIDE		0
+/* Format as hexadecimal only first half of wide instructions. */
+#define RTE_BPF_FORMAT_FLAG_NEVER_WIDE		RTE_BIT32(2)
+
 /**
  * Possible types for function/BPF program arguments.
  */
@@ -391,6 +408,40 @@ __rte_experimental
 int
 rte_bpf_get_jit_ex(const struct rte_bpf *bpf, struct rte_bpf_jit_ex *jit);
 
+/**
+ * Determine instruction width.
+ *
+ * @return
+ *   True if ins points to a wide (128-bit) instruction.
+ */
+__rte_experimental
+bool
+rte_bpf_insn_is_wide(const struct ebpf_insn *ins);
+
+/**
+ * Print eBPF instruction into a buffer.
+ *
+ * Semantics of handling buffer size repeats those of snprintf.
+ *
+ * @param buffer
+ *   Output buffer (may be NULL if bufsz is zero).
+ * @param bufsz
+ *   Output buffer size.
+ * @param ins
+ *   Narrow or wide (depending on opcode) eBPF instruction. That is, when
+ *   `rte_bpf_insn_is_wide` is true `ins[1]` is also accessed.
+ * @param pc
+ *   Current instruction number for displaying absolute jump targets.
+ * @param flags
+ *   Bitwise-OR combination of `RTE_BPF_FORMAT_FLAG_*` values.
+ * @return
+ *   Number of characters to be written excluding terminating zero.
+ */
+__rte_experimental
+int
+rte_bpf_format(char *buffer, size_t bufsz, const struct ebpf_insn *ins,
+	uint32_t pc, uint32_t flags);
+
 /**
  * Dump epf instructions to a file.
  *
-- 
2.43.0


^ permalink raw reply related

* [PATCH v2 01/25] bpf: format and dump jlt, jle, jslt, and jsle
From: Marat Khalili @ 2026-05-19  9:31 UTC (permalink / raw)
  To: Konstantin Ananyev; +Cc: dev
In-Reply-To: <20260519093131.52022-1-marat.khalili@huawei.com>

Signed and unsigned less and less-then conditional jumps were not
supported by the eBPF format and dump functions, add these instructions.

Signed-off-by: Marat Khalili <marat.khalili@huawei.com>
Acked-by: Konstantin Ananyev <konstantin.ananyev@huawei.com>
---
Depends-on: series-38149 ("bpf: introduce extensible load API")
 lib/bpf/bpf_dump.c | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/lib/bpf/bpf_dump.c b/lib/bpf/bpf_dump.c
index 91bc7c0a7a..0abaeef8ae 100644
--- a/lib/bpf/bpf_dump.c
+++ b/lib/bpf/bpf_dump.c
@@ -42,6 +42,8 @@ static const char *const jump_tbl[16] = {
 	[BPF_JSET >> 4] = "jset",  [EBPF_JNE >> 4] = "jne",
 	[EBPF_JSGT >> 4] = "jsgt", [EBPF_JSGE >> 4] = "jsge",
 	[EBPF_CALL >> 4] = "call", [EBPF_EXIT >> 4] = "exit",
+	[EBPF_JLT >> 4] = "jlt",   [EBPF_JLE >> 4] = "jle",
+	[EBPF_JSLT >> 4] = "jslt", [EBPF_JSLE >> 4] = "jsle",
 };
 
 static inline const char *
-- 
2.43.0


^ permalink raw reply related

* [PATCH v2 00/25] bpf: test and fix issues in verifier
From: Marat Khalili @ 2026-05-19  9:31 UTC (permalink / raw)
  Cc: dev
In-Reply-To: <20260506173846.64914-1-marat.khalili@huawei.com>

This patchset addresses numerous bugs in the BPF verifier's abstract
interpretation logic and introduces a new validation debugger API to
enable precise, robust testing of the verifier itself.

While the existing DPDK eBPF verifier is capable of checking basic
execution graph loops and dead code, the mathematical tracking of
register bounds (both signed and unsigned) contained flaws resulting in
false positives and false negatives, undefined behavior, and hardware
exceptions such as SIGFPE during validation.

To resolve these issues and ensure they do not regress, this patchset
first introduces the "Validation Debugger API"
(`rte_bpf_validate_debug_*`). This gdb-like interface allows setting
breakpoints and catchpoints during the validation process to inspect the
verifier's internal state.

Using this new API, a comprehensive test harness
(`app/test/test_bpf_validate.c`) was created to formally check the
abstract domains of instructions across all their valid branches. The
remainder of the patchset incrementally fixes the math and bounds logic
for individual eBPF instructions, using the new tests to prove the
correctness of the fixes.

This debugger API also lays the foundation for an interactive eBPF
validation debugger to be introduced in the future.

Series-Depends-on: series-38149 ("bpf: introduce extensible load API")

v2:
* Addressed AI reviewer comments:
  * replaced `false` and `true` with 0 and 1 in some API descriptions
    and invocations that multiplex boolean and negative error code;
  * made some previously implicit casts explicit;
  * moved new enum value to the end of the definition.
* Added Acked-by and Depends-on tags to all individual commits to
  align with patchwork requirements.
* Added Reported-by tags to fixes of issues discovered by Claudia Cauli
  using a formal methods framework.


Marat Khalili (25):
  bpf: format and dump jlt, jle, jslt, and jsle
  bpf: add format instruction function
  bpf/validate: break on error in evaluate
  bpf/validate: expand comments in evaluate cycle
  bpf/validate: introduce debugging interface
  bpf/validate: fix BPF_ADD of pointer to a scalar
  bpf/validate: fix BPF_LDX | EBPF_DW signed range
  test/bpf_validate: add setup and basic tests
  test/bpf_validate: add harness for pointer tests
  bpf/validate: fix EBPF_JSLT | BPF_X evaluation
  bpf/validate: fix BPF_NEG of INT64_MIN and 0
  bpf/validate: fix BPF_DIV and BPF_MOD signed part
  bpf/validate: fix BPF_MUL ranges minimum typo
  bpf/validate: fix BPF_MUL signed overflow UB
  bpf/validate: fix BPF_JGT/EBPF_JSGT no-jump max
  bpf/validate: fix BPF_JMP source range calculation
  bpf/validate: fix BPF_JMP empty range handling
  bpf/validate: fix BPF_AND min calculations
  bpf/validate: fix BPF_LSH shift-out-of-bounds UB
  bpf/validate: fix BPF_OR min calculations
  bpf/validate: fix BPF_SUB signed max zero case
  bpf/validate: fix BPF_XOR signed min calculation
  bpf/validate: prevent overflow when building graph
  doc: add release notes for BPF validation fixes
  doc: add BPF validate debug to programmer's guide

 app/test/meson.build                   |    1 +
 app/test/test_bpf.c                    |   99 ++
 app/test/test_bpf_validate.c           | 2271 ++++++++++++++++++++++++
 doc/guides/prog_guide/bpf_lib.rst      |   31 +
 doc/guides/rel_notes/release_26_07.rst |   16 +
 lib/bpf/bpf_dump.c                     |  292 +--
 lib/bpf/bpf_validate.c                 |  730 +++++++-
 lib/bpf/bpf_validate.h                 |   60 +
 lib/bpf/bpf_validate_debug.c           |  663 +++++++
 lib/bpf/bpf_validate_debug.h           |   86 +
 lib/bpf/bpf_value_set.c                |  403 +++++
 lib/bpf/bpf_value_set.h                |  126 ++
 lib/bpf/meson.build                    |    9 +-
 lib/bpf/rte_bpf.h                      |   55 +
 lib/bpf/rte_bpf_validate_debug.h       |  377 ++++
 15 files changed, 5022 insertions(+), 197 deletions(-)
 create mode 100644 app/test/test_bpf_validate.c
 create mode 100644 lib/bpf/bpf_validate.h
 create mode 100644 lib/bpf/bpf_validate_debug.c
 create mode 100644 lib/bpf/bpf_validate_debug.h
 create mode 100644 lib/bpf/bpf_value_set.c
 create mode 100644 lib/bpf/bpf_value_set.h
 create mode 100644 lib/bpf/rte_bpf_validate_debug.h

-- 
2.43.0


^ permalink raw reply

* Re: [PATCH] eal: silence -Wconstant-logical-operand in RTE_IS_POWER_OF_2
From: Jack Bond-Preston @ 2026-05-19  9:26 UTC (permalink / raw)
  To: Stephen Hemminger, dev
  Cc: stable, Gavin Hu, Honnappa Nagarahalli, Pablo de Lara
In-Reply-To: <20260518163401.580696-1-stephen@networkplumber.org>

On 18/05/2026 17:33, Stephen Hemminger wrote:
> Newer GCC warns when a non-boolean constant is an operand of &&, which
> trips whenever RTE_IS_POWER_OF_2 is used in a static_assert with a
> power-of-two literal. Make the zero check explicit.
> 
> Fixes: 7c872b96983a ("hash: validate hash bucket entries while compiling")
> Cc: stable@dpdk.org
> 
> Signed-off-by: Stephen Hemminger <stephen@networkplumber.org>
> ---
>   lib/eal/include/rte_bitops.h | 2 +-
>   1 file changed, 1 insertion(+), 1 deletion(-)
> 
> diff --git a/lib/eal/include/rte_bitops.h b/lib/eal/include/rte_bitops.h
> index aa6ac73abb..d2719ecd5e 100644
> --- a/lib/eal/include/rte_bitops.h
> +++ b/lib/eal/include/rte_bitops.h
> @@ -1299,7 +1299,7 @@ rte_fls_u64(uint64_t x)
>   /**
>    * Macro to return 1 if n is a power of 2, 0 otherwise
>    */
> -#define RTE_IS_POWER_OF_2(n) ((n) && !(((n) - 1) & (n)))
> +#define RTE_IS_POWER_OF_2(n) ((n) != 0 && !(((n) - 1) & (n)))
>   
>   /**
>    * Returns true if n is a power of 2

Acked-by: Jack Bond-Preston <jack.bond-preston@foss.arm.com>

^ permalink raw reply

* Re: [PATCH v2 1/2] spinlock: remove volatile qualifier
From: Robin Jarry @ 2026-05-19  9:17 UTC (permalink / raw)
  To: Bruce Richardson, Thomas Monjalon
  Cc: dev, Stephen Hemminger, Konstantin Ananyev, stable
In-Reply-To: <agswGt0gN-edMzjZ@bricha3-mobl1.ger.corp.intel.com>

Bruce Richardson, May 18, 2026 at 17:28:
> On Mon, May 18, 2026 at 05:25:43PM +0200, Thomas Monjalon wrote:
>> 18/05/2026 17:14, Robin Jarry:
>> > Hey Thomas,
>> > 
>> > Thomas Monjalon, May 18, 2026 at 17:07:
>
> <snip>
>
>> 
>> [...]
>> > > @@ -246,10 +246,9 @@ static inline void rte_spinlock_recursive_unlock(rte_spinlock_recursive_t *slr)
>> > >  	__rte_no_thread_safety_analysis
>> > >  {
>> > >  	if (--(slr->count) == 0) {
>> > 
>> > This code is completely broken. Any thread can unlock without any check.
>> 
>> Maybe, but I don't intend to fix recursive unlock in this patch.
>> The subject is "remove volatile qualifier" (and unblock GCC 16).
>> 
> As with regular mutexes, threads should not go around unlocking when not
> holding the lock. Therefore, I think this is fine. [With regular spinlocks
> we can't check since we don't track threads, therefore I think we just need
> to assume a well-behaved app.]

Ok, fair enough :)

As Stephen suggested, maybe we could add some assertions?

 static inline void rte_spinlock_recursive_unlock(rte_spinlock_recursive_t *slr)
        __rte_no_thread_safety_analysis
 {
+       RTE_ASSERT(rte_atomic_load_explicit(&slr->user, rte_memory_order_relaxed) == rte_gettid());
+       RTE_ASSERT(slr->count > 0);
+


^ permalink raw reply

* Re: Bad gateawy on DPDK kernel mods git URL
From: David Marchand @ 2026-05-19  7:11 UTC (permalink / raw)
  To: Noah Bloom; +Cc: dev@dpdk.org, Ali Alnubani
In-Reply-To: <CH3PR14MB63465CDF386C7768B0B65621BF032@CH3PR14MB6346.namprd14.prod.outlook.com>

Hello Noah,

On Tue, 19 May 2026 at 09:07, Noah Bloom <nbloom@nrao.edu> wrote:
>
> I've been running a third-party script that installs DPDK kernel modules from the DPDK.org git repository and it has recently started failing with a 502/Bad Gateway error.  This was previously working as recently as May 14 (4 days ago) but has been happening since at least May 16 (2 days ago).  Is this expected?
>
> Here is the specific error:
>
> $ git clone http://dpdk.org/git/dpdk-kmods
> Cloning into 'dpdk-kmods'...
> fatal: unable to access 'http://dpdk.org/git/dpdk-kmods/': The requested URL returned error: 502

Thanks for reporting.

The issue was global to dpdk git repos, not only the dpdk-kmods repo.
I had reported the errors to Ali, and this issue should be fixed since
yesterday afternoon (CET).


-- 
David Marchand


^ permalink raw reply

* [QUESTION] mlx5/hws: format_select_dw_8_6_ext gating on standalone ConnectX-7
From: Max Makarov @ 2026-05-18 16:44 UTC (permalink / raw)
  To: dev; +Cc: igozlan, valex, erezsh, suanmingm, bingz, kliteyn

Hi all,

We are building an SDN/IaaS data plane on ConnectX-7 using DOCA Flow and
need IPv6 Connection Tracking. CT pipe for IPv6 requires the 11-DW jumbo
STE format, which is gated by HCA_CAP_GENERAL_2.format_select_dw_8_6_ext
(full_dw_jumbo_support in drivers/net/mlx5/hws/mlx5dr_cmd.c).

On all three of our standalone ConnectX-7 SKUs (PSIDs MT_0000000838,
MT_0000000840, MT_0000000892, firmware 28.48.1000) this bit reads as 0 in
BOTH GET_CUR and GET_MAX modes. We have verified the bit is not affected
by any documented NV-config TLV (tested ~20 candidates with mlxfwreset).

Questions:

  1. Is this capability silicon-locked on standalone ConnectX-7, or could
     a future firmware release expose it?

  2. The DOCA Flow CT documentation states "BlueField-3 and above is
     required for IPv6". Is this permanent product positioning, or is
     there a roadmap to enable it on CX-7?

  3. For existing CX-7 deployments that need IPv6 stateful CT, is there
     a recommended DOCA Flow pattern (e.g. splitting the 5-tuple match
     across multiple 8-DW STEs while preserving CT semantics)?

Happy to share full HCA_CAP dumps or any diagnostic output that would
help.

Thanks,
Max Makarov
Volta Cloud

^ permalink raw reply

* Bad gateawy on DPDK kernel mods git URL
From: Noah Bloom @ 2026-05-18 12:12 UTC (permalink / raw)
  To: dev@dpdk.org

[-- Attachment #1: Type: text/plain, Size: 625 bytes --]

Hi,

I've been running a third-party script that installs DPDK kernel modules from the DPDK.org git repository and it has recently started failing with a 502/Bad Gateway error.  This was previously working as recently as May 14 (4 days ago) but has been happening since at least May 16 (2 days ago).  Is this expected?

Here is the specific error:

$ git clone http://dpdk.org/git/dpdk-kmods
Cloning into 'dpdk-kmods'...
fatal: unable to access 'http://dpdk.org/git/dpdk-kmods/': The requested URL returned error: 502

Please advise.  Thanks,

Noah Bloom
Software Engineer
National Radio Astronomy Observatory

[-- Attachment #2: Type: text/html, Size: 3549 bytes --]

^ permalink raw reply

* RE: Memory management BoF summary (DPDK Summit 2026)
From: Konstantin Ananyev @ 2026-05-19  7:06 UTC (permalink / raw)
  To: Morten Brørup, dev@dpdk.org
  Cc: techboard@dpdk.org, mattias.ronnblom@ericsson.com
In-Reply-To: <98CBD80474FA8B44BF855DF32C47DC35F6587B@smartserver.smartshare.dk>



> There are two ways of allocating/freeing memory objects in DPDK:
> 1.	From the memory heap, i.e. rte_malloc() etc.
> 2.	From a memory pool, i.e. rte_mempool_get() etc.
> 
> The current memory heap in DPDK has two issues:
> -	It is too slow to be used in the fast path.
> -	There may be contention between control threads and
> 	EAL threads for the same spinlocks in the heap.
> 
> The memory pools in the DPDK has multiple issues:
> -	It is designed for fixed size objects,
> 	so individual mempools must be allocated for each type of object.
> 	(In theory, mempools could be allocated per object size,
> 	and shared across modules.
> 	But that would require coordination across modules, including
> 	the total number of objects required by these modules,
> 	the choice of underlying mempool driver,
> 	and the optimal mempool cache size.)
> -	The number of objects in each mempool is fixed,
> 	so applications have to size their mempools to worst case usage.
> -	The memory for the objects in a mempool is pre-allocated.
> In summary, mempools consume significantly more memory than effectively
> being used.
> 
> We discussed the need for a new memory heap system,
> and converged on the following features:
> -	Providing functions to allocate and free memory objects of varying
> 	size, like the rte_malloc library, but usable in the fast path.
> -	Perhaps also a need for bulk alloc/free functions,
> 	like the rte_mempool library.
> -	Building on top of memzones.
> -	No dependency on the current DPDK heap, so it can cleanly replace it.
> 	This is a stretch requirement.
> -	NUMA aware.
> -	Using slabs for various block sizes like in the Linux Kernel,
> 	possibly with object size 2^N.
> -	Using per-lcore caches to reduce contention,
> 	resembling the mempool per-lcore caches.
> 
> Mattias Rönnblom has implemented a prototype with the above properties,
> and this was a good starting point for the discussion.
> 
> Discussion:
> -	It does not seem to be a requirement to be able to free the memory
> 	used by the heap back to the memzones.
> -	It is possible having header-less objects.
> 	However, if the headers are relatively small compared to the size of
> 	the objects themselves, having a header may offer some benefits.
> -	The mempool library has implementation details optimizing for
> 	spreading usage across memory channels, cache alignment, etc..
> 	Such performance optimization details might need consideration.
> -	There is a lower limit to how small objects will be allocated.
> 	E.g. objects smaller than the size of a pointer is unlikely.
> -	It is acceptable to return an object larger than the size requested,
> 	e.g. returning an object of 16 bytes when 12 bytes were requested.
> -	Preventing false sharing of allocated objects between CPU caches
> 	may not be necessary, but must be considered.
> -	Optimally, the new heap should replace the existing heap.
> 	As seen with the timer wheel library previously proposed, replacing
> 	core libraries in DPDK is difficult, so adding the new heap library
> 	in parallel with the existing heap, i.e. with separate APIs, might be
> 	a path of less resistance.
> 	It will then be an application choice to use this new memory heap.
> 	And long term, it can replace the existing heap, if appropriate.
> 
> Mbuf discussion:
> -	As a stretch goal, mbufs should be dynamically allocated from
> 	the new heap, instead of being pre-allocated in mempools.
> -	This might be achievable either
> 	-	by replacing the mempool library with a wrapper to the heap, or
> 	-	by providing a new mempool driver using the heap, as an
> 		alternative to the existing ring and stack mempool drivers.
> -	In this context, it is important to note that mbuf objects have some
> 	pre-initialized fields when held in their respective mempools.
> 	If allocating an mbuf directly from the heap, these fields must
> 	somehow be initialized.
> 	We did not discuss solutions for this, but noted that using the new
> 	heap for mbufs should be considered in the design choices.
> 

Thanks Morten and Mattias, good summary and interesting discussion.
I also agree that DPDK will benefit from mem-allocator, that 
probably will be not as fast as mempool, but will provide more flexibility
and will not require up-front memory pre-allocation:
still fixed size objects in the pool, slab-like mechanics underneath, ability to grow/shrink on demand.
Internally we have the lib that satisfies some of the functional requirements above.
If time permits, ant there is an interest from the community, will also try to send an RFC for it,
might be it will help to blend few things together.
Konstantin   



^ permalink raw reply

* RE: [PATCH v4 00/20] Wangxun Fixes
From: Zaiyu Wang @ 2026-05-19  6:56 UTC (permalink / raw)
  To: 'Stephen Hemminger'; +Cc: dev
In-Reply-To: <20260518075422.4d1adf2f@phoenix.local>

[-- Attachment #1: Type: text/plain, Size: 1140 bytes --]



> -----Original Message-----
> From: Stephen Hemminger <stephen@networkplumber.org>
> Sent: Monday, May 18, 2026 10:54 PM
> To: Zaiyu Wang <zaiyuwang@trustnetic.com>; Zaiyu Wang
> <zaiyuwang@trustnetic.com>
> Cc: dev@dpdk.org; dev@dpdk.org
> Subject: Re: [PATCH v4 00/20] Wangxun Fixes
> 
> On Mon, 11 May 2026 18:35:42 +0800
> Zaiyu Wang <zaiyuwang@trustnetic.com> wrote:
> 
> > This series fixes several issues found on Wangxun Emerald, Sapphire
> > and Amber-lite NICs, with a focus on link-related problems.
> > ---
> 
> Since these are fixes, they mostly standalone.
> If you want I can cherrypick the ones that review cleanly; and you can
then
> address the ones that have review feedback.
> 
Hi Stephen,
Thank you so much for the detailed review and the very helpful feedback.
Regarding your suggestion to cherry-pick the clean patches - if it's not too
much trouble for you, that would be a great help.
Our company is currently evaluating AI-based tools. Next time I submit
patches, I might be able to run them through our AI review first. That
should help catch issues like these earlier and hopefully reduce your review
burden.

[-- Attachment #2: winmail.dat --]
[-- Type: application/ms-tnef, Size: 2803 bytes --]

^ permalink raw reply

* [PATCH 2/2] mailmap: add Denis Sergeev
From: Denis Sergeev @ 2026-05-19  4:43 UTC (permalink / raw)
  To: dev; +Cc: kishore.padmanabha, ajit.khaparde, Denis Sergeev
In-Reply-To: <20260519044344.9544-1-denserg.edu@gmail.com>

Add my name and email mapping to .mailmap

Signed-off-by: Denis Sergeev <denserg.edu@gmail.com>
---
 .mailmap | 1 +
 1 file changed, 1 insertion(+)

diff --git a/.mailmap b/.mailmap
index 4d26d9c286..3d0fc8c7da 100644
--- a/.mailmap
+++ b/.mailmap
@@ -373,6 +373,7 @@ Deirdre O'Connor <deirdre.o.connor@intel.com>
 Dekel Peled <dekelp@nvidia.com> <dekelp@mellanox.com>
 Dengdui Huang <huangdengdui@huawei.com>
 Denis Pryazhennikov <denis.pryazhennikov@arknetworks.am>
+Denis Sergeev <denserg.edu@gmail.com>
 Dennis Marinus <dmarinus@amazon.com>
 Derek Chickles <derek.chickles@caviumnetworks.com>
 Des O Dea <des.j.o.dea@intel.com>
-- 
2.50.1


^ permalink raw reply related

* [PATCH 1/2] net/bnxt/tf_core: fix ignored return of EM delete
From: Denis Sergeev @ 2026-05-19  4:43 UTC (permalink / raw)
  To: dev; +Cc: kishore.padmanabha, ajit.khaparde, Denis Sergeev, stable
In-Reply-To: <20260519044344.9544-1-denserg.edu@gmail.com>

The return value of tfc_em_delete_raw() in tfc_em_delete() was
silently discarded: rc was unconditionally overwritten by the
subsequent tfc_cpm_get_cmm_inst() call without any error check.

If tfc_em_delete_raw() fails, the HW EM entry is not removed but
the function continues to free the corresponding SW pool entry,
creating a HW/SW state inconsistency that can lead to stale flow
matches or incorrect pool slot reuse.

Add an error check after the call and return -EINVAL on failure.

Found by Linux Verification Center (linuxtesting.org) with SVACE.

Fixes: 80317ff6adfd ("net/bnxt/tf_core: support Thor2")
Cc: stable@dpdk.org

Signed-off-by: Denis Sergeev <denserg.edu@gmail.com>
---
 drivers/net/bnxt/tf_core/v3/tfc_em.c | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/drivers/net/bnxt/tf_core/v3/tfc_em.c b/drivers/net/bnxt/tf_core/v3/tfc_em.c
index 3fe4dbe3fe..4c126dc2f4 100644
--- a/drivers/net/bnxt/tf_core/v3/tfc_em.c
+++ b/drivers/net/bnxt/tf_core/v3/tfc_em.c
@@ -661,6 +661,11 @@ int tfc_em_delete(struct tfc *tfcp, struct tfc_em_delete_parms *parms)
 			       &db_offset
 #endif
 			       );
+	if (rc != 0) {
+		PMD_DRV_LOG_LINE(ERR, "tfc_em_delete_raw() failed: %s",
+				 strerror(-rc));
+		return -EINVAL;
+	}
 
 	record_offset = REMOVE_POOL_FROM_OFFSET(pi.lkup_pool_sz_exp,
 						record_offset);
-- 
2.50.1


^ permalink raw reply related

* [PATCH 0/2] net/bnxt/tf_core: fix ignored return of EM delete
From: Denis Sergeev @ 2026-05-19  4:43 UTC (permalink / raw)
  To: dev; +Cc: kishore.padmanabha, ajit.khaparde, Denis Sergeev

This series fixes a missing error check in the bnxt TF core EM delete
path that can lead to HW/SW state inconsistency.

The return value of tfc_em_delete_raw() in tfc_em_delete() was silently
discarded, so if the HW EM entry removal fails, the function continues
to free the corresponding SW pool entry, creating stale flow matches
or incorrect pool slot reuse.

Found by Linux Verification Center (linuxtesting.org) with SVACE.

Patches:
- net/bnxt/tf_core: fix ignored return of EM delete
- mailmap: add Denis Sergeev

Denis Sergeev (2):
  net/bnxt/tf_core: fix ignored return of EM delete
  mailmap: add Denis Sergeev

 .mailmap                             | 1 +
 drivers/net/bnxt/tf_core/v3/tfc_em.c | 5 +++++
 2 files changed, 6 insertions(+)

-- 
2.50.1


^ permalink raw reply

* [PATCH v17 08/11] net/sxe2: support queue setup and control
From: liujie5 @ 2026-05-19  3:01 UTC (permalink / raw)
  To: stephen; +Cc: dev, Jie Liu
In-Reply-To: <20260519030132.3780057-1-liujie5@linkdatatechnology.com>

From: Jie Liu <liujie5@linkdatatechnology.com>

Add support for Rx and Tx queue setup, release, and management.
Implement eth_dev_ops callbacks for rx_queue_setup, tx_queue_setup,
rx_queue_release, and tx_queue_release.

This includes:
- Allocating memory for hardware ring descriptors.
- Initializing software ring structures and hardware head/tail pointers.
- Implementing proper resource cleanup logic to prevent memory leaks
  during queue reconfiguration or device close.

Signed-off-by: Jie Liu <liujie5@linkdatatechnology.com>
---
 drivers/net/sxe2/meson.build   |   2 +
 drivers/net/sxe2/sxe2_ethdev.c |  82 +++--
 drivers/net/sxe2/sxe2_ethdev.h |  15 +-
 drivers/net/sxe2/sxe2_rx.c     | 554 +++++++++++++++++++++++++++++++++
 drivers/net/sxe2/sxe2_rx.h     |  32 ++
 drivers/net/sxe2/sxe2_tx.c     | 420 +++++++++++++++++++++++++
 drivers/net/sxe2/sxe2_tx.h     |  32 ++
 7 files changed, 1111 insertions(+), 26 deletions(-)
 create mode 100644 drivers/net/sxe2/sxe2_rx.c
 create mode 100644 drivers/net/sxe2/sxe2_rx.h
 create mode 100644 drivers/net/sxe2/sxe2_tx.c
 create mode 100644 drivers/net/sxe2/sxe2_tx.h

diff --git a/drivers/net/sxe2/meson.build b/drivers/net/sxe2/meson.build
index 00c38b147c..3dfe54903a 100644
--- a/drivers/net/sxe2/meson.build
+++ b/drivers/net/sxe2/meson.build
@@ -18,6 +18,8 @@ sources += files(
         'sxe2_cmd_chnl.c',
         'sxe2_vsi.c',
         'sxe2_queue.c',
+        'sxe2_tx.c',
+        'sxe2_rx.c',
 )
 
 allow_internal_get_api = true
diff --git a/drivers/net/sxe2/sxe2_ethdev.c b/drivers/net/sxe2/sxe2_ethdev.c
index 204add9c98..6abb4672f6 100644
--- a/drivers/net/sxe2/sxe2_ethdev.c
+++ b/drivers/net/sxe2/sxe2_ethdev.c
@@ -24,6 +24,8 @@
 #include "sxe2_ethdev.h"
 #include "sxe2_drv_cmd.h"
 #include "sxe2_cmd_chnl.h"
+#include "sxe2_tx.h"
+#include "sxe2_rx.h"
 #include "sxe2_common.h"
 #include "sxe2_common_log.h"
 #include "sxe2_host_regs.h"
@@ -86,14 +88,6 @@ static int32_t sxe2_dev_configure(struct rte_eth_dev *dev)
 	return ret;
 }
 
-static void __rte_cold sxe2_txqs_all_stop(struct rte_eth_dev *dev __rte_unused)
-{
-}
-
-static void __rte_cold sxe2_rxqs_all_stop(struct rte_eth_dev *dev __rte_unused)
-{
-}
-
 static int32_t sxe2_dev_stop(struct rte_eth_dev *dev)
 {
 	int32_t ret = 0;
@@ -112,16 +106,6 @@ static int32_t sxe2_dev_stop(struct rte_eth_dev *dev)
 	return ret;
 }
 
-static int32_t __rte_cold sxe2_txqs_all_start(struct rte_eth_dev *dev __rte_unused)
-{
-	return 0;
-}
-
-static int32_t __rte_cold sxe2_rxqs_all_start(struct rte_eth_dev *dev __rte_unused)
-{
-	return 0;
-}
-
 static int32_t sxe2_queues_start(struct rte_eth_dev *dev)
 {
 	int32_t ret = 0;
@@ -307,10 +291,18 @@ static const struct eth_dev_ops sxe2_eth_dev_ops = {
 	.dev_stop                   = sxe2_dev_stop,
 	.dev_close                  = sxe2_dev_close,
 	.dev_infos_get              = sxe2_dev_infos_get,
+
+	.rx_queue_setup             = sxe2_rx_queue_setup,
+	.tx_queue_setup             = sxe2_tx_queue_setup,
+	.rx_queue_release           = sxe2_rx_queue_release,
+	.tx_queue_release           = sxe2_tx_queue_release,
+
+	.rxq_info_get               = sxe2_rx_queue_info_get,
+	.txq_info_get               = sxe2_tx_queue_info_get,
 };
 
 struct sxe2_pci_map_bar_info *sxe2_dev_get_bar_info(struct sxe2_adapter *adapter,
-		enum sxe2_pci_map_resource res_type)
+						    enum sxe2_pci_map_resource res_type)
 {
 	struct sxe2_pci_map_context *map_ctxt = &adapter->map_ctxt;
 	struct sxe2_pci_map_bar_info *bar_info = NULL;
@@ -334,6 +326,48 @@ struct sxe2_pci_map_bar_info *sxe2_dev_get_bar_info(struct sxe2_adapter *adapter
 	return bar_info;
 }
 
+void *sxe2_pci_map_addr_get(struct sxe2_adapter *adapter,
+			     enum sxe2_pci_map_resource res_type,
+			     uint16_t idx_in_func)
+{
+	struct sxe2_pci_map_context *map_ctxt = &adapter->map_ctxt;
+	struct sxe2_pci_map_segment_info *seg_info = NULL;
+	struct sxe2_pci_map_bar_info *bar_info = NULL;
+	void *addr = NULL;
+	uintptr_t calc_addr = 0;
+	uint8_t reg_width = 0;
+	uint8_t i = 0;
+
+	bar_info = sxe2_dev_get_bar_info(adapter, res_type);
+	if (bar_info == NULL) {
+		PMD_DEV_LOG_WARN(adapter, INIT, "Failed to get bar info, res_type=[%d]",
+				res_type);
+		goto l_end;
+	}
+	seg_info = bar_info->seg_info;
+
+	reg_width = map_ctxt->addr_info[res_type].reg_width;
+	if (reg_width == 0) {
+		PMD_DEV_LOG_WARN(adapter, INIT, "Invalid reg width with resource type %d",
+				 res_type);
+		goto l_end;
+	}
+
+	for (i = 0; i < bar_info->map_cnt; i++) {
+		seg_info = &bar_info->seg_info[i];
+		if (res_type == seg_info->type) {
+			calc_addr = (uintptr_t)seg_info->addr;
+			calc_addr += (uintptr_t)seg_info->page_inner_offset;
+			calc_addr += (uintptr_t)reg_width * (uintptr_t)idx_in_func;
+			addr = (void *)calc_addr;
+			goto l_end;
+		}
+	}
+
+l_end:
+	return addr;
+}
+
 static void sxe2_drv_dev_caps_set(struct sxe2_adapter *adapter,
 			struct sxe2_drv_dev_caps_resp *dev_caps)
 {
@@ -402,7 +436,9 @@ static int32_t sxe2_dev_caps_get(struct sxe2_adapter *adapter)
 }
 
 int32_t sxe2_dev_pci_seg_map(struct sxe2_adapter *adapter,
-		enum sxe2_pci_map_resource res_type, uint64_t org_len, uint64_t org_offset)
+			     enum sxe2_pci_map_resource res_type,
+			     uint64_t org_len,
+			     uint64_t org_offset)
 {
 	struct sxe2_pci_map_bar_info *bar_info = NULL;
 	struct sxe2_pci_map_segment_info *seg_info = NULL;
@@ -478,8 +514,10 @@ static int32_t sxe2_hw_init(struct rte_eth_dev *dev)
 	return ret;
 }
 
-int32_t sxe2_dev_pci_res_seg_map(struct sxe2_adapter *adapter, uint32_t res_type,
-				 uint32_t item_cnt, uint32_t item_base)
+int32_t sxe2_dev_pci_res_seg_map(struct sxe2_adapter *adapter,
+				 uint32_t res_type,
+				 uint32_t item_cnt,
+				 uint32_t item_base)
 {
 	struct sxe2_pci_map_addr_info *addr_info = NULL;
 	int32_t ret = 0;
diff --git a/drivers/net/sxe2/sxe2_ethdev.h b/drivers/net/sxe2/sxe2_ethdev.h
index 843e652616..001413e75a 100644
--- a/drivers/net/sxe2/sxe2_ethdev.h
+++ b/drivers/net/sxe2/sxe2_ethdev.h
@@ -293,14 +293,21 @@ struct sxe2_adapter {
 #define SXE2_DEV_TO_PCI(eth_dev) \
 		RTE_DEV_TO_PCI((eth_dev)->device)
 
+void *sxe2_pci_map_addr_get(struct sxe2_adapter *adapter,
+			    enum sxe2_pci_map_resource res_type,
+			    uint16_t idx_in_func);
+
 struct sxe2_pci_map_bar_info *sxe2_dev_get_bar_info(struct sxe2_adapter *adapter,
-		enum sxe2_pci_map_resource res_type);
+						    enum sxe2_pci_map_resource res_type);
 
 int32_t sxe2_dev_pci_seg_map(struct sxe2_adapter *adapter,
-		enum sxe2_pci_map_resource res_type, uint64_t org_len, uint64_t org_offset);
+			     enum sxe2_pci_map_resource res_type,
+			     uint64_t org_len, uint64_t org_offset);
 
-int32_t sxe2_dev_pci_res_seg_map(struct sxe2_adapter *adapter, uint32_t res_type,
-		uint32_t item_cnt, uint32_t item_base);
+int32_t sxe2_dev_pci_res_seg_map(struct sxe2_adapter *adapter,
+				 uint32_t res_type,
+				 uint32_t item_cnt,
+				 uint32_t item_base);
 
 void sxe2_dev_pci_seg_unmap(struct sxe2_adapter *adapter, uint32_t res_type);
 
diff --git a/drivers/net/sxe2/sxe2_rx.c b/drivers/net/sxe2/sxe2_rx.c
new file mode 100644
index 0000000000..28832d5f71
--- /dev/null
+++ b/drivers/net/sxe2/sxe2_rx.c
@@ -0,0 +1,554 @@
+/* SPDX-License-Identifier: BSD-3-Clause
+ * Copyright (C), 2025, Wuxi Stars Micro System Technologies Co., Ltd.
+ */
+
+#include <ethdev_driver.h>
+#include <rte_net.h>
+#include <rte_vect.h>
+#include <rte_malloc.h>
+#include <rte_memzone.h>
+
+#include "sxe2_ethdev.h"
+#include "sxe2_queue.h"
+#include "sxe2_rx.h"
+#include "sxe2_cmd_chnl.h"
+
+#include "sxe2_osal.h"
+#include "sxe2_common_log.h"
+
+static void *sxe2_rx_doorbell_tail_addr_get(struct sxe2_adapter *adapter, uint16_t queue_id)
+{
+	return sxe2_pci_map_addr_get(adapter, SXE2_PCI_MAP_RES_DOORBELL_RX_TAIL,
+				     queue_id);
+}
+
+static void sxe2_rx_head_tail_init(struct sxe2_adapter *adapter, struct sxe2_rx_queue *rxq)
+{
+	rxq->rdt_reg_addr = sxe2_rx_doorbell_tail_addr_get(adapter, rxq->queue_id);
+	SXE2_PCI_REG_WRITE_WC(rxq->rdt_reg_addr, 0);
+}
+
+static void __rte_cold sxe2_rx_queue_reset(struct sxe2_rx_queue *rxq)
+{
+	uint16_t i = 0;
+	uint16_t len = 0;
+	static const union sxe2_rx_desc zeroed_desc = {{0}};
+
+	len = rxq->ring_depth + SXE2_RX_PKTS_BURST_BATCH_NUM;
+	for (i = 0; i < len; ++i)
+		rxq->desc_ring[i] = zeroed_desc;
+
+	memset(&rxq->fake_mbuf, 0, sizeof(rxq->fake_mbuf));
+	for (i = rxq->ring_depth; i < len; i++)
+		rxq->buffer_ring[i] = &rxq->fake_mbuf;
+
+	rxq->hold_num            = 0;
+	rxq->next_ret_pkt        = 0;
+	rxq->processing_idx      = 0;
+	rxq->completed_pkts_num  = 0;
+	rxq->batch_alloc_trigger = rxq->rx_free_thresh - 1;
+
+	rxq->pkt_first_seg = NULL;
+	rxq->pkt_last_seg  = NULL;
+
+	rxq->realloc_num   = 0;
+	rxq->realloc_start = 0;
+}
+
+void __rte_cold sxe2_rx_queue_mbufs_release(struct sxe2_rx_queue *rxq)
+{
+	uint16_t i;
+
+	if (rxq->buffer_ring != NULL) {
+		for (i = 0; i < rxq->ring_depth; i++) {
+			if (rxq->buffer_ring[i] != NULL) {
+				rte_pktmbuf_free(rxq->buffer_ring[i]);
+				rxq->buffer_ring[i] = NULL;
+			}
+		}
+	}
+
+	if (rxq->completed_pkts_num) {
+		for (i = 0; i < rxq->completed_pkts_num; ++i) {
+			if (rxq->completed_buf[rxq->next_ret_pkt + i] != NULL) {
+				rte_pktmbuf_free(rxq->completed_buf[rxq->next_ret_pkt + i]);
+				rxq->completed_buf[rxq->next_ret_pkt + i] = NULL;
+			}
+		}
+		rxq->completed_pkts_num = 0;
+	}
+}
+
+const struct sxe2_rxq_ops sxe2_default_rxq_ops = {
+	.queue_reset      = sxe2_rx_queue_reset,
+	.mbufs_release    = sxe2_rx_queue_mbufs_release,
+};
+
+static struct sxe2_rxq_ops sxe2_rx_default_ops_get(void)
+{
+	return sxe2_default_rxq_ops;
+}
+
+void __rte_cold sxe2_rx_queue_info_get(struct rte_eth_dev *dev,
+		uint16_t queue_id, struct rte_eth_rxq_info *qinfo)
+{
+	struct sxe2_rx_queue *rxq = NULL;
+
+	if (queue_id >= dev->data->nb_rx_queues) {
+		PMD_LOG_ERR(RX, "rx queue:%u is out of range:%u",
+			queue_id, dev->data->nb_rx_queues);
+		goto end;
+	}
+
+	rxq = dev->data->rx_queues[queue_id];
+	if (rxq == NULL) {
+		PMD_LOG_ERR(RX, "rx queue:%u is NULL", queue_id);
+		goto end;
+	}
+
+	qinfo->mp           = rxq->mb_pool;
+	qinfo->nb_desc      = rxq->ring_depth;
+	qinfo->scattered_rx = dev->data->scattered_rx;
+	qinfo->conf.rx_free_thresh    = rxq->rx_free_thresh;
+	qinfo->conf.rx_drop_en        = rxq->drop_en;
+	qinfo->conf.rx_deferred_start = rxq->rx_deferred_start;
+
+end:
+	return;
+}
+
+int32_t __rte_cold sxe2_rx_queue_stop(struct rte_eth_dev *dev, uint16_t rx_queue_id)
+{
+	struct sxe2_adapter *adapter = SXE2_DEV_PRIVATE_TO_ADAPTER(dev);
+	struct sxe2_rx_queue *rxq;
+	int32_t ret;
+	PMD_INIT_FUNC_TRACE();
+
+	if (dev->data->rx_queue_state[rx_queue_id] ==
+			RTE_ETH_QUEUE_STATE_STOPPED) {
+		ret = 0;
+		goto l_end;
+	}
+
+	rxq = dev->data->rx_queues[rx_queue_id];
+	if (rxq == NULL) {
+		ret = 0;
+		goto l_end;
+	}
+	ret = sxe2_drv_rxq_switch(adapter, rxq, false);
+	if (ret) {
+		PMD_LOG_ERR(RX, "Failed to switch rx queue %u off, ret = %d",
+				rx_queue_id, ret);
+		if (ret == -EPERM)
+			goto l_free;
+		goto l_end;
+	}
+
+l_free:
+	rxq->ops.mbufs_release(rxq);
+	rxq->ops.queue_reset(rxq);
+	dev->data->rx_queue_state[rx_queue_id] =
+		RTE_ETH_QUEUE_STATE_STOPPED;
+l_end:
+	return ret;
+}
+
+static void __rte_cold sxe2_rx_queue_free(struct sxe2_rx_queue *rxq)
+{
+	if (rxq != NULL) {
+		rxq->ops.mbufs_release(rxq);
+		if (rxq->buffer_ring != NULL) {
+			rte_free(rxq->buffer_ring);
+			rxq->buffer_ring = NULL;
+		}
+		rte_memzone_free(rxq->mz);
+		rte_free(rxq);
+	}
+}
+
+void __rte_cold sxe2_rx_queue_release(struct rte_eth_dev *dev,
+					uint16_t queue_idx)
+{
+	(void)sxe2_rx_queue_stop(dev, queue_idx);
+	sxe2_rx_queue_free(dev->data->rx_queues[queue_idx]);
+	dev->data->rx_queues[queue_idx] = NULL;
+}
+
+void __rte_cold sxe2_all_rxqs_release(struct rte_eth_dev *dev)
+{
+	struct rte_eth_dev_data *data = dev->data;
+	uint16_t nb_rxq;
+
+	for (nb_rxq = 0; nb_rxq < data->nb_rx_queues; nb_rxq++) {
+		if (data->rx_queues[nb_rxq] == NULL)
+			continue;
+		sxe2_rx_queue_release(dev, nb_rxq);
+		data->rx_queues[nb_rxq] = NULL;
+	}
+	data->nb_rx_queues = 0;
+}
+
+static struct sxe2_rx_queue *sxe2_rx_queue_alloc(struct rte_eth_dev *dev, uint16_t queue_idx,
+		uint16_t ring_depth, uint32_t socket_id)
+{
+	struct sxe2_rx_queue *rxq;
+	const struct rte_memzone *tz;
+	uint16_t len;
+
+	if (dev->data->rx_queues[queue_idx] != NULL) {
+		sxe2_rx_queue_release(dev, queue_idx);
+		dev->data->rx_queues[queue_idx] = NULL;
+	}
+
+	rxq = rte_zmalloc_socket("rx_queue", sizeof(*rxq),
+				 RTE_CACHE_LINE_SIZE, socket_id);
+
+	if (rxq == NULL) {
+		PMD_LOG_ERR(RX, "rx queue[%d] alloc failed", queue_idx);
+		goto l_end;
+	}
+
+	rxq->ring_depth = ring_depth;
+	len = rxq->ring_depth + SXE2_RX_PKTS_BURST_BATCH_NUM;
+
+	rxq->buffer_ring = rte_zmalloc_socket("rx_buffer_ring",
+					  sizeof(struct rte_mbuf *) * len,
+					  RTE_CACHE_LINE_SIZE, socket_id);
+
+	if (!rxq->buffer_ring) {
+		PMD_LOG_ERR(RX, "Rxq malloc mbuf mem failed");
+		rte_free(rxq);
+		rxq = NULL;
+		goto l_end;
+	}
+
+	tz = rte_eth_dma_zone_reserve(dev, "rx_dma", queue_idx,
+					SXE2_RX_RING_SIZE, SXE2_DESC_ADDR_ALIGN, socket_id);
+	if (tz == NULL) {
+		PMD_LOG_ERR(RX, "Rxq malloc desc mem failed");
+		rte_free(rxq->buffer_ring);
+		rxq->buffer_ring = NULL;
+		rte_free(rxq);
+		rxq = NULL;
+		goto l_end;
+	}
+
+	rxq->mz = tz;
+	memset(tz->addr, 0, SXE2_RX_RING_SIZE);
+	rxq->base_addr = tz->iova;
+	rxq->desc_ring = (union sxe2_rx_desc *)tz->addr;
+
+l_end:
+	return rxq;
+}
+
+int32_t __rte_cold sxe2_rx_queue_setup(struct rte_eth_dev *dev,
+			uint16_t queue_idx, uint16_t nb_desc, uint32_t socket_id,
+			const struct rte_eth_rxconf *rx_conf,
+			struct rte_mempool *mp)
+{
+	struct sxe2_adapter *adapter = SXE2_DEV_PRIVATE_TO_ADAPTER(dev);
+	struct sxe2_vsi *vsi = adapter->vsi_ctxt.main_vsi;
+	struct sxe2_rx_queue *rxq;
+	uint64_t offloads;
+	int32_t ret;
+	uint16_t rx_nseg;
+	uint16_t i;
+
+	PMD_INIT_FUNC_TRACE();
+
+	if (nb_desc % SXE2_RX_DESC_RING_ALIGN != 0 ||
+		nb_desc > SXE2_MAX_RING_DESC ||
+		nb_desc < SXE2_MIN_RING_DESC) {
+		PMD_LOG_ERR(RX, "param desc num:%u is invalid", nb_desc);
+		ret = -EINVAL;
+		goto l_end;
+	}
+
+	if (mp != NULL)
+		rx_nseg = 1;
+	else
+		rx_nseg = rx_conf->rx_nseg;
+
+	offloads = rx_conf->offloads | dev->data->dev_conf.rxmode.offloads;
+
+	if (rx_nseg > 1 && !(offloads & RTE_ETH_RX_OFFLOAD_BUFFER_SPLIT)) {
+		PMD_LOG_ERR(RX, "Port %u queue %u Buffer split offload not configured, but rx_nseg is %u",
+					dev->data->port_id, queue_idx, rx_nseg);
+		ret = -EINVAL;
+		goto l_end;
+	}
+
+	if ((offloads & RTE_ETH_RX_OFFLOAD_BUFFER_SPLIT) && !(rx_nseg > 1)) {
+		PMD_LOG_ERR(RX, "Port %u queue %u Buffer split offload configured, but rx_nseg is %u",
+					dev->data->port_id, queue_idx, rx_nseg);
+		ret = -EINVAL;
+		goto l_end;
+	}
+
+	if ((offloads & RTE_ETH_RX_OFFLOAD_TCP_LRO) &&
+		(offloads & RTE_ETH_RX_OFFLOAD_KEEP_CRC)) {
+		PMD_LOG_ERR(RX, "port_id %u queue %u, LRO can't be configure with Keep crc.",
+					dev->data->port_id, queue_idx);
+		ret = -EINVAL;
+		goto l_end;
+	}
+
+	rxq = sxe2_rx_queue_alloc(dev, queue_idx, nb_desc, socket_id);
+	if (rxq == NULL) {
+		PMD_LOG_ERR(RX, "rx queue[%d] resource alloc failed", queue_idx);
+		ret = -ENOMEM;
+		goto l_end;
+	}
+
+	if (offloads & RTE_ETH_RX_OFFLOAD_TCP_LRO)
+		dev->data->lro = 1;
+
+	if (rx_nseg > 1) {
+		for (i = 0; i < rx_nseg; i++) {
+			rte_memcpy(&rxq->rx_seg[i], &rx_conf->rx_seg[i].split,
+					sizeof(struct rte_eth_rxseg_split));
+		}
+		rxq->mb_pool = rxq->rx_seg[0].mp;
+	} else {
+		rxq->mb_pool = mp;
+	}
+
+	rxq->rx_free_thresh = rx_conf->rx_free_thresh;
+	rxq->port_id = dev->data->port_id;
+	rxq->offloads = offloads;
+	if (offloads & RTE_ETH_RX_OFFLOAD_KEEP_CRC)
+		rxq->crc_len = RTE_ETHER_CRC_LEN;
+	else
+		rxq->crc_len = 0;
+
+	rxq->queue_id = queue_idx;
+	rxq->idx_in_func = vsi->rxqs.base_idx_in_func + queue_idx;
+	rxq->drop_en = rx_conf->rx_drop_en;
+	rxq->rx_deferred_start = rx_conf->rx_deferred_start;
+	rxq->vsi = vsi;
+	rxq->ops = sxe2_rx_default_ops_get();
+	rxq->ops.queue_reset(rxq);
+	dev->data->rx_queues[queue_idx] = rxq;
+
+	ret = 0;
+l_end:
+	return ret;
+}
+
+static int32_t __rte_cold sxe2_rx_queue_mbufs_alloc(struct sxe2_rx_queue *rxq)
+{
+	struct rte_mbuf **buf_ring = rxq->buffer_ring;
+	struct rte_mbuf *mbuf = NULL;
+	struct rte_mbuf *mbuf_pay;
+	volatile union sxe2_rx_desc *desc;
+	uint64_t dma_addr;
+	int32_t ret;
+	uint16_t i, j;
+
+	for (i = 0; i < rxq->ring_depth; i++) {
+		mbuf = rte_mbuf_raw_alloc(rxq->mb_pool);
+		if (mbuf == NULL) {
+			PMD_LOG_ERR(RX, "Rx queue is not available or setup");
+			ret = -ENOMEM;
+			goto l_err_free_mbuf;
+		}
+
+		buf_ring[i] = mbuf;
+		mbuf->data_off = RTE_PKTMBUF_HEADROOM;
+		mbuf->nb_segs = 1;
+		mbuf->port = rxq->port_id;
+
+		dma_addr = rte_cpu_to_le_64(rte_mbuf_data_iova_default(mbuf));
+		desc = &rxq->desc_ring[i];
+		if (!(rxq->offloads & RTE_ETH_RX_OFFLOAD_BUFFER_SPLIT)) {
+			mbuf->next = NULL;
+			desc->read.hdr_addr = 0;
+			desc->read.pkt_addr = dma_addr;
+		} else {
+			mbuf_pay = rte_mbuf_raw_alloc(rxq->rx_seg[1].mp);
+			if (unlikely(!mbuf_pay)) {
+				PMD_LOG_ERR(RX, "Failed to allocate payload mbuf for RX");
+				ret = -ENOMEM;
+				goto l_err_free_mbuf;
+			}
+
+			mbuf_pay->next = NULL;
+			mbuf_pay->data_off = RTE_PKTMBUF_HEADROOM;
+			mbuf_pay->nb_segs = 1;
+			mbuf_pay->port = rxq->port_id;
+			mbuf->next = mbuf_pay;
+
+			desc->read.hdr_addr = dma_addr;
+			desc->read.pkt_addr =
+				rte_cpu_to_le_64(rte_mbuf_data_iova_default(mbuf_pay));
+		}
+
+#ifndef RTE_LIBRTE_SXE2_16BYTE_RX_DESC
+		desc->read.rsvd1 = 0;
+		desc->read.rsvd2 = 0;
+#endif
+	}
+
+	ret = 0;
+	goto l_end;
+
+l_err_free_mbuf:
+	for (j = 0; j <= i; j++) {
+		if (buf_ring[j] != NULL && buf_ring[j]->next != NULL) {
+			rte_pktmbuf_free(buf_ring[j]->next);
+			buf_ring[j]->next = NULL;
+		}
+
+		if (buf_ring[j] != NULL) {
+			rte_pktmbuf_free(buf_ring[j]);
+			buf_ring[j] = NULL;
+		}
+	}
+
+l_end:
+	return ret;
+}
+
+int32_t __rte_cold sxe2_rx_queue_start(struct rte_eth_dev *dev, uint16_t rx_queue_id)
+{
+	struct sxe2_rx_queue *rxq;
+	struct sxe2_adapter *adapter = SXE2_DEV_PRIVATE_TO_ADAPTER(dev);
+	int32_t ret;
+	PMD_INIT_FUNC_TRACE();
+
+	rxq = dev->data->rx_queues[rx_queue_id];
+	if (rxq == NULL) {
+		PMD_LOG_ERR(RX, "Rx queue %u is not available or setup",
+				rx_queue_id);
+		ret = -EINVAL;
+		goto l_end;
+	}
+
+	if (dev->data->rx_queue_state[rx_queue_id] ==
+			RTE_ETH_QUEUE_STATE_STARTED) {
+		ret = 0;
+		goto l_end;
+	}
+
+	ret = sxe2_rx_queue_mbufs_alloc(rxq);
+	if (ret) {
+		PMD_LOG_ERR(RX, "Rx queue %u apply desc ring fail",
+			rx_queue_id);
+		ret =  -ENOMEM;
+		goto l_end;
+	}
+
+	sxe2_rx_head_tail_init(adapter, rxq);
+
+	ret = sxe2_drv_rxq_ctxt_cfg(adapter, rxq, 1);
+	if (ret) {
+		PMD_LOG_ERR(RX, "Rx queue %u config ctxt fail, ret=%d",
+			rx_queue_id, ret);
+
+		(void)sxe2_drv_rxq_switch(adapter, rxq, false);
+		rxq->ops.mbufs_release(rxq);
+		rxq->ops.queue_reset(rxq);
+		goto l_end;
+	}
+
+	SXE2_PCI_REG_WRITE_WC(rxq->rdt_reg_addr, rxq->ring_depth - 1);
+	dev->data->rx_queue_state[rx_queue_id] = RTE_ETH_QUEUE_STATE_STARTED;
+
+l_end:
+	return  ret;
+}
+
+int32_t __rte_cold sxe2_rxqs_all_start(struct rte_eth_dev *dev)
+{
+	struct rte_eth_dev_data *data = dev->data;
+	struct sxe2_rx_queue *rxq;
+	uint16_t nb_rxq;
+	uint16_t nb_started_rxq;
+	int32_t ret;
+	PMD_INIT_FUNC_TRACE();
+
+	for (nb_rxq = 0; nb_rxq < data->nb_rx_queues; nb_rxq++) {
+		rxq = dev->data->rx_queues[nb_rxq];
+		if (!rxq || rxq->rx_deferred_start)
+			continue;
+
+		ret = sxe2_rx_queue_start(dev, nb_rxq);
+		if (ret) {
+			PMD_LOG_ERR(RX, "Fail to start rx queue %u", nb_rxq);
+			goto l_free_started_queue;
+		}
+
+		rte_atomic_store_explicit(&rxq->sw_stats.pkts, 0,
+			rte_memory_order_relaxed);
+		rte_atomic_store_explicit(&rxq->sw_stats.bytes, 0,
+			rte_memory_order_relaxed);
+		rte_atomic_store_explicit(&rxq->sw_stats.drop_pkts, 0,
+			rte_memory_order_relaxed);
+		rte_atomic_store_explicit(&rxq->sw_stats.drop_bytes, 0,
+			rte_memory_order_relaxed);
+		rte_atomic_store_explicit(&rxq->sw_stats.unicast_pkts, 0,
+			rte_memory_order_relaxed);
+		rte_atomic_store_explicit(&rxq->sw_stats.broadcast_pkts, 0,
+			rte_memory_order_relaxed);
+		rte_atomic_store_explicit(&rxq->sw_stats.multicast_pkts, 0,
+			rte_memory_order_relaxed);
+	}
+	ret = 0;
+	goto l_end;
+
+l_free_started_queue:
+	for (nb_started_rxq = 0; nb_started_rxq <= nb_rxq; nb_started_rxq++)
+		(void)sxe2_rx_queue_stop(dev, nb_started_rxq);
+l_end:
+	return ret;
+}
+
+void __rte_cold sxe2_rxqs_all_stop(struct rte_eth_dev *dev)
+{
+	struct rte_eth_dev_data *data = dev->data;
+	struct sxe2_adapter *adapter  = SXE2_DEV_PRIVATE_TO_ADAPTER(dev);
+	struct sxe2_vsi     *vsi      = adapter->vsi_ctxt.main_vsi;
+	struct sxe2_stats   *sw_stats_prev = &vsi->vsi_stats.vsi_sw_stats_prev;
+	struct sxe2_rx_queue *rxq = NULL;
+	int32_t ret;
+	uint16_t nb_rxq;
+	PMD_INIT_FUNC_TRACE();
+
+	for (nb_rxq = 0; nb_rxq < data->nb_rx_queues; nb_rxq++) {
+		ret = sxe2_rx_queue_stop(dev, nb_rxq);
+		if (ret) {
+			PMD_LOG_ERR(RX, "Fail to stop rx queue %u", nb_rxq);
+			continue;
+		}
+
+		rxq = dev->data->rx_queues[nb_rxq];
+		if (rxq) {
+			sw_stats_prev->ipackets +=
+				rte_atomic_load_explicit(&rxq->sw_stats.pkts,
+					rte_memory_order_relaxed);
+			sw_stats_prev->ierrors +=
+				rte_atomic_load_explicit(&rxq->sw_stats.drop_pkts,
+					rte_memory_order_relaxed);
+			sw_stats_prev->ibytes +=
+				rte_atomic_load_explicit(&rxq->sw_stats.bytes,
+					rte_memory_order_relaxed);
+
+			sw_stats_prev->rx_sw_unicast_packets +=
+				rte_atomic_load_explicit(&rxq->sw_stats.unicast_pkts,
+					rte_memory_order_relaxed);
+			sw_stats_prev->rx_sw_broadcast_packets +=
+				rte_atomic_load_explicit(&rxq->sw_stats.broadcast_pkts,
+					rte_memory_order_relaxed);
+			sw_stats_prev->rx_sw_multicast_packets +=
+				rte_atomic_load_explicit(&rxq->sw_stats.multicast_pkts,
+					rte_memory_order_relaxed);
+			sw_stats_prev->rx_sw_drop_packets +=
+				rte_atomic_load_explicit(&rxq->sw_stats.drop_pkts,
+					rte_memory_order_relaxed);
+			sw_stats_prev->rx_sw_drop_bytes +=
+				rte_atomic_load_explicit(&rxq->sw_stats.drop_bytes,
+					rte_memory_order_relaxed);
+		}
+	}
+}
diff --git a/drivers/net/sxe2/sxe2_rx.h b/drivers/net/sxe2/sxe2_rx.h
new file mode 100644
index 0000000000..1c53f7f559
--- /dev/null
+++ b/drivers/net/sxe2/sxe2_rx.h
@@ -0,0 +1,32 @@
+/* SPDX-License-Identifier: BSD-3-Clause
+ * Copyright (C), 2025, Wuxi Stars Micro System Technologies Co., Ltd.
+ */
+
+#ifndef __SXE2_RX_H__
+#define __SXE2_RX_H__
+
+#include "sxe2_queue.h"
+
+int32_t __rte_cold sxe2_rx_queue_setup(struct rte_eth_dev *dev,
+				uint16_t queue_idx, uint16_t nb_desc, uint32_t socket_id,
+				const struct rte_eth_rxconf *rx_conf,
+				struct rte_mempool *mp);
+
+int32_t __rte_cold sxe2_rx_queue_stop(struct rte_eth_dev *dev, uint16_t rx_queue_id);
+
+void __rte_cold sxe2_rx_queue_mbufs_release(struct sxe2_rx_queue *rxq);
+
+void __rte_cold sxe2_rx_queue_release(struct rte_eth_dev *dev, uint16_t queue_idx);
+
+void __rte_cold sxe2_all_rxqs_release(struct rte_eth_dev *dev);
+
+void __rte_cold sxe2_rx_queue_info_get(struct rte_eth_dev *dev, uint16_t queue_id,
+		struct rte_eth_rxq_info *qinfo);
+
+int32_t __rte_cold sxe2_rx_queue_start(struct rte_eth_dev *dev, uint16_t rx_queue_id);
+
+int32_t __rte_cold sxe2_rxqs_all_start(struct rte_eth_dev *dev);
+
+void __rte_cold sxe2_rxqs_all_stop(struct rte_eth_dev *dev);
+
+#endif /* __SXE2_RX_H__ */
diff --git a/drivers/net/sxe2/sxe2_tx.c b/drivers/net/sxe2/sxe2_tx.c
new file mode 100644
index 0000000000..a05beb8c7a
--- /dev/null
+++ b/drivers/net/sxe2/sxe2_tx.c
@@ -0,0 +1,420 @@
+/* SPDX-License-Identifier: BSD-3-Clause
+ * Copyright (C), 2025, Wuxi Stars Micro System Technologies Co., Ltd.
+ */
+
+#include <rte_common.h>
+#include <rte_net.h>
+#include <rte_vect.h>
+#include <rte_malloc.h>
+#include <rte_memzone.h>
+#include <ethdev_driver.h>
+#include "sxe2_tx.h"
+#include "sxe2_ethdev.h"
+#include "sxe2_common_log.h"
+#include "sxe2_cmd_chnl.h"
+
+static void *sxe2_tx_doorbell_addr_get(struct sxe2_adapter *adapter, uint16_t queue_id)
+{
+	return sxe2_pci_map_addr_get(adapter, SXE2_PCI_MAP_RES_DOORBELL_TX,
+				     queue_id);
+}
+
+static void sxe2_tx_tail_init(struct sxe2_adapter *adapter, struct sxe2_tx_queue *txq)
+{
+	txq->tdt_reg_addr = sxe2_tx_doorbell_addr_get(adapter, txq->queue_id);
+	SXE2_PCI_REG_WRITE_WC(txq->tdt_reg_addr, 0);
+}
+
+void __rte_cold sxe2_tx_queue_reset(struct sxe2_tx_queue *txq)
+{
+	uint16_t prev, i;
+	volatile union sxe2_tx_data_desc *txd;
+	static const union sxe2_tx_data_desc zeroed_desc = {{0}};
+	struct sxe2_tx_buffer *tx_buffer = txq->buffer_ring;
+
+	for (i = 0; i < txq->ring_depth; i++)
+		txq->desc_ring[i] = zeroed_desc;
+
+	prev = txq->ring_depth - 1;
+	for (i = 0; i < txq->ring_depth; i++) {
+		txd = &txq->desc_ring[i];
+		if (txd == NULL)
+			continue;
+
+		txd->wb.dd = rte_cpu_to_le_64(SXE2_TX_DESC_DTYPE_DESC_DONE);
+		tx_buffer[i].mbuf       = NULL;
+		tx_buffer[i].last_id    = i;
+		tx_buffer[prev].next_id = i;
+		prev = i;
+	}
+
+	txq->desc_used_num = 0;
+	txq->desc_free_num = txq->ring_depth - 1;
+	txq->next_use      = 0;
+	txq->next_clean    = txq->ring_depth - 1;
+	txq->next_dd       = txq->rs_thresh  - 1;
+	txq->next_rs       = txq->rs_thresh  - 1;
+}
+
+void __rte_cold sxe2_tx_queue_mbufs_release(struct sxe2_tx_queue *txq)
+{
+	uint32_t i;
+
+	if (txq != NULL && txq->buffer_ring != NULL) {
+		for (i = 0; i < txq->ring_depth; i++) {
+			if (txq->buffer_ring[i].mbuf != NULL) {
+				rte_pktmbuf_free_seg(txq->buffer_ring[i].mbuf);
+				txq->buffer_ring[i].mbuf = NULL;
+			}
+		}
+	}
+}
+
+static void sxe2_tx_buffer_ring_free(struct sxe2_tx_queue *txq)
+{
+	if (txq != NULL && txq->buffer_ring != NULL)
+		rte_free(txq->buffer_ring);
+}
+
+const struct sxe2_txq_ops sxe2_default_txq_ops = {
+	.queue_reset      = sxe2_tx_queue_reset,
+	.mbufs_release    = sxe2_tx_queue_mbufs_release,
+	.buffer_ring_free = sxe2_tx_buffer_ring_free,
+};
+
+static struct sxe2_txq_ops sxe2_tx_default_ops_get(void)
+{
+	return sxe2_default_txq_ops;
+}
+
+static int32_t sxe2_txq_arg_validate(struct rte_eth_dev *dev, uint16_t ring_depth,
+		uint16_t *rs_thresh, uint16_t *free_thresh, const struct rte_eth_txconf *tx_conf)
+{
+	int32_t ret = 0;
+
+	if ((ring_depth % SXE2_TX_DESC_RING_ALIGN) != 0 ||
+		ring_depth > SXE2_MAX_RING_DESC ||
+		ring_depth < SXE2_MIN_RING_DESC) {
+		PMD_LOG_ERR(TX, "number:%u of receive descriptors is invalid", ring_depth);
+		ret = -EINVAL;
+		goto l_end;
+	}
+
+	*free_thresh = (uint16_t)((tx_conf->tx_free_thresh) ?
+			tx_conf->tx_free_thresh : DEFAULT_TX_FREE_THRESH);
+	*rs_thresh   = (uint16_t)((tx_conf->tx_rs_thresh) ?
+			tx_conf->tx_rs_thresh : DEFAULT_TX_RS_THRESH);
+
+	if (*rs_thresh >= (ring_depth - 2)) {
+		PMD_LOG_ERR(TX, "tx_rs_thresh must be less than the number "
+			"of tx descriptors minus 2. (tx_rs_thresh:%u port:%u)",
+			*rs_thresh, dev->data->port_id);
+		ret = -EINVAL;
+		goto l_end;
+	}
+
+	if (*free_thresh >= (ring_depth - 3)) {
+		PMD_LOG_ERR(TX, "tx_free_thresh must be less than the number "
+			"of tx descriptors minus 3. (tx_free_thresh:%u port:%u)",
+			*free_thresh, dev->data->port_id);
+		ret = -EINVAL;
+		goto l_end;
+	}
+
+	if (*rs_thresh > *free_thresh) {
+		PMD_LOG_ERR(TX, "tx_rs_thresh must be less than or equal to "
+			"tx_free_thresh. (tx_free_thresh:%u tx_rs_thresh:%u port:%u)",
+			*free_thresh, *rs_thresh, dev->data->port_id);
+		ret = -EINVAL;
+		goto l_end;
+	}
+
+	if ((ring_depth % *rs_thresh) != 0) {
+		PMD_LOG_ERR(TX, "tx_rs_thresh must be a divisor of the "
+			"number of tx descriptors. (tx_rs_thresh:%u port:%d ring_depth:%u)",
+			*rs_thresh, dev->data->port_id, ring_depth);
+		ret = -EINVAL;
+		goto l_end;
+	}
+
+	ret = 0;
+
+l_end:
+	return ret;
+}
+
+void __rte_cold sxe2_tx_queue_info_get(struct rte_eth_dev *dev, uint16_t queue_id,
+		struct rte_eth_txq_info *qinfo)
+{
+	struct sxe2_tx_queue *txq = NULL;
+
+	txq = dev->data->tx_queues[queue_id];
+	if (txq == NULL) {
+		PMD_LOG_WARN(TX, "tx queue:%u is NULL", queue_id);
+		goto end;
+	}
+
+	qinfo->nb_desc                = txq->ring_depth;
+
+	qinfo->conf.tx_thresh.pthresh = txq->pthresh;
+	qinfo->conf.tx_thresh.hthresh = txq->hthresh;
+	qinfo->conf.tx_thresh.wthresh = txq->wthresh;
+	qinfo->conf.tx_free_thresh    = txq->free_thresh;
+	qinfo->conf.tx_rs_thresh      = txq->rs_thresh;
+	qinfo->conf.offloads          = txq->offloads;
+	qinfo->conf.tx_deferred_start = txq->tx_deferred_start;
+
+end:
+	return;
+}
+
+int32_t __rte_cold sxe2_tx_queue_stop(struct rte_eth_dev *dev, uint16_t queue_id)
+{
+	struct sxe2_adapter *adapter = SXE2_DEV_PRIVATE_TO_ADAPTER(dev);
+	struct sxe2_tx_queue *txq;
+	int32_t ret;
+	PMD_INIT_FUNC_TRACE();
+
+	if (dev->data->tx_queue_state[queue_id] ==
+			RTE_ETH_QUEUE_STATE_STOPPED) {
+		ret = 0;
+		goto l_end;
+	}
+
+	txq = dev->data->tx_queues[queue_id];
+	if (txq == NULL) {
+		ret = 0;
+		goto l_end;
+	}
+
+	ret = sxe2_drv_txq_switch(adapter, txq, false);
+	if (ret) {
+		PMD_LOG_ERR(TX, "Failed to switch tx queue %u off",
+				queue_id);
+		goto l_end;
+	}
+
+	txq->ops.mbufs_release(txq);
+	txq->ops.queue_reset(txq);
+	dev->data->tx_queue_state[queue_id] = RTE_ETH_QUEUE_STATE_STOPPED;
+	ret = 0;
+
+l_end:
+	return ret;
+}
+
+static void __rte_cold sxe2_tx_queue_free(struct sxe2_tx_queue *txq)
+{
+	if (txq != NULL) {
+		txq->ops.mbufs_release(txq);
+		txq->ops.buffer_ring_free(txq);
+
+		rte_memzone_free(txq->mz);
+		rte_free(txq);
+	}
+}
+
+void __rte_cold sxe2_tx_queue_release(struct rte_eth_dev *dev, uint16_t queue_idx)
+{
+	(void)sxe2_tx_queue_stop(dev, queue_idx);
+	sxe2_tx_queue_free(dev->data->tx_queues[queue_idx]);
+	dev->data->tx_queues[queue_idx] = NULL;
+}
+
+void __rte_cold sxe2_all_txqs_release(struct rte_eth_dev *dev)
+{
+	struct rte_eth_dev_data *data = dev->data;
+	uint16_t nb_txq;
+
+	for (nb_txq = 0; nb_txq < data->nb_tx_queues; nb_txq++) {
+		if (data->tx_queues[nb_txq] == NULL)
+			continue;
+
+		sxe2_tx_queue_release(dev, nb_txq);
+		data->tx_queues[nb_txq] = NULL;
+	}
+	data->nb_tx_queues = 0;
+}
+
+static struct sxe2_tx_queue
+*sxe2_tx_queue_alloc(struct rte_eth_dev *dev, uint16_t queue_idx,
+		uint16_t ring_depth, uint32_t socket_id)
+{
+	struct sxe2_tx_queue *txq;
+	const struct rte_memzone *tz;
+
+	if (dev->data->tx_queues[queue_idx]) {
+		sxe2_tx_queue_release(dev, queue_idx);
+		dev->data->tx_queues[queue_idx] = NULL;
+	}
+
+	txq = rte_zmalloc_socket("tx_queue", sizeof(struct sxe2_tx_queue),
+			RTE_CACHE_LINE_SIZE, socket_id);
+	if (txq == NULL) {
+		PMD_LOG_ERR(TX, "tx queue:%d alloc failed", queue_idx);
+		goto l_end;
+	}
+
+	tz = rte_eth_dma_zone_reserve(dev, "tx_dma", queue_idx,
+			sizeof(union sxe2_tx_data_desc) * SXE2_MAX_RING_DESC,
+			SXE2_DESC_ADDR_ALIGN, socket_id);
+	if (tz == NULL) {
+		PMD_LOG_ERR(TX, "tx desc ring alloc failed, queue_id:%d", queue_idx);
+		rte_free(txq);
+		txq = NULL;
+		goto l_end;
+	}
+
+	txq->buffer_ring = rte_zmalloc_socket("tx_buffer_ring",
+		sizeof(struct sxe2_tx_buffer) * ring_depth,
+		RTE_CACHE_LINE_SIZE, socket_id);
+	if (txq->buffer_ring == NULL) {
+		PMD_LOG_ERR(TX, "tx buffer alloc failed, queue_id:%d", queue_idx);
+		rte_memzone_free(tz);
+		rte_free(txq);
+		txq = NULL;
+		goto l_end;
+	}
+
+	txq->mz = tz;
+	txq->base_addr = tz->iova;
+	txq->desc_ring = (volatile union sxe2_tx_data_desc *)tz->addr;
+
+l_end:
+	return txq;
+}
+
+int32_t __rte_cold sxe2_tx_queue_setup(struct rte_eth_dev *dev,
+		uint16_t queue_idx, uint16_t nb_desc, uint32_t socket_id,
+		const struct rte_eth_txconf *tx_conf)
+{
+	int32_t ret = 0;
+	uint16_t tx_rs_thresh;
+	uint16_t tx_free_thresh;
+	struct sxe2_tx_queue *txq;
+	struct sxe2_adapter  *adapter = SXE2_DEV_PRIVATE_TO_ADAPTER(dev);
+	struct sxe2_vsi      *vsi     = adapter->vsi_ctxt.main_vsi;
+	uint64_t offloads;
+	PMD_INIT_FUNC_TRACE();
+
+	ret = sxe2_txq_arg_validate(dev, nb_desc, &tx_rs_thresh, &tx_free_thresh, tx_conf);
+	if (ret) {
+		PMD_LOG_ERR(TX, "tx queue:%u arg validate failed", queue_idx);
+		goto end;
+	}
+
+	offloads = tx_conf->offloads | dev->data->dev_conf.txmode.offloads;
+
+	txq = sxe2_tx_queue_alloc(dev, queue_idx, nb_desc, socket_id);
+	if (txq == NULL) {
+		PMD_LOG_ERR(TX, "failed to alloc sxe2vf tx queue:%u resource", queue_idx);
+		ret = -ENOMEM;
+		goto end;
+	}
+
+	txq->vlan_flag         = SXE2_TX_FLAGS_VLAN_TAG_LOC_L2TAG1;
+	txq->ring_depth        = nb_desc;
+	txq->rs_thresh         = tx_rs_thresh;
+	txq->free_thresh       = tx_free_thresh;
+	txq->pthresh           = tx_conf->tx_thresh.pthresh;
+	txq->hthresh           = tx_conf->tx_thresh.hthresh;
+	txq->wthresh           = tx_conf->tx_thresh.wthresh;
+	txq->queue_id          = queue_idx;
+	txq->idx_in_func       = vsi->txqs.base_idx_in_func + queue_idx;
+	txq->port_id           = dev->data->port_id;
+	txq->offloads          = offloads;
+	txq->tx_deferred_start = tx_conf->tx_deferred_start;
+	txq->vsi               = vsi;
+	txq->ops               = sxe2_tx_default_ops_get();
+	txq->ops.queue_reset(txq);
+
+	dev->data->tx_queues[queue_idx] = txq;
+	ret = 0;
+
+end:
+	return ret;
+}
+
+int32_t __rte_cold sxe2_tx_queue_start(struct rte_eth_dev *dev, uint16_t queue_id)
+{
+	int32_t    ret = 0;
+	struct sxe2_tx_queue *txq;
+	struct sxe2_adapter *adapter = SXE2_DEV_PRIVATE_TO_ADAPTER(dev);
+	PMD_INIT_FUNC_TRACE();
+
+	if (dev->data->tx_queue_state[queue_id] == RTE_ETH_QUEUE_STATE_STARTED) {
+		ret = 0;
+		goto l_end;
+	}
+
+	txq = dev->data->tx_queues[queue_id];
+	if (txq == NULL) {
+		PMD_LOG_ERR(TX, "tx queue:%u is not available or setup", queue_id);
+		ret = -EINVAL;
+		goto l_end;
+	}
+
+	ret = sxe2_drv_txq_ctxt_cfg(adapter, txq, 1);
+	if (ret) {
+		PMD_LOG_ERR(TX, "tx queue:%u config ctxt fail", queue_id);
+
+		(void)sxe2_drv_txq_switch(adapter, txq, false);
+		txq->ops.mbufs_release(txq);
+		txq->ops.queue_reset(txq);
+		goto l_end;
+	}
+
+	sxe2_tx_tail_init(adapter, txq);
+
+	dev->data->tx_queue_state[queue_id] = RTE_ETH_QUEUE_STATE_STARTED;
+	ret = 0;
+
+l_end:
+	return ret;
+}
+
+int32_t __rte_cold sxe2_txqs_all_start(struct rte_eth_dev *dev)
+{
+	struct rte_eth_dev_data *data = dev->data;
+	struct sxe2_tx_queue *txq;
+	uint16_t nb_txq;
+	uint16_t nb_started_txq;
+	int32_t ret;
+	PMD_INIT_FUNC_TRACE();
+
+	for (nb_txq = 0; nb_txq < data->nb_tx_queues; nb_txq++) {
+		txq = dev->data->tx_queues[nb_txq];
+		if (!txq || txq->tx_deferred_start)
+			continue;
+
+		ret = sxe2_tx_queue_start(dev, nb_txq);
+		if (ret) {
+			PMD_LOG_ERR(TX, "Fail to start tx queue %u", nb_txq);
+			goto l_free_started_queue;
+		}
+	}
+	ret = 0;
+	goto l_end;
+
+l_free_started_queue:
+	for (nb_started_txq = 0; nb_started_txq <= nb_txq; nb_started_txq++)
+		(void)sxe2_tx_queue_stop(dev, nb_started_txq);
+
+l_end:
+	return ret;
+}
+
+void __rte_cold sxe2_txqs_all_stop(struct rte_eth_dev *dev)
+{
+	struct rte_eth_dev_data *data = dev->data;
+	uint16_t nb_txq;
+	int32_t ret;
+
+	for (nb_txq = 0; nb_txq < data->nb_tx_queues; nb_txq++) {
+		ret = sxe2_tx_queue_stop(dev, nb_txq);
+		if (ret) {
+			PMD_LOG_WARN(TX, "Fail to stop tx queue %u", nb_txq);
+			continue;
+		}
+	}
+}
diff --git a/drivers/net/sxe2/sxe2_tx.h b/drivers/net/sxe2/sxe2_tx.h
new file mode 100644
index 0000000000..c929b1bee2
--- /dev/null
+++ b/drivers/net/sxe2/sxe2_tx.h
@@ -0,0 +1,32 @@
+/* SPDX-License-Identifier: BSD-3-Clause
+ * Copyright (C), 2025, Wuxi Stars Micro System Technologies Co., Ltd.
+ */
+
+#ifndef __SXE2_TX_H__
+#define __SXE2_TX_H__
+#include "sxe2_queue.h"
+
+void __rte_cold sxe2_tx_queue_reset(struct sxe2_tx_queue *txq);
+
+int32_t __rte_cold sxe2_tx_queue_start(struct rte_eth_dev *dev, uint16_t queue_id);
+
+void sxe2_tx_queue_mbufs_release(struct sxe2_tx_queue *txq);
+
+int32_t __rte_cold sxe2_tx_queue_stop(struct rte_eth_dev *dev, uint16_t queue_id);
+
+int32_t __rte_cold sxe2_tx_queue_setup(struct rte_eth_dev *dev,
+		uint16_t queue_idx, uint16_t nb_desc, uint32_t socket_id,
+		const struct rte_eth_txconf *tx_conf);
+
+void __rte_cold sxe2_tx_queue_release(struct rte_eth_dev *dev, uint16_t queue_idx);
+
+void __rte_cold sxe2_all_txqs_release(struct rte_eth_dev *dev);
+
+void __rte_cold sxe2_tx_queue_info_get(struct rte_eth_dev *dev, uint16_t queue_id,
+		struct rte_eth_txq_info *qinfo);
+
+int32_t __rte_cold sxe2_txqs_all_start(struct rte_eth_dev *dev);
+
+void __rte_cold sxe2_txqs_all_stop(struct rte_eth_dev *dev);
+
+#endif /* __SXE2_TX_H__ */
-- 
2.47.3


^ permalink raw reply related

* [PATCH v17 09/11] drivers: add data path for Rx and Tx
From: liujie5 @ 2026-05-19  3:01 UTC (permalink / raw)
  To: stephen; +Cc: dev, Jie Liu
In-Reply-To: <20260519030132.3780057-1-liujie5@linkdatatechnology.com>

From: Jie Liu <liujie5@linkdatatechnology.com>

Implement receive and transmit burst functions for sxe2 PMD.
Add sxe2_recv_pkts and sxe2_xmit_pkts as the primary data path
interfaces.

The implementation includes:
- Efficient descriptor fetching and mbuf allocation for Rx.
- Descriptor setup and checksum offload handling for Tx.
- Buffer recycling and hardware tail pointer updates.
- Performance-oriented loop unrolling and prefetching where applicable.

Signed-off-by: Jie Liu <liujie5@linkdatatechnology.com>
---
 drivers/common/sxe2/sxe2_ioctl_chnl.c |   8 +-
 drivers/net/sxe2/meson.build          |   2 +
 drivers/net/sxe2/sxe2_ethdev.c        |  11 +-
 drivers/net/sxe2/sxe2_txrx.c          | 246 +++++++
 drivers/net/sxe2/sxe2_txrx.h          |  21 +
 drivers/net/sxe2/sxe2_txrx_poll.c     | 916 ++++++++++++++++++++++++++
 6 files changed, 1199 insertions(+), 5 deletions(-)
 create mode 100644 drivers/net/sxe2/sxe2_txrx.c
 create mode 100644 drivers/net/sxe2/sxe2_txrx.h
 create mode 100644 drivers/net/sxe2/sxe2_txrx_poll.c

diff --git a/drivers/common/sxe2/sxe2_ioctl_chnl.c b/drivers/common/sxe2/sxe2_ioctl_chnl.c
index a40f9b8da2..c02e73f1f7 100644
--- a/drivers/common/sxe2/sxe2_ioctl_chnl.c
+++ b/drivers/common/sxe2/sxe2_ioctl_chnl.c
@@ -177,13 +177,13 @@ void
 		goto l_err;
 	}
 
-	PMD_LOG_DEBUG(COM, "fd=%d, bar idx=%d, len=0x%zx, src=0x%"PRIx64", offset=0x%"PRIx64"",
+	PMD_LOG_DEBUG(COM, "fd=%d, bar idx=%d, len=%"PRIu64", src=0x%"PRIx64", offset=0x%"PRIx64"",
 		bar_idx, cmd_fd, len, offset, SXE2_COM_PCI_OFFSET_GEN(bar_idx, offset));
 
 	virt = mmap(NULL, len, PROT_READ | PROT_WRITE,
 		MAP_SHARED, cmd_fd, SXE2_COM_PCI_OFFSET_GEN(bar_idx, offset));
 	if (virt == MAP_FAILED) {
-		PMD_LOG_ERR(COM, "Failed mmap, cmd_fd=%d, len=0x%zx, offset=0x%"PRIx64", err:%s",
+		PMD_LOG_ERR(COM, "Failed mmap, cmd_fd=%d, len=%"PRIu64", offset=0x%"PRIx64", err:%s",
 			cmd_fd, len, offset, strerror(errno));
 		goto l_err;
 	}
@@ -205,12 +205,12 @@ sxe2_drv_dev_munmap(struct sxe2_common_device *cdev, void *virt, uint64_t len)
 		goto l_end;
 	}
 
-	PMD_LOG_DEBUG(COM, "Munmap virt=%p, len=0x%zx",
+	PMD_LOG_DEBUG(COM, "Munmap virt=%p, len=0x%"PRIx64"",
 		virt, len);
 
 	ret = munmap(virt, len);
 	if (ret < 0) {
-		PMD_LOG_ERR(COM, "Failed to munmap, virt=%p, len=0x%zx, err:%s",
+		PMD_LOG_ERR(COM, "Failed to munmap, virt=%p, len=%"PRIu64", err:%s",
 			virt, len, strerror(errno));
 		ret = -errno;
 		goto l_end;
diff --git a/drivers/net/sxe2/meson.build b/drivers/net/sxe2/meson.build
index 3dfe54903a..5645e3ad61 100644
--- a/drivers/net/sxe2/meson.build
+++ b/drivers/net/sxe2/meson.build
@@ -20,6 +20,8 @@ sources += files(
         'sxe2_queue.c',
         'sxe2_tx.c',
         'sxe2_rx.c',
+        'sxe2_txrx_poll.c',
+        'sxe2_txrx.c',
 )
 
 allow_internal_get_api = true
diff --git a/drivers/net/sxe2/sxe2_ethdev.c b/drivers/net/sxe2/sxe2_ethdev.c
index 6abb4672f6..e47e788d78 100644
--- a/drivers/net/sxe2/sxe2_ethdev.c
+++ b/drivers/net/sxe2/sxe2_ethdev.c
@@ -26,6 +26,7 @@
 #include "sxe2_cmd_chnl.h"
 #include "sxe2_tx.h"
 #include "sxe2_rx.h"
+#include "sxe2_txrx.h"
 #include "sxe2_common.h"
 #include "sxe2_common_log.h"
 #include "sxe2_host_regs.h"
@@ -137,6 +138,9 @@ static int32_t sxe2_dev_start(struct rte_eth_dev *dev)
 		goto l_end;
 	}
 
+	sxe2_rx_mode_func_set(dev);
+	sxe2_tx_mode_func_set(dev);
+
 	ret = sxe2_queues_start(dev);
 	if (ret) {
 		PMD_LOG_ERR(INIT, "enable queues failed");
@@ -763,10 +767,15 @@ static int32_t sxe2_dev_init(struct rte_eth_dev *dev,
 
 	PMD_INIT_FUNC_TRACE();
 
+	sxe2_set_common_function(dev);
+
 	dev->dev_ops = &sxe2_eth_dev_ops;
 
-	if (rte_eal_process_type() != RTE_PROC_PRIMARY)
+	if (rte_eal_process_type() != RTE_PROC_PRIMARY) {
+		sxe2_rx_mode_func_set(dev);
+		sxe2_tx_mode_func_set(dev);
 		goto l_end;
+	}
 
 	ret = sxe2_hw_init(dev);
 	if (ret) {
diff --git a/drivers/net/sxe2/sxe2_txrx.c b/drivers/net/sxe2/sxe2_txrx.c
new file mode 100644
index 0000000000..7daefb294c
--- /dev/null
+++ b/drivers/net/sxe2/sxe2_txrx.c
@@ -0,0 +1,246 @@
+/* SPDX-License-Identifier: BSD-3-Clause
+ * Copyright (C), 2025, Wuxi Stars Micro System Technologies Co., Ltd.
+ */
+
+#include <rte_common.h>
+#include <rte_net.h>
+#include <rte_vect.h>
+#include <rte_malloc.h>
+#include <rte_memzone.h>
+#include <ethdev_driver.h>
+#include <unistd.h>
+
+#include "sxe2_txrx.h"
+#include "sxe2_txrx_common.h"
+#include "sxe2_txrx_poll.h"
+#include "sxe2_ethdev.h"
+
+#include "sxe2_common_log.h"
+#include "sxe2_osal.h"
+#include "sxe2_cmd_chnl.h"
+#if defined(RTE_ARCH_ARM64)
+#include <rte_cpuflags.h>
+#endif
+
+static int32_t sxe2_tx_desciptor_status(void *tx_queue, uint16_t offset)
+{
+	struct sxe2_tx_queue *txq = (struct sxe2_tx_queue *)tx_queue;
+	int32_t ret;
+	uint16_t desc_idx;
+
+	if (unlikely(offset >= txq->ring_depth)) {
+		ret = -EINVAL;
+		goto l_end;
+	}
+
+	desc_idx = txq->next_use + offset;
+	desc_idx = SXE2_DIV_ROUND_UP(desc_idx, txq->rs_thresh) * (txq->rs_thresh);
+	if (desc_idx >= txq->ring_depth) {
+		desc_idx -= txq->ring_depth;
+		if (desc_idx >= txq->ring_depth)
+			desc_idx -= txq->ring_depth;
+	}
+
+	if (desc_idx == 0)
+		desc_idx = txq->rs_thresh - 1;
+	else
+		desc_idx -= 1;
+
+	if (rte_cpu_to_le_64(SXE2_TX_DESC_DTYPE_DESC_DONE) ==
+		(txq->desc_ring[desc_idx].wb.dd &
+		rte_cpu_to_le_64(SXE2_TX_DESC_DTYPE_DESC_MASK)))
+		ret = RTE_ETH_TX_DESC_DONE;
+	else
+		ret = RTE_ETH_TX_DESC_FULL;
+
+l_end:
+	return ret;
+}
+
+static inline int32_t sxe2_tx_mbuf_empty_check(struct rte_mbuf *mbuf)
+{
+	struct rte_mbuf *m_seg = mbuf;
+
+	while (m_seg != NULL) {
+		if (m_seg->data_len == 0)
+			return -EINVAL;
+		m_seg = m_seg->next;
+	}
+
+	return 0;
+}
+
+uint16_t sxe2_tx_pkts_prepare(void *tx_queue,
+		struct rte_mbuf **tx_pkts, uint16_t nb_pkts)
+{
+	struct sxe2_tx_queue *txq = tx_queue;
+	struct rte_mbuf *mbuf;
+	uint64_t ol_flags = 0;
+	int32_t ret = 0;
+	int32_t i = 0;
+
+	for (i = 0; i < nb_pkts; i++) {
+		mbuf = tx_pkts[i];
+		if (!mbuf)
+			continue;
+		ol_flags = mbuf->ol_flags;
+		if (!(ol_flags & (RTE_MBUF_F_TX_TCP_SEG | RTE_MBUF_F_TX_UDP_SEG))) {
+			if (mbuf->nb_segs > SXE2_TX_MTU_SEG_MAX ||
+					mbuf->pkt_len > SXE2_FRAME_SIZE_MAX) {
+				rte_errno = -EINVAL;
+				goto l_end;
+			}
+		} else if ((mbuf->tso_segsz < SXE2_MIN_TSO_MSS) ||
+			(mbuf->tso_segsz > SXE2_MAX_TSO_MSS) ||
+			(mbuf->nb_segs   > txq->ring_depth) ||
+			(mbuf->pkt_len > SXE2_TX_TSO_PKTLEN_MAX)) {
+			rte_errno = -EINVAL;
+			goto l_end;
+		}
+
+		if (mbuf->pkt_len < SXE2_TX_MIN_PKT_LEN) {
+			rte_errno = -EINVAL;
+			goto l_end;
+		}
+
+#ifdef RTE_ETHDEV_DEBUG_TX
+		ret = rte_validate_tx_offload(mbuf);
+		if (ret != 0) {
+			rte_errno = -ret;
+			goto l_end;
+		}
+#endif
+		ret = rte_net_intel_cksum_prepare(mbuf);
+		if (ret != 0) {
+			rte_errno = -ret;
+			goto l_end;
+		}
+
+		ret = sxe2_tx_mbuf_empty_check(mbuf);
+		if (ret != 0) {
+			rte_errno = -ret;
+			goto l_end;
+		}
+	}
+
+l_end:
+	return i;
+}
+
+void sxe2_tx_mode_func_set(struct rte_eth_dev *dev)
+{
+	struct sxe2_adapter *adapter = SXE2_DEV_PRIVATE_TO_ADAPTER(dev);
+	uint32_t tx_mode_flags = 0;
+
+	PMD_INIT_FUNC_TRACE();
+
+	dev->tx_pkt_prepare = sxe2_tx_pkts_prepare;
+	dev->tx_pkt_burst = sxe2_tx_pkts;
+	adapter->q_ctxt.tx_mode_flags = tx_mode_flags;
+	PMD_LOG_DEBUG(TX, "Tx mode flags:0x%016x port_id:%u.",
+				tx_mode_flags, dev->data->port_id);
+}
+
+static int32_t sxe2_rx_desciptor_status(void *rx_queue, uint16_t offset)
+{
+	struct sxe2_rx_queue *rxq = (struct sxe2_rx_queue *)rx_queue;
+	volatile union sxe2_rx_desc *desc;
+	int32_t ret;
+
+	if (unlikely(offset >= rxq->ring_depth)) {
+		ret = -EINVAL;
+		goto l_end;
+	}
+
+	if (offset >= rxq->ring_depth - rxq->hold_num) {
+		ret = RTE_ETH_RX_DESC_UNAVAIL;
+		goto l_end;
+	}
+
+	if (rxq->processing_idx + offset >= rxq->ring_depth)
+		desc = &rxq->desc_ring[rxq->processing_idx + offset - rxq->ring_depth];
+	else
+		desc = &rxq->desc_ring[rxq->processing_idx + offset];
+
+	if (rte_le_to_cpu_64(desc->wb.status_err_ptype_len) & SXE2_RX_DESC_STATUS_DD_MASK)
+		ret = RTE_ETH_RX_DESC_DONE;
+	else
+		ret = RTE_ETH_RX_DESC_AVAIL;
+
+l_end:
+	PMD_LOG_DEBUG(RX, "Rx queue desc[%u] status:%d queue_id:%u port_id:%u",
+				offset, ret, rxq->queue_id, rxq->port_id);
+	return ret;
+}
+
+static int32_t sxe2_rx_queue_count(void *rx_queue)
+{
+	struct sxe2_rx_queue *rxq = (struct sxe2_rx_queue *)rx_queue;
+	volatile union sxe2_rx_desc *desc;
+	uint16_t done_num = 0;
+
+	desc = &rxq->desc_ring[rxq->processing_idx];
+	while ((done_num < rxq->ring_depth) &&
+		(rte_le_to_cpu_64(desc->wb.status_err_ptype_len) &
+		SXE2_RX_DESC_STATUS_DD_MASK)) {
+		done_num += SXE2_RX_QUEUE_CHECK_INTERVAL_NUM;
+		if (rxq->processing_idx + done_num >= rxq->ring_depth)
+			desc = &rxq->desc_ring[rxq->processing_idx + done_num - rxq->ring_depth];
+		else
+			desc += SXE2_RX_QUEUE_CHECK_INTERVAL_NUM;
+	}
+
+	PMD_LOG_DEBUG(RX, "Rx queue done desc count:%u queue_id:%u port_id:%u",
+				done_num, rxq->queue_id, rxq->port_id);
+
+	return done_num;
+}
+
+static bool __rte_cold sxe2_rx_offload_en_check(struct rte_eth_dev *dev, uint64_t offload)
+{
+	struct sxe2_rx_queue *rxq;
+	bool en = false;
+	uint16_t i;
+
+	for (i = 0; i < dev->data->nb_rx_queues; ++i) {
+		rxq = (struct sxe2_rx_queue *)dev->data->rx_queues[i];
+		if (rxq == NULL)
+			continue;
+
+		if (0 != (rxq->offloads & offload)) {
+			en = true;
+			goto l_end;
+		}
+	}
+
+l_end:
+	return en;
+}
+
+void sxe2_rx_mode_func_set(struct rte_eth_dev *dev)
+{
+	struct sxe2_adapter *adapter = SXE2_DEV_PRIVATE_TO_ADAPTER(dev);
+	uint32_t rx_mode_flags = 0;
+
+	PMD_INIT_FUNC_TRACE();
+
+	if (sxe2_rx_offload_en_check(dev, RTE_ETH_RX_OFFLOAD_BUFFER_SPLIT))
+		dev->rx_pkt_burst = sxe2_rx_pkts_scattered_split;
+	else
+		dev->rx_pkt_burst = sxe2_rx_pkts_scattered;
+
+	PMD_LOG_DEBUG(RX, "Rx mode flags:0x%016x port_id:%u.",
+				rx_mode_flags, dev->data->port_id);
+	adapter->q_ctxt.rx_mode_flags = rx_mode_flags;
+}
+
+void sxe2_set_common_function(struct rte_eth_dev *dev)
+{
+	PMD_INIT_FUNC_TRACE();
+
+	dev->rx_queue_count = sxe2_rx_queue_count;
+	dev->rx_descriptor_status = sxe2_rx_desciptor_status;
+
+	dev->tx_descriptor_status = sxe2_tx_desciptor_status;
+	dev->tx_pkt_prepare = sxe2_tx_pkts_prepare;
+}
diff --git a/drivers/net/sxe2/sxe2_txrx.h b/drivers/net/sxe2/sxe2_txrx.h
new file mode 100644
index 0000000000..f6558e2189
--- /dev/null
+++ b/drivers/net/sxe2/sxe2_txrx.h
@@ -0,0 +1,21 @@
+/* SPDX-License-Identifier: BSD-3-Clause
+ * Copyright (C), 2025, Wuxi Stars Micro System Technologies Co., Ltd.
+ */
+
+#ifndef SXE2_TXRX_H
+#define SXE2_TXRX_H
+#include <ethdev_driver.h>
+#include "sxe2_queue.h"
+
+void sxe2_set_common_function(struct rte_eth_dev *dev);
+
+uint16_t sxe2_tx_pkts_prepare(void *tx_queue,
+		struct rte_mbuf **tx_pkts, uint16_t nb_pkts);
+
+void sxe2_tx_mode_func_set(struct rte_eth_dev *dev);
+
+void __rte_cold sxe2_rx_queue_reset(struct sxe2_rx_queue *rxq);
+
+void sxe2_rx_mode_func_set(struct rte_eth_dev *dev);
+
+#endif /* __SXE2_TXRX_H__ */
diff --git a/drivers/net/sxe2/sxe2_txrx_poll.c b/drivers/net/sxe2/sxe2_txrx_poll.c
new file mode 100644
index 0000000000..ce56f9086f
--- /dev/null
+++ b/drivers/net/sxe2/sxe2_txrx_poll.c
@@ -0,0 +1,916 @@
+/* SPDX-License-Identifier: BSD-3-Clause
+ * Copyright (C), 2025, Wuxi Stars Micro System Technologies Co., Ltd.
+ */
+
+#include <rte_common.h>
+#include <rte_net.h>
+#include <rte_vect.h>
+#include <rte_malloc.h>
+#include <rte_memzone.h>
+#include <ethdev_driver.h>
+#include "sxe2_osal.h"
+#include "sxe2_txrx_common.h"
+#include "sxe2_txrx_poll.h"
+#include "sxe2_txrx.h"
+#include "sxe2_queue.h"
+#include "sxe2_ethdev.h"
+#include "sxe2_common_log.h"
+
+static __rte_always_inline int32_t
+sxe2_tx_bufs_free(struct sxe2_tx_queue *txq)
+{
+	struct sxe2_tx_buffer *buffer;
+	struct rte_mbuf *mbuf;
+	struct rte_mbuf *mbuf_free_arr[SXE2_TX_FREE_BUFFER_SIZE_MAX];
+	int32_t ret;
+	uint32_t i;
+	uint16_t rs_thresh;
+	uint16_t free_num;
+	if ((txq->desc_ring[txq->next_dd].wb.dd &
+		     rte_cpu_to_le_64(SXE2_TX_DESC_DTYPE_MASK)) !=
+		     rte_cpu_to_le_64(SXE2_TX_DESC_DTYPE_DESC_DONE)) {
+		ret = 0;
+		goto l_end;
+	}
+	rs_thresh = txq->rs_thresh;
+	buffer = &txq->buffer_ring[txq->next_dd - rs_thresh + 1];
+	if (txq->offloads & RTE_ETH_TX_OFFLOAD_MBUF_FAST_FREE) {
+		if (likely(rs_thresh <= SXE2_TX_FREE_BUFFER_SIZE_MAX)) {
+			mbuf = buffer[0].mbuf;
+			mbuf_free_arr[0] = mbuf;
+			free_num = 1;
+			for (i = 1; i < rs_thresh; ++i) {
+				mbuf = buffer[i].mbuf;
+				if (likely(mbuf->pool == mbuf_free_arr[0]->pool)) {
+					mbuf_free_arr[free_num] = mbuf;
+					free_num++;
+				} else {
+					rte_mempool_put_bulk(mbuf_free_arr[0]->pool,
+								(void *)mbuf_free_arr, free_num);
+					mbuf_free_arr[0] = mbuf;
+					free_num = 1;
+				}
+			}
+			rte_mempool_put_bulk(mbuf_free_arr[0]->pool,
+						(void *)mbuf_free_arr, free_num);
+		} else {
+			for (i = 0; i < rs_thresh; ++i, ++buffer) {
+				rte_mempool_put(buffer->mbuf->pool, buffer->mbuf);
+				buffer->mbuf = NULL;
+			}
+		}
+	} else {
+		for (i = 0; i < rs_thresh; ++i, ++buffer) {
+			mbuf = rte_pktmbuf_prefree_seg(buffer[i].mbuf);
+				if (mbuf != NULL)
+					rte_mempool_put(mbuf->pool, mbuf);
+			buffer->mbuf = NULL;
+		}
+	}
+	txq->desc_free_num += rs_thresh;
+	txq->next_dd       += rs_thresh;
+	if (txq->next_dd >= txq->ring_depth)
+		txq->next_dd = rs_thresh - 1;
+	ret = rs_thresh;
+l_end:
+	return ret;
+}
+
+static inline int32_t sxe2_tx_cleanup(struct sxe2_tx_queue *txq)
+{
+	int32_t ret = 0;
+	volatile union sxe2_tx_data_desc *desc_ring = txq->desc_ring;
+	struct sxe2_tx_buffer *buffer_ring = txq->buffer_ring;
+	uint16_t ring_depth = txq->ring_depth;
+	uint16_t next_clean = txq->next_clean;
+	uint16_t clean_last;
+	uint16_t clean_num;
+
+	clean_last = next_clean + txq->rs_thresh;
+	if (clean_last >= ring_depth)
+		clean_last = clean_last - ring_depth;
+
+	clean_last = buffer_ring[clean_last].last_id;
+	if (rte_cpu_to_le_64(SXE2_TX_DESC_DTYPE_DESC_DONE) !=
+		(txq->desc_ring[clean_last].wb.dd & rte_cpu_to_le_64(SXE2_TX_DESC_DTYPE_MASK))) {
+		PMD_LOG_DEBUG(TX, "desc[%u] is not done.port_id=%u queue_id=%u val=0x%" PRIx64,
+						 clean_last, txq->port_id,
+			txq->queue_id, txq->desc_ring[clean_last].wb.dd);
+		ret = -1;
+		goto l_end;
+	}
+
+	if (clean_last > next_clean)
+		clean_num = clean_last - next_clean;
+	else
+		clean_num = ring_depth - next_clean + clean_last;
+
+	desc_ring[clean_last].wb.dd = 0;
+
+	txq->next_clean = clean_last;
+	txq->desc_free_num += clean_num;
+
+	ret = 0;
+
+l_end:
+	return ret;
+}
+
+static __rte_always_inline uint16_t
+sxe2_tx_pkt_data_desc_count(struct rte_mbuf *tx_pkt)
+{
+	struct rte_mbuf *m_seg = tx_pkt;
+	uint16_t count = 0;
+
+	while (m_seg != NULL) {
+		count += SXE2_DIV_ROUND_UP(m_seg->data_len,
+				SXE2_TX_MAX_DATA_NUM_PER_DESC);
+		m_seg = m_seg->next;
+	}
+
+	return count;
+}
+
+static __rte_always_inline void
+sxe2_tx_desc_checksum_fill(uint64_t offloads, uint32_t *desc_cmd, uint32_t *desc_offset,
+		union sxe2_tx_offload_info ol_info)
+{
+	if (offloads & RTE_MBUF_F_TX_IP_CKSUM) {
+		*desc_cmd    |= SXE2_TX_DATA_DESC_CMD_IIPT_IPV4_CSUM;
+		*desc_offset |= SXE2_TX_DATA_DESC_IPLEN_VAL(ol_info.l3_len);
+	} else if (offloads & RTE_MBUF_F_TX_IPV4) {
+		*desc_cmd    |= SXE2_TX_DATA_DESC_CMD_IIPT_IPV4;
+		*desc_offset |= SXE2_TX_DATA_DESC_IPLEN_VAL(ol_info.l3_len);
+	} else if (offloads & RTE_MBUF_F_TX_IPV6) {
+		*desc_cmd    |= SXE2_TX_DATA_DESC_CMD_IIPT_IPV6;
+		*desc_offset |= SXE2_TX_DATA_DESC_IPLEN_VAL(ol_info.l3_len);
+	}
+
+	if (offloads & RTE_MBUF_F_TX_TCP_SEG) {
+		*desc_cmd    |= SXE2_TX_DATA_DESC_CMD_L4T_EOFT_TCP;
+		*desc_offset |= SXE2_TX_DATA_DESC_L4LEN_VAL(ol_info.l4_len);
+		goto l_end;
+	}
+
+	if (offloads & RTE_MBUF_F_TX_UDP_SEG) {
+		*desc_cmd    |= SXE2_TX_DATA_DESC_CMD_L4T_EOFT_UDP;
+		*desc_offset |= SXE2_TX_DATA_DESC_L4LEN_VAL(ol_info.l4_len);
+		goto l_end;
+	}
+
+	switch (offloads & RTE_MBUF_F_TX_L4_MASK) {
+	case RTE_MBUF_F_TX_TCP_CKSUM:
+		*desc_cmd    |= SXE2_TX_DATA_DESC_CMD_L4T_EOFT_TCP;
+		*desc_offset |= SXE2_TX_DATA_DESC_L4LEN_VAL(ol_info.l4_len);
+		break;
+	case RTE_MBUF_F_TX_SCTP_CKSUM:
+		*desc_cmd    |= SXE2_TX_DATA_DESC_CMD_L4T_EOFT_SCTP;
+		*desc_offset |= SXE2_TX_DATA_DESC_L4LEN_VAL(ol_info.l4_len);
+		break;
+	case RTE_MBUF_F_TX_UDP_CKSUM:
+		*desc_cmd    |= SXE2_TX_DATA_DESC_CMD_L4T_EOFT_UDP;
+		*desc_offset |= SXE2_TX_DATA_DESC_L4LEN_VAL(ol_info.l4_len);
+		break;
+	default:
+
+		break;
+	}
+
+l_end:
+	return;
+}
+
+static __rte_always_inline uint64_t
+sxe2_tx_data_desc_build_cobt(uint32_t cmd, uint32_t offset, uint16_t buf_size, uint16_t l2tag)
+{
+	return rte_cpu_to_le_64(SXE2_TX_DESC_DTYPE_DATA |
+			(((uint64_t)cmd)      << SXE2_TX_DATA_DESC_CMD_SHIFT) |
+			(((uint64_t)offset)   << SXE2_TX_DATA_DESC_OFFSET_SHIFT) |
+			(((uint64_t)buf_size) << SXE2_TX_DATA_DESC_BUF_SZ_SHIFT) |
+			(((uint64_t)l2tag)    << SXE2_TX_DATA_DESC_L2TAG1_SHIFT));
+}
+
+uint16_t sxe2_tx_pkts(void *tx_queue, struct rte_mbuf **tx_pkts, uint16_t nb_pkts)
+{
+	struct sxe2_tx_queue *txq = tx_queue;
+	struct sxe2_tx_buffer *buffer_ring;
+	struct sxe2_tx_buffer *buffer;
+	struct sxe2_tx_buffer *next_buffer;
+	struct rte_mbuf *tx_pkt;
+	struct rte_mbuf *m_seg;
+	volatile union sxe2_tx_data_desc *desc_ring;
+	volatile union sxe2_tx_data_desc *desc;
+	volatile struct sxe2_tx_context_desc *ctxt_desc;
+	union sxe2_tx_offload_info ol_info;
+	struct sxe2_vsi *vsi = txq->vsi;
+	rte_iova_t buf_dma_addr;
+	uint64_t offloads;
+	uint64_t desc_type_cmd_tso_mss;
+	uint32_t desc_cmd;
+	uint32_t desc_offset;
+	uint32_t desc_tag;
+	uint32_t desc_tunneling_params;
+	uint16_t ipsec_offset;
+	uint16_t ctxt_desc_num;
+	uint16_t desc_sum_num;
+	uint16_t tx_num;
+	uint16_t seg_len;
+	uint16_t next_use;
+	uint16_t last_use;
+	uint16_t desc_l2tag2;
+
+	buffer_ring = txq->buffer_ring;
+	desc_ring   = txq->desc_ring;
+	next_use    = txq->next_use;
+	buffer      = &buffer_ring[next_use];
+
+	if (txq->desc_free_num < txq->free_thresh)
+		(void)sxe2_tx_cleanup(txq);
+
+	for (tx_num = 0; tx_num < nb_pkts; tx_num++) {
+		tx_pkt = *tx_pkts++;
+		desc_cmd              = 0;
+		desc_offset           = 0;
+		desc_tag              = 0;
+		desc_tunneling_params = 0;
+		ipsec_offset          = 0;
+		offloads              = tx_pkt->ol_flags;
+		ol_info.l2_len        = tx_pkt->l2_len;
+		ol_info.l3_len        = tx_pkt->l3_len;
+		ol_info.l4_len        = tx_pkt->l4_len;
+		ol_info.tso_segsz     = tx_pkt->tso_segsz;
+		ol_info.outer_l2_len  = tx_pkt->outer_l2_len;
+		ol_info.outer_l3_len  = tx_pkt->outer_l3_len;
+
+		ctxt_desc_num = (offloads &
+				SXE2_TX_OFFLOAD_CTXT_NEEDCK_MASK) ? 1 : 0;
+		if (unlikely(vsi->vsi_type == SXE2_VSI_T_DPDK_ESW))
+			ctxt_desc_num = 1;
+
+		if (offloads & (RTE_MBUF_F_TX_TCP_SEG | RTE_MBUF_F_TX_UDP_SEG))
+			desc_sum_num = sxe2_tx_pkt_data_desc_count(tx_pkt) + ctxt_desc_num;
+		else
+			desc_sum_num = tx_pkt->nb_segs + ctxt_desc_num;
+
+		last_use = next_use + desc_sum_num - 1;
+		if (last_use >= txq->ring_depth)
+			last_use = last_use - txq->ring_depth;
+
+		if (desc_sum_num > txq->desc_free_num) {
+			if (unlikely(sxe2_tx_cleanup(txq) != 0))
+				goto l_cleanup_exit;
+
+			if (unlikely(desc_sum_num > txq->rs_thresh)) {
+				while (desc_sum_num > txq->desc_free_num) {
+					if (unlikely(sxe2_tx_cleanup(txq) != 0))
+						goto l_cleanup_exit;
+				}
+			}
+		}
+
+		desc_offset |= SXE2_TX_DATA_DESC_MACLEN_VAL(ol_info.l2_len);
+
+		if (offloads & SXE2_TX_OFFLOAD_CKSUM_MASK) {
+			sxe2_tx_desc_checksum_fill(offloads, &desc_cmd,
+					&desc_offset, ol_info);
+		}
+
+		if (offloads & (RTE_MBUF_F_TX_VLAN | RTE_MBUF_F_TX_QINQ)) {
+			desc_cmd |= SXE2_TX_DATA_DESC_CMD_IL2TAG1;
+			desc_tag = tx_pkt->vlan_tci;
+		}
+
+		if (ctxt_desc_num) {
+			ctxt_desc = (volatile struct sxe2_tx_context_desc *)
+							&desc_ring[next_use];
+			desc_l2tag2 = 0;
+			desc_type_cmd_tso_mss = SXE2_TX_DESC_DTYPE_CTXT;
+
+			next_buffer = &buffer_ring[buffer->next_id];
+			RTE_MBUF_PREFETCH_TO_FREE(next_buffer->mbuf);
+
+			if (buffer->mbuf) {
+				rte_pktmbuf_free_seg(buffer->mbuf);
+				buffer->mbuf = NULL;
+			}
+
+			if (offloads & RTE_MBUF_F_TX_QINQ) {
+				desc_l2tag2 = tx_pkt->vlan_tci_outer;
+				desc_type_cmd_tso_mss |= SXE2_TX_CTXT_DESC_CMD_IL2TAG2_MASK;
+			}
+
+			ctxt_desc->tunneling_params =
+				rte_cpu_to_le_32(desc_tunneling_params);
+			ctxt_desc->l2tag2 = rte_cpu_to_le_16(desc_l2tag2);
+			ctxt_desc->type_cmd_tso_mss = rte_cpu_to_le_64(desc_type_cmd_tso_mss);
+			ctxt_desc->ipsec_offset = rte_cpu_to_le_64(ipsec_offset);
+
+			buffer->last_id = last_use;
+			next_use        = buffer->next_id;
+			buffer          = next_buffer;
+		}
+
+		m_seg = tx_pkt;
+
+		do {
+			desc = &desc_ring[next_use];
+			next_buffer = &buffer_ring[buffer->next_id];
+			RTE_MBUF_PREFETCH_TO_FREE(next_buffer->mbuf);
+			if (buffer->mbuf) {
+				rte_pktmbuf_free_seg(buffer->mbuf);
+				buffer->mbuf = NULL;
+			}
+
+			buffer->mbuf = m_seg;
+			seg_len = m_seg->data_len;
+			buf_dma_addr = rte_mbuf_data_iova(m_seg);
+			while ((offloads & (RTE_MBUF_F_TX_TCP_SEG | RTE_MBUF_F_TX_UDP_SEG)) &&
+					unlikely(seg_len > SXE2_TX_MAX_DATA_NUM_PER_DESC)) {
+				desc->read.buf_addr = rte_cpu_to_le_64(buf_dma_addr);
+				desc->read.type_cmd_off_bsz_l2t =
+					sxe2_tx_data_desc_build_cobt(desc_cmd, desc_offset,
+						SXE2_TX_MAX_DATA_NUM_PER_DESC,
+						desc_tag);
+				buf_dma_addr += SXE2_TX_MAX_DATA_NUM_PER_DESC;
+				seg_len      -= SXE2_TX_MAX_DATA_NUM_PER_DESC;
+				buffer->last_id = last_use;
+				next_use        = buffer->next_id;
+				buffer          = next_buffer;
+				desc            = &desc_ring[next_use];
+				next_buffer     = &buffer_ring[buffer->next_id];
+				RTE_MBUF_PREFETCH_TO_FREE(next_buffer->mbuf);
+			}
+
+			desc->read.buf_addr = rte_cpu_to_le_64(buf_dma_addr);
+			desc->read.type_cmd_off_bsz_l2t =
+				sxe2_tx_data_desc_build_cobt(desc_cmd,
+					desc_offset, seg_len, desc_tag);
+
+			buffer->last_id = last_use;
+			next_use        = buffer->next_id;
+			buffer          = next_buffer;
+
+			m_seg = m_seg->next;
+		} while (m_seg);
+
+		desc_cmd |= SXE2_TX_DATA_DESC_CMD_EOP;
+		txq->desc_used_num += desc_sum_num;
+		txq->desc_free_num -= desc_sum_num;
+
+		if (txq->desc_used_num >= txq->rs_thresh) {
+			PMD_LOG_DEBUG(TX, "Tx pkts set RS bit."
+					"last_use=%u port_id=%u, queue_id=%u",
+					last_use, txq->port_id, txq->queue_id);
+			desc_cmd |= SXE2_TX_DATA_DESC_CMD_RS;
+			txq->desc_used_num = 0;
+		}
+
+		desc->read.type_cmd_off_bsz_l2t |=
+			rte_cpu_to_le_64(((uint64_t)desc_cmd) << SXE2_TX_DATA_DESC_CMD_SHIFT);
+	}
+	goto l_end_of_tx;
+
+l_cleanup_exit:
+	if (tx_num == 0)
+		return 0;
+l_end_of_tx:
+	SXE2_PCI_REG_WRITE_WC(txq->tdt_reg_addr, next_use);
+	PMD_LOG_DEBUG(TX, "port_id=%u queue_id=%u next_use=%u send_pkts=%u",
+			txq->port_id, txq->queue_id, next_use, tx_num);
+
+	txq->next_use = next_use;
+	return tx_num;
+}
+
+static __rte_always_inline void
+sxe2_tx_data_desc_fill(volatile union sxe2_tx_data_desc *desc,
+		struct rte_mbuf **tx_pkts)
+{
+	rte_iova_t buf_dma_addr;
+	uint32_t desc_offset;
+	buf_dma_addr = rte_mbuf_data_iova(*tx_pkts);
+	desc->read.buf_addr = rte_cpu_to_le_64(buf_dma_addr);
+	desc_offset = SXE2_TX_DATA_DESC_MACLEN_VAL((*tx_pkts)->l2_len);
+	desc->read.type_cmd_off_bsz_l2t =
+				sxe2_tx_data_desc_build_cobt(SXE2_TX_DATA_DESC_CMD_EOP,
+					desc_offset, (*tx_pkts)->data_len, 0);
+}
+static __rte_always_inline void
+sxe2_tx_data_desc_fill_batch(volatile union sxe2_tx_data_desc *desc,
+		struct rte_mbuf **tx_pkts)
+{
+	rte_iova_t buf_dma_addr;
+	uint32_t i;
+	uint32_t desc_offset;
+	for (i = 0; i < SXE2_TX_FILL_PER_LOOP; ++i, ++desc, ++tx_pkts) {
+		buf_dma_addr = rte_mbuf_data_iova(*tx_pkts);
+		desc->read.buf_addr = rte_cpu_to_le_64(buf_dma_addr);
+		desc_offset = SXE2_TX_DATA_DESC_MACLEN_VAL((*tx_pkts)->l2_len);
+		desc->read.type_cmd_off_bsz_l2t =
+			sxe2_tx_data_desc_build_cobt(SXE2_TX_DATA_DESC_CMD_EOP,
+					desc_offset,
+					(*tx_pkts)->data_len,
+					0);
+	}
+}
+
+static inline void sxe2_tx_ring_fill(struct sxe2_tx_queue *txq,
+				struct rte_mbuf **tx_pkts, uint16_t nb_pkts)
+{
+	struct sxe2_tx_buffer *buffer = &txq->buffer_ring[txq->next_use];
+	volatile union sxe2_tx_data_desc *desc = &txq->desc_ring[txq->next_use];
+	uint32_t i, j;
+	uint32_t	mainpart;
+	uint32_t leftover;
+	mainpart = nb_pkts & ((uint32_t)~SXE2_TX_FILL_PER_LOOP_MASK);
+	leftover = nb_pkts & ((uint32_t)SXE2_TX_FILL_PER_LOOP_MASK);
+	for (i = 0; i < mainpart; i += SXE2_TX_FILL_PER_LOOP) {
+		for (j = 0; j < SXE2_TX_FILL_PER_LOOP; ++j)
+			(buffer + i + j)->mbuf = *(tx_pkts + i + j);
+		sxe2_tx_data_desc_fill_batch(desc + i, tx_pkts + i);
+	}
+	if (unlikely(leftover > 0)) {
+		for (i = 0; i < leftover; ++i) {
+			(buffer + mainpart + i)->mbuf = *(tx_pkts + mainpart + i);
+			sxe2_tx_data_desc_fill(desc + mainpart + i,
+					tx_pkts + mainpart + i);
+		}
+	}
+}
+
+static inline uint16_t sxe2_tx_pkts_batch(void *tx_queue,
+			struct rte_mbuf **tx_pkts, uint16_t nb_pkts)
+{
+	struct sxe2_tx_queue *txq = (struct sxe2_tx_queue *)tx_queue;
+	volatile union sxe2_tx_data_desc *desc_ring = txq->desc_ring;
+	uint16_t res_num = 0;
+	if (txq->desc_free_num < txq->free_thresh)
+		(void)sxe2_tx_bufs_free(txq);
+	nb_pkts = RTE_MIN(txq->desc_free_num, nb_pkts);
+	if (unlikely(nb_pkts == 0)) {
+		PMD_LOG_DEBUG(TX, "Tx batch: may not enough free desc, "
+				"free_desc=%u, need_tx_pkts=%u",
+				txq->desc_free_num, nb_pkts);
+		goto l_end;
+	}
+	txq->desc_free_num -= nb_pkts;
+	if ((txq->next_use + nb_pkts) > txq->ring_depth) {
+		res_num = txq->ring_depth - txq->next_use;
+		sxe2_tx_ring_fill(txq, tx_pkts, res_num);
+		desc_ring[txq->next_rs].read.type_cmd_off_bsz_l2t |=
+				rte_cpu_to_le_64(SXE2_TX_DATA_DESC_CMD_RS_MASK);
+		txq->next_rs = txq->rs_thresh - 1;
+		txq->next_use = 0;
+	}
+	sxe2_tx_ring_fill(txq, tx_pkts + res_num, nb_pkts - res_num);
+	txq->next_use = txq->next_use + (nb_pkts - res_num);
+	if (txq->next_use > txq->next_rs) {
+		desc_ring[txq->next_rs].read.type_cmd_off_bsz_l2t |=
+				rte_cpu_to_le_64(SXE2_TX_DATA_DESC_CMD_RS_MASK);
+		txq->next_rs += txq->rs_thresh;
+		if (txq->next_rs >= txq->ring_depth)
+			txq->next_rs = txq->rs_thresh - 1;
+	}
+	if (txq->next_use >= txq->ring_depth)
+		txq->next_use = 0;
+	PMD_LOG_DEBUG(TX, "port_id=%u queue_id=%u next_use=%u send_pkts=%u",
+			txq->port_id, txq->queue_id, txq->next_use, nb_pkts);
+	SXE2_PCI_REG_WRITE_WC(txq->tdt_reg_addr, txq->next_use);
+l_end:
+	return nb_pkts;
+}
+
+static inline void
+sxe2_update_rx_tail(struct sxe2_rx_queue *rxq, uint16_t hold_num, uint16_t rx_id)
+{
+	hold_num += rxq->hold_num;
+
+	if (hold_num > rxq->rx_free_thresh) {
+		rx_id = (uint16_t)((rx_id == 0) ? (rxq->ring_depth - 1) : (rx_id - 1));
+		SXE2_PCI_REG_WRITE_WC(rxq->rdt_reg_addr, rx_id);
+		hold_num = 0;
+	}
+	rxq->hold_num = hold_num;
+}
+
+static inline uint64_t
+sxe2_rx_desc_error_para(__rte_unused struct sxe2_rx_queue *rxq,
+		union sxe2_rx_desc *desc)
+{
+	uint64_t flags = 0;
+	uint64_t desc_qw1 = rte_le_to_cpu_64(desc->wb.status_err_ptype_len);
+
+	if (unlikely(0 == (desc_qw1 & SXE2_RX_DESC_STATUS_L3L4_P_MASK)))
+		goto l_end;
+
+	if (likely(0 == (desc->wb.rxdid_src & SXE2_RX_DESC_EUDPE_MASK)))
+		flags = RTE_MBUF_F_RX_OUTER_L4_CKSUM_GOOD;
+	else
+		flags = RTE_MBUF_F_RX_OUTER_L4_CKSUM_BAD;
+
+	if (likely(0 == (desc_qw1 & SXE2_RX_DESC_QW1_ERRORS_MASK))) {
+		flags |= (RTE_MBUF_F_RX_IP_CKSUM_GOOD |
+				RTE_MBUF_F_RX_L4_CKSUM_GOOD |
+				RTE_MBUF_F_RX_OUTER_L4_CKSUM_GOOD);
+		goto l_end;
+	}
+
+	if (likely(0 == (desc_qw1 & SXE2_RX_DESC_ERROR_CSUM_IPE_MASK)))
+		flags |= RTE_MBUF_F_RX_IP_CKSUM_GOOD;
+	else
+		flags |= RTE_MBUF_F_RX_IP_CKSUM_BAD;
+
+	if (likely(0 == (desc_qw1 & SXE2_RX_DESC_ERROR_CSUM_L4_MASK)))
+		flags |= RTE_MBUF_F_RX_L4_CKSUM_GOOD;
+	else
+		flags |= RTE_MBUF_F_RX_L4_CKSUM_BAD;
+
+	if (unlikely(0 != (desc_qw1 & SXE2_RX_DESC_ERROR_CSUM_EIP_MASK)))
+		flags |= RTE_MBUF_F_RX_OUTER_IP_CKSUM_BAD;
+
+l_end:
+	return flags;
+}
+
+static __rte_always_inline void
+sxe2_rx_mbuf_common_fields_fill(struct sxe2_rx_queue *rxq, struct rte_mbuf *mbuf,
+		union sxe2_rx_desc *rxd)
+{
+	uint32_t *ptype_tbl = rxq->vsi->adapter->ptype_tbl;
+	uint64_t qword1;
+	uint64_t pkt_flags;
+	qword1 = rte_le_to_cpu_64(rxd->wb.status_err_ptype_len);
+
+	mbuf->ol_flags = 0;
+	mbuf->packet_type = ptype_tbl[SXE2_RX_DESC_PTYPE_VAL_GET(qword1)];
+
+	pkt_flags = sxe2_rx_desc_error_para(rxq, rxd);
+
+	mbuf->ol_flags |= pkt_flags;
+}
+
+static __rte_always_inline void
+sxe2_rx_sw_stats_update(struct sxe2_rx_queue *rxq, struct rte_mbuf *mbuf,
+		union sxe2_rx_desc *rxd)
+{
+	uint64_t qword1 = rte_le_to_cpu_64(rxd->wb.status_err_ptype_len);
+	rte_atomic_fetch_add_explicit(&rxq->sw_stats.pkts, 1,
+		rte_memory_order_relaxed);
+	rte_atomic_fetch_add_explicit(&rxq->sw_stats.bytes,
+			mbuf->pkt_len + RTE_ETHER_CRC_LEN,
+			rte_memory_order_relaxed);
+	switch (SXE2_RX_DESC_STATUS_UMBCAST_VAL_GET(qword1)) {
+	case SXE2_RX_DESC_STATUS_UNICAST:
+		rte_atomic_fetch_add_explicit(&rxq->sw_stats.unicast_pkts, 1,
+			rte_memory_order_relaxed);
+		break;
+	case SXE2_RX_DESC_STATUS_MUTICAST:
+		rte_atomic_fetch_add_explicit(&rxq->sw_stats.multicast_pkts, 1,
+			rte_memory_order_relaxed);
+		break;
+	case SXE2_RX_DESC_STATUS_BOARDCAST:
+		rte_atomic_fetch_add_explicit(&rxq->sw_stats.broadcast_pkts, 1,
+			rte_memory_order_relaxed);
+		break;
+	default:
+		break;
+	}
+}
+
+uint16_t sxe2_rx_pkts_scattered(void *rx_queue, struct rte_mbuf **rx_pkts, uint16_t nb_pkts)
+{
+	struct sxe2_rx_queue *rxq = (struct sxe2_rx_queue *)rx_queue;
+	volatile union sxe2_rx_desc *desc_ring;
+	volatile union sxe2_rx_desc *desc;
+	union sxe2_rx_desc desc_tmp;
+	struct rte_mbuf **buffer_ring;
+	struct rte_mbuf **cur_buffer;
+	struct rte_mbuf *cur_mbuf;
+	struct rte_mbuf *new_mbuf;
+	struct rte_mbuf *first_seg;
+	struct rte_mbuf *last_seg;
+	uint64_t qword1;
+	uint16_t done_num;
+	uint16_t hold_num;
+	uint16_t cur_idx;
+	uint16_t pkt_len;
+
+	desc_ring   = rxq->desc_ring;
+	buffer_ring = rxq->buffer_ring;
+	cur_idx     = rxq->processing_idx;
+	first_seg   = rxq->pkt_first_seg;
+	last_seg    = rxq->pkt_last_seg;
+	done_num    = 0;
+	hold_num    = 0;
+	while (done_num < nb_pkts) {
+		desc = &desc_ring[cur_idx];
+		qword1 = rte_le_to_cpu_64(desc->wb.status_err_ptype_len);
+		if (0 == (SXE2_RX_DESC_STATUS_DD_MASK & qword1))
+			break;
+
+		new_mbuf = rte_mbuf_raw_alloc(rxq->mb_pool);
+		if (unlikely(new_mbuf == NULL)) {
+			rxq->vsi->adapter->dev_info.dev_data->rx_mbuf_alloc_failed++;
+			PMD_LOG_INFO(RX, "Rx new_mbuf alloc failed port_id:%u "
+					"queue_id:%u", rxq->port_id, rxq->queue_id);
+			break;
+		}
+
+		hold_num++;
+		desc_tmp = *desc;
+		cur_buffer = &buffer_ring[cur_idx];
+		cur_idx++;
+		if (unlikely(cur_idx == rxq->ring_depth))
+			cur_idx = 0;
+
+		rte_prefetch0(buffer_ring[cur_idx]);
+
+		if (0 == (cur_idx & 0x3)) {
+			rte_prefetch0(&desc_ring[cur_idx]);
+			rte_prefetch0(&buffer_ring[cur_idx]);
+		}
+
+		cur_mbuf = *cur_buffer;
+
+		*cur_buffer = new_mbuf;
+
+		desc->read.hdr_addr = 0;
+		desc->read.pkt_addr =
+			rte_cpu_to_le_64(rte_mbuf_data_iova_default(new_mbuf));
+
+		pkt_len = SXE2_RX_DESC_PKT_LEN_VAL_GET(qword1);
+		cur_mbuf->data_len = pkt_len;
+		cur_mbuf->data_off = RTE_PKTMBUF_HEADROOM;
+
+		if (first_seg == NULL) {
+			first_seg = cur_mbuf;
+			first_seg->nb_segs = 1;
+			first_seg->pkt_len = pkt_len;
+		} else {
+			first_seg->pkt_len += pkt_len;
+			first_seg->nb_segs++;
+			last_seg->next = cur_mbuf;
+		}
+
+		if (0 == (qword1 & SXE2_RX_DESC_STATUS_EOP_MASK)) {
+			last_seg = cur_mbuf;
+			continue;
+		}
+
+		if (unlikely(qword1 & SXE2_RX_DESC_ERROR_RXE_MASK) ||
+			unlikely(qword1 & SXE2_RX_DESC_ERROR_OVERSIZE_MASK)) {
+			rte_atomic_fetch_add_explicit(&rxq->sw_stats.drop_pkts, 1,
+				rte_memory_order_relaxed);
+			rte_atomic_fetch_add_explicit(&rxq->sw_stats.drop_bytes,
+				first_seg->pkt_len - rxq->crc_len + RTE_ETHER_CRC_LEN,
+				rte_memory_order_relaxed);
+			rte_pktmbuf_free(first_seg);
+			first_seg = NULL;
+			continue;
+		}
+
+		cur_mbuf->next = NULL;
+		if (unlikely(rxq->crc_len > 0)) {
+			first_seg->pkt_len -= RTE_ETHER_CRC_LEN;
+
+			if (pkt_len <= RTE_ETHER_CRC_LEN) {
+				rte_pktmbuf_free_seg(cur_mbuf);
+				first_seg->nb_segs--;
+				last_seg->data_len = last_seg->data_len + pkt_len -
+					RTE_ETHER_CRC_LEN;
+				last_seg->next = NULL;
+			} else {
+				cur_mbuf->data_len = pkt_len - RTE_ETHER_CRC_LEN;
+			}
+
+		} else if (pkt_len == 0) {
+			rte_pktmbuf_free_seg(cur_mbuf);
+			first_seg->nb_segs--;
+			last_seg->next = NULL;
+		}
+
+		rte_prefetch0(RTE_PTR_ADD(first_seg->buf_addr, first_seg->data_off));
+		first_seg->port     = rxq->port_id;
+
+		sxe2_rx_mbuf_common_fields_fill(rxq, first_seg, &desc_tmp);
+
+		if (rxq->vsi->adapter->devargs.sw_stats_en)
+			sxe2_rx_sw_stats_update(rxq, first_seg, &desc_tmp);
+
+		rte_prefetch0(RTE_PTR_ADD(first_seg->buf_addr, first_seg->data_off));
+
+		rx_pkts[done_num] = first_seg;
+		done_num++;
+
+		first_seg = NULL;
+	}
+
+	rxq->processing_idx = cur_idx;
+	rxq->pkt_first_seg  = first_seg;
+	rxq->pkt_last_seg   = last_seg;
+
+	sxe2_update_rx_tail(rxq, hold_num, cur_idx);
+
+	return done_num;
+}
+
+uint16_t sxe2_rx_pkts_scattered_split(void *rx_queue, struct rte_mbuf **rx_pkts, uint16_t nb_pkts)
+{
+	struct sxe2_rx_queue *rxq = (struct sxe2_rx_queue *)rx_queue;
+	volatile union sxe2_rx_desc *desc_ring;
+	volatile union sxe2_rx_desc *desc;
+	union sxe2_rx_desc desc_tmp;
+	struct rte_mbuf **buffer_ring;
+	struct rte_mbuf **cur_buffer;
+	struct rte_mbuf *cur_mbuf;
+	struct rte_mbuf *cur_mbuf_pay;
+	struct rte_mbuf *new_mbuf;
+	struct rte_mbuf *new_mbuf_pay = NULL;
+	struct rte_mbuf *first_seg;
+	struct rte_mbuf *last_seg;
+	uint64_t qword1;
+	uint16_t done_num;
+	uint16_t hold_num;
+	uint16_t cur_idx;
+	uint16_t pkt_len;
+	uint16_t hdr_len;
+
+	desc_ring = rxq->desc_ring;
+	buffer_ring = rxq->buffer_ring;
+	cur_idx = rxq->processing_idx;
+	first_seg = rxq->pkt_first_seg;
+	last_seg = rxq->pkt_last_seg;
+	done_num = 0;
+	hold_num = 0;
+	new_mbuf = NULL;
+
+	while (done_num < nb_pkts) {
+		desc = &desc_ring[cur_idx];
+		qword1 = rte_le_to_cpu_64(desc->wb.status_err_ptype_len);
+
+		if (0 == (SXE2_RX_DESC_STATUS_DD_MASK & qword1))
+			break;
+
+		if ((rxq->offloads & RTE_ETH_RX_OFFLOAD_BUFFER_SPLIT) == 0 ||
+			first_seg == NULL) {
+			new_mbuf = rte_mbuf_raw_alloc(rxq->mb_pool);
+			if (unlikely(new_mbuf == NULL)) {
+				rxq->vsi->adapter->dev_info.dev_data->rx_mbuf_alloc_failed++;
+				break;
+			}
+		}
+
+		if (rxq->offloads & RTE_ETH_RX_OFFLOAD_BUFFER_SPLIT) {
+			new_mbuf_pay = rte_mbuf_raw_alloc(rxq->rx_seg[1].mp);
+			if (unlikely(new_mbuf_pay == NULL)) {
+				rxq->vsi->adapter->dev_info.dev_data->rx_mbuf_alloc_failed++;
+				if (new_mbuf != NULL)
+					rte_pktmbuf_free(new_mbuf);
+				new_mbuf = NULL;
+				break;
+			}
+		}
+
+		hold_num++;
+		desc_tmp = *desc;
+		cur_buffer = &buffer_ring[cur_idx];
+		cur_idx++;
+		if (unlikely(cur_idx == rxq->ring_depth))
+			cur_idx = 0;
+		rte_prefetch0(buffer_ring[cur_idx]);
+		if (0 == (cur_idx & 0x3)) {
+			rte_prefetch0(&desc_ring[cur_idx]);
+			rte_prefetch0(&buffer_ring[cur_idx]);
+		}
+		cur_mbuf = *cur_buffer;
+		if (0 == (rxq->offloads & RTE_ETH_RX_OFFLOAD_BUFFER_SPLIT)) {
+			*cur_buffer = new_mbuf;
+			desc->read.hdr_addr = 0;
+			desc->read.pkt_addr =
+				rte_cpu_to_le_64(rte_mbuf_data_iova_default(new_mbuf));
+		} else {
+			if (first_seg == NULL) {
+				*cur_buffer = new_mbuf;
+				new_mbuf->next = new_mbuf_pay;
+				new_mbuf->data_off = RTE_PKTMBUF_HEADROOM;
+				new_mbuf_pay->next = NULL;
+				new_mbuf_pay->data_off = RTE_PKTMBUF_HEADROOM;
+				desc->read.hdr_addr =
+					rte_cpu_to_le_64(rte_mbuf_data_iova_default(new_mbuf));
+				desc->read.pkt_addr =
+					rte_cpu_to_le_64(rte_mbuf_data_iova_default(new_mbuf_pay));
+			} else {
+				cur_mbuf_pay = cur_mbuf->next;
+				new_mbuf_pay->next = NULL;
+				new_mbuf_pay->data_off = RTE_PKTMBUF_HEADROOM;
+				cur_mbuf->next = new_mbuf_pay;
+				desc->read.hdr_addr =
+					rte_cpu_to_le_64(rte_mbuf_data_iova_default(cur_mbuf));
+				desc->read.pkt_addr =
+					rte_cpu_to_le_64(rte_mbuf_data_iova_default(new_mbuf_pay));
+				cur_mbuf = cur_mbuf_pay;
+			}
+		}
+		new_mbuf = NULL;
+		if (0 == (rxq->offloads & RTE_ETH_RX_OFFLOAD_BUFFER_SPLIT)) {
+			pkt_len = SXE2_RX_DESC_PKT_LEN_VAL_GET(qword1);
+			cur_mbuf->data_len = pkt_len;
+			cur_mbuf->data_off = RTE_PKTMBUF_HEADROOM;
+			if (first_seg == NULL) {
+				first_seg = cur_mbuf;
+				first_seg->nb_segs = 1;
+				first_seg->pkt_len = pkt_len;
+			} else {
+				first_seg->pkt_len += pkt_len;
+				first_seg->nb_segs++;
+				last_seg->next = cur_mbuf;
+			}
+		} else {
+			if (first_seg == NULL) {
+				cur_mbuf->nb_segs = 2;
+				cur_mbuf->next->next = NULL;
+				pkt_len = SXE2_RX_DESC_PKT_LEN_VAL_GET(qword1);
+				hdr_len = SXE2_RX_DESC_HDR_LEN_VAL_GET(qword1);
+				cur_mbuf->data_len = hdr_len;
+				cur_mbuf->pkt_len = hdr_len + pkt_len;
+				cur_mbuf->next->data_len = pkt_len;
+				first_seg = cur_mbuf;
+				cur_mbuf = cur_mbuf->next;
+				last_seg = cur_mbuf;
+			} else {
+				cur_mbuf->nb_segs = 1;
+				cur_mbuf->next = NULL;
+				pkt_len = SXE2_RX_DESC_PKT_LEN_VAL_GET(qword1);
+				cur_mbuf->data_len = pkt_len;
+
+				first_seg->pkt_len += pkt_len;
+				first_seg->nb_segs++;
+				last_seg->next = cur_mbuf;
+			}
+		}
+
+#ifdef RTE_ETHDEV_DEBUG_RX
+
+		rte_pktmbuf_dump(stdout, first_seg, rte_pktmbuf_pkt_len(first_seg));
+#endif
+
+		if (0 == (rte_le_to_cpu_64(desc_tmp.wb.status_err_ptype_len) &
+					SXE2_RX_DESC_STATUS_EOP_MASK)) {
+			last_seg = cur_mbuf;
+			continue;
+		}
+
+		if (unlikely(qword1 & SXE2_RX_DESC_ERROR_RXE_MASK) ||
+			unlikely(qword1 & SXE2_RX_DESC_ERROR_OVERSIZE_MASK)) {
+			rte_atomic_fetch_add_explicit(&rxq->sw_stats.drop_pkts, 1,
+				rte_memory_order_relaxed);
+			rte_atomic_fetch_add_explicit(&rxq->sw_stats.drop_bytes,
+				first_seg->pkt_len - rxq->crc_len + RTE_ETHER_CRC_LEN,
+				rte_memory_order_relaxed);
+			rte_pktmbuf_free(first_seg);
+			first_seg = NULL;
+			continue;
+		}
+
+		cur_mbuf->next = NULL;
+		if (unlikely(rxq->crc_len > 0)) {
+			first_seg->pkt_len -= RTE_ETHER_CRC_LEN;
+			if (pkt_len <= RTE_ETHER_CRC_LEN) {
+				rte_pktmbuf_free_seg(cur_mbuf);
+				cur_mbuf = NULL;
+				first_seg->nb_segs--;
+				last_seg->data_len = last_seg->data_len +
+					pkt_len - RTE_ETHER_CRC_LEN;
+				last_seg->next = NULL;
+			} else {
+				cur_mbuf->data_len = pkt_len - RTE_ETHER_CRC_LEN;
+			}
+		} else if (pkt_len == 0) {
+			rte_pktmbuf_free_seg(cur_mbuf);
+			cur_mbuf = NULL;
+			first_seg->nb_segs--;
+			last_seg->next = NULL;
+		}
+
+		first_seg->port = rxq->port_id;
+		sxe2_rx_mbuf_common_fields_fill(rxq, first_seg, &desc_tmp);
+
+		if (rxq->vsi->adapter->devargs.sw_stats_en)
+			sxe2_rx_sw_stats_update(rxq, first_seg, &desc_tmp);
+
+		rte_prefetch0(RTE_PTR_ADD(first_seg->buf_addr, first_seg->data_off));
+
+		rx_pkts[done_num] = first_seg;
+		done_num++;
+
+		first_seg = NULL;
+	}
+
+	rxq->processing_idx = cur_idx;
+	rxq->pkt_first_seg = first_seg;
+	rxq->pkt_last_seg = last_seg;
+
+	sxe2_update_rx_tail(rxq, hold_num, cur_idx);
+
+	return done_num;
+}
-- 
2.47.3


^ permalink raw reply related

* [PATCH v17 02/11] doc: add sxe2 guide and release notes
From: liujie5 @ 2026-05-19  3:01 UTC (permalink / raw)
  To: stephen; +Cc: dev, Jie Liu
In-Reply-To: <20260519030132.3780057-1-liujie5@linkdatatechnology.com>

From: Jie Liu <liujie5@linkdatatechnology.com>

Add a new guide for SXE2 PMD in the nics directory.
The guide contains driver capabilities, prerequisites,
and compilation/usage instructions.

Update the release notes to announce the addition of the
sxe2 network driver.

Signed-off-by: Jie Liu <liujie5@linkdatatechnology.com>
---
 doc/guides/nics/features/sxe2.ini      | 29 ++++++++++++++++++++++
 doc/guides/nics/index.rst              |  1 +
 doc/guides/nics/sxe2.rst               | 34 ++++++++++++++++++++++++++
 doc/guides/rel_notes/release_26_07.rst |  4 +++
 4 files changed, 68 insertions(+)
 create mode 100644 doc/guides/nics/features/sxe2.ini
 create mode 100644 doc/guides/nics/sxe2.rst

diff --git a/doc/guides/nics/features/sxe2.ini b/doc/guides/nics/features/sxe2.ini
new file mode 100644
index 0000000000..7e350bab54
--- /dev/null
+++ b/doc/guides/nics/features/sxe2.ini
@@ -0,0 +1,29 @@
+;
+; Supported features of the 'sxe2' network poll mode driver.
+;
+; Refer to default.ini for the full list of available PMD features.
+;
+; A feature with "P" indicates only be supported when non-vector path
+; is selected.
+;
+[Features]
+Fast mbuf free       = P
+Free Tx mbuf on demand = Y
+Burst mode info      = Y
+Queue start/stop     = Y
+Buffer split on Rx   = P
+Scattered Rx         = Y
+CRC offload          = Y
+VLAN offload         = Y
+QinQ offload         = P
+L3 checksum offload  = Y
+L4 checksum offload  = Y
+Timestamp offload    = P
+Inner L3 checksum    = P
+Inner L4 checksum    = P
+Rx descriptor status = Y
+Tx descriptor status = Y
+FreeBSD              = Y
+Linux                = Y
+x86-32               = Y
+x86-64               = Y
diff --git a/doc/guides/nics/index.rst b/doc/guides/nics/index.rst
index cb818284fe..e20be478f8 100644
--- a/doc/guides/nics/index.rst
+++ b/doc/guides/nics/index.rst
@@ -68,6 +68,7 @@ Network Interface Controller Drivers
     rnp
     sfc_efx
     softnic
+    sxe2
     tap
     thunderx
     txgbe
diff --git a/doc/guides/nics/sxe2.rst b/doc/guides/nics/sxe2.rst
new file mode 100644
index 0000000000..7fcf9c085b
--- /dev/null
+++ b/doc/guides/nics/sxe2.rst
@@ -0,0 +1,34 @@
+..  SPDX-License-Identifier: BSD-3-Clause
+    Copyright (C), 2025, Wuxi Stars Micro System Technologies Co., Ltd.
+
+SXE2 Poll Mode Driver
+======================
+
+The sxe2 PMD (**librte_net_sxe2**) provides poll mode driver support for
+10/25/50/100 Gbps Network Adapters.
+The embedded switch, Physical Functions (PF),
+and SR-IOV Virtual Functions (VF) are supported.
+
+Implementation details
+----------------------
+
+The sxe2 PMD is designed to operate alongside the sxe2 kernel network driver.
+For management and control operations, the PMD communicates with the kernel
+driver via ioctl interfaces. These commands are processed by the kernel
+driver and subsequently dispatched to the hardware firmware for execution.
+
+For security and robustness, the driver's data path is optimized to operate
+using virtual addresses (IOVA as VA mode). However, to ensure full
+compatibility in system environments where an IOMMU is absent or disabled,
+the driver also provides an explicit path to support physical addressing
+(IOVA as PA mode).
+
+The hardware is capable of handling the corresponding IOVA addresses (either
+VA or PA) directly, as provided by the DPDK memory subsystem. This ensures
+that DPDK applications can only access memory segments explicitly allocated
+to the current process, preventing unauthorized access to random physical
+memory.
+
+This capability allows the PMD to coexist with kernel network interfaces
+which remain functional, although they stop receiving unicast packets as
+long as they share the same MAC address.
diff --git a/doc/guides/rel_notes/release_26_07.rst b/doc/guides/rel_notes/release_26_07.rst
index f012d47a4b..fa0f0f5cca 100644
--- a/doc/guides/rel_notes/release_26_07.rst
+++ b/doc/guides/rel_notes/release_26_07.rst
@@ -64,6 +64,10 @@ New Features
   * ``--auto-probing`` enables the initial bus probing, which is the current default behavior.
 
 
+* **Added Linkdata sxe2 ethernet driver.**
+
+  Added network driver for the Linkdata Network Adapters.
+
 Removed Items
 -------------
 
-- 
2.47.3


^ permalink raw reply related


This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox