Repository navigation
bpf: Unwinding with unwind_goto and unwind_exit - #14435
kernel-patches-daemon-bpf[bot] wants to merge 5 commits into
Conversation
|
Upstream branch: 4f84263 |
AI reviewed your patch. Please fix the bug or email reply why it's not a bug. In-Reply-To-Subject: |
|
Forwarding comment 6260714011 via email |
AI reviewed your patch. Please fix the bug or email reply why it's not a bug. In-Reply-To-Subject: |
|
Forwarding comment 6260757822 via email |
c28fbec to
77b7c10
Compare
|
Upstream branch: 868e56d |
a613396 to
c0b51dc
Compare
77b7c10 to
d75c0e9
Compare
|
Upstream branch: 4f84263 |
c0b51dc to
0dd3cab
Compare
d75c0e9 to
f6374da
Compare
|
Upstream branch: 25bb03f |
0dd3cab to
9b58567
Compare
f6374da to
4eb7180
Compare
|
Upstream branch: 0867b7b |
9b58567 to
c2cce6f
Compare
4eb7180 to
d799ab0
Compare
|
Upstream branch: 1907a7d |
c2cce6f to
0d33786
Compare
d799ab0 to
5b8b9d6
Compare
Rust Drop, C++ destructors and __attribute__((cleanup)) in C have to run
when a panic or an exception passes through a frame.
v1 [1] did it with a flag in r3 and 'if r3 != 0 goto pad' after the call.
Three bugs that the bots found there had the same cause: r3 is an ordinary
register, and JITs guessed the meaning from the shape of the code.
Kumar suggested new insns instead [2]:
unwind_goto pad BPF_JMP | BPF_JCOND, src_reg = BPF_UNWIND_GOTO
unwind_exit BPF_JMP | BPF_EXIT, src_reg = BPF_UNWIND_EXIT
'call foo; unwind_goto pad' goes to the pad when foo left with unwind_exit
and falls through when it left with exit. No register says which one it
was. In everything else unwind_exit is exit.
llvm lowers unwinding to them [3]:
ret exit
invoke foo unwind pad call foo; unwind_goto pad
resume r0 = 0; unwind_exit
call _Unwind_RaiseException r0 = 0; unwind_exit
invoke _Unwind_RaiseException goto pad
Landing pads are ordinary blocks, so the verifier checks them like any
other code. A pad that forgets to drop a lock or a reference is a leak on
that path. There are no tables, no stack walk, no kfuncs, no BTF and
nothing to do for libbpf and other loaders.
In the verifier:
- unwind_goto has to follow a call or callx of a subprog and cannot be
a jump target. For cfg, liveness and the rest it's one more conditional
jump, like may_goto.
- How the callee left is one bit of the verifier state. It lives from
the exit of the callee to the next insn. states_equal() looks at it,
since the registers can be the same either way.
- unwind_exit is rejected when unwind_goto doesn't follow the call.
- A global subprog that has unwind_exit can leave either way. Both are
explored after the call, like it's done for might_throw.
- Callbacks cannot unwind: sync, async and exception ones. Whoever calls
them doesn't expect that.
- unwind_exit of the main prog is exit.
- Of tracing progs only fentry can attach to a subprog that has
unwind_exit. It would return to the trampoline that called it.
- unwind_goto goes away together with a pad that turned out to be dead
code. It stays when what follows it is dead code.
JITs don't emit a branch:
unwind_goto:
extable[n++] = { ret_addr - 1 -> pad }
unwind_exit:
my_ret_addr = bpf_unwind_target(my_ret_addr)
epilogue
So exit and the normal path after the call cost nothing. Every frame fixes
only its own return address. There is no stack walk. The lookup is what
the page fault handler does for bpf progs.
The interpreter cannot do that, so the prog has to be JITed, and JITs opt
in with bpf_jit_supports_unwind(). x86-64 and arm64 are in the next
patches.
Compared to v1 there is no BTF decl_tag and no 'r3 = 0' before every exit.
Tail call and ld_abs are fine in a subprog that can unwind, since they
leave it like exit does. So is freplace: an extension leaves with exit and
the callers are ready for that.
In [4] I wrote that unwind_exit sets r0 = 0. It doesn't. An implicit
definition of r0 needs special cases in backtracking, in liveness and in
the check of the return value of the main prog. A plain 'r0 = 0' in front
of it needs none.
Rust
rust-bpf [5] builds with that llvm as it is. The programs are built with
-C panic=unwind. The panic handler prints the message and calls
_Unwind_RaiseException().
exc_cleanup.rs panics three frames deep, with preemption disabled in one
frame and rcu_read_lock held in another. All Drop impls run and the prog
returns 0.
scx_simple.rs is 2195 insns with 48 unwind_goto, 20 of them after callx,
and 16 unwind_exit. With r3 it was 2216 insns. scx_cosmos.rs is 3694 insns
with 68 and 18. It was 3716.
C++
Exceptions work too. This compiles, loads and runs:
struct rcu_guard {
rcu_guard() { bpf_rcu_read_lock(); }
~rcu_guard() { bpf_rcu_read_unlock(); }
};
static __noinline int inner(int len)
{
rcu_guard guard;
if (len > 100)
throw 1;
return len;
}
SEC("tc") int prog(struct __sk_buff *skb)
{
try {
return inner(skb->len);
} catch (...) {
return 42;
}
}
It returns 64 for a packet of 64 bytes and 42 for a packet of 200 bytes.
The destructor runs in both cases, otherwise the verifier would reject
the prog. 'throw' is a jump to the pad with the destructor. 'catch' is
unwind_goto:
inner:
w6 = w1
call bpf_rcu_read_lock
if w6 s< 101 goto ret
r1 = exc_obj ll
*(u32 *)(r1 + 0) = 1
goto pad
ret:
call bpf_rcu_read_unlock
w0 = w6
exit
pad:
call bpf_rcu_read_unlock
r0 = 0
unwind_exit
prog:
w6 = *(u32 *)(r1 + 0)
w1 = w6
call inner
unwind_goto catch
out:
w0 = w6
exit
catch:
w6 = 42
goto out
It needs -fexceptions -funwind-tables and four one line __cxa_*()
functions, where __cxa_throw() calls _Unwind_RaiseException().
Every pad is entered whatever was thrown, so llvm takes catch (...) only.
It's a selftest in the last patch.
[1] https://lore.kernel.org/bpf/20261018010246.1643877-1-alexei.starovoitov@gmail.com/
[2] https://lore.kernel.org/bpf/DM7KR7J8VY2T.1CZIRUF6QRCWG@gmail.com/
[3] llvm/llvm-project#233140
[4] https://lore.kernel.org/bpf/DM8G3O0ZTUM9.MMBWXXQ7OP98@gmail.com/
[5] https://github.com/4ast/rust-bpf
Suggested-by: Kumar Kartikeya Dwivedi <memxor@gmail.com>
Signed-off-by: Alexei Starovoitov <ast@kernel.org>
|
Upstream branch: f25f631 |
bpf x86
call inner call inner
unwind_goto pad
w0 = w6 mov eax, ebx
r0 = 0 xor eax, eax
unwind_exit push rax
push rsi
mov rdi, [rbp + 8]
call bpf_unwind_target
mov [rbp + 8], rax
pop rsi
pop rax
leave
ret
unwind_goto is an extable entry and no code. unwind_exit looks its return
address up there and returns to the pad. r0 and r2 are what a subprog
returns, so rax and rsi are saved around the lookup.
The caller of the main prog doesn't tell exit from unwind_exit, so there
it's exit.
With private stack r9 is saved around a call, and 'pop r9' has to be done
on the way to the pad too. In such progs unwind_goto is:
push r9
call foo
pop r9 <- exit returns here
jmp 1f
pop r9 <- unwind_exit returns here
jmp pad
1:
Signed-off-by: Alexei Starovoitov <ast@kernel.org>
bpf arm64
call inner bl inner
mov x8, x0
unwind_goto pad
w0 = w6 mov w8, w19
Like on x86 unwind_goto is an extable entry and no code.
unwind_exit returns to the pad past 'mov x8, x0'. That's fine: r0 is x8,
and the epilogue of the callee copies it to x0 and leaves x8 as it is.
unwind_exit fixes LR in its frame record. With pointer authentication it's
signed with SP at the entry of the prog, which is FP + 16:
stp x8, x1, [sp, #-16]!
ldr x17, [x29, #8]
add x16, x29, #16
autia1716
mov x0, x17
bl bpf_unwind_target
mov x17, x0
add x16, x29, #16
pacia1716
str x17, [x29, #8]
ldp x8, x1, [sp], #16
So the lookup gets an authenticated address, and autiasp in the epilogue
checks the new one. autia1716 and pacia1716 are hints like paciasp and
autiasp: nops when the cpu doesn't have pointer authentication.
RET is not subject to BTI.
r0 and r2 are what a subprog returns, so x8 and x1 are saved around the
lookup.
The private stack pointer is in a callee saved register. Unlike x86 there
is nothing to undo after the call.
Tested in qemu with -cpu max and -cpu cortex-a72, i.e. with and without
pointer authentication.
Signed-off-by: Alexei Starovoitov <ast@kernel.org>
The tests spell the two insns out as raw insns, so they don't depend on llvm version. verifier_unwind: - A static subprog, a global one and a callee of callx leave either way at run time, through one and two frames. The verifier follows a static subprog, so the tests derive the argument from skb->len or read it from a map. - r0 of unwind_exit, r6-r9 and the stack are there in the pad. r1-r5 are not readable. - A pad is checked like any other code: it has to drop rcu_read_lock. - How the callee left is a part of the verifier state: two states that differ only in that are not pruned. - The pad of a call of a global subprog is always checked. - A callee that never returns, and one that never unwinds, where the pad goes away. - unwind_exit without unwind_goto after the call. - unwind_goto that doesn't follow a call of a subprog or is a jump target. - Reserved fields. - A callback cannot unwind. What it calls can. - Tail call and ld_abs in a subprog that can unwind. - unwind_exit of the main prog is exit. - What x86-64 and arm64 JITs emit. - Private stack. On x86-64 r9 has to be restored on the way to the pad. The last one has to look at the return value of a prog that uses private stack. raw_tp is such a prog. Its test_run doesn't take a packet, so let test_loader run it like it runs syscall progs. unwind_attach: fentry and freplace are attached to a subprog that unwinds and the caller is run. fexit is refused. Signed-off-by: Alexei Starovoitov <ast@kernel.org>
static __noinline int inner(int len)
{
rcu_guard guard;
if (len > 100)
throw 1;
return len;
}
static __noinline int middle(int len)
{
preempt_guard guard;
return inner(len) + 1;
}
SEC("tc") int catches(struct __sk_buff *skb)
{
try {
return middle(skb->len);
} catch (...) {
return 42;
}
}
The exception passes through two frames. The destructors call
bpf_rcu_read_unlock() and bpf_preempt_enable(). The verifier insists on
both, and the test checks that they ran in that order.
In the second prog nobody catches. The destructors run and the prog
returns 0.
All that is left of the C++ runtime is four one line functions.
__cxa_throw() calls _Unwind_RaiseException(), which llvm lowers.
progs/*.cpp are built like progs/*.rs: by clang only and without skeletons.
vmlinux.h and bpf_helpers.h are not C++, so the prog declares what it
needs.
clang that lowers unwinding [1] defines __BPF_FEATURE_UNWIND. Without it
the object has no progs and the test is skipped.
[1] llvm/llvm-project#233140
Signed-off-by: Alexei Starovoitov <ast@kernel.org>
0d33786 to
8c4f47d
Compare
Pull request for series with
subject: bpf: Unwinding with unwind_goto and unwind_exit
version: 2
url: https://patchwork.kernel.org/series/1188607/