public inbox for bpf@vger.kernel.org
 help / color / mirror / Atom feed
* [PATCH v2 0/2] bpf: Fix abs(INT_MIN) undefined behavior in interpreter sdiv/smod
@ 2026-03-06 19:08 Jenny Guanni Qu
  2026-03-06 19:08 ` [PATCH v2 1/2] bpf: Fix undefined behavior in interpreter sdiv/smod for INT_MIN Jenny Guanni Qu
  2026-03-06 19:08 ` [PATCH v2 2/2] selftests/bpf: Add tests for sdiv32/smod32 with INT_MIN dividend Jenny Guanni Qu
  0 siblings, 2 replies; 5+ messages in thread
From: Jenny Guanni Qu @ 2026-03-06 19:08 UTC (permalink / raw)
  To: bpf; +Cc: daniel, ast, andrii, lkp, Jenny Guanni Qu

The BPF interpreter's signed 32-bit division and modulo handlers use
abs() on s32 operands, which is undefined for S32_MIN. This causes
the interpreter to compute wrong results, creating a mismatch with
the verifier's range tracking.

For example, INT_MIN / 2 returns 0x40000000 instead of the correct
0xC0000000. The verifier tracks the correct range, so a crafted BPF
program can exploit the mismatch for out-of-bounds map value access
(confirmed by KASAN).

Patch 1 introduces __safe_abs32() which handles S32_MIN correctly
and replaces all 8 abs((s32)...) call sites.

Patch 2 adds selftests covering sdiv32 and smod32 with INT_MIN
dividend to prevent regression.

Changes since v1:
  - Moved __safe_abs32() helper above the kerneldoc comment block
    for ___bpf_prog_run() to fix build warnings reported by
    kernel test robot (W=1 doc warnings)

Jenny Guanni Qu (2):
  bpf: Fix undefined behavior in interpreter sdiv/smod for INT_MIN
  selftests/bpf: Add tests for sdiv32/smod32 with INT_MIN dividend

 kernel/bpf/core.c                             | 23 ++++---
 .../selftests/bpf/progs/verifier_sdiv.c       | 58 +++++++++++++++++++
 2 files changed, 72 insertions(+), 9 deletions(-)

-- 
2.34.1

^ permalink raw reply	[flat|nested] 5+ messages in thread

* [PATCH v2 1/2] bpf: Fix undefined behavior in interpreter sdiv/smod for INT_MIN
  2026-03-06 19:08 [PATCH v2 0/2] bpf: Fix abs(INT_MIN) undefined behavior in interpreter sdiv/smod Jenny Guanni Qu
@ 2026-03-06 19:08 ` Jenny Guanni Qu
  2026-03-09 18:22   ` Mykyta Yatsenko
  2026-03-06 19:08 ` [PATCH v2 2/2] selftests/bpf: Add tests for sdiv32/smod32 with INT_MIN dividend Jenny Guanni Qu
  1 sibling, 1 reply; 5+ messages in thread
From: Jenny Guanni Qu @ 2026-03-06 19:08 UTC (permalink / raw)
  To: bpf; +Cc: daniel, ast, andrii, lkp, Jenny Guanni Qu

The BPF interpreter's signed 32-bit division and modulo handlers use
the kernel abs() macro on s32 operands. The abs() macro documentation
(include/linux/math.h) explicitly states the result is undefined when
the input is the type minimum. When DST contains S32_MIN (0x80000000),
abs((s32)DST) triggers undefined behavior and returns S32_MIN unchanged
on arm64/x86. This value is then sign-extended to u64 as
0xFFFFFFFF80000000, causing do_div() to compute the wrong result.

The verifier's abstract interpretation (scalar32_min_max_sdiv) computes
the mathematically correct result for range tracking, creating a
verifier/interpreter mismatch that can be exploited for out-of-bounds
map value access.

Introduce __safe_abs32() which handles S32_MIN correctly and replace
all 8 abs((s32)...) call sites in the interpreter's sdiv32/smod32
handlers.

Fixes: ec0e2da95f72 ("bpf: Support new signed div/mod instructions.")
Signed-off-by: Jenny Guanni Qu <qguanni@gmail.com>
---
 kernel/bpf/core.c | 23 ++++++++++++++---------
 1 file changed, 14 insertions(+), 9 deletions(-)

diff --git a/kernel/bpf/core.c b/kernel/bpf/core.c
index 3ece2da55625..663b9af6d19b 100644
--- a/kernel/bpf/core.c
+++ b/kernel/bpf/core.c
@@ -16,7 +16,6 @@
  * Andi Kleen - Fix a few bad bugs and races.
  * Kris Katterjohn - Added many additional checks in bpf_check_classic()
  */
-
 #include <uapi/linux/btf.h>
 #include <crypto/sha1.h>
 #include <linux/filter.h>
@@ -1736,6 +1735,12 @@ bool bpf_opcode_in_insntable(u8 code)
 }
 
 #ifndef CONFIG_BPF_JIT_ALWAYS_ON
+/* Safe absolute value for s32 - abs() is undefined for S32_MIN */
+static inline u32 __safe_abs32(s32 x)
+{
+	return x >= 0 ? (u32)x : x == S32_MIN ? (u32)S32_MIN : (u32)-x;
+}
+
 /**
  *	___bpf_prog_run - run eBPF program on a given context
  *	@regs: is the array of MAX_BPF_EXT_REG eBPF pseudo-registers
@@ -1900,8 +1905,8 @@ static u64 ___bpf_prog_run(u64 *regs, const struct bpf_insn *insn)
 			DST = do_div(AX, (u32) SRC);
 			break;
 		case 1:
-			AX = abs((s32)DST);
-			AX = do_div(AX, abs((s32)SRC));
+			AX = __safe_abs32((s32)DST);
+			AX = do_div(AX, __safe_abs32((s32)SRC));
 			if ((s32)DST < 0)
 				DST = (u32)-AX;
 			else
@@ -1928,8 +1933,8 @@ static u64 ___bpf_prog_run(u64 *regs, const struct bpf_insn *insn)
 			DST = do_div(AX, (u32) IMM);
 			break;
 		case 1:
-			AX = abs((s32)DST);
-			AX = do_div(AX, abs((s32)IMM));
+			AX = __safe_abs32((s32)DST);
+			AX = do_div(AX, __safe_abs32((s32)IMM));
 			if ((s32)DST < 0)
 				DST = (u32)-AX;
 			else
@@ -1955,8 +1960,8 @@ static u64 ___bpf_prog_run(u64 *regs, const struct bpf_insn *insn)
 			DST = (u32) AX;
 			break;
 		case 1:
-			AX = abs((s32)DST);
-			do_div(AX, abs((s32)SRC));
+			AX = __safe_abs32((s32)DST);
+			do_div(AX, __safe_abs32((s32)SRC));
 			if (((s32)DST < 0) == ((s32)SRC < 0))
 				DST = (u32)AX;
 			else
@@ -1982,8 +1987,8 @@ static u64 ___bpf_prog_run(u64 *regs, const struct bpf_insn *insn)
 			DST = (u32) AX;
 			break;
 		case 1:
-			AX = abs((s32)DST);
-			do_div(AX, abs((s32)IMM));
+			AX = __safe_abs32((s32)DST);
+			do_div(AX, __safe_abs32((s32)IMM));
 			if (((s32)DST < 0) == ((s32)IMM < 0))
 				DST = (u32)AX;
 			else
-- 
2.34.1


^ permalink raw reply related	[flat|nested] 5+ messages in thread

* [PATCH v2 2/2] selftests/bpf: Add tests for sdiv32/smod32 with INT_MIN dividend
  2026-03-06 19:08 [PATCH v2 0/2] bpf: Fix abs(INT_MIN) undefined behavior in interpreter sdiv/smod Jenny Guanni Qu
  2026-03-06 19:08 ` [PATCH v2 1/2] bpf: Fix undefined behavior in interpreter sdiv/smod for INT_MIN Jenny Guanni Qu
@ 2026-03-06 19:08 ` Jenny Guanni Qu
  1 sibling, 0 replies; 5+ messages in thread
From: Jenny Guanni Qu @ 2026-03-06 19:08 UTC (permalink / raw)
  To: bpf; +Cc: daniel, ast, andrii, lkp, Jenny Guanni Qu

Add tests to verify that signed 32-bit division and modulo operations
produce correct results when the dividend is INT_MIN (0x80000000).

These test the fix in the previous commit which replaced abs() with a
safe helper to avoid undefined behavior for S32_MIN.

Test cases:
  - SDIV32 INT_MIN / 2 = -1073741824 (imm and reg divisor)
  - SMOD32 INT_MIN % 2 = 0 (positive and negative divisor)

Signed-off-by: Jenny Guanni Qu <qguanni@gmail.com>
---
 .../selftests/bpf/progs/verifier_sdiv.c       | 58 +++++++++++++++++++
 1 file changed, 58 insertions(+)

diff --git a/tools/testing/selftests/bpf/progs/verifier_sdiv.c b/tools/testing/selftests/bpf/progs/verifier_sdiv.c
index 148d2299e5b4..fd59d57e8e37 100644
--- a/tools/testing/selftests/bpf/progs/verifier_sdiv.c
+++ b/tools/testing/selftests/bpf/progs/verifier_sdiv.c
@@ -1209,6 +1209,64 @@ __naked void smod32_ri_divisor_neg_1(void)
 	: __clobber_all);
 }
 
+SEC("socket")
+__description("SDIV32, INT_MIN divided by 2, imm")
+__success __success_unpriv __retval(-1073741824)
+__naked void sdiv32_int_min_div_2_imm(void)
+{
+	asm volatile ("					\
+	w0 = %[int_min];				\
+	w0 s/= 2;					\
+	exit;						\
+"	:
+	: __imm_const(int_min, INT_MIN)
+	: __clobber_all);
+}
+
+SEC("socket")
+__description("SDIV32, INT_MIN divided by 2, reg")
+__success __success_unpriv __retval(-1073741824)
+__naked void sdiv32_int_min_div_2_reg(void)
+{
+	asm volatile ("					\
+	w0 = %[int_min];				\
+	w1 = 2;						\
+	w0 s/= w1;					\
+	exit;						\
+"	:
+	: __imm_const(int_min, INT_MIN)
+	: __clobber_all);
+}
+
+SEC("socket")
+__description("SMOD32, INT_MIN modulo 2, imm")
+__success __success_unpriv __retval(0)
+__naked void smod32_int_min_mod_2_imm(void)
+{
+	asm volatile ("					\
+	w0 = %[int_min];				\
+	w0 s%%= 2;					\
+	exit;						\
+"	:
+	: __imm_const(int_min, INT_MIN)
+	: __clobber_all);
+}
+
+SEC("socket")
+__description("SMOD32, INT_MIN modulo -2, imm")
+__success __success_unpriv __retval(0)
+__naked void smod32_int_min_mod_neg2_imm(void)
+{
+	asm volatile ("					\
+	w0 = %[int_min];				\
+	w0 s%%= -2;					\
+	exit;						\
+"	:
+	: __imm_const(int_min, INT_MIN)
+	: __clobber_all);
+}
+
+
 #else
 
 SEC("socket")
-- 
2.34.1


^ permalink raw reply related	[flat|nested] 5+ messages in thread

* Re: [PATCH v2 1/2] bpf: Fix undefined behavior in interpreter sdiv/smod for INT_MIN
  2026-03-06 19:08 ` [PATCH v2 1/2] bpf: Fix undefined behavior in interpreter sdiv/smod for INT_MIN Jenny Guanni Qu
@ 2026-03-09 18:22   ` Mykyta Yatsenko
  2026-03-09 18:53     ` Guanni Qu
  0 siblings, 1 reply; 5+ messages in thread
From: Mykyta Yatsenko @ 2026-03-09 18:22 UTC (permalink / raw)
  To: Jenny Guanni Qu, bpf; +Cc: daniel, ast, andrii, lkp, Jenny Guanni Qu

Jenny Guanni Qu <qguanni@gmail.com> writes:

> The BPF interpreter's signed 32-bit division and modulo handlers use
> the kernel abs() macro on s32 operands. The abs() macro documentation
> (include/linux/math.h) explicitly states the result is undefined when
> the input is the type minimum. When DST contains S32_MIN (0x80000000),
> abs((s32)DST) triggers undefined behavior and returns S32_MIN unchanged
> on arm64/x86. This value is then sign-extended to u64 as
> 0xFFFFFFFF80000000, causing do_div() to compute the wrong result.
>
> The verifier's abstract interpretation (scalar32_min_max_sdiv) computes
> the mathematically correct result for range tracking, creating a
> verifier/interpreter mismatch that can be exploited for out-of-bounds
> map value access.
>
> Introduce __safe_abs32() which handles S32_MIN correctly and replace
> all 8 abs((s32)...) call sites in the interpreter's sdiv32/smod32
> handlers.
>
> Fixes: ec0e2da95f72 ("bpf: Support new signed div/mod instructions.")
> Signed-off-by: Jenny Guanni Qu <qguanni@gmail.com>
> ---
>  kernel/bpf/core.c | 23 ++++++++++++++---------
>  1 file changed, 14 insertions(+), 9 deletions(-)
>
> diff --git a/kernel/bpf/core.c b/kernel/bpf/core.c
> index 3ece2da55625..663b9af6d19b 100644
> --- a/kernel/bpf/core.c
> +++ b/kernel/bpf/core.c
> @@ -16,7 +16,6 @@
>   * Andi Kleen - Fix a few bad bugs and races.
>   * Kris Katterjohn - Added many additional checks in bpf_check_classic()
>   */
> -
>  #include <uapi/linux/btf.h>
>  #include <crypto/sha1.h>
>  #include <linux/filter.h>
> @@ -1736,6 +1735,12 @@ bool bpf_opcode_in_insntable(u8 code)
>  }
>  
>  #ifndef CONFIG_BPF_JIT_ALWAYS_ON
> +/* Safe absolute value for s32 - abs() is undefined for S32_MIN */
> +static inline u32 __safe_abs32(s32 x)
> +{
> +	return x >= 0 ? (u32)x : x == S32_MIN ? (u32)S32_MIN : (u32)-x;
can we write it as x >= 0 ? (u32)x : -(u32)x; ?
By casting to u32 first, the negation operates in unsigned arithmetic
where overflow does not happen — it wraps.
Please correct me if I'm wrong.
> +}
> +
>  /**
>   *	___bpf_prog_run - run eBPF program on a given context
>   *	@regs: is the array of MAX_BPF_EXT_REG eBPF pseudo-registers
> @@ -1900,8 +1905,8 @@ static u64 ___bpf_prog_run(u64 *regs, const struct bpf_insn *insn)
>  			DST = do_div(AX, (u32) SRC);
>  			break;
>  		case 1:
> -			AX = abs((s32)DST);
> -			AX = do_div(AX, abs((s32)SRC));
> +			AX = __safe_abs32((s32)DST);
> +			AX = do_div(AX, __safe_abs32((s32)SRC));
>  			if ((s32)DST < 0)
>  				DST = (u32)-AX;
>  			else
> @@ -1928,8 +1933,8 @@ static u64 ___bpf_prog_run(u64 *regs, const struct bpf_insn *insn)
>  			DST = do_div(AX, (u32) IMM);
>  			break;
>  		case 1:
> -			AX = abs((s32)DST);
> -			AX = do_div(AX, abs((s32)IMM));
> +			AX = __safe_abs32((s32)DST);
> +			AX = do_div(AX, __safe_abs32((s32)IMM));
>  			if ((s32)DST < 0)
>  				DST = (u32)-AX;
>  			else
> @@ -1955,8 +1960,8 @@ static u64 ___bpf_prog_run(u64 *regs, const struct bpf_insn *insn)
>  			DST = (u32) AX;
>  			break;
>  		case 1:
> -			AX = abs((s32)DST);
> -			do_div(AX, abs((s32)SRC));
> +			AX = __safe_abs32((s32)DST);
> +			do_div(AX, __safe_abs32((s32)SRC));
>  			if (((s32)DST < 0) == ((s32)SRC < 0))
>  				DST = (u32)AX;
>  			else
> @@ -1982,8 +1987,8 @@ static u64 ___bpf_prog_run(u64 *regs, const struct bpf_insn *insn)
>  			DST = (u32) AX;
>  			break;
>  		case 1:
> -			AX = abs((s32)DST);
> -			do_div(AX, abs((s32)IMM));
> +			AX = __safe_abs32((s32)DST);
> +			do_div(AX, __safe_abs32((s32)IMM));
>  			if (((s32)DST < 0) == ((s32)IMM < 0))
>  				DST = (u32)AX;
>  			else
> -- 
> 2.34.1

^ permalink raw reply	[flat|nested] 5+ messages in thread

* Re: [PATCH v2 1/2] bpf: Fix undefined behavior in interpreter sdiv/smod for INT_MIN
  2026-03-09 18:22   ` Mykyta Yatsenko
@ 2026-03-09 18:53     ` Guanni Qu
  0 siblings, 0 replies; 5+ messages in thread
From: Guanni Qu @ 2026-03-09 18:53 UTC (permalink / raw)
  To: Mykyta Yatsenko; +Cc: bpf, daniel, ast, andrii, lkp

You're right, casting to u32 before negating avoids the UB entirely
without needing a special case for S32_MIN. Will send v3 with that
simplification. Thanks!

On Mon, Mar 9, 2026 at 11:22 AM Mykyta Yatsenko
<mykyta.yatsenko5@gmail.com> wrote:
>
> Jenny Guanni Qu <qguanni@gmail.com> writes:
>
> > The BPF interpreter's signed 32-bit division and modulo handlers use
> > the kernel abs() macro on s32 operands. The abs() macro documentation
> > (include/linux/math.h) explicitly states the result is undefined when
> > the input is the type minimum. When DST contains S32_MIN (0x80000000),
> > abs((s32)DST) triggers undefined behavior and returns S32_MIN unchanged
> > on arm64/x86. This value is then sign-extended to u64 as
> > 0xFFFFFFFF80000000, causing do_div() to compute the wrong result.
> >
> > The verifier's abstract interpretation (scalar32_min_max_sdiv) computes
> > the mathematically correct result for range tracking, creating a
> > verifier/interpreter mismatch that can be exploited for out-of-bounds
> > map value access.
> >
> > Introduce __safe_abs32() which handles S32_MIN correctly and replace
> > all 8 abs((s32)...) call sites in the interpreter's sdiv32/smod32
> > handlers.
> >
> > Fixes: ec0e2da95f72 ("bpf: Support new signed div/mod instructions.")
> > Signed-off-by: Jenny Guanni Qu <qguanni@gmail.com>
> > ---
> >  kernel/bpf/core.c | 23 ++++++++++++++---------
> >  1 file changed, 14 insertions(+), 9 deletions(-)
> >
> > diff --git a/kernel/bpf/core.c b/kernel/bpf/core.c
> > index 3ece2da55625..663b9af6d19b 100644
> > --- a/kernel/bpf/core.c
> > +++ b/kernel/bpf/core.c
> > @@ -16,7 +16,6 @@
> >   * Andi Kleen - Fix a few bad bugs and races.
> >   * Kris Katterjohn - Added many additional checks in bpf_check_classic()
> >   */
> > -
> >  #include <uapi/linux/btf.h>
> >  #include <crypto/sha1.h>
> >  #include <linux/filter.h>
> > @@ -1736,6 +1735,12 @@ bool bpf_opcode_in_insntable(u8 code)
> >  }
> >
> >  #ifndef CONFIG_BPF_JIT_ALWAYS_ON
> > +/* Safe absolute value for s32 - abs() is undefined for S32_MIN */
> > +static inline u32 __safe_abs32(s32 x)
> > +{
> > +     return x >= 0 ? (u32)x : x == S32_MIN ? (u32)S32_MIN : (u32)-x;
> can we write it as x >= 0 ? (u32)x : -(u32)x; ?
> By casting to u32 first, the negation operates in unsigned arithmetic
> where overflow does not happen — it wraps.
> Please correct me if I'm wrong.
> > +}
> > +
> >  /**
> >   *   ___bpf_prog_run - run eBPF program on a given context
> >   *   @regs: is the array of MAX_BPF_EXT_REG eBPF pseudo-registers
> > @@ -1900,8 +1905,8 @@ static u64 ___bpf_prog_run(u64 *regs, const struct bpf_insn *insn)
> >                       DST = do_div(AX, (u32) SRC);
> >                       break;
> >               case 1:
> > -                     AX = abs((s32)DST);
> > -                     AX = do_div(AX, abs((s32)SRC));
> > +                     AX = __safe_abs32((s32)DST);
> > +                     AX = do_div(AX, __safe_abs32((s32)SRC));
> >                       if ((s32)DST < 0)
> >                               DST = (u32)-AX;
> >                       else
> > @@ -1928,8 +1933,8 @@ static u64 ___bpf_prog_run(u64 *regs, const struct bpf_insn *insn)
> >                       DST = do_div(AX, (u32) IMM);
> >                       break;
> >               case 1:
> > -                     AX = abs((s32)DST);
> > -                     AX = do_div(AX, abs((s32)IMM));
> > +                     AX = __safe_abs32((s32)DST);
> > +                     AX = do_div(AX, __safe_abs32((s32)IMM));
> >                       if ((s32)DST < 0)
> >                               DST = (u32)-AX;
> >                       else
> > @@ -1955,8 +1960,8 @@ static u64 ___bpf_prog_run(u64 *regs, const struct bpf_insn *insn)
> >                       DST = (u32) AX;
> >                       break;
> >               case 1:
> > -                     AX = abs((s32)DST);
> > -                     do_div(AX, abs((s32)SRC));
> > +                     AX = __safe_abs32((s32)DST);
> > +                     do_div(AX, __safe_abs32((s32)SRC));
> >                       if (((s32)DST < 0) == ((s32)SRC < 0))
> >                               DST = (u32)AX;
> >                       else
> > @@ -1982,8 +1987,8 @@ static u64 ___bpf_prog_run(u64 *regs, const struct bpf_insn *insn)
> >                       DST = (u32) AX;
> >                       break;
> >               case 1:
> > -                     AX = abs((s32)DST);
> > -                     do_div(AX, abs((s32)IMM));
> > +                     AX = __safe_abs32((s32)DST);
> > +                     do_div(AX, __safe_abs32((s32)IMM));
> >                       if (((s32)DST < 0) == ((s32)IMM < 0))
> >                               DST = (u32)AX;
> >                       else
> > --
> > 2.34.1

^ permalink raw reply	[flat|nested] 5+ messages in thread

end of thread, other threads:[~2026-03-09 18:53 UTC | newest]

Thread overview: 5+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-03-06 19:08 [PATCH v2 0/2] bpf: Fix abs(INT_MIN) undefined behavior in interpreter sdiv/smod Jenny Guanni Qu
2026-03-06 19:08 ` [PATCH v2 1/2] bpf: Fix undefined behavior in interpreter sdiv/smod for INT_MIN Jenny Guanni Qu
2026-03-09 18:22   ` Mykyta Yatsenko
2026-03-09 18:53     ` Guanni Qu
2026-03-06 19:08 ` [PATCH v2 2/2] selftests/bpf: Add tests for sdiv32/smod32 with INT_MIN dividend Jenny Guanni Qu

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