Rust and Clang/LLVM Control Flow Guard

We’re happy to announce that the Clang C/C++ compiler and Rust now support Windows Control Flow Guard ( CFG ) as part of our ongoing efforts to program safer systems.

Control Flow Guard: What Is It?

A platform security technology called CFG is intended to uphold the integrity of the control flow. Since Windows 8.1, it has been accessible, and Windows 10 now makes extensive use of it. An attacker might attempt to launch a code-reuse attack, for instance, given an initial memory safety vulnerability. This almost always necessitates the attacker altering the program’s control flow, or violating its integrity. For instance, an attacker might attempt to corrupt a function pointer so that it can run from any location in the program code.

By requiring coarse-grained forward-edge control flow integrity, CFG seeks to reduce this kind of exploit. Runtime checks are specifically used to verify each indirect branch instruction’s target address ( call, jump, etc. ). before letting the branch finish. The compiler must perform two tasks in order to accomplish this: provide a list of legitimate indirect branch targets and add runtime checks where necessary. The compiler adds a CFG check to each indirect branch it identifies during compilation. Additionally, it emits metadata that includes all address-taken functions’ relative addresses. The loader uses the CFG metadata to generate a bitmap of the address space and marks which addresses contain valid branch targets at runtime if the binary is executed on an operating system that is aware of it. The inserted code stops the process if the target address is not marked in this bitmap on each indirect branch.

Address Space Layout Randomization ( ASLR ) and Data Execution Prevention ( DEP ) are two additional exploit mitigations that complementCFG. CFG was previously only accessible for C/C++ code created with Microsoft Visual C++.

Control Flow Guard for Clang and LLVM

A collection of reusable and modular compiler and toolchain technologies is called the LLVM Project. The Clang C/C++ compiler and rustc, the Rust compiler, are just a couple of the compiler frontends that use the LLVM Core libraries as their language-independent foundation. The machine code generation for various CPU architectures is provided by the core libraries, which also include a common set of optimizations.

LLVM 10.0 now supports CFG. Our implementation of CFG is fully contained within the core libraries, making it reusable by any compiler built on LLVM – the frontend compiler simply needs to set the correct flags. Our implementation supports all target architectures for which CFG is currently available, namely x86 (32-bit and 64-bit), ARM, and Aarch64. We’ve added CFG support to Clang 10.0 for C/C++ projects. To enable CFG in your projects, simply use the -cfguard cc1 option (e.g. -Xclang -cfguard), or if you’re using the clang-cl compatibility driver, use the same /guard:cf flags as in MSVC. Clang also supports the __declspec(guard(nocf)) modifier to elide CFG checks in the specified function, but this should only be used if absolutely necessary as it may allow exploits.

The Chromium team is attempting to implement CFG in Windows builds as a first step toward implementing it in Google Chrome and Microsoft Edge because the codebase is built with Clang.

Rust Control Flow Guard

Microsoft is looking into using Rust as a safe systems programming language, as was previously stated in posts. CFG’s use within our software may have been hampered in the past because Rust did not support it. Rust’s ownership model offers robust memory-safety guarantees, which should prevent the vulnerabilities used as the basis for an exploit. This is one of its main selling points. Why then would a language like CFG need to take advantage of mitigations? CFG should be used in two situations in Rust:

  • Where Rust and C/C++ coexist in codebases,
  • any unsafe codebases in pure Rust.

1. C/C++ and Rust are connected.

The first case for enabling CFG is whenever Rust interoperates with C/C++ code, either as a Rust program calling a C/C++ library, or vice-versa. The simple Rust example below demonstrates two ways to call the add_one() function: either calling it directly by name, or calling it indirectly via a function pointer passed in from main(). There’s an init() function provided by an external C library. The C library is compiled with CFG enabled. As expected, the C function call is in an unsafe block. However, the function pointer (fptr) is never handled by this unsafe code, so we would not expect it to be modified.

#[link(name = "init")]extern "C" { fn init(y: u32); } // A simple function to increment an integerfn add_one(x: &mut i32) {    *x += 1;}fn do_math(fptr: fn(&mut i32)) {    unsafe{init(add_one as u32);}     let mut x = 1;    add_one(&mut x);    println!("Calling function by name: 1 + 1 = {}", x);     let mut x = 1;    fptr(&mut x);    println!("Calling via function pointer: 1 + 1 = {}", x);} fn main() {    do_math(add_one);}

However, the outcomes of this straightforward program are unexpected:

Calling function by name: 1 + 1 = 2
Calling via function pointer: 1 + 1 = 1

In this example, the init() function reaches beyond its stack frame and corrupts the stack to make fptr point to somewhere in the middle of the add_one() function (if reproducing this example, you might need to change the 0x2D offset, depending on your platform and compiler settings).

#include 
void init(uint32_t y) {
    for (uint32_t n = 0x01; n < 0x20; n++) {
        if (*(&y + n) == y) { 
            *(&y + n) += 0x2D;
        }
    }
}

Although this is a very fabricated example ( hopefully there is n’t any actual codebase with it ), it shows how it’s possible for an attacker to use the linked C/C++ code to violate control flow integrity in the ( safe ) Rust code. To mitigate this vulnerability, we must enable CFG for the Rust code even though the C/C++ code is already compiled with it enabled.

Furthermore, it’s always a good idea to enable CFG even if you’re not explicitly linking against C/C++. This is because any user-space Rust program will probably use OS-provided C++ functionality under the hood ( e .g., to print output to the terminal, as above ).

2. unsafe Rust codebases that are pure

The second case for enabling CFG is in pure Rust codebases using any unsafe code. Similarly to the above example, the init() function could have been a pure Rust function, containing unsafe code that modified the function pointer on the stack, as shown below.

fn init(y: u32) {
    for n in 0x20..0x50 {
        unsafe { if *(&y as *const u32).offset(n) == y { 
            *((&y as *const u32).offset(n) as *mut _) = y + 0x2D }; };
    }
}

Again, this is a fabricated example, but it shows how other components of the ( safe ) Rust program could be harmed by flaws in unsafe Russian code. Enabling CFG for Rust also lessens this, as shown in the example above.

CFG can assist in reducing vulnerabilities caused by bugs in the Rust core language or standard library in addition to codebases using unsafe.

How can I make CFG available in Rust?

CFG is available in Rust 1.47 (currently the nightly version). To enable CFG, simply add the -C control-flow-guard flag. If you’re building with cargo, you can enable CFG using the rustc command cargo rustc -- -C control-flow-guard. Importantly, to get full protection, you need to use a version of the Rust standard library that also has CFG enabled. At present, CFG is not yet enabled in the pre-built versions of the standard library, but you can enable it in your own builds by adding control-flow-guard = true in your config.toml file.

Control Flow Guard Overhead

In terms of binary size and runtime performance, enabling this kind of control flow integrity enforcement typically carries some overhead costs. Both of these aspects are greatly reduced by CFG’s highly optimized design. Due to the use of the same OS-provided checking logic, the overhead costs for the MSVC and LLVM implementations are very similar. The number and frequency of indirect calls made while compiling the program determine the size of any overhead. For instance, enabling CFG for the Rust standard library results in a 0.144 % increase in binary size. According to the table below, enabling CFG in the Clang/LLVM-compiled SPEC CPU 2017 Integer Speed benchmark suite results in approximate runtime overheads of up to 8 %.

Benchmark ( seconds ) Without CFG CFG ( seconds ) Overhead
600.perlbench_s 314 322 2.5%
602.gcc_s 538 546 1.5%
605.mcf_s 723 767 6.1%
620.omnetpp_s 486 521 7.2%
623.xalancbmk_s 225 243 8.0%
625.x264_s 186 193 3.8%
631.deepsjeng_s 326 323 -0.9%
641.leela_s 435 428 -1.6%
657.xz_s 487 488 0.2%
meaning of geometric 381.6 392.7 2.9%

These benchmarks were run on an Intel Xeon W-2155 CPU @ 3.30GHz using clang-cl with the default SPEC CPU flags for Windows/MSVC. The quoted times are the median of three runs. The 648.exchange2s benchmark requires Fortran, so was not included. Performance on the 631.deepsjeng_s and 641.leela_s benchmarks actually _improved when enabling CFG, likely due to better cache alignment.

Observing the future

Control flow integrity is a crucial issue, and both academic literature and practical systems have offered numerous solutions. Some strategies provide more precise enforcement, further reducing the number of legitimate branch targets. For instance, Microsoft recently unveiled XFG as CFG’s replacement. Other solutions use brand-new CPU hardware, like the recently unveiled CET from Intel, which, once widely used, may further enhance performance. Although there are other options on the horizon, CFG, which is currently available on all Windows 10 devices, can still assist in protecting against exploits.

Acknowledgements

It has been a great experience to work with the LLVM and Rust open-source communities. We are especially grateful to the communities for their input in the form of design recommendations, code reviews, and other suggestions.

Senior Researcher Andrew Paverd from Microsoft Research and MSRC


Skip to content