eBPF Summit Day 1 Recap(cilium.io) |
eBPF Summit Day 1 Recap(cilium.io) |
(Bounded loops are really fussy anyways and you probably won't use them in most programs, so writing in a single language rather than Rust and C might easily be enough of a win).
Disclaimer: I wrote libbpf-rs.
Because I kind of expected the algorithm would say "hey this loop is bounded up to 4K, this nested one is up to 3K, therefore I can't prove the total is below 1M, therefore I reject", but from your description it sounds like it actually does some kind of bounded model checking up to 1M instructions instead of taking shortcuts like this? Or did you mean it actually does take shortcuts like this?
The idea here is that without any kind of loop, it's quite tricky to do some basic packet processing. For instance, if you want to clamp the MSS of a TCP connection, you have to grovel through TCP options, which do not occur at fixed offsets or in a particular order; you want to write a "for" loop over the (inherently limited) range of bytes at the computed offset of the TCP options. You can trivially bound that loop by the MTU of the link your program is loaded on (and also the limited possible size of TCP options), the loop won't iterate that many times, and it won't do much inside the loop.
But it's very much not the case that bounded loops give you general-purpose programming, like to implement your own data structures. BPF programs rely on kernel helpers and userland programs that maintain maps and read perf to do that stuff.
int nested_loops(volatile struct pt_regs* ctx)
{
int i, j, sum = 0, m;
for (j = 0; j < 300; j++)
for (i = 0; i < j; i++) {
if (j & 1)
m = ctx->rax;
else
m = j;
sum += i * m;
}
return sum;
}
Or for example another one that is also part of selftests with induction variable i: int while_true(volatile struct pt_regs* ctx)
{
int i = 0;
while (true) {
if (ctx->rax & 1)
i += 3;
else
i += 7;
if (i > 40)
break;
}
return i;
}
Overall this is very useful to avoid unrolling loops & keeping the code dense and icache friendly, and to parse (e.g.) IPv6 extension headers and such.For example, can it handle something like the following, where there's no bound, but the loop necessarily always terminates? (I assumed this loop would be called "unbounded", but maybe I'm confused by the terminology?)
int test(unsigned i, unsigned j) {
while (true) {
i ^= j; j ^= i; i ^= j;
if (i <= j) { return 0; }
i ^= j; j ^= i; i ^= j;
if (j <= i) { return 1; }
i ^= j; j ^= i; i ^= j;
}
return 2;
} ; __u64 i = PT_REGS_FP(ctx), j = PT_REGS_RC(ctx);
0: (79) r2 = *(u64 *)(r1 +32)
; __u64 i = PT_REGS_FP(ctx), j = PT_REGS_RC(ctx);
1: (79) r1 = *(u64 *)(r1 +80)
;
2: (bf) r3 = r1
3: (bf) r1 = r2
4: (bf) r2 = r3
; if (i <= j) { return 0; }
5: (2d) if r3 > r1 goto pc-4
from 5 to 2: R1=inv(id=2) R2=inv(id=1) R3=inv(id=1) R10=fp0
;
2: (bf) r3 = r1
3: (bf) r1 = r2
4: (bf) r2 = r3
; if (i <= j) { return 0; }
5: (2d) if r3 > r1 goto pc-4
from 5 to 2: R1_w=inv(id=1) R2_w=inv(id=2) R3_w=inv(id=2) R10=fp0
;
2: (bf) r3 = r1
3: (bf) r1 = r2
4: (bf) r2 = r3
; if (i <= j) { return 0; }
5: (2d) if r3 > r1 goto pc-4
;
infinite loop detected at insn 21. If the answer is no, then what is the precise reason? Is there a legitimate reason for it? After all, a bounded loop that loops for too long is just as bad as one that never terminates, so clearly they need a way to upper-bound the instruction count for any loop—at which point, why is the bound even relevant? The only reason I can think of is that they do simplistic analysis (e.g. multiplying the bounds on nested loops to naively approximate an overall bound), but your examples suggest they have more sophisticated (SMT/BMC?) solvers, and it's not obvious to me why a modern solver would fail on all unbounded loops.
2. If the answer is yes, then it would seem they actually do allow unbounded loops after all?
The other possibility is they're using the word "bounded" differently (e.g. maybe as a synonym for "terminating"), in which case it would be true that they would need bounded loops by definition.
The answer to whether BPF effectively allows unbounded loops is "no". The verifier essentially emulates the instructions in your loop, iteration by iteration, and gives itself a fixed budget to do so. If it can't prove the loop invariably exits in that budget, it rejects the program. Allowing an unbounded loop would be an important security vulnerability, and is kind of the whole original point of the verifier.
Probably, this is just a terminology issue; what the verifier in fact cares about is indeed whether the program terminates.