xref: /linux/tools/testing/selftests/bpf/progs/verifier_btf_array_access.c (revision a8471eb0747f083ae5bde949e667d3d799d65cf7)
1 // SPDX-License-Identifier: GPL-2.0
2 /* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */
3 
4 #include <vmlinux.h>
5 #include <bpf/bpf_helpers.h>
6 #include <bpf/bpf_core_read.h>
7 #include "bpf_experimental.h"
8 #include "bpf_misc.h"
9 
10 /*
11  * Variable-offset access into arrays within a BTF access chain, e.g. a[i].b,
12  * where 'i' is a register with a varying offset. The register's step has to
13  * match the stride of one of the arrays crossed while walking the access
14  * chain, and the maximal possible offset has to stay within that array.
15  *
16  * A program-allocated object (bpf_obj_new) is used as the source of a
17  * PTR_TO_BTF_ID with a shape we fully control.
18  */
19 struct inner {
20 	int a;
21 	int b;
22 };
23 
24 struct outer {
25 	struct inner arr[8];	/* off 0,   stride 8, size 64 */
26 	char bytes[32];		/* off 64,  stride 1, size 32 */
27 	long longs[8];		/* off 96,  stride 8, size 64 */
28 	int grid[4][4];		/* off 160, 16 ints,  size 64 */
29 };
30 
31 /* arr[i].b, i in [0, 7], 4-byte read: stays within arr. */
32 SEC("syscall")
33 __success
34 int arr_field_in_bounds(void *ctx)
35 {
36 	unsigned long i = bpf_get_prandom_u32();
37 	struct outer *o;
38 	int val = 0;
39 
40 	o = bpf_obj_new(typeof(*o));
41 	if (!o)
42 		return 0;
43 	asm volatile ("						\
44 	r1 = %[o];						\
45 	r2 = %[i];						\
46 	r2 &= 7;						\
47 	r2 *= 8;						\
48 	r1 += r2;						\
49 	%[val] = *(u32 *)(r1 + 4);				\
50 "	: [val] "=r"(val)
51 	: [o] "r"(o), [i] "r"(i)
52 	: "r1", "r2");
53 	bpf_obj_drop(o);
54 	return val;
55 }
56 
57 /* arr[i].b, i in [0, 15]: max offset runs past the end of arr. */
58 SEC("syscall")
59 __failure
60 __msg("invalid variable offset access into struct outer")
61 int arr_field_out_of_bounds(void *ctx)
62 {
63 	unsigned long i = bpf_get_prandom_u32();
64 	struct outer *o;
65 	int val = 0;
66 
67 	o = bpf_obj_new(typeof(*o));
68 	if (!o)
69 		return 0;
70 	asm volatile ("						\
71 	r1 = %[o];						\
72 	r2 = %[i];						\
73 	r2 &= 15;						\
74 	r2 *= 8;						\
75 	r1 += r2;						\
76 	%[val] = *(u32 *)(r1 + 4);				\
77 "	: [val] "=r"(val)
78 	: [o] "r"(o), [i] "r"(i)
79 	: "r1", "r2");
80 	bpf_obj_drop(o);
81 	return val;
82 }
83 
84 /* longs[i], i in [0, 7], 8-byte read: scalar array with stride 8. */
85 SEC("syscall")
86 __success
87 int long_array_in_bounds(void *ctx)
88 {
89 	unsigned long i = bpf_get_prandom_u32();
90 	struct outer *o;
91 	long val = 0;
92 
93 	o = bpf_obj_new(typeof(*o));
94 	if (!o)
95 		return 0;
96 	asm volatile ("						\
97 	r1 = %[o];						\
98 	r2 = %[i];						\
99 	r2 &= 7;						\
100 	r2 *= 8;						\
101 	r1 += r2;						\
102 	%[val] = *(u64 *)(r1 + 96);				\
103 "	: [val] "=r"(val)
104 	: [o] "r"(o), [i] "r"(i)
105 	: "r1", "r2");
106 	bpf_obj_drop(o);
107 	return val;
108 }
109 
110 /*
111  * bytes[i], i in [0, 31], read as 4 bytes: min_off is valid within the array,
112  * but the 4-byte read at max_off runs past the array end. Exercises the
113  * size-aware bound in btf_struct_access().
114  */
115 SEC("syscall")
116 __failure
117 __flag(BPF_F_ANY_ALIGNMENT)
118 __msg("invalid variable offset access into struct outer")
119 int byte_array_size_spanning(void *ctx)
120 {
121 	unsigned long i = bpf_get_prandom_u32();
122 	struct outer *o;
123 	int val = 0;
124 
125 	o = bpf_obj_new(typeof(*o));
126 	if (!o)
127 		return 0;
128 	asm volatile ("						\
129 	r1 = %[o];						\
130 	r2 = %[i];						\
131 	r2 &= 31;						\
132 	r1 += r2;						\
133 	%[val] = *(u32 *)(r1 + 64);				\
134 "	: [val] "=r"(val)
135 	: [o] "r"(o), [i] "r"(i)
136 	: "r1", "r2");
137 	bpf_obj_drop(o);
138 	return val;
139 }
140 
141 /* bytes[i], i in [0, 31], 1-byte read: step 1 matches the char array stride. */
142 SEC("syscall")
143 __success
144 int byte_array_in_bounds(void *ctx)
145 {
146 	unsigned long i = bpf_get_prandom_u32();
147 	struct outer *o;
148 	int val = 0;
149 
150 	o = bpf_obj_new(typeof(*o));
151 	if (!o)
152 		return 0;
153 	asm volatile ("						\
154 	r1 = %[o];						\
155 	r2 = %[i];						\
156 	r2 &= 31;						\
157 	r1 += r2;						\
158 	%[val] = *(u8 *)(r1 + 64);				\
159 "	: [val] "=r"(val)
160 	: [o] "r"(o), [i] "r"(i)
161 	: "r1", "r2");
162 	bpf_obj_drop(o);
163 	return val;
164 }
165 
166 /*
167  * arr[] accessed with step 4 (half of sizeof(struct inner)): the step is not
168  * a whole number of elements, so no crossed array has a matching stride.
169  */
170 SEC("syscall")
171 __failure
172 __msg("invalid variable offset access into struct outer")
173 int arr_unaligned_step(void *ctx)
174 {
175 	unsigned long i = bpf_get_prandom_u32();
176 	struct outer *o;
177 	int val = 0;
178 
179 	o = bpf_obj_new(typeof(*o));
180 	if (!o)
181 		return 0;
182 	asm volatile ("						\
183 	r1 = %[o];						\
184 	r2 = %[i];						\
185 	r2 &= 7;						\
186 	r2 *= 4;						\
187 	r1 += r2;						\
188 	%[val] = *(u32 *)(r1 + 0);				\
189 "	: [val] "=r"(val)
190 	: [o] "r"(o), [i] "r"(i)
191 	: "r1", "r2");
192 	bpf_obj_drop(o);
193 	return val;
194 }
195 
196 /*
197  * grid[i][0], i in [0, 3]: steps along the outer dimension of a 2D array
198  * (stride 16). __btf_resolve_size() linearizes the array; the step is a
199  * multiple of the innermost element size (4).
200  */
201 SEC("syscall")
202 __success
203 int grid_outer_dim(void *ctx)
204 {
205 	unsigned long i = bpf_get_prandom_u32();
206 	struct outer *o;
207 	int val = 0;
208 
209 	o = bpf_obj_new(typeof(*o));
210 	if (!o)
211 		return 0;
212 	asm volatile ("						\
213 	r1 = %[o];						\
214 	r2 = %[i];						\
215 	r2 &= 3;						\
216 	r2 *= 16;						\
217 	r1 += r2;						\
218 	%[val] = *(u32 *)(r1 + 160);				\
219 "	: [val] "=r"(val)
220 	: [o] "r"(o), [i] "r"(i)
221 	: "r1", "r2");
222 	bpf_obj_drop(o);
223 	return val;
224 }
225 
226 /*
227  * grid[0][i], i in [0, 3]: steps along the inner dimension of a 2D array
228  * (stride 4, the innermost element size).
229  */
230 SEC("syscall")
231 __success
232 int grid_inner_dim(void *ctx)
233 {
234 	unsigned long i = bpf_get_prandom_u32();
235 	struct outer *o;
236 	int val = 0;
237 
238 	o = bpf_obj_new(typeof(*o));
239 	if (!o)
240 		return 0;
241 	asm volatile ("						\
242 	r1 = %[o];						\
243 	r2 = %[i];						\
244 	r2 &= 3;						\
245 	r2 *= 4;						\
246 	r1 += r2;						\
247 	%[val] = *(u32 *)(r1 + 160);				\
248 "	: [val] "=r"(val)
249 	: [o] "r"(o), [i] "r"(i)
250 	: "r1", "r2");
251 	bpf_obj_drop(o);
252 	return val;
253 }
254 
255 /*
256  * An array of pointers within a program-allocated object. Reading a pointer
257  * member of a local object yields a SCALAR_VALUE, but the varying offset still
258  * has to be validated against the array bounds.
259  */
260 struct with_ptrs {
261 	struct inner *parr[8];	/* off 0, stride 8 (sizeof ptr), size 64 */
262 };
263 
264 /* parr[i], i in [0, 7], 8-byte read: stays within parr. */
265 SEC("syscall")
266 __success
267 int ptr_array_in_bounds(void *ctx)
268 {
269 	unsigned long i = bpf_get_prandom_u32();
270 	struct with_ptrs *o;
271 	long val = 0;
272 
273 	o = bpf_obj_new(typeof(*o));
274 	if (!o)
275 		return 0;
276 	asm volatile ("						\
277 	r1 = %[o];						\
278 	r2 = %[i];						\
279 	r2 &= 7;						\
280 	r2 *= 8;						\
281 	r1 += r2;						\
282 	%[val] = *(u64 *)(r1 + 0);				\
283 "	: [val] "=r"(val)
284 	: [o] "r"(o), [i] "r"(i)
285 	: "r1", "r2");
286 	bpf_obj_drop(o);
287 	return val;
288 }
289 
290 /*
291  * parr[i], i in [0, 15]: max offset runs past the end of parr. Without the
292  * variable-offset check on the WALK_PTR path this would be wrongly accepted.
293  */
294 SEC("syscall")
295 __failure
296 __msg("invalid variable offset access into struct with_ptrs")
297 int ptr_array_out_of_bounds(void *ctx)
298 {
299 	unsigned long i = bpf_get_prandom_u32();
300 	struct with_ptrs *o;
301 	long val = 0;
302 
303 	o = bpf_obj_new(typeof(*o));
304 	if (!o)
305 		return 0;
306 	asm volatile ("						\
307 	r1 = %[o];						\
308 	r2 = %[i];						\
309 	r2 &= 15;						\
310 	r2 *= 8;						\
311 	r1 += r2;						\
312 	%[val] = *(u64 *)(r1 + 0);				\
313 "	: [val] "=r"(val)
314 	: [o] "r"(o), [i] "r"(i)
315 	: "r1", "r2");
316 	bpf_obj_drop(o);
317 	return val;
318 }
319 
320 /*
321  * The cases above all use bpf_obj_new() (PTR_TO_BTF_ID | MEM_ALLOC). Exercise
322  * the trusted kernel-BTF path too, using a task_struct from
323  * bpf_get_current_task_btf() and its embedded char comm[] array. The comm
324  * offset is folded into the pointer as a constant before the varying index.
325  */
326 
327 /* task->comm[i], i in [0, 15], 1-byte read: stays within comm. */
328 SEC("syscall")
329 __success
330 int kernel_btf_array_in_bounds(void *ctx)
331 {
332 	unsigned long i = bpf_get_prandom_u32();
333 	struct task_struct *task;
334 	int val = 0;
335 
336 	task = bpf_get_current_task_btf();
337 	asm volatile ("						\
338 	r1 = %[task];						\
339 	r1 += %[off];						\
340 	r2 = %[i];						\
341 	r2 &= 15;						\
342 	r1 += r2;						\
343 	%[val] = *(u8 *)(r1 + 0);				\
344 "	: [val] "=r"(val)
345 	: [task] "r"(task), [i] "r"(i),
346 	  [off] "i"(offsetof(struct task_struct, comm))
347 	: "r1", "r2");
348 	return val;
349 }
350 
351 /* task->comm[i], i in [0, 31]: max offset runs past comm. */
352 SEC("syscall")
353 __failure
354 __msg("invalid variable offset access into struct task_struct")
355 int kernel_btf_array_out_of_bounds(void *ctx)
356 {
357 	unsigned long i = bpf_get_prandom_u32();
358 	struct task_struct *task;
359 	int val = 0;
360 
361 	task = bpf_get_current_task_btf();
362 	asm volatile ("						\
363 	r1 = %[task];						\
364 	r1 += %[off];						\
365 	r2 = %[i];						\
366 	r2 &= 31;						\
367 	r1 += r2;						\
368 	%[val] = *(u8 *)(r1 + 0);				\
369 "	: [val] "=r"(val)
370 	: [task] "r"(task), [i] "r"(i),
371 	  [off] "i"(offsetof(struct task_struct, comm))
372 	: "r1", "r2");
373 	return val;
374 }
375 
376 /*
377  * Array nested below a struct member at a non-zero offset. The walk descends
378  * through 'm' (resetting the running offset) before reaching 'arr', exercising
379  * the array_start = min_off - arrays[i].off telescoping across a WALK_STRUCT
380  * dive.
381  */
382 struct mid {
383 	struct inner arr[8];	/* stride 8, size 64 */
384 };
385 
386 struct nest {
387 	long pad;		/* off 0 */
388 	struct mid m;		/* off 8 */
389 };
390 
391 /* m.arr[i].b, i in [0, 7], 4-byte read: stays within m.arr. */
392 SEC("syscall")
393 __success
394 int nested_struct_in_bounds(void *ctx)
395 {
396 	unsigned long i = bpf_get_prandom_u32();
397 	struct nest *o;
398 	int val = 0;
399 
400 	o = bpf_obj_new(typeof(*o));
401 	if (!o)
402 		return 0;
403 	asm volatile ("						\
404 	r1 = %[o];						\
405 	r2 = %[i];						\
406 	r2 &= 7;						\
407 	r2 *= 8;						\
408 	r1 += r2;						\
409 	%[val] = *(u32 *)(r1 + 12);				\
410 "	: [val] "=r"(val)
411 	: [o] "r"(o), [i] "r"(i)
412 	: "r1", "r2");
413 	bpf_obj_drop(o);
414 	return val;
415 }
416 
417 /* m.arr[i].b, i in [0, 15]: max offset runs past the end of m.arr. */
418 SEC("syscall")
419 __failure
420 __msg("invalid variable offset access into struct nest")
421 int nested_struct_out_of_bounds(void *ctx)
422 {
423 	unsigned long i = bpf_get_prandom_u32();
424 	struct nest *o;
425 	int val = 0;
426 
427 	o = bpf_obj_new(typeof(*o));
428 	if (!o)
429 		return 0;
430 	asm volatile ("						\
431 	r1 = %[o];						\
432 	r2 = %[i];						\
433 	r2 &= 15;						\
434 	r2 *= 8;						\
435 	r1 += r2;						\
436 	%[val] = *(u32 *)(r1 + 12);				\
437 "	: [val] "=r"(val)
438 	: [o] "r"(o), [i] "r"(i)
439 	: "r1", "r2");
440 	bpf_obj_drop(o);
441 	return val;
442 }
443 
444 /*
445  * A trailing flexible array member makes the struct tail an unbounded region,
446  * so a varying access into it has no upper bound to exceed (mirrors how
447  * unix_address.name[] is accessed via sun_path[i]).
448  *
449  * A custom (program) BTF struct can only be reached as a PTR_TO_BTF_ID through
450  * bpf_obj_new(), which does not allow flexible array members. So use a kernel
451  * type via an untrusted PTR_TO_BTF_ID (bpf_core_cast()). struct vring_used ends
452  * with a flexible array 'ring[]' of struct vring_used_elem { __virtio32 id, len; },
453  * mirroring a 'struct foo { int a; int b; }' flexible array.
454  */
455 
456 /* ring[i].len, i in [0, 63], 4-byte read: the flexible array has no upper bound. */
457 SEC("syscall")
458 __success
459 int flex_array_field(void *ctx)
460 {
461 	unsigned long i = bpf_get_prandom_u32();
462 	struct vring_used *o;
463 	int val = 0;
464 
465 	o = bpf_core_cast(bpf_get_current_task_btf(), struct vring_used);
466 	asm volatile ("						\
467 	r1 = %[o];						\
468 	r2 = %[i];						\
469 	r2 &= 63;						\
470 	r2 *= 8;						\
471 	r1 += r2;						\
472 	%[val] = *(u32 *)(r1 + 8);				\
473 "	: [val] "=r"(val)
474 	: [o] "r"(o), [i] "r"(i)
475 	: "r1", "r2");
476 	return val;
477 }
478 
479 /*
480  * ring[] accessed with step 4 (half of sizeof(struct vring_used_elem)): the
481  * step is not a whole number of elements, so the flexible array stride does
482  * not match.
483  */
484 SEC("syscall")
485 __failure
486 __msg("invalid variable offset access into struct vring_used")
487 int flex_array_unaligned_step(void *ctx)
488 {
489 	unsigned long i = bpf_get_prandom_u32();
490 	struct vring_used *o;
491 	int val = 0;
492 
493 	o = bpf_core_cast(bpf_get_current_task_btf(), struct vring_used);
494 	asm volatile ("						\
495 	r1 = %[o];						\
496 	r2 = %[i];						\
497 	r2 &= 7;						\
498 	r2 *= 4;						\
499 	r1 += r2;						\
500 	%[val] = *(u32 *)(r1 + 4);				\
501 "	: [val] "=r"(val)
502 	: [o] "r"(o), [i] "r"(i)
503 	: "r1", "r2");
504 	return val;
505 }
506 
507 /*
508  * Access either name[0].sun_path[0] or one byte past sun_path.
509  * Step 108 does not match sizeof(struct sockaddr_un), which is 110.
510  * It matches the inner char array's stride, but exceeds its bounds.
511  * The flexible-tail exemption must not apply to that fixed array.
512  */
513 SEC("syscall")
514 __failure
515 __msg("invalid variable offset access into struct unix_address")
516 int flex_array_inner_array_out_of_bounds(void *ctx)
517 {
518 	unsigned long i = bpf_get_prandom_u32();
519 	struct unix_address *o;
520 	int val = 0;
521 
522 	o = bpf_core_cast(bpf_get_current_task_btf(), struct unix_address);
523 	asm volatile ("						\
524 	r1 = %[o];						\
525 	r2 = %[i];						\
526 	r2 &= 1;						\
527 	r2 *= %[len];						\
528 	r1 += r2;						\
529 	%[val] = *(u8 *)(r1 + %[off]);				\
530 "	: [val] "=r"(val)
531 	: [o] "r"(o), [i] "r"(i),
532 	  __imm_const(len, sizeof_field(struct sockaddr_un, sun_path)),
533 	  __imm_const(off, offsetof(struct unix_address, name) +
534 			   offsetof(struct sockaddr_un, sun_path))
535 	: "r1", "r2");
536 	return val;
537 }
538 
539 typedef int array_element_t;
540 typedef array_element_t array_type_t[4];
541 
542 struct typedef_array {
543 	array_type_t values;
544 };
545 
546 /*
547  * Resolving values' size crosses both the array and element typedefs.
548  * Record the array ID, not the int ID left by element-type resolution.
549  * values[i], i in [0, 3], must pass the variable-offset array check.
550  */
551 SEC("syscall")
552 __success
553 int typedef_array_in_bounds(void *ctx)
554 {
555 	unsigned long i = bpf_get_prandom_u32();
556 	struct typedef_array *o;
557 	int val = 0;
558 
559 	o = bpf_obj_new(typeof(*o));
560 	if (!o)
561 		return 0;
562 	asm volatile ("						\
563 	r1 = %[o];						\
564 	r2 = %[i];						\
565 	r2 &= 3;						\
566 	r2 *= %[elem_size];					\
567 	r1 += r2;						\
568 	%[val] = *(u32 *)(r1 + 0);				\
569 "	: [val] "=r"(val)
570 	: [o] "r"(o), [i] "r"(i),
571 	  __imm_const(elem_size, sizeof(array_element_t))
572 	: "r1", "r2");
573 	bpf_obj_drop(o);
574 	return val;
575 }
576 
577 char _license[] SEC("license") = "GPL";
578