Spectre v1 Mitigation via Compiler options

What is Spectre v1?

Spectre Logo
(Quelle: https://spectreattack.com/)

When I first heard about Spectre, I thought that it was quite a fitting name for two reasons. Firstly, it ignores standard security measures like a spectre passes through a wall without problems. Secondly, its implications and the sheer scale of vulnerable systems were surely a shock for security professionals like if they had seen a ghost.

In June 2017, Google Project Zero employee Jann Horn and a separate team around Paul Kocher independently discovered the problem in the design of modern microprocessors. The affected companies were informed first to give them time to handle the problem before the information was finally made public in 2018. [1]

How does it work?

 

Spectre v1 exploits the structure of modern microprocessors, which use so-called speculative execution to increase performance. It is even hidden in the first four letters „SPECtre“. Speculative execution works by already executing later instructions on suspicion. If it turns out the suspicion was false, the results are simply discarded. One type of speculative execution is branch prediction, where the taken path in a conditional branch is predicted.

It’s important to note that a wrong prediction can happen and in that case, its results must not become visible. The state of the memory and registers needs to be rolled back.  The problem is that even though the direct results from misspeculation are reverted, indirect traces are still left in the system, especially the cache. These traces in the cache can be analyzed via a timing attack to extract details about the discarded results. By comparing the access times for different values it is possible to find out which values were cached because of the misprediction.

The idea of Spectre is to purposefully cause such a branch misprediction and afterward analyze these reaction times. As triggering such a situation is possible rather reliably, the threat of the exploit is high. The problem is that over this side-channel attack it is also possible to read sensitive data like passwords, private keys, contents of other browser tabs, etc.. The discoverers published a paper including a proof-of-concept example in JavaScript, which they tested in Google Chrome [2].

As mentioned, under certain circumstances this guess on the right branch will be wrong. A way to provoke a misprediction is to train the microprocessor with valid input values to a conditional code block. After enough training, the cache is manipulated to not contain all information to evaluate the condition. If the attacker now provides an „invalid“ input value, the processor will speculatively execute the conditional code block nevertheless.

If that code has a specific structure, it is vulnerable to a cache-timing-based side-channel attack.

 

Example Code

An example snippet of vulnerable pseudo-code could look like this:

if index < length(array1):
x = array1[index]
y = array2[x * 4096]

In the above example, the inputs are the values of index. The attacker is interested in a secret value that lies somewhere in memory behind array1.

During the training phase, inputs smaller than the length of array1 are used. Then the attacker will send an index greater than or equal to the length of array1. Additionally, it will evict the length information of array1 from the cache, forcing the processor to speculate about the outcome of the range check. The branch predictor assumes from its prior training that this condition is probably true. It executes the out-of-bounds access on array1 and stores the retrieved value in x. Afterward, it uses x as index into a second array, array2. This second array access has an effect on the cache: the piece of array2 that has just been accessed will now be „warm“; the other parts of array2 will remain „cold“.

After the processor realizes that this was a misprediction, it will roll back the value of x and other traces – but, unfortunately, it won’t roll back the cache state. Now the attacker just needs to do a timing analysis to find out which piece of array2 is the „hot“ one. The position of this „hot“ piece equals the secret value x the attacker is after. By systematically repeating this process, it is possible to successively read out entire memory areas.

Mitigation

The most effective and obvious way to mitigate Spectre vulnerabilities would be to use microprocessors without speculative execution. The problem that would come with it is a considerable drop in performance. Just removing speculative-execution capabilities from the existing microprocessors isn’t a practicable way to tackle the problem. Therefore, other ways to handle the problem are needed. Today I will talk about one of these ways to mitigate the risk by influencing the vulnerability through compiler options. Even though this blog article will focus on the possibilities with ICC (the Intel C Compiler), other compilers such as GCC offer similar options.

Turning off Speculative Execution in the Code

While completely foregoing speculative execution isn’t an option, it is possible to temporarily disable it on critical code parts. ICC offers the following two compiler options: -mconditional-branch=all-fix and -mconditional-branch=pattern-fix¹, which reduce the usage of speculative execution.

When using these options, a code analysis is done to find the potentially critical parts in the code. The options disable speculative execution in vulnerable code snippets by inserting LFENCE instructions at critical sites. LFENCE forces the processor to wait for all prior instructions to finish and therefore prevents the result of a misprediction to influence later lines. [3]  In the above example, the LFENCE command would be placed between the range check and the first array access.

Of course, this will have an impact on the performance. Depending on the application, this impact might be too high for practical use. The option pattern-fix will analyze the code during compilation and insert the instruction at all places where the compiler recognizes a vulnerable pattern. The other option, all-fix, starts from the other direction and tends to insert more LFENCE as it guarantees to either prevent speculative execution or to be sure that there is no observable side-channel.  As the compiled assembler code normally contains more of the LFENCE commands, there is also a higher performance reduction than for the  pattern-fix variant.

Influence of Compiler Optimization

These two compiler options are the obvious and direct way you would normally consider mitigating the risk. There are also other compiler options for which I saw results that indicate a positive influence on vulnerability mitigation. The -O3 option turns on advanced compiler-level optimization and therefore also affects the resulting assembler code. The optimized code might have no parts where a branch misprediction happens anymore by the same inputs. In my experience, the effect of the -O3 option relative to the -O0 option often is even greater than the difference between pattern-fix and all-fix. If your project consists of multiple files, the -ipo option for advanced multi-file inter-procedural optimization on the linker level is also recommended by Intel. [4]

Conclusion

To decrease your code’s vulnerability to Spectre attacks it is recommended to use the -O3 and -ipo option and depending on your performance requirements either the pattern-fix or all-fix option. As speculative execution is part of all current chip architectures, the threat of branch (mis)prediction is an unavoidable side-effect in modern microprocessors. For reasonable management of this problem, you have to consider the trade-off between performance and security risk and its consequences depending on your project

 

¹ On Windows, the option is used by /Qconditional-branch:<keyword> instead.

 

Sources:

https://www.theguardian.com/technology/2018/jan/04/meltdown-spectre-worst-cpu-bugs-ever-found-affect-computers-intel-processors-security-flaw

https://spectreattack.com/

https://www.felixcloutier.com/x86/lfence

https://software.intel.com/content/www/us/en/develop/documentation/cpp-compiler-developer-guide-and-reference/top/compiler-reference/compiler-options/compiler-option-details/code-generation-options/mconditional-branch-qconditional-branch.html

 

Letzte Artikel von Oliver Rehberg (Alle anzeigen)