While much of the work on kernel Control Flow Integrity (CFI) is focused on arm64 (since kernel CFI is available on Android), a significant portion is in the core kernel itself (and especially the build system). Recently I got a sane build and boot on x86 with everything enabled, and I’ve been picking through some of the remaining pieces. I figured now would be a good time to document everything I do to get a build working in case other people want to play with it and find stuff that needs fixing.
First, everything is based on Sami Tolvanen’s upstream port of Clang’s forward-edge CFI, which includes his Link Time Optimization (LTO) work, which CFI requires. This tree also includes his backward-edge CFI work on arm64 with Clang’s Shadow Call Stack (SCS).
On top of that, I’ve got a few x86-specific patches that get me far enough to boot a kernel without warnings pouring across the console. Along with that are general linker script cleanups, CFI cast fixes, and x86 crypto fixes, all in various states of getting upstreamed. The resulting tree is here.
On the compiler side, you need a very recent Clang and LLD (i.e. “Clang 10”, or what I do is build from the latest git). For example, here’s how to get started. First, checkout, configure, and build Clang (leave out “--depth=1
” if you want the full git history):
# Check out latest LLVM mkdir -p $HOME/src cd $HOME/src git clone --depth=1 https://github.com/llvm/llvm-project.git mkdir -p llvm-build cd llvm-build # Configure mkdir -p $HOME/bin/clang-release cmake -G Ninja \ -DCMAKE_BUILD_TYPE=Release \ -DLLVM_ENABLE_PROJECTS='clang;lld;compiler-rt' \ -DCMAKE_INSTALL_PREFIX="$HOME/bin/clang-release" \ ../llvm-project/llvm # Build! ninja install
Then checkout, configure, and build the CFI tree. (This assumes you’ve already got a checkout of Linus’s tree.)
# Check out my branch cd ../linux git remote add kees https://git.kernel.org/pub/scm/linux/kernel/git/kees/linux.git git fetch kees git checkout kees/kspp/cfi/x86 -b test/cfi # Use the above built Clang path first for the needed binaries: clang, ld.lld, and llvm-ar. PATH="$HOME/bin/clang-release/bin:$PATH" # Configure (this uses "defconfig" but you could use "menuconfig"), but you must # include CC and LD in the make args or your .config won't know about Clang. make defconfig CC=clang LD=ld.lld # Enable LTO and CFI. scripts/config \ -e CONFIG_LTO \ -e CONFIG_THINLTO \ -d CONFIG_LTO_NONE \ -e CONFIG_LTO_CLANG \ -e CONFIG_CFI_CLANG \ -e CONFIG_CFI_PERMISSIVE \ -e CONFIG_CFI_CLANG_SHADOW # Enable LKDTM if you want runtime fault testing: scripts/config -e CONFIG_LKDTM # Build! make -j$(getconf _NPROCESSORS_ONLN) CC=clang LD=ld.lld
Do not be alarmed by various warnings, such as:
ld.lld: warning: cannot find entry symbol _start; defaulting to 0x1000 llvm-ar: error: unable to load 'arch/x86/kernel/head_64.o': file too small to be an archive llvm-ar: error: unable to load 'arch/x86/kernel/head64.o': file too small to be an archive llvm-ar: error: unable to load 'arch/x86/kernel/ebda.o': file too small to be an archive llvm-ar: error: unable to load 'arch/x86/kernel/platform-quirks.o': file too small to be an archive WARNING: EXPORT symbol "page_offset_base" [vmlinux] version generation failed, symbol will not be versioned. WARNING: EXPORT symbol "vmalloc_base" [vmlinux] version generation failed, symbol will not be versioned. WARNING: EXPORT symbol "vmemmap_base" [vmlinux] version generation failed, symbol will not be versioned. WARNING: "__memcat_p" [vmlinux] is a static (unknown) no symbols
Adjust your .config
as you want (but, again, make sure the CC and LD args are pointed at Clang and LLD respectively). This should(!) result in a happy bootable x86 CFI-enabled kernel. If you want to see what a CFI failure looks like, you can poke LKDTM:
# Log into the booted system as root, then: cat <(echo CFI_FORWARD_PROTO) >/sys/kernel/debug/provoke-crash/DIRECT dmesg
Here’s the CFI splat I see on the console:
[ 16.288372] lkdtm: Performing direct entry CFI_FORWARD_PROTO [ 16.290563] lkdtm: Calling matched prototype ... [ 16.292367] lkdtm: Calling mismatched prototype ... [ 16.293696] ------------[ cut here ]------------ [ 16.294581] CFI failure (target: lkdtm_increment_int$53641d38e2dc4a151b75cbe816cbb86b.cfi_jt+0x0/0x10): [ 16.296288] WARNING: CPU: 3 PID: 2612 at kernel/cfi.c:29 __cfi_check_fail+0x38/0x40 ... [ 16.346873] ---[ end trace 386b3874d294d2f7 ]--- [ 16.347669] lkdtm: Fail: survived mismatched prototype function call!
The claim of “Fail: survived …” is due to CONFIG_CFI_PERMISSIVE=y
. This allows the kernel to warn but continue with the bad call anyway. This is handy for debugging. In a production kernel that would be removed and the offending kernel thread would be killed. If you run this again with the config disabled, there will be no continuation from LKDTM. :)
Enjoy! And if you can figure out before me why there is still CFI instrumentation in the KPTI entry handler, please let me know and help us fix it. ;)
© 2019 – 2020, Kees Cook. This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 License.
Hello Kees, thanks for the article!
I want to ask if the linux kernel has plans to support CFI based on gcc in the future, or if I want to use CFI, I must use clang to compile the kernel.
Best regards!
Comment by ashimida — November 20, 2019 @ 10:21 pm
I’m not aware of a CFI implementation in gcc. Hopefully this will change soon, but for now it’s just Clang.
Comment by kees — November 24, 2019 @ 4:26 am