Comparing GCC and Clang security features
Cook started by noting that most of the "old-school" security features have long since been supported by both compilers. These include stack canaries, warnings on unsafe format-string use, and more. Rather than look at those, he chose to focus on relatively new security-oriented features.
The first of these is per-function sections — putting each function into
its own ELF section. This behavior is requested with the
-ffunction-sections switch and is well supported by both
compilers. The value of per-function sections is that they enable
fine-grained address-space layout randomization, where the location of each
function can be randomized independently of the others. It is a "bizarre
and wonderful" feature, he said.
Implicit fall-through behavior in switch statements is a common source of bugs, so many projects are trying to eliminate it. To that end, both compilers support the -Wimplicit-fallthrough option. GCC has supported a special attribute making fall-through behavior explicit for some time; Clang has just gained that support as well. There are evidently no plans in the Clang community to support fall-through markers in comments, though, as GCC does. The kernel is now free of implicit fall-throughs; of the roughly 500 patches fixing fall-through warnings in the last year, Cook said, about 10% turned out to be addressing real bugs in the code.
Link-time optimization (LTO) works with both compilers now. It's not primarily a security feature, but it turns out to be necessary to implement control-flow integrity, which requires a view of all of the functions in a program. Both compilers support LTO, but updating the build tooling to make use of it is still painful. There are also, he said, concerns that LTO can expose differences between the C memory model and the model that the kernel uses, but nobody has provided any specifics about where things could go wrong. It is theoretically a problem, but "practicality matters" and these concerns shouldn't hold up adoption of LTO unless somebody can demonstrate a real-world problem.
Stack probing is the practice of reading a newly expanded stack in relatively small increments to defeat any attempt to jump over guard pages. GCC can build in this behavior now, controlled by the -fstack-clash-protection flag; Clang still lacks this capability. This feature is more useful in user space than in the kernel, Cook said, since the kernel has fully eliminated the use of variable-length arrays.
Clang provides a -mspeculative-load-hardening flag to turn on mitigations for Spectre v1; GCC does not have this support. Details about this feature can be found in this LLVM documentation. Enabling this feature has a notable performance impact, but it is still less costly than inserting lfence barriers everywhere. An attribute can be used to restrict hardening to specific functions, avoiding the need to slow down the entire program.
Functions do not need to preserve the contents of caller-saved registers, so they normally return with random data in those registers. Clearing those registers at return time, instead, may be useful to block any number of speculative attacks or side channels. The performance impact, Cook said, is tiny. Peter Zijlstra objected, saying that he would like to see a proper description of just what is being mitigated by this technique; the impact may be small, but the accumulation of such measures adds up to "death by a thousand cuts". Cook responded that there is value in bringing the architecture to a known state at function return; it may not block a specific attack right now, but "we don't know what is coming next". There is a patch for GCC implementing register clearing, but not for Clang.
Another relatively controversial measure is automatically initializing stack variables on function entry. GCC can do that now via a plugin; work is being done to add it to Clang, though the specific behavior is not what the kernel community would like. Clang will initialize variables to a poison pattern, but Linus Torvalds would rather be able to count on them being initialized to zero.
There are a couple of concerns about automatic initialization of stack variables, though. One is that it might mask warnings about the use of uninitialized variables; those warnings are still wanted. The tricks used by the GCC plugin can evidently confuse tools like KASAN. And, more importantly, this behavior is seen as a fundamental change to the semantics of C code, essentially creating a fork of the language. That is a big step that not everybody wants to take.
The next technique is structure layout randomization; GCC has been able to do this for the kernel via a plugin for a couple of years. There is a port of this support for Clang, but it seems to be stalled at the moment. Cook said that this feature is for "really paranoid builds" but is not really needed for most.
Signed integer overflow is technically undefined behavior in C — though Zijlstra quickly interjected that, in the kernel, it is well defined as twos-complement wrapping. Most of the time, the overflow of a signed int is unexpected, Cook said. Both compilers support the -fsanitize=signed-overflow flag, but its behavior is not ideal. If warnings are enabled, the build size grows by about 6%; if they are not enabled, the program just dies instead — not desirable behavior for the kernel. The warning also allows the overflow to happen; Cook would rather see the value saturate and stay there. Best, he said, would be to support a user-defined handler that can decide what to do about signed overflows.
Unsigned integer overflow, instead, is often done intentionally in the kernel. That behavior is well defined in C, but overflows can still lead to exploits. Clang can trap unsigned overflows now, while GCC cannot. Once again, though, he would rather see a mode where the value saturates rather than being allowed to wrap.
Control-flow integrity (CFI) is, to put it briefly, ensuring that code always jumps to a location that was intended to be jumped to. One aspect of that problem is returns from functions, which should go only to the place the function was called from. X86 processors can support this "backward-edge" checking in hardware, so no compiler support is needed. Arm64 processors have the PAC instruction, but those must be inserted by the compiler. Both compilers have support for these instructions. For processors without backward-edge CFI support, software needs to implement a shadow stack to preserve the integrity of function returns. Clang had support for shadow stacks, but problems resulted and the support has been removed; GCC has never had this support.
"Forward-edge" CFI, instead, ensures that indirect jumps go to the intended location; it's a matter of validating the destination as an appropriate target for the jump. Hardware support is limited to verifying that a given location is, indeed, the entry point of a function; that gives a big reduction in the attack surface, Cook said, but still does not provide a lot of real-world protection since attackers can just chain function calls together. X86 implements this feature with the ENDBR instruction, while Arm has BTI; both compilers support this feature. In software, Clang can make things tighter by checking in software that the called function has the correct prototype as well. But what we really need, Cook said, is truly fine-grained forward-edge CFI.
With that last item, Cook's talk concluded. The conversation returned briefly to integer overflow before things wound down; H. Peter Anvin suggested that, if the desire was to change the semantics of the integer type, a better approach might be to switch to a language like C++ where such changes are more readily supported. It is fair to say, though, that this suggestion was not widely accepted by the audience.
[Your editor thanks the Linux Foundation, LWN's travel sponsor, for
supporting his travel to this event.]
| Index entries for this article | |
|---|---|
| Security | Clang |
| Security | GCC |
| Security | Tools/Compilers |
| Conference | Linux Plumbers Conference/2019 |