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