Linux kernel mirror (for testing)
git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
kernel
os
linux
1// SPDX-License-Identifier: GPL-2.0
2/* Copyright (c) 2025 Meta Platforms, Inc. and affiliates. */
3
4#include <linux/bpf.h>
5#include <bpf/bpf_helpers.h>
6#include "../../../include/linux/filter.h"
7#include "bpf_misc.h"
8
9char _license[] SEC("license") = "GPL";
10struct {
11 __uint(type, BPF_MAP_TYPE_HASH);
12 __uint(max_entries, 1);
13 __type(key, int);
14 __type(value, long long);
15} map SEC(".maps");
16
17struct {
18 __uint(type, BPF_MAP_TYPE_ARRAY);
19 __uint(max_entries, 1);
20 __type(key, __u32);
21 __type(value, __u64);
22} array_map_8b SEC(".maps");
23
24const char snprintf_u64_fmt[] = "%llu";
25
26SEC("socket")
27__log_level(2)
28__msg("0: (79) r1 = *(u64 *)(r10 -8) ; use: fp0-8")
29__msg("1: (79) r2 = *(u64 *)(r10 -24) ; use: fp0-24")
30__msg("2: (7b) *(u64 *)(r10 -8) = r1 ; def: fp0-8")
31__naked void simple_read_simple_write(void)
32{
33 asm volatile (
34 "r1 = *(u64 *)(r10 - 8);"
35 "r2 = *(u64 *)(r10 - 24);"
36 "*(u64 *)(r10 - 8) = r1;"
37 "r0 = 0;"
38 "exit;"
39 ::: __clobber_all);
40}
41
42SEC("socket")
43__log_level(2)
44__msg("2: (79) r0 = *(u64 *)(r10 -8) ; use: fp0-8")
45__msg("6: (79) r0 = *(u64 *)(r10 -16) ; use: fp0-16")
46__naked void read_write_join(void)
47{
48 asm volatile (
49 "call %[bpf_get_prandom_u32];"
50 "if r0 > 42 goto 1f;"
51 "r0 = *(u64 *)(r10 - 8);"
52 "*(u64 *)(r10 - 32) = r0;"
53 "*(u64 *)(r10 - 40) = r0;"
54 "exit;"
55"1:"
56 "r0 = *(u64 *)(r10 - 16);"
57 "*(u64 *)(r10 - 32) = r0;"
58 "exit;"
59 :: __imm(bpf_get_prandom_u32)
60 : __clobber_all);
61}
62
63SEC("socket")
64__log_level(2)
65__msg("stack use/def subprog#0 must_write_not_same_slot (d0,cs0):")
66__msg("6: (7b) *(u64 *)(r2 +0) = r0{{$}}")
67__msg("Live regs before insn:")
68__naked void must_write_not_same_slot(void)
69{
70 asm volatile (
71 "call %[bpf_get_prandom_u32];"
72 "r1 = -8;"
73 "if r0 > 42 goto 1f;"
74 "r1 = -16;"
75"1:"
76 "r2 = r10;"
77 "r2 += r1;"
78 "*(u64 *)(r2 + 0) = r0;"
79 "exit;"
80 :: __imm(bpf_get_prandom_u32)
81 : __clobber_all);
82}
83
84SEC("socket")
85__log_level(2)
86__msg("0: (7a) *(u64 *)(r10 -8) = 0 ; def: fp0-8")
87__msg("5: (85) call bpf_map_lookup_elem#1 ; use: fp0-8h")
88__naked void must_write_not_same_type(void)
89{
90 asm volatile (
91 "*(u64*)(r10 - 8) = 0;"
92 "r2 = r10;"
93 "r2 += -8;"
94 "r1 = %[map] ll;"
95 "call %[bpf_map_lookup_elem];"
96 "if r0 != 0 goto 1f;"
97 "r0 = r10;"
98 "r0 += -16;"
99"1:"
100 "*(u64 *)(r0 + 0) = 42;"
101 "exit;"
102 :
103 : __imm(bpf_get_prandom_u32),
104 __imm(bpf_map_lookup_elem),
105 __imm_addr(map)
106 : __clobber_all);
107}
108
109SEC("socket")
110__log_level(2)
111/* Callee writes fp[0]-8: stack_use at call site has slots 0,1 live */
112__msg("stack use/def subprog#0 caller_stack_write (d0,cs0):")
113__msg("2: (85) call pc+1{{$}}")
114__msg("stack use/def subprog#1 write_first_param (d1,cs2):")
115__msg("4: (7a) *(u64 *)(r1 +0) = 7 ; def: fp0-8")
116__naked void caller_stack_write(void)
117{
118 asm volatile (
119 "r1 = r10;"
120 "r1 += -8;"
121 "call write_first_param;"
122 "exit;"
123 ::: __clobber_all);
124}
125
126static __used __naked void write_first_param(void)
127{
128 asm volatile (
129 "*(u64 *)(r1 + 0) = 7;"
130 "r0 = 0;"
131 "exit;"
132 ::: __clobber_all);
133}
134
135SEC("socket")
136__log_level(2)
137__msg("stack use/def subprog#0 caller_stack_read (d0,cs0):")
138__msg("2: (85) call pc+{{.*}} ; use: fp0-8{{$}}")
139__msg("5: (85) call pc+{{.*}} ; use: fp0-16{{$}}")
140__msg("stack use/def subprog#1 read_first_param (d1,cs2):")
141__msg("7: (79) r0 = *(u64 *)(r1 +0) ; use: fp0-8{{$}}")
142__msg("8: (95) exit")
143__msg("stack use/def subprog#1 read_first_param (d1,cs5):")
144__msg("7: (79) r0 = *(u64 *)(r1 +0) ; use: fp0-16{{$}}")
145__msg("8: (95) exit")
146__naked void caller_stack_read(void)
147{
148 asm volatile (
149 "r1 = r10;"
150 "r1 += -8;"
151 "call read_first_param;"
152 "r1 = r10;"
153 "r1 += -16;"
154 "call read_first_param;"
155 "exit;"
156 ::: __clobber_all);
157}
158
159static __used __naked void read_first_param(void)
160{
161 asm volatile (
162 "r0 = *(u64 *)(r1 + 0);"
163 "exit;"
164 ::: __clobber_all);
165}
166
167SEC("socket")
168__success
169__naked void arg_track_join_convergence(void)
170{
171 asm volatile (
172 "r1 = 1;"
173 "r2 = 2;"
174 "call arg_track_join_convergence_subprog;"
175 "r0 = 0;"
176 "exit;"
177 ::: __clobber_all);
178}
179
180static __used __naked void arg_track_join_convergence_subprog(void)
181{
182 asm volatile (
183 "if r1 == 0 goto 1f;"
184 "r0 = r1;"
185 "goto 2f;"
186"1:"
187 "r0 = r2;"
188"2:"
189 "r0 = 0;"
190 "exit;"
191 ::: __clobber_all);
192}
193
194SEC("socket")
195__flag(BPF_F_TEST_STATE_FREQ)
196__log_level(2)
197/* fp0-8 consumed at insn 9, dead by insn 11. stack_def at insn 4 kills slots 0,1. */
198__msg("4: (7b) *(u64 *)(r10 -8) = r0 ; def: fp0-8")
199/* stack_use at call site: callee reads fp0-8, slots 0,1 live */
200__msg("7: (85) call pc+{{.*}} ; use: fp0-8")
201/* read_first_param2: no caller stack live inside callee after first read */
202__msg("9: (79) r0 = *(u64 *)(r1 +0) ; use: fp0-8")
203__msg("10: (b7) r0 = 0{{$}}")
204__msg("11: (05) goto pc+0{{$}}")
205__msg("12: (95) exit")
206/*
207 * Checkpoint at goto +0 fires because fp0-8 is dead → state pruning.
208 */
209__msg("12: safe")
210__naked void caller_stack_pruning(void)
211{
212 asm volatile (
213 "call %[bpf_get_prandom_u32];"
214 "if r0 == 42 goto 1f;"
215 "r0 = %[map] ll;"
216"1:"
217 "*(u64 *)(r10 - 8) = r0;"
218 "r1 = r10;"
219 "r1 += -8;"
220 /*
221 * fp[0]-8 is either pointer to map or a scalar,
222 * preventing state pruning at checkpoint created for call.
223 */
224 "call read_first_param2;"
225 "exit;"
226 :
227 : __imm(bpf_get_prandom_u32),
228 __imm_addr(map)
229 : __clobber_all);
230}
231
232static __used __naked void read_first_param2(void)
233{
234 asm volatile (
235 "r0 = *(u64 *)(r1 + 0);"
236 "r0 = 0;"
237 /*
238 * Checkpoint at goto +0 should fire,
239 * as caller stack fp[0]-8 is not alive at this point.
240 */
241 "goto +0;"
242 "exit;"
243 ::: __clobber_all);
244}
245
246SEC("socket")
247__flag(BPF_F_TEST_STATE_FREQ)
248__failure
249__msg("R1 type=scalar expected=map_ptr")
250__naked void caller_stack_pruning_callback(void)
251{
252 asm volatile (
253 "r0 = %[map] ll;"
254 "*(u64 *)(r10 - 8) = r0;"
255 "r1 = 2;"
256 "r2 = loop_cb ll;"
257 "r3 = r10;"
258 "r3 += -8;"
259 "r4 = 0;"
260 /*
261 * fp[0]-8 is either pointer to map or a scalar,
262 * preventing state pruning at checkpoint created for call.
263 */
264 "call %[bpf_loop];"
265 "r0 = 42;"
266 "exit;"
267 :
268 : __imm(bpf_get_prandom_u32),
269 __imm(bpf_loop),
270 __imm_addr(map)
271 : __clobber_all);
272}
273
274static __used __naked void loop_cb(void)
275{
276 asm volatile (
277 /*
278 * Checkpoint at function entry should not fire, as caller
279 * stack fp[0]-8 is alive at this point.
280 */
281 "r6 = r2;"
282 "r1 = *(u64 *)(r6 + 0);"
283 "*(u64*)(r10 - 8) = 7;"
284 "r2 = r10;"
285 "r2 += -8;"
286 "call %[bpf_map_lookup_elem];"
287 /*
288 * This should stop verifier on a second loop iteration,
289 * but only if verifier correctly maintains that fp[0]-8
290 * is still alive.
291 */
292 "*(u64 *)(r6 + 0) = 0;"
293 "r0 = 0;"
294 "exit;"
295 :
296 : __imm(bpf_map_lookup_elem),
297 __imm(bpf_get_prandom_u32)
298 : __clobber_all);
299}
300
301/*
302 * Because of a bug in verifier.c:compute_postorder()
303 * the program below overflowed traversal queue in that function.
304 */
305SEC("socket")
306__naked void syzbot_postorder_bug1(void)
307{
308 asm volatile (
309 "r0 = 0;"
310 "if r0 != 0 goto -1;"
311 "exit;"
312 ::: __clobber_all);
313}
314
315struct {
316 __uint(type, BPF_MAP_TYPE_PROG_ARRAY);
317 __uint(max_entries, 1);
318 __type(key, __u32);
319 __type(value, __u32);
320} map_array SEC(".maps");
321
322SEC("socket")
323__failure __msg("invalid read from stack R2 off=-1024 size=8")
324__flag(BPF_F_TEST_STATE_FREQ)
325__naked unsigned long caller_stack_write_tail_call(void)
326{
327 asm volatile (
328 "r6 = r1;"
329 "*(u64 *)(r10 - 8) = -8;"
330 "call %[bpf_get_prandom_u32];"
331 "if r0 != 42 goto 1f;"
332 "goto 2f;"
333 "1:"
334 "*(u64 *)(r10 - 8) = -1024;"
335 "2:"
336 "r1 = r6;"
337 "r2 = r10;"
338 "r2 += -8;"
339 "call write_tail_call;"
340 "r1 = *(u64 *)(r10 - 8);"
341 "r2 = r10;"
342 "r2 += r1;"
343 "r0 = *(u64 *)(r2 + 0);"
344 "exit;"
345 :: __imm(bpf_get_prandom_u32)
346 : __clobber_all);
347}
348
349static __used __naked unsigned long write_tail_call(void)
350{
351 asm volatile (
352 "r6 = r2;"
353 "r2 = %[map_array] ll;"
354 "r3 = 0;"
355 "call %[bpf_tail_call];"
356 "*(u64 *)(r6 + 0) = -16;"
357 "r0 = 0;"
358 "exit;"
359 :
360 : __imm(bpf_tail_call),
361 __imm_addr(map_array)
362 : __clobber_all);
363}
364
365/* Test precise subprog stack access analysis.
366 * Caller passes fp-32 (SPI 3) to callee that only accesses arg+0 and arg+8
367 * (SPIs 3 and 2). Slots 0 and 1 should NOT be live at the call site.
368 *
369 * Insn layout:
370 * 0: *(u64*)(r10 - 8) = 0 write SPI 0
371 * 1: *(u64*)(r10 - 16) = 0 write SPI 1
372 * 2: *(u64*)(r10 - 24) = 0 write SPI 2
373 * 3: *(u64*)(r10 - 32) = 0 write SPI 3
374 * 4: r1 = r10
375 * 5: r1 += -32
376 * 6: call precise_read_two passes fp-32 (SPI 3)
377 * 7: r0 = 0
378 * 8: exit
379 *
380 * At insn 6 only SPIs 2,3 should be live (slots 4-7, 0xf0).
381 * SPIs 0,1 are written but never read → dead.
382 */
383SEC("socket")
384__log_level(2)
385__msg("6: (85) call pc+{{.*}} ; use: fp0-24 fp0-32{{$}}")
386__naked void subprog_precise_stack_access(void)
387{
388 asm volatile (
389 "*(u64 *)(r10 - 8) = 0;"
390 "*(u64 *)(r10 - 16) = 0;"
391 "*(u64 *)(r10 - 24) = 0;"
392 "*(u64 *)(r10 - 32) = 0;"
393 "r1 = r10;"
394 "r1 += -32;"
395 "call precise_read_two;"
396 "r0 = 0;"
397 "exit;"
398 ::: __clobber_all);
399}
400
401/* Callee reads only at arg+0 (SPI 3) and arg+8 (SPI 2) */
402static __used __naked void precise_read_two(void)
403{
404 asm volatile (
405 "r0 = *(u64 *)(r1 + 0);"
406 "r2 = *(u64 *)(r1 + 8);"
407 "r0 = 0;"
408 "exit;"
409 ::: __clobber_all);
410}
411
412/* Test that multi-level subprog calls (callee passes arg-derived ptr
413 * to another BPF subprog) are analyzed precisely.
414 *
415 * Caller passes fp-32 (SPI 3). The callee forwards it to inner_callee.
416 * inner_callee only reads at offset 0 from the pointer.
417 * The analysis recurses into forward_to_inner -> inner_callee and
418 * determines only SPI 3 is accessed (slots 6-7, 0xc0), not all of SPIs 0-3.
419 *
420 * Insn layout:
421 * 0: *(u64*)(r10 - 8) = 0 write SPI 0
422 * 1: *(u64*)(r10 - 16) = 0 write SPI 1
423 * 2: *(u64*)(r10 - 24) = 0 write SPI 2
424 * 3: *(u64*)(r10 - 32) = 0 write SPI 3
425 * 4: r1 = r10
426 * 5: r1 += -32
427 * 6: call forward_to_inner passes fp-32 (SPI 3)
428 * 7: r0 = 0
429 * 8: exit
430 */
431SEC("socket")
432__log_level(2)
433__msg("6: (85) call pc+{{.*}} ; use: fp0-32{{$}}")
434__naked void subprog_multilevel_conservative(void)
435{
436 asm volatile (
437 "*(u64 *)(r10 - 8) = 0;"
438 "*(u64 *)(r10 - 16) = 0;"
439 "*(u64 *)(r10 - 24) = 0;"
440 "*(u64 *)(r10 - 32) = 0;"
441 "r1 = r10;"
442 "r1 += -32;"
443 "call forward_to_inner;"
444 "r0 = 0;"
445 "exit;"
446 ::: __clobber_all);
447}
448
449/* Forwards arg to another subprog */
450static __used __naked void forward_to_inner(void)
451{
452 asm volatile (
453 "call inner_callee;"
454 "r0 = 0;"
455 "exit;"
456 ::: __clobber_all);
457}
458
459static __used __naked void inner_callee(void)
460{
461 asm volatile (
462 "r0 = *(u64 *)(r1 + 0);"
463 "r0 = 0;"
464 "exit;"
465 ::: __clobber_all);
466}
467
468/* Test multi-frame precision loss: callee consumes caller stack early,
469 * but static liveness keeps it live at pruning points inside callee.
470 *
471 * Caller stores map_ptr or scalar(42) at fp-8, then calls
472 * consume_and_call_inner. The callee reads fp0-8 at entry (consuming
473 * the slot), then calls do_nothing2. After do_nothing2 returns (a
474 * pruning point), fp-8 should be dead -- the read already happened.
475 * But because the call instruction's stack_use includes SPI 0, the
476 * static live_stack_before at insn 7 is 0x1, keeping fp-8 live inside
477 * the callee and preventing state pruning between the two paths.
478 *
479 * Insn layout:
480 * 0: call bpf_get_prandom_u32
481 * 1: if r0 == 42 goto pc+2 -> insn 4
482 * 2: r0 = map ll (ldimm64 part1)
483 * 3: (ldimm64 part2)
484 * 4: *(u64)(r10 - 8) = r0 fp-8 = map_ptr OR scalar(42)
485 * 5: r1 = r10
486 * 6: r1 += -8
487 * 7: call consume_and_call_inner
488 * 8: r0 = 0
489 * 9: exit
490 *
491 * At insn 7, live_stack_before = 0x3 (slots 0-1 live due to stack_use).
492 * At insn 8, live_stack_before = 0x0 (SPI 0 dead, caller doesn't need it).
493 */
494SEC("socket")
495__flag(BPF_F_TEST_STATE_FREQ)
496__log_level(2)
497__success
498__msg(" 7: (85) call pc+{{.*}} ; use: fp0-8")
499__msg(" 8: {{.*}} (b7)")
500__naked void callee_consumed_caller_stack(void)
501{
502 asm volatile (
503 "call %[bpf_get_prandom_u32];"
504 "if r0 == 42 goto 1f;"
505 "r0 = %[map] ll;"
506"1:"
507 "*(u64 *)(r10 - 8) = r0;"
508 "r1 = r10;"
509 "r1 += -8;"
510 "call consume_and_call_inner;"
511 "r0 = 0;"
512 "exit;"
513 :
514 : __imm(bpf_get_prandom_u32),
515 __imm_addr(map)
516 : __clobber_all);
517}
518
519static __used __naked void consume_and_call_inner(void)
520{
521 asm volatile (
522 "r0 = *(u64 *)(r1 + 0);" /* read fp[0]-8 into caller-saved r0 */
523 "call do_nothing2;" /* inner call clobbers r0 */
524 "r0 = 0;"
525 "goto +0;" /* checkpoint */
526 "r0 = 0;"
527 "goto +0;" /* checkpoint */
528 "r0 = 0;"
529 "goto +0;" /* checkpoint */
530 "r0 = 0;"
531 "goto +0;" /* checkpoint */
532 "exit;"
533 ::: __clobber_all);
534}
535
536static __used __naked void do_nothing2(void)
537{
538 asm volatile (
539 "r0 = 0;"
540 "r0 = 0;"
541 "r0 = 0;"
542 "r0 = 0;"
543 "r0 = 0;"
544 "r0 = 0;"
545 "r0 = 0;"
546 "exit;"
547 ::: __clobber_all);
548}
549
550/*
551 * Reproducer for unsound pruning when clean_verifier_state() promotes
552 * live STACK_ZERO bytes to STACK_MISC.
553 *
554 * Program shape:
555 * - Build key at fp-4:
556 * - path A keeps key byte as STACK_ZERO;
557 * - path B writes unknown byte making it STACK_MISC.
558 * - Branches merge at a prune point before map_lookup.
559 * - map_lookup on ARRAY map is value-sensitive to constant zero key:
560 * - path A: const key 0 => PTR_TO_MAP_VALUE (non-NULL);
561 * - path B: non-const key => PTR_TO_MAP_VALUE_OR_NULL.
562 * - Dereference lookup result without null check.
563 *
564 * Note this behavior won't trigger at fp-8, since the verifier will
565 * track 32-bit scalar spill differently as spilled_ptr.
566 *
567 * Correct verifier behavior: reject (path B unsafe).
568 * With blanket STACK_ZERO->STACK_MISC promotion on live slots, cached path A
569 * state can be generalized and incorrectly prune path B, making program load.
570 */
571SEC("socket")
572__flag(BPF_F_TEST_STATE_FREQ)
573__failure __msg("R0 invalid mem access 'map_value_or_null'")
574__naked void stack_zero_to_misc_unsound_array_lookup(void)
575{
576 asm volatile (
577 /* key at fp-4: all bytes STACK_ZERO */
578 "*(u32 *)(r10 - 4) = 0;"
579 "call %[bpf_get_prandom_u32];"
580 /* fall-through (path A) explored first */
581 "if r0 != 0 goto l_nonconst%=;"
582 /* path A: keep key constant zero */
583 "goto l_lookup%=;"
584"l_nonconst%=:"
585 /* path B: key byte turns to STACK_MISC, key no longer const */
586 "*(u8 *)(r10 - 4) = r0;"
587"l_lookup%=:"
588 /* value-sensitive lookup */
589 "r2 = r10;"
590 "r2 += -4;"
591 "r1 = %[array_map_8b] ll;"
592 "call %[bpf_map_lookup_elem];"
593 /* unsafe when lookup result is map_value_or_null */
594 "r0 = *(u64 *)(r0 + 0);"
595 "exit;"
596 :
597 : __imm(bpf_get_prandom_u32),
598 __imm(bpf_map_lookup_elem),
599 __imm_addr(array_map_8b)
600 : __clobber_all);
601}
602
603/*
604 * Subprog variant of stack_zero_to_misc_unsound_array_lookup.
605 *
606 * Check unsound pruning when a callee modifies the caller's
607 * stack through a pointer argument.
608 *
609 * Program shape:
610 * main:
611 * *(u32)(fp - 4) = 0 key = 0 (all bytes STACK_ZERO)
612 * r1 = fp - 4
613 * call maybe_clobber_key may overwrite key[0] with scalar
614 * <-- prune point: two states meet here -->
615 * r2 = fp - 4
616 * r1 = array_map_8b
617 * call bpf_map_lookup_elem value-sensitive on const-zero key
618 * r0 = *(u64)(r0 + 0) deref without null check
619 * exit
620 *
621 * maybe_clobber_key(r1):
622 * r6 = r1 save &key
623 * call bpf_get_prandom_u32
624 * if r0 == 0 goto skip path A: key stays STACK_ZERO
625 * *(u8)(r6 + 0) = r0 path B: key[0] becomes STACK_MISC
626 * skip:
627 * r0 = 0
628 * exit
629 *
630 * Path A: const-zero key => array lookup => PTR_TO_MAP_VALUE => deref OK.
631 * Path B: non-const key => array lookup => PTR_TO_MAP_VALUE_OR_NULL => UNSAFE.
632 *
633 * If the cleaner collapses STACK_ZERO -> STACK_MISC for the live key
634 * slot, path A's cached state matches path B, pruning the unsafe path.
635 *
636 * Correct verifier behaviour: reject.
637 */
638SEC("socket")
639__flag(BPF_F_TEST_STATE_FREQ)
640__failure __msg("R0 invalid mem access 'map_value_or_null'")
641__naked void subprog_stack_zero_to_misc_unsound(void)
642{
643 asm volatile (
644 /* key at fp-4: all bytes STACK_ZERO */
645 "*(u32 *)(r10 - 4) = 0;"
646 /* subprog may clobber key[0] with a scalar byte */
647 "r1 = r10;"
648 "r1 += -4;"
649 "call maybe_clobber_key;"
650 /* value-sensitive array lookup */
651 "r2 = r10;"
652 "r2 += -4;"
653 "r1 = %[array_map_8b] ll;"
654 "call %[bpf_map_lookup_elem];"
655 /* unsafe when result is map_value_or_null (path B) */
656 "r0 = *(u64 *)(r0 + 0);"
657 "exit;"
658 :
659 : __imm(bpf_map_lookup_elem),
660 __imm_addr(array_map_8b)
661 : __clobber_all);
662}
663
664static __used __naked void maybe_clobber_key(void)
665{
666 asm volatile (
667 "r6 = r1;"
668 "call %[bpf_get_prandom_u32];"
669 /* path A (r0==0): key stays STACK_ZERO, explored first */
670 "if r0 == 0 goto 1f;"
671 /* path B (r0!=0): overwrite key[0] with scalar */
672 "*(u8 *)(r6 + 0) = r0;"
673 "1:"
674 "r0 = 0;"
675 "exit;"
676 :: __imm(bpf_get_prandom_u32)
677 : __clobber_all);
678}
679
680/*
681 * Demonstrate that subprog arg spill/reload breaks arg tracking,
682 * inflating caller stack liveness and preventing state pruning.
683 *
684 * modifier2(fp-24) has two paths: one writes a scalar to *(r1+8)
685 * = caller fp-16, the other leaves it as zero. After modifier2
686 * returns, fp-16 is never read again — it is dead.
687 *
688 * spill_reload_reader2(fp-24) only reads caller fp-8 via
689 * *(r1+16), but it spills r1 across a helper call. This
690 * breaks compute_subprog_arg_access(): the reload from callee
691 * stack cannot be connected back to arg1, so arg1 access goes
692 * "all (conservative)". At the call site (r1 = fp-24, slot 5)
693 * apply_callee_stack_access() marks slots 0..5 as stack_use —
694 * pulling fp-16 (slots 2-3) into live_stack_before even though
695 * the reader never touches it.
696 *
697 * Result: at modifier2's return point two states with different
698 * fp-16 values cannot be pruned.
699 *
700 * With correct (or old dynamic) liveness fp-16 is dead at that
701 * point and the states prune → "6: safe" appears in the log.
702 */
703SEC("socket")
704__flag(BPF_F_TEST_STATE_FREQ)
705__log_level(2)
706__success
707__msg("6: safe")
708__naked void spill_reload_inflates_stack_liveness(void)
709{
710 asm volatile (
711 /* struct at fp-24: { ctx; ptr; tail; } */
712 "*(u64 *)(r10 - 24) = r1;" /* fp-24 = ctx */
713 "*(u64 *)(r10 - 16) = r1;" /* fp-16 = ctx (STACK_SPILL ptr) */
714 "*(u64 *)(r10 - 8) = 0;" /* fp-8 = tail */
715 /* modifier2 writes different values to fp-16 on two paths */
716 "r1 = r10;"
717 "r1 += -24;"
718 "call modifier2;"
719 /* insn 6: prune point — two states with different fp-16
720 * path A: fp-16 = STACK_MISC (scalar overwrote pointer)
721 * path B: fp-16 = STACK_SPILL (original ctx pointer)
722 * STACK_MISC does NOT subsume STACK_SPILL(ptr),
723 * so pruning fails unless fp-16 is cleaned (dead).
724 */
725 "r1 = r10;"
726 "r1 += -24;"
727 "call spill_reload_reader2;" /* reads fp-8 via *(r1+16) */
728 "r0 = 0;"
729 "exit;"
730 ::: __clobber_all);
731}
732
733/* Two paths: one writes a scalar to *(r1+8) = caller fp-16,
734 * the other leaves it unchanged. Both return 0 via separate
735 * exits to prevent pruning inside the subprog at the merge.
736 */
737static __used __naked void modifier2(void)
738{
739 asm volatile (
740 "r6 = r1;"
741 "call %[bpf_get_prandom_u32];"
742 "if r0 == 0 goto 1f;"
743 "*(u64 *)(r6 + 8) = r0;" /* fp-16 = random */
744 "r0 = 0;"
745 "exit;" /* path A exit */
746 "1:"
747 "r0 = 0;"
748 "exit;" /* path B exit */
749 :: __imm(bpf_get_prandom_u32)
750 : __clobber_all);
751}
752
753/* Receives r1 = caller fp-24. Only reads *(r1+16) = fp-8.
754 * Spills r1 across a helper call → arg tracking goes conservative →
755 * slots 0..5 all appear used instead of just slot 1 (fp-8).
756 */
757static __used __naked void spill_reload_reader2(void)
758{
759 asm volatile (
760 "*(u64 *)(r10 - 8) = r1;" /* spill arg1 */
761 "call %[bpf_get_prandom_u32];" /* clobbers r1-r5 */
762 "r1 = *(u64 *)(r10 - 8);" /* reload arg1 */
763 "r0 = *(u64 *)(r1 + 16);" /* read caller fp-8 */
764 "r0 = 0;"
765 "exit;"
766 :: __imm(bpf_get_prandom_u32)
767 : __clobber_all);
768}
769
770/* BTF FUNC records are not generated for kfuncs referenced
771 * from inline assembly. These records are necessary for
772 * libbpf to link the program. The function below is a hack
773 * to ensure that BTF FUNC records are generated.
774 */
775void __kfunc_btf_root(void)
776{
777 bpf_iter_num_new(0, 0, 0);
778 bpf_iter_num_next(0);
779 bpf_iter_num_destroy(0);
780}
781
782/* Test that open-coded iterator kfunc arguments get precise stack
783 * liveness tracking. struct bpf_iter_num is 8 bytes (1 SPI).
784 *
785 * Insn layout:
786 * 0: *(u64*)(r10 - 8) = 0 write SPI 0 (dead)
787 * 1: *(u64*)(r10 - 16) = 0 write SPI 1 (dead)
788 * 2: r1 = r10
789 * 3: r1 += -24 iter state at fp-24 (SPI 2)
790 * 4: r2 = 0
791 * 5: r3 = 10
792 * 6: call bpf_iter_num_new defines SPI 2 (KF_ITER_NEW) → 0x0
793 * 7-8: r1 = fp-24
794 * 9: call bpf_iter_num_next uses SPI 2 → 0x30
795 * 10: if r0 == 0 goto 2f
796 * 11: goto 1b
797 * 12-13: r1 = fp-24
798 * 14: call bpf_iter_num_destroy uses SPI 2 → 0x30
799 * 15: r0 = 0
800 * 16: exit
801 *
802 * At insn 6, SPI 2 is defined (KF_ITER_NEW initializes, doesn't read),
803 * so it kills liveness from successors. live_stack_before = 0x0.
804 * At insns 9 and 14, SPI 2 is used (iter_next/destroy read the state),
805 * so live_stack_before = 0x30.
806 */
807SEC("socket")
808__success __log_level(2)
809__msg(" 6: (85) call bpf_iter_num_new{{.*}} ; def: fp0-24{{$}}")
810__msg(" 9: (85) call bpf_iter_num_next{{.*}} ; use: fp0-24{{$}}")
811__msg("14: (85) call bpf_iter_num_destroy{{.*}} ; use: fp0-24{{$}}")
812__naked void kfunc_iter_stack_liveness(void)
813{
814 asm volatile (
815 "*(u64 *)(r10 - 8) = 0;" /* SPI 0 - dead */
816 "*(u64 *)(r10 - 16) = 0;" /* SPI 1 - dead */
817 "r1 = r10;"
818 "r1 += -24;"
819 "r2 = 0;"
820 "r3 = 10;"
821 "call %[bpf_iter_num_new];"
822"1:"
823 "r1 = r10;"
824 "r1 += -24;"
825 "call %[bpf_iter_num_next];"
826 "if r0 == 0 goto 2f;"
827 "goto 1b;"
828"2:"
829 "r1 = r10;"
830 "r1 += -24;"
831 "call %[bpf_iter_num_destroy];"
832 "r0 = 0;"
833 "exit;"
834 :: __imm(bpf_iter_num_new),
835 __imm(bpf_iter_num_next),
836 __imm(bpf_iter_num_destroy)
837 : __clobber_all);
838}
839
840/*
841 * Test for soundness bug in static stack liveness analysis.
842 *
843 * The static pre-pass tracks FP-derived register offsets to determine
844 * which stack slots are accessed. When a PTR_TO_STACK is spilled to
845 * the stack and later reloaded, the reload (BPF_LDX) kills FP-derived
846 * tracking, making subsequent accesses through the reloaded pointer
847 * invisible to the static analysis.
848 *
849 * This causes the analysis to incorrectly mark SPI 0 as dead at the
850 * merge point. clean_verifier_state() zeros it in the cached state,
851 * and stacksafe() accepts the new state against STACK_INVALID,
852 * enabling incorrect pruning.
853 *
854 * Path A (verified first): stores PTR_TO_MAP_VALUE in SPI 0
855 * Path B (verified second): stores scalar 42 in SPI 0
856 * After merge: reads SPI 0 through spilled/reloaded PTR_TO_STACK
857 * and dereferences the result as a pointer.
858 *
859 * Correct behavior: reject (path B dereferences a scalar)
860 * Bug behavior: accept (path B is incorrectly pruned)
861 */
862SEC("socket")
863__flag(BPF_F_TEST_STATE_FREQ)
864__failure __msg("R0 invalid mem access 'scalar'")
865__naked void spill_ptr_liveness_type_confusion(void)
866{
867 asm volatile (
868 /* Map lookup to get PTR_TO_MAP_VALUE */
869 "r1 = %[map] ll;"
870 "*(u32 *)(r10 - 32) = 0;"
871 "r2 = r10;"
872 "r2 += -32;"
873 "call %[bpf_map_lookup_elem];"
874 "if r0 == 0 goto l_exit%=;"
875 /* r6 = PTR_TO_MAP_VALUE (callee-saved) */
876 "r6 = r0;"
877 /* Branch: fall-through (path A) verified first */
878 "call %[bpf_get_prandom_u32];"
879 "if r0 != 0 goto l_scalar%=;"
880 /* Path A: store map value ptr at SPI 0 */
881 "*(u64 *)(r10 - 8) = r6;"
882 "goto l_merge%=;"
883"l_scalar%=:"
884 /* Path B: store scalar at SPI 0 */
885 "r1 = 42;"
886 "*(u64 *)(r10 - 8) = r1;"
887"l_merge%=:"
888 /*
889 * Spill PTR_TO_STACK{off=-8} to SPI 1, then reload.
890 * Reload kills FP-derived tracking, hiding the
891 * subsequent SPI 0 access from the static analysis.
892 */
893 "r1 = r10;"
894 "r1 += -8;"
895 "*(u64 *)(r10 - 16) = r1;"
896 "goto +0;" /* checkpoint */
897 "goto +0;" /* checkpoint */
898 "goto +0;" /* checkpoint */
899 "r1 = *(u64 *)(r10 - 16);"
900 /* Read SPI 0 through reloaded pointer */
901 "r0 = *(u64 *)(r1 + 0);"
902 /* Dereference: safe for map value (path A),
903 * unsafe for scalar (path B).
904 */
905 "r0 = *(u64 *)(r0 + 0);"
906 "exit;"
907"l_exit%=:"
908 "r0 = 0;"
909 "exit;"
910 :
911 : __imm(bpf_map_lookup_elem),
912 __imm(bpf_get_prandom_u32),
913 __imm_addr(map)
914 : __clobber_all);
915}
916
917/* === Tests for 4-byte stack slot liveness granularity === */
918
919/* Test that a 4-byte aligned write is stack_def and kills liveness.
920 *
921 * 0: *(u64 *)(r10 - 8) = 0 def slots 0,1 (full SPI 0)
922 * 1: *(u32 *)(r10 - 8) = 0 def slot 1 (4-byte write kills slot 1)
923 * 2: r0 = *(u64 *)(r10 - 8) use slots 0,1
924 * 3: r0 = 0
925 * 4: exit
926 *
927 * At insn 1, the 4-byte write defines slot 1. Slot 0 still flows
928 * backward from insn 2's read: live_stack_before = 0x1.
929 */
930SEC("socket")
931__log_level(2)
932__msg("1: (62) *(u32 *)(r10 -8) = 0 ; def: fp0-8h")
933__naked void four_byte_write_kills_slot(void)
934{
935 asm volatile (
936 "*(u64 *)(r10 - 8) = 0;"
937 "*(u32 *)(r10 - 8) = 0;"
938 "r0 = *(u64 *)(r10 - 8);"
939 "r0 = 0;"
940 "exit;"
941 ::: __clobber_all);
942}
943
944/* Test that a write to the upper half of an SPI is dead when only
945 * the lower half is read. This was impossible at SPI granularity
946 * where any read of the SPI kept the entire SPI live.
947 *
948 * 0: *(u32 *)(r10 - 8) = 0 def slot 1 (DEAD: never read)
949 * 1: *(u32 *)(r10 - 4) = 0 def slot 0
950 * 2: r0 = *(u32 *)(r10 - 4) use slot 0 only
951 * 3: r0 = 0
952 * 4: exit
953 *
954 * At insn 0, nothing is live (0x0). Previously at SPI granularity,
955 * the read at insn 2 would mark the full SPI 0 as live and the
956 * 4-byte writes wouldn't count as def, so insn 0 would have had
957 * SPI 0 live (0x1).
958 */
959SEC("socket")
960__log_level(2)
961__msg("0: (62) *(u32 *)(r10 -8) = 0 ; def: fp0-8h")
962__msg("2: (61) r0 = *(u32 *)(r10 -4) ; use: fp0-4h")
963__naked void dead_half_spi_write(void)
964{
965 asm volatile (
966 "*(u32 *)(r10 - 8) = 0;"
967 "*(u32 *)(r10 - 4) = 0;"
968 "r0 = *(u32 *)(r10 - 4);"
969 "r0 = 0;"
970 "exit;"
971 ::: __clobber_all);
972}
973
974/* Test that a 4-byte read from the upper half of SPI 0 makes only
975 * slot 1 live (0x2), not the full SPI (0x3).
976 *
977 * 0: *(u64 *)(r10 - 8) = 0 def slots 0,1
978 * 1: r0 = *(u32 *)(r10 - 8) use slot 1 only (upper half)
979 * 2: r0 = 0
980 * 3: exit
981 *
982 * At insn 1, live_stack_before = 0x2 (slot 1 only).
983 */
984SEC("socket")
985__log_level(2)
986__msg("1: (61) r0 = *(u32 *)(r10 -8) ; use: fp0-8h")
987__naked void four_byte_read_upper_half(void)
988{
989 asm volatile (
990 "*(u64 *)(r10 - 8) = 0;"
991 "r0 = *(u32 *)(r10 - 8);"
992 "r0 = 0;"
993 "exit;"
994 ::: __clobber_all);
995}
996
997/* Test that a 2-byte write does NOT count as stack_def.
998 * Sub-4-byte writes don't fully cover a 4-byte slot,
999 * so liveness passes through.
1000 *
1001 * 0: *(u64 *)(r10 - 8) = 0 def slots 0,1
1002 * 1: *(u16 *)(r10 - 4) = 0 NOT stack_def (2 < 4 bytes)
1003 * 2: r0 = *(u32 *)(r10 - 4) use slot 0
1004 * 3: r0 = 0
1005 * 4: exit
1006 *
1007 * At insn 1, slot 0 still live (0x1) because 2-byte write
1008 * didn't kill it.
1009 */
1010SEC("socket")
1011__log_level(2)
1012__msg("0: (7a) *(u64 *)(r10 -8) = 0 ; def: fp0-8")
1013__msg("1: (6a) *(u16 *)(r10 -4) = 0{{$}}")
1014__msg("2: (61) r0 = *(u32 *)(r10 -4) ; use: fp0-4h")
1015__naked void two_byte_write_no_kill(void)
1016{
1017 asm volatile (
1018 "*(u64 *)(r10 - 8) = 0;"
1019 "*(u16 *)(r10 - 4) = 0;"
1020 "r0 = *(u32 *)(r10 - 4);"
1021 "r0 = 0;"
1022 "exit;"
1023 ::: __clobber_all);
1024}
1025
1026/* Test that a 1-byte write does NOT count as stack_def.
1027 *
1028 * 0: *(u64 *)(r10 - 8) = 0 def slots 0,1
1029 * 1: *(u8 *)(r10 - 4) = 0 NOT stack_def (1 < 4 bytes)
1030 * 2: r0 = *(u32 *)(r10 - 4) use slot 0
1031 * 3: r0 = 0
1032 * 4: exit
1033 *
1034 * At insn 1, slot 0 still live (0x1).
1035 */
1036SEC("socket")
1037__log_level(2)
1038__msg("0: (7a) *(u64 *)(r10 -8) = 0 ; def: fp0-8")
1039__msg("1: (72) *(u8 *)(r10 -4) = 0")
1040__msg("2: (61) r0 = *(u32 *)(r10 -4) ; use: fp0-4h")
1041__naked void one_byte_write_no_kill(void)
1042{
1043 asm volatile (
1044 "*(u64 *)(r10 - 8) = 0;"
1045 "*(u8 *)(r10 - 4) = 0;"
1046 "r0 = *(u32 *)(r10 - 4);"
1047 "r0 = 0;"
1048 "exit;"
1049 ::: __clobber_all);
1050}
1051
1052/* Test stack access beyond fp-256 exercising the second bitmask word.
1053 * fp-264 is SPI 32, slots 64-65, which are bits 0-1 of live_stack[1].
1054 *
1055 * 0: *(u64 *)(r10 - 264) = 0 def slots 64,65
1056 * 1: r0 = *(u64 *)(r10 - 264) use slots 64,65
1057 * 2: r0 = 0
1058 * 3: exit
1059 *
1060 * At insn 1, live_stack high word has bits 0,1 set: 0x3:0x0.
1061 */
1062SEC("socket")
1063__log_level(2)
1064__msg("1: (79) r0 = *(u64 *)(r10 -264) ; use: fp0-264")
1065__naked void high_stack_second_bitmask_word(void)
1066{
1067 asm volatile (
1068 "*(u64 *)(r10 - 264) = 0;"
1069 "r0 = *(u64 *)(r10 - 264);"
1070 "r0 = 0;"
1071 "exit;"
1072 ::: __clobber_all);
1073}
1074
1075/* Test that two separate 4-byte writes to each half of an SPI
1076 * together kill liveness for the full SPI.
1077 *
1078 * 0: *(u32 *)(r10 - 8) = 0 def slot 1 (upper half)
1079 * 1: *(u32 *)(r10 - 4) = 0 def slot 0 (lower half)
1080 * 2: r0 = *(u64 *)(r10 - 8) use slots 0,1
1081 * 3: r0 = 0
1082 * 4: exit
1083 *
1084 * At insn 0: live_stack_before = 0x0 (both slots killed by insns 0,1).
1085 * At insn 1: live_stack_before = 0x2 (slot 1 still live, slot 0 killed here).
1086 */
1087SEC("socket")
1088__log_level(2)
1089__msg("0: (62) *(u32 *)(r10 -8) = 0 ; def: fp0-8h")
1090__msg("1: (62) *(u32 *)(r10 -4) = 0 ; def: fp0-4h")
1091__naked void two_four_byte_writes_kill_full_spi(void)
1092{
1093 asm volatile (
1094 "*(u32 *)(r10 - 8) = 0;"
1095 "*(u32 *)(r10 - 4) = 0;"
1096 "r0 = *(u64 *)(r10 - 8);"
1097 "r0 = 0;"
1098 "exit;"
1099 ::: __clobber_all);
1100}
1101
1102/* Test that 4-byte writes on both branches kill a slot at the
1103 * join point. Previously at SPI granularity, a 4-byte write was
1104 * not stack_def, so liveness would flow backward through the
1105 * branch that only had a 4-byte write.
1106 *
1107 * 0: call bpf_get_prandom_u32
1108 * 1: if r0 != 0 goto 1f
1109 * 2: *(u64 *)(r10 - 8) = 0 path A: def slots 0,1
1110 * 3: goto 2f
1111 * 1:4: *(u32 *)(r10 - 4) = 0 path B: def slot 0
1112 * 2:5: r0 = *(u32 *)(r10 - 4) use slot 0
1113 * 6: r0 = 0
1114 * 7: exit
1115 *
1116 * Both paths define slot 0 before the read. At insn 1 (branch),
1117 * live_stack_before = 0x0 because slot 0 is killed on both paths.
1118 */
1119SEC("socket")
1120__log_level(2)
1121__msg("1: (55) if r0 != 0x0 goto pc+2")
1122__msg("2: (7a) *(u64 *)(r10 -8) = 0 ; def: fp0-8")
1123__msg("3: (05) goto pc+1")
1124__msg("4: (62) *(u32 *)(r10 -4) = 0 ; def: fp0-4h")
1125__msg("5: (61) r0 = *(u32 *)(r10 -4) ; use: fp0-4h")
1126__naked void both_branches_kill_slot(void)
1127{
1128 asm volatile (
1129 "call %[bpf_get_prandom_u32];"
1130 "if r0 != 0 goto 1f;"
1131 "*(u64 *)(r10 - 8) = 0;"
1132 "goto 2f;"
1133"1:"
1134 "*(u32 *)(r10 - 4) = 0;"
1135"2:"
1136 "r0 = *(u32 *)(r10 - 4);"
1137 "r0 = 0;"
1138 "exit;"
1139 :: __imm(bpf_get_prandom_u32)
1140 : __clobber_all);
1141}
1142
1143/* Soundness: cleaning the dead upper half of an SPI must not
1144 * affect the live lower half's type information for pruning.
1145 *
1146 * Both halves of SPI 0 are written separately. Only the lower
1147 * half (slot 0) is used as a 4-byte map key. The upper half
1148 * (slot 1) is dead and cleaned to STACK_INVALID.
1149 *
1150 * Path A: key stays 0 (STACK_ZERO) → non-null array lookup
1151 * Path B: key byte turns STACK_MISC → may-null array lookup
1152 * Deref without null check: safe for A, unsafe for B.
1153 *
1154 * If half-SPI cleaning incorrectly corrupted the live half's
1155 * type info, path A's cached state could generalize and unsoundly
1156 * prune path B.
1157 *
1158 * Expected: reject (path B unsafe).
1159 */
1160SEC("socket")
1161__flag(BPF_F_TEST_STATE_FREQ)
1162__failure __msg("R0 invalid mem access 'map_value_or_null'")
1163__naked void half_spi_clean_preserves_stack_zero(void)
1164{
1165 asm volatile (
1166 "*(u32 *)(r10 - 4) = 0;" /* slot 0: STACK_ZERO */
1167 "*(u32 *)(r10 - 8) = 0;" /* slot 1: STACK_ZERO (dead) */
1168 "call %[bpf_get_prandom_u32];"
1169 "if r0 != 0 goto l_nonconst%=;"
1170 "goto l_lookup%=;"
1171"l_nonconst%=:"
1172 "*(u8 *)(r10 - 4) = r0;" /* slot 0: STACK_MISC */
1173"l_lookup%=:"
1174 "r2 = r10;"
1175 "r2 += -4;"
1176 "r1 = %[array_map_8b] ll;"
1177 "call %[bpf_map_lookup_elem];"
1178 "r0 = *(u64 *)(r0 + 0);" /* unsafe if null */
1179 "exit;"
1180 :
1181 : __imm(bpf_get_prandom_u32),
1182 __imm(bpf_map_lookup_elem),
1183 __imm_addr(array_map_8b)
1184 : __clobber_all);
1185}
1186
1187/*
1188 * Model of scx_lavd's pick_idle_cpu_at_cpdom iat block:
1189 * conditional block with helper call and temporary stack spill,
1190 * spill dead after merge.
1191 *
1192 * Path A (fall-through): spill r6 to fp-8 across helper call
1193 * Path B (branch taken): skip the block entirely
1194 * At merge (insn 6): fp-8 is dead (never read after merge)
1195 *
1196 * Static liveness marks fp-8 dead at merge. clean_verifier_state()
1197 * converts path A's STACK_SPILL to STACK_INVALID. Path B has
1198 * STACK_INVALID. stacksafe() matches -> path B pruned -> "6: safe".
1199 */
1200SEC("socket")
1201__flag(BPF_F_TEST_STATE_FREQ)
1202__success
1203__log_level(2)
1204__msg("6: safe")
1205__naked void dead_spill_at_merge_enables_pruning(void)
1206{
1207 asm volatile (
1208 "call %[bpf_get_prandom_u32];"
1209 "r6 = 7;"
1210 "if r0 != 0 goto l_skip%=;"
1211 /* conditional block: spill, call, reload */
1212 "*(u64 *)(r10 - 8) = r6;"
1213 "call %[bpf_get_prandom_u32];"
1214 "r6 = *(u64 *)(r10 - 8);"
1215"l_skip%=:"
1216 /* fp-8 dead. Path B pruned here -> "6: safe" */
1217 "r0 = r6;"
1218 "exit;"
1219 :
1220 : __imm(bpf_get_prandom_u32)
1221 : __clobber_all);
1222}
1223
1224/*
1225 * FP-offset tracking loses precision on second ADD, killing all liveness.
1226 *
1227 * fp_off_insn_xfer() handles "FP itself + negative imm" precisely
1228 * (e.g. r6 = r10; r6 += -24 -> slot 5). But any subsequent ADD/SUB
1229 * on a register that already has non-zero spis falls through to
1230 * spis_set_all(), because the code only handles the FP-itself case.
1231 *
1232 * A write through this imprecise register enters the non-zero-spis
1233 * branch of set_indirect_stack_access(), which OR's the all-ones
1234 * mask into stack_def. The backward liveness equation
1235 *
1236 * stack_in = (stack_out & ~stack_def) | stack_use
1237 *
1238 * sees ~ALL = 0, killing ALL slot liveness at that instruction.
1239 *
1240 * At the merge pruning point, live_stack_before is empty.
1241 * clean_verifier_state() marks fp-8 as STACK_INVALID.
1242 * stacksafe() skips STACK_INVALID (line "continue"), so pruning
1243 * succeeds regardless of the current state's fp-8 value.
1244 * Path B is pruned, its null deref is never explored.
1245 *
1246 * Correct behavior: reject (path B dereferences NULL).
1247 * Bug behavior: accept (path B pruned away).
1248 */
1249SEC("socket")
1250__flag(BPF_F_TEST_STATE_FREQ)
1251__failure __msg("R1 invalid mem access 'scalar'")
1252__naked void fp_add_loses_precision_kills_liveness(void)
1253{
1254 asm volatile (
1255 "call %[bpf_get_prandom_u32];"
1256 "if r0 != 0 goto l_pathB%=;"
1257
1258 /* Path A (fall-through, explored first): fp-8 = 0 */
1259 "r1 = 0;"
1260 "*(u64 *)(r10 - 8) = r1;"
1261 "goto l_merge%=;"
1262
1263"l_pathB%=:"
1264 /* Path B (explored second): fp-8 = 42 */
1265 "r1 = 42;"
1266 "*(u64 *)(r10 - 8) = r1;"
1267
1268"l_merge%=:"
1269 /*
1270 * Create imprecise FP-derived register.
1271 * r6 = r10 - 24 gets precise slot 5.
1272 * r6 += 8 hits the else branch (spis non-zero, delta > 0)
1273 * and sets spis to ALL. r6 is actually r10-16.
1274 */
1275 "r6 = r10;"
1276 "r6 += -24;"
1277 "r6 += 8;"
1278
1279 /*
1280 * Write through imprecise r6. Actually writes to fp-16
1281 * (does NOT touch fp-8), but liveness marks ALL slots
1282 * as stack_def, killing fp-8's liveness.
1283 */
1284 "r7 = 0;"
1285 "*(u64 *)(r6 + 0) = r7;"
1286
1287 /* Read fp-8: liveness says dead, but value is needed. */
1288 "r2 = *(u64 *)(r10 - 8);"
1289 "if r2 == 42 goto l_danger%=;"
1290
1291 /* r2 != 42 (path A: r2 == 0): safe exit */
1292 "r0 = 0;"
1293 "exit;"
1294
1295"l_danger%=:"
1296 /* Only reachable from path B (r2 == 42): null deref */
1297 "r1 = 0;"
1298 "r0 = *(u64 *)(r1 + 0);"
1299 "exit;"
1300 :
1301 : __imm(bpf_get_prandom_u32)
1302 : __clobber_all);
1303}
1304
1305SEC("socket")
1306__flag(BPF_F_TEST_STATE_FREQ)
1307__failure __msg("R1 invalid mem access 'scalar'")
1308__naked void fp_spill_loses_precision_kills_liveness(void)
1309{
1310 asm volatile (
1311 "call %[bpf_get_prandom_u32];"
1312 "if r0 != 0 goto l_pathB%=;"
1313
1314 "r1 = 0;"
1315 "*(u64 *)(r10 - 8) = r1;"
1316 "goto l_merge%=;"
1317
1318"l_pathB%=:"
1319 "r1 = 42;"
1320 "*(u64 *)(r10 - 8) = r1;"
1321
1322"l_merge%=:"
1323 "r6 = r10;"
1324 "r6 += -64;"
1325 "*(u64 *)(r10 - 160) = r6;"
1326 "r6 = *(u64 *)(r10 - 160);"
1327
1328 "r7 = 0;"
1329 "*(u64 *)(r6 + 0) = r7;"
1330
1331 "r2 = *(u64 *)(r10 - 8);"
1332 "if r2 == 42 goto l_danger%=;"
1333
1334 "r0 = *(u64 *)(r10 - 56);"
1335 "exit;"
1336
1337"l_danger%=:"
1338 "r1 = 0;"
1339 "r0 = *(u64 *)(r1 + 0);"
1340 "exit;"
1341 :
1342 : __imm(bpf_get_prandom_u32)
1343 : __clobber_all);
1344}
1345
1346/* === Tests for frame-based AT_FP tracking === */
1347
1348/*
1349 * Test 1: conditional_stx_in_subprog
1350 * Subprog conditionally writes caller's slot.
1351 * Verify slot stays live (backward pass handles conditional def via CFG).
1352 *
1353 * Main writes fp-8=42, calls cond_writer(fp-8), reads fp-8.
1354 * cond_writer only writes on one path → parent_def only on that path.
1355 * The backward parent_live correctly keeps fp-8 live at entry
1356 * (conditional write doesn't kill liveness at the join).
1357 */
1358SEC("socket")
1359__log_level(2)
1360/* fp-8 live at call (callee conditionally writes → slot not killed) */
1361__msg("1: (7b) *(u64 *)(r10 -8) = r1 ; def: fp0-8")
1362__msg("4: (85) call pc+2{{$}}")
1363__msg("5: (79) r0 = *(u64 *)(r10 -8) ; use: fp0-8")
1364__naked void conditional_stx_in_subprog(void)
1365{
1366 asm volatile (
1367 "r1 = 42;"
1368 "*(u64 *)(r10 - 8) = r1;"
1369 "r1 = r10;"
1370 "r1 += -8;"
1371 "call cond_writer;"
1372 "r0 = *(u64 *)(r10 - 8);"
1373 "exit;"
1374 ::: __clobber_all);
1375}
1376
1377/* Conditionally writes to *(r1+0) */
1378static __used __naked void cond_writer(void)
1379{
1380 asm volatile (
1381 "r6 = r1;"
1382 "call %[bpf_get_prandom_u32];"
1383 "if r0 == 0 goto 1f;"
1384 "*(u64 *)(r6 + 0) = r0;"
1385 "1:"
1386 "r0 = 0;"
1387 "exit;"
1388 :: __imm(bpf_get_prandom_u32)
1389 : __clobber_all);
1390}
1391
1392SEC("socket")
1393__log_level(2)
1394__msg("4: (85) call pc+{{.*}} ; use: fp0-16")
1395__msg("7: (85) call pc+{{.*}} ; use: fp0-32")
1396__naked void multiple_callsites_different_offsets(void)
1397{
1398 asm volatile (
1399 "*(u64 *)(r10 - 16) = 0;"
1400 "*(u64 *)(r10 - 32) = 0;"
1401 "r1 = r10;"
1402 "r1 += -16;"
1403 "call read_first_param;"
1404 "r1 = r10;"
1405 "r1 += -32;"
1406 "call read_first_param;"
1407 "r0 = 0;"
1408 "exit;"
1409 ::: __clobber_all);
1410}
1411
1412/*
1413 * Test 3: nested_fp_passthrough
1414 * main→A→B, main's FP forwarded to B. B accesses main's stack.
1415 * Verify liveness propagates through.
1416 *
1417 * Main passes fp-32 to outer_forwarder, which passes it to inner_reader.
1418 * inner_reader reads at arg+0 (= main's fp-32).
1419 * parent_live propagates transitively: inner→outer→main.
1420 */
1421SEC("socket")
1422__log_level(2)
1423/* At call to outer_forwarder: main's fp-32 (slots 6,7) should be live */
1424__msg("6: (85) call pc+{{.*}} ; use: fp0-32")
1425__naked void nested_fp_passthrough(void)
1426{
1427 asm volatile (
1428 "*(u64 *)(r10 - 8) = 0;"
1429 "*(u64 *)(r10 - 16) = 0;"
1430 "*(u64 *)(r10 - 24) = 0;"
1431 "*(u64 *)(r10 - 32) = 0;"
1432 "r1 = r10;"
1433 "r1 += -32;"
1434 "call outer_forwarder;"
1435 "r0 = 0;"
1436 "exit;"
1437 ::: __clobber_all);
1438}
1439
1440/* Forwards arg to inner_reader */
1441static __used __naked void outer_forwarder(void)
1442{
1443 asm volatile (
1444 "call inner_reader;"
1445 "r0 = 0;"
1446 "exit;"
1447 ::: __clobber_all);
1448}
1449
1450static __used __naked void inner_reader(void)
1451{
1452 asm volatile (
1453 "r0 = *(u64 *)(r1 + 0);"
1454 "r0 = 0;"
1455 "exit;"
1456 ::: __clobber_all);
1457}
1458
1459/*
1460 * Test 4: callee_must_write_before_read
1461 * Callee unconditionally writes parent slot before reading.
1462 * Verify slot is NOT live at call site (parent_def kills it).
1463 */
1464SEC("socket")
1465__log_level(2)
1466/* fp-8 NOT live at call: callee writes before reading (parent_def kills it) */
1467__msg("2: .12345.... (85) call pc+")
1468__naked void callee_must_write_before_read(void)
1469{
1470 asm volatile (
1471 "r1 = r10;"
1472 "r1 += -8;"
1473 "call write_then_read;"
1474 "r0 = 0;"
1475 "exit;"
1476 ::: __clobber_all);
1477}
1478
1479/* Unconditionally writes *(r1+0), then reads it back */
1480static __used __naked void write_then_read(void)
1481{
1482 asm volatile (
1483 "r6 = r1;"
1484 "r7 = 99;"
1485 "*(u64 *)(r6 + 0) = r7;"
1486 "r0 = *(u64 *)(r6 + 0);"
1487 "r0 = 0;"
1488 "exit;"
1489 ::: __clobber_all);
1490}
1491
1492/*
1493 * Test 5: return_site_liveness_bleeding
1494 * Main calls subprog twice. Slot used after one call but not the other.
1495 * Context-insensitive: slot conservatively live at both.
1496 *
1497 * After first call: read fp-8.
1498 * After second call: don't read fp-8.
1499 * Since parent_live is per-subprog (not per call-site),
1500 * fp-8 is live at both call sites.
1501 */
1502SEC("socket")
1503__log_level(2)
1504/* Both calls have fp-8 live due to context-insensitive parent_live */
1505__msg("3: (85) call pc+{{.*}} ; use: fp0-8")
1506__msg("7: (85) call pc+{{.*}} ; use: fp0-8")
1507__naked void return_site_liveness_bleeding(void)
1508{
1509 asm volatile (
1510 "*(u64 *)(r10 - 8) = 0;"
1511 "r1 = r10;"
1512 "r1 += -8;"
1513 "call read_first_param;"
1514 "r0 = *(u64 *)(r10 - 8);"
1515 "r1 = r10;"
1516 "r1 += -8;"
1517 "call read_first_param;"
1518 "r0 = 0;"
1519 "exit;"
1520 ::: __clobber_all);
1521}
1522
1523SEC("socket")
1524__log_level(2)
1525__msg("9: (85) call bpf_loop#181 ; use: fp0-16")
1526__naked void callback_conditional_read_beyond_ctx(void)
1527{
1528 asm volatile (
1529 "r1 = 42;"
1530 "*(u64 *)(r10 - 8) = r1;"
1531 "*(u64 *)(r10 - 16) = r1;"
1532 "r1 = 2;"
1533 "r2 = cb_cond_read ll;"
1534 "r3 = r10;"
1535 "r3 += -8;"
1536 "r4 = 0;"
1537 "call %[bpf_loop];"
1538 "r0 = 0;"
1539 "exit;"
1540 :: __imm(bpf_loop)
1541 : __clobber_all);
1542}
1543
1544/* Callback conditionally reads *(ctx - 8) = caller fp-16 */
1545static __used __naked void cb_cond_read(void)
1546{
1547 asm volatile (
1548 "r6 = r2;"
1549 "call %[bpf_get_prandom_u32];"
1550 "if r0 == 0 goto 1f;"
1551 "r0 = *(u64 *)(r6 - 8);"
1552 "1:"
1553 "r0 = 0;"
1554 "exit;"
1555 :: __imm(bpf_get_prandom_u32)
1556 : __clobber_all);
1557}
1558
1559SEC("socket")
1560__log_level(2)
1561__msg("14: (7b) *(u64 *)(r6 -8) = r7 ; def: fp0-16")
1562__msg("15: (79) r0 = *(u64 *)(r6 -8) ; use: fp0-16")
1563__naked void callback_write_before_read_kills(void)
1564{
1565 asm volatile (
1566 "r1 = 42;"
1567 "*(u64 *)(r10 - 8) = r1;"
1568 "*(u64 *)(r10 - 16) = r1;"
1569 "r1 = 2;"
1570 "r2 = cb_write_read ll;"
1571 "r3 = r10;"
1572 "r3 += -8;"
1573 "r4 = 0;"
1574 "call %[bpf_loop];"
1575 "r0 = 0;"
1576 "exit;"
1577 :: __imm(bpf_loop)
1578 : __clobber_all);
1579}
1580
1581/* Callback unconditionally writes *(ctx-8), then reads it back.
1582 * The write (parent_def) kills liveness before entry.
1583 */
1584static __used __naked void cb_write_read(void)
1585{
1586 asm volatile (
1587 "r6 = r2;"
1588 "r7 = 99;"
1589 "*(u64 *)(r6 - 8) = r7;"
1590 "r0 = *(u64 *)(r6 - 8);"
1591 "r0 = 0;"
1592 "exit;"
1593 ::: __clobber_all);
1594}
1595
1596/*
1597 * bpf_loop callback conditionally writes fp-16 then unconditionally
1598 * reads it. The conditional write does NOT kill liveness
1599 */
1600SEC("socket")
1601__log_level(2)
1602__msg("9: (85) call bpf_loop#181 ; use: fp0-16")
1603__naked void callback_conditional_write_preserves(void)
1604{
1605 asm volatile (
1606 "r1 = 42;"
1607 "*(u64 *)(r10 - 8) = r1;"
1608 "*(u64 *)(r10 - 16) = r1;"
1609 "r1 = 2;"
1610 "r2 = cb_cond_write_read ll;"
1611 "r3 = r10;"
1612 "r3 += -8;"
1613 "r4 = 0;"
1614 "call %[bpf_loop];"
1615 "r0 = 0;"
1616 "exit;"
1617 :: __imm(bpf_loop)
1618 : __clobber_all);
1619}
1620
1621static __used __naked void cb_cond_write_read(void)
1622{
1623 asm volatile (
1624 "r6 = r2;"
1625 "call %[bpf_get_prandom_u32];"
1626 "if r0 == 0 goto 1f;"
1627 "*(u64 *)(r6 - 8) = r0;"
1628 "1:"
1629 "r0 = *(u64 *)(r6 - 8);"
1630 "r0 = 0;"
1631 "exit;"
1632 :: __imm(bpf_get_prandom_u32)
1633 : __clobber_all);
1634}
1635
1636/*
1637 * Two bpf_loop calls with the same callback but different ctx pointers.
1638 *
1639 * First call: ctx=fp-8, second call: ctx=fp-24.
1640 */
1641SEC("socket")
1642__log_level(2)
1643__msg(" 8: (85) call bpf_loop{{.*}} ; use: fp0-8")
1644__msg("15: (85) call bpf_loop{{.*}} ; use: fp0-24")
1645__naked void callback_two_calls_different_ctx(void)
1646{
1647 asm volatile (
1648 "*(u64 *)(r10 - 8) = 0;"
1649 "*(u64 *)(r10 - 24) = 0;"
1650 "r1 = 1;"
1651 "r2 = cb_read_ctx ll;"
1652 "r3 = r10;"
1653 "r3 += -8;"
1654 "r4 = 0;"
1655 "call %[bpf_loop];"
1656 "r1 = 1;"
1657 "r2 = cb_read_ctx ll;"
1658 "r3 = r10;"
1659 "r3 += -24;"
1660 "r4 = 0;"
1661 "call %[bpf_loop];"
1662 "r0 = 0;"
1663 "exit;"
1664 :: __imm(bpf_loop)
1665 : __clobber_all);
1666}
1667
1668/* Callback reads at ctx+0 unconditionally */
1669static __used __naked void cb_read_ctx(void)
1670{
1671 asm volatile (
1672 "r0 = *(u64 *)(r2 + 0);"
1673 "r0 = 0;"
1674 "exit;"
1675 ::: __clobber_all);
1676}
1677
1678/*
1679 * Reproducer for unsound pruning in refined_caller_live_stack().
1680 *
1681 * Three-level call chain: main → mid_fwd → grandchild_deref.
1682 * Main passes &fp-8 to mid_fwd, which forwards R1 to grandchild_deref.
1683 * grandchild_deref reads main's fp-8 through the forwarded pointer
1684 * and dereferences the result.
1685 *
1686 * refined_caller_live_stack() has a callee_offset++ when mid_fwd
1687 * (frame 1) is mid-call. This drops the transitive parent_live
1688 * contribution at mid_fwd's call instruction — the only place
1689 * where grandchild_deref's read of main's fp-8 is recorded.
1690 * As a result, main's fp-8 is cleaned to STACK_INVALID at the
1691 * pruning point inside grandchild_deref, and path B is
1692 * incorrectly pruned against path A.
1693 *
1694 * Path A: main stores PTR_TO_MAP_VALUE at fp-8
1695 * Path B: main stores scalar 42 at fp-8
1696 *
1697 * Correct behavior: reject (path B dereferences scalar)
1698 * Bug behavior: accept (path B pruned against cleaned path A)
1699 */
1700SEC("socket")
1701__flag(BPF_F_TEST_STATE_FREQ)
1702__failure __msg("R0 invalid mem access 'scalar'")
1703__naked void transitive_parent_stack_read_unsound(void)
1704{
1705 asm volatile (
1706 /* Map lookup to get PTR_TO_MAP_VALUE */
1707 "r1 = %[map] ll;"
1708 "*(u32 *)(r10 - 32) = 0;"
1709 "r2 = r10;"
1710 "r2 += -32;"
1711 "call %[bpf_map_lookup_elem];"
1712 "if r0 == 0 goto l_exit%=;"
1713 "r6 = r0;"
1714 /* Branch: path A (fall-through) explored first */
1715 "call %[bpf_get_prandom_u32];"
1716 "if r0 != 0 goto l_scalar%=;"
1717 /* Path A: fp-8 = PTR_TO_MAP_VALUE */
1718 "*(u64 *)(r10 - 8) = r6;"
1719 "goto l_merge%=;"
1720"l_scalar%=:"
1721 /* Path B: fp-8 = scalar 42 */
1722 "r1 = 42;"
1723 "*(u64 *)(r10 - 8) = r1;"
1724"l_merge%=:"
1725 /* Pass &fp-8 to mid_fwd → grandchild_deref */
1726 "r1 = r10;"
1727 "r1 += -8;"
1728 "call mid_fwd;"
1729 "r0 = 0;"
1730 "exit;"
1731"l_exit%=:"
1732 "r0 = 0;"
1733 "exit;"
1734 :
1735 : __imm(bpf_map_lookup_elem),
1736 __imm(bpf_get_prandom_u32),
1737 __imm_addr(map)
1738 : __clobber_all);
1739}
1740
1741/* Forwards R1 (ptr to main's fp-8) to grandchild_deref */
1742static __used __naked void mid_fwd(void)
1743{
1744 asm volatile (
1745 "call grandchild_deref;"
1746 "r0 = 0;"
1747 "exit;"
1748 ::: __clobber_all);
1749}
1750
1751/* Reads main's fp-8 through forwarded pointer, dereferences result */
1752static __used __naked void grandchild_deref(void)
1753{
1754 asm volatile (
1755 "goto +0;" /* checkpoint */
1756 "goto +0;" /* checkpoint */
1757 /* read main's fp-8: map_ptr (path A) or scalar (path B) */
1758 "r0 = *(u64 *)(r1 + 0);"
1759 /* dereference: safe for map_ptr, unsafe for scalar */
1760 "r0 = *(u64 *)(r0 + 0);"
1761 "r0 = 0;"
1762 "exit;"
1763 ::: __clobber_all);
1764}
1765
1766SEC("socket")
1767__log_level(2)
1768__success
1769__msg("14: (79) r1 = *(u64 *)(r10 -8) // r6=fp0-8 r7=fp1-16 fp-8=fp1-16 fp-16=fp0-8")
1770__msg("15: (79) r0 = *(u64 *)(r1 +0) // r1=fp1-16 r6=fp0-8 r7=fp1-16 fp-8=fp1-16 fp-16=fp0-8")
1771__msg("stack use/def subprog#1 mid_two_fp_threshold (d1,cs2):")
1772__msg("14: (79) r1 = *(u64 *)(r10 -8) ; use: fp1-8")
1773__msg("15: (79) r0 = *(u64 *)(r1 +0) ; use: fp1-16")
1774__naked void two_fp_clear_stack_threshold(void)
1775{
1776 asm volatile (
1777 "r1 = r10;"
1778 "r1 += -8;"
1779 "call mid_two_fp_threshold;"
1780 "r0 = 0;"
1781 "exit;"
1782 ::: __clobber_all);
1783}
1784
1785static __used __naked void mid_two_fp_threshold(void)
1786{
1787 asm volatile (
1788 "r6 = r1;"
1789 "r7 = r10;"
1790 "r7 += -16;"
1791 "*(u64 *)(r10 - 8) = r7;"
1792 "*(u64 *)(r10 - 16) = r6;"
1793 "r1 = r10;"
1794 "r1 += -8;"
1795 "r2 = r6;"
1796 "call inner_nop_fptest;"
1797 "r1 = *(u64 *)(r10 - 8);"
1798 "r0 = *(u64 *)(r1 + 0);"
1799 "r0 = 0;"
1800 "exit;"
1801 ::: __clobber_all);
1802}
1803
1804static __used __naked void inner_nop_fptest(void)
1805{
1806 asm volatile (
1807 "r0 = 0;"
1808 "exit;"
1809 ::: __clobber_all);
1810}
1811
1812SEC("socket")
1813__log_level(2)
1814__success
1815__msg("13: (79) r1 = *(u64 *)(r10 -8) // r6=fp0-8 r7=fp1-16 fp-8=fp1-16 fp-16=fp0-8")
1816__msg("14: (79) r0 = *(u64 *)(r1 +0) // r1=fp1-16 r6=fp0-8 r7=fp1-16 fp-8=fp1-16 fp-16=fp0-8")
1817__msg("stack use/def subprog#1 mid_one_fp_threshold (d1,cs2):")
1818__msg("13: (79) r1 = *(u64 *)(r10 -8) ; use: fp1-8")
1819__msg("14: (79) r0 = *(u64 *)(r1 +0) ; use: fp1-16")
1820__naked void one_fp_clear_stack_threshold(void)
1821{
1822 asm volatile (
1823 "r1 = r10;"
1824 "r1 += -8;"
1825 "call mid_one_fp_threshold;"
1826 "r0 = 0;"
1827 "exit;"
1828 ::: __clobber_all);
1829}
1830
1831static __used __naked void mid_one_fp_threshold(void)
1832{
1833 asm volatile (
1834 "r6 = r1;"
1835 "r7 = r10;"
1836 "r7 += -16;"
1837 "*(u64 *)(r10 - 8) = r7;"
1838 "*(u64 *)(r10 - 16) = r6;"
1839 "r1 = r10;"
1840 "r1 += -8;"
1841 "call inner_nop_fptest;"
1842 "r1 = *(u64 *)(r10 - 8);"
1843 "r0 = *(u64 *)(r1 + 0);"
1844 "r0 = 0;"
1845 "exit;"
1846 ::: __clobber_all);
1847}
1848
1849/*
1850 * Reproducer for unsound pruning when a subprog forwards a parent
1851 * stack pointer (AT_PARENT) to a helper with a memory argument.
1852 *
1853 * set_call_stack_access_at() previously only tracked AT_CURRENT args,
1854 * skipping AT_PARENT entirely. This meant helper reads through parent
1855 * stack pointers did not set parent_use, letting the slot appear dead
1856 * at pruning checkpoints inside the subprog.
1857 *
1858 * Program shape:
1859 * main:
1860 * *(u32)(fp-4) = 0 key = STACK_ZERO (const 0)
1861 * call bpf_get_prandom_u32
1862 * if r0 != 0 goto clobber path A (fall-through) first
1863 * goto merge
1864 * clobber:
1865 * *(u8)(fp-4) = r0 path B: key[0] = STACK_MISC
1866 * merge:
1867 * r1 = fp - 4
1868 * call fwd_parent_key_to_helper
1869 * r0 = 0
1870 * exit
1871 *
1872 * fwd_parent_key_to_helper(r1 = &caller_fp-4):
1873 * goto +0 checkpoint
1874 * r2 = r1 R2 = AT_PARENT ptr to caller fp-4
1875 * r1 = array_map_8b ll R1 = array map
1876 * call bpf_map_lookup_elem reads key_size(4) from parent fp-4
1877 * r0 = *(u64 *)(r0 + 0) deref without null check
1878 * r0 = 0
1879 * exit
1880 *
1881 * Path A: STACK_ZERO key = const 0 -> array lookup -> PTR_TO_MAP_VALUE
1882 * (non-NULL for in-bounds const key) -> deref OK.
1883 * Path B: STACK_MISC key = unknown -> array lookup ->
1884 * PTR_TO_MAP_VALUE_OR_NULL -> deref UNSAFE.
1885 *
1886 * Bug: AT_PARENT R2 arg to bpf_map_lookup_elem skipped -> parent_use
1887 * not set -> fp-4 cleaned at checkpoint -> STACK_ZERO collapses
1888 * to STACK_INVALID -> path B pruned -> deref never checked.
1889 *
1890 * Correct verifier behavior: reject (path B deref of map_value_or_null).
1891 */
1892SEC("socket")
1893__flag(BPF_F_TEST_STATE_FREQ)
1894__failure __msg("R0 invalid mem access 'map_value_or_null'")
1895__naked void helper_parent_stack_read_unsound(void)
1896{
1897 asm volatile (
1898 /* key at fp-4: all bytes STACK_ZERO */
1899 "*(u32 *)(r10 - 4) = 0;"
1900 "call %[bpf_get_prandom_u32];"
1901 /* fall-through (path A) explored first */
1902 "if r0 != 0 goto l_clobber%=;"
1903 /* path A: key stays constant zero */
1904 "goto l_merge%=;"
1905"l_clobber%=:"
1906 /* path B: key[0] becomes STACK_MISC, key no longer const */
1907 "*(u8 *)(r10 - 4) = r0;"
1908"l_merge%=:"
1909 "r1 = r10;"
1910 "r1 += -4;"
1911 "call fwd_parent_key_to_helper;"
1912 "r0 = 0;"
1913 "exit;"
1914 :
1915 : __imm(bpf_get_prandom_u32)
1916 : __clobber_all);
1917}
1918
1919/*
1920 * Subprog forwards parent stack pointer to bpf_map_lookup_elem as key
1921 * on an array map, then dereferences the result without a null check.
1922 * R1 = &parent_fp-4 (AT_PARENT in this frame).
1923 *
1924 * The helper reads key_size(4) bytes from parent stack. The deref of
1925 * R0 reads the map value, NOT parent stack, so record_insn_mem_accesses
1926 * does not set parent_use for it. The ONLY parent stack access is
1927 * through the helper's R2 arg.
1928 */
1929static __used __naked void fwd_parent_key_to_helper(void)
1930{
1931 asm volatile (
1932 "goto +0;" /* checkpoint */
1933 "r2 = r1;" /* R2 = parent ptr (AT_PARENT) */
1934 "r1 = %[array_map_8b] ll;" /* R1 = array map */
1935 "call %[bpf_map_lookup_elem];" /* reads 4 bytes from parent fp-4 */
1936 /* deref without null check: safe for PTR_TO_MAP_VALUE,
1937 * unsafe for PTR_TO_MAP_VALUE_OR_NULL
1938 */
1939 "r0 = *(u64 *)(r0 + 0);"
1940 "r0 = 0;"
1941 "exit;"
1942 :
1943 : __imm(bpf_map_lookup_elem),
1944 __imm_addr(array_map_8b)
1945 : __clobber_all);
1946}
1947
1948/*
1949 * Regression for keeping later helper args after a whole-stack fallback
1950 * on an earlier local arg. The first bpf_snprintf() arg is a local
1951 * frame-derived pointer with offset-imprecise tracking (`fp1 ?`), which
1952 * conservatively marks the whole local stack live. The fourth arg still
1953 * forwards &parent_fp-8 and must contribute nonlocal_use[0]=0:3.
1954 */
1955SEC("socket")
1956__log_level(2)
1957__success
1958__msg("call bpf_snprintf{{.*}} ; use: fp1-8..-512 fp0-8")
1959__naked void helper_arg_fallback_keeps_scanning(void)
1960{
1961 asm volatile (
1962 "r1 = 42;"
1963 "*(u64 *)(r10 - 8) = r1;"
1964 "r1 = r10;"
1965 "r1 += -8;"
1966 "call helper_snprintf_parent_after_local_fallback;"
1967 "r0 = 0;"
1968 "exit;"
1969 ::: __clobber_all);
1970}
1971
1972static __used __naked void helper_snprintf_parent_after_local_fallback(void)
1973{
1974 asm volatile (
1975 "r6 = r1;" /* save &parent_fp-8 */
1976 "call %[bpf_get_prandom_u32];"
1977 "r0 &= 8;"
1978 "r1 = r10;"
1979 "r1 += -16;"
1980 "r1 += r0;" /* local fp, offset-imprecise */
1981 "r2 = 8;"
1982 "r3 = %[snprintf_u64_fmt] ll;"
1983 "r4 = r6;" /* later arg: parent fp-8 */
1984 "r5 = 8;"
1985 "call %[bpf_snprintf];"
1986 "r0 = 0;"
1987 "exit;"
1988 :
1989 : __imm(bpf_get_prandom_u32),
1990 __imm(bpf_snprintf),
1991 __imm_addr(snprintf_u64_fmt)
1992 : __clobber_all);
1993}
1994
1995/*
1996 * Test that propagate_callee_ancestor() correctly chains ancestor
1997 * liveness across sequential calls within a single frame.
1998 *
1999 * main → mid_seq_touch → {nop_callee, deref_ancestor}
2000 *
2001 * mid_seq_touch receives two pointers: R1 = &main_fp-8 (forwarded to
2002 * deref_ancestor) and R2 = &main_fp-16 (read directly by mid_seq_touch).
2003 * The direct read of fp-16 forces ensure_anc_arrays() to allocate
2004 * ancestor_live[0] for mid_seq_touch, so refined_caller_live_stack()
2005 * uses the refined path (not the conservative fallback).
2006 *
2007 * mid_seq_touch calls nop_callee first (no-op, creates a pruning point),
2008 * then calls deref_ancestor which reads main's fp-8 and dereferences it.
2009 *
2010 * propagate_callee_ancestor() propagates deref_ancestor's entry
2011 * ancestor_live[0] into mid_seq_touch's anc_use[0] at the call-to-deref
2012 * instruction. mid_seq_touch's backward pass flows this backward so
2013 * ancestor_live[0] includes fp-8 at the pruning point between the calls.
2014 *
2015 * Without propagation, mid_seq_touch's ancestor_live[0] only has fp-16
2016 * (from the direct read) — fp-8 is missing. refined_caller_live_stack()
2017 * Term 1 says fp-8 is dead, the verifier cleans it, and path B
2018 * (scalar 42) is incorrectly pruned against path A (MAP_VALUE).
2019 *
2020 * Path A: main stores PTR_TO_MAP_VALUE at fp-8 → deref succeeds
2021 * Path B: main stores scalar 42 at fp-8 → deref must fail
2022 *
2023 * Correct: reject (path B dereferences scalar)
2024 */
2025SEC("socket")
2026__flag(BPF_F_TEST_STATE_FREQ)
2027__failure __msg("R0 invalid mem access 'scalar'")
2028__naked void propagate_callee_ancestor_chain(void)
2029{
2030 asm volatile (
2031 /* Map lookup to get PTR_TO_MAP_VALUE */
2032 "r1 = %[map] ll;"
2033 "*(u32 *)(r10 - 32) = 0;"
2034 "r2 = r10;"
2035 "r2 += -32;"
2036 "call %[bpf_map_lookup_elem];"
2037 "if r0 == 0 goto l_exit%=;"
2038 "r6 = r0;"
2039 /* Branch: path A (fall-through) explored first */
2040 "call %[bpf_get_prandom_u32];"
2041 "if r0 != 0 goto l_scalar%=;"
2042 /* Path A: fp-8 = PTR_TO_MAP_VALUE */
2043 "*(u64 *)(r10 - 8) = r6;"
2044 "goto l_merge%=;"
2045"l_scalar%=:"
2046 /* Path B: fp-8 = scalar 42 */
2047 "r1 = 42;"
2048 "*(u64 *)(r10 - 8) = r1;"
2049"l_merge%=:"
2050 /* fp-16 = dummy value (mid_seq_touch reads it directly) */
2051 "r1 = 99;"
2052 "*(u64 *)(r10 - 16) = r1;"
2053 /* R1 = &fp-8 (for deref_ancestor), R2 = &fp-16 (for mid_seq_touch) */
2054 "r1 = r10;"
2055 "r1 += -8;"
2056 "r2 = r10;"
2057 "r2 += -16;"
2058 "call mid_seq_touch;"
2059 "r0 = 0;"
2060 "exit;"
2061"l_exit%=:"
2062 "r0 = 0;"
2063 "exit;"
2064 :
2065 : __imm(bpf_map_lookup_elem),
2066 __imm(bpf_get_prandom_u32),
2067 __imm_addr(map)
2068 : __clobber_all);
2069}
2070
2071/*
2072 * R1 = &main_fp-8 (forwarded to deref_ancestor)
2073 * R2 = &main_fp-16 (read directly here → allocates ancestor_live[0])
2074 *
2075 * Reads main's fp-16 to force ancestor_live[0] allocation, then
2076 * calls nop_callee (pruning point), then deref_ancestor.
2077 */
2078static __used __naked void mid_seq_touch(void)
2079{
2080 asm volatile (
2081 "r6 = r1;" /* save &main_fp-8 in callee-saved */
2082 "r0 = *(u64 *)(r2 + 0);" /* read main's fp-16: triggers anc_use[0] */
2083 "call nop_callee;" /* no-op, creates pruning point after */
2084 "r1 = r6;" /* restore ptr to &main_fp-8 */
2085 "call deref_ancestor;" /* reads main's fp-8, dereferences */
2086 "r0 = 0;"
2087 "exit;"
2088 ::: __clobber_all);
2089}
2090
2091static __used __naked void nop_callee(void)
2092{
2093 asm volatile (
2094 "r0 = 0;"
2095 "exit;"
2096 ::: __clobber_all);
2097}
2098
2099/* Reads main's fp-8 through forwarded pointer, dereferences result */
2100static __used __naked void deref_ancestor(void)
2101{
2102 asm volatile (
2103 "r0 = *(u64 *)(r1 + 0);" /* read main's fp-8 */
2104 "r0 = *(u64 *)(r0 + 0);" /* deref: safe for map_ptr, unsafe for scalar */
2105 "r0 = 0;"
2106 "exit;"
2107 ::: __clobber_all);
2108}
2109
2110/*
2111 * Test: callee loads an fp-derived pointer from caller's stack, then
2112 * reads through it to access another caller stack slot.
2113 *
2114 * main stores PTR_TO_MAP_VALUE at fp-24, stores &fp-24 (an fp-derived
2115 * pointer) at fp-8, passes &fp-8 through mid_fwd_spilled_ptr to
2116 * load_ptr_deref_grandchild. The leaf loads the pointer from main's
2117 * fp-8, then reads main's fp-24 through the loaded pointer.
2118 *
2119 * fill_from_stack() in arg_track_xfer() only handles local-frame
2120 * FP-derived loads (src_is_local_fp check requires frame == depth).
2121 * When a callee loads from a parent-frame pointer (frame < depth),
2122 * the loaded value gets ARG_NONE instead of being recognized as
2123 * fp-derived. Subsequent reads through that loaded pointer are
2124 * invisible to liveness — nonlocal_use is never set for fp-24.
2125 *
2126 * clean_live_states() cleans the current state at every prune point.
2127 * Because liveness misses fp-24, refined_caller_live_stack() tells
2128 * __clean_func_state() that fp-24 is dead, which destroys the
2129 * PTR_TO_MAP_VALUE spill before the grandchild can read it.
2130 * The grandchild then reads STACK_INVALID → scalar, and the deref
2131 * is rejected with "R0 invalid mem access 'scalar'" — even though
2132 * fp-24 is genuinely live and holds a valid map pointer.
2133 *
2134 * This is a false positive: a valid program incorrectly rejected.
2135 */
2136SEC("socket")
2137__flag(BPF_F_TEST_STATE_FREQ)
2138__success
2139__naked void spilled_fp_cross_frame_deref(void)
2140{
2141 asm volatile (
2142 /* Map lookup to get PTR_TO_MAP_VALUE */
2143 "r1 = %[map] ll;"
2144 "*(u32 *)(r10 - 32) = 0;"
2145 "r2 = r10;"
2146 "r2 += -32;"
2147 "call %[bpf_map_lookup_elem];"
2148 "if r0 == 0 goto l_exit%=;"
2149 /* fp-24 = PTR_TO_MAP_VALUE */
2150 "*(u64 *)(r10 - 24) = r0;"
2151 /* Store pointer to fp-24 at fp-8 */
2152 "r1 = r10;"
2153 "r1 += -24;"
2154 "*(u64 *)(r10 - 8) = r1;"
2155 /* R1 = &fp-8: pointer to the spilled ptr */
2156 "r1 = r10;"
2157 "r1 += -8;"
2158 "call mid_fwd_spilled_ptr;"
2159 "r0 = 0;"
2160 "exit;"
2161"l_exit%=:"
2162 "r0 = 0;"
2163 "exit;"
2164 :
2165 : __imm(bpf_map_lookup_elem),
2166 __imm_addr(map)
2167 : __clobber_all);
2168}
2169
2170/* Forwards R1 (ptr to main's fp-8, which holds &main_fp-24) to leaf */
2171static __used __naked void mid_fwd_spilled_ptr(void)
2172{
2173 asm volatile (
2174 "call load_ptr_deref_grandchild;"
2175 "r0 = 0;"
2176 "exit;"
2177 ::: __clobber_all);
2178}
2179
2180/*
2181 * R1 = &main_fp-8 (where main stored ptr to fp-24)
2182 * Loads the ptr from main's fp-8, reads main's fp-24 through it,
2183 * then dereferences the result.
2184 */
2185static __used __naked void load_ptr_deref_grandchild(void)
2186{
2187 asm volatile (
2188 /* Load ptr from main's fp-8 → r2 = &main_fp-24 */
2189 "r2 = *(u64 *)(r1 + 0);"
2190 /* Read main's fp-24 through loaded ptr */
2191 "r0 = *(u64 *)(r2 + 0);"
2192 /* Dereference: safe for map_ptr */
2193 "r0 = *(u64 *)(r0 + 0);"
2194 "r0 = 0;"
2195 "exit;"
2196 ::: __clobber_all);
2197}
2198
2199/*
2200 * Exercise merge_nonlocal_live().
2201 *
2202 * merge_shared_mid is analyzed twice (once from each wrapper), so the
2203 * callsite within merge_shared_mid that calls merge_leaf_read gets its
2204 * nonlocal_live info merged twice via merge_nonlocal_live().
2205 */
2206SEC("socket")
2207__log_level(2)
2208__success
2209__msg("14: (85) call pc+2 r1: fp0-16")
2210__msg("17: (79) r0 = *(u64 *)(r1 +0) // r1=fp0-16")
2211__msg("14: (85) call pc+2 r1: fp0-8")
2212__msg("17: (79) r0 = *(u64 *)(r1 +0) // r1=fp0-8")
2213__msg("5: (85) call pc+{{.*}} ; use: fp0-8 fp0-16")
2214__naked void test_merge_nonlocal_live(void)
2215{
2216 asm volatile (
2217 "r1 = 0;"
2218 "*(u64 *)(r10 - 8) = r1;"
2219 "*(u64 *)(r10 - 16) = r1;"
2220 "r1 = r10;"
2221 "r1 += -8;"
2222 "call merge_wrapper_a;"
2223 "r1 = r10;"
2224 "r1 += -16;"
2225 "call merge_wrapper_b;"
2226 "r0 = 0;"
2227 "exit;"
2228 ::: __clobber_all);
2229}
2230
2231static __used __naked void merge_wrapper_a(void)
2232{
2233 asm volatile (
2234 "call merge_shared_mid;"
2235 "r0 = 0;"
2236 "exit;"
2237 ::: __clobber_all);
2238}
2239
2240static __used __naked void merge_wrapper_b(void)
2241{
2242 asm volatile (
2243 "call merge_shared_mid;"
2244 "r0 = 0;"
2245 "exit;"
2246 ::: __clobber_all);
2247}
2248
2249static __used __naked void merge_shared_mid(void)
2250{
2251 asm volatile (
2252 "call merge_leaf_read;"
2253 "r0 = 0;"
2254 "exit;"
2255 ::: __clobber_all);
2256}
2257
2258static __used __naked void merge_leaf_read(void)
2259{
2260 asm volatile (
2261 "r0 = *(u64 *)(r1 + 0);"
2262 "r0 = 0;"
2263 "exit;"
2264 ::: __clobber_all);
2265}
2266
2267/* Same bpf_loop instruction calls different callbacks depending on branch. */
2268SEC("socket")
2269__log_level(2)
2270__success
2271__msg("call bpf_loop#181 ; use: fp2-8..-512 fp1-8..-512 fp0-8..-512")
2272__naked void bpf_loop_two_callbacks(void)
2273{
2274 asm volatile (
2275 "r1 = 0;"
2276 "*(u64 *)(r10 - 8) = r1;"
2277 "*(u64 *)(r10 - 16) = r1;"
2278 "r1 = r10;"
2279 "r1 += -8;"
2280 "call dyn_wrapper_a;"
2281 "r1 = r10;"
2282 "r1 += -16;"
2283 "call dyn_wrapper_b;"
2284 "r0 = 0;"
2285 "exit;"
2286 ::: __clobber_all);
2287}
2288
2289static __used __naked void dyn_wrapper_a(void)
2290{
2291 asm volatile (
2292 "call mid_dynamic_cb;"
2293 "r0 = 0;"
2294 "exit;"
2295 ::: __clobber_all);
2296}
2297
2298static __used __naked void dyn_wrapper_b(void)
2299{
2300 asm volatile (
2301 "call mid_dynamic_cb;"
2302 "r0 = 0;"
2303 "exit;"
2304 ::: __clobber_all);
2305}
2306
2307static __used __naked void mid_dynamic_cb(void)
2308{
2309 asm volatile (
2310 "r6 = r1;"
2311 "call %[bpf_get_prandom_u32];"
2312 "if r0 == 0 goto 1f;"
2313 "r2 = dyn_cb_a ll;"
2314 "goto 2f;"
2315 "1:"
2316 "r2 = dyn_cb_b ll;"
2317 "2:"
2318 "r1 = 1;"
2319 "r3 = r6;" /* ctx = fp-derived ptr from parent */
2320 "r4 = 0;"
2321 "call %[bpf_loop];"
2322 "r0 = 0;"
2323 "exit;"
2324 :: __imm(bpf_get_prandom_u32),
2325 __imm(bpf_loop)
2326 : __clobber_all);
2327}
2328
2329/* Callback A/B: read parent stack through ctx */
2330static __used __naked void dyn_cb_a(void)
2331{
2332 asm volatile (
2333 "r0 = *(u64 *)(r2 + 0);"
2334 "r0 = 0;"
2335 "exit;"
2336 ::: __clobber_all);
2337}
2338
2339static __used __naked void dyn_cb_b(void)
2340{
2341 asm volatile (
2342 "r0 = *(u64 *)(r2 + 0);"
2343 "r0 = 0;"
2344 "exit;"
2345 ::: __clobber_all);
2346}
2347
2348/*
2349 * Path A: r0 = map_lookup result (non-FP, ARG_NONE for stack tracking)
2350 * Path B: r0 = fp-8 (FP-derived, frame=0, off=-8)
2351 * At the join: r0 is not guaranteed to be a frame pointer.
2352 */
2353SEC("socket")
2354__log_level(2)
2355__msg("10: (79) r0 = *(u64 *)(r10 -8) // r0=fp0-8|fp0+0")
2356__naked void stack_or_non_stack_write(void)
2357{
2358 asm volatile (
2359 /* initial write to fp-8 */
2360 "*(u64 *)(r10 - 8) = 0;"
2361 /* map lookup to get a non-FP pointer */
2362 "r2 = r10;"
2363 "r2 += -4;"
2364 "r1 = %[map] ll;"
2365 "call %[bpf_map_lookup_elem];"
2366 /* r0 = map_value (ARG_NONE) */
2367 "if r0 != 0 goto 1f;"
2368 /* path B: r0 = fp-8 */
2369 "r0 = r10;"
2370 "r0 += -8;"
2371"1:"
2372 /* join: the write is not a def for fp[0]-8 */
2373 "*(u64 *)(r0 + 0) = 7;"
2374 /* read fp-8: should be non-poisoned */
2375 "r0 = *(u64 *)(r10 - 8);"
2376 "exit;"
2377 :
2378 : __imm(bpf_map_lookup_elem),
2379 __imm_addr(map)
2380 : __clobber_all);
2381}
2382
2383SEC("socket")
2384__log_level(2)
2385__flag(BPF_F_TEST_STATE_FREQ)
2386__msg("subprog#2 write_first_read_second:")
2387__msg("17: (7a) *(u64 *)(r1 +0) = 42{{$}}")
2388__msg("18: (79) r0 = *(u64 *)(r2 +0) // r1=fp0-8 r2=fp0-16{{$}}")
2389__msg("stack use/def subprog#2 write_first_read_second (d2,cs15):")
2390__msg("17: (7a) *(u64 *)(r1 +0) = 42{{$}}")
2391__msg("18: (79) r0 = *(u64 *)(r2 +0) ; use: fp0-8 fp0-16")
2392__naked void shared_instance_must_write_overwrite(void)
2393{
2394 asm volatile (
2395 "r1 = 1;"
2396 "*(u64 *)(r10 - 8) = r1;"
2397 "*(u64 *)(r10 - 16) = r1;"
2398 /* Call 1: write_first_read_second(&fp[-8], &fp[-16]) */
2399 "r1 = r10;"
2400 "r1 += -8;"
2401 "r2 = r10;"
2402 "r2 += -16;"
2403 "call forwarding_rw;"
2404 /* Call 2: write_first_read_second(&fp[-16], &fp[-8]) */
2405 "r1 = r10;"
2406 "r1 += -16;"
2407 "r2 = r10;"
2408 "r2 += -8;"
2409 "call forwarding_rw;"
2410 "r0 = 0;"
2411 "exit;"
2412 ::: __clobber_all);
2413}
2414
2415static __used __naked void forwarding_rw(void)
2416{
2417 asm volatile (
2418 "call write_first_read_second;"
2419 "exit;"
2420 ::: __clobber_all);
2421}
2422
2423static __used __naked void write_first_read_second(void)
2424{
2425 asm volatile (
2426 "*(u64 *)(r1 + 0) = 42;"
2427 "r0 = *(u64 *)(r2 + 0);"
2428 "exit;"
2429 ::: __clobber_all);
2430}
2431
2432/*
2433 * Shared must_write when (callsite, depth) instance is reused.
2434 * Main calls fwd_to_stale_wr at two sites. fwd_to_stale_wr calls
2435 * stale_wr_leaf at a single internal callsite. Both calls share
2436 * stale_wr_leaf's (callsite, depth) instance.
2437 *
2438 * Call 1: stale_wr_leaf(map_value, fp-8) writes map, reads fp-8.
2439 * Call 2: stale_wr_leaf(fp-8, fp-8) writes fp-8, reads fp-8.
2440 *
2441 * The analysis can't presume that stale_wr_leaf() always writes fp-8,
2442 * it must conservatively join must_write masks computed for both calls.
2443 */
2444SEC("socket")
2445__success
2446__naked void stale_must_write_cross_callsite(void)
2447{
2448 asm volatile (
2449 "*(u64 *)(r10 - 8) = 0;"
2450 /* Call 1: map_value write, fp-8 read (processed second in PO) */
2451 "*(u32 *)(r10 - 16) = 0;"
2452 "r1 = %[map] ll;"
2453 "r2 = r10;"
2454 "r2 += -16;"
2455 "call %[bpf_map_lookup_elem];"
2456 "if r0 == 0 goto 1f;"
2457 "r1 = r0;"
2458 "r2 = r10;"
2459 "r2 += -8;"
2460 "call fwd_to_stale_wr;"
2461 /* Call 2: fp-8 write, fp-8 read (processed first in PO) */
2462 "r1 = r10;"
2463 "r1 += -8;"
2464 "r2 = r1;"
2465 "call fwd_to_stale_wr;"
2466"1:"
2467 "r0 = 0;"
2468 "exit;"
2469 :: __imm_addr(map),
2470 __imm(bpf_map_lookup_elem)
2471 : __clobber_all);
2472}
2473
2474static __used __naked void fwd_to_stale_wr(void)
2475{
2476 asm volatile (
2477 "call stale_wr_leaf;"
2478 "exit;"
2479 ::: __clobber_all);
2480}
2481
2482static __used __naked void stale_wr_leaf(void)
2483{
2484 asm volatile (
2485 "*(u64 *)(r1 + 0) = 42;"
2486 "r0 = *(u64 *)(r2 + 0);"
2487 "exit;"
2488 ::: __clobber_all);
2489}
2490
2491#ifdef CAN_USE_LOAD_ACQ_STORE_REL
2492
2493SEC("socket")
2494__log_level(2)
2495__success
2496__msg("*(u64 *)(r0 +0) = 42 ; def: fp0-16")
2497__naked void load_acquire_dont_clear_dst(void)
2498{
2499 asm volatile (
2500 "r0 = r10;"
2501 "r0 += -16;"
2502 "*(u64 *)(r0 + 0) = r0;" /* fp[-16] == &fp[-16] */
2503 ".8byte %[load_acquire_insn];" /* load_acquire is a special case for BPF_STX, */
2504 "r0 = *(u64 *)(r10 - 16);" /* it shouldn't clear tracking info for */
2505 "*(u64 *)(r0 + 0) = 42;" /* dst register, r0 in this case. */
2506 "r0 = 0;"
2507 "exit;"
2508 :
2509 : __imm_insn(load_acquire_insn,
2510 BPF_ATOMIC_OP(BPF_DW, BPF_LOAD_ACQ, BPF_REG_0, BPF_REG_0, 0))
2511 : __clobber_all);
2512}
2513
2514#endif /* CAN_USE_LOAD_ACQ_STORE_REL */
2515
2516SEC("socket")
2517__success
2518__naked void imprecise_fill_loses_cross_frame(void)
2519{
2520 asm volatile (
2521 "*(u64 *)(r10 - 8) = 0;"
2522 "r1 = r10;"
2523 "r1 += -8;"
2524 "call imprecise_fill_cross_frame;"
2525 "exit;"
2526 ::: __clobber_all);
2527}
2528
2529static __used __naked void imprecise_fill_cross_frame(void)
2530{
2531 asm volatile (
2532 /* spill &caller_fp-8 to callee's fp-8 */
2533 "*(u64 *)(r10 - 8) = r1;"
2534 /* imprecise FP pointer in r1 */
2535 "r1 = r10;"
2536 "r2 = -8;"
2537 "r1 += r2;"
2538 /* load from imprecise offset. fill_from_stack returns
2539 * ARG_IMPRECISE{mask=BIT(1)}, losing frame 0
2540 */
2541 "r1 = *(u64 *)(r1 + 0);"
2542 /* read caller's fp-8 through loaded pointer, should mark fp0-8 live */
2543 "r0 = *(u64 *)(r1 + 0);"
2544 "r0 = 0;"
2545 "exit;"
2546 :: __imm(bpf_get_prandom_u32)
2547 : __clobber_all);
2548}
2549
2550/* Test that spill_to_stack with multi-offset dst (sz=8) joins instead
2551 * of overwriting. r1 has offsets [-8, -16]. Both slots hold FP-derived
2552 * pointers. Writing through r1 should join *val with existing values,
2553 * not destroy them.
2554 *
2555 * fp-8 = &fp-24
2556 * fp-16 = &fp-32
2557 * r1 = fp-8 or fp-16 (two offsets from branch)
2558 * *(u64 *)(r1 + 0) = &fp-24 -- writes to one slot, other untouched
2559 * r0 = *(u64 *)(r10 - 16) -- fill from fp-16
2560 * r0 = *(u64 *)(r0 + 0) -- deref: should produce use
2561 */
2562SEC("socket")
2563__log_level(2)
2564__success
2565__msg("20: (79) r0 = *(u64 *)(r10 -16)")
2566__msg("21: (79) r0 = *(u64 *)(r0 +0) ; use: fp0-24 fp0-32")
2567__naked void spill_join_with_multi_off(void)
2568{
2569 asm volatile (
2570 /* fp-8 = &fp-24, fp-16 = &fp-32 (different pointers) */
2571 "*(u64 *)(r10 - 24) = 0;"
2572 "*(u64 *)(r10 - 32) = 0;"
2573 "r1 = r10;"
2574 "r1 += -24;"
2575 "*(u64 *)(r10 - 8) = r1;"
2576 "r1 = r10;"
2577 "r1 += -32;"
2578 "*(u64 *)(r10 - 16) = r1;"
2579 /* create r1 with two candidate offsets: fp-8 or fp-16 */
2580 "call %[bpf_get_prandom_u32];"
2581 "if r0 == 0 goto 1f;"
2582 "r1 = r10;"
2583 "r1 += -8;"
2584 "goto 2f;"
2585"1:"
2586 "r1 = r10;"
2587 "r1 += -16;"
2588"2:"
2589 /* write &fp-24 through multi-offset r1: hits one slot, other untouched */
2590 "r2 = r10;"
2591 "r2 += -24;"
2592 "*(u64 *)(r1 + 0) = r2;"
2593 /* read back *fp-8 and *fp-16 */
2594 "r0 = *(u64 *)(r10 - 8);"
2595 "r0 = *(u64 *)(r0 + 0);"
2596 "r0 = *(u64 *)(r10 - 16);"
2597 "r0 = *(u64 *)(r0 + 0);"
2598 "exit;"
2599 :: __imm(bpf_get_prandom_u32)
2600 : __clobber_all);
2601}
2602
2603/* Test that spill_to_stack with imprecise dst (off_cnt == 0, sz=8)
2604 * joins instead of overwriting. Use "r2 = -8; r1 += r2" to make
2605 * arg tracking lose offset precision while the main verifier keeps
2606 * r1 as PTR_TO_STACK with fixed offset. Both slots hold FP-derived
2607 * pointers. Writing through r1 should join *val with existing
2608 * values, not destroy them.
2609 *
2610 * fp-8 = &fp-24
2611 * fp-16 = &fp-32
2612 * r1 = fp-8 (imprecise to arg tracking)
2613 * *(u64 *)(r1 + 0) = &fp-24 -- since r1 is imprecise, this adds &fp-24
2614 * to the set of possible values for all slots,
2615 * hence the values at fp-16 become [fp-24, fp-32]
2616 * r0 = *(u64 *)(r10 - 16)
2617 * r0 = *(u64 *)(r0 + 0) -- deref: should produce use of fp-24 or fp-32
2618 */
2619SEC("socket")
2620__log_level(2)
2621__success
2622__msg("15: (79) r0 = *(u64 *)(r0 +0) ; use: fp0-24 fp0-32")
2623__naked void spill_join_with_imprecise_off(void)
2624{
2625 asm volatile (
2626 "*(u64 *)(r10 - 24) = 0;"
2627 "*(u64 *)(r10 - 32) = 0;"
2628 "r1 = r10;"
2629 "r1 += -24;"
2630 "*(u64 *)(r10 - 8) = r1;"
2631 "r1 = r10;"
2632 "r1 += -32;"
2633 "*(u64 *)(r10 - 16) = r1;"
2634 /* r1 = fp-8 but arg tracking sees off_cnt == 0 */
2635 "r1 = r10;"
2636 "r2 = -8;"
2637 "r1 += r2;"
2638 /* write through imprecise r1 */
2639 "r3 = r10;"
2640 "r3 += -24;"
2641 "*(u64 *)(r1 + 0) = r3;"
2642 /* read back fp-16: at_stack should still track &fp-32 */
2643 "r0 = *(u64 *)(r10 - 16);"
2644 /* deref: should produce use for fp-32 */
2645 "r0 = *(u64 *)(r0 + 0);"
2646 "r0 = 0;"
2647 "exit;"
2648 ::: __clobber_all);
2649}
2650
2651/*
2652 * Same as spill_join_with_multi_off but the write is BPF_ST (store
2653 * immediate) instead of BPF_STX. BPF_ST goes through
2654 * clear_stack_for_all_offs() rather than spill_to_stack(), and that
2655 * path also needs to join instead of overwriting.
2656 *
2657 * fp-8 = &fp-24
2658 * fp-16 = &fp-32
2659 * r1 = fp-8 or fp-16 (two offsets from branch)
2660 * *(u64 *)(r1 + 0) = 0 -- BPF_ST with immediate
2661 * r0 = *(u64 *)(r10 - 16) -- fill from fp-16
2662 * r0 = *(u64 *)(r0 + 0) -- deref: should produce use
2663 */
2664SEC("socket")
2665__log_level(2)
2666__failure
2667__msg("15: (7a) *(u64 *)(r1 +0) = 0 fp-8: fp0-24 -> fp0-24|fp0+0 fp-16: fp0-32 -> fp0-32|fp0+0")
2668__msg("17: (79) r0 = *(u64 *)(r0 +0) ; use: fp0-32")
2669__naked void st_imm_join_with_multi_off(void)
2670{
2671 asm volatile (
2672 "*(u64 *)(r10 - 24) = 0;"
2673 "*(u64 *)(r10 - 32) = 0;"
2674 "r1 = r10;"
2675 "r1 += -24;"
2676 "*(u64 *)(r10 - 8) = r1;"
2677 "r1 = r10;"
2678 "r1 += -32;"
2679 "*(u64 *)(r10 - 16) = r1;"
2680 /* create r1 with two candidate offsets: fp-8 or fp-16 */
2681 "call %[bpf_get_prandom_u32];"
2682 "if r0 == 0 goto 1f;"
2683 "r1 = r10;"
2684 "r1 += -8;"
2685 "goto 2f;"
2686"1:"
2687 "r1 = r10;"
2688 "r1 += -16;"
2689"2:"
2690 /* BPF_ST: store immediate through multi-offset r1 */
2691 "*(u64 *)(r1 + 0) = 0;"
2692 /* read back fp-16 and deref */
2693 "r0 = *(u64 *)(r10 - 16);"
2694 "r0 = *(u64 *)(r0 + 0);"
2695 "r0 = 0;"
2696 "exit;"
2697 :: __imm(bpf_get_prandom_u32)
2698 : __clobber_all);
2699}
2700
2701/*
2702 * Check that BPF_ST with a known offset fully overwrites stack slot
2703 * from the arg tracking point of view.
2704 */
2705SEC("socket")
2706__log_level(2)
2707__success
2708__msg("5: (7a) *(u64 *)(r1 +0) = 0 fp-8: fp0-16 -> _{{$}}")
2709__naked void st_imm_join_with_single_off(void)
2710{
2711 asm volatile (
2712 "r2 = r10;"
2713 "r2 += -16;"
2714 "*(u64 *)(r10 - 8) = r2;"
2715 "r1 = r10;"
2716 "r1 += -8;"
2717 "*(u64 *)(r1 + 0) = 0;"
2718 "r0 = 0;"
2719 "exit;"
2720 ::: __clobber_all);
2721}
2722
2723/*
2724 * Same as spill_join_with_imprecise_off but the write is BPF_ST.
2725 * Use "r2 = -8; r1 += r2" to make arg tracking lose offset
2726 * precision while the main verifier keeps r1 as fixed-offset.
2727 *
2728 * fp-8 = &fp-24
2729 * fp-16 = &fp-32
2730 * r1 = fp-8 (imprecise to arg tracking)
2731 * *(u64 *)(r1 + 0) = 0 -- BPF_ST with immediate
2732 * r0 = *(u64 *)(r10 - 16) -- fill from fp-16
2733 * r0 = *(u64 *)(r0 + 0) -- deref: should produce use
2734 */
2735SEC("socket")
2736__log_level(2)
2737__success
2738__msg("13: (79) r0 = *(u64 *)(r0 +0) ; use: fp0-32")
2739__naked void st_imm_join_with_imprecise_off(void)
2740{
2741 asm volatile (
2742 "*(u64 *)(r10 - 24) = 0;"
2743 "*(u64 *)(r10 - 32) = 0;"
2744 "r1 = r10;"
2745 "r1 += -24;"
2746 "*(u64 *)(r10 - 8) = r1;"
2747 "r1 = r10;"
2748 "r1 += -32;"
2749 "*(u64 *)(r10 - 16) = r1;"
2750 /* r1 = fp-8 but arg tracking sees off_cnt == 0 */
2751 "r1 = r10;"
2752 "r2 = -8;"
2753 "r1 += r2;"
2754 /* store immediate through imprecise r1 */
2755 "*(u64 *)(r1 + 0) = 0;"
2756 /* read back fp-16 */
2757 "r0 = *(u64 *)(r10 - 16);"
2758 /* deref: should produce use */
2759 "r0 = *(u64 *)(r0 + 0);"
2760 "r0 = 0;"
2761 "exit;"
2762 ::: __clobber_all);
2763}
2764
2765/*
2766 * Test that spilling through an ARG_IMPRECISE pointer joins with
2767 * existing at_stack values. Subprog receives r1 = fp0-24 and
2768 * r2 = map_value, creates an ARG_IMPRECISE pointer by joining caller
2769 * and callee FP on two branches.
2770 *
2771 * Setup: callee spills &fp1-16 to fp1-8 (precise, tracked).
2772 * Then writes map_value through ARG_IMPRECISE r1 — on path A
2773 * this hits fp1-8, on path B it hits caller stack.
2774 * Since spill_to_stack is skipped for ARG_IMPRECISE dst,
2775 * fp1-8 tracking isn't joined with none.
2776 *
2777 * Expected after the imprecise write:
2778 * - arg tracking should show fp1-8 = fp1-16|fp1+0 (joined with none)
2779 * - read from fp1-8 and deref should produce use for fp1-16
2780 * - write through it should NOT produce def for fp1-16
2781 */
2782SEC("socket")
2783__log_level(2)
2784__success
2785__msg("26: (79) r0 = *(u64 *)(r10 -8) // r1=IMP3 r6=fp0-24 r7=fp1-16 fp-8=fp1-16|fp1+0")
2786__naked void imprecise_dst_spill_join(void)
2787{
2788 asm volatile (
2789 "*(u64 *)(r10 - 24) = 0;"
2790 /* map lookup for a valid non-FP pointer */
2791 "*(u32 *)(r10 - 32) = 0;"
2792 "r1 = %[map] ll;"
2793 "r2 = r10;"
2794 "r2 += -32;"
2795 "call %[bpf_map_lookup_elem];"
2796 "if r0 == 0 goto 1f;"
2797 /* r1 = &caller_fp-24, r2 = map_value */
2798 "r1 = r10;"
2799 "r1 += -24;"
2800 "r2 = r0;"
2801 "call imprecise_dst_spill_join_sub;"
2802"1:"
2803 "r0 = 0;"
2804 "exit;"
2805 :: __imm_addr(map),
2806 __imm(bpf_map_lookup_elem)
2807 : __clobber_all);
2808}
2809
2810static __used __naked void imprecise_dst_spill_join_sub(void)
2811{
2812 asm volatile (
2813 /* r6 = &caller_fp-24 (frame=0), r8 = map_value */
2814 "r6 = r1;"
2815 "r8 = r2;"
2816 /* spill &fp1-16 to fp1-8: at_stack[0] = fp1-16 */
2817 "*(u64 *)(r10 - 16) = 0;"
2818 "r7 = r10;"
2819 "r7 += -16;"
2820 "*(u64 *)(r10 - 8) = r7;"
2821 /* branch to create ARG_IMPRECISE pointer */
2822 "call %[bpf_get_prandom_u32];"
2823 /* path B: r1 = caller fp-24 (frame=0) */
2824 "r1 = r6;"
2825 "if r0 == 0 goto 1f;"
2826 /* path A: r1 = callee fp-8 (frame=1) */
2827 "r1 = r10;"
2828 "r1 += -8;"
2829"1:"
2830 /* r1 = ARG_IMPRECISE{mask=BIT(0)|BIT(1)}.
2831 * Write map_value (non-FP) through r1. On path A this overwrites fp1-8.
2832 * Should join at_stack[0] with none: fp1-16|fp1+0.
2833 */
2834 "*(u64 *)(r1 + 0) = r8;"
2835 /* read fp1-8: should be fp1-16|fp1+0 (joined) */
2836 "r0 = *(u64 *)(r10 - 8);"
2837 "*(u64 *)(r0 + 0) = 42;"
2838 "r0 = 0;"
2839 "exit;"
2840 :: __imm(bpf_get_prandom_u32)
2841 : __clobber_all);
2842}