Intel® Memory Protection Extensions (Intel® MPX) support in the GCC compiler
Note
As of r261304 MPX support has been removed. GCC 9.1 will be released without the functionality. All introduced options have now no effect.
Introduction
Invalid memory access problem is commonly found in many C/C++ programs and leads to time consuming debugging, program instability and vulnerability. Many attacks exploit software bugs related to inappropriate memory accesses caused by buffer overflow (or buffer overruns). Existing set of techniques/tools to find such memory bugs in the programs and defend them from the attacks are software only solutions which result in poor performance of the protected code.
We present the new compiler pass which adds support for recently announced technology Intel® Memory Protection Extensions (http://software.intel.com/en-us/articles/introduction-to-intel-memory-protection-extensions) and is a part of the support of this technology in the OS kernel, binutils and system libraries (see #Links section below).
Intel® MPX introduces new registers called bound registers to hold bounds for pointer. Intel® MPX also introduces new instructions to create bounds, move bounds, load and store bounds in bounds tables, and check pointers against bounds (see details in http://download-software.intel.com/sites/default/files/319433-015.pdf ). The new compiler pass Pointers Checker is designed in the most general way so support could be implemented for any other target using similar hardware feature or software emulation. Current support could be considered as enabling of the technology, there will be more changes for performance tuning.
How to get Intel® MPX enabled GCC
All necessary changes are available starting from GCC 5.0.
Intel® MPX runtime libraries are required to run instrumented binaries. To build these libraries you should use --enable-libmpx (disabled by default) when configure your compiler. Note that you also need Intel® MPX enabled binutils (see #Links section). You should either install new binutils on your system or use --with-as and --with-ld configure options. To produce instrumented binaries you should add '-fcheck-pointer-bounds -mmpx' flags to your compilation string. Note that these flags are supported for C/C++ and x86 target only. For more info on new flags see #Compiler options section.
Definition of terms
Here are some terms we will use in this article.
- INIT bounds - bounds which allow full memory access. Checks against these bounds always pass for all pointers.
- NULL bounds - bounds which do not allow access to memory. Checks against these bounds always fail for all pointers.
- Bounds Table - A data structure used to store bounds for pointers stored in memory. Bounds table associate pointer and its location in memory with bounds of pointer.
Code instrumentation
Memory protection is achieved via code instrumentation. Each memory reference is instrumented with checks of base pointer used for memory access against bounds associated with that pointer. The main problem for the compiler is to determine correct bounds for each dereferenced pointer.
Bounds computation
Bounds are computed by compiler analysis of data flow for used pointer. Note that pointer arithmetic and type casts do not affect bounds values and we just keep looking until reach one of basic pointer sources. There are five such sources:
- Function call. If pointer is returned by call then its bounds are also returned by this call.
Load. If pointer is loaded from the memory then bounds are loaded from the Bounds Table. See #Bounds table management.
- Input function argument. If pointer is an input argument then its bounds should be passed by the caller.
- Object address. If pointer is an address of an object then we just create bounds using object address as a low bound and use its size to compute upper bound. For objects with incomplete type we have to use dynamic bounds computation. Current implementation for i386 target uses size relocation to obtain object size in runtime and compute corresponding bounds.
Field address. When address of a field in object is taken then narrowing procedure is applied. See #Narrowing.
Example:
int foo (void *p) { int *ip = (int *)p; // ip gets bounds passed for input argument p ip += 10; // ip bounds are not changed return *ip; // Checks should be made here }
Example:
int foo (void **p) { int *ip = (int *)(*p); // ip gets bounds loaded from bounds table for pointer *p return *ip; // Checks should be made here }
Example:
int buf[100]; int foo (int i) { int *p = buf; // Bounds [buf, buf + 399] are used return p[i]; // Checks should be made here }
Example:
int *getptr (); int foo (int i) { int *p = getptr (); // returned bounds are associated with p return p[i]; // Checks should be made here }
There are some cases when we cannot find valid sources of pointer. It happens when pointer is a result of cast of non pointer type into pointer. By default compiler does not check such pointers (uses INIT bounds for them). We are considering having a compiler switch to change this behavior.
Bounds table management
Compiler is responsible for managing Bounds Table. It has to find all pointer stores and generate corresponding Bounds Table modification. It is important that in Bounds Table we do not associate pointer location or pointer itself with bounds, but associate pair of pointer and its location with bounds. It allows us:
Have different bounds for pointers of the same value. It may happen when narrowing takes place or when bounds are manually assigned by user (See #Compiler intrinsics and attributes)
Detect pointer modification in the memory by legacy (not instrumented) code (See #Mixing instrumented and legacy code)
Narrowing
When object's field address is taken we narrow object's bounds to the field. No narrowing is applied when address of array element is taken. Following rules are applied when narrowing takes place (including nested field accesses):
- If there are static array accesses then bounds of the outermost array are taken
- If there are no static array accesses then bounds of the innermost field, which is not the first in outer object, are taken
There are some compilation flags which affect narrowing. See #Compiler options.
Example of narrowing result (default behavior):
struct S1 { int f11; //size is 4 int f12; //size is 4 }; struct S2 { S1 f21; //size is 8 S1 f22[10]; //size is 80 S1 f23; //size is 8 }; struct S3 { S1 f31; //size is 8 S2 f32[10]; //size is 960 S2 f33; //size is 96 }; S3 s; //size is 1064 &s.f31.f12; // Bounds are [&s.f31.f12, &s.f31.f12 + 3] &s.f31.f11; // Bounds are [&s, &s + 1063] &s.f32[5].f22[4].f12; //Bounds are [&s.f32, &s.f32 + 959] &s.f33.f22[4].f12; //Bounds are [&s.f33.f22, &s.f33.f22 + 79] &s.f33.f23.f11; //Bounds are [&s.f33.f23, &s.f33.f23 + 7]
Programming Model
In the most cases enabling Pointer Checker is as simple as adding a single compiler switch. But in some cases user may be required to change code to avoid unexpected bound violations. Here are some examples when user may have to change code:
- Field address. In some cases address of the structure's field is used to access whole structure. In instrumented code it may cause failures due to narrowing. A different way to obtain field's address should be used in such case.
Example:
struct S { int a; int b[10]; int c; }; #define OFFSETOF(s,f) (((char *)(&s)) + __builtin_offsetof (typeof (s), f)) void print (int *p, int i) { printf ("%d\n", p[i]); } int foo (S &s) { print ((int *)s.b, 10); //Bounds violation print (&s.c, -1); //Bounds violation print ((int *)OFFSETOF (s, b), 10); //OK print ((int *)OFFSETOF (s, c), -1); //OK }
Variable arrays in objects. It is a common practice to have variable-length object using flexible array member. To avoid problems with narrowing compiler should know about all such fields. It automatically assumes that array field with zero length has variable length, but there are cases when flexible arrays are declared with non-zero length. In such cases you should mark them with a special attribute 'bnd_variable_size' (See #Compiler intrinsics and attributes)
In some cases users also may want to manually control bounds for some pointers (e.g. when custom memory allocation is used), modify Bounds Table (e.g. to reflect pointer update in memory performed in legacy code) and introduce manual checks (e.g. before legacy code call). Compiler provides a set of intrinsics for manual code instrumentation. Note that all intrinsics behavior depends on -fcheck-pointer-bounds switch.
Compiler intrinsics and attributes
Here is a full list of intrinsics added to GCC:
void * __bnd_set_ptr_bounds (const void * q, size_t size)
- Return a new pointer with the value of q, and associate it with the bounds [q, q+size-1]
Example:
p = __bnd_set_ptr_bounds (q, 8); //Associate p with bounds [q, q + 7] and value q //Equal to p = q when instrumentation is disabled
void * __bnd_narrow_ptr_bounds (const void *p, const void *q, size_t size)
- Return a new pointer with the value of p and associate it with the narrowed bounds formed by the intersection of bounds associated with q and the [p, p + size - 1].
Example:
r = __bnd_narrow_ptr_bounds (p, q, 8); //Associate pointer r with bounds formed by the intersection (bnd(q), [p, p + 7]) and the value p //Equal to q = r when instrumentation is disabled
void * __bnd_copy_ptr_bounds (const void *q, const void *r)
- Return a new pointer with the value of q, and associate it with the bounds already associated with pointer r (essentially BNDMOV with pointer association).
Example:
p = __bnd_copy_ptr_bounds (q, r); //Associate pointer p with bounds of r and the value q //Equal to p = q when instrumentation is disabled
void * __bnd_init_ptr_bounds (const void *q)
- Return a new pointer with the value of q, and associate it with INIT bounds.
Example:
p = __bnd_init_ptr_bounds (q); //Associate pointer p with INIT bounds and the value q //Equal to p = q when instrumentation is disabled
void * __bnd_null_ptr_bounds (const void *q)
- Return a new pointer with the value of q, and associate it with NULL bounds.
Example:
p = __bnd_null_ptr_bounds (q); //Associate pointer p with NULL bounds and the value q //Equal to p = q when instrumentation is disabled
void __bnd_store_ptr_bounds (const void **ptr_addr, const void *ptr_val)
- Store the bounds associated with pointer ptr_val and location ptr_addr into Bounds table. This can be useful to propagate bounds from legacy code without touching the associated pointer's memory when pointers were copied as integers.
Example:
__bnd_store_ptr_bounds (p, q); //Store the bounds associated with pointer q and location p to Bounds Table. //Ignored when instrumentation is disabled
void __bnd_chk_ptr_lbounds (const void *q)
- Check if the pointer is within the lower bounds of its associated bounds.
Example:
__bnd_chk_ptr_lbounds (q); //Gets the bounds associated with q, and do lower bound check on it with q //Ignored when instrumentation is disabled
void __bnd_chk_ptr_ubounds (const void *q)
- Check if the pointer is within the upper bounds of its associated bounds.
Example:
__bnd_chk_ptr_ubounds (q); //Gets the bounds associated with q, and do upper bound check on it with q //Ignored when instrumentation is disabled
void __bnd_chk_ptr_bounds (const void *q, size_t size)
- Check that [q, q + size - 1] is within the lower and upper bounds of its associated bounds.
Example:
__bnd_chk_ptr_bounds (q, 8); //Gets the bounds associated with q, and do bounds check on it with [q, q + 7] //Ignored when instrumentation is disabled
const void * __bnd_get_ptr_lbound (const void * q)
- Return the lower bound (which is a pointer) associated with the pointer q. This is at least useful for debugging using printf.
Example:
lb = __bnd_get_ptr_lbound (q); printf ("lb(q)=%p", lb);
const void * __bnd_get_ptr_ubound (const void * q)
- Return the upper bound (which is a pointer) associated with the pointer q. This is at least useful for debugging using printf.
Example:
ub = __bnd_get_ptr_ubound (q); printf ("ub(q)=%p", ub);
Here is a full list of attributes added to GCC:
bnd_legacy
- Used to prevent generation of BND prefix and parameter passing code for a legacy function call. Also used to prevent instrumentation for selected functions.
Example:
__attribute__((bnd_legacy)) int* legacy_function (int*); <some code> int *p = legacy_function (q); //No BND prefix for CALL and no bounds arguments passed
Example:
__attribute__((bnd_legacy)) int* legacy_function (int* p) //No incoming bounds { <some_code> *p = 0; //No bounds checks <some code> return p; //No BND prefix for RET and no bounds returned }
bnd_variable_size
- This attribute is used to mark variable sized fields in objects.
Example:
struct dyn_data { int additional_data_length; char contents[4] __attribute__((bnd_variable_size)); //No narrowing for this field };
bnd_instrument
- This attribute is used to mark functions to instrument in case -fchkp-instrument-marked-only is used.
Compiler macros
If user wants to have a special code version for pointer checker, a special macro may be used. Here are macros defined by compiler:
__CHKP__ is defined when -fcheck-pointer-bounds flag is passed
__MPX__ is defined when -mmpx flag is passed
Compiler options
Here is a set of compiler switches that may be used to control instrumentation:
-fcheck-pointer-bounds
- Enable instrumentation
-fchkp-check-incomplete-type
- Add instrumentation for use of variables with incomplete type (otherwise INIT bounds are used for such objects). By default is enabled when -fcheck-pointer-bounds is specified.
-fchkp-zero-input-bounds-for-main
- Use zero bounds for all incoming arguments in 'main' function. used for debugging purposes. Most probably your setup is wrong if you need it. Disabled by default.
-fchkp-treat-zero-size-reloc-as-infinite
- With this option dynamic zero size is treated as infinite. Can be useful if you link with legacy objects with incorrect size information. Disabled by default.
-fchkp-first-field-has-own-bounds
- Forces instrumentation to use narrowed bounds for address of the first field in the structure. By default pointer to the first field has the same bounds as pointer to the whole structure.
-fchkp-narrow-bounds
- Control how Pointer Bounds Checker handle pointers to object fields. When narrowing is on, field bounds are used. Otherwise full object bounds are used.
-fchkp-narrow-to-innermost-array
- Forces instrumentation to use bounds of the innermost arrays in the case of nested static arrays access. By default outermost array is used.
-fchkp-optimize
Allow optimizations of instrumentation. By default allowed on optimization levels >0.
-fchkp-use-fast-string-functions
- Allow usage of *_nobnd versions (which assumes there is no bounds copying) of string functions when optimizing instrumentation. Disabled by default.
-fchkp-use-nochk-string-functions
- Allow usage of *_nochk versions (which do not check memory accesses) of string functions when optimizing instrumentation. Disabled by default.
-fchkp-use-static-bounds
- Create and use static bounds objects instead of creating them dynamically each time it is required. Enabled when -fcheck-pointer-bounds is specified.
-fchkp-use-static-const-bounds
- Use static variables for constant bounds rather than generate them each time it is required.
-fchkp-check-read
- Generate checks for all read accesses to memory. Enabled by default when -fcheck-pointer-bounds is specified.
-fchkp-check-write
- Generate checks for all write accesses to memory. Enabled by default when -fcheck-pointer-bounds is specified.
-fchkp-store-bounds
- Generate bounds stores for pointer writes. Enabled by default when -fcheck-pointer-bounds is specified.
-fchkp-instrument-calls
- Generate bounds passing for calls. Enabled by default when -fcheck-pointer-bounds is specified.
-fchkp-instrument-marked-only
- Instrument only functions marked with bnd_instrument attribute.
-fchkp-use-wrappers
- Wrap calls to builtin functions which have instrumented variants in libmpxwrappers.
-Wchkp
- Warn about memory access errors found by Pointer Bounds Checker.
-static-libmpx
- Link with static version of libmpx library.
-static-libmpxwrappers
- Link with static version of libmpxwrappers library.
MPX Runtime options
MPX based instrumentation requires a run-time library to enable MPX in a hardware and handle bounds violation signals. GCC provides library libmpx which is linked to a program by default when '-fcheck-pointer-bounds-mmpx' is used. The runtime library behavior can be influenced using various environment variables:
CHKP_RT_OUT_FILE
- Set output file for info and debug [default: stdout]
CHKP_RT_ERR_FILE
- Set output file for error [default: stderr]
CHKP_RT_VERBOSE
- Set verbosity level. 0 - print only internal errors; 1 - print summary; 2 - print bound violations information; 3 - print debug information [default: 2]
CHKP_RT_MODE
- Set MPX runtime behavior on #BR exception (stop | count) [default: count]
CHKP_RT_ADDPID
- Generate out, err files for each process. Generated files will be MPX_RT_{OUT,ERR}_FILE.pid [default: no]
CHKP_RT_BNDPRESERVE
- Set value for BNDPRESERVE bit of BNDCFG register [default: 0]
CHKP_RT_PRINT_SUMMARY
- Print summary at the end of the run [default: no]
CHKP_RT_HELP
- Print help and exit [default: no]
Mixing instrumented and legacy code
Pointer Checker is designed to provide an ability to mix instrumented and legacy code. Legacy code does not experience any change in its functionality. Instrumented applications can link with, call into, or be called from legacy software. It allows granular control on providing protection to higher priority modules first. Possibility to mix instrumented and legacy codes is achieved by the following few rules:
- When instrumented code is called from legacy code, it gets INIT bounds for all incoming arguments
- When legacy code returns pointer, INIT bounds are returned for that pointer
- When legacy code changes pointer in memory, instrumented code gets INIT bounds when loads this pointer
First two conditions should be achieved by designing proper ABI which is backward compatible with the legacy one. The last one is achieved by Bounds Table which associate pointer and its location with bounds. If legacy code changes a pointer then, when we request bounds for new pointer value, Bounds Table detects pointer change and returns INIT bounds. Note that in rare cases Bounds Table mechanism may miss bounds changes. We may model a case when legacy code rewrites a pointer in a memory with pointer of the same value but with different bounds. In such case false bound violation may occur. User is responsible for avoiding such cases. To get higher level of protection try to use instrumentation for modules generating external data.
Other checkers in GCC
Currently GCC has address sanitizer (https://code.google.com/p/address-sanitizer/) and mudflap (http://gcc.gnu.org/wiki/Mudflap_Pointer_Debugging) which were added to solve the problem with invalid memory accesses. The main difference of the described approach from these existing solutions is that each memory access is checked against pointer bounds, not against tables of valid addresses. It implies following advantages and drawbacks:
- When overflows occur and pointer points out of the object, it may still point to a valid memory. Pointers Checker instrumentation allows to detect such corruptions.
- Narrowing is possible for Pointers Checker instrumentation only. E.g. it allows to catch overflow in static array which is a field of a structure.
- Pointers Checker instrumentation does not detect dangling pointers.
- Pointers Checker instrumentation allows manual bounds assignment for pointers.
- It seems compatibility with legacy code is much higher for Pointers Checker instrumented code because it may cause false violations in really rare cases only.
- Pointers Checker instrumentation is more complex. Compiler has to track all pointers movements.
- [i386 target only] Pointers Checker instrumentation has hardware support (Intel® MPX).
- [i386 target only] Pointers Checker code may be disabled in runtime (all Intel® MPX instruction are executed as NOPs) resulting in minimum overhead. Thus it is possible to use single binary file for both debug and release version of a product.
Implementation details
To operate with bounds in GIMPLE we introduce new basic type BOUND_TYPE. We also add additional operand to return statement to hold returned bounds. We do not introduce any new expression to work with new type but use builtin function calls instead.
Builtins used for instrumentation
We define a set of generic builtins operating with bounds but target may have own implementation and provide them via builtin_chkp_function hook. Currently generic builtins are not implemented and target has to provide own version for all of them to support instrumentation pass. Here is a list of introduced builtins:
bnd __chkp_bndmk (const void *lb, size_t s) - BUILT_IN_CHKP_BNDMK
- Create and return bounds with low bound LB and size S.
void __chkp_bndstx (const void **loc, const void *ptr, bnd b) - BUILT_IN_CHKP_BNDSTX
- Associate bounds B with pointer PTR and its location LOC in Bounds Table.
bnd __chkp_bndldx (const void **loc, const void *ptr) - BUILT_IN_CHKP_BNDLDX
- Return bounds associated with pointer PTR and location LOC in Bounds Table.
void __chkp_bndcl (bnd b, const void *ptr) - BUILT_IN_CHKP_BNDCL
- Check pointer PTR against lower bound in B.
void __chkp_bndcu (bnd b, const void *ptr) - BUILT_IN_CHKP_BNDCU
- Check pointer PTR against lower bound in B.
bnd __chkp_bndret (void *p) - BUILT_IN_CHKP_BNDRET
- Return bounds associated with the return value of the last called function.
bnd __chkp_intersect (bnd b1, bnd b2) - BUILT_IN_CHKP_INTERSECT
- Return intersection of bounds B1 and B2.
bnd __chkp_narrow (const void *ptr, bnd b, size_t s) - BUILT_IN_CHKP_NARROW
- Return intersection of bounds B and [ptr, ptr + s - 1].
size_t __chkp_sizeof (tree var) - BUILT_IN_CHKP_SIZEOF
- Return a size of an object VAR. Used when object has incomplete type and its size cannot be determined statically.
const void *__chkp_extract_lower (bnd b) - BUILT_IN_CHKP_EXTRACT_LOWER
- Extract lower bound from B and return it.
const void *__chkp_extract_upper (bnd b) - BUILT_IN_CHKP_EXTRACT_UPPER
- Extract upper bound from B and return it.
Target hooks
A set of hooks is introduced to make instrumentation adjustable to different targets. Here is a list of introduced target hooks.
tree builtin_chkp_function (unsigned fcode)
- Return a target specific hook fndecl to be used instead of generic hook with code FCODE.
tree chkp_bound_type (void)
- Return a type node to be used for bounds.
enum machine_mode chkp_bound_mode (void)
- Return a mode to be used for bounds.
enum machine_mode chkp_bound_mode (void)
- Return a mode to be used for bounds.
tree chkp_make_bounds_constant (HOST_WIDE_INT lb, HOST_WIDE_INT ub)
- Return constant used to statically initialize constant bounds with specified lower bound LB and upper bounds UB.
int chkp_initialize_bounds (tree var, tree lb, tree ub, tree *stmts)
- Generate a list of statements STMTS to initialize pointer bounds variable VAR with bounds LB and UB. Return the number of generated statements.
tree fn_abi_va_list_bounds_size (tree fndecl)
- Return size for va_list object for specified function FNDECL or integer_zero_node if it is a scalar pointer.
rtx load_bounds_for_arg (rtx location, rtx pointer, rtx bnd_slot
- Expand pass uses this hook to emit insn to get input bounds from specified BND_SLOT for POINTER passed in LOCATION.
rtx store_bounds_for_arg (rtx pointer, rtx location, rtx bounds, rtx bnd_slot)
- Store BOUNDS passed for POINTER located at LOCATION into BND_SLOT.
rtx load_returned_bounds (rtx slot
- This hook is used by expand pass to emit insn to load bounds returned by function call in SLOT. Hook returns RTX holding loaded bounds.
rtx store_returned_bounds (rtx slot, rtx bounds
- This hook is used by expand pass to emit insn to store BOUNDS returned by function call into SLOT.
void setup_incoming_vararg_bounds (cumulative_args_t args_so_far, enum machine_mode mode, tree type, int *pretend_args_size, int second_time
- Use it to store bounds for anonymous register arguments stored into the stack.
rtx chkp_function_value_bounds (const_tree ret_type, const_tree fn_decl_or_type, bool outgoing)
- Define this to return an RTX representing the place where a function returns bounds for returned pointers.
Instrumentation clones
In instrumented code each pointer is provided with bounds. For input pointer parameters it means we also have bounds passed. For calls it means we have additional bounds arguments for pointer arguments.
To have all IPA optimizations working correctly we have to express data flow between passed and received bounds explicitly via additional entries in function declaration arguments list and in function type. Since we may have both instrumented and not instrumented code at the same time, we cannot replace all original functions with their instrumented variants. Therefore we create clones (versions) instead.
Instrumentation clones creation is a separate IPA pass which is a part of early local passes. Clones are created after SSA is built (because instrumentation pass works on SSA) and before any transformations which may change pointer flow and therefore lead to incorrect code instrumentation (possibly causing false bounds check failures).
Instrumentation clones have pointer bounds arguments added right after pointer arguments. Clones have assembler name of the original function with suffix added. New assembler name is in transparent alias chain with the original name. Thus we expect all calls to the original and instrumented functions look similar in assembler.
During instrumentation versioning pass we create instrumented versions of all function with body and also for all their aliases and thunks. Clones for functions with no body are created on demand (usually during call instrumentation).
Original and instrumented function nodes are connected with IPA reference IPA_REF_CHKP. It is mostly done to have reachability analysis working correctly. We may have no references to the instrumented function in the code but it still should be counted as reachable if the original function is reachable.
When original function bodies are not needed anymore we release them and transform functions into a special kind of thunks. Each thunk has a call edge to the instrumented version. These thunks help to keep externally visible instrumented functions visible when linker resolution files are used. Linker has no info about connection between original and instrumented function and therefore we may wrongly decide (due to difference in assembler names) that instrumented function version is local and can be removed.
Instrumentation pass
The instrumentation pass works after all instrumentation clones are created and code is transformed into SSA form. Here is a list of pass responsibilities (all example dumps use i386 target versions of builtins):
Bounds computation. Bounds are computed in accordance with #Bounds computation. Here is an example of instrumented code (GIMPLE dump):
foo.chkp () { __bounds_type __bound_tmp.0; int buf[100]; <bb 3>: __bound_tmp.0_1 = __builtin_ia32_bndmk (&buf, 400); <bb 2>: goo.chkp (&buf, __bound_tmp.0_1); buf ={v} {CLOBBER}; return; }
Memory accesses instrumentation. Compiler detects all memory accesses and inserts code to check used address against computed bounds. Here is an example of instrumented code (put zero into p[i]):
<bb 2>: _2 = (long unsigned int) i_1(D); _3 = _2 * 4; _6 = p_4(D) + _3; __builtin_ia32_bndcl (__bounds_of_p_5, _6); _8 = _6 + 3; __builtin_ia32_bndcu (__bounds_of_p_5, _8); *_6 = 0;
Calls instrumentation. For call statements we replace function decl with instrumented one and add required bound arguments. Here is an example of instrumented 'Hello world' code:
foo.chkp () { __bounds_type __bound_tmp.0; <bb 3>: __bound_tmp.0_1 = __builtin_ia32_bndmk ("Hello world!", 13); <bb 2>: __builtin_puts.chkp (&"Hello world!"[0], __bound_tmp.0_1); return; }
Returns instrumentation. We add new operand to return statement to hold returned bounds and instrumentation pass is responsible to fill this operand with correct bounds.
Bounds Table updates. Each pointer is stored into the memory should have its bounds stored in the corresponding row of the Bounds Table. Compiler generates appropriate code to have the bounds table in the consistent state. Here is an example of instrumented pointer store:
foo.chkp (int * p, __bounds_type __bounds_of_p) { <bb 2>: stored_ptr = p_1(D); __builtin_ia32_bndstx (&stored_ptr, p_1(D), __bounds_of_p_2); return; }
User builtins transformation. All user builtin calls (see #Compiler intrinsics and attributes) are translated into internal builtin calls (see #Builtins used for instrumentation).
- Example user code:
void foo (int *p, unsigned size) { __bnd_chk_ptr_bounds (p, size); access_and_store (p, size); __bnd_store_ptr_bounds (&stored_ptr, p); }
- Instrumented code:
foo.chkp (int * p, __bounds_type __bounds_of_p, unsigned int size) { long unsigned int D.2239; long unsigned int _2; sizetype _6; int * _7; <bb 2>: _2 = (long unsigned int) size_1(D); __builtin_ia32_bndcl (__bounds_of_p_4, p_3(D)); _6 = _2 + 18446744073709551615; _7 = p_3(D) + _6; __builtin_ia32_bndcu (__bounds_of_p_4, _7); access_and_store.chkp (p_3(D), __bounds_of_p_4, size_1(D)); __builtin_ia32_bndstx (&stored_ptr, p_3(D), __bounds_of_p_4); return; }
Static constructors
We introduce static constructors to initialize static bounds and Bounds Table for statically initialized pointers. All statically initialized objects are registered from front-end parsers in a special map. After module compilation we generate constructor (or few constructors to avoid very big functions) to initialize all emitted static pointers. The technique to build such constructors is very simple. We just create functions holding initialization code and put special mark (attribute) to the function. During instrumentation pass all required Bound Table modifications are added automatically and after that we (detecting constructor mark) just remove original initialization code.
Example
- Source code:
int buf[100]; int *p = buf;
- GIMPLE before instrumentation:
_GLOBAL__sub_P_00102_0_p () { <bb 2>: p = &buf; return; }
- GIMPLE after instrumentation:
_GLOBAL__sub_P_00102_0_p () { __bounds_type __bound_tmp.0; <bb 3>: __bound_tmp.0_1 = __builtin_ia32_bndmk (&buf, 400); <bb 2>: __builtin_ia32_bndstx (&p, &buf, __bound_tmp.0_1); return; }
Support in expand
We introduce some changes in expand pass to handle incoming bounds, bounds passed to calls and returned bounds. We define two new macros (DECL_BOUNDS_RTL and SET_DECL_BOUNDS_RTL) to associate PARM_DECL and RESULT_DECL with RTL slot where input or returned bounds are located.
To handle bounds passed to calls we change structure representing an argument by adding fields to hold original bounds, expanded bounds and bounds slot to be used to pass bounds. If passed argument should have a slot for its bounds then target hook function_arg returns it in PARALLEL expr (similarly to the way few regular registers are returned). Special function chkp_split_slot is used to split returned slot into one used to pass value and other one used to pass bounds. If bounds slot is a register then expanded bounds are just simply moved to it. If it is an integer constant then it is id of special slot to Bounds Table to be used for bounds passing. Target hook store_bounds_for_arg is used to store it then. The same hook is used when bounds slot is NULL (i.e. when pointer is passed in a memory, bounds are stored in Bounds Table in a regular way). Bounds loading happens in a similar way but using load_bounds_for_arg target hook.
Target support
Currently support in implemented for i386 target only. Target support consists of:
- Intel® MPX hardware features support (new registers, new prefixes and new instructions)
New ABI support (http://software.intel.com/en-us/articles/linux-abi)
- New hooks support
- New builtins support (in the most cases builtin is mapped to Intel® MPX instruction)
- Size relocations support (used for BUILT_IN_CHKP_SIZEOF support)
Known issues
- C99 VLA is not supported. Overflows in such arrays might not be caught
Links
* Support of the Intel® MPX in binutils - http://sourceware.org/ml/binutils/2013-07/msg00233.html
* Since real hardware does not exist so far, anyone can use Intel® SDE to try new technology, see http://software.intel.com/en-us/articles/using-intel-mpx-with-the-intel-software-development-emulator. Binaries of MPX binutils and GCC are provided there.