Improvements to GCC's -fanalyzer option
When GCC is invoked with -fanalyzer, it runs a module that creates an "exploded graph" combining information on the state of the program's control and data flow. That state includes an abstract representation of memory contents, known constraints on the values of variables, and information like whether the code might be running in a signal handler. The analyzer then uses this graph to try to explore all of the interesting paths through the code to see what might happen.
The GCC 10 release added 15 new warnings for potential errors like freeing
memory twice, using memory after freeing it, unsafe calls within signal
handlers, possible writing of sensitive data to a log file, and more. The
double-free detection was the motivating factor that led to the development
of many of those checks. Five more warnings came with GCC 11,
including the ability to check whether memory obtained from one allocator is
not accidentally freed to a different one and a check for undefined behavior
in bit shifts. Also in GCC 11 is support for plugins that extend the
analyzer. One example plugin can be found in the test suite; it checks for
misuse of the Python global interpreter lock (GIL).
Recently, Malcolm was wondering what he should focus on for GCC 12. He had originally thought that he would add C++ support, but then concluded that he would rather improve the functionality for C instead. Before coming to that conclusion, he had implemented new and delete support in GCC 11 and was looking at exception handling as the next challenge, but that turned out to be "quite involved". Meanwhile, Google Summer of Code student Ankur Saini had added support for virtual functions. So some progress had been made, but Malcolm's work will continue to be focused on C for now.
There were a couple of problems for C that drew his attention, one of which is buffer-overflow detection. He has an experimental implementation that can capture the size of dynamic allocations in symbolic form and issue diagnostics about reads and writes that might go beyond that size. But determining when those warnings should be generated is hard; the code as it is now produces far too many false positives ("a wall of noise") and is not really useful.
It occurred to him, though, that there could be a way to find one specific class of problems: places where an attacker is able to influence whether an access is valid or not. That leads to the problem of taint detection and determining where the trust boundaries in the program are, which turns out to be a hard problem for arbitrary C code. It might, though, be possible to annotate specific programs and get useful diagnostics. His attention went to the kernel, which has a well-defined trust boundary and an API for moving data across that boundary. By annotating functions like copy_from_user() and system-call handlers, it might be possible to find code that does not properly sanitize user-supplied data.
GCC has an attribute (access) that describes how data moves through a particular variable or function. Malcolm added two new values (untrusted_read and untrusted_write) for that attribute to mark data that is read from (or written to) an untrusted location. Thus, for example, the data read into the kernel by copy_from_user() would be marked as untrusted_read. He added a new tainted attribute for functions as well; it indicates that all arguments to that function should be treated as untrusted. By tweaking one macro in the kernel headers, he was able to mark all of the system-call handlers in the kernel with this tainted attribute. Similar things can be done with, for example, callbacks for kernel-generated filesystems.
With those annotations, there are two categories of problems that the analyzer can detect: information leaks and using tainted data. Information leaks happen when uninitialized data is written back to user space. This case is relatively easy to detect — or, at least, he thought it would be. As an example of this type of problem, Malcolm brought up CVE-2017-18549, a driver bug that wrote random stack data back to user space. The uninitialized data that was written in this case was padding within a structure that had otherwise been fully initialized; the analyzer was able to find this problem. Getting this to work required refactoring the handling of uninitialized-data tracking; it was not a small task.
A similar problem comes about in code that reads data from user space, modifies it, then copies the result back, possibly to a different location. If the read fails, the kernel may be working with uninitialized data, which it will then duly write to user space. Handling this required bifurcating the analysis to handle the case when copy_from_user() fails. Once that was done, the analyzer also gained the ability to handle realloc(), which has three possible outcomes.
The tainted-data case comes about when, for example, a user-supplied value is used as an array index. It is harder to detect but also seems more important, since vulnerabilities of this type can often be exploited to compromise the kernel. Consider, for example, CVE-2011-0521, where kernel code would read a signed "size" value, check it against the maximum allowable value, then use it without checking for a negative value. The improved analyzer is able to catch this case.
He is still working on a prototype implementation of this functionality to show to the world; as part of that, he has developed the world's worst kernel module, which contains as many problems as he can come up with. Making the analyzer work with the full kernel, though, is complicated by the fact that the kernel uses a lot of inline assembly code. He has added some basic handling for this, but it doesn't look at the actual opcodes.
He's been running the result on upstream kernels, and has found one real vulnerability already; it has been reported but is not yet fixed or disclosed. For this reason, Malcolm's latest work still lives in an internal company repository; it finds vulnerabilities and he doesn't want to release a zero-day-finding tool until the problems it turns up have been fixed. Much of the rest of the work is in the GCC 12 trunk now. He hopes to be able to finish this work and upstream it by the end of the GCC 12 stage 1 period (this page describes the GCC development-cycle stages).
More information on this project can be found in the GCC
wiki.
| Index entries for this article | |
|---|---|
| Conference | Linux Plumbers Conference/2021 |
