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