From: Alexei Starovoitov A fix of liveness.c. This series needs it: scalar evolution keeps the expression of a stack slot across a call unless bpf_may_write_mask() of the call has the slot, and this is a write that the mask does not have. struct scope { struct scope *parent; u64 hits; }; static __noinline int leaf(struct scope *up) { for (s = up; s && n < 8; s = s->parent, n++) s->hits++; .. } static __noinline int level2(struct scope *up) { struct scope me = { .parent = up }; return leaf(&me) + me.hits; } level1() is like level2() and calls it. The program has the first scope and calls level1(). In leaf(), frame 3, s points to frame 2, then to frame 1: at the loop header struct arg_track is ARG_IMPRECISE with the mask BIT(1) | BIT(2). For the load of s->parent arg_track_deref() returns the pointer that it was given, so for liveness.c s never points to frame 0. The scope of the program gets neither stack_use nor may_def at the call of level1(), and the program is rejected: reading from stack off -16+0 size 8, slot poisoned by dead code elimination put(cond ? out : &tmp) with out in frame 1, tmp in frame 2 and out->buf in frame 0 has such a pointer without a loop. That program loads, but the call in frame 0 has no may_def for buf. A frame holds pointers to its own stack and to the stacks of its callers: a pointer to the stack is never spilled into the frame of a caller. So what is loaded through a pointer to one of the frames in the mask points to the youngest of them or to an older one. fill_from_stack() does the same for a known frame and an unknown offset: GENMASK(frame, 0). Fixes: bf0c571f7feb ("bpf: introduce forward arg-tracking dataflow analysis") Signed-off-by: Alexei Starovoitov --- kernel/bpf/liveness.c | 4 +- .../selftests/bpf/progs/verifier_live_stack.c | 60 +++++++++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/kernel/bpf/liveness.c b/kernel/bpf/liveness.c index e930d928afa5..c52f0435193b 100644 --- a/kernel/bpf/liveness.c +++ b/kernel/bpf/liveness.c @@ -1398,8 +1398,10 @@ static struct arg_track arg_track_deref(struct bpf_verifier_env *env, struct bpf return fill_from_stack(insn, at_out, reg, parent->at, parent->slots, ptr->frame); } + /* A frame holds pointers to its own stack and to the stacks of its callers */ if (ptr->frame == ARG_IMPRECISE && !(ptr->mask & BIT(depth)) && ptr->mask) - return *ptr; + return (struct arg_track){ .mask = GENMASK(fls(ptr->mask) - 1, 0), + .frame = ARG_IMPRECISE }; return (struct arg_track){ .frame = ARG_NONE }; } diff --git a/tools/testing/selftests/bpf/progs/verifier_live_stack.c b/tools/testing/selftests/bpf/progs/verifier_live_stack.c index 32bab3cb3fcb..d6a6420d219d 100644 --- a/tools/testing/selftests/bpf/progs/verifier_live_stack.c +++ b/tools/testing/selftests/bpf/progs/verifier_live_stack.c @@ -3143,6 +3143,66 @@ static __used __naked void imprecise_fill_cross_frame(void) : __clobber_all); } +/* + * fill_older_frame_3() loads through a pointer to the stack of frame 1 or of frame 2. Both slots + * hold a pointer to fp-8 of frame 0: the store is a may_def of that slot at the call in frame 0. + */ +SEC("socket") +__log_level(2) +__success +__msg("3: (85) call pc+2{{ +}}; may_def: fp0-8..-{{[0-9]+}}") +__naked void imprecise_parent_fill_loses_older_frame(void) +{ + asm volatile ( + "*(u64 *)(r10 - 8) = 0;" + "r1 = r10;" + "r1 += -8;" + "call fill_older_frame_1;" + "r0 = *(u64 *)(r10 - 8);" + "exit;" + ::: __clobber_all); +} + +static __used __naked void fill_older_frame_1(void) +{ + asm volatile ( + "*(u64 *)(r10 - 8) = r1;" + "r1 = r10;" + "r1 += -8;" + "call fill_older_frame_2;" + "exit;" + ::: __clobber_all); +} + +static __used __naked void fill_older_frame_2(void) +{ + asm volatile ( + "r2 = *(u64 *)(r1 + 0);" + "*(u64 *)(r10 - 8) = r2;" + "r2 = r10;" + "r2 += -8;" + "call fill_older_frame_3;" + "exit;" + ::: __clobber_all); +} + +static __used __naked void fill_older_frame_3(void) +{ + asm volatile ( + "r6 = r1;" + "r7 = r2;" + "call %[bpf_get_prandom_u32];" + "if r0 == 0 goto +1;" + "r6 = r7;" + /* ARG_IMPRECISE{mask=BIT(1)|BIT(2)} */ + "r1 = *(u64 *)(r6 + 0);" + "*(u64 *)(r1 + 0) = 7;" + "r0 = 0;" + "exit;" + :: __imm(bpf_get_prandom_u32) + : __clobber_all); +} + /* Test that spill_to_stack with multi-offset dst (sz=8) joins instead * of overwriting. r1 has offsets [-8, -16]. Both slots hold FP-derived * pointers. Writing through r1 should join *val with existing values, -- 2.55.0