Bounded loops in BPF programs
Fastabend started by noting that the lack of loops hurts; BPF developers are doing "crazy things" to work around their absence. He is working to enable the use of simple loops that can be modeled by the verifier. There is academic work on ways to verify more complex loops, but that is a problem for later. For now, the objective is to detect simple loops and verify that they will terminate; naturally, it's important that the verifier, too, is able to terminate in a reasonable amount of time.
The key to determining the behavior of a loop is to find the induction variable that controls it. For a simple loop like:
    for (i = 0; i < MAX; i++)
	/* loop body */
the induction variable is i. If the variable can be shown to be both monotonically increasing and bounded in value, then the verifier can conclude that the loop will terminate. Once the induction variable and its bounds have been identified, the verifier can also check that any memory references using that variable remain in range.
This kind of verification could be done relatively easily with knowledge of
where the loops are.  But BPF is an unstructured virtual machine language
that doesn't contain information about loops, so the verifier has to figure
out where they are itself.  This is done by creating a dominator
tree that describes the program to be examined; it will identify tests
that control the execution of specific blocks of code.  From there, it is
possible to identify loops (by looking for reverse jumps to the dominator
node) and the blocks of code that belong to each loop.
Doing so is not entirely easy. The use of a dominator tree requires that there be a single entry point into every loop; code that jumps into the middle of a loop cannot be verified. Identifying the looped-over code is also an expensive (O(n2)) algorithm.
Fastabend outlined a few approaches to this problem, describing the first as the "by the book" method. This algorithm builds the dominator tree, then works to detect (and abort on) loops that cannot be reduced to a verifiable case. For each loop, it finds the induction variable, and verifies that variable's bounds; the execution of the loop is then simulated with the induction variable's largest and smallest values. The problem here is that the induction variable must be found with pattern matching, and the LLVM optimizer creates a wide variety of patterns that change with every release. That makes the code fragile.
The next approach is to get the compiler to help by limiting the number of loop types that it generates. That makes the pattern-matching task easier, since the patterns that identify loops will be reduced in number and less prone to change. The verifier still has to do all of the work, but it becomes quite a bit more robust.
But the best solution, he said, would be to create a new set of BPF instructions specifically to implement loops. They would mark the beginning and end of each loop, and include the test of the induction variable; the verifier would replace those instructions with actual tests and jumps. With these markers to denote the loop blocks, there would be no need for the dominator tree, since the markers themselves would make it clear which code is controlled by the loop. That would keep the verifier code minimal which, he said, is the right tradeoff in the end.
The description of the session said that the goal "to come to a
consensus on how to proceed to make progress on supporting bounded
loops
".  That did not happen, but there was some discussion about the
options and the development community is more aware of how this complex
work is proceeding.  In the end, real consensus is likely to come about in
the usual way: through the posting of code that shows how the idea is
implemented in the real world.
The slides from this talk [PDF] and a video recording [YouTube] are available. Curious readers can also see the implementation of the first alternative as it was posted in June.
[Thanks to the Linux Foundation, LWN's travel sponsor, for supporting my
travel to the event.]
| Index entries for this article | |
|---|---|
| Kernel | BPF/Loops | 
| Conference | Linux Plumbers Conference/2018 | 
      Posted Dec 3, 2018 23:16 UTC (Mon)
                               by rvolgers (guest, #63218)
                              [Link] (11 responses)
       
This is why WebAssembly is explicit about control flow nesting and doesn't allow arbitrary jumps. Wouldn't be surprised at all if eventually BPF gets replaced with some fancy formally-verified minimal WebAssembly JIT from Google or something. 
     
    
      Posted Dec 3, 2018 23:52 UTC (Mon)
                               by roc (subscriber, #30627)
                              [Link] (6 responses)
       
     
    
      Posted Dec 4, 2018 0:48 UTC (Tue)
                               by rvolgers (guest, #63218)
                              [Link] (2 responses)
       
Either way there is still the problem of doing something with those loops to make sure they complete within a certain number of iterations though. 
     
    
      Posted Dec 5, 2018 8:18 UTC (Wed)
                               by edeloget (subscriber, #88392)
                              [Link] (1 responses)
       
That, in turn, means that if I handcraft the code I send to the kernel, it will then have no way to perform the needed verifications :) Does not sound that great :) 
     
    
      Posted Dec 9, 2018 1:05 UTC (Sun)
                               by nteon (subscriber, #53899)
                              [Link] 
       
     
      Posted Dec 5, 2018 23:30 UTC (Wed)
                               by wahern (subscriber, #37304)
                              [Link] (2 responses)
       The Relooper algorithm isn't much better, if at all. Per the original paper, Emscripten: an LLVM-to-JavaScript compiler:
 Of course "almost all" code is amenable because very little code comprises the more critical, performant inner loops and state machines--the parts which often heavily rely on goto.
 Arguably if JavaScript had goto there likely never would have been a need to create and rely on the Relooper, and WebAssembly may have turned out differently. Lua 5.2 added a goto construct precisely because it made code generation so much easier. I suspect that eventually much BPF code will be generated from high-level DSLs. Not everybody will be using the same toolchains and libraries, and not having a goto construct is a serious headache when machine generating code. Generating performant assembly from a task-specific, declarative DSL is often trivial; much less so if you now need to implement a relooper or integrate a relooper library.
 WebAssembly was originally envisioned as a simple, pure stack-based VM where variable scoping and structured blocks naturally aligned with the overall design. A goto construct was unfathomable. But then WebAssembly got much more complex and the simple stack-based design fell away. I suspect adding a goto construct could have been possible after the original, simpler concept gave way, and I wouldn't rule out its addition in the future. Gotos (including computed gotos) are still very useful in performant C code because optimizing compilers still can't do the job--in real-world code the best switch-statement optimizations still can't come close to idiomatic use of gotos and computed gotos. Like with JIT compiling, microbenchmarks and theoretical optimizations never quite make the transition to the real-world undiminished. Performance demands may still necessitate goto.
      
           
     
    
      Posted Dec 6, 2018 0:30 UTC (Thu)
                               by wahern (subscriber, #37304)
                              [Link] (1 responses)
       
What I had in mind here was the transition from the AST-based bytecode format to the structured stack machine format. The transition shifted WASM in the direction of a model where you could contemplate adding gotos. The transition happened because implementations were *already* using modern compiler techniques, like SSA transforms. Adding a goto construct with a sophisticated verifier is much less of a leap than when your machine model is based directly on ASTs. 
For more context see https://github.com/WebAssembly/design/issues/755 and https://docs.google.com/document/d/1CieRxPy3Fp62LQdtWfhym... 
 
 
     
    
      Posted May 12, 2019 11:16 UTC (Sun)
                               by ibukanov (subscriber, #3942)
                              [Link] 
       
     
      Posted Dec 4, 2018 12:20 UTC (Tue)
                               by nix (subscriber, #2304)
                              [Link] (3 responses)
       
(And why would Google have a WASM JITter suitable for running in *kernelspace* anyway?) 
 
     
    
      Posted Dec 4, 2018 12:31 UTC (Tue)
                               by roc (subscriber, #30627)
                              [Link] (2 responses)
       
1) We have a framework X. 
Duplicating effort along the path of least resistance, one feature at a time. 
     
    
      Posted Dec 4, 2018 14:30 UTC (Tue)
                               by Herve5 (subscriber, #115399)
                              [Link] 
       
     
      Posted Dec 4, 2018 20:02 UTC (Tue)
                               by simcop2387 (subscriber, #101710)
                              [Link] 
       
     
      Posted Dec 3, 2018 23:50 UTC (Mon)
                               by roc (subscriber, #30627)
                              [Link] (10 responses)
       
     
    
      Posted Dec 4, 2018 8:32 UTC (Tue)
                               by mjthayer (guest, #39183)
                              [Link] (9 responses)
       
In my naivety, I wonder whether it would be possible simply to bound BPF programme execution time, and make it the user's responsibility to provide something which can finish within the time. 
     
    
      Posted Dec 4, 2018 8:39 UTC (Tue)
                               by Cyberax (✭ supporter ✭, #52523)
                              [Link] (6 responses)
       
BPF is moving with a rapid speed towards a completely generic in-kernel virtual machine. 
     
    
      Posted Dec 4, 2018 8:45 UTC (Tue)
                               by mjthayer (guest, #39183)
                              [Link] (5 responses)
       
I was more thinking of something in the execution engine. 
     
    
      Posted Dec 4, 2018 9:27 UTC (Tue)
                               by Sesse (subscriber, #53779)
                              [Link] 
       
     
      Posted Dec 4, 2018 9:33 UTC (Tue)
                               by roc (subscriber, #30627)
                              [Link] (3 responses)
       
     
    
      Posted Dec 4, 2018 10:33 UTC (Tue)
                               by sdalley (subscriber, #18550)
                              [Link] (2 responses)
       
     
    
      Posted Dec 4, 2018 10:48 UTC (Tue)
                               by corbet (editor, #1)
                              [Link] (1 responses)
       
     
    
      Posted Dec 4, 2018 18:43 UTC (Tue)
                               by Cyberax (✭ supporter ✭, #52523)
                              [Link] 
       
The comparison will be branch-predicted most of the time and will be fast. 
     
      Posted Dec 4, 2018 12:34 UTC (Tue)
                               by roc (subscriber, #30627)
                              [Link] (1 responses)
       
It might be significantly better to bound the number of BPF instructions executed by some (configurable?) constant. Then running the same program on the same input data should always either succeed or fail, which would make  debugging easier. 
     
    
      Posted Dec 4, 2018 13:00 UTC (Tue)
                               by mjthayer (guest, #39183)
                              [Link] 
       
Right.  I used the word "time", but I was also more thinking of instruction count or similar. 
     
      Posted Dec 4, 2018 1:19 UTC (Tue)
                               by ringerc (subscriber, #3071)
                              [Link] (20 responses)
       
     
    
      Posted Dec 4, 2018 5:04 UTC (Tue)
                               by marcH (subscriber, #57642)
                              [Link] 
       
It's not mentioned in the (three) slides either, just: "Lots of academic work on complex loops... fun but lets stick to basic ax+c for now." 
> > In the end, real consensus is likely to come about in the usual way: through the posting of code that shows how the idea is implemented in the real world. 
Scarier and scarier :-) 
 
     
      Posted Dec 4, 2018 7:20 UTC (Tue)
                               by corbet (editor, #1)
                              [Link] (18 responses)
       
     
    
      Posted Dec 4, 2018 8:26 UTC (Tue)
                               by vbabka (subscriber, #91706)
                              [Link] (10 responses)
       
     
    
      Posted Dec 4, 2018 8:52 UTC (Tue)
                               by amacater (subscriber, #790)
                              [Link] 
       
[If Turing machines are obsolete, when do you stop production? ] 
     
      Posted Dec 4, 2018 10:32 UTC (Tue)
                               by excors (subscriber, #95769)
                              [Link] (8 responses)
       
And by "programs", it specifically means programs in a system equivalent to a Turing machine, which has infinite memory and infinite time. That means it doesn't apply to any practical computer - they will have limited memory, so it's trivial to prove whether a program will halt by just enumerating all possible states. (Of course that's also a practically useless thing to prove - the BPF verifier wouldn't want to accept a program that definitely halts but might take a million years to do so.) 
     
    
      Posted Dec 4, 2018 12:35 UTC (Tue)
                               by nix (subscriber, #2304)
                              [Link] (6 responses)
       
     
    
      Posted Dec 4, 2018 14:27 UTC (Tue)
                               by excors (subscriber, #95769)
                              [Link] (5 responses)
       
     
    
      Posted Dec 4, 2018 16:31 UTC (Tue)
                               by dskoll (subscriber, #1630)
                              [Link] (4 responses)
       Well, you know that N is so large that 2^N might as well be infinite for all practical purposes.  Also, you assume that the computation is deterministic.  In real-world situations where external events can change the state (a network packet arriving, for example), a program might terminate even if it repeats a state it has been in before.
 Actually, it's trivial to prove that all programs on computers on earth will eventually halt, becuase the Sun will eventually expand and destroy the earth, thereby halting the program.  That's the ultimate example of an external event affecting the state.
      
           
     
    
      Posted Dec 4, 2018 17:40 UTC (Tue)
                               by excors (subscriber, #95769)
                              [Link] (3 responses)
       
     
    
      Posted Dec 4, 2018 19:35 UTC (Tue)
                               by dskoll (subscriber, #1630)
                              [Link] 
       I wouldn't say the halting problem isn't relevant at all to BPF, but it's not the only problem with trying to make it work practically.
 But yeah, mostly it's just fun to point out weird edge cases that distinguish theory from practice. :)
      
           
     
      Posted Dec 4, 2018 23:53 UTC (Tue)
                               by nix (subscriber, #2304)
                              [Link] 
       
     
      Posted Dec 5, 2018 10:46 UTC (Wed)
                               by farnz (subscriber, #17727)
                              [Link] 
       It's also not relevant because the halting problem says that you can't sort all programs into "does not halt" and "does halt". If you accept a third bucket - "may or may not halt", then the problem falls away; you can sort all programs into those three buckets in theory, and then it's just a practical problem.
 This is not a new insight - it's basically a conservative approximation to the halting problem (where you treat "may or may not halt" as "does not halt", and accept that you are rejecting some programs that do halt), and is a common process in compilers.
      
           
     
      Posted Dec 5, 2018 14:16 UTC (Wed)
                               by mina86 (guest, #68442)
                              [Link] 
       
     
      Posted Dec 4, 2018 12:25 UTC (Tue)
                               by nix (subscriber, #2304)
                              [Link] (6 responses)
       
(Really, it's a shame the verifier and JITters have to run in kernel space: this sort of work seems like the sort of thing far better done by an upcall to something in userspace, unprivileged but only installable by root -- only that would stop BPF from being used to protect the system *from* root, and it wouldn't help with the time problem at all since the kernel would still have to block the process installing the BPF to wait for verification/JITing anyway...) 
It just makes me icky seeing that people are basically writing compilers that run in kernelspace. Surely there must be a better way. 
     
    
      Posted Dec 4, 2018 14:05 UTC (Tue)
                               by excors (subscriber, #95769)
                              [Link] 
       
     
      Posted Dec 4, 2018 16:18 UTC (Tue)
                               by farnz (subscriber, #17727)
                              [Link] (3 responses)
       This actually sounds like a potential application for proof-carrying code. The user-space verifier could attach a simple-to-verify proof to the program it's about to attach (even if it's an arbitrary program with loops and other such weirdnesses) that the kernel can check.
      
           
     
    
      Posted Dec 4, 2018 23:49 UTC (Tue)
                               by nix (subscriber, #2304)
                              [Link] (2 responses)
       
     
    
      Posted Dec 5, 2018 10:16 UTC (Wed)
                               by farnz (subscriber, #17727)
                              [Link] (1 responses)
       It would be rather surprising if a researcher at Berkeley working on Packet Filters was completely unaware of BPF :-)
      
           
     
    
      Posted Dec 11, 2018 17:45 UTC (Tue)
                               by nix (subscriber, #2304)
                              [Link] 
       
     
      Posted Dec 8, 2018 7:02 UTC (Sat)
                               by marcH (subscriber, #57642)
                              [Link] 
       
Did anyone say it was? 
> the problem here is always finding an approximation... 
The "feat" was just to never mention once the name of what this approximates. 
The misunderstanding and overreaction is even more interesting. 
     
      Posted Dec 4, 2018 8:28 UTC (Tue)
                               by dezgeg (subscriber, #92243)
                              [Link] (2 responses)
       
     
    
      Posted Dec 4, 2018 9:37 UTC (Tue)
                               by roc (subscriber, #30627)
                              [Link] (1 responses)
       
     
    
      Posted Dec 5, 2018 19:55 UTC (Wed)
                               by matthias (subscriber, #94967)
                              [Link] 
       
     
      Posted Dec 4, 2018 11:50 UTC (Tue)
                               by ibukanov (subscriber, #3942)
                              [Link] (5 responses)
       
     
    
      Posted Dec 4, 2018 14:19 UTC (Tue)
                               by dskoll (subscriber, #1630)
                              [Link] (4 responses)
       A few nested loops and once again you can execute billions of iterations...
      
           
     
    
      Posted Dec 4, 2018 22:46 UTC (Tue)
                               by ibukanov (subscriber, #3942)
                              [Link] (1 responses)
       
     
    
      Posted Dec 5, 2018 2:24 UTC (Wed)
                               by dskoll (subscriber, #1630)
                              [Link] 
       That would work if BPF programs couldn't call subroutines, or if the verifier chased subroutines down to make sure anything called within a loop didn't contain its own loop.  Sounds pretty tricky and tedious. I don't envy those doing this work.
      
           
     
      Posted Dec 5, 2018 14:25 UTC (Wed)
                               by mina86 (guest, #68442)
                              [Link] (1 responses)
       
     
    
      Posted Dec 5, 2018 20:10 UTC (Wed)
                               by matthias (subscriber, #94967)
                              [Link] 
       
     
      Posted Dec 5, 2018 10:51 UTC (Wed)
                               by pctammela (guest, #126687)
                              [Link] (3 responses)
       
     
    
      Posted Dec 5, 2018 10:52 UTC (Wed)
                               by pctammela (guest, #126687)
                              [Link] 
       
     
      Posted Dec 6, 2018 0:53 UTC (Thu)
                               by wahern (subscriber, #37304)
                              [Link] (1 responses)
       
Not sure exactly how that relates to eBPF as the parallels aren't exact. eBPF is already bytecode, but because it's destined to be JIT-compiled it's similar to Lua script in terms of the pipeline. And unlike Lua where the VM and compiler are developed completely separately (except coordinated releases), in eBPF I presume the verifier and JIT compiler are more intimately bound. 
Still, the rule of thumb in security engineering is that verification should be disfavored as it duplicates and disassociates logic in a way that increases the risk for bugs. It can be difficult to keep track of the places where the JITer and the runtime depend on the verifier. Even with a safe-by-design bytecode spec, bugs happen, and they're more likely when invariants are expressed thousands of lines apart than when they're expressed a few lines apart. 
 
     
    
      Posted Dec 6, 2018 12:30 UTC (Thu)
                               by pctammela (guest, #126687)
                              [Link] 
       
Lua doesn't need a bytecode verifier if the interpreter exposed is already sandboxed by the developer, which was the real decision why the bytecode verifier was removed. Some people need it, some don't. 
> Not sure exactly how that relates to eBPF as the parallels aren't exact. 
Both are virtual machines running inside the operating system. 
>  Lua where the VM and compiler are developed completely separately 
Lua has matured enough to be able to release new versions of both separately. It's just a matter of time to eBPF catch up to this pace. 
This is also a design goal of Lua, allowing developers to tune/rewrite the VM without interfering with the compiler and maximizing portability of Lua scripts. 
> Even with a safe-by-design bytecode spec, bugs happen, and they're more likely when invariants are expressed thousands of lines apart than when they're expressed a few lines apart. 
Bugs happen because "Errare humanum est". The likeliness of bugs are intrinsically connected to software architecture and bugs can be minimized with a good test suite, which Lua has. 
Lua is extremelly small, in fact the entire Lua language implementation (VM/Interpreter/C API) is just ~20k lines of logical C code, with the C API dominating a huge chunk of this. Last time I checked, this was smaller than (e)BPF itself. I don't understand your point here. 
     
      Posted Dec 6, 2018 20:29 UTC (Thu)
                               by Jandar (subscriber, #85683)
                              [Link] 
       
char x[1]; 
Checking only the larges and smallest value of the induction variable isn't enough. 
     
    Bounded loops in BPF programs
      
Bounded loops in BPF programs
      
Bounded loops in BPF programs
      
Bounded loops in BPF programs
      
Bounded loops in BPF programs
      
Bounded loops in BPF programs
      
In general though, this is not always easy or even practical –
there may not be a straightforward high-level loop structure
corresponding to the low-level one, if for example the original
C code relied heavily on goto instructions. In practice,
however, almost all real-world C and C++ code tends to be
amenable to loop recreation.
Bounded loops in BPF programs
      
Bounded loops in BPF programs
      
Bounded loops in BPF programs
      
Bounded loops in BPF programs
      
2) X is missing a small feature. Framework Y has that feature, and is overall great, but it's more complex than we need right now, and besides the cost of migrating from X to Y for just this one feature can't be justified.
3) Add feature to X.
4) Goto 1.
"duplicating effort along the path of least resistance"
      
Bounded loops in BPF programs
      
Bounded loops in BPF programs
      
Bounded loops in BPF programs
      
Bounded loops in BPF programs
      
Bounded loops in BPF programs
      
Bounded loops in BPF programs
      
Bounded loops in BPF programs
      
Bounded loops in BPF programs
      
      There is no separate thread, no.  One could always set a timer to bring an end to runaway execution, but that would be an expensive thing to do.
      
          Bounded loops in BPF programs
      Bounded loops in BPF programs
      
Bounded loops in BPF programs
      
Bounded loops in BPF programs
      
Bounded loops in BPF programs
      
Bounded loops in BPF programs
      
      Nobody is trying to solve the halting problem in any kind of general sense, so it's not really relevant here.  After all, current BPF can guarantee that all programs it accepts will halt, no problem...
      
          Halting problem
      Halting problem
      
Halting problem
      
Alan Mathison Turing
Said to his colleagues
"It's now us or them.
Now, if you will
Help me hedge
Programatically
Goedel's insoluble
Entscheidungsproblem"
Halting problem
      
Halting problem
      
Halting problem
      
Halting problem
      Halting problem
      
Halting problem
      Halting problem
      
Halting problem
      Halting problem
      
Halting problem
      
Halting problem
      
Halting problem
      Halting problem
      
Halting problem
      Halting problem
      
Halting problem
      
Bounded loops in BPF programs
      
Bounded loops in BPF programs
      
Bounded loops in BPF programs
      
Bounded loops in BPF programs
      
Bounded loops in BPF programs
      Bounded loops in BPF programs
      
Bounded loops in BPF programs
      Bounded loops in BPF programs
      
Bounded loops in BPF programs
      
Bounded loops in BPF programs
      
Bounded loops in BPF programs
      
Bounded loops in BPF programs
      
Bounded loops in BPF programs
      
Bounded loops in BPF programs
      
int i;
for( i=0; i<=10; ++i )
  x[ i < 5 ? i : 10-i ] = 0;
           