DPDK-dev Archive on lore.kernel.org
 help / color / mirror / Atom feed
* Re: [PATCH v3 6/6] test/bpf: check that bpf_convert can be JIT'd
From: Stephen Hemminger @ 2026-06-23 15:51 UTC (permalink / raw)
  To: Marat Khalili; +Cc: dev@dpdk.org, Konstantin Ananyev
In-Reply-To: <c510bf24a3cd492693fc54f0b558656f@huawei.com>

On Tue, 23 Jun 2026 13:57:35 +0000
Marat Khalili <marat.khalili@huawei.com> wrote:

> Thank you for working on this, please see some comments inline.

I think it is still worth keeping the tests split into two.
One test just make sure that some basic filters work as expected (match vs not match).

The other one is just doing its job by causing pcap_compile() to generate more complex
and different code. Since the packet we are feeding it is just a dummy packet, I suspect
all of the filters will be false. Let me recheck, if so then can look at return value.

^ permalink raw reply

* Re: [PATCH v3 0/9] ENETC driver related changes series
From: Stephen Hemminger @ 2026-06-23 15:46 UTC (permalink / raw)
  To: Gagandeep Singh; +Cc: dev, hemant.agrawal
In-Reply-To: <20260623060004.2187716-1-g.singh@nxp.com>

On Tue, 23 Jun 2026 11:29:55 +0530
Gagandeep Singh <g.singh@nxp.com> wrote:

> V3 changes:
>   - Added documentation for all devargs in enetc4.rst.
>   - Fixed kvlist memory leak issue.
> 
> V2 changes:
>   - Fixed an un-used variable compilation issue reported on fedora:43-gcc-minsize
>   - Fixed various AI reported issues:
> 	- Release notes updated for all new devargs
> 	- enect4.ini features doc updated for scattered RX.
> 	- removed Not required RTE_PTYPE_UNKNOWN.
> 	- Fixed mid-frame mbuf leak in SG case.
> 	- Enabled SG for enetc4 PF also.
> 	- move to calloc from rte_zmalloc in parse_txq_prior().
> 	- added vaidation checks on strdup, strtoul.
> 	- added NC devargs to use cacheable ops conditionally.
> 	- removed dead code like bd_base_p etc.
> 	- Fixed rte_cpu_to_le_16() conversion on flags and combined
> 	  all flags related patches in one patch.
> 	- Fixed memory leak issue due to TXQ priority patch.
>    - There were some false positives, I have ignored them:
> 	Race condition on flags field:
> 		clean_tx_ring only touches HW-completed BDs (next_to_clean→hwci),
> 		never newly-submitted BDs; doorbell hasn't fired yet.
> 	Missing dcbf in clean_tx_ring:
> 		DPDK is single-threaded per queue; TX path always overwrites
> 		flags completely before dcbf.
> 	TX dcbf granularity with wrap:
> 		Safe (AI admits it).
> 	RX refill flush at wrap:
> 		In-loop dcbf at i & mask == 0 already flushes aligned groups;
> 		trailing flush only needed for partial groups.
> 	RX reading before invalidate:
> 		dccivac precedes the read for every group in the loop
> 
> Gagandeep Singh (7):
>   net/enetc: fix TX BD structure
>   net/enetc: fix queue initialization
>   net/enetc: support ESP packet type in packet parsing
>   net/enetc: update random MAC generation code
>   net/enetc: add option to disable VSI messaging
>   net/enetc: add devargs to control VSI-PSI timeout and delay
>   net/enetc4: add cacheable BD ring support with SW cache maintenance
> 
> Vanshika Shukla (2):
>   net/enetc: support scatter-gather
>   net/enetc: set user configurable priority to TX rings
> 
>  doc/guides/nics/enetc4.rst             |  62 +++-
>  doc/guides/nics/features/enetc4.ini    |   1 +
>  doc/guides/rel_notes/release_26_07.rst |  10 +
>  drivers/net/enetc/base/enetc_hw.h      |  13 +-
>  drivers/net/enetc/enetc.h              |  31 +-
>  drivers/net/enetc/enetc4_ethdev.c      | 172 ++++++++--
>  drivers/net/enetc/enetc4_vf.c          | 206 ++++++++++--
>  drivers/net/enetc/enetc_ethdev.c       |  25 +-
>  drivers/net/enetc/enetc_rxtx.c         | 430 ++++++++++++++++++++++---
>  9 files changed, 831 insertions(+), 119 deletions(-)
> 

Did followup AI review and it had some more things that need fixing:

Error
=====

[PATCH v2 7/9] net/enetc: add devargs to control VSI-PSI timeout and delay

drivers/net/enetc/enetc4_vf.c, enetc4_vf_dev_init()

  kvlist is leaked on the two invalid-value error paths. It is
  allocated by rte_kvargs_parse() (line 1347) and only freed at
  line 1385, but both

      return -1;   /* invalid VSI Timeout, line 1367 */
      return -1;   /* invalid VSI Delay,   line 1380 */

  return before that free. A malformed enetc4_vsi_timeout= or
  enetc4_vsi_delay= leaks the kvargs structure on every probe.

  Free before returning, e.g.:

      if (errno != 0 || hw->vsi_timeout == 0) {
              ENETC_PMD_ERR("Invalid VSI Timeout value = %u",
                              hw->vsi_timeout);
              rte_kvargs_free(kvlist);
              return -1;
      }

  (same for the delay path), or restructure with a goto.


Warning
=======

Series (patches 6-9)

  The new runtime devargs - enetc4_vsi_disable, enetc4_vsi_timeout,
  enetc4_vsi_delay, enetc4_txq_prior, and nc - are registered via
  RTE_PMD_REGISTER_PARAM_STRING and noted in the release notes, but
  doc/guides/nics/enetc4.rst has no Runtime Configuration section
  describing them. Convention is to document devargs in the NIC guide
  so users can find the syntax (e.g. the nc=1 / 'a|b|c' priority list
  formats are non-obvious).

Info
====

[PATCH v2 5/9] and [PATCH v2 9/9] - RX multi-segment reassembly

  In enetc_clean_rx_ring_nc() and enetc_clean_rx_ring_cacheable(),
  on the frame-last BD:

      first_seg->pkt_len -= rx_ring->crc_len;

  reduces pkt_len but leaves the final segment's data_len unchanged,
  so pkt_len != sum(data_len) when crc_len is non-zero. The old
  single-segment path kept them equal (pkt_len = data_len = buf_len
  - crc_len).

  This is currently unreachable: enetc4 does not advertise
  RTE_ETH_RX_OFFLOAD_KEEP_CRC, so crc_len is always 0 and the
  subtraction is a no-op. Flagging only so the asymmetry is on record
  if KEEP_CRC is ever added - at that point the last segment's
  data_len would need the same adjustment (and the CRC may straddle
  the last two segments).

^ permalink raw reply

* Re: [PATCH 0/5] add versioned symbols for recently stabilized APIs
From: Dariusz Sosnowski @ 2026-06-23 15:43 UTC (permalink / raw)
  To: David Marchand
  Cc: Thomas Monjalon, dpdk-techboard, Bruce Richardson,
	Andrew Rybchenko, Viacheslav Ovsiienko, Bing Zhao, Ori Kam,
	Suanming Mou, Matan Azrad, dev
In-Reply-To: <CAJFAV8yS6dLM2EUDgdG13U1Zdt208nmk2T3F8DYwT97U1coF3Q@mail.gmail.com>

On Tue, Jun 23, 2026 at 03:50:52PM +0200, David Marchand wrote:
> Hello Dariusz,
> 
> On Tue, 23 Jun 2026 at 13:38, Dariusz Sosnowski <dsosnowski@nvidia.com> wrote:
> >
> > Main goal of this patchset is to address https://bugs.dpdk.org/show_bug.cgi?id=1957
> 
> It is expected that experimental symbols may disappear overnight, and
> this bug could also be closed as NOTABUG.
> 
> On the other hand, we do state in the doc that compatibility could be
> provided when stabilising an experimental API, so ok.. let's try.
> 
> > but it also handles other recently stabilized symbols and has some minor fixes:
> >
> > - Patch 1 - Fix RTE_VERSION_EXPERIMENTAL_SYMBOL macro on clang.
> 
> Ouch... /me hides.
> 
> 
> > - Patch 2 - Allow function versioning inside drivers.
> > - Patch 3 - Version the function symbols stabilized in
> >   https://git.dpdk.org/dpdk/commit/?id=e8cab133645f5466ef75e511629add43b68a5027
> > - Patch 4 - Introduce versioning macros for global variable symbols.
> > - Patch 5 - Version the function and variable symbols stabilized in
> >   https://git.dpdk.org/dpdk/commit/?id=4ee2f5c1cedf9ee7f39afa667f71b07f4004ba5c
> >
> > Issue is still not fully fixed for stabilized global variables:
> > rte_flow_dynf_metadata_offs and rte_flow_dynf_metadata_mask.
> 
> Well, symbol versioning is not something for variables.
> Exposing global variables was a mistake from the start...

After fighting with this issue for some time,
I am coming to the similar conclusion :)

> Those were exported for "performance" reasons as those are accessed
> via inline helpers (but I am not sure there were benchmarks showing
> the benefits).
> 
> I am for forbidding exports of global variables from now, unless some
> really good performance benchmark is provided (@techboard for info).

Sounds like a good proposal IMO.
Especially since, from a quick glance, almost all existing variables
expose values not expected to change frequently at runtime.
For example, like here, mbuf dynamic field offset.
These could be retrieved once and stored somewhere locally
(Rx/Tx queue context for example).

> 
> 
> Now, in practice for your issue, rather than reintroducing symbol
> aliases (technical solution that I dropped when refactoring the
> macros), I think we can do with some middle ground approach:
> - leaving the inline helpers as "stable" (not __rte_experimental),
> - restoring the EXPERIMENTAL version on the global variables, this
> will restore the location of those symbols from the previous ABI pov,
> and the checks won't catch this discrepancy anyway,
> - during 26.11, drop the EXPERIMENTAL version on those variables,
> 
> 
> In other words, stopping at your patch 3 of the series, then adding:
> 
> $ git diff
> diff --git a/lib/ethdev/rte_flow.c b/lib/ethdev/rte_flow.c
> index ec0fe08355..8bd21ccd31 100644
> --- a/lib/ethdev/rte_flow.c
> +++ b/lib/ethdev/rte_flow.c
> @@ -23,11 +23,11 @@
>  #define FLOW_LOG RTE_ETHDEV_LOG_LINE
> 
>  /* Mbuf dynamic field name for metadata. */
> -RTE_EXPORT_SYMBOL(rte_flow_dynf_metadata_offs)
> +RTE_EXPORT_EXPERIMENTAL_SYMBOL(rte_flow_dynf_metadata_offs, 19.11)
>  int32_t rte_flow_dynf_metadata_offs = -1;
> 
>  /* Mbuf dynamic field flag bit number for metadata. */
> -RTE_EXPORT_SYMBOL(rte_flow_dynf_metadata_mask)
> +RTE_EXPORT_EXPERIMENTAL_SYMBOL(rte_flow_dynf_metadata_mask, 19.11)
>  uint64_t rte_flow_dynf_metadata_mask;
> 
>  /**

Thank you for the suggestion.
That looks good to me.
I'll prepare a v2.

> 
> > Patch 4 and 5 address the bug for these global variables,
> > by providing a single storage for both EXPERIMENTAL and
> > DPDK_26 variable symbol versions.
> > This is achieved through symbol aliasing.
> > But this solution is limited only to executables compiled with clang.
> >
> > clang and gcc have a different default behavior regarding relocations
> > of global variables exposed by shared libraries.
> >
> 
> Yeah... not even thinking about adding MSVC in the list...
> 
> 
> -- 
> David Marchand
> 

^ permalink raw reply

* [PATCH] examples: use strlcpy and strlcat
From: Bruce Richardson @ 2026-06-23 15:41 UTC (permalink / raw)
  To: dev
  Cc: Bruce Richardson, stable, Cristian Dumitrescu, Radu Nicolau,
	Akhil Goyal, Fan Zhang, Anatoly Burakov, Sivaprasad Tummala,
	Jasvinder Singh, Sergio Gonzalez Monroy, Ferruh Yigit,
	Pablo de Lara, Declan Doherty, Alan Carew

Replace strncpy and other unbounded string functions, e.g. strcpy,
strcat, with the safer alternatives strlcpy and strlcat, so that we can
guarantee null termination of strings.

Fixes: 4bbf8e30aa5e ("examples/ip_pipeline: add CLI interface")
Fixes: 5f657a7fbe86 ("examples/pipeline: add message passing mechanism")
Fixes: 83f58a7b7b0a ("examples/pipeline: add commands for direct registers")
Fixes: 0d547ed03717 ("examples/ipsec-secgw: support configuration file")
Fixes: 63e8c07c7245 ("examples/ipsec-secgw: fix configuration parsing")
Fixes: 41e97c2ea9e6 ("examples/l2fwd-crypto: extend crypto information")
Fixes: e8ae9b662506 ("examples/vm_power: channel manager and monitor in host")
Cc: stable@dpdk.org

Signed-off-by: Bruce Richardson <bruce.richardson@intel.com>
---
 examples/ip_pipeline/conn.c                 |  4 ++--
 examples/ipsec-secgw/sa.c                   |  4 ++--
 examples/l2fwd-crypto/main.c                | 12 ++++++------
 examples/pipeline/cli.c                     |  4 ++--
 examples/pipeline/conn.c                    |  4 ++--
 examples/vm_power_manager/channel_manager.c |  3 +--
 6 files changed, 15 insertions(+), 16 deletions(-)

diff --git a/examples/ip_pipeline/conn.c b/examples/ip_pipeline/conn.c
index 30fca80c14..9f347fd1c2 100644
--- a/examples/ip_pipeline/conn.c
+++ b/examples/ip_pipeline/conn.c
@@ -115,8 +115,8 @@ conn_init(struct conn_params *p)
 	}
 
 	/* Fill in */
-	strncpy(conn->welcome, p->welcome, CONN_WELCOME_LEN_MAX);
-	strncpy(conn->prompt, p->prompt, CONN_PROMPT_LEN_MAX);
+	strlcpy(conn->welcome, p->welcome, CONN_WELCOME_LEN_MAX + 1);
+	strlcpy(conn->prompt, p->prompt, CONN_PROMPT_LEN_MAX + 1);
 	conn->buf_size = p->buf_size;
 	conn->msg_in_len_max = p->msg_in_len_max;
 	conn->msg_out_len_max = p->msg_out_len_max;
diff --git a/examples/ipsec-secgw/sa.c b/examples/ipsec-secgw/sa.c
index 866ba04b86..b5068765b6 100644
--- a/examples/ipsec-secgw/sa.c
+++ b/examples/ipsec-secgw/sa.c
@@ -338,12 +338,12 @@ parse_key_string(const char *key_str, uint8_t *key)
 		if (pt_end == NULL) {
 			if (strlen(pt_start) > 2)
 				return 0;
-			strncpy(sub_str, pt_start, 2);
+			memcpy(sub_str, pt_start, 2);
 		} else {
 			if (pt_end - pt_start > 2)
 				return 0;
 
-			strncpy(sub_str, pt_start, pt_end - pt_start);
+			memcpy(sub_str, pt_start, pt_end - pt_start);
 			pt_start = pt_end + 1;
 		}
 
diff --git a/examples/l2fwd-crypto/main.c b/examples/l2fwd-crypto/main.c
index ff189b5fab..22ad825c91 100644
--- a/examples/l2fwd-crypto/main.c
+++ b/examples/l2fwd-crypto/main.c
@@ -1576,19 +1576,19 @@ l2fwd_crypto_options_print(struct l2fwd_crypto_options *options)
 	char string_aead_op[MAX_STR_LEN];
 
 	if (options->cipher_xform.cipher.op == RTE_CRYPTO_CIPHER_OP_ENCRYPT)
-		strcpy(string_cipher_op, "Encrypt");
+		strlcpy(string_cipher_op, "Encrypt", sizeof(string_cipher_op));
 	else
-		strcpy(string_cipher_op, "Decrypt");
+		strlcpy(string_cipher_op, "Decrypt", sizeof(string_cipher_op));
 
 	if (options->auth_xform.auth.op == RTE_CRYPTO_AUTH_OP_GENERATE)
-		strcpy(string_auth_op, "Auth generate");
+		strlcpy(string_auth_op, "Auth generate", sizeof(string_auth_op));
 	else
-		strcpy(string_auth_op, "Auth verify");
+		strlcpy(string_auth_op, "Auth verify", sizeof(string_auth_op));
 
 	if (options->aead_xform.aead.op == RTE_CRYPTO_AEAD_OP_ENCRYPT)
-		strcpy(string_aead_op, "Authenticated encryption");
+		strlcpy(string_aead_op, "Authenticated encryption", sizeof(string_aead_op));
 	else
-		strcpy(string_aead_op, "Authenticated decryption");
+		strlcpy(string_aead_op, "Authenticated decryption", sizeof(string_aead_op));
 
 
 	printf("Options:-\nn");
diff --git a/examples/pipeline/cli.c b/examples/pipeline/cli.c
index 215b4061d5..901706fab9 100644
--- a/examples/pipeline/cli.c
+++ b/examples/pipeline/cli.c
@@ -172,9 +172,9 @@ parse_table_entry(struct rte_swx_ctl_pipeline *p,
 	line[0] = 0;
 	for (i = 0; i < n_tokens; i++) {
 		if (i)
-			strcat(line, " ");
+			strlcat(line, " ", MAX_LINE_SIZE);
 
-		strcat(line, tokens[i]);
+		strlcat(line, tokens[i], MAX_LINE_SIZE);
 	}
 
 	/* Read the table entry from the input buffer. */
diff --git a/examples/pipeline/conn.c b/examples/pipeline/conn.c
index e168c4ddaa..257f3c9f78 100644
--- a/examples/pipeline/conn.c
+++ b/examples/pipeline/conn.c
@@ -116,8 +116,8 @@ conn_init(struct conn_params *p)
 	}
 
 	/* Fill in */
-	strncpy(conn->welcome, p->welcome, CONN_WELCOME_LEN_MAX);
-	strncpy(conn->prompt, p->prompt, CONN_PROMPT_LEN_MAX);
+	strlcpy(conn->welcome, p->welcome, CONN_WELCOME_LEN_MAX + 1);
+	strlcpy(conn->prompt, p->prompt, CONN_PROMPT_LEN_MAX + 1);
 	conn->buf_size = p->buf_size;
 	conn->msg_in_len_max = p->msg_in_len_max;
 	conn->msg_out_len_max = p->msg_out_len_max;
diff --git a/examples/vm_power_manager/channel_manager.c b/examples/vm_power_manager/channel_manager.c
index b69449c61d..339c7fbb93 100644
--- a/examples/vm_power_manager/channel_manager.c
+++ b/examples/vm_power_manager/channel_manager.c
@@ -875,8 +875,7 @@ add_vm(const char *vm_name)
 		rte_free(new_domain);
 		return -1;
 	}
-	strncpy(new_domain->name, vm_name, sizeof(new_domain->name));
-	new_domain->name[sizeof(new_domain->name) - 1] = '\0';
+	strlcpy(new_domain->name, vm_name, sizeof(new_domain->name));
 	memset(new_domain->channel_mask, 0, RTE_MAX_LCORE);
 	new_domain->num_channels = 0;
 
-- 
2.53.0


^ permalink raw reply related

* Re: [PATCH 1/5] eal: fix macro for versioned experimental symbol
From: Dariusz Sosnowski @ 2026-06-23 15:26 UTC (permalink / raw)
  To: Stephen Hemminger; +Cc: David Marchand, dev, Bruce Richardson
In-Reply-To: <20260623065000.57d775c9@phoenix.local>

On Tue, Jun 23, 2026 at 06:50:00AM -0700, Stephen Hemminger wrote:
> On Tue, 23 Jun 2026 13:37:47 +0200
> Dariusz Sosnowski <dsosnowski@nvidia.com> wrote:
> 
> > Add a missing semicolon after __asm__ block in
> > RTE_VERSION_EXPERIMENTAL_SYMBOL macro.
> > It's lack triggers the following compilation error with clang:
> > 
> >     ../lib/ethdev/rte_flow.c:320:1: error: expected ';' after top-level asm block
> >       320 | RTE_VERSION_EXPERIMENTAL_SYMBOL(int, rte_flow_dynf_metadata_register, (void))
> >           | ^
> >     ../lib/eal/common/eal_export.h:75:74: note: expanded from macro 'RTE_VERSION_EXPERIMENTAL_SYMBOL'
> >        75 | __asm__(".symver " RTE_STR(name) "_exp, " RTE_STR(name) "@EXPERIMENTAL") \
> >           |                                                                          ^
> >     ../lib/eal/include/rte_common.h:237:20: note: expanded from macro '\
> >     __rte_used'
> >       237 | #define __rte_used __attribute__((used))
> >           |                    ^
> > 
> > Fixes: e30e194c4d06 ("eal: rework function versioning macros")
> > Cc: david.marchand@redhat.com
> > 
> > Signed-
> 
> I didn't see this because clang doesn't have symver support.
> Which version of clang is this?

clang 19 available on Debian 13:

  $ clang --version
  Debian clang version 19.1.7 (3+b1)
  Target: x86_64-pc-linux-gnu
  Thread model: posix
  InstalledDir: /usr/lib/llvm-19/bin

^ permalink raw reply

* [PATCH] app: remove use of strncpy
From: Bruce Richardson @ 2026-06-23 14:50 UTC (permalink / raw)
  To: dev
  Cc: Bruce Richardson, stable, Kai Ji, Cheng Jiang, Chengwen Feng,
	Jerin Jacob, Ori Kam, Aman Singh, Michal Kobylinski,
	Piotr Azarewicz, Marcin Kerlin, Slawomir Mrozowicz,
	Declan Doherty, Aleksander Gajewski, Pablo de Lara,
	Guduri Prathyusha, Harry van Haaren, Morten Brørup, Jiayu Hu,
	Yuan Wang, Anoob Joseph, Xiaoyu Min, Xueming Li, Yuval Avnery

Use of strncpy is not recommended, so replace it with strlcpy or memcpy
as appropriate.

Fixes: f8be1786b1b8 ("app/crypto-perf: introduce performance test application")
Fixes: 8ecd4048ba5d ("app/crypto-perf: fix string not null terminated")
Fixes: 0add6c27cd7c ("app/testeventdev: define the test options")
Fixes: 623dc9364dc6 ("app/dma-perf: introduce DMA performance test")
Fixes: 1e8a4e97b057 ("app/testpmd: add flow dump command")
Fixes: de06137cb295 ("app/regex: add RegEx test application")
Cc: stable@dpdk.org

Signed-off-by: Bruce Richardson <bruce.richardson@intel.com>
---
 app/test-crypto-perf/cperf_options_parsing.c | 5 ++---
 app/test-dma-perf/main.c                     | 2 +-
 app/test-eventdev/evt_options.c              | 2 +-
 app/test-pmd/cmdline_flow.c                  | 2 +-
 app/test-regex/main.c                        | 9 ++-------
 5 files changed, 7 insertions(+), 13 deletions(-)

diff --git a/app/test-crypto-perf/cperf_options_parsing.c b/app/test-crypto-perf/cperf_options_parsing.c
index 14e731586b..0951293adb 100644
--- a/app/test-crypto-perf/cperf_options_parsing.c
+++ b/app/test-crypto-perf/cperf_options_parsing.c
@@ -481,8 +481,7 @@ parse_device_type(struct cperf_options *opts, const char *arg)
 	if (strlen(arg) > (sizeof(opts->device_type) - 1))
 		return -1;
 
-	strncpy(opts->device_type, arg, sizeof(opts->device_type) - 1);
-	*(opts->device_type + sizeof(opts->device_type) - 1) = '\0';
+	strlcpy(opts->device_type, arg, sizeof(opts->device_type));
 
 	return 0;
 }
@@ -1125,7 +1124,7 @@ cperf_options_default(struct cperf_options *opts)
 	opts->segment_sz = 0;
 
 	opts->imix_distribution_count = 0;
-	strncpy(opts->device_type, "crypto_aesni_mb",
+	strlcpy(opts->device_type, "crypto_aesni_mb",
 			sizeof(opts->device_type));
 	opts->nb_qps = 1;
 
diff --git a/app/test-dma-perf/main.c b/app/test-dma-perf/main.c
index 4249dcfd3d..13bf07a764 100644
--- a/app/test-dma-perf/main.c
+++ b/app/test-dma-perf/main.c
@@ -220,7 +220,7 @@ parse_entry(const char *value, struct test_configure_entry *entry)
 	int args_nr = -1;
 	int ret;
 
-	strncpy(input, value, 254);
+	strlcpy(input, value, sizeof(input));
 	if (*input == '\0')
 		goto out;
 
diff --git a/app/test-eventdev/evt_options.c b/app/test-eventdev/evt_options.c
index 0e70c971eb..1da0aba386 100644
--- a/app/test-eventdev/evt_options.c
+++ b/app/test-eventdev/evt_options.c
@@ -23,7 +23,7 @@ evt_options_default(struct evt_options *opt)
 	memset(opt, 0, sizeof(*opt));
 	opt->verbose_level = 1; /* Enable minimal prints */
 	opt->dev_id = 0;
-	strncpy(opt->test_name, "order_queue", EVT_TEST_NAME_MAX_LEN);
+	strlcpy(opt->test_name, "order_queue", sizeof(opt->test_name));
 	opt->nb_flows = 1024;
 	opt->socket_id = SOCKET_ID_ANY;
 	opt->pool_sz = 16 * 1024;
diff --git a/app/test-pmd/cmdline_flow.c b/app/test-pmd/cmdline_flow.c
index 67f200f2e3..465396d2e5 100644
--- a/app/test-pmd/cmdline_flow.c
+++ b/app/test-pmd/cmdline_flow.c
@@ -11910,7 +11910,7 @@ parse_string0(struct context *ctx, const struct token *token __rte_unused,
 	if (!ctx->object)
 		return len;
 	buf = (uint8_t *)ctx->object + arg_data->offset;
-	strncpy(buf, str, len);
+	memcpy(buf, str, len);
 	if (ctx->objmask)
 		memset((uint8_t *)ctx->objmask + arg_data->offset, 0xff, len);
 	return len;
diff --git a/app/test-regex/main.c b/app/test-regex/main.c
index acb834a8b4..81719f2e04 100644
--- a/app/test-regex/main.c
+++ b/app/test-regex/main.c
@@ -103,7 +103,6 @@ args_parse(int argc, char **argv, char *rules_file, char *data_file,
 	char **argvopt;
 	int opt;
 	int opt_idx;
-	size_t len;
 	static struct option lgopts[] = {
 		{ "help",  0, 0, ARG_HELP},
 		/* Rules database file to load. */
@@ -133,20 +132,16 @@ args_parse(int argc, char **argv, char *rules_file, char *data_file,
 				lgopts, &opt_idx)) != EOF) {
 		switch (opt) {
 		case ARG_RULES_FILE_NAME:
-			len = strnlen(optarg, MAX_FILE_NAME - 1);
-			if (len == MAX_FILE_NAME)
+			if (strlcpy(rules_file, optarg, MAX_FILE_NAME) >= MAX_FILE_NAME)
 				rte_exit(EXIT_FAILURE,
 					 "Rule file name to long max %d\n",
 					 MAX_FILE_NAME - 1);
-			strncpy(rules_file, optarg, MAX_FILE_NAME - 1);
 			break;
 		case ARG_DATA_FILE_NAME:
-			len = strnlen(optarg, MAX_FILE_NAME - 1);
-			if (len == MAX_FILE_NAME)
+			if (strlcpy(data_file, optarg, MAX_FILE_NAME) >= MAX_FILE_NAME)
 				rte_exit(EXIT_FAILURE,
 					 "Data file name to long max %d\n",
 					 MAX_FILE_NAME - 1);
-			strncpy(data_file, optarg, MAX_FILE_NAME - 1);
 			break;
 		case ARG_NUM_OF_JOBS:
 			*nb_jobs = atoi(optarg);
-- 
2.53.0


^ permalink raw reply related

* [PATCH v4 24/24] doc: add release notes for BPF validation fixes
From: Marat Khalili @ 2026-06-23 14:32 UTC (permalink / raw)
  Cc: dev, Konstantin Ananyev
In-Reply-To: <20260623143215.95318-1-marat.khalili@huawei.com>

Document hardening the BPF validator.

Signed-off-by: Marat Khalili <marat.khalili@huawei.com>
Acked-by: Konstantin Ananyev <konstantin.ananyev@huawei.com>
---
 doc/guides/rel_notes/release_26_07.rst | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/doc/guides/rel_notes/release_26_07.rst b/doc/guides/rel_notes/release_26_07.rst
index 8471966a4992..9376e7acad24 100644
--- a/doc/guides/rel_notes/release_26_07.rst
+++ b/doc/guides/rel_notes/release_26_07.rst
@@ -164,7 +164,7 @@ New Features
     for installing already loaded BPF programs as port callbacks
     (as opposed to loading them directly from ELF files).
 
-* **Added BPF validation debugging API.**
+* **Added BPF validation debugging API and hardened BPF validator.**
 
   * Introduced a new set of APIs (prefixed with ``rte_bpf_validate_debug_``) to
     introspect the BPF validator. This provides a mechanism to set breakpoints
@@ -172,6 +172,10 @@ New Features
     (such as tracked register bounds). This API is crucial primarily for writing
     comprehensive tests for the validator, but also serves as a foundation for a
     future interactive eBPF validation debugger.
+  * Fixed numerous bugs in the BPF validator's abstract interpretation logic,
+    including incorrect bounds tracking for jumps and arithmetic operations, as
+    well as fixing several instances of undefined behavior (UB) when verifying
+    malicious or corrupt programs.
 
 * **Added AI review helpers.**
 
-- 
2.43.0


^ permalink raw reply related

* [PATCH v4 23/24] bpf/validate: prevent overflow when building graph
From: Marat Khalili @ 2026-06-23 14:32 UTC (permalink / raw)
  To: Konstantin Ananyev; +Cc: dev, stable, Claudia Cauli
In-Reply-To: <20260623143215.95318-1-marat.khalili@huawei.com>

Function `evst_pool_init` for malicious or corrupt BPF program with
number of conditional jumps exceeding a third of UINT32_MAX could cause
arithmetic and buffer overflows when working with the program graph.

Fix the issue by limiting maximum number of conditional jumps supported
by UINT32_MAX / 4, or more than 1 billion.

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>
---
 lib/bpf/bpf_validate.c | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/lib/bpf/bpf_validate.c b/lib/bpf/bpf_validate.c
index 03c590c75377..f9960088a285 100644
--- a/lib/bpf/bpf_validate.c
+++ b/lib/bpf/bpf_validate.c
@@ -2662,6 +2662,10 @@ evst_pool_init(struct bpf_verifier *bvf)
 {
 	uint32_t k, n;
 
+	if (bvf->nb_jcc_nodes > UINT32_MAX / 4)
+		/* Calculations that follow may overflow. */
+		return -E2BIG;
+
 	/*
 	 * We need nb_jcc_nodes + 1 for save_cur/restore_cur
 	 * remaining ones will be used for state tracking/pruning.
-- 
2.43.0


^ permalink raw reply related

* [PATCH v4 22/24] bpf/validate: fix BPF_XOR signed min calculation
From: Marat Khalili @ 2026-06-23 14:32 UTC (permalink / raw)
  To: Konstantin Ananyev; +Cc: dev, stable, Claudia Cauli
In-Reply-To: <20260623143215.95318-1-marat.khalili@huawei.com>

Function `eval_xor` calculated signed minimum using essentially unsigned
algorithm as long as any of the operands have non-negative range, which
is incorrect since it ignores any negative numbers that may have the
sign or any other bits set.

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

    Tested program:
        0:  mov r0, #0x0
        1:  ldxdw r2, [r1 + 0]
        2:  jsgt r2, #0x0, L5
        3:  xor r2, #0x0  ; tested instruction
        4:  mov r0, #0x1
        5:  exit
    Pre-state:
       r2:  INT64_MIN..0
    Post-state:
       r2:  0

After the tested instruction validator considers r2 to equal 0, however
if -1 was loaded on step 1 it is possible for it to be -1.

Set signed range to full if any of the operands can be negative,
otherwise (if both operands are non-negative) use same algorithm as for
unsigned numbers. Add test.

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>
---
 app/test/test_bpf_validate.c | 17 +++++++++++++++++
 lib/bpf/bpf_validate.c       |  2 +-
 2 files changed, 18 insertions(+), 1 deletion(-)

diff --git a/app/test/test_bpf_validate.c b/app/test/test_bpf_validate.c
index 205373a4f86b..a06d3254d6ba 100644
--- a/app/test/test_bpf_validate.c
+++ b/app/test/test_bpf_validate.c
@@ -1764,6 +1764,23 @@ test_alu64_sub_x_src_signed_max_zero(void)
 REGISTER_FAST_TEST(bpf_validate_alu64_sub_x_src_signed_max_zero_autotest, NOHUGE_OK, ASAN_OK,
 	test_alu64_sub_x_src_signed_max_zero);
 
+/* 64-bit bitwise XOR between a negative scalar range and zero immediate. */
+static int
+test_alu64_xor_k_negative(void)
+{
+	return verify_instruction((struct verify_instruction_param){
+		.tested_instruction = {
+			.code = (EBPF_ALU64 | BPF_XOR | BPF_K),
+			.imm = 0,
+		},
+		.pre.dst = make_signed_domain(INT64_MIN, 0),
+		.post.dst = unknown,
+	});
+}
+
+REGISTER_FAST_TEST(bpf_validate_alu64_xor_k_negative_autotest, NOHUGE_OK, ASAN_OK,
+	test_alu64_xor_k_negative);
+
 /* 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 131a5468dbc4..03c590c75377 100644
--- a/lib/bpf/bpf_validate.c
+++ b/lib/bpf/bpf_validate.c
@@ -910,7 +910,7 @@ eval_xor(struct bpf_reg_val *rd, const struct bpf_reg_val *rs, size_t opsz,
 		rd->s.max ^= rs->s.max;
 
 	/* both operands are non-negative */
-	} else if (rd->s.min >= 0 || rs->s.min >= 0) {
+	} else if (rd->s.min >= 0 && rs->s.min >= 0) {
 		rd->s.max = eval_uor_max(rd->s.max, rs->s.max, opsz);
 		rd->s.min = 0;
 	} else
-- 
2.43.0


^ permalink raw reply related

* [PATCH v4 21/24] bpf/validate: fix BPF_SUB signed max zero case
From: Marat Khalili @ 2026-06-23 14:32 UTC (permalink / raw)
  To: Konstantin Ananyev; +Cc: dev, stable, Claudia Cauli
In-Reply-To: <20260623143215.95318-1-marat.khalili@huawei.com>

Function `eval_sub` used source register signed minimum to detect
overflow of the difference (operation result) signed minimum, and source
register signed maximum to detect overflow of the difference signed
maximum. However in the actual formula for difference source register
bounds are swapped (correctly, since we subtract it), so in overflow
detection we should also have swapped them. It caused false negatives in
certain cases.

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

    Tested program:
        0:  mov r0, #0x0
        1:  ldxdw r2, [r1 + 0]
        2:  jsgt r2, #0x0, L7
        3:  ldxdw r3, [r1 + 8]
        4:  jsgt r3, #0x0, L7
        5:  sub r2, r3  ; tested instruction
        6:  mov r0, #0x1
        7:  exit
    Pre-state:
       r2:  INT64_MIN..0
       r3:  INT64_MIN..0
    Post-state:
       r2:  INT64_MIN

Validator ignores overflow of signed minimum and considers result to
always equal INT64_MIN. However, if -1 was loaded on step 1 and -2 was
loaded on step 3 it is possible for the difference to equal 1.

Swap source register signed minimum and maximum in the overflow
condition to match the new range formula, add test.

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>
---
 app/test/test_bpf_validate.c | 17 +++++++++++++++++
 lib/bpf/bpf_validate.c       |  4 ++--
 2 files changed, 19 insertions(+), 2 deletions(-)

diff --git a/app/test/test_bpf_validate.c b/app/test/test_bpf_validate.c
index 7cf9e404b697..205373a4f86b 100644
--- a/app/test/test_bpf_validate.c
+++ b/app/test/test_bpf_validate.c
@@ -1747,6 +1747,23 @@ test_alu64_or_k_positive(void)
 REGISTER_FAST_TEST(bpf_validate_alu64_or_k_positive_autotest, NOHUGE_OK, ASAN_OK,
 	test_alu64_or_k_positive);
 
+/* 64-bit difference between two negative ranges.. */
+static int
+test_alu64_sub_x_src_signed_max_zero(void)
+{
+	return verify_instruction((struct verify_instruction_param){
+		.tested_instruction = {
+			.code = (EBPF_ALU64 | BPF_SUB | BPF_X),
+		},
+		.pre.dst = make_signed_domain(INT64_MIN, 0),
+		.pre.src = make_signed_domain(INT64_MIN, 0),
+		.post.dst = unknown,
+	});
+}
+
+REGISTER_FAST_TEST(bpf_validate_alu64_sub_x_src_signed_max_zero_autotest, NOHUGE_OK, ASAN_OK,
+	test_alu64_sub_x_src_signed_max_zero);
+
 /* 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 abb39cfd328d..131a5468dbc4 100644
--- a/lib/bpf/bpf_validate.c
+++ b/lib/bpf/bpf_validate.c
@@ -716,9 +716,9 @@ eval_sub(struct bpf_reg_val *rd, const struct bpf_reg_val *rs, uint64_t msk)
 		eval_umax_bound(&rv, msk);
 
 	if ((rd->s.min != rd->s.max || rs->s.min != rs->s.max) &&
-			(((rs->s.min < 0 && rv.s.min < rd->s.min) ||
+			(((rs->s.max < 0 && rv.s.min < rd->s.min) ||
 			rv.s.min > rd->s.min) ||
-			((rs->s.max < 0 && rv.s.max < rd->s.max) ||
+			((rs->s.min < 0 && rv.s.max < rd->s.max) ||
 			rv.s.max > rd->s.max)))
 		eval_smax_bound(&rv, msk);
 
-- 
2.43.0


^ permalink raw reply related

* [PATCH v4 20/24] bpf/validate: fix BPF_OR min calculations
From: Marat Khalili @ 2026-06-23 14:32 UTC (permalink / raw)
  To: Konstantin Ananyev; +Cc: dev, stable, Claudia Cauli
In-Reply-To: <20260623143215.95318-1-marat.khalili@huawei.com>

This commit fixes two different problems in signed and unsigned minimum
calculations within `eval_or`. Passing tests requires both problems to
be fixed which is why the changes are squashed in one commit.

1) Function `eval_or` calculated result signed minimum as bitwise OR
between corresponding minimums as long as any of them is non-negative,
which is incorrect since values within the range can have zeroes where
the minimums don't, including the sign bit.

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

    Tested program:
        0:  mov r0, #0x0
        1:  ldxdw r2, [r1 + 0]
        2:  jlt r2, #0x5, L8
        3:  jgt r2, #0x6, L8
        4:  jslt r2, #0x5, L8
        5:  jsgt r2, #0x6, L8
        6:  or r2, #0xfffffffe  ; tested instruction
        7:  mov r0, #0x1
        8:  exit
    Pre-state:
       r2:  5..6
    Post-state:
       r2:  -1

After the tested instruction validator considers r2 to always equal -1,
however if 6 was loaded on step 1 it is possible for it to be -2:

     0x6 & 0xfffffffffffffffe == 0xfffffffffffffffe = -2

Set signed range to full if any of the operands can be negative,
otherwise use the maximum of both minimums as a new signed minimum
following the idea that result of bitwise OR cannot be smaller than its
operands. Add test.

2) Function `eval_or` calculated result unsigned minimum as bitwise OR
between corresponding minimums, which is incorrect since values within
the range can have zeroes the minimums don't.

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

    Tested program:
        0:  mov r0, #0x0
        1:  ldxdw r2, [r1 + 0]
        2:  jlt r2, #0x5, L8
        3:  jgt r2, #0x6, L8
        4:  jslt r2, #0x5, L8
        5:  jsgt r2, #0x6, L8
        6:  or r2, #0x2  ; tested instruction
        7:  mov r0, #0x1
        8:  exit
    Pre-state:
       r2:  5..6
    Post-state:
       r2:  7

After the tested instruction validator considers r2 to always equal 7,
however if 6 was loaded on step 1 it is possible for it to be 6:

    0x6 & 0x2 == 0x6

Use the maximum of both minimums as a new unsigned minimum following the
idea that result of bitwise OR cannot be smaller than its operands. Add
test.

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>
---
 app/test/test_bpf_validate.c | 34 ++++++++++++++++++++++++++++++++++
 lib/bpf/bpf_validate.c       |  6 +++---
 2 files changed, 37 insertions(+), 3 deletions(-)

diff --git a/app/test/test_bpf_validate.c b/app/test/test_bpf_validate.c
index c3d1bdb78fbc..7cf9e404b697 100644
--- a/app/test/test_bpf_validate.c
+++ b/app/test/test_bpf_validate.c
@@ -1713,6 +1713,40 @@ test_alu64_neg_zero_last(void)
 REGISTER_FAST_TEST(bpf_validate_alu64_neg_zero_last_autotest, NOHUGE_OK, ASAN_OK,
 	test_alu64_neg_zero_last);
 
+/* 64-bit bitwise OR between a positive scalar range and negative immediate. */
+static int
+test_alu64_or_k_negative(void)
+{
+	return verify_instruction((struct verify_instruction_param){
+		.tested_instruction = {
+			.code = (EBPF_ALU64 | BPF_OR | BPF_K),
+			.imm = -2,
+		},
+		.pre.dst = make_signed_domain(5, 6),
+		.post.dst = make_signed_domain(-2, -1),
+	});
+}
+
+REGISTER_FAST_TEST(bpf_validate_alu64_or_k_negative_autotest, NOHUGE_OK, ASAN_OK,
+	test_alu64_or_k_negative);
+
+/* 64-bit bitwise OR between a positive scalar range and positive immediate. */
+static int
+test_alu64_or_k_positive(void)
+{
+	return verify_instruction((struct verify_instruction_param){
+		.tested_instruction = {
+			.code = (EBPF_ALU64 | BPF_OR | BPF_K),
+			.imm = 2,
+		},
+		.pre.dst = make_signed_domain(5, 6),
+		.post.dst = make_signed_domain(5, 7),
+	});
+}
+
+REGISTER_FAST_TEST(bpf_validate_alu64_or_k_positive_autotest, NOHUGE_OK, ASAN_OK,
+	test_alu64_or_k_positive);
+
 /* 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 4e4c0ddeb2b8..abb39cfd328d 100644
--- a/lib/bpf/bpf_validate.c
+++ b/lib/bpf/bpf_validate.c
@@ -875,7 +875,7 @@ eval_or(struct bpf_reg_val *rd, const struct bpf_reg_val *rs, size_t opsz,
 		rd->u.max |= rs->u.max;
 	} else {
 		rd->u.max = eval_uor_max(rd->u.max, rs->u.max, opsz);
-		rd->u.min |= rs->u.min;
+		rd->u.min = RTE_MAX(rd->u.min, rs->u.min);
 	}
 
 	/* both operands are constants */
@@ -884,9 +884,9 @@ eval_or(struct bpf_reg_val *rd, const struct bpf_reg_val *rs, size_t opsz,
 		rd->s.max |= rs->s.max;
 
 	/* both operands are non-negative */
-	} else if (rd->s.min >= 0 || rs->s.min >= 0) {
+	} else if (rd->s.min >= 0 && rs->s.min >= 0) {
 		rd->s.max = eval_uor_max(rd->s.max, rs->s.max, opsz);
-		rd->s.min |= rs->s.min;
+		rd->s.min = RTE_MAX(rd->s.min, rs->s.min);
 	} else
 		eval_smax_bound(rd, msk);
 }
-- 
2.43.0


^ permalink raw reply related

* [PATCH v4 19/24] bpf/validate: fix BPF_LSH shift-out-of-bounds UB
From: Marat Khalili @ 2026-06-23 14:32 UTC (permalink / raw)
  To: Konstantin Ananyev; +Cc: dev, stable, Claudia Cauli
In-Reply-To: <20260623143215.95318-1-marat.khalili@huawei.com>

Function `eval_lsh` when validating left shift by 63 invoked macro
`RTE_LEN2MASK(0, int64_t)` which triggered shift-out-of-bounds 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:  jlt r2, #0x3, L8
        3:  jgt r2, #0x5, L8
        4:  jslt r2, #0x3, L8
        5:  jsgt r2, #0x5, L8
        6:  lsh r2, #0x3f  ; tested instruction
        7:  mov r0, #0x1
        8:  exit
    Pre-state:
       r2:  3..5
    Post-state:
       r2:  0..UINT64_MAX

With sanitizer the following diagnostic is generated:

    lib/bpf/bpf_validate.c:785:4: runtime error: shift exponent 64 is
    too large for 64-bit type 'long unsigned int'
        #0 0x00000274d5e0 in eval_lsh lib/bpf/bpf_validate.c:785
        #1 0x00000275a2ea in eval_alu lib/bpf/bpf_validate.c:1310
        #2 0x00000276ce3d in evaluate lib/bpf/bpf_validate.c:3284

Add guard for this case, add test.

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>
---
 app/test/test_bpf_validate.c | 17 +++++++++++++++++
 lib/bpf/bpf_validate.c       |  3 ++-
 2 files changed, 19 insertions(+), 1 deletion(-)

diff --git a/app/test/test_bpf_validate.c b/app/test/test_bpf_validate.c
index 5d26299ba65d..c3d1bdb78fbc 100644
--- a/app/test/test_bpf_validate.c
+++ b/app/test/test_bpf_validate.c
@@ -1536,6 +1536,23 @@ test_alu64_div_mod_overflow(void)
 REGISTER_FAST_TEST(bpf_validate_alu64_div_mod_overflow_autotest, NOHUGE_OK, ASAN_OK,
 	test_alu64_div_mod_overflow);
 
+/* 64-bit left shift by 63. */
+static int
+test_alu64_lsh_63(void)
+{
+	return verify_instruction((struct verify_instruction_param){
+		.tested_instruction = {
+			.code = (EBPF_ALU64 | BPF_LSH | BPF_K),
+			.imm = 63,
+		},
+		.pre.dst = make_signed_domain(3, 5),
+		.post.dst = unknown,
+	});
+}
+
+REGISTER_FAST_TEST(bpf_validate_alu64_lsh_63_autotest, NOHUGE_OK, ASAN_OK,
+	test_alu64_lsh_63);
+
 /* 64-bit multiplication of constant and immediate with overflow. */
 static int
 test_alu64_mul_k_overflow(void)
diff --git a/lib/bpf/bpf_validate.c b/lib/bpf/bpf_validate.c
index d4d8ec4251f1..4e4c0ddeb2b8 100644
--- a/lib/bpf/bpf_validate.c
+++ b/lib/bpf/bpf_validate.c
@@ -746,7 +746,8 @@ eval_lsh(struct bpf_reg_val *rd, const struct bpf_reg_val *rs, size_t opsz,
 
 	/* check that dreg values are and would remain always positive */
 	if ((uint64_t)rd->s.min >> (opsz - 1) != 0 || rd->s.max >=
-			RTE_LEN2MASK(opsz - rs->u.max - 1, int64_t))
+			(rs->u.max == opsz - 1 ? 0 :
+				 RTE_LEN2MASK(opsz - rs->u.max - 1, int64_t)))
 		eval_smax_bound(rd, msk);
 	else {
 		rd->s.max <<= rs->u.max;
-- 
2.43.0


^ permalink raw reply related

* [PATCH v4 18/24] bpf/validate: fix BPF_AND min calculations
From: Marat Khalili @ 2026-06-23 14:32 UTC (permalink / raw)
  To: Konstantin Ananyev; +Cc: dev, stable, Claudia Cauli
In-Reply-To: <20260623143215.95318-1-marat.khalili@huawei.com>

Function `eval_and` calculated both signed (if positive) and unsigned
minimum values as bitwise AND between corresponding minimums, which is
incorrect since intermediate values can have zeroes in bits where
minimum values don't.

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

    Tested program:
        0:  mov r0, #0x0
        1:  ldxdw r2, [r1 + 0]
        2:  jlt r2, #0x6, L8
        3:  jgt r2, #0x8, L8
        4:  jslt r2, #0x6, L8
        5:  jsgt r2, #0x8, L8
        6:  and r2, #0x5  ; tested instruction
        7:  mov r0, #0x1
        8:  exit
    Pre-state:
       r2:  6..8
    Post-state:
       r2:  4..7

After the tested instruction validator considers r2 to be equal or
greater than 4, however if 8 was loaded on step 1 it is possible for it
to be zero (0x8 & 0x5 == 0).

Use zero as a new safe lower bound for both signed (if positive) and
unsigned minimum. Add test.

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>
---
 app/test/test_bpf_validate.c | 17 +++++++++++++++++
 lib/bpf/bpf_validate.c       |  4 ++--
 2 files changed, 19 insertions(+), 2 deletions(-)

diff --git a/app/test/test_bpf_validate.c b/app/test/test_bpf_validate.c
index f80dee24a677..5d26299ba65d 100644
--- a/app/test/test_bpf_validate.c
+++ b/app/test/test_bpf_validate.c
@@ -1384,6 +1384,23 @@ 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 bitwise AND between a scalar range and immediate. */
+static int
+test_alu64_and_k(void)
+{
+	return verify_instruction((struct verify_instruction_param){
+		.tested_instruction = {
+			.code = (EBPF_ALU64 | BPF_AND | BPF_K),
+			.imm = 5,
+		},
+		.pre.dst = make_signed_domain(6, 8),
+		.post.dst = make_signed_domain(0, 7),
+	});
+}
+
+REGISTER_FAST_TEST(bpf_validate_alu64_and_k_autotest, NOHUGE_OK, ASAN_OK,
+	test_alu64_and_k);
+
 /* 64-bit division and modulo of UINT64_MAX*2/3. */
 static int
 test_alu64_div_mod_big_constant(void)
diff --git a/lib/bpf/bpf_validate.c b/lib/bpf/bpf_validate.c
index af084e36c8d0..d4d8ec4251f1 100644
--- a/lib/bpf/bpf_validate.c
+++ b/lib/bpf/bpf_validate.c
@@ -848,7 +848,7 @@ eval_and(struct bpf_reg_val *rd, const struct bpf_reg_val *rs, size_t opsz,
 		rd->u.max &= rs->u.max;
 	} else {
 		rd->u.max = eval_uand_max(rd->u.max, rs->u.max, opsz);
-		rd->u.min &= rs->u.min;
+		rd->u.min = 0;
 	}
 
 	/* both operands are constants */
@@ -859,7 +859,7 @@ eval_and(struct bpf_reg_val *rd, const struct bpf_reg_val *rs, size_t opsz,
 	} else if (rd->s.min >= 0 || rs->s.min >= 0) {
 		rd->s.max = eval_uand_max(rd->s.max & (msk >> 1),
 			rs->s.max & (msk >> 1), opsz);
-		rd->s.min &= rs->s.min;
+		rd->s.min = 0;
 	} else
 		eval_smax_bound(rd, msk);
 }
-- 
2.43.0


^ permalink raw reply related

* [PATCH v4 17/24] bpf/validate: fix BPF_JMP empty range handling
From: Marat Khalili @ 2026-06-23 14:32 UTC (permalink / raw)
  To: Konstantin Ananyev; +Cc: dev, stable, Claudia Cauli
In-Reply-To: <20260623143215.95318-1-marat.khalili@huawei.com>

Function `eval_jcc` did not account for 'dynamically unreachable' code
paths. Some code paths may be _dynamically_ unreachable, which measn
that according to validator calculations no valid values are left to
evaluate. This does not indicate dead code since same code might be
reachable through other code paths. Previous behaviour resulted in:
* undefined behaviour in corner cases;
* ranges breaking min <= max invariant 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);
* unnecessary work for validator contributing to exponential code paths
  grow in some cases.

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

    Tested program:
        0:  mov r0, #0x0
        1:  mov r2, #0x2a
        2:  lddw r3, #0x8000000000000000
        4:  jslt r2, r3, L7  ; tested instruction
        5:  mov r0, #0x1
        6:  exit
        7:  mov r0, #0x2
        8:  exit
    Pre-state:
       r2:  42
       r3:  INT64_MIN
    Post-state:
       r2:  42
       r3:  INT64_MIN
    Jump-state:
       r2:  42
       r3:  43..INT64_MIN INTERSECT 0x8000000000000000 (!)

At step 7 after jump from tested instruction validator considers r3 to
equal 0x8000000000000000 if viewed as unsigned, or have nonsensical
range 43..INT64_MIN if viewed as signed. In reality there is just no
valid range for this code path since it will never occur.

With sanitizer the following diagnostic is generated:

    lib/bpf/bpf_validate.c:1824:15: runtime error: signed integer
    overflow: -9223372036854775808 - 1 cannot be represented in type
    'long int'
        #0 0x000002761e41 in eval_jslt_jsge lib/bpf/bpf_validate.c:1824
        #1 0x000002762acb in eval_jcc lib/bpf/bpf_validate.c:1881
        #2 0x00000276b749 in evaluate lib/bpf/bpf_validate.c:3245
    ...

    SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior
    lib/bpf/bpf_validate.c:1824:15

Add pruning of dynamically unreachable code paths that arise from
ordering comparisons. Add tests for remaining ordering jump cases.

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>
---
 app/test/test_bpf_validate.c     | 277 ++++++++++++++++++++++++++++++-
 lib/bpf/bpf_validate.c           |  96 ++++++++---
 lib/bpf/rte_bpf_validate_debug.h |   2 +
 3 files changed, 351 insertions(+), 24 deletions(-)

diff --git a/app/test/test_bpf_validate.c b/app/test/test_bpf_validate.c
index 15ccf4f6a573..f80dee24a677 100644
--- a/app/test/test_bpf_validate.c
+++ b/app/test/test_bpf_validate.c
@@ -135,6 +135,11 @@ static const struct domain unknown = {
 	.u = { .min = 0, .max = UINT64_MAX },
 };
 
+/* Unreachable state. */
+static const struct state unreachable = {
+	.is_unreachable = true,
+};
+
 
 /* BUILDING DOMAINS */
 
@@ -1710,6 +1715,55 @@ test_jmp64_jslt_x(void)
 REGISTER_FAST_TEST(bpf_validate_jmp64_jslt_x_autotest, NOHUGE_OK, ASAN_OK,
 	test_jmp64_jslt_x);
 
+/* Jump on ordering comparisons with potential bound overflow. */
+static int
+test_jmp64_ordering_overflow(void)
+{
+	/* In this test signed and unsigned cases are spelled out explicitly. */
+	const bool also_signed = false;
+
+	TEST_ASSERT_SUCCESS(verify_comparison((struct verify_instruction_param){
+		.tested_instruction = {
+			.code = (BPF_JMP | EBPF_JSLT | BPF_X),
+		},
+		.pre.dst = make_singleton_domain(42),
+		.pre.src = make_singleton_domain(INT64_MIN),
+		.jump = unreachable,
+	}, also_signed), "signed less than INT64_MIN");
+
+	TEST_ASSERT_SUCCESS(verify_comparison((struct verify_instruction_param){
+		.tested_instruction = {
+			.code = (BPF_JMP | EBPF_JSGT | BPF_X),
+		},
+		.pre.dst = make_singleton_domain(42),
+		.pre.src = make_singleton_domain(INT64_MAX),
+		.jump = unreachable,
+	}, also_signed), "signed greater than INT64_MAX");
+
+	TEST_ASSERT_SUCCESS(verify_comparison((struct verify_instruction_param){
+		.tested_instruction = {
+			.code = (BPF_JMP | EBPF_JLT | BPF_X),
+		},
+		.pre.dst = make_singleton_domain(42),
+		.pre.src = make_singleton_domain(0),
+		.jump = unreachable,
+	}, also_signed), "unsigned less than zero");
+
+	TEST_ASSERT_SUCCESS(verify_comparison((struct verify_instruction_param){
+		.tested_instruction = {
+			.code = (BPF_JMP | BPF_JGT | BPF_X),
+		},
+		.pre.dst = make_singleton_domain(42),
+		.pre.src = make_singleton_domain(UINT64_MAX),
+		.jump = unreachable,
+	}, also_signed), "unsigned greater than UINT64_MAX");
+
+	return TEST_SUCCESS;
+}
+
+REGISTER_FAST_TEST(bpf_validate_jmp64_ordering_overflow_autotest, NOHUGE_OK, ASAN_OK,
+	test_jmp64_ordering_overflow);
+
 /* Jump on ordering comparisons between two ranges. */
 static int
 test_jmp64_ordering_ranges(void)
@@ -1717,6 +1771,29 @@ test_jmp64_ordering_ranges(void)
 	/* All ranges used are valid for both signed and unsigned comparisons. */
 	const bool also_signed = true;
 
+	/*
+	 *               20 ---- dst ---- 60
+	 * 0 - src - 10
+	 */
+
+	TEST_ASSERT_SUCCESS(verify_comparison((struct verify_instruction_param){
+		.tested_instruction = {
+			.code = (BPF_JMP | EBPF_JLT | BPF_X),
+		},
+		.pre.dst = make_signed_domain(20, 60),
+		.pre.src = make_signed_domain(0, 10),
+		.jump = unreachable,
+	}, also_signed), "strict, dst range strongly greater than src range");
+
+	TEST_ASSERT_SUCCESS(verify_comparison((struct verify_instruction_param){
+		.tested_instruction = {
+			.code = (BPF_JMP | EBPF_JLE | BPF_X),
+		},
+		.pre.dst = make_signed_domain(20, 60),
+		.pre.src = make_signed_domain(0, 10),
+		.jump = unreachable,
+	}, also_signed), "non-strict, dst range strongly greater than src range");
+
 	/*
 	 *     20 ---- dst ---- 60
 	 * 10 -- src -- 40
@@ -1817,15 +1894,38 @@ test_jmp64_ordering_ranges(void)
 		.post.src = make_signed_domain(40, 59),
 	}, also_signed), "non-strict, dst range weakly less than src range");
 
+	/*
+	 *     20 ---- dst ---- 60
+	 *                          70 - src - 80
+	 */
+
+	TEST_ASSERT_SUCCESS(verify_comparison((struct verify_instruction_param){
+		.tested_instruction = {
+			.code = (BPF_JMP | EBPF_JLT | BPF_X),
+		},
+		.pre.dst = make_signed_domain(20, 60),
+		.pre.src = make_signed_domain(70, 80),
+		.post = unreachable,
+	}, also_signed), "strict, dst range strongly less than src range");
+
+	TEST_ASSERT_SUCCESS(verify_comparison((struct verify_instruction_param){
+		.tested_instruction = {
+			.code = (BPF_JMP | EBPF_JLE | BPF_X),
+		},
+		.pre.dst = make_signed_domain(20, 60),
+		.pre.src = make_signed_domain(70, 80),
+		.post = unreachable,
+	}, also_signed), "non-strict, dst range strongly less than src range");
+
 	return TEST_SUCCESS;
 }
 
 REGISTER_FAST_TEST(bpf_validate_jmp64_ordering_ranges_autotest, NOHUGE_OK, ASAN_OK,
 	test_jmp64_ordering_ranges);
 
-/* Jump on ordering comparisons with singleton. */
+/* Jump on ordering comparisons with singleton inside the range. */
 static int
-test_jmp64_ordering_singleton(void)
+test_jmp64_ordering_singleton_inside(void)
 {
 	/* All ranges used are valid for both signed and unsigned comparisons. */
 	const bool also_signed = true;
@@ -1878,8 +1978,177 @@ test_jmp64_ordering_singleton(void)
 	return TEST_SUCCESS;
 }
 
-REGISTER_FAST_TEST(bpf_validate_jmp64_ordering_singleton_autotest, NOHUGE_OK, ASAN_OK,
-	test_jmp64_ordering_singleton);
+REGISTER_FAST_TEST(bpf_validate_jmp64_ordering_singleton_inside_autotest, NOHUGE_OK, ASAN_OK,
+	test_jmp64_ordering_singleton_inside);
+
+/* Jump on ordering comparisons with singleton outside the range. */
+static int
+test_jmp64_ordering_singleton_outside(void)
+{
+	/* All ranges used are valid for both signed and unsigned comparisons. */
+	const bool also_signed = true;
+
+	/*
+	 *       20 ---- dst ---- 60
+	 *  imm
+	 */
+
+	TEST_ASSERT_SUCCESS(verify_comparison((struct verify_instruction_param){
+		.tested_instruction = {
+			.code = (BPF_JMP | EBPF_JLT | BPF_K),
+			.imm = 10,
+		},
+		.pre.dst = make_signed_domain(20, 60),
+		.jump = unreachable,
+	}, also_signed), "(BPF_JMP | EBPF_JLT | BPF_K) check, range greater than imm");
+
+	TEST_ASSERT_SUCCESS(verify_comparison((struct verify_instruction_param){
+		.tested_instruction = {
+			.code = (BPF_JMP | EBPF_JLE | BPF_K),
+			.imm = 10,
+		},
+		.pre.dst = make_signed_domain(20, 60),
+		.jump = unreachable,
+	}, also_signed), "(BPF_JMP | EBPF_JLE | BPF_K) check, range greater than imm");
+
+	TEST_ASSERT_SUCCESS(verify_comparison((struct verify_instruction_param){
+		.tested_instruction = {
+			.code = (BPF_JMP | BPF_JGT | BPF_K),
+			.imm = 10,
+		},
+		.pre.dst = make_signed_domain(20, 60),
+		.post = unreachable,
+	}, also_signed), "(BPF_JMP | EBPF_JGT | BPF_K) check, range greater than imm");
+
+	TEST_ASSERT_SUCCESS(verify_comparison((struct verify_instruction_param){
+		.tested_instruction = {
+			.code = (BPF_JMP | BPF_JGE | BPF_K),
+			.imm = 10,
+		},
+		.pre.dst = make_signed_domain(20, 60),
+		.post = unreachable,
+	}, also_signed), "(BPF_JMP | EBPF_JGE | BPF_K) check, range greater than imm");
+
+	/*
+	 *       20 ---- dst ---- 60
+	 *                            imm
+	 */
+
+	TEST_ASSERT_SUCCESS(verify_comparison((struct verify_instruction_param){
+		.tested_instruction = {
+			.code = (BPF_JMP | EBPF_JLT | BPF_K),
+			.imm = 70,
+		},
+		.pre.dst = make_signed_domain(20, 60),
+		.post = unreachable,
+	}, also_signed), "(BPF_JMP | EBPF_JLT | BPF_K) check, range less than imm");
+
+	TEST_ASSERT_SUCCESS(verify_comparison((struct verify_instruction_param){
+		.tested_instruction = {
+			.code = (BPF_JMP | EBPF_JLE | BPF_K),
+			.imm = 70,
+		},
+		.pre.dst = make_signed_domain(20, 60),
+		.post = unreachable,
+	}, also_signed), "(BPF_JMP | EBPF_JLE | BPF_K) check, range less than imm");
+
+	TEST_ASSERT_SUCCESS(verify_comparison((struct verify_instruction_param){
+		.tested_instruction = {
+			.code = (BPF_JMP | BPF_JGT | BPF_K),
+			.imm = 70,
+		},
+		.pre.dst = make_signed_domain(20, 60),
+		.jump = unreachable,
+	}, also_signed), "(BPF_JMP | EBPF_JGT | BPF_K) check, range less than imm");
+
+	TEST_ASSERT_SUCCESS(verify_comparison((struct verify_instruction_param){
+		.tested_instruction = {
+			.code = (BPF_JMP | BPF_JGE | BPF_K),
+			.imm = 70,
+		},
+		.pre.dst = make_signed_domain(20, 60),
+		.jump = unreachable,
+	}, also_signed), "(BPF_JMP | EBPF_JGE | BPF_K) check, range less than imm");
+
+	return TEST_SUCCESS;
+}
+
+REGISTER_FAST_TEST(bpf_validate_jmp64_ordering_singleton_outside_autotest, NOHUGE_OK, ASAN_OK,
+	test_jmp64_ordering_singleton_outside);
+
+/* Jump on ordering comparisons with ranges "touching" each other. */
+static int
+test_jmp64_ordering_touching(void)
+{
+	/* All ranges used are valid for both signed and unsigned comparisons. */
+	const bool also_signed = true;
+
+	for (int overlap = 0; overlap != 3; ++overlap) {
+
+		/*
+		 *                  20 - dst - 30
+		 * 10 - src - (19 + overlap)
+		 */
+
+		TEST_ASSERT_SUCCESS(verify_comparison((struct verify_instruction_param){
+			.tested_instruction = {
+				.code = (BPF_JMP | EBPF_JLT | BPF_X),
+			},
+			.pre.dst = make_signed_domain(20, 30),
+			.pre.src = make_signed_domain(10, 19 + overlap),
+			.jump = overlap <= 1 ? unreachable : (struct state){
+				.dst = make_singleton_domain(20),
+				.src = make_singleton_domain(21),
+			},
+		}, also_signed), "strict, dst left touching src right, overlap=%d", overlap);
+
+		TEST_ASSERT_SUCCESS(verify_comparison((struct verify_instruction_param){
+			.tested_instruction = {
+				.code = (BPF_JMP | EBPF_JLE | BPF_X),
+			},
+			.pre.dst = make_signed_domain(20, 30),
+			.pre.src = make_signed_domain(10, 19 + overlap),
+			.jump = overlap < 1 ? unreachable : (struct state){
+				.dst = make_signed_domain(20, 19 + overlap),
+				.src = make_signed_domain(20, 19 + overlap),
+			},
+		}, also_signed), "non-strict, dst left touching src right, overlap=%d", overlap);
+
+		/*
+		 * 10 - dst - (19 + overlap)
+		 *                  20 - src - 30
+		 */
+
+		TEST_ASSERT_SUCCESS(verify_comparison((struct verify_instruction_param){
+			.tested_instruction = {
+				.code = (BPF_JMP | EBPF_JLT | BPF_X),
+			},
+			.pre.dst = make_signed_domain(10, 19 + overlap),
+			.pre.src = make_signed_domain(20, 30),
+			.post = overlap < 1 ? unreachable : (struct state){
+				.dst = make_signed_domain(20, 19 + overlap),
+				.src = make_signed_domain(20, 19 + overlap),
+			},
+		}, also_signed), "strict, dst right touching src left, overlap=%d", overlap);
+
+		TEST_ASSERT_SUCCESS(verify_comparison((struct verify_instruction_param){
+			.tested_instruction = {
+				.code = (BPF_JMP | EBPF_JLE | BPF_X),
+			},
+			.pre.dst = make_signed_domain(10, 19 + overlap),
+			.pre.src = make_signed_domain(20, 30),
+			.post = overlap <= 1 ? unreachable : (struct state){
+				.dst = make_singleton_domain(21),
+				.src = make_singleton_domain(20),
+			},
+		}, also_signed), "non-strict, dst right touching src left, overlap=%d", overlap);
+	}
+
+	return TEST_SUCCESS;
+}
+
+REGISTER_FAST_TEST(bpf_validate_jmp64_ordering_touching_autotest, NOHUGE_OK, ASAN_OK,
+	test_jmp64_ordering_touching);
 
 /* 64-bit load from heap (should be set to unknown). */
 static int
diff --git a/lib/bpf/bpf_validate.c b/lib/bpf/bpf_validate.c
index 2e535069fe4d..af084e36c8d0 100644
--- a/lib/bpf/bpf_validate.c
+++ b/lib/bpf/bpf_validate.c
@@ -19,6 +19,9 @@
 
 #define BPF_ARG_PTR_STACK RTE_BPF_ARG_RESERVED
 
+/* type containing no values (AKA "bottom", "never" etc)  */
+#define BPF_ARG_UNINHABITED ((enum rte_bpf_arg_type)(RTE_BPF_ARG_UNDEF - 1))
+
 struct bpf_reg_val {
 	struct rte_bpf_arg v;
 	uint64_t mask;
@@ -36,6 +39,8 @@ struct bpf_eval_state {
 	SLIST_ENTRY(bpf_eval_state) next; /* for @safe list traversal */
 	struct bpf_reg_val rv[EBPF_REG_NUM];
 	struct bpf_reg_val sv[MAX_BPF_STACK_SIZE / sizeof(uint64_t)];
+	/* flag set for branches determined to be dynamically unreachable */
+	bool unreachable;
 };
 
 SLIST_HEAD(bpf_evst_head, bpf_eval_state);
@@ -174,6 +179,9 @@ __rte_bpf_validate_can_access(const struct bpf_verifier *verifier,
 	struct value_set access_set;
 	uint32_t opsz;
 
+	if (st->unreachable)
+		return -ENOENT;
+
 	switch (BPF_CLASS(access->code)) {
 	case BPF_LDX:
 		rv = &st->rv[access->src_reg];
@@ -310,6 +318,10 @@ __rte_bpf_validate_may_jump(const struct bpf_verifier *verifier,
 	if (!may_jump_code_is_supported(jump->code))
 		return -ENOTSUP;
 
+	if (st->unreachable)
+		/* Set no bits since neither false nor true is possible. */
+		return 0;
+
 	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);
@@ -1521,40 +1533,68 @@ static void
 eval_jgt_jle(struct bpf_reg_val *trd, struct bpf_reg_val *trs,
 	struct bpf_reg_val *frd, struct bpf_reg_val *frs)
 {
-	frd->u.max = RTE_MIN(frd->u.max, frs->u.max);
-	frs->u.min = RTE_MAX(frs->u.min, frd->u.min);
-	trd->u.min = RTE_MAX(trd->u.min, trs->u.min + 1);
-	trs->u.max = RTE_MIN(trs->u.max, trd->u.max - 1);
+	if (frd->u.min <= frs->u.max) {
+		frd->u.max = RTE_MIN(frd->u.max, frs->u.max);
+		frs->u.min = RTE_MAX(frs->u.min, frd->u.min);
+	} else
+		frd->v.type = frs->v.type = BPF_ARG_UNINHABITED;
+
+	if (trs->u.min < trd->u.max) {
+		trd->u.min = RTE_MAX(trd->u.min, trs->u.min + 1);
+		trs->u.max = RTE_MIN(trs->u.max, trd->u.max - 1);
+	} else
+		trd->v.type = trs->v.type = BPF_ARG_UNINHABITED;
 }
 
 static void
 eval_jlt_jge(struct bpf_reg_val *trd, struct bpf_reg_val *trs,
 	struct bpf_reg_val *frd, struct bpf_reg_val *frs)
 {
-	frd->u.min = RTE_MAX(frd->u.min, frs->u.min);
-	frs->u.max = RTE_MIN(frs->u.max, frd->u.max);
-	trd->u.max = RTE_MIN(trd->u.max, trs->u.max - 1);
-	trs->u.min = RTE_MAX(trs->u.min, trd->u.min + 1);
+	if (frs->u.min <= frd->u.max) {
+		frd->u.min = RTE_MAX(frd->u.min, frs->u.min);
+		frs->u.max = RTE_MIN(frs->u.max, frd->u.max);
+	} else
+		frd->v.type = frs->v.type = BPF_ARG_UNINHABITED;
+
+	if (trd->u.min < trs->u.max) {
+		trd->u.max = RTE_MIN(trd->u.max, trs->u.max - 1);
+		trs->u.min = RTE_MAX(trs->u.min, trd->u.min + 1);
+	} else
+		trd->v.type = trs->v.type = BPF_ARG_UNINHABITED;
 }
 
 static void
 eval_jsgt_jsle(struct bpf_reg_val *trd, struct bpf_reg_val *trs,
 	struct bpf_reg_val *frd, struct bpf_reg_val *frs)
 {
-	frd->s.max = RTE_MIN(frd->s.max, frs->s.max);
-	frs->s.min = RTE_MAX(frs->s.min, frd->s.min);
-	trd->s.min = RTE_MAX(trd->s.min, trs->s.min + 1);
-	trs->s.max = RTE_MIN(trs->s.max, trd->s.max - 1);
+	if (frd->s.min <= frs->s.max) {
+		frd->s.max = RTE_MIN(frd->s.max, frs->s.max);
+		frs->s.min = RTE_MAX(frs->s.min, frd->s.min);
+	} else
+		frd->v.type = frs->v.type = BPF_ARG_UNINHABITED;
+
+	if (trs->s.min < trd->s.max) {
+		trd->s.min = RTE_MAX(trd->s.min, trs->s.min + 1);
+		trs->s.max = RTE_MIN(trs->s.max, trd->s.max - 1);
+	} else
+		trd->v.type = trs->v.type = BPF_ARG_UNINHABITED;
 }
 
 static void
 eval_jslt_jsge(struct bpf_reg_val *trd, struct bpf_reg_val *trs,
 	struct bpf_reg_val *frd, struct bpf_reg_val *frs)
 {
-	frd->s.min = RTE_MAX(frd->s.min, frs->s.min);
-	frs->s.max = RTE_MIN(frs->s.max, frd->s.max);
-	trd->s.max = RTE_MIN(trd->s.max, trs->s.max - 1);
-	trs->s.min = RTE_MAX(trs->s.min, trd->s.min + 1);
+	if (frs->s.min <= frd->s.max) {
+		frd->s.min = RTE_MAX(frd->s.min, frs->s.min);
+		frs->s.max = RTE_MIN(frs->s.max, frd->s.max);
+	} else
+		frd->v.type = frs->v.type = BPF_ARG_UNINHABITED;
+
+	if (trd->s.min < trs->s.max) {
+		trd->s.max = RTE_MIN(trd->s.max, trs->s.max - 1);
+		trs->s.min = RTE_MAX(trs->s.min, trd->s.min + 1);
+	} else
+		trd->v.type = trs->v.type = BPF_ARG_UNINHABITED;
 }
 
 static const char *
@@ -1609,6 +1649,14 @@ eval_jcc(struct bpf_verifier *bvf, const struct ebpf_insn *ins)
 	else if (op == EBPF_JSGE)
 		eval_jslt_jsge(frd, frs, trd, trs);
 
+	if (trd->v.type == BPF_ARG_UNINHABITED ||
+			trs->v.type == BPF_ARG_UNINHABITED)
+		tst->unreachable = true;
+
+	if (frd->v.type == BPF_ARG_UNINHABITED ||
+			frs->v.type == BPF_ARG_UNINHABITED)
+		fst->unreachable = true;
+
 	return NULL;
 }
 
@@ -2349,7 +2397,7 @@ set_edge_type(struct bpf_verifier *bvf, struct inst_node *node,
  * Depth-First Search (DFS) through previously constructed
  * Control Flow Graph (CFG).
  * Information collected at this path would be used later
- * to determine is there any loops, and/or unreachable instructions.
+ * to determine is there any loops, and/or statically unreachable instructions.
  * PREREQUISITE: there is at least one node.
  */
 static void
@@ -2397,7 +2445,7 @@ dfs(struct bpf_verifier *bvf)
 }
 
 /*
- * report unreachable instructions.
+ * report statically unreachable instructions.
  */
 static void
 log_unreachable(const struct bpf_verifier *bvf)
@@ -2970,13 +3018,21 @@ evaluate(struct bpf_verifier *bvf)
 				stats.nb_restore++;
 			}
 
+			if (bvf->evst->unreachable) {
+				rc = __rte_bpf_validate_debug_evaluate_step(
+					debug, get_node_idx(bvf, next),
+					RTE_BPF_VALIDATE_DEBUG_EVENT_BRANCH_UNREACHABLE);
+				if (rc < 0)
+					break;
+
+				next = NULL;
 			/*
 			 * for jcc targets: check did we already evaluated
 			 * that path and can it's evaluation be skipped that
 			 * time.
 			 */
-			if (node->nb_edge > 1 && prune_eval_state(bvf, node,
-					next) == 0) {
+			} else 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);
diff --git a/lib/bpf/rte_bpf_validate_debug.h b/lib/bpf/rte_bpf_validate_debug.h
index 89bf587f0211..f30fa926f10a 100644
--- a/lib/bpf/rte_bpf_validate_debug.h
+++ b/lib/bpf/rte_bpf_validate_debug.h
@@ -49,6 +49,8 @@ enum rte_bpf_validate_debug_event {
 	RTE_BPF_VALIDATE_DEBUG_EVENT_BRANCH_PRUNE,
 	/* End of branch verification, after the last verified instruction. */
 	RTE_BPF_VALIDATE_DEBUG_EVENT_BRANCH_RETURN,
+	/* Pruning branch as dynamically unreachable. */
+	RTE_BPF_VALIDATE_DEBUG_EVENT_BRANCH_UNREACHABLE,
 	/* Number of valid event values. */
 	RTE_BPF_VALIDATE_DEBUG_EVENT_END,
 };
-- 
2.43.0


^ permalink raw reply related

* [PATCH v4 16/24] bpf/validate: fix BPF_JMP source range calculation
From: Marat Khalili @ 2026-06-23 14:32 UTC (permalink / raw)
  To: Konstantin Ananyev; +Cc: dev, stable, Claudia Cauli
In-Reply-To: <20260623143215.95318-1-marat.khalili@huawei.com>

All two-register ordering comparison functions (`eval_jgt_jle`,
`eval_jlt_jge`, `eval_jsgt_jsle`, `eval_jslt_jsge`) were updating only
the destination register value set but not the source register one. For
instance, instruction `jgt r2, r3` should be exactly equivalent to `jlt
r3, r2`, but previously the former only updated the possible values of
r2 while the latter only updated possible values of r3. Thus the
estimate for source register was conservative and could cause false
positives.

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

    Tested program:
        0:  mov r0, #0x0
        1:  mov r2, #0x28
        2:  ldxdw r3, [r1 + 0]
        3:  jlt r3, #0x14, L11
        4:  jgt r3, #0x3c, L11
        5:  jslt r3, #0x14, L11
        6:  jsgt r3, #0x3c, L11
        7:  jgt r2, r3, L10  ; tested instruction
        8:  mov r0, #0x1
        9:  exit
       10:  mov r0, #0x2
       11:  exit
    Pre-state:
       r2:  40
       r3:  20..60
    ...
    Jump-state:
       r2:  40
       r3:  20..60

If tested instruction jumped from step 7 to step 10 validator expects r3
to contain values up to 60, for example 55, however for this value jump
condition r2 > r3 will never be satisfied since r2 is known to equal 40,
and thus execution would always continue to step 8 instead of jumping.

Add missing source register values update.

Introduce test harness for verifying all equivalent variations of a
comparison instruction. Add tests for all cases where both code branches
are reachable (unreachable branches will be covered by subsequent
commits).

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>
---
 app/test/test_bpf_validate.c | 394 +++++++++++++++++++++++++++++++----
 lib/bpf/bpf_validate.c       |   8 +
 2 files changed, 358 insertions(+), 44 deletions(-)

diff --git a/app/test/test_bpf_validate.c b/app/test/test_bpf_validate.c
index acc238f7d324..15ccf4f6a573 100644
--- a/app/test/test_bpf_validate.c
+++ b/app/test/test_bpf_validate.c
@@ -32,6 +32,31 @@ RTE_LOG_REGISTER(test_bpf_validate_logtype, test.bpf_validate, NOTICE);
 #define REGISTER_FORMAT_BUFFER_SIZE 256
 #define DISASSEMBLY_FORMAT_BUFFER_SIZE 64
 
+#define COMPARISON_INDEX_IMMEDIATE RTE_BIT32(0)
+#define COMPARISON_INDEX_GREATER   RTE_BIT32(1)
+#define COMPARISON_INDEX_INCLUSIVE RTE_BIT32(2)
+#define COMPARISON_INDEX_SIGNED    RTE_BIT32(3)
+
+/* List comparison opcodes to make their index bits match constants above.  */
+static const uint8_t comparisons_opcode[] = {
+	(BPF_JMP | EBPF_JLT  | BPF_X),
+	(BPF_JMP | EBPF_JLT  | BPF_K),
+	(BPF_JMP |  BPF_JGT  | BPF_X),
+	(BPF_JMP |  BPF_JGT  | BPF_K),
+	(BPF_JMP | EBPF_JLE  | BPF_X),
+	(BPF_JMP | EBPF_JLE  | BPF_K),
+	(BPF_JMP |  BPF_JGE  | BPF_X),
+	(BPF_JMP |  BPF_JGE  | BPF_K),
+	(BPF_JMP | EBPF_JSLT | BPF_X),
+	(BPF_JMP | EBPF_JSLT | BPF_K),
+	(BPF_JMP | EBPF_JSGT | BPF_X),
+	(BPF_JMP | EBPF_JSGT | BPF_K),
+	(BPF_JMP | EBPF_JSLE | BPF_X),
+	(BPF_JMP | EBPF_JSLE | BPF_K),
+	(BPF_JMP | EBPF_JSGE | BPF_X),
+	(BPF_JMP | EBPF_JSGE | BPF_K),
+};
+
 /* Interval bounded by two signed values, inclusive; min <= max. */
 struct signed_interval {
 	int64_t min;
@@ -1044,6 +1069,206 @@ verify_instruction(struct verify_instruction_param prm)
 	return rc;
 }
 
+static int
+opcode_comparison_index(uint8_t opcode)
+{
+	for (int index = 0; index != RTE_DIM(comparisons_opcode); ++index)
+		if (comparisons_opcode[index] == opcode)
+			return index;
+	TEST_LOG_LINE(ERR, "Unsupported or not a comparison opcode: %hhx", opcode);
+	RTE_VERIFY(false);
+}
+
+/* Change two-register comparison verification to immediate one. */
+static bool
+make_comparison_immediate(struct verify_instruction_param *prm)
+{
+	int comparison_index = opcode_comparison_index(prm->tested_instruction.code);
+	const int64_t value = prm->pre.src.s.min;
+
+	if ((comparison_index & COMPARISON_INDEX_IMMEDIATE) != 0) {
+		TEST_LOG_LINE(ERR, "Comparison %hhx is already immediate.",
+			prm->tested_instruction.code);
+		RTE_VERIFY(false);
+	}
+
+	if (!domain_is_singleton(&prm->pre.src) || !domain_is_singleton(&prm->post.src) ||
+			!domain_is_singleton(&prm->jump.src)) {
+		TEST_LOG_LINE(DEBUG, "Cannot make immediate out of a non-singleton domain.");
+		return false;
+	}
+	if (prm->pre.src.is_pointer || prm->post.src.is_pointer || prm->jump.src.is_pointer) {
+		TEST_LOG_LINE(DEBUG, "Cannot make immediate out of a pointer.");
+		return false;
+	}
+	if (prm->post.src.s.min != value || prm->jump.src.s.min != value) {
+		TEST_LOG_LINE(DEBUG, "Cannot make immediate if the value changes.");
+		return false;
+	}
+	if (!fits_in_imm32(value)) {
+		TEST_LOG_LINE(ERR, "Cannot make immediate unless value fits in int32.");
+		return false;
+	}
+
+	comparison_index |= COMPARISON_INDEX_IMMEDIATE;
+	prm->tested_instruction.code = comparisons_opcode[comparison_index];
+	prm->tested_instruction.imm = value;
+
+	RTE_VERIFY(prm->pre.src.is_defined);
+	prm->pre.src.is_defined = false;
+
+	if (!prm->post.is_unreachable) {
+		RTE_VERIFY(prm->post.src.is_defined);
+		prm->post.src.is_defined = false;
+	}
+
+	if (!prm->jump.is_unreachable) {
+		RTE_VERIFY(prm->jump.src.is_defined);
+		prm->jump.src.is_defined = false;
+	}
+
+	return true;
+}
+
+/* Change immediate comparison verification to two-register one. */
+static void
+make_comparison_two_register(struct verify_instruction_param *prm)
+{
+	int comparison_index = opcode_comparison_index(prm->tested_instruction.code);
+	const int64_t value = prm->tested_instruction.imm;
+
+	if ((comparison_index & COMPARISON_INDEX_IMMEDIATE) == 0) {
+		TEST_LOG_LINE(ERR, "Comparison %hhx is already two-register.",
+			prm->tested_instruction.code);
+		RTE_VERIFY(false);
+	}
+
+	comparison_index &= ~COMPARISON_INDEX_IMMEDIATE;
+	prm->tested_instruction.code = comparisons_opcode[comparison_index];
+	prm->tested_instruction.imm = 0;
+
+	RTE_VERIFY(!prm->pre.src.is_defined);
+	prm->pre.src = make_singleton_domain(value);
+
+	if (!prm->post.is_unreachable) {
+		RTE_VERIFY(!prm->post.src.is_defined);
+		prm->post.src = prm->pre.src;
+	}
+
+	if (!prm->jump.is_unreachable) {
+		RTE_VERIFY(!prm->jump.src.is_defined);
+		prm->jump.src = prm->pre.src;
+	}
+}
+
+/* Change comparison verification to complement (negated result) one. */
+static void
+make_comparison_complement(struct verify_instruction_param *prm)
+{
+	int comparison_index = opcode_comparison_index(prm->tested_instruction.code);
+	comparison_index ^= COMPARISON_INDEX_GREATER | COMPARISON_INDEX_INCLUSIVE;
+	prm->tested_instruction.code = comparisons_opcode[comparison_index];
+	RTE_SWAP(prm->post, prm->jump);
+}
+
+/* Change comparison verification to converse (swapped operands) one. */
+static void
+make_comparison_converse(struct verify_instruction_param *prm)
+{
+	int comparison_index = opcode_comparison_index(prm->tested_instruction.code);
+	comparison_index ^= COMPARISON_INDEX_GREATER;
+	prm->tested_instruction.code = comparisons_opcode[comparison_index];
+	RTE_SWAP(prm->pre.dst, prm->pre.src);
+	RTE_SWAP(prm->post.dst, prm->post.src);
+	RTE_SWAP(prm->jump.dst, prm->jump.src);
+}
+
+/* Change signed comparison verification to unsigned one. */
+static void
+make_comparison_signed(struct verify_instruction_param *prm)
+{
+	int comparison_index = opcode_comparison_index(prm->tested_instruction.code);
+	if ((comparison_index & COMPARISON_INDEX_SIGNED) != 0) {
+		TEST_LOG_LINE(ERR, "Comparison %hhx is already signed.",
+			prm->tested_instruction.code);
+		RTE_VERIFY(false);
+	}
+	comparison_index |= COMPARISON_INDEX_SIGNED;
+	prm->tested_instruction.code = comparisons_opcode[comparison_index];
+}
+
+/* Verify specified two-register comparison and, if possible, immediate one. */
+static int
+verify_comparison_subcase(struct verify_instruction_param prm)
+{
+	TEST_ASSERT_SUCCESS(verify_instruction(prm), "two-register version check");
+
+	if (make_comparison_immediate(&prm))
+		TEST_ASSERT_SUCCESS(verify_instruction(prm), "immediate version check");
+
+	return TEST_SUCCESS;
+}
+
+/*
+ * Verify comparison instruction validation behaviour.
+ *
+ * Call `verify_instruction` for all valid variations of the instruction.
+ *
+ * For instance, `jgt r2, r3` verifies:
+ * * `jgt r2, r3`;
+ * * `jlt r3, r2` src and dst swapped with each other;
+ * * `jle r2, r3` with post and jump domains swapped with each other;
+ * * `jge r3, r2` with all corresponding swaps;
+ * * immediate versions of everything above where possible,
+ *   that is, register on the right is an int32 scalar singleton;
+ * * signed versions of everything above if `also_signed` is true;
+ *
+ * Regardless if passed instruction compares with immediate or singleton src
+ * both cases are generated and tested.
+ */
+static int
+verify_comparison(struct verify_instruction_param prm, bool also_signed)
+{
+	fill_verify_instruction_defaults(&prm);
+
+	if (!prm.pre.src.is_defined)
+		/* Convert from immediate form to simplify further logic. */
+		make_comparison_two_register(&prm);
+
+	/* All reachable domains must be defined by this point. */
+	RTE_VERIFY(prm.pre.dst.is_defined);
+	RTE_VERIFY(prm.pre.src.is_defined);
+	if (!prm.post.is_unreachable) {
+		RTE_VERIFY(prm.post.dst.is_defined);
+		RTE_VERIFY(prm.post.src.is_defined);
+	}
+	if (!prm.jump.is_unreachable) {
+		RTE_VERIFY(prm.jump.dst.is_defined);
+		RTE_VERIFY(prm.jump.src.is_defined);
+	}
+
+	for (int make_signed = 0; make_signed <= also_signed; ++make_signed) {
+		if (make_signed)
+			make_comparison_signed(&prm);
+
+		for (int complement = false; complement <= true; ++complement) {
+
+			for (int converse = false; converse <= true; ++converse) {
+
+				TEST_ASSERT_SUCCESS(verify_comparison_subcase(prm),
+					"make_signed=%d, complement=%d, converse=%d",
+					make_signed, complement, converse);
+
+				make_comparison_converse(&prm);
+			}
+
+			make_comparison_complement(&prm);
+		}
+	}
+
+	return TEST_SUCCESS;
+}
+
 
 /* TESTS FOR SPECIFIC INSTRUCTIONS */
 
@@ -1485,31 +1710,69 @@ test_jmp64_jslt_x(void)
 REGISTER_FAST_TEST(bpf_validate_jmp64_jslt_x_autotest, NOHUGE_OK, ASAN_OK,
 	test_jmp64_jslt_x);
 
-/* Jump on ordering relationship with narrower range. */
+/* Jump on ordering comparisons between two ranges. */
 static int
-test_jmp64_jxx_x_ordering_narrower(void)
+test_jmp64_ordering_ranges(void)
 {
-	TEST_ASSERT_SUCCESS(verify_instruction((struct verify_instruction_param){
+	/* All ranges used are valid for both signed and unsigned comparisons. */
+	const bool also_signed = true;
+
+	/*
+	 *     20 ---- dst ---- 60
+	 * 10 -- src -- 40
+	 */
+
+	TEST_ASSERT_SUCCESS(verify_comparison((struct verify_instruction_param){
 		.tested_instruction = {
-			.code = (BPF_JMP | BPF_JGT | BPF_X),
+			.code = (BPF_JMP | EBPF_JLT | BPF_X),
 		},
 		.pre.dst = make_signed_domain(20, 60),
-		.pre.src = make_signed_domain(30, 50),
-		.post.dst = make_signed_domain(20, 50),
-		.jump.dst = make_signed_domain(31, 60),
-	}), "(BPF_JMP | BPF_JGT | BPF_X) check");
+		.pre.src = make_signed_domain(10, 40),
+		.jump.dst = make_signed_domain(20, 39),
+		.jump.src = make_signed_domain(21, 40),
+	}, also_signed), "strict, dst range weakly greater than src range");
 
-	TEST_ASSERT_SUCCESS(verify_instruction((struct verify_instruction_param){
+	TEST_ASSERT_SUCCESS(verify_comparison((struct verify_instruction_param){
 		.tested_instruction = {
-			.code = (BPF_JMP | BPF_JGE | BPF_X),
+			.code = (BPF_JMP | EBPF_JLE | BPF_X),
 		},
 		.pre.dst = make_signed_domain(20, 60),
-		.pre.src = make_signed_domain(30, 50),
-		.post.dst = make_signed_domain(20, 49),
-		.jump.dst = make_signed_domain(30, 60),
-	}), "(BPF_JMP | BPF_JGE | BPF_X) check");
+		.pre.src = make_signed_domain(10, 40),
+		.jump.dst = make_signed_domain(20, 40),
+		.jump.src = make_signed_domain(20, 40),
+	}, also_signed), "non-strict, dst range weakly greater than src range");
 
-	TEST_ASSERT_SUCCESS(verify_instruction((struct verify_instruction_param){
+	/*
+	 *     20 ---- dst ---- 60
+	 * 10 -------- src -------- 70
+	 */
+
+	TEST_ASSERT_SUCCESS(verify_comparison((struct verify_instruction_param){
+		.tested_instruction = {
+			.code = (BPF_JMP | EBPF_JLT | BPF_X),
+		},
+		.pre.dst = make_signed_domain(20, 60),
+		.pre.src = make_signed_domain(10, 70),
+		.post.src = make_signed_domain(10, 60),
+		.jump.src = make_signed_domain(21, 70),
+	}, also_signed), "strict, dst range included in src range");
+
+	TEST_ASSERT_SUCCESS(verify_comparison((struct verify_instruction_param){
+		.tested_instruction = {
+			.code = (BPF_JMP | EBPF_JLE | BPF_X),
+		},
+		.pre.dst = make_signed_domain(20, 60),
+		.pre.src = make_signed_domain(10, 70),
+		.post.src = make_signed_domain(10, 59),
+		.jump.src = make_signed_domain(20, 70),
+	}, also_signed), "non-strict, dst range included in src range");
+
+	/*
+	 *     20 ---- dst ---- 60
+	 *        30 - src - 50
+	 */
+
+	TEST_ASSERT_SUCCESS(verify_comparison((struct verify_instruction_param){
 		.tested_instruction = {
 			.code = (BPF_JMP | EBPF_JLT | BPF_X),
 		},
@@ -1517,9 +1780,9 @@ test_jmp64_jxx_x_ordering_narrower(void)
 		.pre.src = make_signed_domain(30, 50),
 		.post.dst = make_signed_domain(30, 60),
 		.jump.dst = make_signed_domain(20, 49),
-	}), "(BPF_JMP | EBPF_JLT | BPF_X) check");
+	}, also_signed), "strict, dst range includes src range");
 
-	TEST_ASSERT_SUCCESS(verify_instruction((struct verify_instruction_param){
+	TEST_ASSERT_SUCCESS(verify_comparison((struct verify_instruction_param){
 		.tested_instruction = {
 			.code = (BPF_JMP | EBPF_JLE | BPF_X),
 		},
@@ -1527,53 +1790,96 @@ test_jmp64_jxx_x_ordering_narrower(void)
 		.pre.src = make_signed_domain(30, 50),
 		.post.dst = make_signed_domain(31, 60),
 		.jump.dst = make_signed_domain(20, 50),
-	}), "(BPF_JMP | EBPF_JLE | BPF_X) check");
+	}, also_signed), "non-strict, dst range includes src range");
 
-	TEST_ASSERT_SUCCESS(verify_instruction((struct verify_instruction_param){
+	/*
+	 *     20 ---- dst ---- 60
+	 *             40 -- src -- 70
+	 */
+
+	TEST_ASSERT_SUCCESS(verify_comparison((struct verify_instruction_param){
 		.tested_instruction = {
-			.code = (BPF_JMP | EBPF_JSGT | BPF_X),
+			.code = (BPF_JMP | EBPF_JLT | BPF_X),
 		},
 		.pre.dst = make_signed_domain(20, 60),
-		.pre.src = make_signed_domain(30, 50),
-		.post.dst = make_signed_domain(20, 50),
-		.jump.dst = make_signed_domain(31, 60),
-	}), "(BPF_JMP | EBPF_JSGT | BPF_X) check");
+		.pre.src = make_signed_domain(40, 70),
+		.post.dst = make_signed_domain(40, 60),
+		.post.src = make_signed_domain(40, 60),
+	}, also_signed), "strict, dst range weakly less than src range");
 
-	TEST_ASSERT_SUCCESS(verify_instruction((struct verify_instruction_param){
+	TEST_ASSERT_SUCCESS(verify_comparison((struct verify_instruction_param){
 		.tested_instruction = {
-			.code = (BPF_JMP | EBPF_JSGE | BPF_X),
+			.code = (BPF_JMP | EBPF_JLE | BPF_X),
 		},
 		.pre.dst = make_signed_domain(20, 60),
-		.pre.src = make_signed_domain(30, 50),
-		.post.dst = make_signed_domain(20, 49),
-		.jump.dst = make_signed_domain(30, 60),
-	}), "(BPF_JMP | EBPF_JSGE | BPF_X) check");
+		.pre.src = make_signed_domain(40, 70),
+		.post.dst = make_signed_domain(41, 60),
+		.post.src = make_signed_domain(40, 59),
+	}, also_signed), "non-strict, dst range weakly less than src range");
 
-	TEST_ASSERT_SUCCESS(verify_instruction((struct verify_instruction_param){
+	return TEST_SUCCESS;
+}
+
+REGISTER_FAST_TEST(bpf_validate_jmp64_ordering_ranges_autotest, NOHUGE_OK, ASAN_OK,
+	test_jmp64_ordering_ranges);
+
+/* Jump on ordering comparisons with singleton. */
+static int
+test_jmp64_ordering_singleton(void)
+{
+	/* All ranges used are valid for both signed and unsigned comparisons. */
+	const bool also_signed = true;
+
+	/*
+	 *     20 ---- dst ---- 60
+	 *             imm
+	 */
+
+	TEST_ASSERT_SUCCESS(verify_comparison((struct verify_instruction_param){
 		.tested_instruction = {
-			.code = (BPF_JMP | EBPF_JSLT | BPF_X),
+			.code = (BPF_JMP | EBPF_JLT | BPF_K),
+			.imm = 40,
 		},
 		.pre.dst = make_signed_domain(20, 60),
-		.pre.src = make_signed_domain(30, 50),
-		.post.dst = make_signed_domain(30, 60),
-		.jump.dst = make_signed_domain(20, 49),
-	}), "(BPF_JMP | EBPF_JSLT | BPF_X) check");
+		.post.dst = make_signed_domain(40, 60),
+		.jump.dst = make_signed_domain(20, 39),
+	}, also_signed), "(BPF_JMP | EBPF_JLT | BPF_K) check");
 
-	TEST_ASSERT_SUCCESS(verify_instruction((struct verify_instruction_param){
+	TEST_ASSERT_SUCCESS(verify_comparison((struct verify_instruction_param){
 		.tested_instruction = {
-			.code = (BPF_JMP | EBPF_JSLE | BPF_X),
+			.code = (BPF_JMP | BPF_JGT | BPF_K),
+			.imm = 40,
 		},
 		.pre.dst = make_signed_domain(20, 60),
-		.pre.src = make_signed_domain(30, 50),
-		.post.dst = make_signed_domain(31, 60),
-		.jump.dst = make_signed_domain(20, 50),
-	}), "(BPF_JMP | EBPF_JSLE | BPF_X) check");
+		.post.dst = make_signed_domain(20, 40),
+		.jump.dst = make_signed_domain(41, 60),
+	}, also_signed), "(BPF_JMP | EBPF_JGT | BPF_K) check");
+
+	TEST_ASSERT_SUCCESS(verify_comparison((struct verify_instruction_param){
+		.tested_instruction = {
+			.code = (BPF_JMP | EBPF_JLE | BPF_K),
+			.imm = 40,
+		},
+		.pre.dst = make_signed_domain(20, 60),
+		.post.dst = make_signed_domain(41, 60),
+		.jump.dst = make_signed_domain(20, 40),
+	}, also_signed), "(BPF_JMP | EBPF_JLE | BPF_K) check");
+
+	TEST_ASSERT_SUCCESS(verify_comparison((struct verify_instruction_param){
+		.tested_instruction = {
+			.code = (BPF_JMP | BPF_JGE | BPF_K),
+			.imm = 40,
+		},
+		.pre.dst = make_signed_domain(20, 60),
+		.post.dst = make_signed_domain(20, 39),
+		.jump.dst = make_signed_domain(40, 60),
+	}, also_signed), "(BPF_JMP | EBPF_JGE | BPF_K) check");
 
 	return TEST_SUCCESS;
 }
 
-REGISTER_FAST_TEST(bpf_validate_jmp64_jxx_x_ordering_narrower_autotest, NOHUGE_OK, ASAN_OK,
-	test_jmp64_jxx_x_ordering_narrower);
+REGISTER_FAST_TEST(bpf_validate_jmp64_ordering_singleton_autotest, NOHUGE_OK, ASAN_OK,
+	test_jmp64_ordering_singleton);
 
 /* 64-bit load from heap (should be set to unknown). */
 static int
diff --git a/lib/bpf/bpf_validate.c b/lib/bpf/bpf_validate.c
index 0578029dccbb..2e535069fe4d 100644
--- a/lib/bpf/bpf_validate.c
+++ b/lib/bpf/bpf_validate.c
@@ -1522,7 +1522,9 @@ eval_jgt_jle(struct bpf_reg_val *trd, struct bpf_reg_val *trs,
 	struct bpf_reg_val *frd, struct bpf_reg_val *frs)
 {
 	frd->u.max = RTE_MIN(frd->u.max, frs->u.max);
+	frs->u.min = RTE_MAX(frs->u.min, frd->u.min);
 	trd->u.min = RTE_MAX(trd->u.min, trs->u.min + 1);
+	trs->u.max = RTE_MIN(trs->u.max, trd->u.max - 1);
 }
 
 static void
@@ -1530,7 +1532,9 @@ eval_jlt_jge(struct bpf_reg_val *trd, struct bpf_reg_val *trs,
 	struct bpf_reg_val *frd, struct bpf_reg_val *frs)
 {
 	frd->u.min = RTE_MAX(frd->u.min, frs->u.min);
+	frs->u.max = RTE_MIN(frs->u.max, frd->u.max);
 	trd->u.max = RTE_MIN(trd->u.max, trs->u.max - 1);
+	trs->u.min = RTE_MAX(trs->u.min, trd->u.min + 1);
 }
 
 static void
@@ -1538,7 +1542,9 @@ eval_jsgt_jsle(struct bpf_reg_val *trd, struct bpf_reg_val *trs,
 	struct bpf_reg_val *frd, struct bpf_reg_val *frs)
 {
 	frd->s.max = RTE_MIN(frd->s.max, frs->s.max);
+	frs->s.min = RTE_MAX(frs->s.min, frd->s.min);
 	trd->s.min = RTE_MAX(trd->s.min, trs->s.min + 1);
+	trs->s.max = RTE_MIN(trs->s.max, trd->s.max - 1);
 }
 
 static void
@@ -1546,7 +1552,9 @@ eval_jslt_jsge(struct bpf_reg_val *trd, struct bpf_reg_val *trs,
 	struct bpf_reg_val *frd, struct bpf_reg_val *frs)
 {
 	frd->s.min = RTE_MAX(frd->s.min, frs->s.min);
+	frs->s.max = RTE_MIN(frs->s.max, frd->s.max);
 	trd->s.max = RTE_MIN(trd->s.max, trs->s.max - 1);
+	trs->s.min = RTE_MAX(trs->s.min, trd->s.min + 1);
 }
 
 static const char *
-- 
2.43.0


^ permalink raw reply related

* [PATCH v4 15/24] bpf/validate: fix BPF_JGT/EBPF_JSGT no-jump max
From: Marat Khalili @ 2026-06-23 14:32 UTC (permalink / raw)
  To: Konstantin Ananyev; +Cc: dev, stable, Claudia Cauli
In-Reply-To: <20260623143215.95318-1-marat.khalili@huawei.com>

Functions `eval_jgt_jle` and `eval_jsgt_jsle` reduced range maximum for
BPF_JGT and EBPF_JSGT instructions in the no-jump case to the minimum of
src register instead of the maximum, producing more conservative
estimate that could cause false positives.

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

    Tested program:
        0:  mov r0, #0x0
        1:  ldxdw r2, [r1 + 0]
        2:  jlt r2, #0x14, L15
        3:  jgt r2, #0x3c, L15
        4:  jslt r2, #0x14, L15
        5:  jsgt r2, #0x3c, L15
        6:  ldxdw r3, [r1 + 8]
        7:  jlt r3, #0x1e, L15
        8:  jgt r3, #0x32, L15
        9:  jslt r3, #0x1e, L15
       10:  jsgt r3, #0x32, L15
       11:  jgt r2, r3, L14  ; tested instruction
       12:  mov r0, #0x1
       13:  exit
       14:  mov r0, #0x2
       15:  exit
    Pre-state:
       r2:  20..60
       r3:  30..50
    Post-state:
       r2:  20..60 INTERSECT 0x14..0x1e (!)

Immediately after the tested instruction on step 12 validator expects r2
to contain values up to 60, for example 55, however for this value jump
condition r2 > r3 on step 11 would be always satisfied since r3 is known
to not exceed 50, and thus execution will always jump to step 14 instead
of continuing to step 12.

Fix range calculation, add tests for cases where range of src register
values is a strict subset of dst. Other cases will be covered in the
subsequent commits.

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>
---
 app/test/test_bpf_validate.c | 90 ++++++++++++++++++++++++++++++++++++
 lib/bpf/bpf_validate.c       |  4 +-
 2 files changed, 92 insertions(+), 2 deletions(-)

diff --git a/app/test/test_bpf_validate.c b/app/test/test_bpf_validate.c
index 15cdc83f4f14..acc238f7d324 100644
--- a/app/test/test_bpf_validate.c
+++ b/app/test/test_bpf_validate.c
@@ -1485,6 +1485,96 @@ test_jmp64_jslt_x(void)
 REGISTER_FAST_TEST(bpf_validate_jmp64_jslt_x_autotest, NOHUGE_OK, ASAN_OK,
 	test_jmp64_jslt_x);
 
+/* Jump on ordering relationship with narrower range. */
+static int
+test_jmp64_jxx_x_ordering_narrower(void)
+{
+	TEST_ASSERT_SUCCESS(verify_instruction((struct verify_instruction_param){
+		.tested_instruction = {
+			.code = (BPF_JMP | BPF_JGT | BPF_X),
+		},
+		.pre.dst = make_signed_domain(20, 60),
+		.pre.src = make_signed_domain(30, 50),
+		.post.dst = make_signed_domain(20, 50),
+		.jump.dst = make_signed_domain(31, 60),
+	}), "(BPF_JMP | BPF_JGT | BPF_X) check");
+
+	TEST_ASSERT_SUCCESS(verify_instruction((struct verify_instruction_param){
+		.tested_instruction = {
+			.code = (BPF_JMP | BPF_JGE | BPF_X),
+		},
+		.pre.dst = make_signed_domain(20, 60),
+		.pre.src = make_signed_domain(30, 50),
+		.post.dst = make_signed_domain(20, 49),
+		.jump.dst = make_signed_domain(30, 60),
+	}), "(BPF_JMP | BPF_JGE | BPF_X) check");
+
+	TEST_ASSERT_SUCCESS(verify_instruction((struct verify_instruction_param){
+		.tested_instruction = {
+			.code = (BPF_JMP | EBPF_JLT | BPF_X),
+		},
+		.pre.dst = make_signed_domain(20, 60),
+		.pre.src = make_signed_domain(30, 50),
+		.post.dst = make_signed_domain(30, 60),
+		.jump.dst = make_signed_domain(20, 49),
+	}), "(BPF_JMP | EBPF_JLT | BPF_X) check");
+
+	TEST_ASSERT_SUCCESS(verify_instruction((struct verify_instruction_param){
+		.tested_instruction = {
+			.code = (BPF_JMP | EBPF_JLE | BPF_X),
+		},
+		.pre.dst = make_signed_domain(20, 60),
+		.pre.src = make_signed_domain(30, 50),
+		.post.dst = make_signed_domain(31, 60),
+		.jump.dst = make_signed_domain(20, 50),
+	}), "(BPF_JMP | EBPF_JLE | BPF_X) check");
+
+	TEST_ASSERT_SUCCESS(verify_instruction((struct verify_instruction_param){
+		.tested_instruction = {
+			.code = (BPF_JMP | EBPF_JSGT | BPF_X),
+		},
+		.pre.dst = make_signed_domain(20, 60),
+		.pre.src = make_signed_domain(30, 50),
+		.post.dst = make_signed_domain(20, 50),
+		.jump.dst = make_signed_domain(31, 60),
+	}), "(BPF_JMP | EBPF_JSGT | BPF_X) check");
+
+	TEST_ASSERT_SUCCESS(verify_instruction((struct verify_instruction_param){
+		.tested_instruction = {
+			.code = (BPF_JMP | EBPF_JSGE | BPF_X),
+		},
+		.pre.dst = make_signed_domain(20, 60),
+		.pre.src = make_signed_domain(30, 50),
+		.post.dst = make_signed_domain(20, 49),
+		.jump.dst = make_signed_domain(30, 60),
+	}), "(BPF_JMP | EBPF_JSGE | BPF_X) check");
+
+	TEST_ASSERT_SUCCESS(verify_instruction((struct verify_instruction_param){
+		.tested_instruction = {
+			.code = (BPF_JMP | EBPF_JSLT | BPF_X),
+		},
+		.pre.dst = make_signed_domain(20, 60),
+		.pre.src = make_signed_domain(30, 50),
+		.post.dst = make_signed_domain(30, 60),
+		.jump.dst = make_signed_domain(20, 49),
+	}), "(BPF_JMP | EBPF_JSLT | BPF_X) check");
+
+	TEST_ASSERT_SUCCESS(verify_instruction((struct verify_instruction_param){
+		.tested_instruction = {
+			.code = (BPF_JMP | EBPF_JSLE | BPF_X),
+		},
+		.pre.dst = make_signed_domain(20, 60),
+		.pre.src = make_signed_domain(30, 50),
+		.post.dst = make_signed_domain(31, 60),
+		.jump.dst = make_signed_domain(20, 50),
+	}), "(BPF_JMP | EBPF_JSLE | BPF_X) check");
+
+	return TEST_SUCCESS;
+}
+
+REGISTER_FAST_TEST(bpf_validate_jmp64_jxx_x_ordering_narrower_autotest, NOHUGE_OK, ASAN_OK,
+	test_jmp64_jxx_x_ordering_narrower);
+
 /* 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 2b73e3628881..0578029dccbb 100644
--- a/lib/bpf/bpf_validate.c
+++ b/lib/bpf/bpf_validate.c
@@ -1521,7 +1521,7 @@ static void
 eval_jgt_jle(struct bpf_reg_val *trd, struct bpf_reg_val *trs,
 	struct bpf_reg_val *frd, struct bpf_reg_val *frs)
 {
-	frd->u.max = RTE_MIN(frd->u.max, frs->u.min);
+	frd->u.max = RTE_MIN(frd->u.max, frs->u.max);
 	trd->u.min = RTE_MAX(trd->u.min, trs->u.min + 1);
 }
 
@@ -1537,7 +1537,7 @@ static void
 eval_jsgt_jsle(struct bpf_reg_val *trd, struct bpf_reg_val *trs,
 	struct bpf_reg_val *frd, struct bpf_reg_val *frs)
 {
-	frd->s.max = RTE_MIN(frd->s.max, frs->s.min);
+	frd->s.max = RTE_MIN(frd->s.max, frs->s.max);
 	trd->s.min = RTE_MAX(trd->s.min, trs->s.min + 1);
 }
 
-- 
2.43.0


^ permalink raw reply related

* [PATCH v4 14/24] bpf/validate: fix BPF_MUL signed overflow UB
From: Marat Khalili @ 2026-06-23 14:32 UTC (permalink / raw)
  To: Konstantin Ananyev; +Cc: dev, stable, Claudia Cauli
In-Reply-To: <20260623143215.95318-1-marat.khalili@huawei.com>

Function `eval_mul` triggered signed overflow for large constants.

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

    Tested program:
        0:  mov r0, #0x0
        1:  lddw r2, #0x9876543210
        3:  mul r2, #0x12345678  ; tested instruction
        4:  mov r0, #0x1
        5:  exit

With sanitizer the following diagnostic is generated:

    lib/bpf/bpf_validate.c:1032:26: runtime error: signed integer
    overflow: 654820258320 * 305419896 cannot be represented in type
    'long int'
        #0 0x000002746bfd in eval_mul lib/bpf/bpf_validate.c:1032
        #1 0x00000274b6ac in eval_alu lib/bpf/bpf_validate.c:1260
        #2 0x00000275c526 in evaluate lib/bpf/bpf_validate.c:3174
        ...

    SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior
    lib/bpf/bpf_validate.c:1032:26

Multiply constants as unsigned which will produce mathematically correct
result in two's complement representation, add test.

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>
---
 app/test/test_bpf_validate.c | 17 +++++++++++++++++
 lib/bpf/bpf_validate.c       |  4 ++--
 2 files changed, 19 insertions(+), 2 deletions(-)

diff --git a/app/test/test_bpf_validate.c b/app/test/test_bpf_validate.c
index ecebfd48dd52..15cdc83f4f14 100644
--- a/app/test/test_bpf_validate.c
+++ b/app/test/test_bpf_validate.c
@@ -1289,6 +1289,23 @@ test_alu64_div_mod_overflow(void)
 REGISTER_FAST_TEST(bpf_validate_alu64_div_mod_overflow_autotest, NOHUGE_OK, ASAN_OK,
 	test_alu64_div_mod_overflow);
 
+/* 64-bit multiplication of constant and immediate with overflow. */
+static int
+test_alu64_mul_k_overflow(void)
+{
+	return verify_instruction((struct verify_instruction_param){
+		.tested_instruction = {
+			.code = (EBPF_ALU64 | BPF_MUL | BPF_K),
+			.imm = 0x12345678,
+		},
+		.pre.dst = make_singleton_domain(0x9876543210),
+		.post.dst = make_singleton_domain(0x9876543210u * 0x12345678),
+	});
+}
+
+REGISTER_FAST_TEST(bpf_validate_alu64_mul_k_overflow_autotest, NOHUGE_OK, ASAN_OK,
+	test_alu64_mul_k_overflow);
+
 /* 64-bit mul of small scalar range and immediate. */
 static int
 test_alu64_mul_k_range_small(void)
diff --git a/lib/bpf/bpf_validate.c b/lib/bpf/bpf_validate.c
index 08717671a863..2b73e3628881 100644
--- a/lib/bpf/bpf_validate.c
+++ b/lib/bpf/bpf_validate.c
@@ -921,8 +921,8 @@ eval_mul(struct bpf_reg_val *rd, const struct bpf_reg_val *rs, size_t opsz,
 
 	/* both operands are constants */
 	if (rd->s.min == rd->s.max && rs->s.min == rs->s.max) {
-		rd->s.min = (rd->s.min * rs->s.min) & msk;
-		rd->s.max = (rd->s.max * rs->s.max) & msk;
+		rd->s.min = ((uint64_t)rd->s.min * (uint64_t)rs->s.min) & msk;
+		rd->s.max = ((uint64_t)rd->s.max * (uint64_t)rs->s.max) & msk;
 	/* check that both operands are positive and no overflow */
 	} else if (rd->s.min >= 0 && rs->s.min >= 0) {
 		rd->s.max *= rs->s.max;
-- 
2.43.0


^ permalink raw reply related

* [PATCH v4 13/24] bpf/validate: fix BPF_MUL ranges minimum typo
From: Marat Khalili @ 2026-06-23 14:32 UTC (permalink / raw)
  To: Konstantin Ananyev; +Cc: dev, stable, Claudia Cauli
In-Reply-To: <20260623143215.95318-1-marat.khalili@huawei.com>

Function `eval_mul` calculated minimum of the both signed and unsigned
ranges as destination square instead of product with source due to a
typo.

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

     Tested program:
         0:  mov r0, #0x0
         1:  ldxdw r2, [r1 + 0]
         2:  jlt r2, #0x11, L8
         3:  jgt r2, #0x1d, L8
         4:  jslt r2, #0x11, L8
         5:  jsgt r2, #0x1d, L8
         6:  mul r2, #0xb  ; tested instruction
         7:  mov r0, #0x1
         8:  exit
     Pre-state:
        r2:  17..29
     Post-state:
        r2:  289..319

After the tested instruction validator considers r2 to be no less than
289, however if 20 was loaded on step 1 it is possible for it after
multiplying by 11 to become 220 which is less than 289.

Fix the typo, add test.

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>
---
 app/test/test_bpf_validate.c | 17 +++++++++++++++++
 lib/bpf/bpf_validate.c       |  4 ++--
 2 files changed, 19 insertions(+), 2 deletions(-)

diff --git a/app/test/test_bpf_validate.c b/app/test/test_bpf_validate.c
index 31a235a55af6..ecebfd48dd52 100644
--- a/app/test/test_bpf_validate.c
+++ b/app/test/test_bpf_validate.c
@@ -1289,6 +1289,23 @@ test_alu64_div_mod_overflow(void)
 REGISTER_FAST_TEST(bpf_validate_alu64_div_mod_overflow_autotest, NOHUGE_OK, ASAN_OK,
 	test_alu64_div_mod_overflow);
 
+/* 64-bit mul of small scalar range and immediate. */
+static int
+test_alu64_mul_k_range_small(void)
+{
+	return verify_instruction((struct verify_instruction_param){
+		.tested_instruction = {
+			.code = (EBPF_ALU64 | BPF_MUL | BPF_K),
+			.imm = 11,
+		},
+		.pre.dst = make_unsigned_domain(17, 29),
+		.post.dst = make_unsigned_domain(17 * 11, 29 * 11),
+	});
+}
+
+REGISTER_FAST_TEST(bpf_validate_alu64_mul_k_range_small_autotest, NOHUGE_OK, ASAN_OK,
+	test_alu64_mul_k_range_small);
+
 /* 64-bit negation when interval first element is INT64_MIN. */
 static int
 test_alu64_neg_int64min_first(void)
diff --git a/lib/bpf/bpf_validate.c b/lib/bpf/bpf_validate.c
index 14a186b7cbf7..08717671a863 100644
--- a/lib/bpf/bpf_validate.c
+++ b/lib/bpf/bpf_validate.c
@@ -915,7 +915,7 @@ eval_mul(struct bpf_reg_val *rd, const struct bpf_reg_val *rs, size_t opsz,
 	/* check for overflow */
 	} else if (rd->u.max <= msk >> opsz / 2 && rs->u.max <= msk >> opsz) {
 		rd->u.max *= rs->u.max;
-		rd->u.min *= rd->u.min;
+		rd->u.min *= rs->u.min;
 	} else
 		eval_umax_bound(rd, msk);
 
@@ -926,7 +926,7 @@ eval_mul(struct bpf_reg_val *rd, const struct bpf_reg_val *rs, size_t opsz,
 	/* check that both operands are positive and no overflow */
 	} else if (rd->s.min >= 0 && rs->s.min >= 0) {
 		rd->s.max *= rs->s.max;
-		rd->s.min *= rd->s.min;
+		rd->s.min *= rs->s.min;
 	} else
 		eval_smax_bound(rd, msk);
 }
-- 
2.43.0


^ permalink raw reply related

* [PATCH v4 12/24] bpf/validate: fix BPF_DIV and BPF_MOD signed part
From: Marat Khalili @ 2026-06-23 14:31 UTC (permalink / raw)
  To: Konstantin Ananyev; +Cc: dev, stable, Claudia Cauli
In-Reply-To: <20260623143215.95318-1-marat.khalili@huawei.com>

Function `eval_divmod` for _unsigned_ division or modulo operation
calculated signed ranges using _signed_ division, which is
mathematically incorrect: unlike some other mathematical operations,
signed and unsigned divisions in the CPU register cyclic ring math are
not equivalent.

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

     Tested program:
         0:  mov r0, #0x0
         1:  lddw r2, #0xaaaaaaaaaaaaaaaa
         3:  mov r3, #0x2
         4:  div r2, r3  ; tested instruction
         5:  mov r0, #0x1
         6:  exit
     Pre-state:
        r2:  -6148914691236517206
        r3:  2
     Post-state:
        r2:  -3074457345618258603 INTERSECT 0x5555555555555555 (!)

After the tested instruction validator considers r2 to equal
0x5555555555555555 if viewed as unsigned (correct, this is
0xaaaaaaaaaaaaaaaaull / 2), but equal -3074457345618258603 or
0xd555555555555555 if viewed as signed, although it cannot be both true.

Additionally, when validating division or modulo of INT64_MIN by -1
overflow happened in the validator possibly triggering an exception.

The following error is shown without sanitizer:

    1/1 DPDK:fast-tests / bpf_autotest FAIL            0.37s
    killed by signal 8 SIGFPE

With sanitizer the following diagnostic is generated:

    lib/bpf/bpf_validate.c:1086:14: runtime error: division of
    -9223372036854775808 by -1 cannot be represented in type 'long int'
        #0 0x0000027484bb in eval_divmod lib/bpf/bpf_validate.c:1086
        #1 0x00000274bcf3 in eval_alu lib/bpf/bpf_validate.c:1280
        #2 0x00000275cb3e in evaluate lib/bpf/bpf_validate.c:3192
        ...

    SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior
    lib/bpf/bpf_validate.c:1086:14

Change logic to copy results from unsigned division into signed. Add
both validation and execution tests for the case that triggered an
exception. Add validation tests for non-constant division to make sure
it is still valid (ranges of the non-constant division or modulo are not
really minimal, this can be addressed in the future).

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>
---
 app/test/test_bpf.c          |  99 +++++++++++++++++++++++++
 app/test/test_bpf_validate.c | 135 +++++++++++++++++++++++++++++++++++
 lib/bpf/bpf_validate.c       |  38 +++-------
 3 files changed, 244 insertions(+), 28 deletions(-)

diff --git a/app/test/test_bpf.c b/app/test/test_bpf.c
index 6b07e72295db..3205afaa63ad 100644
--- a/app/test/test_bpf.c
+++ b/app/test/test_bpf.c
@@ -393,6 +393,13 @@ cmp_res(const char *func, uint64_t exp_rc, uint64_t ret_rc,
 	return ret;
 }
 
+/* Empty prepare function */
+static void
+dummy_prepare(void *arg)
+{
+	RTE_SET_USED(arg);
+}
+
 /* store immediate test-cases */
 static const struct ebpf_insn test_store1_prog[] = {
 	{
@@ -3157,6 +3164,70 @@ static const struct ebpf_insn test_ld_mbuf3_prog[] = {
 	},
 };
 
+/* divide INT64_MIN by -1 */
+static const struct ebpf_insn test_int64min_udiv_uint64max_prog[] = {
+	/* Load INT64_MIN into r0 */
+	{
+		.code = (BPF_LD | BPF_IMM | EBPF_DW),
+		.dst_reg = EBPF_REG_0,
+		.imm = (int32_t)INT64_MIN,
+	},
+	{
+		.imm = (int32_t)(INT64_MIN >> 32),
+	},
+	/* Divide r0 by immediate -1 */
+	{
+		.code = (EBPF_ALU64 | BPF_DIV | BPF_K),
+		.dst_reg = EBPF_REG_0,
+		.imm = -1,
+	},
+	/* Exit for correctness otherwise */
+	{
+		.code = (BPF_JMP | EBPF_EXIT),
+	},
+};
+
+static int
+test_int64min_udiv_uint64max_check(uint64_t rc, const void *arg)
+{
+	RTE_SET_USED(arg);
+	/* 0x8000000000000000ull / 0xFFFFFFFFFFFFFFFFull == 0 */
+	TEST_ASSERT_EQUAL(rc, 0, "expected 0, found %#" PRIx64, rc);
+	return TEST_SUCCESS;
+}
+
+/* modulo INT64_MIN by -1 */
+static const struct ebpf_insn test_int64min_umod_uint64max_prog[] = {
+	/* Load INT64_MIN into r0 */
+	{
+		.code = (BPF_LD | BPF_IMM | EBPF_DW),
+		.dst_reg = EBPF_REG_0,
+		.imm = (int32_t)INT64_MIN,
+	},
+	{
+		.imm = (int32_t)(INT64_MIN >> 32),
+	},
+	/* Modulo r0 by immediate -1 */
+	{
+		.code = (EBPF_ALU64 | BPF_MOD | BPF_K),
+		.dst_reg = EBPF_REG_0,
+		.imm = -1,
+	},
+	/* Exit for correctness otherwise */
+	{
+		.code = (BPF_JMP | EBPF_EXIT),
+	},
+};
+
+static int
+test_int64min_umod_uint64max_check(uint64_t rc, const void *arg)
+{
+	RTE_SET_USED(arg);
+	/* 0x8000000000000000ull % 0xFFFFFFFFFFFFFFFFull == 0x8000000000000000ull  */
+	TEST_ASSERT_EQUAL(rc, (uint64_t)INT64_MIN, "expected INT64_MIN, found %#" PRIx64, rc);
+	return TEST_SUCCESS;
+}
+
 /* all bpf test cases */
 static const struct bpf_test tests[] = {
 	{
@@ -3465,6 +3536,34 @@ static const struct bpf_test tests[] = {
 		/* mbuf as input argument is not supported on 32 bit platform */
 		.allow_fail = (sizeof(uint64_t) != sizeof(uintptr_t)),
 	},
+	{
+		.name = "test_int64min_udiv_uint64max",
+		.arg_sz = sizeof(struct dummy_vect8),
+		.prm = {
+			.ins = test_int64min_udiv_uint64max_prog,
+			.nb_ins = RTE_DIM(test_int64min_udiv_uint64max_prog),
+			.prog_arg = {
+				.type = RTE_BPF_ARG_PTR,
+				.size = sizeof(struct dummy_vect8),
+			},
+		},
+		.prepare = dummy_prepare,
+		.check_result = test_int64min_udiv_uint64max_check,
+	},
+	{
+		.name = "test_int64min_umod_uint64max",
+		.arg_sz = 1,
+		.prm = {
+			.ins = test_int64min_umod_uint64max_prog,
+			.nb_ins = RTE_DIM(test_int64min_umod_uint64max_prog),
+			.prog_arg = {
+				.type = RTE_BPF_ARG_PTR,
+				.size = 1,
+			},
+		},
+		.prepare = dummy_prepare,
+		.check_result = test_int64min_umod_uint64max_check,
+	},
 };
 
 static int
diff --git a/app/test/test_bpf_validate.c b/app/test/test_bpf_validate.c
index c752d8635756..31a235a55af6 100644
--- a/app/test/test_bpf_validate.c
+++ b/app/test/test_bpf_validate.c
@@ -1154,6 +1154,141 @@ 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 division and modulo of UINT64_MAX*2/3. */
+static int
+test_alu64_div_mod_big_constant(void)
+{
+	const uint64_t dividend = UINT64_MAX / 3 * 2;
+	static const uint64_t divisors[] = {
+		1,
+		2,
+		3,
+		UINT64_MAX / 3,
+		INT64_MAX,
+		INT64_MIN,
+		UINT64_MAX / 3 * 2,
+		UINT64_MAX / 4 * 3,
+		UINT64_MAX,
+	};
+	for (int index = 0; index != RTE_DIM(divisors); ++index) {
+		const uint64_t divisor = divisors[index];
+
+		TEST_ASSERT_SUCCESS(verify_instruction((struct verify_instruction_param){
+			.tested_instruction = {
+				.code = (EBPF_ALU64 | BPF_DIV | BPF_X),
+			},
+			.pre.dst = make_singleton_domain(dividend),
+			.pre.src = make_singleton_domain(divisor),
+			.post.dst = make_singleton_domain(dividend / divisor),
+		}), "(EBPF_ALU64 | BPF_DIV | BPF_X) check, index=%d", index);
+
+		TEST_ASSERT_SUCCESS(verify_instruction((struct verify_instruction_param){
+			.tested_instruction = {
+				.code = (EBPF_ALU64 | BPF_MOD | BPF_X),
+			},
+			.pre.dst = make_singleton_domain(dividend),
+			.pre.src = make_singleton_domain(divisor),
+			.post.dst = make_singleton_domain(dividend % divisor),
+		}), "(EBPF_ALU64 | BPF_MOD | BPF_X) check, index=%d", index);
+	}
+
+	return TEST_SUCCESS;
+}
+
+REGISTER_FAST_TEST(bpf_validate_alu64_div_mod_big_constant_autotest, NOHUGE_OK, ASAN_OK,
+	test_alu64_div_mod_big_constant);
+
+/* 64-bit division and modulo of UINT64_MAX/3..UINT64_MAX*2/3 by a constant. */
+static int
+test_alu64_div_mod_big_range(void)
+{
+	const uint64_t dividend_first = UINT64_MAX / 3;
+	const uint64_t dividend_last = UINT64_MAX / 3 * 2;
+	static const uint64_t divisors[] = {
+		1,
+		2,
+		3,
+		UINT64_MAX / 3,
+		INT64_MAX,
+		INT64_MIN,
+		UINT64_MAX / 3 * 2,
+		UINT64_MAX / 4 * 3,
+		UINT64_MAX,
+	};
+	for (int index = 0; index != RTE_DIM(divisors); ++index) {
+		const uint64_t divisor = divisors[index];
+
+		TEST_ASSERT_SUCCESS(verify_instruction((struct verify_instruction_param){
+			.tested_instruction = {
+				.code = (EBPF_ALU64 | BPF_DIV | BPF_X),
+			},
+			.pre.dst = make_unsigned_domain(dividend_first, dividend_last),
+			.pre.src = make_singleton_domain(divisor),
+			.post.dst = make_unsigned_domain(0, dividend_last),
+		}), "(EBPF_ALU64 | BPF_DIV | BPF_X) check, index=%d", index);
+
+		TEST_ASSERT_SUCCESS(verify_instruction((struct verify_instruction_param){
+			.tested_instruction = {
+				.code = (EBPF_ALU64 | BPF_MOD | BPF_X),
+			},
+			.pre.dst = make_unsigned_domain(dividend_first, dividend_last),
+			.pre.src = make_singleton_domain(divisor),
+			.post.dst = make_unsigned_domain(0, RTE_MIN(dividend_last, divisor - 1)),
+		}), "(EBPF_ALU64 | BPF_MOD | BPF_X) check, index=%d", index);
+	}
+
+	return TEST_SUCCESS;
+}
+
+REGISTER_FAST_TEST(bpf_validate_alu64_div_mod_big_range_autotest, NOHUGE_OK, ASAN_OK,
+	test_alu64_div_mod_big_range);
+
+/* 64-bit division and modulo of INT64_MIN by -1. */
+static int
+test_alu64_div_mod_overflow(void)
+{
+	TEST_ASSERT_SUCCESS(verify_instruction((struct verify_instruction_param){
+		.tested_instruction = {
+			.code = (EBPF_ALU64 | BPF_DIV | BPF_K),
+			.imm = -1,
+		},
+		.pre.dst = make_singleton_domain(INT64_MIN),
+		.post.dst = make_singleton_domain(0),
+	}), "(EBPF_ALU64 | BPF_DIV | BPF_K) check");
+
+	TEST_ASSERT_SUCCESS(verify_instruction((struct verify_instruction_param){
+		.tested_instruction = {
+			.code = (EBPF_ALU64 | BPF_DIV | BPF_X),
+		},
+		.pre.dst = make_singleton_domain(INT64_MIN),
+		.pre.src = make_singleton_domain(-1),
+		.post.dst = make_singleton_domain(0),
+	}), "(EBPF_ALU64 | BPF_DIV | BPF_X) check");
+
+	TEST_ASSERT_SUCCESS(verify_instruction((struct verify_instruction_param){
+		.tested_instruction = {
+			.code = (EBPF_ALU64 | BPF_MOD | BPF_K),
+			.imm = -1,
+		},
+		.pre.dst = make_singleton_domain(INT64_MIN),
+		.post.dst = make_singleton_domain(INT64_MIN),
+	}), "(EBPF_ALU64 | BPF_MOD | BPF_K) check");
+
+	TEST_ASSERT_SUCCESS(verify_instruction((struct verify_instruction_param){
+		.tested_instruction = {
+			.code = (EBPF_ALU64 | BPF_MOD | BPF_X),
+		},
+		.pre.dst = make_singleton_domain(INT64_MIN),
+		.pre.src = make_singleton_domain(-1),
+		.post.dst = make_singleton_domain(INT64_MIN),
+	}), "(EBPF_ALU64 | BPF_MOD | BPF_X) check");
+
+	return TEST_SUCCESS;
+}
+
+REGISTER_FAST_TEST(bpf_validate_alu64_div_mod_overflow_autotest, NOHUGE_OK, ASAN_OK,
+	test_alu64_div_mod_overflow);
+
 /* 64-bit negation when interval first element is INT64_MIN. */
 static int
 test_alu64_neg_int64min_first(void)
diff --git a/lib/bpf/bpf_validate.c b/lib/bpf/bpf_validate.c
index 38e9b033c6d9..14a186b7cbf7 100644
--- a/lib/bpf/bpf_validate.c
+++ b/lib/bpf/bpf_validate.c
@@ -932,8 +932,7 @@ eval_mul(struct bpf_reg_val *rd, const struct bpf_reg_val *rs, size_t opsz,
 }
 
 static const char *
-eval_divmod(uint32_t op, struct bpf_reg_val *rd, struct bpf_reg_val *rs,
-	size_t opsz, uint64_t msk)
+eval_divmod(uint32_t op, struct bpf_reg_val *rd, struct bpf_reg_val *rs, uint64_t msk)
 {
 	/* both operands are constants */
 	if (rd->u.min == rd->u.max && rs->u.min == rs->u.max) {
@@ -954,34 +953,17 @@ eval_divmod(uint32_t op, struct bpf_reg_val *rd, struct bpf_reg_val *rs,
 		rd->u.min = 0;
 	}
 
-	/* if we have 32-bit values - extend them to 64-bit */
-	if (opsz == sizeof(uint32_t) * CHAR_BIT) {
-		rd->s.min = (int32_t)rd->s.min;
-		rd->s.max = (int32_t)rd->s.max;
-		rs->s.min = (int32_t)rs->s.min;
-		rs->s.max = (int32_t)rs->s.max;
-	}
-
-	/* both operands are constants */
-	if (rd->s.min == rd->s.max && rs->s.min == rs->s.max) {
-		if (rs->s.max == 0)
-			return "division by 0";
-		if (op == BPF_DIV) {
-			rd->s.min /= rs->s.min;
-			rd->s.max /= rs->s.max;
-		} else {
-			rd->s.min %= rs->s.min;
-			rd->s.max %= rs->s.max;
-		}
-	} else if (op == BPF_MOD) {
-		rd->s.min = RTE_MAX(rd->s.max, 0);
-		rd->s.min = RTE_MIN(rd->s.min, 0);
+	if (rd->u.min >= (uint64_t)INT64_MIN || rd->u.max <= (uint64_t)INT64_MAX) {
+		/*
+		 * All values have the same sign bit, which means range
+		 * contiguous as unsigned is also contiguous as signed,
+		 * so we can just reuse it without any changes.
+		 */
+		rd->s.min = rd->u.min;
+		rd->s.max = rd->u.max;
 	} else
 		eval_smax_bound(rd, msk);
 
-	rd->s.max &= msk;
-	rd->s.min &= msk;
-
 	return NULL;
 }
 
@@ -1165,7 +1147,7 @@ eval_alu(struct bpf_verifier *bvf, const struct ebpf_insn *ins)
 	else if (op == BPF_MUL)
 		eval_mul(rd, &rs, opsz, msk);
 	else if (op == BPF_DIV || op == BPF_MOD)
-		err = eval_divmod(op, rd, &rs, opsz, msk);
+		err = eval_divmod(op, rd, &rs, msk);
 	else if (op == BPF_NEG)
 		eval_neg(rd, opsz, msk);
 	else if (op == EBPF_MOV)
-- 
2.43.0


^ permalink raw reply related

* [PATCH v4 11/24] bpf/validate: fix BPF_NEG of INT64_MIN and 0
From: Marat Khalili @ 2026-06-23 14:31 UTC (permalink / raw)
  To: Konstantin Ananyev; +Cc: dev, stable, Claudia Cauli
In-Reply-To: <20260623143215.95318-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>
---
 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 aa385ec8c275..c752d8635756 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 73e925a7dff2..38e9b033c6d9 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 v4 10/24] bpf/validate: fix EBPF_JSLT | BPF_X evaluation
From: Marat Khalili @ 2026-06-23 14:31 UTC (permalink / raw)
  To: Konstantin Ananyev, Ferruh Yigit; +Cc: dev, stable
In-Reply-To: <20260623143215.95318-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>
---
 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 f05f8a248281..aa385ec8c275 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 6a2d5974e036..73e925a7dff2 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 v4 09/24] test/bpf_validate: add harness for pointer tests
From: Marat Khalili @ 2026-06-23 14:31 UTC (permalink / raw)
  Cc: dev, Konstantin Ananyev
In-Reply-To: <20260623143215.95318-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>
---
 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 350847d07ae5..f05f8a248281 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 v4 08/24] test/bpf_validate: add setup and basic tests
From: Marat Khalili @ 2026-06-23 14:31 UTC (permalink / raw)
  Cc: dev, Konstantin Ananyev
In-Reply-To: <20260623143215.95318-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>
---
 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 61024125a7c6..909d76f2f50a 100644
--- a/app/test/meson.build
+++ b/app/test/meson.build
@@ -35,6 +35,7 @@ source_file_deps = {
     'test_bitset.c': [],
     'test_bitratestats.c': ['metrics', 'bitratestats', 'ethdev'] + sample_packet_forward_deps,
     'test_bpf.c': ['bpf', 'net'],
+    'test_bpf_validate.c': ['bpf'],
     'test_byteorder.c': [],
     'test_cfgfile.c': ['cfgfile'],
     'test_cksum.c': ['net'],
diff --git a/app/test/test_bpf_validate.c b/app/test/test_bpf_validate.c
new file mode 100644
index 000000000000..350847d07ae5
--- /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 v4 07/24] bpf/validate: fix BPF_LDX | EBPF_DW signed range
From: Marat Khalili @ 2026-06-23 14:31 UTC (permalink / raw)
  To: Konstantin Ananyev; +Cc: dev, stable
In-Reply-To: <20260623143215.95318-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>
---
 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 5609bfcd5c16..6a2d5974e036 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 v4 06/24] bpf/validate: fix BPF_ADD of pointer to a scalar
From: Marat Khalili @ 2026-06-23 14:31 UTC (permalink / raw)
  To: Konstantin Ananyev; +Cc: dev, stable
In-Reply-To: <20260623143215.95318-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>
---
 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 f3f462920a3d..5609bfcd5c16 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


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