[PATCH bpf 0/1] bpf: reject overlarge global subprog argument sizes

Taegu Ha posted 1 patch 1 week, 5 days ago
There is a newer version of this series
kernel/bpf/verifier.c                           |  7 ++++++-
.../bpf/progs/verifier_global_subprogs.c        | 17 +++++++++++++++++
2 files changed, 23 insertions(+), 1 deletion(-)
[PATCH bpf 0/1] bpf: reject overlarge global subprog argument sizes
Posted by Taegu Ha 1 week, 5 days ago
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
[PATCH v2 0/1] bpf: reject overlarge global subprog argument sizes
Posted by Taegu Ha 1 week, 4 days ago
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
[PATCH v2 1/1] bpf: reject overlarge global subprog argument sizes
Posted by Taegu Ha 1 week, 4 days ago
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
Re: [PATCH v2 1/1] bpf: reject overlarge global subprog argument sizes
Posted by bot+bpf-ci@kernel.org 1 week, 4 days ago
> 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
[PATCH bpf-next v3] bpf: reject overlarge global subprog argument sizes
Posted by Taegu Ha 1 week, 4 days ago
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
Re: [PATCH bpf-next v3] bpf: reject overlarge global subprog argument sizes
Posted by Yonghong Song 1 week, 4 days ago

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>