selftests/bpf: Test __nullable pointer members

Add a struct_ops member to bpf_testmod whose argument struct carries two
task pointers, one plain and one __nullable, both made trusted through a
__safe_trusted alias of the struct in the module BTF, and a kfunc that
calls the member once with the nullable pointer unset and once set.

The callback acquires the plain pointer as is and the nullable one only
after a check, driven through the kfunc from a syscall program. The
failure case passes the nullable pointer to bpf_task_acquire() unchecked
and is rejected as possibly NULL.

More members cover the other kinds of pointer: a per-CPU one, which
only the per-CPU helpers consume and which keeps the flag, the struct
itself through a plain pointer and through an untrusted one out of a
__kptr_untrusted map value that is never set, and a pointer to a scalar,
which are probed and lose it. The callback uses each after a check; a
failure test passes the per-CPU pointer unchecked, and three programs
read the probed ones unchecked and load. bpf_testmod_ops3 gets the base
helpers for the per-CPU helpers.

Signed-off-by: Kumar Kartikeya Dwivedi <memxor@gmail.com>
Signed-off-by: Alexei Starovoitov <ast@kernel.org>
Link: https://patch.msgid.link/20261019132203.3034698-9-memxor@gmail.com
This commit is contained in:
Kumar Kartikeya Dwivedi 2026-10-19 15:21:50 +02:00 committed by Alexei Starovoitov
parent 7efe1d4e11
commit 9278bedb22
No known key found for this signature in database
12 changed files with 427 additions and 0 deletions

View File

@ -0,0 +1,62 @@
// SPDX-License-Identifier: GPL-2.0
/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */
#include <test_progs.h>
#include "struct_ops_nullable.skel.h"
#include "struct_ops_nullable_fail.skel.h"
#include "struct_ops_nullable_fail__percpu.skel.h"
#include "struct_ops_nullable_probed__plain.skel.h"
#include "struct_ops_nullable_probed__scalar.skel.h"
#include "struct_ops_nullable_probed__untrusted.skel.h"
/*
* Attach a callback that gets a trusted and a trusted __nullable task pointer
* through its argument struct and drive it through the
* bpf_testmod_ops3_call_test_nullable() kfunc.
*/
static void nullable_member(void)
{
LIBBPF_OPTS(bpf_test_run_opts, topts);
struct struct_ops_nullable *skel;
struct bpf_link *link;
int err;
skel = struct_ops_nullable__open_and_load();
if (!ASSERT_OK_PTR(skel, "struct_ops_nullable__open_and_load"))
return;
link = bpf_map__attach_struct_ops(skel->maps.testmod_nullable);
if (!ASSERT_OK_PTR(link, "attach_struct_ops"))
goto out;
err = bpf_prog_test_run_opts(bpf_program__fd(skel->progs.trigger), &topts);
ASSERT_OK(err, "test_run");
ASSERT_EQ(topts.retval, 0, "trigger_retval");
bpf_link__destroy(link);
out:
struct_ops_nullable__destroy(skel);
}
/* Serialized because it attaches the singleton bpf_testmod_ops3. */
void serial_test_struct_ops_nullable(void)
{
int has_tag;
/* the tag reaches the module BTF only from a toolchain with btf_type_tag support */
has_tag = testmod_member_has_type_tag("bpf_testmod_nullable_args", "task_or_null", "nullable");
if (!ASSERT_GE(has_tag, 0, "testmod_btf"))
return;
if (!has_tag) {
printf("%s:SKIP: no nullable type tag in the bpf_testmod BTF\n", __func__);
test__skip();
return;
}
if (test__start_subtest("nullable_member"))
nullable_member();
RUN_TESTS(struct_ops_nullable_fail);
RUN_TESTS(struct_ops_nullable_fail__percpu);
RUN_TESTS(struct_ops_nullable_probed__plain);
RUN_TESTS(struct_ops_nullable_probed__scalar);
RUN_TESTS(struct_ops_nullable_probed__untrusted);
}

View File

@ -0,0 +1,82 @@
// SPDX-License-Identifier: GPL-2.0
/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */
#include <vmlinux.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include "../test_kmods/bpf_testmod.h"
#include "../test_kmods/bpf_testmod_kfunc.h"
char _license[] SEC("license") = "GPL";
/* an untrusted pointer to the argument struct, never set, for the verifier */
struct untrusted_val {
struct bpf_testmod_nullable_args __kptr_untrusted *args;
};
struct {
__uint(type, BPF_MAP_TYPE_ARRAY);
__uint(max_entries, 1);
__type(key, int);
__type(value, struct untrusted_val);
} untrusted_map SEC(".maps");
SEC("struct_ops/test_nullable")
int BPF_PROG(test_nullable_cb, struct bpf_testmod_nullable_args *args)
{
struct bpf_testmod_nullable_args *u;
struct untrusted_val *v;
struct task_struct *t;
int ret = 0, *i, zero = 0;
/* trusted, so good for a kfunc as is */
t = bpf_task_acquire(args->task);
if (!t)
return -1;
bpf_task_release(t);
/* trusted but __nullable, so a check has to come first */
if (args->task_or_null) {
t = bpf_task_acquire(args->task_or_null);
if (!t)
return -1;
bpf_task_release(t);
ret = 1;
}
/* per-CPU and __nullable: a check has to come first, per_cpu_ptr() has none */
if (args->pcpu_or_null) {
struct bpf_testmod_pcpu *v = bpf_this_cpu_ptr(args->pcpu_or_null);
if (v->val != 0x5ca1ab1e)
return -1;
}
/* through a plain pointer the loads are probed, __nullable still asks for a check */
t = args->self->task_or_null;
if (t && t->pid != args->task->pid)
return -1;
/* a pointer to a scalar walks as untrusted memory, __nullable applies all the same */
i = args->int_or_null;
if (i && *i != 7)
return -1;
/* through an untrusted pointer the member is probed too, and stays nullable */
v = bpf_map_lookup_elem(&untrusted_map, &zero);
u = v ? v->args : NULL;
if (u) {
t = u->task_or_null;
if (t && t->pid != args->task->pid)
return -1;
}
return ret;
}
SEC(".struct_ops.link")
struct bpf_testmod_ops3 testmod_nullable = {
.test_nullable = (void *)test_nullable_cb,
};
SEC("syscall")
int trigger(void *ctx)
{
/* one call without task_or_null, one with */
return bpf_testmod_ops3_call_test_nullable() != 2;
}

View File

@ -0,0 +1,28 @@
// SPDX-License-Identifier: GPL-2.0
/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */
#include <vmlinux.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include "bpf_misc.h"
#include "../test_kmods/bpf_testmod.h"
char _license[] SEC("license") = "GPL";
/* The member is trusted, but __nullable, so it needs a check before the kfunc. */
SEC("struct_ops/test_nullable")
__failure __msg("Possibly NULL pointer passed to trusted")
int BPF_PROG(unchecked_nullable, struct bpf_testmod_nullable_args *args)
{
struct task_struct *t;
t = bpf_task_acquire(args->task_or_null);
if (t)
bpf_task_release(t);
return 0;
}
SEC(".struct_ops.link")
struct bpf_testmod_ops3 testmod_nullable_fail = {
.test_nullable = (void *)unchecked_nullable,
};

View File

@ -0,0 +1,28 @@
// SPDX-License-Identifier: GPL-2.0
/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */
#include <vmlinux.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include "bpf_misc.h"
#include "../test_kmods/bpf_testmod.h"
char _license[] SEC("license") = "GPL";
/*
* The member is per-CPU and __nullable: per_cpu_ptr() shifts NULL like any
* other pointer, so a check has to come before the helper.
*/
SEC("struct_ops/test_nullable")
__failure __msg("Possibly NULL pointer passed to trusted R1")
int BPF_PROG(unchecked_percpu, struct bpf_testmod_nullable_args *args)
{
struct bpf_testmod_pcpu *v = bpf_this_cpu_ptr(args->pcpu_or_null);
return v->val;
}
SEC(".struct_ops.link")
struct bpf_testmod_ops3 testmod_nullable_fail = {
.test_nullable = (void *)unchecked_percpu,
};

View File

@ -0,0 +1,26 @@
// SPDX-License-Identifier: GPL-2.0
/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */
#include <vmlinux.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include "bpf_misc.h"
#include "../test_kmods/bpf_testmod.h"
char _license[] SEC("license") = "GPL";
/*
* Through a plain pointer the member is probed, which tolerates NULL, so the
* tag asks for no check there, as the BTF_TYPE_SAFE_*_OR_NULL lists never did.
*/
SEC("struct_ops/test_nullable")
__success
int BPF_PROG(unchecked_plain, struct bpf_testmod_nullable_args *args)
{
return args->self->task_or_null->pid;
}
SEC(".struct_ops.link")
struct bpf_testmod_ops3 testmod_nullable_probed = {
.test_nullable = (void *)unchecked_plain,
};

View File

@ -0,0 +1,26 @@
// SPDX-License-Identifier: GPL-2.0
/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */
#include <vmlinux.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include "bpf_misc.h"
#include "../test_kmods/bpf_testmod.h"
char _license[] SEC("license") = "GPL";
/*
* A pointer to a scalar walks as untrusted memory, which is probed, so the
* tag asks for no check there.
*/
SEC("struct_ops/test_nullable")
__success
int BPF_PROG(unchecked_scalar, struct bpf_testmod_nullable_args *args)
{
return *args->int_or_null;
}
SEC(".struct_ops.link")
struct bpf_testmod_ops3 testmod_nullable_probed = {
.test_nullable = (void *)unchecked_scalar,
};

View File

@ -0,0 +1,45 @@
// SPDX-License-Identifier: GPL-2.0
/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */
#include <vmlinux.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include "bpf_misc.h"
#include "../test_kmods/bpf_testmod.h"
char _license[] SEC("license") = "GPL";
struct untrusted_val {
struct bpf_testmod_nullable_args __kptr_untrusted *args;
};
struct {
__uint(type, BPF_MAP_TYPE_ARRAY);
__uint(max_entries, 1);
__type(key, int);
__type(value, struct untrusted_val);
} untrusted_map SEC(".maps");
/*
* Through an untrusted pointer the member is probed, which tolerates NULL, so
* the tag asks for no check there.
*/
SEC("struct_ops/test_nullable")
__success
int BPF_PROG(unchecked_untrusted, struct bpf_testmod_nullable_args *args)
{
struct bpf_testmod_nullable_args *u;
struct untrusted_val *v;
int zero = 0;
v = bpf_map_lookup_elem(&untrusted_map, &zero);
u = v ? v->args : NULL;
if (!u)
return 0;
return u->task_or_null->pid;
}
SEC(".struct_ops.link")
struct bpf_testmod_ops3 testmod_nullable_probed = {
.test_nullable = (void *)unchecked_untrusted,
};

View File

@ -506,6 +506,11 @@ static int bpf_testmod_ops3__test_arena_multislot(struct bpf_testmod_arena_pair
return 0;
}
static int bpf_testmod_ops3__test_nullable(struct bpf_testmod_nullable_args *args)
{
return 0;
}
static struct bpf_testmod_ops3 __bpf_testmod_ops3 = {
.test_1 = bpf_testmod_test_3,
.test_2 = bpf_testmod_test_4,
@ -513,6 +518,7 @@ static struct bpf_testmod_ops3 __bpf_testmod_ops3 = {
.test_arena_nullable = bpf_testmod_ops3__test_arena_nullable,
.test_arena_stack = bpf_testmod_ops3__test_arena_stack,
.test_arena_multislot = bpf_testmod_ops3__test_arena_multislot,
.test_nullable = bpf_testmod_ops3__test_nullable,
};
static void bpf_testmod_test_struct_ops3(void)
@ -553,6 +559,41 @@ __bpf_kfunc int bpf_testmod_ops3_call_test_arena_multislot(u64 *ptr__arena)
return st_ops3->test_arena_multislot(p, ptr__arena);
}
/*
* The members of struct bpf_testmod_nullable_args are trusted the way the
* verifier's BTF_TYPE_SAFE_TRUSTED() lists vmlinux members: through a struct
* of the same name with the __safe_trusted suffix in the BTF of the module.
*/
struct bpf_testmod_nullable_args__safe_trusted {
struct task_struct *task;
struct task_struct *task_or_null;
};
static DEFINE_PER_CPU(struct bpf_testmod_pcpu, bpf_testmod_nullable_pcpu);
/*
* Call test_nullable() with the __nullable members unset, then set. The
* callback returns 1 when it found a task in @task_or_null, so the expected
* result is 2.
*/
__bpf_kfunc int bpf_testmod_ops3_call_test_nullable(void)
{
struct bpf_testmod_nullable_args args = { .task = current };
int seven = 7, ret;
BTF_TYPE_EMIT(struct bpf_testmod_nullable_args__safe_trusted);
if (!st_ops3 || !st_ops3->test_nullable)
return -EOPNOTSUPP;
args.self = &args;
ret = st_ops3->test_nullable(&args);
args.task_or_null = current;
this_cpu_write(bpf_testmod_nullable_pcpu.val, 0x5ca1ab1e);
args.pcpu_or_null = &bpf_testmod_nullable_pcpu;
args.int_or_null = &seven;
return ret | st_ops3->test_nullable(&args) << 1;
}
struct bpf_testmod_btf_type_tag_1 {
int a;
};
@ -980,6 +1021,7 @@ BTF_ID_FLAGS(func, bpf_testmod_ops3_call_test_arena)
BTF_ID_FLAGS(func, bpf_testmod_ops3_call_test_arena_nullable)
BTF_ID_FLAGS(func, bpf_testmod_ops3_call_test_arena_stack)
BTF_ID_FLAGS(func, bpf_testmod_ops3_call_test_arena_multislot)
BTF_ID_FLAGS(func, bpf_testmod_ops3_call_test_nullable)
BTF_ID_FLAGS(func, bpf_kfunc_get_default_trusted_ptr_test);
BTF_ID_FLAGS(func, bpf_kfunc_put_default_trusted_ptr_test);
BTF_ID_FLAGS(func, bpf_kfunc_kasan_poison)
@ -1956,6 +1998,7 @@ static const struct bpf_verifier_ops bpf_testmod_verifier_ops = {
};
static const struct bpf_verifier_ops bpf_testmod_verifier_ops3 = {
.get_func_proto = bpf_base_func_proto,
.is_valid_access = bpf_testmod_ops_is_valid_access,
};

View File

@ -114,6 +114,38 @@ struct bpf_testmod_arena_pair {
u64 b;
};
/*
* The qualifiers are BTF type tags for the verifier. The module gets them
* from bpf_compiler_types.h, user space and BPF programs need them empty.
*/
#ifdef __KERNEL__
#include <linux/bpf_compiler_types.h>
#endif
#ifndef __nullable
#define __nullable
#endif
#ifndef __percpu
#define __percpu
#endif
struct bpf_testmod_pcpu {
int val;
};
/* Arguments of test_nullable(), trusted through the __safe_trusted alias in the module */
struct bpf_testmod_nullable_args {
/* never NULL */
struct task_struct *task;
/* NULL on the first call, set on the second */
struct task_struct __nullable *task_or_null;
/* NULL on the first call, a per-CPU variable on the second */
struct bpf_testmod_pcpu __percpu __nullable *pcpu_or_null;
/* NULL on the first call, set on the second */
int __nullable *int_or_null;
/* this struct again, through a plain pointer, which the verifier probes */
struct bpf_testmod_nullable_args *self;
};
struct bpf_testmod_ops3 {
int (*test_1)(void);
int (*test_2)(void);
@ -125,6 +157,8 @@ struct bpf_testmod_ops3 {
u64 g, u64 h, u64 *ptr);
/* a multi-slot leading arg, so @ptr is not at the slot its arg index suggests */
int (*test_arena_multislot)(struct bpf_testmod_arena_pair p, u64 *ptr);
/* Used to test __nullable pointer members. */
int (*test_nullable)(struct bpf_testmod_nullable_args *args);
};
struct st_ops_args {

View File

@ -224,6 +224,7 @@ int bpf_testmod_ops3_call_test_arena(__u64 *ptr__arena) __ksym;
int bpf_testmod_ops3_call_test_arena_nullable(__u64 *ptr__arena__nullable) __ksym;
int bpf_testmod_ops3_call_test_arena_stack(__u64 *ptr__arena) __ksym;
int bpf_testmod_ops3_call_test_arena_multislot(__u64 *ptr__arena) __ksym;
int bpf_testmod_ops3_call_test_nullable(void) __ksym;
__u64 bpf_kfunc_call_test1(struct sock *sk, __u32 a, __u64 b,
__u32 c, __u64 d) __ksym;

View File

@ -8,6 +8,7 @@
#include <sys/mman.h>
#include <alloca.h>
#include <bpf/bpf.h>
#include <bpf/btf.h>
#include <bpf/libbpf.h>
#include "disasm.h"
#include "test_progs.h"
@ -644,3 +645,47 @@ int compare_text_to_expected(const char *actual, const char *expected)
}
return -1;
}
int testmod_member_has_type_tag(const char *struct_name, const char *member, const char *tag)
{
struct btf *vmlinux_btf, *module_btf = NULL;
const struct btf_member *m;
const struct btf_type *t;
int err = 0, id, i;
vmlinux_btf = btf__load_vmlinux_btf();
if (!vmlinux_btf)
return -errno;
module_btf = btf__load_module_btf("bpf_testmod", vmlinux_btf);
if (!module_btf) {
err = -errno;
goto out;
}
id = btf__find_by_name_kind_own(module_btf, struct_name, BTF_KIND_STRUCT);
if (id < 0) {
err = id;
goto out;
}
t = btf__type_by_id(module_btf, id);
err = -ENOENT;
for (i = 0, m = btf_members(t); i < btf_vlen(t); i++, m++) {
if (strcmp(btf__name_by_offset(module_btf, m->name_off), member))
continue;
err = 0;
/* the tag sits on the pointee, behind the pointer and other modifiers */
for (id = m->type; id; id = t->type) {
t = btf__type_by_id(module_btf, id);
if (btf_is_type_tag(t) && !strcmp(btf__name_by_offset(module_btf, t->name_off), tag)) {
err = 1;
break;
}
if (!btf_is_ptr(t) && !btf_is_mod(t) && !btf_is_typedef(t))
break;
}
break;
}
out:
btf__free(module_btf);
btf__free(vmlinux_btf);
return err;
}

View File

@ -67,4 +67,11 @@ int stack_mprotect(void);
/* Runs diff(1) on mismatch */
int compare_text_to_expected(const char *actual, const char *expected);
/*
* Whether @member of struct @struct_name in the BTF of bpf_testmod carries
* the BTF type tag @tag: 1 if so, 0 if not, negative on a BTF error or when
* the struct or the member is missing.
*/
int testmod_member_has_type_tag(const char *struct_name, const char *member, const char *tag);
#endif /* __TESTING_HELPERS_H */