Linux kernel mirror (for testing) git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
kernel os linux

bpf: properly verify tail call behavior

A successful ebpf tail call does not return to the caller, but to the
caller-of-the-caller, often just finishing the ebpf program altogether.

Any restrictions that the verifier needs to take into account - notably
the fact that the tail call might have modified packet pointers - are to
be checked on the caller-of-the-caller. Checking it on the caller made
the verifier refuse perfectly fine programs that would use the packet
pointers after a tail call, which is no problem as this code is only
executed if the tail call was unsuccessful, i.e. nothing happened.

This patch simulates the behavior of a tail call in the verifier. A
conditional jump to the code after the tail call is added for the case
of an unsucessful tail call, and a return to the caller is simulated for
a successful tail call.

For the successful case we assume that the tail call returns an int,
as tail calls are currently only allowed in functions that return and
int. We always assume that the tail call modified the packet pointers,
as we do not know what the tail call did.

For the unsuccessful case we know nothing happened, so we do not need to
add new constraints.

This approach also allows to check other problems that may occur with
tail calls, namely we are now able to check that precision is properly
propagated into subprograms using tail calls, as well as checking the
live slots in such a subprogram.

Fixes: 1a4607ffba35 ("bpf: consider that tail calls invalidate packet pointers")
Link: https://lore.kernel.org/bpf/20251029105828.1488347-1-martin.teichmann@xfel.eu/
Signed-off-by: Martin Teichmann <martin.teichmann@xfel.eu>
Acked-by: Eduard Zingerman <eddyz87@gmail.com>
Link: https://lore.kernel.org/r/20251119160355.1160932-2-martin.teichmann@xfel.eu
Signed-off-by: Alexei Starovoitov <ast@kernel.org>

authored by

Martin Teichmann and committed by
Alexei Starovoitov
e3245f89 4dd3a48d

+28 -3
+28 -3
kernel/bpf/verifier.c
··· 4438 4438 bt_reg_mask(bt)); 4439 4439 return -EFAULT; 4440 4440 } 4441 + if (insn->src_reg == BPF_REG_0 && insn->imm == BPF_FUNC_tail_call 4442 + && subseq_idx - idx != 1) { 4443 + if (bt_subprog_enter(bt)) 4444 + return -EFAULT; 4445 + } 4441 4446 } else if (opcode == BPF_EXIT) { 4442 4447 bool r0_precise; 4443 4448 ··· 11069 11064 bool in_callback_fn; 11070 11065 int err; 11071 11066 11067 + err = bpf_update_live_stack(env); 11068 + if (err) 11069 + return err; 11070 + 11072 11071 callee = state->frame[state->curframe]; 11073 11072 r0 = &callee->regs[BPF_REG_0]; 11074 11073 if (r0->type == PTR_TO_STACK) { ··· 11977 11968 if (check_get_func_ip(env)) 11978 11969 return -ENOTSUPP; 11979 11970 env->prog->call_get_func_ip = true; 11971 + } 11972 + 11973 + if (func_id == BPF_FUNC_tail_call) { 11974 + if (env->cur_state->curframe) { 11975 + struct bpf_verifier_state *branch; 11976 + 11977 + mark_reg_scratched(env, BPF_REG_0); 11978 + branch = push_stack(env, env->insn_idx + 1, env->insn_idx, false); 11979 + if (IS_ERR(branch)) 11980 + return PTR_ERR(branch); 11981 + clear_all_pkt_pointers(env); 11982 + mark_reg_unknown(env, regs, BPF_REG_0); 11983 + err = prepare_func_exit(env, &env->insn_idx); 11984 + if (err) 11985 + return err; 11986 + env->insn_idx--; 11987 + } else { 11988 + changes_data = false; 11989 + } 11980 11990 } 11981 11991 11982 11992 if (changes_data) ··· 20174 20146 return PROCESS_BPF_EXIT; 20175 20147 20176 20148 if (env->cur_state->curframe) { 20177 - err = bpf_update_live_stack(env); 20178 - if (err) 20179 - return err; 20180 20149 /* exit from nested function */ 20181 20150 err = prepare_func_exit(env, &env->insn_idx); 20182 20151 if (err)