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