This repo provides tools for compartmentalizing an application and its dependencies using Intel's memory protection keys (MPK) for userspace. The repo includes a tool that rewrites headers to create call gate wrappers as well as the runtime to initialize the protection keys for trusted compartments.
Ubuntu 20.04 is used for testing. Other Linux distributions may or may not work. Adjust the commands below accordingly.
sudo apt install -y libusb-1.0-0-dev libclang-dev llvm-dev \
ninja-build zlib1g-dev python3-pip cmake \
libavformat-dev libavutil-dev pcregrep patchelf
pip install lit
rustup install nightly
Note: Adjust paths to your version of Clang/LLVM
mkdir build && pushd build
cmake .. \
-DClang_DIR=/usr/lib/cmake/clang-12 \
-DLLVM_DIR=/usr/lib/llvm-12/cmake \
-DLLVM_EXTERNAL_LIT=`which lit` \
-G Ninja
Note: Pass -v
to ninja to see build commands and output from failing tests.
ninja check-ia2
First invoke the INIT_RUNTIME
macro once in any binary or shared library to define the number of protection keys that need to be allocated. The argument passed to INIT_RUNTIME
must be a number between 1 and 15. Then the IA2_COMPARTMENT
#define
is used to define trusted compartments at the shared object level. This can be the main executable ELF or any dynamically-linked shared libraries. Memory belonging to a trusted compartment is assigned one of the 15 protection keys and can only be accessed by the shared object itself. Objects that don't explicitly define a compartment are treated as untrusted by default.
To assign a protection key to a trusted compartment, insert #define IA2_COMPARTMENT n
with an argument between 1-15 specifying the index of the protection key, and include ia2_compartment_init.inc
:
#define IA2_COMPARTMENT 1
#include <ia2_compartment_init.inc>
This argument must differ from the other trusted compartments. Trusted compartments must also be aligned and padded properly by using the padding.ld
script in libia2/
. In CMake this is done automatically for executables built with define_test
while libraries built with define_shared_lib
must add LINK_OPTS "-Wl,-T${libia2_BINARY_DIR}/padding.ld"
. To use in manual builds just include -Wl,-T/path/to/padding.ld
in the final compilation step. Manual builds also require disabling lazy binding with -Wl,-z,now
.
Calls between compartments must have call gates to toggle the PKRU permissions at each transition. For direct calls, this is done by rewriting headers to generate the source for a wrapper that provides versions of every function with call gates. These wrappers are specific to both the wrapped library and caller. This means that the generated source must be compiled once per compartment that links against the wrapped library. Each caller's wrapper must define the CALLER_PKEY
macro with the appropriate value for the caller.
We provide a CMake rule to wrap a library or the main executable. This rule builds a wrapper and provides its dependency information to consumers of its outputs. Specifically, wrapper libs also depend on libia2 and have additional required compilation flags (-fno-omit-frame-pointer) for application code.
Usage from CMake looks like this (wrapping myunsafelib
which is used by your existing my_prog
target):
+define_ia2_wrapper(
+ WRAPPER my_wrapper_target
+ WRAPPED_LIB myunsafelib-1.0
+ HEADERS myunsafelib.h myunsafelib_config.h
+ CALLER_PKEY 0
+)
+
add_executable(my_prog main.c)
-target_link_libraries(my_prog PRIVATE myunsafelib-1.0)
+target_link_libraries(my_prog PRIVATE my_wrapper_target)
Wrapped libraries are treated as untrusted by default. If the library being wrapped defined a trusted compartment, COMPARTMENT_PKEY n
must be specified in define_ia2_wrapper. Here n
is the argument used in IA2_COMPARTMENT
to define the compartment. If the caller is an untrusted compartment, set CALLER_PKEY UNTRUSTED
. To create a wrapper for the main binary (i.e. if shared libraries call it directly) the WRAP_MAIN
option must be specified.
See this doc for notes on manual usage.