|
|
Log in / Subscribe / Register

LLVM improvements for BPF verification

By Daroc Alden
May 27, 2024

LSFMM+BPF

Alan Jowett gave a remote presentation at the 2024 Linux Storage, Filesystem, Memory Management, and BPF Summit about what features could be added to LLVM to make writing BPF programs easier. While there is nothing specific to LLVM about BPF code (and the next session in the track was led by GCC developer José Marchesi about better support for that compiler), LLVM is currently the most common way to turn C code into BPF bytecode. That translation, however, runs into problems when the BPF verifier cannot understand the code LLVM's optimizations produce.

Jowett began by talking about how LLVM processes code internally. First, the C code is translated to LLVM intermediate representation (IR). Then, several passes of optimizations gradually turn the IR into a more efficient version. Finally, the code generator creates BPF bytecode corresponding to the IR. The problem with this process is that LLVM has had many years to develop sophisticated optimizations. It is not uncommon, Jowett said, for LLVM to produce code that is correct, but that the BPF verifier cannot understand — a problem LWN has covered before. For that reason, developers sometimes have to use inline BPF assembly to circumvent the optimizer in order to have their programs accepted.

Before opening up discussion of possible solutions to the problem, however, Jowett first covered some other things that would be nice to see from LLVM. One example is likely/unlikely branch hints that may be present at the source level, but which are lost by the time a program is translated to bytecode. Another possibility Jowett raised was support for code-coverage information, possibly as a prelude to supporting profile-guided optimization of BPF programs.

Jowett then presented a few rough ideas for how it might be possible to prevent LLVM optimization passes from breaking the verifiability of BPF code. One solution might be to move the verifier into the compiler, not as the authoritative source, but as a check to prevent optimization passes from making changes that the verifier cannot understand. Jowett did not propose using the kernel's BPF verifier, however — perhaps because of the licensing problems that would pose — but rather the PREVAIL verifier, an MIT-licensed verifier produced as an academic project that runs in polynomial time (as opposed to the kernel's exponential time).

Using a different verifier is not a perfect solution, however. Jowett pointed out that PREVAIL's design means that it will not verify programs with "correlated branches", where taking one branch always implies that another branch should be taken as well. This is a somewhat common pattern in BPF programs that conditionally acquire and release locks, for example. This pattern can also be introduced artificially by the LLVM optimizer when it tries to avoid repeating tests.

I asked Jowett how he planned to use the PREVAIL verifier inside LLVM's optimization passes, since the former operates on BPF bytecode and the latter operates on LLVM IR. Jowett acknowledged that it would be a problem. Marchesi noted that something like that might be possible in GCC, which supports undoing optimization passes — the compiler could run code generation repeatedly during optimization, and back out the results of any passes that made the generated code fail the verifier. Another audience member noted that they had code that had to run on many possible kernels with different verifiers, and that therefore including any one verifier was insufficient. Dave Thaler indicated that cross-platform BPF compatibility was something that he had a session about later in the day.

Jowett suggested some less intrusive alternatives, such as permitting more fine-grained control over which passes the optimizer runs, allowing developers to assert that some code compiles in a certain way, or just making the optimizer smarter, before opening the floor for suggestions. Yonghong Song, an LLVM developer, said that he had discussed allowing fine-grained control over the optimizer passes with the upstream project, and with the GCC developers at the 2023 Linux Plumbers Conference. In short, it would be hard. The compilers could add a flag to let the optimizer know it needs to do special verifier-friendly things, but that has not yet been implemented.

Jowett asked whether Song had any thoughts about code-coverage instrumentation, or whether perhaps BPF programs were too small to benefit. Song thought code coverage was not likely to be extremely useful, but it may still be useful, and invited people interested in the idea to talk with him about it. An audience member suggested that perhaps BPF programs could get code-coverage information already by analyzing the verifier log — which records every instruction it analyzes. Jowett indicated that this was not sufficient, because it does not actually provide any information about whether a particular branch is covered at runtime. Another audience member indicated that they had written a tool that increments counters in a BPF map for each instruction executed, but that the tool had various limitations.

During the general discussion toward the end of the session, Marchesi asked whether Jowett's idea of preserving likely/unlikely branch hints would require changes to BPF bytecode. Jowett indicated that it would. Another participant noted that they were unsure whether a smarter JIT would be worth it, saying that a more complicated JIT was "really scary" from a memory-model perspective. The BPF JIT runs after the compiler's and verifier's safety checks, so a bug in the JIT is much more likely to break things than an optimization done in the compiler.

Since the group didn't appear to come to much of a consensus, it seems likely that this will remain a topic of discussion. Modern C compilers are, in some ways, a bad fit for BPF; the verifier cares about many properties of programs that have not been a concern for historical targets. Whether and how the BPF developers will be able to overcome this wrinkle remains to be seen.


Index entries for this article
KernelBPF/Compiler support
ConferenceStorage, Filesystem, Memory-Management and BPF Summit/2024


to post comments

LLVM improvements for BPF verification

Posted May 28, 2024 12:00 UTC (Tue) by foom (subscriber, #14868) [Link] (1 responses)

If the BPF-with-verifier architecture actually had a specification, possibly compilers could implement it (although even then it would be a heavy lift due to the weird semantics.) But as it stands, the behavior of the verifier is not apparently considered part of the architecture -- instead, an internal implementation detail of the kernel which can change at any time.

IMHO, the best option is to continue the work to allow runtime aborts, which can then eliminate the need for some of the problematic static verifier failures.

LLVM improvements for BPF verification

Posted May 28, 2024 13:15 UTC (Tue) by daroc (editor, #160859) [Link]

Unfortunately, I cannot match Jon's speed when it comes to writing these things up. But there were multiple sessions in the BPF track about the standardization process, including one on the last day that got into what the IETF working group is expected to deliver. So there are more upcoming articles that touch on this; stay tuned.

LLVM improvements for BPF verification

Posted May 30, 2024 3:48 UTC (Thu) by DemiMarie (subscriber, #164188) [Link]

Could this be solved by having much more information explicit in BPF or provided as side tables? Right now, the verifier needs to infer various properties of the program, which is undecidable (Rice’s theorem). A better solution might be for userspace to provide a proof of memory safety and termination, which the kernel would only need to check. Checking the proof is decidable, and hopefully efficient.

LLVM improvements for BPF verification

Posted May 31, 2024 12:20 UTC (Fri) by SLi (subscriber, #53131) [Link] (2 responses)

A lot in this feels like it has parallels in how VHDL and Verilog constructs need to be translated into actual hardware like logic gates. Those languages are powerful enough to express constructs that are inherently unsynthesizable, like unbounded loops, similar to C for BPF. The trend there seems to be towards inputting higher level languages and inferring bounds.

Would one sensible way to express this problem be this: We can design a low-level language that can only express safe (in the BPF sense) programs and that can be reasonably mechanically and efficiently be translated into bytecode that the verifier accepts. This language needs to have some higher level constructs such as loops with specified bounds, so in this sense it's perhaps best thought of as a target-specific intermediate language instead of a backend language, which I assume in most compilers are strongly associated with something highly resembling lists of machine instructions.

The problem from general compiler point of view is that this language is rather distinct from the IRs that the compiler otherwise works with. It is inherently designed to not only carry but _require_ safety information; and it is inherently incomplete, in that there are valid programs that it cannot express (though obviously any C-to-BPF compiler needs to deal with this impendance somewhere in the pipeline).

It sounds to me like the current approach either does not use such a language at all (perhaps more likely?), or that that language is constructed from fairly far lowered LLVM IR or even from the generated BPF instructions.

It may well be that introducing such a language into a generic compiler framework is largely impossible. But such a language as a "target" would be what I would think of if I was working on a compiler for a such a system. Granted, I would very much prefer to work on a frontend for a language that actually cannot express non-verifiable programs (i.e. not C). If such a language was generated from LLVM IR, there's also the tricky question of "when". LLVM optimization passes work on the LLVM IR. Doing the lowering too early makes it impossible to run those optimization passes. Doing it as late as reasonably possible will probably make it trickier to do because of difficult things the optimization passes have done. And even then it seems likely that optimizing the bytecode generated "mechanically" from this intermediate language would still benefit from many optimization passes that only operate on LLVM IR.

LLVM improvements for BPF verification

Posted May 31, 2024 16:04 UTC (Fri) by Wol (subscriber, #4433) [Link] (1 responses)

In that case, couldn't you just run a subset of LLVM optimisers? Ones that are either "verifier safe" such as unrolling loops, or make the verification process easier.

You could probably actually write a lot of the verification code in an LLVM optimiser, just that it's designed to optimise for verification, not to optimise code speed/size.

Cheers,
Wol

LLVM improvements for BPF verification

Posted May 31, 2024 16:20 UTC (Fri) by SLi (subscriber, #53131) [Link]

I suspect that the story about "verification safe" passes may be trickier. That approach would work if the process started from something verification safe. But if you start from arbitrary code with no safety guarantees, I'd expect to run into cases where the same optimization pass, given two inputs A and B where A is verifiable and B is not, would flip it so that it makes B verifiable and A not.

But just to be clear, I claim to understand something about compilers and software verification in general; I'm not particularly familiar with this specific case.


Copyright © 2024, Eklektix, Inc.
This article may be redistributed under the terms of the Creative Commons CC BY-SA 4.0 license
Comments and public postings are copyrighted by their creators.
Linux is a registered trademark of Linus Torvalds