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_SPECULATEwhich 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.