summaryrefslogtreecommitdiff
path: root/tools
diff options
context:
space:
mode:
authorEduard Zingerman <eddyz87@gmail.com>2026-10-09 03:48:06 -0700
committerAlexei Starovoitov <ast@kernel.org>2026-10-09 20:29:15 +0000
commite187fac8ac6bc1a73647938e600da91c5edf2624 (patch)
treec350654dcdcc83aa21b4ef3b2c6be25df6827d29 /tools
parent8c5f6dfefdf17f42979e2ca0fe13f80dd80b34df (diff)
selftests/bpf: Tests for varying offset access to PTR_TO_BTF_ID
Verifier tests for accessing arrays at a varying offset through a PTR_TO_BTF_ID pointer, e.g. a[i].b where 'i' is a register with a varying value: - bpf_obj_new() is used to obtain a PTR_TO_BTF_ID to a program-defined struct - bpf_get_current_task_btf() is used to obtain a PTR_TO_BTF_ID from the kernel BTF. The cases cover: - in-bounds access to a field of an array of structs; - out-of-bounds access where the maximal offset runs past the array; - a scalar (long) array with a matching stride; - a byte array (step 1); - a 4-byte read whose maximal offset spills past the array end; - an unaligned step that is not a whole number of elements; - both dimensions of a 2D array; - an array of pointers within a program-allocated object, where reading a pointer member yields a SCALAR_VALUE but the varying offset is still validated against the array bounds (in-bounds and out-of-bounds); - a varying access into an array embedded in a kernel BTF type (task_struct's comm[]), covering the non-allocated, trusted-pointer path (in-bounds and out-of-bounds); - an array nested below a struct member at a non-zero offset, exercising the offset telescoping done while walking across a struct boundary (in-bounds and out-of-bounds); - a trailing flexible array member, whose unbounded tail makes a varying access have no upper bound to exceed. A program-defined struct cannot be used here because bpf_obj_new() forbids flexible array members, so a kernel type (struct vring_used, with its 'ring[]' tail) is used (in-bounds, and an unaligned step that is not a whole number of elements). Signed-off-by: Eduard Zingerman <eddyz87@gmail.com> Signed-off-by: Alexei Starovoitov <ast@kernel.org> Link: https://patch.msgid.link/20261009-scev-minimal-rebase-v3-48-bcba9bc53ac9@gmail.com
Diffstat (limited to 'tools')
-rw-r--r--tools/testing/selftests/bpf/prog_tests/verifier.c2
-rw-r--r--tools/testing/selftests/bpf/progs/verifier_btf_array_access.c576
2 files changed, 578 insertions, 0 deletions
diff --git a/tools/testing/selftests/bpf/prog_tests/verifier.c b/tools/testing/selftests/bpf/prog_tests/verifier.c
index 4f3af1cb998b..2f05a0ce829f 100644
--- a/tools/testing/selftests/bpf/prog_tests/verifier.c
+++ b/tools/testing/selftests/bpf/prog_tests/verifier.c
@@ -27,6 +27,7 @@
#include "verifier_bpf_get_stack.skel.h"
#include "verifier_bpf_trap.skel.h"
#include "verifier_bswap.skel.h"
+#include "verifier_btf_array_access.skel.h"
#include "verifier_btf_ctx_access.skel.h"
#include "verifier_btf_flex_array.skel.h"
#include "verifier_btf_unreliable_prog.skel.h"
@@ -213,6 +214,7 @@ void test_verifier_bounds_mix_sign_unsign(void) { RUN(verifier_bounds_mix_sign_u
void test_verifier_bpf_get_stack(void) { RUN(verifier_bpf_get_stack); }
void test_verifier_bpf_trap(void) { RUN(verifier_bpf_trap); }
void test_verifier_bswap(void) { RUN(verifier_bswap); }
+void test_verifier_btf_array_access(void) { RUN(verifier_btf_array_access); }
void test_verifier_btf_ctx_access(void) { RUN(verifier_btf_ctx_access); }
void test_verifier_btf_flex_array(void) { RUN(verifier_btf_flex_array); }
void test_verifier_btf_unreliable_prog(void) { RUN(verifier_btf_unreliable_prog); }
diff --git a/tools/testing/selftests/bpf/progs/verifier_btf_array_access.c b/tools/testing/selftests/bpf/progs/verifier_btf_array_access.c
new file mode 100644
index 000000000000..be001cc77f81
--- /dev/null
+++ b/tools/testing/selftests/bpf/progs/verifier_btf_array_access.c
@@ -0,0 +1,576 @@
+// 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_core_read.h>
+#include "bpf_experimental.h"
+#include "bpf_misc.h"
+
+/*
+ * Variable-offset access into arrays within a BTF access chain, e.g. a[i].b,
+ * where 'i' is a register with a varying offset. The register's step has to
+ * match the stride of one of the arrays crossed while walking the access
+ * chain, and the maximal possible offset has to stay within that array.
+ *
+ * A program-allocated object (bpf_obj_new) is used as the source of a
+ * PTR_TO_BTF_ID with a shape we fully control.
+ */
+struct inner {
+ int a;
+ int b;
+};
+
+struct outer {
+ struct inner arr[8]; /* off 0, stride 8, size 64 */
+ char bytes[32]; /* off 64, stride 1, size 32 */
+ long longs[8]; /* off 96, stride 8, size 64 */
+ int grid[4][4]; /* off 160, 16 ints, size 64 */
+};
+
+/* arr[i].b, i in [0, 7], 4-byte read: stays within arr. */
+SEC("syscall")
+__success
+int arr_field_in_bounds(void *ctx)
+{
+ unsigned long i = bpf_get_prandom_u32();
+ struct outer *o;
+ int val = 0;
+
+ o = bpf_obj_new(typeof(*o));
+ if (!o)
+ return 0;
+ asm volatile (" \
+ r1 = %[o]; \
+ r2 = %[i]; \
+ r2 &= 7; \
+ r2 *= 8; \
+ r1 += r2; \
+ %[val] = *(u32 *)(r1 + 4); \
+" : [val] "=r"(val)
+ : [o] "r"(o), [i] "r"(i)
+ : "r1", "r2");
+ bpf_obj_drop(o);
+ return val;
+}
+
+/* arr[i].b, i in [0, 15]: max offset runs past the end of arr. */
+SEC("syscall")
+__failure
+__msg("invalid variable offset access into struct outer")
+int arr_field_out_of_bounds(void *ctx)
+{
+ unsigned long i = bpf_get_prandom_u32();
+ struct outer *o;
+ int val = 0;
+
+ o = bpf_obj_new(typeof(*o));
+ if (!o)
+ return 0;
+ asm volatile (" \
+ r1 = %[o]; \
+ r2 = %[i]; \
+ r2 &= 15; \
+ r2 *= 8; \
+ r1 += r2; \
+ %[val] = *(u32 *)(r1 + 4); \
+" : [val] "=r"(val)
+ : [o] "r"(o), [i] "r"(i)
+ : "r1", "r2");
+ bpf_obj_drop(o);
+ return val;
+}
+
+/* longs[i], i in [0, 7], 8-byte read: scalar array with stride 8. */
+SEC("syscall")
+__success
+int long_array_in_bounds(void *ctx)
+{
+ unsigned long i = bpf_get_prandom_u32();
+ struct outer *o;
+ long val = 0;
+
+ o = bpf_obj_new(typeof(*o));
+ if (!o)
+ return 0;
+ asm volatile (" \
+ r1 = %[o]; \
+ r2 = %[i]; \
+ r2 &= 7; \
+ r2 *= 8; \
+ r1 += r2; \
+ %[val] = *(u64 *)(r1 + 96); \
+" : [val] "=r"(val)
+ : [o] "r"(o), [i] "r"(i)
+ : "r1", "r2");
+ bpf_obj_drop(o);
+ return val;
+}
+
+/*
+ * bytes[i], i in [0, 31], read as 4 bytes: min_off is valid within the array,
+ * but the 4-byte read at max_off runs past the array end. Exercises the
+ * size-aware bound in btf_struct_access().
+ */
+SEC("syscall")
+__failure
+__msg("invalid variable offset access into struct outer")
+int byte_array_size_spanning(void *ctx)
+{
+ unsigned long i = bpf_get_prandom_u32();
+ struct outer *o;
+ int val = 0;
+
+ o = bpf_obj_new(typeof(*o));
+ if (!o)
+ return 0;
+ asm volatile (" \
+ r1 = %[o]; \
+ r2 = %[i]; \
+ r2 &= 31; \
+ r1 += r2; \
+ %[val] = *(u32 *)(r1 + 64); \
+" : [val] "=r"(val)
+ : [o] "r"(o), [i] "r"(i)
+ : "r1", "r2");
+ bpf_obj_drop(o);
+ return val;
+}
+
+/* bytes[i], i in [0, 31], 1-byte read: step 1 matches the char array stride. */
+SEC("syscall")
+__success
+int byte_array_in_bounds(void *ctx)
+{
+ unsigned long i = bpf_get_prandom_u32();
+ struct outer *o;
+ int val = 0;
+
+ o = bpf_obj_new(typeof(*o));
+ if (!o)
+ return 0;
+ asm volatile (" \
+ r1 = %[o]; \
+ r2 = %[i]; \
+ r2 &= 31; \
+ r1 += r2; \
+ %[val] = *(u8 *)(r1 + 64); \
+" : [val] "=r"(val)
+ : [o] "r"(o), [i] "r"(i)
+ : "r1", "r2");
+ bpf_obj_drop(o);
+ return val;
+}
+
+/*
+ * arr[] accessed with step 4 (half of sizeof(struct inner)): the step is not
+ * a whole number of elements, so no crossed array has a matching stride.
+ */
+SEC("syscall")
+__failure
+__msg("invalid variable offset access into struct outer")
+int arr_unaligned_step(void *ctx)
+{
+ unsigned long i = bpf_get_prandom_u32();
+ struct outer *o;
+ int val = 0;
+
+ o = bpf_obj_new(typeof(*o));
+ if (!o)
+ return 0;
+ asm volatile (" \
+ r1 = %[o]; \
+ r2 = %[i]; \
+ r2 &= 7; \
+ r2 *= 4; \
+ r1 += r2; \
+ %[val] = *(u32 *)(r1 + 0); \
+" : [val] "=r"(val)
+ : [o] "r"(o), [i] "r"(i)
+ : "r1", "r2");
+ bpf_obj_drop(o);
+ return val;
+}
+
+/*
+ * grid[i][0], i in [0, 3]: steps along the outer dimension of a 2D array
+ * (stride 16). __btf_resolve_size() linearizes the array; the step is a
+ * multiple of the innermost element size (4).
+ */
+SEC("syscall")
+__success
+int grid_outer_dim(void *ctx)
+{
+ unsigned long i = bpf_get_prandom_u32();
+ struct outer *o;
+ int val = 0;
+
+ o = bpf_obj_new(typeof(*o));
+ if (!o)
+ return 0;
+ asm volatile (" \
+ r1 = %[o]; \
+ r2 = %[i]; \
+ r2 &= 3; \
+ r2 *= 16; \
+ r1 += r2; \
+ %[val] = *(u32 *)(r1 + 160); \
+" : [val] "=r"(val)
+ : [o] "r"(o), [i] "r"(i)
+ : "r1", "r2");
+ bpf_obj_drop(o);
+ return val;
+}
+
+/*
+ * grid[0][i], i in [0, 3]: steps along the inner dimension of a 2D array
+ * (stride 4, the innermost element size).
+ */
+SEC("syscall")
+__success
+int grid_inner_dim(void *ctx)
+{
+ unsigned long i = bpf_get_prandom_u32();
+ struct outer *o;
+ int val = 0;
+
+ o = bpf_obj_new(typeof(*o));
+ if (!o)
+ return 0;
+ asm volatile (" \
+ r1 = %[o]; \
+ r2 = %[i]; \
+ r2 &= 3; \
+ r2 *= 4; \
+ r1 += r2; \
+ %[val] = *(u32 *)(r1 + 160); \
+" : [val] "=r"(val)
+ : [o] "r"(o), [i] "r"(i)
+ : "r1", "r2");
+ bpf_obj_drop(o);
+ return val;
+}
+
+/*
+ * An array of pointers within a program-allocated object. Reading a pointer
+ * member of a local object yields a SCALAR_VALUE, but the varying offset still
+ * has to be validated against the array bounds.
+ */
+struct with_ptrs {
+ struct inner *parr[8]; /* off 0, stride 8 (sizeof ptr), size 64 */
+};
+
+/* parr[i], i in [0, 7], 8-byte read: stays within parr. */
+SEC("syscall")
+__success
+int ptr_array_in_bounds(void *ctx)
+{
+ unsigned long i = bpf_get_prandom_u32();
+ struct with_ptrs *o;
+ long val = 0;
+
+ o = bpf_obj_new(typeof(*o));
+ if (!o)
+ return 0;
+ asm volatile (" \
+ r1 = %[o]; \
+ r2 = %[i]; \
+ r2 &= 7; \
+ r2 *= 8; \
+ r1 += r2; \
+ %[val] = *(u64 *)(r1 + 0); \
+" : [val] "=r"(val)
+ : [o] "r"(o), [i] "r"(i)
+ : "r1", "r2");
+ bpf_obj_drop(o);
+ return val;
+}
+
+/*
+ * parr[i], i in [0, 15]: max offset runs past the end of parr. Without the
+ * variable-offset check on the WALK_PTR path this would be wrongly accepted.
+ */
+SEC("syscall")
+__failure
+__msg("invalid variable offset access into struct with_ptrs")
+int ptr_array_out_of_bounds(void *ctx)
+{
+ unsigned long i = bpf_get_prandom_u32();
+ struct with_ptrs *o;
+ long val = 0;
+
+ o = bpf_obj_new(typeof(*o));
+ if (!o)
+ return 0;
+ asm volatile (" \
+ r1 = %[o]; \
+ r2 = %[i]; \
+ r2 &= 15; \
+ r2 *= 8; \
+ r1 += r2; \
+ %[val] = *(u64 *)(r1 + 0); \
+" : [val] "=r"(val)
+ : [o] "r"(o), [i] "r"(i)
+ : "r1", "r2");
+ bpf_obj_drop(o);
+ return val;
+}
+
+/*
+ * The cases above all use bpf_obj_new() (PTR_TO_BTF_ID | MEM_ALLOC). Exercise
+ * the trusted kernel-BTF path too, using a task_struct from
+ * bpf_get_current_task_btf() and its embedded char comm[] array. The comm
+ * offset is folded into the pointer as a constant before the varying index.
+ */
+
+/* task->comm[i], i in [0, 15], 1-byte read: stays within comm. */
+SEC("syscall")
+__success
+int kernel_btf_array_in_bounds(void *ctx)
+{
+ unsigned long i = bpf_get_prandom_u32();
+ struct task_struct *task;
+ int val = 0;
+
+ task = bpf_get_current_task_btf();
+ asm volatile (" \
+ r1 = %[task]; \
+ r1 += %[off]; \
+ r2 = %[i]; \
+ r2 &= 15; \
+ r1 += r2; \
+ %[val] = *(u8 *)(r1 + 0); \
+" : [val] "=r"(val)
+ : [task] "r"(task), [i] "r"(i),
+ [off] "i"(offsetof(struct task_struct, comm))
+ : "r1", "r2");
+ return val;
+}
+
+/* task->comm[i], i in [0, 31]: max offset runs past comm. */
+SEC("syscall")
+__failure
+__msg("invalid variable offset access into struct task_struct")
+int kernel_btf_array_out_of_bounds(void *ctx)
+{
+ unsigned long i = bpf_get_prandom_u32();
+ struct task_struct *task;
+ int val = 0;
+
+ task = bpf_get_current_task_btf();
+ asm volatile (" \
+ r1 = %[task]; \
+ r1 += %[off]; \
+ r2 = %[i]; \
+ r2 &= 31; \
+ r1 += r2; \
+ %[val] = *(u8 *)(r1 + 0); \
+" : [val] "=r"(val)
+ : [task] "r"(task), [i] "r"(i),
+ [off] "i"(offsetof(struct task_struct, comm))
+ : "r1", "r2");
+ return val;
+}
+
+/*
+ * Array nested below a struct member at a non-zero offset. The walk descends
+ * through 'm' (resetting the running offset) before reaching 'arr', exercising
+ * the array_start = min_off - arrays[i].off telescoping across a WALK_STRUCT
+ * dive.
+ */
+struct mid {
+ struct inner arr[8]; /* stride 8, size 64 */
+};
+
+struct nest {
+ long pad; /* off 0 */
+ struct mid m; /* off 8 */
+};
+
+/* m.arr[i].b, i in [0, 7], 4-byte read: stays within m.arr. */
+SEC("syscall")
+__success
+int nested_struct_in_bounds(void *ctx)
+{
+ unsigned long i = bpf_get_prandom_u32();
+ struct nest *o;
+ int val = 0;
+
+ o = bpf_obj_new(typeof(*o));
+ if (!o)
+ return 0;
+ asm volatile (" \
+ r1 = %[o]; \
+ r2 = %[i]; \
+ r2 &= 7; \
+ r2 *= 8; \
+ r1 += r2; \
+ %[val] = *(u32 *)(r1 + 12); \
+" : [val] "=r"(val)
+ : [o] "r"(o), [i] "r"(i)
+ : "r1", "r2");
+ bpf_obj_drop(o);
+ return val;
+}
+
+/* m.arr[i].b, i in [0, 15]: max offset runs past the end of m.arr. */
+SEC("syscall")
+__failure
+__msg("invalid variable offset access into struct nest")
+int nested_struct_out_of_bounds(void *ctx)
+{
+ unsigned long i = bpf_get_prandom_u32();
+ struct nest *o;
+ int val = 0;
+
+ o = bpf_obj_new(typeof(*o));
+ if (!o)
+ return 0;
+ asm volatile (" \
+ r1 = %[o]; \
+ r2 = %[i]; \
+ r2 &= 15; \
+ r2 *= 8; \
+ r1 += r2; \
+ %[val] = *(u32 *)(r1 + 12); \
+" : [val] "=r"(val)
+ : [o] "r"(o), [i] "r"(i)
+ : "r1", "r2");
+ bpf_obj_drop(o);
+ return val;
+}
+
+/*
+ * A trailing flexible array member makes the struct tail an unbounded region,
+ * so a varying access into it has no upper bound to exceed (mirrors how
+ * unix_address.name[] is accessed via sun_path[i]).
+ *
+ * A custom (program) BTF struct can only be reached as a PTR_TO_BTF_ID through
+ * bpf_obj_new(), which does not allow flexible array members. So use a kernel
+ * type via an untrusted PTR_TO_BTF_ID (bpf_core_cast()). struct vring_used ends
+ * with a flexible array 'ring[]' of struct vring_used_elem { __virtio32 id, len; },
+ * mirroring a 'struct foo { int a; int b; }' flexible array.
+ */
+
+/* ring[i].len, i in [0, 63], 4-byte read: the flexible array has no upper bound. */
+SEC("syscall")
+__success
+int flex_array_field(void *ctx)
+{
+ unsigned long i = bpf_get_prandom_u32();
+ struct vring_used *o;
+ int val = 0;
+
+ o = bpf_core_cast(bpf_get_current_task_btf(), struct vring_used);
+ asm volatile (" \
+ r1 = %[o]; \
+ r2 = %[i]; \
+ r2 &= 63; \
+ r2 *= 8; \
+ r1 += r2; \
+ %[val] = *(u32 *)(r1 + 8); \
+" : [val] "=r"(val)
+ : [o] "r"(o), [i] "r"(i)
+ : "r1", "r2");
+ return val;
+}
+
+/*
+ * ring[] accessed with step 4 (half of sizeof(struct vring_used_elem)): the
+ * step is not a whole number of elements, so the flexible array stride does
+ * not match.
+ */
+SEC("syscall")
+__failure
+__msg("invalid variable offset access into struct vring_used")
+int flex_array_unaligned_step(void *ctx)
+{
+ unsigned long i = bpf_get_prandom_u32();
+ struct vring_used *o;
+ int val = 0;
+
+ o = bpf_core_cast(bpf_get_current_task_btf(), struct vring_used);
+ asm volatile (" \
+ r1 = %[o]; \
+ r2 = %[i]; \
+ r2 &= 7; \
+ r2 *= 4; \
+ r1 += r2; \
+ %[val] = *(u32 *)(r1 + 4); \
+" : [val] "=r"(val)
+ : [o] "r"(o), [i] "r"(i)
+ : "r1", "r2");
+ return val;
+}
+
+/*
+ * Access either name[0].sun_path[0] or one byte past sun_path.
+ * Step 108 does not match sizeof(struct sockaddr_un), which is 110.
+ * It matches the inner char array's stride, but exceeds its bounds.
+ * The flexible-tail exemption must not apply to that fixed array.
+ */
+SEC("syscall")
+__failure
+__msg("invalid variable offset access into struct unix_address")
+int flex_array_inner_array_out_of_bounds(void *ctx)
+{
+ unsigned long i = bpf_get_prandom_u32();
+ struct unix_address *o;
+ int val = 0;
+
+ o = bpf_core_cast(bpf_get_current_task_btf(), struct unix_address);
+ asm volatile (" \
+ r1 = %[o]; \
+ r2 = %[i]; \
+ r2 &= 1; \
+ r2 *= %[len]; \
+ r1 += r2; \
+ %[val] = *(u8 *)(r1 + %[off]); \
+" : [val] "=r"(val)
+ : [o] "r"(o), [i] "r"(i),
+ __imm_const(len, sizeof_field(struct sockaddr_un, sun_path)),
+ __imm_const(off, offsetof(struct unix_address, name) +
+ offsetof(struct sockaddr_un, sun_path))
+ : "r1", "r2");
+ return val;
+}
+
+typedef int array_element_t;
+typedef array_element_t array_type_t[4];
+
+struct typedef_array {
+ array_type_t values;
+};
+
+/*
+ * Resolving values' size crosses both the array and element typedefs.
+ * Record the array ID, not the int ID left by element-type resolution.
+ * values[i], i in [0, 3], must pass the variable-offset array check.
+ */
+SEC("syscall")
+__success
+int typedef_array_in_bounds(void *ctx)
+{
+ unsigned long i = bpf_get_prandom_u32();
+ struct typedef_array *o;
+ int val = 0;
+
+ o = bpf_obj_new(typeof(*o));
+ if (!o)
+ return 0;
+ asm volatile (" \
+ r1 = %[o]; \
+ r2 = %[i]; \
+ r2 &= 3; \
+ r2 *= %[elem_size]; \
+ r1 += r2; \
+ %[val] = *(u32 *)(r1 + 0); \
+" : [val] "=r"(val)
+ : [o] "r"(o), [i] "r"(i),
+ __imm_const(elem_size, sizeof(array_element_t))
+ : "r1", "r2");
+ bpf_obj_drop(o);
+ return val;
+}
+
+char _license[] SEC("license") = "GPL";