Linux Kernel Selftest development
 help / color / mirror / Atom feed
* [PATCH bpf 0/1] bpf: reject overlarge global subprog argument sizes
@ 2026-05-27  5:25 Taegu Ha
  2026-05-27  5:25 ` [PATCH bpf 1/1] " Taegu Ha
  2026-05-28  5:25 ` [PATCH v2 0/1] " Taegu Ha
  0 siblings, 2 replies; 13+ messages in thread
From: Taegu Ha @ 2026-05-27  5:25 UTC (permalink / raw)
  To: Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko
  Cc: Eduard Zingerman, Kumar Kartikeya Dwivedi, Shuah Khan, bpf,
	linux-kernel, linux-kselftest, Taegu Ha

This fixes a verifier argument-size overflow in global BPF subprogram
calls.

For global subprogram generic pointer arguments, the verifier derives the
pointee size from program BTF and stores it as u32. That value is later
passed to check_mem_reg(), which feeds a signed int access-size path. For
stack pointers, the value is negated to mark the call-site validation path
where STACK_POISON is allowed.

That conversion is unsafe for BTF-resolved sizes above INT_MAX. A type
such as int[0x3fffffff] resolves to 0xfffffffc bytes. On the vulnerable
stack path, (int)0xfffffffc becomes -4, and the negation validates only a
four-byte stack object. The callee is still verified with the original
large memory size, so the caller/callee memory contract is inconsistent.

I confirmed the issue with a non-executing raw-BTF verifier reproducer. On
a vulnerable kernel, the verifier accepted a program with:

  - caller object: a four-byte stack slot
  - BTF callee argument: int[0x3fffffff]
  - resolved BTF size: 0xfffffffc
  - accepted callee access: *(u32 *)(r1 + 4)

The relevant vulnerable verifier log contained:

  R1=mem_or_null(id=1,sz=0xfffffffc)
  r0 = *(u32 *)(r1 +4)

The program was only loaded to prove verifier acceptance. It was not
attached or executed.

The fix rejects sizes that cannot be represented by the signed verifier
access-size API before any conversion, and adds a verifier regression test
that expects:

  R1 memory size 4294967292 is too large

Security and reachability:

This is reachable from BPF-loadable contexts that can supply program BTF
and BPF-to-BPF global subprogram calls: direct CAP_BPF or CAP_SYS_ADMIN
callers, explicit BPF token delegation, or privileged BPF loader services
that accept user-controlled BPF objects. The delegated-loader case is
relevant to bpfman/bpfd-style deployments where an API/RBAC boundary can
ask a privileged daemon to perform the BTF and program load. The issue is
not reachable by ordinary unprivileged users on systems where unprivileged
BPF is disabled.

Validation performed:

  - vulnerable QEMU guest: raw-BTF reproducer accepted
  - patched QEMU guest: raw-BTF reproducer rejected the oversized size
  - git diff --check
  - scripts/checkpatch.pl --strict
  - git apply --check on a clean tree
  - clean worktree object build:
    kernel/bpf/verifier.o kernel/bpf/btf.o
  - git send-email --dry-run

Full BPF selftests were not run in this environment because clang is not
installed.

Taegu Ha (1):
  bpf: reject overlarge global subprog argument sizes

 kernel/bpf/verifier.c                           |  7 ++++++-
 .../bpf/progs/verifier_global_subprogs.c        | 17 +++++++++++++++++
 2 files changed, 23 insertions(+), 1 deletion(-)

-- 
2.43.0

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

* [PATCH bpf 1/1] bpf: reject overlarge global subprog argument sizes
  2026-05-27  5:25 [PATCH bpf 0/1] bpf: reject overlarge global subprog argument sizes Taegu Ha
@ 2026-05-27  5:25 ` Taegu Ha
  2026-05-27 16:59   ` Yonghong Song
  2026-05-28  5:25 ` [PATCH v2 0/1] " Taegu Ha
  1 sibling, 1 reply; 13+ messages in thread
From: Taegu Ha @ 2026-05-27  5:25 UTC (permalink / raw)
  To: Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko
  Cc: Eduard Zingerman, Kumar Kartikeya Dwivedi, Shuah Khan, bpf,
	linux-kernel, linux-kselftest, Taegu Ha

Global subprogram argument checking derives generic pointer sizes from BTF
and passes the resolved size to check_mem_reg() as a u32. The access-size
validation path then uses a signed int, and stack pointers negate the value
before calling check_helper_mem_access().

A BTF type such as int[0x3fffffff] resolves to 0xfffffffc bytes. On a stack
pointer, (int)mem_size becomes -4 and the negation validates only four
bytes. A caller can therefore pass a four-byte stack slot while the callee
is verified with a nearly 4GiB memory argument, allowing accesses outside
the caller object.

This was confirmed with a non-executing raw-BTF reproducer. On a
vulnerable kernel, the verifier accepted a program where the caller passed
a four-byte stack slot, while the callee argument was described by BTF as
int[0x3fffffff]. The verifier log showed:

  R1=mem_or_null(id=1,sz=0xfffffffc)
  r0 = *(u32 *)(r1 +4)

The program was only loaded to prove verifier acceptance and was not
attached or executed.

Reject sizes that cannot be represented by the signed verifier access-size
API before any conversion. Cast the non-stack case after the bound check to
make the conversion explicit, and add a verifier regression test for the
oversized BTF argument.

Fixes: 2cb27158adb3 ("bpf: poison dead stack slots")
Signed-off-by: Taegu Ha <hataegu0826@gmail.com>
---
 kernel/bpf/verifier.c                           |  7 ++++++-
 .../bpf/progs/verifier_global_subprogs.c        | 17 +++++++++++++++++
 2 files changed, 23 insertions(+), 1 deletion(-)

diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index 7fb88e1cd7c4..1007f204a1f5 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -7107,6 +7107,11 @@ static int check_mem_reg(struct bpf_verifier_env *env, struct bpf_reg_state *reg
 	struct bpf_reg_state saved_reg;
 	int err;
 
+	if (mem_size > S32_MAX) {
+		verbose(env, "R%d memory size %u is too large\n", regno, mem_size);
+		return -EACCES;
+	}
+
 	if (bpf_register_is_null(reg))
 		return 0;
 
@@ -7119,7 +7124,7 @@ static int check_mem_reg(struct bpf_verifier_env *env, struct bpf_reg_state *reg
 		mark_ptr_not_null_reg(reg);
 	}
 
-	int size = base_type(reg->type) == PTR_TO_STACK ? -(int)mem_size : mem_size;
+	int size = base_type(reg->type) == PTR_TO_STACK ? -(int)mem_size : (int)mem_size;
 
 	err = check_helper_mem_access(env, regno, size, BPF_READ, true, NULL);
 	err = err ?: check_helper_mem_access(env, regno, size, BPF_WRITE, true, NULL);
diff --git a/tools/testing/selftests/bpf/progs/verifier_global_subprogs.c b/tools/testing/selftests/bpf/progs/verifier_global_subprogs.c
index 1e08aff7532e..0ff8f85b4d46 100644
--- a/tools/testing/selftests/bpf/progs/verifier_global_subprogs.c
+++ b/tools/testing/selftests/bpf/progs/verifier_global_subprogs.c
@@ -151,6 +151,23 @@ int anon_user_mem_valid(void *ctx)
 	return subprog_user_anon_mem(&t);
 }
 
+__noinline __weak int subprog_user_anon_mem_huge(int (*p)[0x3fffffff])
+{
+	return p ? (*p)[1] : 0;
+}
+
+SEC("?tracepoint")
+__failure __log_level(2)
+__msg("R1 memory size 4294967292 is too large")
+int anon_user_mem_huge_size_invalid(void *ctx)
+{
+	int (*p)[0x3fffffff];
+	int tiny = 42;
+
+	p = (void *)&tiny;
+	return subprog_user_anon_mem_huge(p) + tiny;
+}
+
 __noinline __weak int subprog_nonnull_ptr_good(int *p1 __arg_nonnull, int *p2 __arg_nonnull)
 {
 	return (*p1) * (*p2); /* good, no need for NULL checks */
-- 
2.43.0

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

* Re: [PATCH bpf 1/1] bpf: reject overlarge global subprog argument sizes
  2026-05-27  5:25 ` [PATCH bpf 1/1] " Taegu Ha
@ 2026-05-27 16:59   ` Yonghong Song
  2026-05-27 17:48     ` 하태구
  0 siblings, 1 reply; 13+ messages in thread
From: Yonghong Song @ 2026-05-27 16:59 UTC (permalink / raw)
  To: Taegu Ha, Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko
  Cc: Eduard Zingerman, Kumar Kartikeya Dwivedi, Shuah Khan, bpf,
	linux-kernel, linux-kselftest



On 5/26/26 10:25 PM, Taegu Ha wrote:
> Global subprogram argument checking derives generic pointer sizes from BTF
> and passes the resolved size to check_mem_reg() as a u32. The access-size
> validation path then uses a signed int, and stack pointers negate the value
> before calling check_helper_mem_access().
>
> A BTF type such as int[0x3fffffff] resolves to 0xfffffffc bytes. On a stack
> pointer, (int)mem_size becomes -4 and the negation validates only four
> bytes. A caller can therefore pass a four-byte stack slot while the callee
> is verified with a nearly 4GiB memory argument, allowing accesses outside
> the caller object.
>
> This was confirmed with a non-executing raw-BTF reproducer. On a
> vulnerable kernel, the verifier accepted a program where the caller passed
> a four-byte stack slot, while the callee argument was described by BTF as
> int[0x3fffffff]. The verifier log showed:
>
>    R1=mem_or_null(id=1,sz=0xfffffffc)
>    r0 = *(u32 *)(r1 +4)
>
> The program was only loaded to prove verifier acceptance and was not
> attached or executed.
>
> Reject sizes that cannot be represented by the signed verifier access-size
> API before any conversion. Cast the non-stack case after the bound check to
> make the conversion explicit, and add a verifier regression test for the
> oversized BTF argument.
>
> Fixes: 2cb27158adb3 ("bpf: poison dead stack slots")
> Signed-off-by: Taegu Ha <hataegu0826@gmail.com>
> ---
>   kernel/bpf/verifier.c                           |  7 ++++++-
>   .../bpf/progs/verifier_global_subprogs.c        | 17 +++++++++++++++++
>   2 files changed, 23 insertions(+), 1 deletion(-)
>
> diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
> index 7fb88e1cd7c4..1007f204a1f5 100644
> --- a/kernel/bpf/verifier.c
> +++ b/kernel/bpf/verifier.c
> @@ -7107,6 +7107,11 @@ static int check_mem_reg(struct bpf_verifier_env *env, struct bpf_reg_state *reg
>   	struct bpf_reg_state saved_reg;
>   	int err;
>   
> +	if (mem_size > S32_MAX) {
> +		verbose(env, "R%d memory size %u is too large\n", regno, mem_size);
> +		return -EACCES;
> +	}
> +
>   	if (bpf_register_is_null(reg))
>   		return 0;
>   
> @@ -7119,7 +7124,7 @@ static int check_mem_reg(struct bpf_verifier_env *env, struct bpf_reg_state *reg
>   		mark_ptr_not_null_reg(reg);
>   	}
>   
> -	int size = base_type(reg->type) == PTR_TO_STACK ? -(int)mem_size : mem_size;
> +	int size = base_type(reg->type) == PTR_TO_STACK ? -(int)mem_size : (int)mem_size;
>   
>   	err = check_helper_mem_access(env, regno, size, BPF_READ, true, NULL);
>   	err = err ?: check_helper_mem_access(env, regno, size, BPF_WRITE, true, NULL);
> diff --git a/tools/testing/selftests/bpf/progs/verifier_global_subprogs.c b/tools/testing/selftests/bpf/progs/verifier_global_subprogs.c
> index 1e08aff7532e..0ff8f85b4d46 100644
> --- a/tools/testing/selftests/bpf/progs/verifier_global_subprogs.c
> +++ b/tools/testing/selftests/bpf/progs/verifier_global_subprogs.c
> @@ -151,6 +151,23 @@ int anon_user_mem_valid(void *ctx)
>   	return subprog_user_anon_mem(&t);
>   }
>   
> +__noinline __weak int subprog_user_anon_mem_huge(int (*p)[0x3fffffff])
> +{
> +	return p ? (*p)[1] : 0;
> +}
> +
> +SEC("?tracepoint")
> +__failure __log_level(2)
> +__msg("R1 memory size 4294967292 is too large")
> +int anon_user_mem_huge_size_invalid(void *ctx)
> +{
> +	int (*p)[0x3fffffff];
> +	int tiny = 42;
> +
> +	p = (void *)&tiny;
> +	return subprog_user_anon_mem_huge(p) + tiny;
> +}

Without verifier.c change, verification is successful.

The objdump:

0000000000000160 <subprog_user_anon_mem_huge>:
; {
       44:       b4 00 00 00 00 00 00 00 w0 = 0x0
;       return p ? (*p)[1] : 0;
       45:       15 01 01 00 00 00 00 00 if r1 == 0x0 goto +0x1 <L0>
       46:       61 10 04 00 00 00 00 00 w0 = *(u32 *)(r1 + 0x4)
<L0>:
       47:       95 00 00 00 00 00 00 00 exit

0000000000000040 <anon_user_mem_huge_size_invalid>:
;       int tiny = 42;
        8:       b4 01 00 00 2a 00 00 00 w1 = 0x2a
        9:       63 1a fc ff 00 00 00 00 *(u32 *)(r10 - 0x4) = w1
       10:       bf a1 00 00 00 00 00 00 r1 = r10
       11:       07 01 00 00 fc ff ff ff r1 += -0x4
;       return subprog_user_anon_mem_huge(p) + tiny;
       12:       85 10 00 00 ff ff ff ff call -0x1
                 0000000000000060:  R_BPF_64_32  subprog_user_anon_mem_huge
       13:       61 a1 fc ff 00 00 00 00 w1 = *(u32 *)(r10 - 0x4)
       14:       0c 01 00 00 00 00 00 00 w1 += w0
       15:       bc 10 00 00 00 00 00 00 w0 = w1
       16:       95 00 00 00 00 00 00 00 exit

The big 0x3fffffff does not really matter.

> +
>   __noinline __weak int subprog_nonnull_ptr_good(int *p1 __arg_nonnull, int *p2 __arg_nonnull)
>   {
>   	return (*p1) * (*p2); /* good, no need for NULL checks */


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

* Re: [PATCH bpf 1/1] bpf: reject overlarge global subprog argument sizes
  2026-05-27 16:59   ` Yonghong Song
@ 2026-05-27 17:48     ` 하태구
  2026-05-27 17:53       ` 하태구
  0 siblings, 1 reply; 13+ messages in thread
From: 하태구 @ 2026-05-27 17:48 UTC (permalink / raw)
  To: Yonghong Song
  Cc: Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko,
	Eduard Zingerman, Kumar Kartikeya Dwivedi, Shuah Khan, bpf,
	linux-kernel, linux-kselftest

Hi Yonghong, Thanks for checking. Yes, the generated instruction is
only a 4-byte load at r1 + 4. The large array bound is not expected to
appear in the instruction stream. It matters through the BTF-derived
argument size used by the verifier. For the global subprog argument:
int (*p)[0x3fffffff] btf_resolve_size() resolves the pointee size to
0xfffffffc. At the call site, R1 is PTR_TO_STACK pointing to the
4-byte local variable at fp-4. The current stack path in
check_mem_reg() computes: size = -(int)mem_size so 0xfffffffc becomes
-4 as a signed int, and the negation validates only 4 bytes. This lets
the caller pass the 4-byte stack slot. The callee is then verified
independently with R1 as PTR_TO_MEM and mem_size 0xfffffffc, so the
4-byte access at r1 + 4 is accepted as being inside the BTF-described
memory region. At runtime, however, the actual argument value is still
fp-4, so r1 + 4 addresses fp+0, outside the 4-byte object that the
caller provided. I rechecked this on current origin/master
eb3f4b7426cf with the raw-BTF reproducer. The verifier accepts the
program and logs: 4: (85) call pc+2 // r1=fp0-4 ... 7:
R1=mem_or_null(id=1,sz=0xfffffffc) R10=fp0 9: (61) r0 = *(u32 *)(r1
+4) Func#1 ('subprog') is safe for any args that match its prototype
So I agree that the objdump only shows a small load. The issue is that
the oversized BTF pointee size wraps the caller-side stack argument
check and lets that small load be accepted past the caller object. I
can send a v2 with the commit message and selftest comments clarified
to make this distinction explicit. Thanks, Taegu

2026년 5월 28일 (목) 오전 1:59, Yonghong Song <yonghong.song@linux.dev>님이 작성:
>
>
>
> On 5/26/26 10:25 PM, Taegu Ha wrote:
> > Global subprogram argument checking derives generic pointer sizes from BTF
> > and passes the resolved size to check_mem_reg() as a u32. The access-size
> > validation path then uses a signed int, and stack pointers negate the value
> > before calling check_helper_mem_access().
> >
> > A BTF type such as int[0x3fffffff] resolves to 0xfffffffc bytes. On a stack
> > pointer, (int)mem_size becomes -4 and the negation validates only four
> > bytes. A caller can therefore pass a four-byte stack slot while the callee
> > is verified with a nearly 4GiB memory argument, allowing accesses outside
> > the caller object.
> >
> > This was confirmed with a non-executing raw-BTF reproducer. On a
> > vulnerable kernel, the verifier accepted a program where the caller passed
> > a four-byte stack slot, while the callee argument was described by BTF as
> > int[0x3fffffff]. The verifier log showed:
> >
> >    R1=mem_or_null(id=1,sz=0xfffffffc)
> >    r0 = *(u32 *)(r1 +4)
> >
> > The program was only loaded to prove verifier acceptance and was not
> > attached or executed.
> >
> > Reject sizes that cannot be represented by the signed verifier access-size
> > API before any conversion. Cast the non-stack case after the bound check to
> > make the conversion explicit, and add a verifier regression test for the
> > oversized BTF argument.
> >
> > Fixes: 2cb27158adb3 ("bpf: poison dead stack slots")
> > Signed-off-by: Taegu Ha <hataegu0826@gmail.com>
> > ---
> >   kernel/bpf/verifier.c                           |  7 ++++++-
> >   .../bpf/progs/verifier_global_subprogs.c        | 17 +++++++++++++++++
> >   2 files changed, 23 insertions(+), 1 deletion(-)
> >
> > diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
> > index 7fb88e1cd7c4..1007f204a1f5 100644
> > --- a/kernel/bpf/verifier.c
> > +++ b/kernel/bpf/verifier.c
> > @@ -7107,6 +7107,11 @@ static int check_mem_reg(struct bpf_verifier_env *env, struct bpf_reg_state *reg
> >       struct bpf_reg_state saved_reg;
> >       int err;
> >
> > +     if (mem_size > S32_MAX) {
> > +             verbose(env, "R%d memory size %u is too large\n", regno, mem_size);
> > +             return -EACCES;
> > +     }
> > +
> >       if (bpf_register_is_null(reg))
> >               return 0;
> >
> > @@ -7119,7 +7124,7 @@ static int check_mem_reg(struct bpf_verifier_env *env, struct bpf_reg_state *reg
> >               mark_ptr_not_null_reg(reg);
> >       }
> >
> > -     int size = base_type(reg->type) == PTR_TO_STACK ? -(int)mem_size : mem_size;
> > +     int size = base_type(reg->type) == PTR_TO_STACK ? -(int)mem_size : (int)mem_size;
> >
> >       err = check_helper_mem_access(env, regno, size, BPF_READ, true, NULL);
> >       err = err ?: check_helper_mem_access(env, regno, size, BPF_WRITE, true, NULL);
> > diff --git a/tools/testing/selftests/bpf/progs/verifier_global_subprogs.c b/tools/testing/selftests/bpf/progs/verifier_global_subprogs.c
> > index 1e08aff7532e..0ff8f85b4d46 100644
> > --- a/tools/testing/selftests/bpf/progs/verifier_global_subprogs.c
> > +++ b/tools/testing/selftests/bpf/progs/verifier_global_subprogs.c
> > @@ -151,6 +151,23 @@ int anon_user_mem_valid(void *ctx)
> >       return subprog_user_anon_mem(&t);
> >   }
> >
> > +__noinline __weak int subprog_user_anon_mem_huge(int (*p)[0x3fffffff])
> > +{
> > +     return p ? (*p)[1] : 0;
> > +}
> > +
> > +SEC("?tracepoint")
> > +__failure __log_level(2)
> > +__msg("R1 memory size 4294967292 is too large")
> > +int anon_user_mem_huge_size_invalid(void *ctx)
> > +{
> > +     int (*p)[0x3fffffff];
> > +     int tiny = 42;
> > +
> > +     p = (void *)&tiny;
> > +     return subprog_user_anon_mem_huge(p) + tiny;
> > +}
>
> Without verifier.c change, verification is successful.
>
> The objdump:
>
> 0000000000000160 <subprog_user_anon_mem_huge>:
> ; {
>        44:       b4 00 00 00 00 00 00 00 w0 = 0x0
> ;       return p ? (*p)[1] : 0;
>        45:       15 01 01 00 00 00 00 00 if r1 == 0x0 goto +0x1 <L0>
>        46:       61 10 04 00 00 00 00 00 w0 = *(u32 *)(r1 + 0x4)
> <L0>:
>        47:       95 00 00 00 00 00 00 00 exit
>
> 0000000000000040 <anon_user_mem_huge_size_invalid>:
> ;       int tiny = 42;
>         8:       b4 01 00 00 2a 00 00 00 w1 = 0x2a
>         9:       63 1a fc ff 00 00 00 00 *(u32 *)(r10 - 0x4) = w1
>        10:       bf a1 00 00 00 00 00 00 r1 = r10
>        11:       07 01 00 00 fc ff ff ff r1 += -0x4
> ;       return subprog_user_anon_mem_huge(p) + tiny;
>        12:       85 10 00 00 ff ff ff ff call -0x1
>                  0000000000000060:  R_BPF_64_32  subprog_user_anon_mem_huge
>        13:       61 a1 fc ff 00 00 00 00 w1 = *(u32 *)(r10 - 0x4)
>        14:       0c 01 00 00 00 00 00 00 w1 += w0
>        15:       bc 10 00 00 00 00 00 00 w0 = w1
>        16:       95 00 00 00 00 00 00 00 exit
>
> The big 0x3fffffff does not really matter.
>
> > +
> >   __noinline __weak int subprog_nonnull_ptr_good(int *p1 __arg_nonnull, int *p2 __arg_nonnull)
> >   {
> >       return (*p1) * (*p2); /* good, no need for NULL checks */
>

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

* Re: [PATCH bpf 1/1] bpf: reject overlarge global subprog argument sizes
  2026-05-27 17:48     ` 하태구
@ 2026-05-27 17:53       ` 하태구
  2026-05-28  5:03         ` Yonghong Song
  0 siblings, 1 reply; 13+ messages in thread
From: 하태구 @ 2026-05-27 17:53 UTC (permalink / raw)
  To: Yonghong Song
  Cc: Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko,
	Eduard Zingerman, Kumar Kartikeya Dwivedi, Shuah Khan, bpf,
	linux-kernel, linux-kselftest

Sorry for the poor formatting in the previous reply. Reflowing the main
point:

The 0x3fffffff array bound is not expected to appear in the generated
BPF instructions. It affects the verifier through the BTF-derived
argument size.

For:

  int (*p)[0x3fffffff]

btf_resolve_size() returns 0xfffffffc. At the call site R1 is fp-4,
pointing to a 4-byte stack slot. check_mem_reg() currently does:

  size = -(int)mem_size

for PTR_TO_STACK, so 0xfffffffc is converted to -4 and then to a
4-byte stack check. The caller side therefore accepts fp-4.

The callee is then verified with:

  R1=mem_or_null(id=1,sz=0xfffffffc)

so its 4-byte load at r1 + 4 is considered inside the BTF-described
memory region. But the actual argument is still fp-4, so r1 + 4 is
fp+0, outside the 4-byte caller object.

I rechecked this on current origin/master eb3f4b7426cf with the raw-BTF
reproducer, and the verifier accepted the program with:

  4: (85) call pc+2 // r1=fp0-4
  7: R1=mem_or_null(id=1,sz=0xfffffffc) R10=fp0
  9: (61) r0 = *(u32 *)(r1 +4)
  Func#1 ('subprog') is safe for any args that match its prototype

So the issue is not a large immediate in the instruction stream, but the
oversized BTF pointee size wrapping the caller-side stack argument check.

I can send v2 with the commit message and selftest comments clarified.

Thanks,
Taegu

2026년 5월 28일 (목) 오전 2:48, 하태구 <hataegu0826@gmail.com>님이 작성:
>
> Hi Yonghong, Thanks for checking. Yes, the generated instruction is
> only a 4-byte load at r1 + 4. The large array bound is not expected to
> appear in the instruction stream. It matters through the BTF-derived
> argument size used by the verifier. For the global subprog argument:
> int (*p)[0x3fffffff] btf_resolve_size() resolves the pointee size to
> 0xfffffffc. At the call site, R1 is PTR_TO_STACK pointing to the
> 4-byte local variable at fp-4. The current stack path in
> check_mem_reg() computes: size = -(int)mem_size so 0xfffffffc becomes
> -4 as a signed int, and the negation validates only 4 bytes. This lets
> the caller pass the 4-byte stack slot. The callee is then verified
> independently with R1 as PTR_TO_MEM and mem_size 0xfffffffc, so the
> 4-byte access at r1 + 4 is accepted as being inside the BTF-described
> memory region. At runtime, however, the actual argument value is still
> fp-4, so r1 + 4 addresses fp+0, outside the 4-byte object that the
> caller provided. I rechecked this on current origin/master
> eb3f4b7426cf with the raw-BTF reproducer. The verifier accepts the
> program and logs: 4: (85) call pc+2 // r1=fp0-4 ... 7:
> R1=mem_or_null(id=1,sz=0xfffffffc) R10=fp0 9: (61) r0 = *(u32 *)(r1
> +4) Func#1 ('subprog') is safe for any args that match its prototype
> So I agree that the objdump only shows a small load. The issue is that
> the oversized BTF pointee size wraps the caller-side stack argument
> check and lets that small load be accepted past the caller object. I
> can send a v2 with the commit message and selftest comments clarified
> to make this distinction explicit. Thanks, Taegu
>
> 2026년 5월 28일 (목) 오전 1:59, Yonghong Song <yonghong.song@linux.dev>님이 작성:
> >
> >
> >
> > On 5/26/26 10:25 PM, Taegu Ha wrote:
> > > Global subprogram argument checking derives generic pointer sizes from BTF
> > > and passes the resolved size to check_mem_reg() as a u32. The access-size
> > > validation path then uses a signed int, and stack pointers negate the value
> > > before calling check_helper_mem_access().
> > >
> > > A BTF type such as int[0x3fffffff] resolves to 0xfffffffc bytes. On a stack
> > > pointer, (int)mem_size becomes -4 and the negation validates only four
> > > bytes. A caller can therefore pass a four-byte stack slot while the callee
> > > is verified with a nearly 4GiB memory argument, allowing accesses outside
> > > the caller object.
> > >
> > > This was confirmed with a non-executing raw-BTF reproducer. On a
> > > vulnerable kernel, the verifier accepted a program where the caller passed
> > > a four-byte stack slot, while the callee argument was described by BTF as
> > > int[0x3fffffff]. The verifier log showed:
> > >
> > >    R1=mem_or_null(id=1,sz=0xfffffffc)
> > >    r0 = *(u32 *)(r1 +4)
> > >
> > > The program was only loaded to prove verifier acceptance and was not
> > > attached or executed.
> > >
> > > Reject sizes that cannot be represented by the signed verifier access-size
> > > API before any conversion. Cast the non-stack case after the bound check to
> > > make the conversion explicit, and add a verifier regression test for the
> > > oversized BTF argument.
> > >
> > > Fixes: 2cb27158adb3 ("bpf: poison dead stack slots")
> > > Signed-off-by: Taegu Ha <hataegu0826@gmail.com>
> > > ---
> > >   kernel/bpf/verifier.c                           |  7 ++++++-
> > >   .../bpf/progs/verifier_global_subprogs.c        | 17 +++++++++++++++++
> > >   2 files changed, 23 insertions(+), 1 deletion(-)
> > >
> > > diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
> > > index 7fb88e1cd7c4..1007f204a1f5 100644
> > > --- a/kernel/bpf/verifier.c
> > > +++ b/kernel/bpf/verifier.c
> > > @@ -7107,6 +7107,11 @@ static int check_mem_reg(struct bpf_verifier_env *env, struct bpf_reg_state *reg
> > >       struct bpf_reg_state saved_reg;
> > >       int err;
> > >
> > > +     if (mem_size > S32_MAX) {
> > > +             verbose(env, "R%d memory size %u is too large\n", regno, mem_size);
> > > +             return -EACCES;
> > > +     }
> > > +
> > >       if (bpf_register_is_null(reg))
> > >               return 0;
> > >
> > > @@ -7119,7 +7124,7 @@ static int check_mem_reg(struct bpf_verifier_env *env, struct bpf_reg_state *reg
> > >               mark_ptr_not_null_reg(reg);
> > >       }
> > >
> > > -     int size = base_type(reg->type) == PTR_TO_STACK ? -(int)mem_size : mem_size;
> > > +     int size = base_type(reg->type) == PTR_TO_STACK ? -(int)mem_size : (int)mem_size;
> > >
> > >       err = check_helper_mem_access(env, regno, size, BPF_READ, true, NULL);
> > >       err = err ?: check_helper_mem_access(env, regno, size, BPF_WRITE, true, NULL);
> > > diff --git a/tools/testing/selftests/bpf/progs/verifier_global_subprogs.c b/tools/testing/selftests/bpf/progs/verifier_global_subprogs.c
> > > index 1e08aff7532e..0ff8f85b4d46 100644
> > > --- a/tools/testing/selftests/bpf/progs/verifier_global_subprogs.c
> > > +++ b/tools/testing/selftests/bpf/progs/verifier_global_subprogs.c
> > > @@ -151,6 +151,23 @@ int anon_user_mem_valid(void *ctx)
> > >       return subprog_user_anon_mem(&t);
> > >   }
> > >
> > > +__noinline __weak int subprog_user_anon_mem_huge(int (*p)[0x3fffffff])
> > > +{
> > > +     return p ? (*p)[1] : 0;
> > > +}
> > > +
> > > +SEC("?tracepoint")
> > > +__failure __log_level(2)
> > > +__msg("R1 memory size 4294967292 is too large")
> > > +int anon_user_mem_huge_size_invalid(void *ctx)
> > > +{
> > > +     int (*p)[0x3fffffff];
> > > +     int tiny = 42;
> > > +
> > > +     p = (void *)&tiny;
> > > +     return subprog_user_anon_mem_huge(p) + tiny;
> > > +}
> >
> > Without verifier.c change, verification is successful.
> >
> > The objdump:
> >
> > 0000000000000160 <subprog_user_anon_mem_huge>:
> > ; {
> >        44:       b4 00 00 00 00 00 00 00 w0 = 0x0
> > ;       return p ? (*p)[1] : 0;
> >        45:       15 01 01 00 00 00 00 00 if r1 == 0x0 goto +0x1 <L0>
> >        46:       61 10 04 00 00 00 00 00 w0 = *(u32 *)(r1 + 0x4)
> > <L0>:
> >        47:       95 00 00 00 00 00 00 00 exit
> >
> > 0000000000000040 <anon_user_mem_huge_size_invalid>:
> > ;       int tiny = 42;
> >         8:       b4 01 00 00 2a 00 00 00 w1 = 0x2a
> >         9:       63 1a fc ff 00 00 00 00 *(u32 *)(r10 - 0x4) = w1
> >        10:       bf a1 00 00 00 00 00 00 r1 = r10
> >        11:       07 01 00 00 fc ff ff ff r1 += -0x4
> > ;       return subprog_user_anon_mem_huge(p) + tiny;
> >        12:       85 10 00 00 ff ff ff ff call -0x1
> >                  0000000000000060:  R_BPF_64_32  subprog_user_anon_mem_huge
> >        13:       61 a1 fc ff 00 00 00 00 w1 = *(u32 *)(r10 - 0x4)
> >        14:       0c 01 00 00 00 00 00 00 w1 += w0
> >        15:       bc 10 00 00 00 00 00 00 w0 = w1
> >        16:       95 00 00 00 00 00 00 00 exit
> >
> > The big 0x3fffffff does not really matter.
> >
> > > +
> > >   __noinline __weak int subprog_nonnull_ptr_good(int *p1 __arg_nonnull, int *p2 __arg_nonnull)
> > >   {
> > >       return (*p1) * (*p2); /* good, no need for NULL checks */
> >

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

* Re: [PATCH bpf 1/1] bpf: reject overlarge global subprog argument sizes
  2026-05-27 17:53       ` 하태구
@ 2026-05-28  5:03         ` Yonghong Song
  0 siblings, 0 replies; 13+ messages in thread
From: Yonghong Song @ 2026-05-28  5:03 UTC (permalink / raw)
  To: 하태구
  Cc: Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko,
	Eduard Zingerman, Kumar Kartikeya Dwivedi, Shuah Khan, bpf,
	linux-kernel, linux-kselftest



On 5/27/26 10:53 AM, 하태구 wrote:
> Sorry for the poor formatting in the previous reply. Reflowing the main
> point:
>
> The 0x3fffffff array bound is not expected to appear in the generated
> BPF instructions. It affects the verifier through the BTF-derived
> argument size.
>
> For:
>
>    int (*p)[0x3fffffff]
>
> btf_resolve_size() returns 0xfffffffc. At the call site R1 is fp-4,
> pointing to a 4-byte stack slot. check_mem_reg() currently does:
>
>    size = -(int)mem_size
>
> for PTR_TO_STACK, so 0xfffffffc is converted to -4 and then to a
> 4-byte stack check. The caller side therefore accepts fp-4.
>
> The callee is then verified with:
>
>    R1=mem_or_null(id=1,sz=0xfffffffc)
>
> so its 4-byte load at r1 + 4 is considered inside the BTF-described
> memory region. But the actual argument is still fp-4, so r1 + 4 is
> fp+0, outside the 4-byte caller object.
>
> I rechecked this on current origin/master eb3f4b7426cf with the raw-BTF
> reproducer, and the verifier accepted the program with:
>
>    4: (85) call pc+2 // r1=fp0-4
>    7: R1=mem_or_null(id=1,sz=0xfffffffc) R10=fp0
>    9: (61) r0 = *(u32 *)(r1 +4)
>    Func#1 ('subprog') is safe for any args that match its prototype

The below is my verifier log:

arg#0 reference type('UNKNOWN ') size cannot be determined: -22
0: R1=ctx() R10=fp0
; int tiny = 42; @ verifier_global_subprogs.c:166
0: (b4) w1 = 42                       ; R1=42
1: (63) *(u32 *)(r10 -4) = r1         ; R1=42 R10=fp0 fp-8=mmmm????
2: (bf) r1 = r10                      ; R1=fp0 R10=fp0
3: (07) r1 += -4                      ; R1=fp-4
; return subprog_user_anon_mem_huge(p) + tiny; @ verifier_global_subprogs.c:169
4: (85) call pc+4
Func#1 ('subprog_user_anon_mem_huge') is global and assumed valid.
5: R0=scalar()
5: (61) r1 = *(u32 *)(r10 -4)         ; R1=scalar(smin=0,smax=umax=0xffffffff,var_off=(0x0; 0xffffffff)) R10=fp0 fp-8=mmmmpppp
6: (0c) w1 += w0                      ; R0=scalar() R1=scalar(smin=0,smax=umax=0xffffffff,var_off=(0x0; 0xffffffff))
7: (bc) w0 = w1                       ; R0=scalar(id=1,smin=0,smax=umax=0xffffffff,var_off=(0x0; 0xffffffff))
                                         R1=scalar(id=1,smin=0,smax=umax=0xffffffff,var)
8: (95) exit
Validating subprog_user_anon_mem_huge() func#1...
9: R1=mem_or_null(id=2,sz=0xfffffffc) R10=fp0
; __noinline __weak int subprog_user_anon_mem_huge(int (*p)[0x3fffffff]) @ verifier_global_subprogs.c:155
9: (b4) w0 = 0                        ; R0=0
; return p ? (*p)[1] : 0; @ verifier_global_subprogs.c:157
10: (15) if r1 == 0x0 goto pc+1       ; R1=mem(sz=0xfffffffc)
11: (61) r0 = *(u32 *)(r1 +4)         ; R0=scalar(smin=0,smax=umax=0xffffffff,var_off=(0x0; 0xffffffff))
12: (95) exit

from 10 to 12: R0=0 R10=fp0
12: R0=0 R10=fp0
12: (95) exit
Func#1 ('subprog_user_anon_mem_huge') is safe for any args that match its prototype
processed 14 insns (limit 1000000) max_states_per_insn 0 total_states 1 peak_states 1 mark_read 0

As you mentioned, the key thing is due to this line of code:
         int size = base_type(reg->type) == PTR_TO_STACK ? -(int)mem_size : mem_size;

If mem_size = 0xfffffffc, then -(int)mem_size will be 4. Unfortunately,
size 4 does have coverage due to *(u32 *)(r10 -4) = r1.
If mem_size = 0xfffffff8, then -(int)mem_size will be 8 and verifier
will reject it since only 4 slot for stack.

>
> So the issue is not a large immediate in the instruction stream, but the
> oversized BTF pointee size wrapping the caller-side stack argument check.
>
> I can send v2 with the commit message and selftest comments clarified.

Please have more details in the commit message.

>
> Thanks,
> Taegu
>
> 2026년 5월 28일 (목) 오전 2:48, 하태구 <hataegu0826@gmail.com>님이 작성:
>> Hi Yonghong, Thanks for checking. Yes, the generated instruction is
>> only a 4-byte load at r1 + 4. The large array bound is not expected to
>> appear in the instruction stream. It matters through the BTF-derived
>> argument size used by the verifier. For the global subprog argument:
>> int (*p)[0x3fffffff] btf_resolve_size() resolves the pointee size to
>> 0xfffffffc. At the call site, R1 is PTR_TO_STACK pointing to the
>> 4-byte local variable at fp-4. The current stack path in
>> check_mem_reg() computes: size = -(int)mem_size so 0xfffffffc becomes
>> -4 as a signed int, and the negation validates only 4 bytes. This lets
>> the caller pass the 4-byte stack slot. The callee is then verified
>> independently with R1 as PTR_TO_MEM and mem_size 0xfffffffc, so the
>> 4-byte access at r1 + 4 is accepted as being inside the BTF-described
>> memory region. At runtime, however, the actual argument value is still
>> fp-4, so r1 + 4 addresses fp+0, outside the 4-byte object that the
>> caller provided. I rechecked this on current origin/master
>> eb3f4b7426cf with the raw-BTF reproducer. The verifier accepts the
>> program and logs: 4: (85) call pc+2 // r1=fp0-4 ... 7:
>> R1=mem_or_null(id=1,sz=0xfffffffc) R10=fp0 9: (61) r0 = *(u32 *)(r1
>> +4) Func#1 ('subprog') is safe for any args that match its prototype
>> So I agree that the objdump only shows a small load. The issue is that
>> the oversized BTF pointee size wraps the caller-side stack argument
>> check and lets that small load be accepted past the caller object. I
>> can send a v2 with the commit message and selftest comments clarified
>> to make this distinction explicit. Thanks, Taegu
>>
>> 2026년 5월 28일 (목) 오전 1:59, Yonghong Song <yonghong.song@linux.dev>님이 작성:
>>>
>>>
>>> On 5/26/26 10:25 PM, Taegu Ha wrote:
>>>> Global subprogram argument checking derives generic pointer sizes from BTF
>>>> and passes the resolved size to check_mem_reg() as a u32. The access-size
>>>> validation path then uses a signed int, and stack pointers negate the value
>>>> before calling check_helper_mem_access().
>>>>
>>>> A BTF type such as int[0x3fffffff] resolves to 0xfffffffc bytes. On a stack
>>>> pointer, (int)mem_size becomes -4 and the negation validates only four
>>>> bytes. A caller can therefore pass a four-byte stack slot while the callee
>>>> is verified with a nearly 4GiB memory argument, allowing accesses outside
>>>> the caller object.
>>>>
>>>> This was confirmed with a non-executing raw-BTF reproducer. On a
>>>> vulnerable kernel, the verifier accepted a program where the caller passed
>>>> a four-byte stack slot, while the callee argument was described by BTF as
>>>> int[0x3fffffff]. The verifier log showed:
>>>>
>>>>     R1=mem_or_null(id=1,sz=0xfffffffc)
>>>>     r0 = *(u32 *)(r1 +4)
>>>>
>>>> The program was only loaded to prove verifier acceptance and was not
>>>> attached or executed.
>>>>
>>>> Reject sizes that cannot be represented by the signed verifier access-size
>>>> API before any conversion. Cast the non-stack case after the bound check to
>>>> make the conversion explicit, and add a verifier regression test for the
>>>> oversized BTF argument.
>>>>
>>>> Fixes: 2cb27158adb3 ("bpf: poison dead stack slots")
>>>> Signed-off-by: Taegu Ha <hataegu0826@gmail.com>
>>>> ---
>>>>    kernel/bpf/verifier.c                           |  7 ++++++-
>>>>    .../bpf/progs/verifier_global_subprogs.c        | 17 +++++++++++++++++
>>>>    2 files changed, 23 insertions(+), 1 deletion(-)
>>>>
>>>> diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
>>>> index 7fb88e1cd7c4..1007f204a1f5 100644
>>>> --- a/kernel/bpf/verifier.c
>>>> +++ b/kernel/bpf/verifier.c
>>>> @@ -7107,6 +7107,11 @@ static int check_mem_reg(struct bpf_verifier_env *env, struct bpf_reg_state *reg
>>>>        struct bpf_reg_state saved_reg;
>>>>        int err;
>>>>
>>>> +     if (mem_size > S32_MAX) {
>>>> +             verbose(env, "R%d memory size %u is too large\n", regno, mem_size);
>>>> +             return -EACCES;
>>>> +     }
>>>> +
>>>>        if (bpf_register_is_null(reg))
>>>>                return 0;
>>>>
>>>> @@ -7119,7 +7124,7 @@ static int check_mem_reg(struct bpf_verifier_env *env, struct bpf_reg_state *reg
>>>>                mark_ptr_not_null_reg(reg);
>>>>        }
>>>>
>>>> -     int size = base_type(reg->type) == PTR_TO_STACK ? -(int)mem_size : mem_size;
>>>> +     int size = base_type(reg->type) == PTR_TO_STACK ? -(int)mem_size : (int)mem_size;

Since we have mem_size > S32_MAX guard, the above 'int size = ...' can be stored back to
	int size = base_type(reg->type) == PTR_TO_STACK ? -(int)mem_size : mem_size;

>>>>
>>>>        err = check_helper_mem_access(env, regno, size, BPF_READ, true, NULL);
>>>>        err = err ?: check_helper_mem_access(env, regno, size, BPF_WRITE, true, NULL);
>>>> diff --git a/tools/testing/selftests/bpf/progs/verifier_global_subprogs.c b/tools/testing/selftests/bpf/progs/verifier_global_subprogs.c
>>>> index 1e08aff7532e..0ff8f85b4d46 100644
>>>> --- a/tools/testing/selftests/bpf/progs/verifier_global_subprogs.c
>>>> +++ b/tools/testing/selftests/bpf/progs/verifier_global_subprogs.c
>>>> @@ -151,6 +151,23 @@ int anon_user_mem_valid(void *ctx)
>>>>        return subprog_user_anon_mem(&t);
>>>>    }
>>>>
>>>> +__noinline __weak int subprog_user_anon_mem_huge(int (*p)[0x3fffffff])
>>>> +{
>>>> +     return p ? (*p)[1] : 0;
>>>> +}
>>>> +
>>>> +SEC("?tracepoint")
>>>> +__failure __log_level(2)
>>>> +__msg("R1 memory size 4294967292 is too large")
>>>> +int anon_user_mem_huge_size_invalid(void *ctx)
>>>> +{
>>>> +     int (*p)[0x3fffffff];
>>>> +     int tiny = 42;
>>>> +
>>>> +     p = (void *)&tiny;
>>>> +     return subprog_user_anon_mem_huge(p) + tiny;
>>>> +}
>>> Without verifier.c change, verification is successful.
>>>
>>> The objdump:
>>>
>>> 0000000000000160 <subprog_user_anon_mem_huge>:
>>> ; {
>>>         44:       b4 00 00 00 00 00 00 00 w0 = 0x0
>>> ;       return p ? (*p)[1] : 0;
>>>         45:       15 01 01 00 00 00 00 00 if r1 == 0x0 goto +0x1 <L0>
>>>         46:       61 10 04 00 00 00 00 00 w0 = *(u32 *)(r1 + 0x4)
>>> <L0>:
>>>         47:       95 00 00 00 00 00 00 00 exit
>>>
>>> 0000000000000040 <anon_user_mem_huge_size_invalid>:
>>> ;       int tiny = 42;
>>>          8:       b4 01 00 00 2a 00 00 00 w1 = 0x2a
>>>          9:       63 1a fc ff 00 00 00 00 *(u32 *)(r10 - 0x4) = w1
>>>         10:       bf a1 00 00 00 00 00 00 r1 = r10
>>>         11:       07 01 00 00 fc ff ff ff r1 += -0x4
>>> ;       return subprog_user_anon_mem_huge(p) + tiny;
>>>         12:       85 10 00 00 ff ff ff ff call -0x1
>>>                   0000000000000060:  R_BPF_64_32  subprog_user_anon_mem_huge
>>>         13:       61 a1 fc ff 00 00 00 00 w1 = *(u32 *)(r10 - 0x4)
>>>         14:       0c 01 00 00 00 00 00 00 w1 += w0
>>>         15:       bc 10 00 00 00 00 00 00 w0 = w1
>>>         16:       95 00 00 00 00 00 00 00 exit
>>>
>>> The big 0x3fffffff does not really matter.
>>>
>>>> +
>>>>    __noinline __weak int subprog_nonnull_ptr_good(int *p1 __arg_nonnull, int *p2 __arg_nonnull)
>>>>    {
>>>>        return (*p1) * (*p2); /* good, no need for NULL checks */


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

* [PATCH v2 0/1] bpf: reject overlarge global subprog argument sizes
  2026-05-27  5:25 [PATCH bpf 0/1] bpf: reject overlarge global subprog argument sizes Taegu Ha
  2026-05-27  5:25 ` [PATCH bpf 1/1] " Taegu Ha
@ 2026-05-28  5:25 ` Taegu Ha
  2026-05-28  5:25   ` [PATCH v2 1/1] " Taegu Ha
  2026-05-28  6:21   ` [PATCH bpf-next v3] " Taegu Ha
  1 sibling, 2 replies; 13+ messages in thread
From: Taegu Ha @ 2026-05-28  5:25 UTC (permalink / raw)
  To: Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko
  Cc: Yonghong Song, Eduard Zingerman, Kumar Kartikeya Dwivedi,
	Shuah Khan, bpf, linux-kernel, linux-kselftest, Taegu Ha

This rejects BTF-derived global subprog argument sizes that cannot be
represented by the verifier's signed access-size API.

The issue is not a large immediate in the generated BPF instruction stream.
The oversized BTF pointee size wraps the caller-side PTR_TO_STACK argument
check before the callee is verified with the original large mem_size.

Changes in v2:
- Expand the commit message to describe the caller/callee verifier mismatch.
- Keep the existing size expression unchanged after adding the S32_MAX guard,
  as suggested by Yonghong.

Taegu Ha (1):
  bpf: reject overlarge global subprog argument sizes

 kernel/bpf/verifier.c                           |  5 +++++
 .../bpf/progs/verifier_global_subprogs.c        | 17 +++++++++++++++++
 2 files changed, 22 insertions(+)

-- 
2.43.0

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

* [PATCH v2 1/1] bpf: reject overlarge global subprog argument sizes
  2026-05-28  5:25 ` [PATCH v2 0/1] " Taegu Ha
@ 2026-05-28  5:25   ` Taegu Ha
  2026-05-28  6:05     ` bot+bpf-ci
  2026-05-28  6:21   ` [PATCH bpf-next v3] " Taegu Ha
  1 sibling, 1 reply; 13+ messages in thread
From: Taegu Ha @ 2026-05-28  5:25 UTC (permalink / raw)
  To: Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko
  Cc: Yonghong Song, Eduard Zingerman, Kumar Kartikeya Dwivedi,
	Shuah Khan, bpf, linux-kernel, linux-kselftest, Taegu Ha

Global subprogram argument checking derives generic pointer sizes from BTF
and passes the resolved size to check_mem_reg() as a u32. The access-size
validation path then uses a signed int, and stack pointers negate the value
before calling check_helper_mem_access().

This creates a wrap when BTF describes a pointee size larger than S32_MAX.
For example, a global subprogram argument of type:

  int (*p)[0x3fffffff]

has a BTF-resolved pointee size of 0xfffffffc bytes. At a call site the
caller can pass a pointer to a 4-byte stack slot at fp-4. The current
PTR_TO_STACK path computes:

  size = -(int)mem_size

so 0xfffffffc becomes -4 as a signed int and the negation validates only
a 4-byte stack range. That range is covered by the caller's stack slot,
so the call is accepted.

The callee is then verified independently with R1 as PTR_TO_MEM and
mem_size 0xfffffffc. A small instruction such as:

  r0 = *(u32 *)(r1 + 4)

is accepted as being inside that BTF-described memory region. At run time,
however, the actual argument value is still fp-4, so r1 + 4 addresses fp+0,
outside the 4-byte object that the caller provided.

Reject sizes that cannot be represented by the verifier's signed
access-size API before the stack-specific negation. Add a verifier
regression test for the oversized BTF argument.

Fixes: 2cb27158adb3 ("bpf: poison dead stack slots")
Signed-off-by: Taegu Ha <hataegu0826@gmail.com>
---
 kernel/bpf/verifier.c                           |  5 +++++
 .../bpf/progs/verifier_global_subprogs.c        | 17 +++++++++++++++++
 2 files changed, 22 insertions(+)

diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index 7fb88e1cd7c4..caa5a6323810 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -7107,6 +7107,11 @@ static int check_mem_reg(struct bpf_verifier_env *env, struct bpf_reg_state *reg
 	struct bpf_reg_state saved_reg;
 	int err;
 
+	if (mem_size > S32_MAX) {
+		verbose(env, "R%d memory size %u is too large\n", regno, mem_size);
+		return -EACCES;
+	}
+
 	if (bpf_register_is_null(reg))
 		return 0;
 
diff --git a/tools/testing/selftests/bpf/progs/verifier_global_subprogs.c b/tools/testing/selftests/bpf/progs/verifier_global_subprogs.c
index 1e08aff7532e..0ff8f85b4d46 100644
--- a/tools/testing/selftests/bpf/progs/verifier_global_subprogs.c
+++ b/tools/testing/selftests/bpf/progs/verifier_global_subprogs.c
@@ -151,6 +151,23 @@ int anon_user_mem_valid(void *ctx)
 	return subprog_user_anon_mem(&t);
 }
 
+__noinline __weak int subprog_user_anon_mem_huge(int (*p)[0x3fffffff])
+{
+	return p ? (*p)[1] : 0;
+}
+
+SEC("?tracepoint")
+__failure __log_level(2)
+__msg("R1 memory size 4294967292 is too large")
+int anon_user_mem_huge_size_invalid(void *ctx)
+{
+	int (*p)[0x3fffffff];
+	int tiny = 42;
+
+	p = (void *)&tiny;
+	return subprog_user_anon_mem_huge(p) + tiny;
+}
+
 __noinline __weak int subprog_nonnull_ptr_good(int *p1 __arg_nonnull, int *p2 __arg_nonnull)
 {
 	return (*p1) * (*p2); /* good, no need for NULL checks */
-- 
2.43.0


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

* Re: [PATCH v2 1/1] bpf: reject overlarge global subprog argument sizes
  2026-05-28  5:25   ` [PATCH v2 1/1] " Taegu Ha
@ 2026-05-28  6:05     ` bot+bpf-ci
  0 siblings, 0 replies; 13+ messages in thread
From: bot+bpf-ci @ 2026-05-28  6:05 UTC (permalink / raw)
  To: hataegu0826, ast, daniel, andrii
  Cc: yonghong.song, eddyz87, memxor, shuah, bpf, linux-kernel,
	linux-kselftest, hataegu0826, ast, andrii, daniel, martin.lau,
	eddyz87, yonghong.song, clm, ihor.solodrai

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

> diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
> index c8d980fdd709..9248ba0f8089 100644
> --- a/kernel/bpf/verifier.c
> +++ b/kernel/bpf/verifier.c
> @@ -6924,6 +6924,11 @@ static int check_mem_reg(struct bpf_verifier_env *env, struct bpf_reg_state *reg
>  	struct bpf_reg_state saved_reg;
>  	int err;
>
> +	if (mem_size > S32_MAX) {
> +		verbose(env, "R%d memory size %u is too large\n", regno, mem_size);
> +		return -EACCES;
> +	}
> +

Does this hunk build?  The function signature is:

  static int check_mem_reg(struct bpf_verifier_env *env,
                           struct bpf_reg_state *reg,
                           argno_t argno, u32 mem_size)

There is no parameter or local named regno here, only argno (of typedef
argno_t).  Other helpers in this file that need a register number
compute it locally, e.g.:

  int regno = reg_from_argno(argno);

Without that, the verbose() call references an undeclared identifier
and the file fails to compile.

The selftest below expects the literal text "R1 memory size 4294967292
is too large", so the format string presumably wants the integer
register number printed.  Would something like:

  int regno = reg_from_argno(argno);

  if (mem_size > S32_MAX) {
          verbose(env, "R%d memory size %u is too large\n",
                  regno, mem_size);
          return -EACCES;
  }

be the intended form, or should this switch to reg_arg_name(env, argno)
with a matching update to the selftest __msg()?

> diff --git a/tools/testing/selftests/bpf/progs/verifier_global_subprogs.c b/tools/testing/selftests/bpf/progs/verifier_global_subprogs.c
> index dc09d0e2d8ad..75a2e3f48d0f 100644
> --- a/tools/testing/selftests/bpf/progs/verifier_global_subprogs.c
> +++ b/tools/testing/selftests/bpf/progs/verifier_global_subprogs.c
> @@ -152,6 +152,23 @@ int anon_user_mem_valid(void *ctx)
>  	return subprog_user_anon_mem(&t);
>  }
>
> +__noinline __weak int subprog_user_anon_mem_huge(int (*p)[0x3fffffff])
> +{
> +	return p ? (*p)[1] : 0;
> +}
> +
> +SEC("?tracepoint")
> +__failure __log_level(2)
> +__msg("R1 memory size 4294967292 is too large")
> +int anon_user_mem_huge_size_invalid(void *ctx)
> +{
> +	int (*p)[0x3fffffff];
> +	int tiny = 42;
> +
> +	p = (void *)&tiny;
> +	return subprog_user_anon_mem_huge(p) + tiny;
> +}
> +


---
AI reviewed your patch. Please fix the bug or email reply why it's not a bug.
See: https://github.com/kernel-patches/vmtest/blob/master/ci/claude/README.md

CI run summary: https://github.com/kernel-patches/bpf/actions/runs/26556945758

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

* [PATCH bpf-next v3] bpf: reject overlarge global subprog argument sizes
  2026-05-28  5:25 ` [PATCH v2 0/1] " Taegu Ha
  2026-05-28  5:25   ` [PATCH v2 1/1] " Taegu Ha
@ 2026-05-28  6:21   ` Taegu Ha
  2026-05-28 15:17     ` Yonghong Song
  2026-06-01  1:00     ` patchwork-bot+netdevbpf
  1 sibling, 2 replies; 13+ messages in thread
From: Taegu Ha @ 2026-05-28  6:21 UTC (permalink / raw)
  To: Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko
  Cc: Yonghong Song, Eduard Zingerman, Kumar Kartikeya Dwivedi,
	Shuah Khan, John Fastabend, Jiri Olsa, Stanislav Fomichev,
	Song Liu, Puranjay Mohan, Emil Tantilov, Martin KaFai Lau, bpf,
	linux-kernel, linux-kselftest, Taegu Ha

Global subprogram argument checking derives generic pointer sizes from BTF
and passes the resolved size to check_mem_reg() as a u32. The access-size
validation path then uses a signed int, and stack pointers negate the value
before calling check_helper_mem_access().

This creates a wrap when BTF describes a pointee size larger than S32_MAX.
For example, a global subprogram argument of type:

  int (*p)[0x3fffffff]

has a BTF-resolved pointee size of 0xfffffffc bytes. At a call site the
caller can pass a pointer to a 4-byte stack slot at fp-4. The current
PTR_TO_STACK path computes:

  size = -(int)mem_size

so 0xfffffffc becomes -4 as a signed int and the negation validates only
a 4-byte stack range. That range is covered by the caller's stack slot,
so the call is accepted.

The callee is then verified independently with R1 as PTR_TO_MEM and
mem_size 0xfffffffc. A small instruction such as:

  r0 = *(u32 *)(r1 + 4)

is accepted as being inside that BTF-described memory region. At run time,
however, the actual argument value is still fp-4, so r1 + 4 addresses fp+0,
outside the 4-byte object that the caller provided.

Reject sizes that cannot be represented by the verifier's signed
access-size API before the stack-specific negation. Add a verifier
regression test for the oversized BTF argument.

Fixes: 2cb27158adb3 ("bpf: poison dead stack slots")
Signed-off-by: Taegu Ha <hataegu0826@gmail.com>
---
v3:
- Fix bpf-next build by using reg_arg_name(env, argno) in
  check_mem_reg(); v2 referenced a stale regno parameter name.
- Keep the existing known-NULL fast path before the overlarge-size guard.
- Send as a single bpf-next patch without a cover letter.

v2:
- Expanded the commit message with the BTF-derived size wrap details.
- Kept the verifier fix to a mem_size > S32_MAX guard.

 kernel/bpf/verifier.c                           |  6 ++++++
 .../bpf/progs/verifier_global_subprogs.c        | 17 +++++++++++++++++
 2 files changed, 23 insertions(+)

diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index c8d980fdd709..3a270bc485c2 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -6927,6 +6927,12 @@ static int check_mem_reg(struct bpf_verifier_env *env, struct bpf_reg_state *reg
 	if (bpf_register_is_null(reg))
 		return 0;
 
+	if (mem_size > S32_MAX) {
+		verbose(env, "%s memory size %u is too large\n",
+			reg_arg_name(env, argno), mem_size);
+		return -EACCES;
+	}
+
 	/* Assuming that the register contains a value check if the memory
 	 * access is safe. Temporarily save and restore the register's state as
 	 * the conversion shouldn't be visible to a caller.
diff --git a/tools/testing/selftests/bpf/progs/verifier_global_subprogs.c b/tools/testing/selftests/bpf/progs/verifier_global_subprogs.c
index dc09d0e2d8ad..75a2e3f48d0f 100644
--- a/tools/testing/selftests/bpf/progs/verifier_global_subprogs.c
+++ b/tools/testing/selftests/bpf/progs/verifier_global_subprogs.c
@@ -152,6 +152,23 @@ int anon_user_mem_valid(void *ctx)
 	return subprog_user_anon_mem(&t);
 }
 
+__noinline __weak int subprog_user_anon_mem_huge(int (*p)[0x3fffffff])
+{
+	return p ? (*p)[1] : 0;
+}
+
+SEC("?tracepoint")
+__failure __log_level(2)
+__msg("R1 memory size 4294967292 is too large")
+int anon_user_mem_huge_size_invalid(void *ctx)
+{
+	int (*p)[0x3fffffff];
+	int tiny = 42;
+
+	p = (void *)&tiny;
+	return subprog_user_anon_mem_huge(p) + tiny;
+}
+
 __noinline __weak int subprog_nonnull_ptr_good(int *p1 __arg_nonnull, int *p2 __arg_nonnull)
 {
 	return (*p1) * (*p2); /* good, no need for NULL checks */
-- 
2.43.0

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

* Re: [PATCH bpf-next v3] bpf: reject overlarge global subprog argument sizes
  2026-05-28  6:21   ` [PATCH bpf-next v3] " Taegu Ha
@ 2026-05-28 15:17     ` Yonghong Song
  2026-06-01  1:00     ` patchwork-bot+netdevbpf
  1 sibling, 0 replies; 13+ messages in thread
From: Yonghong Song @ 2026-05-28 15:17 UTC (permalink / raw)
  To: Taegu Ha, Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko
  Cc: Eduard Zingerman, Kumar Kartikeya Dwivedi, Shuah Khan,
	John Fastabend, Jiri Olsa, Stanislav Fomichev, Song Liu,
	Puranjay Mohan, Emil Tantilov, Martin KaFai Lau, bpf,
	linux-kernel, linux-kselftest



On 5/27/26 11:21 PM, Taegu Ha wrote:
> Global subprogram argument checking derives generic pointer sizes from BTF
> and passes the resolved size to check_mem_reg() as a u32. The access-size
> validation path then uses a signed int, and stack pointers negate the value
> before calling check_helper_mem_access().
>
> This creates a wrap when BTF describes a pointee size larger than S32_MAX.
> For example, a global subprogram argument of type:
>
>    int (*p)[0x3fffffff]
>
> has a BTF-resolved pointee size of 0xfffffffc bytes. At a call site the
> caller can pass a pointer to a 4-byte stack slot at fp-4. The current
> PTR_TO_STACK path computes:
>
>    size = -(int)mem_size
>
> so 0xfffffffc becomes -4 as a signed int and the negation validates only
> a 4-byte stack range. That range is covered by the caller's stack slot,
> so the call is accepted.
>
> The callee is then verified independently with R1 as PTR_TO_MEM and
> mem_size 0xfffffffc. A small instruction such as:
>
>    r0 = *(u32 *)(r1 + 4)
>
> is accepted as being inside that BTF-described memory region. At run time,
> however, the actual argument value is still fp-4, so r1 + 4 addresses fp+0,
> outside the 4-byte object that the caller provided.
>
> Reject sizes that cannot be represented by the verifier's signed
> access-size API before the stack-specific negation. Add a verifier
> regression test for the oversized BTF argument.
>
> Fixes: 2cb27158adb3 ("bpf: poison dead stack slots")
> Signed-off-by: Taegu Ha <hataegu0826@gmail.com>

Acked-by: Yonghong Song <yonghong.song@linux.dev>


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

* Re: [PATCH bpf-next v3] bpf: reject overlarge global subprog argument sizes
  2026-05-28  6:21   ` [PATCH bpf-next v3] " Taegu Ha
  2026-05-28 15:17     ` Yonghong Song
@ 2026-06-01  1:00     ` patchwork-bot+netdevbpf
  2026-06-01  6:04       ` 하태구
  1 sibling, 1 reply; 13+ messages in thread
From: patchwork-bot+netdevbpf @ 2026-06-01  1:00 UTC (permalink / raw)
  To: Taegu Ha
  Cc: ast, daniel, andrii, yonghong.song, eddyz87, memxor, shuah,
	john.fastabend, jolsa, yatsenko, song, puranjay, emil, martin.lau,
	bpf, linux-kernel, linux-kselftest

Hello:

This patch was applied to bpf/bpf-next.git (master)
by Alexei Starovoitov <ast@kernel.org>:

On Thu, 28 May 2026 15:21:55 +0900 you wrote:
> Global subprogram argument checking derives generic pointer sizes from BTF
> and passes the resolved size to check_mem_reg() as a u32. The access-size
> validation path then uses a signed int, and stack pointers negate the value
> before calling check_helper_mem_access().
> 
> This creates a wrap when BTF describes a pointee size larger than S32_MAX.
> For example, a global subprogram argument of type:
> 
> [...]

Here is the summary with links:
  - [bpf-next,v3] bpf: reject overlarge global subprog argument sizes
    https://git.kernel.org/bpf/bpf-next/c/de36adca6346

You are awesome, thank you!
-- 
Deet-doot-dot, I am a bot.
https://korg.docs.kernel.org/patchwork/pwbot.html



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

* Re: [PATCH bpf-next v3] bpf: reject overlarge global subprog argument sizes
  2026-06-01  1:00     ` patchwork-bot+netdevbpf
@ 2026-06-01  6:04       ` 하태구
  0 siblings, 0 replies; 13+ messages in thread
From: 하태구 @ 2026-06-01  6:04 UTC (permalink / raw)
  To: patchwork-bot+netdevbpf
  Cc: ast, daniel, andrii, yonghong.song, eddyz87, memxor, shuah,
	john.fastabend, jolsa, yatsenko, song, puranjay, emil, martin.lau,
	bpf, linux-kernel, linux-kselftest

Thanks Pavan, that answers my question.

I will drop this as a security issue and will not pursue the defensive
data-path checks for bnxt_en.
Thanks for clarifying the HW completion contract.

Regards,
Taegu

2026년 6월 1일 (월) 오전 10:00, <patchwork-bot+netdevbpf@kernel.org>님이 작성:
>
> Hello:
>
> This patch was applied to bpf/bpf-next.git (master)
> by Alexei Starovoitov <ast@kernel.org>:
>
> On Thu, 28 May 2026 15:21:55 +0900 you wrote:
> > Global subprogram argument checking derives generic pointer sizes from BTF
> > and passes the resolved size to check_mem_reg() as a u32. The access-size
> > validation path then uses a signed int, and stack pointers negate the value
> > before calling check_helper_mem_access().
> >
> > This creates a wrap when BTF describes a pointee size larger than S32_MAX.
> > For example, a global subprogram argument of type:
> >
> > [...]
>
> Here is the summary with links:
>   - [bpf-next,v3] bpf: reject overlarge global subprog argument sizes
>     https://git.kernel.org/bpf/bpf-next/c/de36adca6346
>
> You are awesome, thank you!
> --
> Deet-doot-dot, I am a bot.
> https://korg.docs.kernel.org/patchwork/pwbot.html
>
>

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

end of thread, other threads:[~2026-06-01  6:04 UTC | newest]

Thread overview: 13+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-05-27  5:25 [PATCH bpf 0/1] bpf: reject overlarge global subprog argument sizes Taegu Ha
2026-05-27  5:25 ` [PATCH bpf 1/1] " Taegu Ha
2026-05-27 16:59   ` Yonghong Song
2026-05-27 17:48     ` 하태구
2026-05-27 17:53       ` 하태구
2026-05-28  5:03         ` Yonghong Song
2026-05-28  5:25 ` [PATCH v2 0/1] " Taegu Ha
2026-05-28  5:25   ` [PATCH v2 1/1] " Taegu Ha
2026-05-28  6:05     ` bot+bpf-ci
2026-05-28  6:21   ` [PATCH bpf-next v3] " Taegu Ha
2026-05-28 15:17     ` Yonghong Song
2026-06-01  1:00     ` patchwork-bot+netdevbpf
2026-06-01  6:04       ` 하태구

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