Compiler support for mitigations

Updated on 03/Jan/2018

This page should be read in conjunction with the Cache Speculation Side-channels whitepaper.

It is expected that defences against Variant 1 form constructions will always require some software support.  This page describes a new compiler builtin function that can be used to provide that support.

Note

At the time of writing the proposed builtin function is still subject to review by the developer community; changes to the specification are therefore still possible. This page will be updated to reflect any changes made.

Implementations have been developed for both GCC and LLVM and are being submitted for community review.

This builtin function will also be supported in Arm Compiler 6 and Arm Allinea Studio.  Please feel free to contact Arm by email at support-sw@arm.com or submit a support ticket for details of how to get updated Arm Compiler 6 and Arm Allinea Studio toolchains.

New builtin function

Arm has introduced the following builtin function:

TYP __builtin_load_no_speculate
        (const volatile TYP *ptr,
         const volatile void *lower,
         const volatile void *upper,
         TYP failval,
         const volatile void *cmpptr)

Where TYP can be any integral type (signed or unsigned char, int, short, long, etc) or any pointer type.
The builtin implements the following behavior:

inline TYP __builtin_load_no_speculate
         (const volatile TYP *ptr,
          const volatile void *lower,
          const volatile void *upper,
          TYP failval,
          const volatile void *cmpptr)
{
  TYP result;
  if (cmpptr >= lower && cmpptr < upper)
    result = *ptr;
  else
    result = failval;
  return result;
}

In order to defend against speculative side channel execution attacks, the builtin will also ensure that if ptr is dereferenced speculatively (that is, without checking the boundary conditions) the result will not be used for further speculation unless the boundary conditions are satisfied. If they are not satisfied, further speculation will either be inhibited entirely, or will continue only using failval.

The use of const volatile qualifiers on all the pointer formal parameters ensures that the parameter can take almost any type-qualified pointer as an argument without the need for additional casts.

The builtin is defined for all architectures.

Does my compiler support the builtin?

Compilers supporting the new builtin define the pre-processor macro

__HAVE_LOAD_NO_SPECULATE
which can be tested during pre-processing.

Using the builtin

Consider the following function:

int array[N];
int foo (unsigned n)
{
  int tmp;
  if (n < N)
    tmp = array[n]
  else
    tmp = FAIL;
  return tmp;
}

This can result in a speculative return of the value at array[n], even if n >= N. To mitigate against this, we can use the new builtin:

int foo (unsigned n)
{
  int *lower = array;
  int *ptr = array + n;
  int *upper = array + N;
  return __builtin_load_no_speculate (ptr, lower, upper,
                                      FAIL, ptr);
}

This will ensure that speculative execution can only continue using a value stored within the array or with FAIL. Neither of these values is likely to be of value to an attacker.

Simplifications

To simplify usage, cmpptr may be omitted, in which case the compiler will use the value of ptr whenever cmpptr is referenced.  If cmpptr is omitted then failval may also be omitted and failval will take the default value of 0 (NULL for a pointer).

Additionally either lower or upper may be expressed as a literal NULL causing the builtin to expand without the bounds check for the operand that is NULL (it is not permitted to omit both bounds checks in this way).

For example, to safely dereference ptr only when it is greater than lower and to use 0 as the failsafe result, we can write:

t = __builtin_load_no_speculate (ptr, lower, NULL);

which architecturally implements:

{
  TYP result;
  if (ptr >= lower) // Substituting ptr for cmpptr
    result = *ptr;
  else
    result = 0;     // Substituting 0 for failval
  return result;
}

Note

The upper bounds check has been omitted entirely because that was an explicit NULL in the source code.  This is notably different from a variable that happens to contain NULL at the time of the check, which would almost certainly lead to the upper bounds check failing.

More complex cases

It is not always possible to use the builtin to directly protect atomic data structures or the primitives that are sometimes used to update them.  However, we can protect use of the result from such an access using the builtin.  For example, if we have:

int *atomic_ptr, v;

if (atomic_ptr < upper)
  v = __atomic_fetch_and inc (atomic_ptr);

We can protect the result by re-writing this as:

int *atomic_ptr, v;

if (atomic_ptr < upper)
{
  int tmp_v = __atomic_fetch_and_inc (atomic_ptr);
  v = __builtin_load_no_speculate (&tmp_v, NULL, upper, 0,
                                   atomic_ptr);
}

Notice how we push the result of the atomic operation into a memory location by taking its address in the builtin. However, we use the original range check conditions (atomic_ptr < upper) as the builitin’s guard to ensure that any speculative accesses are still correctly guarded (a range check on the address used by tmp_v would never be useful for inhibiting speculation).

Additional considerations

The builtin will only prevent speculation when the calculation of cmpptr, lower and upper is correct, and a range check on the result of any comparisons is predicted to be correct when it is, in fact, wrong. That is, it will not give you protection against cmpptr itself being a speculative value.  To protect against that you need to move one stage further back and protect that calculation as well (or instead, if that is sufficient).

For example, the sequence:

if (some_condition)
  upper -= *p2;
if (ptr < upper)
  v = __builtin_load_no_speculate (ptr, NULL, upper);

will not provide protection against the some_condition test being speculatively skipped and thus potentially using a value of upper that is different from that which would be expected in a non-speculative execution of the program.

Migration path

Arm has developed a header file which provides a migration path to using the builtin function for users who are unable to upgrade to a compiler which supports it. Arm recommends using an upgraded compiler where possible to ensure the most comprehensive support for the mitigation provided by the builtin function.

The header file is available at http://github.com/arm-software/speculation-barrier. The header file provides three macros, which correspond to the three, four, and five argument variants of the builtin.

These are:

load_no_speculate (__ptr, __low, __high)
load_no_speculate_fail (__ptr, __low, __high, __failval)
load_no_speculate_cmp (__ptr, __low, __high, __failval, __cmpptr)

The macros can be used in a similar manner to the builtin function. A user should include "speculation_barrier.h". The example given above can then be written as:

#include "speculation_barrier.h"
int foo (unsigned n) 

  int *lower = array;
  int *ptr = array + n; 
  int *upper = array + N; 
  return load_no_speculate_fail (ptr, lower, upper, FAIL);
}

The header has been tested to work with Arm Compiler 6 versions 6.5 and above, GCC versions 4.8 and above, including GCC 7 with prototype support for the compiler builtin function, and with an LLVM/Clang development toolchain (version 6.0.0).