A new LLVM CFI implementation
The kernel makes extensive use of indirect function calls; they are at the heart of its internal object model. Every one of those calls is a potential entry point for an attacker; if the target of the call can be somehow changed to an address of the attacker's choosing, the game is usually over. Forward-edge CFI works to thwart such attacks by ensuring that every indirect function call sends control to a code location that was actually intended to be a target of that call. Specifically, an indirect function call should only go to a known function entry point, and the prototype of the function should match what is expected at the call site.
The CFI implementation merged for 5.13 works by creating "jump tables" containing all of the legitimate targets of indirect function calls in the kernel; there is one jump table for each observed function prototype. Actual indirect calls are replaced with a jump-table lookup to ensure that the intended target meets the criteria; the target should be found in the jump table corresponding to the intended function prototype. If that test fails, a kernel panic results. See this article for a more detailed description of how this mechanism works.
That implementation of CFI does the job, but it has a few disadvantages as well. Creating the jump tables requires a view of the full kernel binary; in practice, it requires that link-time optimization be used to build the kernel, which is a slow and sometimes tricky process. The replacement of function-pointer variables with jump-table entries also means that those variables cannot be compared against the address of a specific function, which is something that kernel code needs to do on occasion. It would be nicer to have a CFI implementation that doesn't impose problems of this sort.
That implementation would appear to exist in this
patch set from Sami Tolvanen. It depends on a new Clang compiler option
(-fsanitize=kcfi), which has not yet landed in the LLVM mainline.
This CFI mechanism, which is "intended to be used in low-level code,
such as operating system kernels
", avoids the above-mentioned problems
at the cost of a couple of other tradeoffs, notably that it cannot work
with execute-only memory (read access is always required).
When code is compiled with -fsanitize=kcfi, the entry point to each function is preceded by a 32-bit value representing the prototype of that function. This value is (part of) a hash calculated from the C++ mangled name for the function and its arguments. On x86 systems, this hash is placed into a simple MOV instruction and surrounded by INT3 instructions; this is meant to prevent the hash itself from becoming a useful gadget for attackers. When an indirect call is made, extra code is emitted to fetch and check this hash value prior to emitting the call itself; if the hash does not match what was expected, a trap (which will be turned into a kernel oops) results. The checking of the hash is why execute-only memory cannot be supported: it must be possible to read the hash value from the executable code.
For the most part, this mechanism just works without the need for much change in the kernel code itself — at least, not beyond the changes that were already required for the previous CFI implementation. There is, however, the problem of functions written in assembly, which will need to have the necessary preamble generated by some other means. Generating the requisite hash value for each indirectly called assembly function could be a tiresome task; fortunately, the compiler provides some help. Whenever it sees (in C code) the address of a function being taken (as in this example):
static const struct v4l2_file_operations mcam_v4l_fops = {
.open = mcam_v4l_open,
/* ... */
};
it will generate a corresponding symbol defined as the resulting hash value; in this case, the symbol would be __kcfi_typeid_mcam_v4l_open. The existence of these symbols means that the preambles for assembly functions can be generated automatically via some tweaks to the macros already used to define those functions.
This patch series is currently in its third version, and it would appear
that all of the substantive concerns have been addressed. It is, in other
words, looking ready to be merged into the mainline. There is only one
remaining obstacle to overcome: kernel developers will be reluctant to
merge this feature until it is actually supported in the LLVM Clang
compiler. Assuming that happens in the near future, it should not be too
long until the kernel acquires an upgraded CFI implementation for the arm64
and x86 architectures.
| Index entries for this article | |
|---|---|
| Kernel | Releases/6.1 |
| Kernel | Security/Control-flow integrity |
