As of 2016, about 86% of all vulnerabilities on Android are memory safety related. Most vulnerabilities are exploited by attackers changing the normal control flow of an app to perform arbitrary malicious activities with all the privileges of the exploited app. Control flow integrity (CFI) is a security mechanism that disallows changes to the original control flow graph of a compiled binary, making it significantly harder to perform such attacks.
In Android 8.1, we enabled LLVM's implementation of CFI in the media stack. In Android 9, we enabled CFI in more components and also the kernel. System CFI is on by default but you need to enable kernel CFI.
LLVM's CFI requires compiling with Link-Time Optimization (LTO). LTO preserves the LLVM bitcode representation of object files until link-time, which allows the compiler to better reason about what optimizations can be performed. Enabling LTO reduces the size of the final binary and improves performance, but increases compile time. In testing on Android, the combination of LTO and CFI results in negligible overhead to code size and performance; in a few cases both improved.
For more technical details about CFI and how other forward-control checks are handled, see the LLVM design documentation.
Examples and source
CFI is provided by the compiler and adds instrumentation into the binary during compile time. We support CFI in the Clang toolchain and the Android build system in AOSP.
CFI is enabled by default for Arm64 devices for the set of components in
/platform/build/target/product/cfi-common.mk
.
It's also directly enabled in a set of media components' makefiles/blueprint
files, such as /platform/frameworks/av/media/libmedia/Android.bp
and /platform/frameworks/av/cmds/stagefright/Android.mk
.
Implementing system CFI
CFI is enabled by default if you use Clang and the Android build system. Because CFI helps keep Android users safe, you should not disable it.
In fact, we strongly encourage you to enable CFI for additional components. Ideal candidates are privileged native code, or native code that processes untrusted user input. If you're using clang and the Android build system, you can enable CFI in new components by adding a few lines to your makefiles or blueprint files.
Supporting CFI in makefiles
To enable CFI in a make file, such as /platform/frameworks/av/cmds/stagefright/Android.mk
,
add:
LOCAL_SANITIZE := cfi # Optional features LOCAL_SANITIZE_DIAG := cfi LOCAL_SANITIZE_BLACKLIST := cfi_blacklist.txt
LOCAL_SANITIZE
specifies CFI as the sanitizer during the build.LOCAL_SANITIZE_DIAG
turns on diagnostic mode for CFI. Diagnostic mode prints out additional debug information in logcat during crashes, which is useful while developing and testing your builds. Make sure to remove diagnostic mode on productions builds, though.LOCAL_SANITIZE_BLACKLIST
allows components to selectively disable CFI instrumentation for individual functions or source files. You can use a blacklist as a last resort to fix any user-facing issues that might otherwise exist. For more details, see Disabling CFI.
Supporting CFI in blueprint files
To enable CFI in a blueprint file, such as /platform/frameworks/av/media/libmedia/Android.bp
,
add:
sanitize: { cfi: true, diag: { cfi: true, }, blacklist: "cfi_blacklist.txt", },
Troubleshooting
If you're enabling CFI in new components, you may run into a few issues with function type mismatch errors and assembly code type mismatch errors.
Function type mismatch errors occur because CFI restricts indirect calls to only jump to functions that have the same dynamic type as the static type used in the call. CFI restricts virtual and non-virtual member function calls to only jump to objects that are a derived class of the static type of the object used to make the call. This means, when you have code that violates either of these assumptions, the instrumentation that CFI adds will abort. For example, the stack trace shows a SIGABRT and logcat contains a line about control flow integrity finding a mismatch.
To fix this, ensure that the called function has the same type that was statically declared. Here are two example CLs:
- Bluetooth: /c/platform/system/bt/+/532377
- NFC: /c/platform/system/nfc/+/527858
Another possible issue is trying to enable CFI in code that contains indirect calls to assembly. Because assembly code is not typed, this results in a type mismatch.
To fix this, create native code wrappers for each assembly call, and give the wrappers the same function signature as the calling poiner. The wrapper can then directly call the assembly code. Because direct branches are not instrumented by CFI (they cannot be repointed at runtime and so do not pose a security risk), this will fix the issue.
If there are too many assembly functions and they cannot all be fixed, you can also blacklist all functions that contain indirect calls to assembly. This is not recommended as it disables CFI checks on these functions, thereby opening attack surface.
Disabling CFI
We didn't observe any performance overhead, so you shouldn't need to disable CFI. However, if there is a user-facing impact, you can selectively disable CFI for individual functions or source files by supplying a sanitizer blacklist file at compile time. The blacklist instructs the compiler to disable CFI instrumentation in specified locations.
The Android build system provides support for per-component blacklists (allowing you to choose source files or individual functions that will not receive CFI instrumentation) for both Make and Soong. For more details on the format of a blacklist file, see the upstream Clang docs.
Validation
Currently, there are no CTS test specifically for CFI. Instead, make sure that CTS tests pass with or without CFI enabled to verify that CFI isn't impacting the device.