include/linux/bpf.h | 3 +- kernel/bpf/btf.c | 54 ++- kernel/bpf/verifier.c | 4 +- net/bpf/test_run.c | 128 ++++++ .../prog_tests/fentry_fexit_multi_level_ptr.c | 204 +++++++++ .../selftests/bpf/prog_tests/verifier.c | 2 + .../progs/fentry_fexit_pptr_nullable_test.c | 52 +++ .../bpf/progs/fentry_fexit_pptr_test.c | 60 +++ .../bpf/progs/fentry_fexit_void_ppptr_test.c | 31 ++ .../bpf/progs/fentry_fexit_void_pptr_test.c | 64 +++ .../bpf/progs/verifier_ctx_multilevel_ptr.c | 429 ++++++++++++++++++ 11 files changed, 1021 insertions(+), 10 deletions(-) create mode 100644 tools/testing/selftests/bpf/prog_tests/fentry_fexit_multi_level_ptr.c create mode 100644 tools/testing/selftests/bpf/progs/fentry_fexit_pptr_nullable_test.c create mode 100644 tools/testing/selftests/bpf/progs/fentry_fexit_pptr_test.c create mode 100644 tools/testing/selftests/bpf/progs/fentry_fexit_void_ppptr_test.c create mode 100644 tools/testing/selftests/bpf/progs/fentry_fexit_void_pptr_test.c create mode 100644 tools/testing/selftests/bpf/progs/verifier_ctx_multilevel_ptr.c
This patch adds BPF verifier support for multi-level pointer parameters
and return values in BPF trampolines. The implementation treats these
parameters as PTR_TO_MEM with read-only semantics, applying either
untrusted or trusted access patterns while honoring __nullable
annotations. Runtime safety is ensured through existing exception
handling mechanisms for untrusted memory reads, with the verifier
enforcing bounds checking and null validation. The series includes
selftests covering double and triple pointer arguments across
fentry/fexit/lsm programs and verifier context validation.
Background:
Prior to these changes, accessing multi-level pointer parameters or
return values through BPF trampoline context arrays resulted in
verification failures in btf_ctx_access, producing errors such as:
func '%s' arg%d type %s is not a struct
For example, consider a BPF program that logs an input parameter of type
struct posix_acl **:
SEC("fentry/__posix_acl_chmod")
int BPF_PROG(trace_posix_acl_chmod, struct posix_acl **ppacl, gfp_t gfp,
umode_t mode)
{
bpf_printk("__posix_acl_chmod ppacl = %px\n", ppacl);
return 0;
}
This program failed BPF verification with the following error:
libbpf: prog 'trace_posix_acl_chmod': -- BEGIN PROG LOAD LOG --
0: R1=ctx() R10=fp0
; int BPF_PROG(trace_posix_acl_chmod, struct posix_acl **ppacl,
gfp_t gfp, umode_t mode) @ posix_acl_monitor.bpf.c:23
0: (79) r6 = *(u64 *)(r1 +16) ; R1=ctx() R6_w=scalar()
1: (79) r1 = *(u64 *)(r1 +0)
func '__posix_acl_chmod' arg0 type PTR is not a struct
invalid bpf_context access off=0 size=8
processed 2 insns (limit 1000000) max_states_per_insn 0 total_states 0
peak_states 0 mark_read 0
-- END PROG LOAD LOG --
The common workaround involved using helper functions to fetch parameter
values by passing the address of the context array entry:
SEC("fentry/__posix_acl_chmod")
int BPF_PROG(trace_posix_acl_chmod, struct posix_acl **ppacl, gfp_t gfp,
umode_t mode)
{
struct posix_acl **p;
bpf_probe_read_kernel(&p, sizeof(ppacl), &ctx[0]);
bpf_printk("__posix_acl_chmod before %px\n", p);
return 0;
}
This approach introduced helper call overhead and created inconsistency
with parameter access patterns.
Improvements:
With this patch, trampoline programs can directly read parameters and
dereference memory using load instructions, eliminating helper call
overhead and ensuring consistent parameter handling. For example, the
following helper call sequence:
{
struct posix_acl **pp;
struct posix_acl *p;
bpf_probe_read_kernel(&pp, sizeof(pp), &ctx[0]);
bpf_probe_read_kernel(&p, sizeof(p), pp);
...
}
can be replaced by two load instructions implementing a single C
statement:
{
struct posix_acl *p = *ppacl;
...
}
Design Rationale: PTR_TO_MEM vs SCALAR
The verifier assigns SCALAR type to single-level pointers (void*, int*).
For multi-level pointers, I selected PTR_TO_MEM to enable memory access
through a single load instruction for the first level of dereference,
with subsequent dereferences becoming SCALAR. This design eliminates
helper call for parameter dereference, replacing it with a load
instruction (e.g., void* ptr = *pptr).
Access safety is maintained through existing verify-time checks,
exception handling, and kernel virtual address range boundary checks:
- User-mode memory address access is prevented by runtime virtual
address range checks for untrusted PTR_TO_MEM
- Invalid kernel address space accesses are intercepted by the
exception handler for untrusted PTR_TO_MEM
- Trusted PTR_TO_MEM access safety is maintained at verify time
v1 -> v2: corrected maintainer's email
Slava Imameev (2):
bpf: Support multi-level pointer params via PTR_TO_MEM for trampolines
selftests/bpf: Add trampolines multi-level pointer params test
coverage
include/linux/bpf.h | 3 +-
kernel/bpf/btf.c | 54 ++-
kernel/bpf/verifier.c | 4 +-
net/bpf/test_run.c | 128 ++++++
.../prog_tests/fentry_fexit_multi_level_ptr.c | 204 +++++++++
.../selftests/bpf/prog_tests/verifier.c | 2 +
.../progs/fentry_fexit_pptr_nullable_test.c | 52 +++
.../bpf/progs/fentry_fexit_pptr_test.c | 60 +++
.../bpf/progs/fentry_fexit_void_ppptr_test.c | 31 ++
.../bpf/progs/fentry_fexit_void_pptr_test.c | 64 +++
.../bpf/progs/verifier_ctx_multilevel_ptr.c | 429 ++++++++++++++++++
11 files changed, 1021 insertions(+), 10 deletions(-)
create mode 100644 tools/testing/selftests/bpf/prog_tests/fentry_fexit_multi_level_ptr.c
create mode 100644 tools/testing/selftests/bpf/progs/fentry_fexit_pptr_nullable_test.c
create mode 100644 tools/testing/selftests/bpf/progs/fentry_fexit_pptr_test.c
create mode 100644 tools/testing/selftests/bpf/progs/fentry_fexit_void_ppptr_test.c
create mode 100644 tools/testing/selftests/bpf/progs/fentry_fexit_void_pptr_test.c
create mode 100644 tools/testing/selftests/bpf/progs/verifier_ctx_multilevel_ptr.c
--
2.50.1 (Apple Git-155)
On Wed, 2026-02-18 at 09:13 +1100, Slava Imameev wrote:
[...]
> The verifier assigns SCALAR type to single-level pointers (void*, int*).
So, the simplest change for pointers to pointers would be as below, right?
--- a/kernel/bpf/btf.c
+++ b/kernel/bpf/btf.c
@@ -6906,7 +6906,8 @@ bool btf_ctx_access(int off, int size, enum bpf_access_type type,
* If it's a pointer to void, it's the same as scalar from the verifier
* safety POV. Either way, no futher pointer walking is allowed.
*/
- if (is_void_or_int_ptr(btf, t))
+ if (is_void_or_int_ptr(btf, t) || !is_ptr_to_struct(btf, t))
return true;
/* this is a pointer to another type */
Except that loaded value would be marked as scalar() and one would
need to cast it using e.g. bpf_core_cast() to obtain an untrusted
pointer.
> For multi-level pointers, I selected PTR_TO_MEM to enable memory access
> through a single load instruction for the first level of dereference,
> with subsequent dereferences becoming SCALAR. This design eliminates
> helper call for parameter dereference, replacing it with a load
> instruction (e.g., void* ptr = *pptr).
If going this route instead, is there a technical reason to limit this
logic to multi-level pointers? Applying same rules to `int *` and
alike seem more consistent.
[...]
> > The verifier assigns SCALAR type to single-level pointers (void*, int*).
>
> So, the simplest change for pointers to pointers would be as below, right?
>
> --- a/kernel/bpf/btf.c
> +++ b/kernel/bpf/btf.c
> @@ -6906,7 +6906,8 @@ bool btf_ctx_access(int off, int size, enum bpf_acc=
ess_type type,
> * If it's a pointer to void, it's the same as scalar from the ve=
rifier
> * safety POV. Either way, no futher pointer walking is allowed.
> */
> - if (is_void_or_int_ptr(btf, t))
> + if (is_void_or_int_ptr(btf, t) || !is_ptr_to_struct(btf, t))
> return true;
>
> /* this is a pointer to another type */
>
> Except that loaded value would be marked as scalar() and one would
> need to cast it using e.g. bpf_core_cast() to obtain an untrusted
> pointer.
I considered using a scalar as a simpler solution, but there are some
disadvantages with casting to scalar and using bpf_core_cast:
- Casting to scalar removes nullable and trusted properties
- bpf_core_cast cannot cast to multi-level pointers without
introducing a new typedef or a wrapper for a pointer
Let's consider the following LSM program which has trusted parameters, and
logs the value for (*mnt_opts):
SEC("lsm/sb_eat_lsm_opts")
int BPF_PROG(sb_eat_lsm_opts_1,char *options, void **mnt_opts)
With this patch:
- This program is valid:
SEC("lsm/sb_eat_lsm_opts")
int BPF_PROG(sb_eat_lsm_opts_1,char *options, void **mnt_opts)
{
bpf_printk("%p\n", *mnt_opts);
return 0;
}
- This program is semantically invalid as mnt_opts is a trusted
parameter, so there are no run-time checks and the verifier rejects
out-of-bounds access:
SEC("lsm/sb_eat_lsm_opts")
int BPF_PROG(sb_eat_lsm_opts_1,char *options, void **mnt_opts)
{
bpf_printk("%p\n", *(mnt_opts+10));
return 0;
}
With casting to a scalar and following bpf_core_cast:
- This programs cannot be compiled as bpf_core_cast cannot cast to a
multi-level pointer:
SEC("lsm/sb_eat_lsm_opts")
int BPF_PROG(sb_eat_lsm_opts_1,char *options, void **mnt_opts)
{
void** ppt = bpf_core_cast(mnt_opts, void*);
bpf_printk("%p\t", *ppt);
return 0;
}
- There is a workaround, which requires introducing a wrapper for
a pointer or typedef:
struct pvoid {
void* v;
};
typedef void* pvoid;
SEC("lsm/sb_eat_lsm_opts")
int BPF_PROG(sb_eat_lsm_opts_1,char *options, void **mnt_opts)
{
struct pvoid* ppt = bpf_core_cast(mnt_opts, struct pvoid);
bpf_printk("%p\t", ppt->v);
return 0;
}
SEC("lsm/sb_eat_lsm_opts")
int BPF_PROG(sb_eat_lsm_opts_2,char *options, void **mnt_opts)
{
pvoid* ppt = bpf_core_cast(mnt_opts, pvoid);
bpf_printk("%p\t", *ppt);
return 0;
}
- This program passes verifier, though it is semantically invalid
as logs an invalid data using a trusted parameter:
SEC("lsm/sb_eat_lsm_opts")
int BPF_PROG(sb_eat_lsm_opts_1,char *options, void **mnt_opts)
{
struct pvoid* ppt = bpf_core_cast(mnt_opts + 10, struct pvoid);
bpf_printk("%p\t", ppt->v);
return 0;
}
The similar examples can be done for nullable annotation, which
is ignored for a scalar allowing semantically invalid BPF programs to
pass verifier.
> > For multi-level pointers, I selected PTR_TO_MEM to enable memory access
> > through a single load instruction for the first level of dereference,
> > with subsequent dereferences becoming SCALAR. This design eliminates
> > helper call for parameter dereference, replacing it with a load
> > instruction (e.g., void* ptr =3D *pptr).
>
> If going this route instead, is there a technical reason to limit this
> logic to multi-level pointers? Applying same rules to `int *` and
> alike seem more consistent.
I decided to address only multilevel pointers as this is what we
encountered in practice and have to use BPF helper workarounds.
I think there are no technical restrictions for treating single
level pointers as PTR_TO_MEM.
However, there is some cohesion between multilevel pointers being
PTR_TO_MEM and single level being scalar, as verifier infers a scalar
for PTR_TO_MEM dereference, so:
foo(void *ptr1, void **pptr)
{
void* ptr2 = *pptr; /* verifier infers a scalar for ptr2*/
/* both ptr1 and ptr2 are scalars */
}
On Wed, Feb 18, 2026 at 2:44 AM Slava Imameev
<slava.imameev@crowdstrike.com> wrote:
>
>
> - This programs cannot be compiled as bpf_core_cast cannot cast to a
> multi-level pointer:
>
> SEC("lsm/sb_eat_lsm_opts")
> int BPF_PROG(sb_eat_lsm_opts_1,char *options, void **mnt_opts)
> {
> void** ppt = bpf_core_cast(mnt_opts, void*);
> bpf_printk("%p\t", *ppt);
> return 0;
> }
Looks like there is a bug in llvm, since it crashes on the above.
But the following works:
void** ppt = bpf_rdonly_cast(mnt_opts, 0);
bpf_printk("%lx\t", *ppt);
Plus Eduard's diff:
- if (is_void_or_int_ptr(btf, t))
+ if (is_void_or_int_ptr(btf, t) || !btf_type_is_struct_ptr(btf, t))
> - There is a workaround, which requires introducing a wrapper for
> a pointer or typedef:
>
> struct pvoid {
> void* v;
> };
llvm should have handled it without the workaround. It's a bug
that should be fixed.
> I think there are no technical restrictions for treating single
> level pointers as PTR_TO_MEM.
I think it will be a missed opportunity and a potential foot gun.
We didn't support access to 'char *' initially.
Later relaxed it to mean that it's a valid pointer to a single byte,
but since the code is generic it also became the case
that 'char *' is allowed in kfunc and the verifier checks
that one valid byte is there.
That was a bad footgun, since we saw several cases of broken
kfunc implementations that assume that 'char *' means a string.
There are only two lsm hooks that pass 'struct foo **'.
If we make it ptr_to_mem of 8 bytes we lose the ability to do
something smarter in the future.
I think we better add support to annotate such '**' as an actual
array with given size and track types completely, so
'struct foo * var[N]' will become array_to_ptr_to_btf_id.
That's probably more work that you signed up to do,
so I suggest treating 'void **' as a scalar as Eduard suggested.
This particular sb_eat_lsm_opts() hook
doesn't have a useful type behind it anyway.
I'm less certain about 'char **'. If we make it scalar too
it will be harder to make it a pointer to nul terminated string later.
So I would do 'void **' -> scalar for now only.
On Wed, 18 Feb 2026 19:15:47 Alexei Starovoitov <alexei.starovoitov@gmail.com> wrote > so I suggest treating 'void **' as a scalar as Eduard suggested. > This particular sb_eat_lsm_opts() hook > doesn't have a useful type behind it anyway. > I'm less certain about 'char **'. If we make it scalar too > it will be harder to make it a pointer to nul terminated string later. > So I would do 'void **' -> scalar for now only. I changed to scalar in v3, keeping broader scope for pointer types. We encountered double pointers of various types that required workarounds, such as: int __posix_acl_chmod(struct posix_acl **acl, gfp_t gfp, umode_t mode) Adding support for void** alone doesn't address the broader issue with other double pointer types. When annotated array support (including char**) is added in the future, it should remain compatible with the scalar approach for legacy (unannotated) parameters. Unannotated parameters will continue using scalar handling.
On 2/18/26 7:15 PM, Alexei Starovoitov wrote:
> On Wed, Feb 18, 2026 at 2:44 AM Slava Imameev
> <slava.imameev@crowdstrike.com> wrote:
>>
>> - This programs cannot be compiled as bpf_core_cast cannot cast to a
>> multi-level pointer:
>>
>> SEC("lsm/sb_eat_lsm_opts")
>> int BPF_PROG(sb_eat_lsm_opts_1,char *options, void **mnt_opts)
>> {
>> void** ppt = bpf_core_cast(mnt_opts, void*);
>> bpf_printk("%p\t", *ppt);
>> return 0;
>> }
> Looks like there is a bug in llvm, since it crashes on the above.
> But the following works:
>
> void** ppt = bpf_rdonly_cast(mnt_opts, 0);
> bpf_printk("%lx\t", *ppt);
>
> Plus Eduard's diff:
> - if (is_void_or_int_ptr(btf, t))
> + if (is_void_or_int_ptr(btf, t) || !btf_type_is_struct_ptr(btf, t))
>
>
>> - There is a workaround, which requires introducing a wrapper for
>> a pointer or typedef:
>>
>> struct pvoid {
>> void* v;
>> };
> llvm should have handled it without the workaround. It's a bug
> that should be fixed.
Okay, I will take a look.
>
>> I think there are no technical restrictions for treating single
>> level pointers as PTR_TO_MEM.
> I think it will be a missed opportunity and a potential foot gun.
>
> We didn't support access to 'char *' initially.
> Later relaxed it to mean that it's a valid pointer to a single byte,
> but since the code is generic it also became the case
> that 'char *' is allowed in kfunc and the verifier checks
> that one valid byte is there.
> That was a bad footgun, since we saw several cases of broken
> kfunc implementations that assume that 'char *' means a string.
>
> There are only two lsm hooks that pass 'struct foo **'.
> If we make it ptr_to_mem of 8 bytes we lose the ability to do
> something smarter in the future.
> I think we better add support to annotate such '**' as an actual
> array with given size and track types completely, so
> 'struct foo * var[N]' will become array_to_ptr_to_btf_id.
> That's probably more work that you signed up to do,
> so I suggest treating 'void **' as a scalar as Eduard suggested.
> This particular sb_eat_lsm_opts() hook
> doesn't have a useful type behind it anyway.
> I'm less certain about 'char **'. If we make it scalar too
> it will be harder to make it a pointer to nul terminated string later.
>
> So I would do 'void **' -> scalar for now only.
> I decided to address only multilevel pointers as this is what we encountered in practice and have to use BPF helper workarounds. I think there are no technical restrictions for treating single level pointers as PTR_TO_MEM. Hi Slava and Eduard, If we add support for writable single-level int pointers, we could trivially implement bpf_inode_set_xattr in the way that Alexei originally suggested[1] when it was first attempted to be added. One note, for this particular case, the kfunc would need to be able to write to the xattr int* param, as lsm_get_xattr_slot[2] increments the LSM-internal xattr_count. Others would be possible as well (cred_getsecid). [1] https://kernsec.org/pipermail/linux-security-module-archive/2022-October/034878.html [2] https://elixir.bootlin.com/linux/v6.19-rc5/source/include/linux/lsm_hooks.h#L215
© 2016 - 2026 Red Hat, Inc.