On January 3, 2018, Google Project Zero and others
uncovered the first three of the new class of vulnerabilities that affect processors with speculative execution. They were named
Specter (1 and 2) and
Meltdown . Using the mechanisms of
speculative CPU
execution , an attacker can temporarily bypass both explicit and implicit software security checks that prevent programs from reading inaccessible data in memory. While speculative execution is designed as a microarchitecture detail that is invisible at the architectural level, carefully designed programs could read inaccessible information in the speculative block and reveal it through side channels, such as the execution time of a program fragment.
When it was shown that Specter attacks are possible using JavaScript, the V8 team took part in solving the problem. We formed an emergency response team and worked closely with other teams at Google, our partners from a number of other browser developers, and hardware partners. Together with them, we proactively conducted both offensive research (designing attacking modules to prove the concept) and defensive (mitigating potential attacks).
Specter’s attack consists of two parts:
- Leakage otherwise inaccessible data in the hidden state of the CPU . All Specter's well-known attacks use specs to transfer bits of inaccessible data to CPU caches.
- Extract hidden state to restore inaccessible data. To do this, the attacker needs a watch of sufficient accuracy. (To the surprisingly low accuracy, especially with such methods as edge thresholding - a comparison with a threshold along the outlined contour).
Theoretically, it would be enough to block any of the two components of the attack. Since we do not know how to completely block any of them, we have developed and deployed mitigations that significantly reduce the amount of information leaking into the CPU caches, and mitigations that make it difficult to recover the hidden state.
')
Precision Timers
The tiny state changes that remain after speculative execution generate correspondingly tiny, almost impossible tiny, temporal differences — on the order of a billionth of a second. An offensive program requires a high-precision timer to directly detect such differences. Processors offer such timers, but the web platform does not expose them. The most accurate timer on the web platform
performance.now()
had a resolution of several microseconds, which was initially considered unsuitable for this purpose. However, two years ago, a research team specializing in microarchitectural attacks published
an article about timers on a web platform. They concluded that simultaneous variable shared memory and various methods for restoring resolution allow the creation of even higher resolution timers, even nanosecond. Such timers are accurate enough to detect individual L1 cache hits and misses. That he is usually used to capture information in the attacks Specter.
Timer protection
To disrupt the ability to detect small differences in time, browser developers have chosen a multi-pronged approach. In all browsers, the
performance.now()
resolution has been reduced (in Chrome from 5 microseconds to 100) and random jitter is introduced to prevent resolution recovery. After consultations between the developers of all browsers, we decided together to take an unprecedented step: immediately and retroactively disable the
SharedArrayBuffer
API in all browsers to prevent the creation of a nanosecond timer.
Gain
At the beginning of our offensive research, it became clear that timer mitigation alone was not enough. One of the reasons is that an attacker can simply repeatedly run his code so that the cumulative time difference is much more than a single hit or cache miss. We were able to construct reliable “gadgets” that use many cache lines at a time, up to the entire cache capacity, which gives a time difference of up to 600 microseconds. Later we found arbitrary amplification methods that are not limited by the cache capacity. Such amplification methods are based on repeated attempts to read secret data.
JIT protection
To read inaccessible data with Specter, an attacker forces the CPU to speculatively execute code that reads normally inaccessible data and places it in the cache. Protection can be considered from two sides:
- Prevent speculative code execution.
- Prevent reading inaccessible data from the speculative pipeline.
We experimented with the first option, inserting recommended instructions to prevent speculation, such as Intel's
LFENCE
, for each critical conditional branch and using
retpolines for indirect branches. Unfortunately, such heavy mitigations significantly reduce productivity (slowing down by 2-3 times on the Octane benchmark). Instead, we chose the second approach, inserting mitigation sequences that prevent secret data from being read due to incorrect speculation. Let me illustrate a technique with the following code fragment:
if (condition) { return a[i]; }
For simplicity, suppose condition
0
or
1
. The code above is vulnerable if the CPU reads a speculative from
a[i]
when
i
is out of limits, gaining access to normally inaccessible data. An important observation is that in this case speculation tries to read
a[i]
when the condition is
0
. Our mitigation rewrites this program so that it behaves just like the original program, but does not allow any speculatively loaded data to leak.
We reserve one register of the CPU, which we call “poison”, to track whether the code is running in an incorrectly interpreted branch. A poisonous register is maintained in all branches and calls of the generated code, so that any incorrectly interpreted branch causes the poison register to become
0
. Then we measure all memory accesses so that they unconditionally mask the result of all downloads with the current value of the poison register. This does not prevent the processor from predicting (or misinterpreting) branches, but destroying information (potentially outside the limits) of loaded values ​​due to incorrectly interpreted branches. The tool code is shown below (
a
is an array of numbers).
let poison = 1;
Additional code has no effect on the normal (defined by the architecture) program behavior. It only affects the micro-architectural state when working on a CPU with speculative execution. If you program the program at the source code level, advanced optimizations in modern compilers can remove such instrumentation. In V8, we prevent compiler from deleting extensions by inserting them at a very late stage of compilation.
We also use this “poisoning” technique to prevent leaks from indirect branches in the interpreter byte-code sending cycle and in the sequence of calls to JavaScript functions. In the interpreter, we set the poison to
0
if the bytecode handler (that is, a machine code sequence that interprets one byte code) does not match the current byte code. For JavaScript calls, we pass the target function as a parameter (in the register) and set the poison to
0
at the beginning of each function if the incoming target function does not match the current function. With this softening, we see a slowdown of less than 20% on the Octane benchmark.
Mitigation for WebAssembly is simpler, since the basic security check is to provide memory access within bounds. For 32-bit platforms, in addition to the usual border checks, we fill all the memory to the next power of two and unconditionally mask any upper bits of the user memory index. 64-bit platforms do not need this softening, as the implementation uses virtual memory protection for border checks. We experimented with compiling switch / case statements into binary search code instead of using a potentially vulnerable indirect branch, but this is too expensive for some workloads. Indirect calls are protected by retpolines.
Software Protection - Unreliable Option
Fortunately or unfortunately, our offensive research advanced much faster than defensive, and we quickly discovered the impossibility of programmatically mitigating all possible leaks during Specter attacks. This is due to several reasons. First, engineering efforts to counter Specter are disproportionate to the level of threat. In V8, we face many other security threats that are much worse, from direct reading beyond the borders due to common bugs (which is faster and easier than Specter), writing beyond the borders (this is not possible with Specter and worse) and potential remote code execution (impossible with Specter and much, much worse). Secondly, the increasingly sophisticated mitigation measures that we developed and implemented carried considerable complexity, which is a technical debt and can actually increase the attack surface and the performance overhead. Thirdly, testing and maintaining the mitigation of micro-architectural leaks is even more difficult than designing the gadgets themselves for an attack, because it is difficult to be sure that mitigations continue to work the way they were designed. At least once, important mitigations have been effectively canceled by later compiler optimizations. Fourth, we found that effectively mitigating some Specter options, especially Option 4, is simply not possible in software, even after the heroic efforts of our partners at Apple to combat the problem in their JIT compiler.
Isolation of sites
Our research led to the conclusion: in principle, an unreliable code can read the entire address space of a process using Specter and side channels. Software mitigations reduce the effectiveness of many potential gadgets, but are not effective or comprehensive. The only effective measure is to move sensitive data outside the process address space. Fortunately, Chrome has for many years been trying to divide sites into different processes in order to reduce the attack surface due to common vulnerabilities. These investments paid off, and by May 2018 we brought to the stage of readiness and deployed
isolation of sites on the maximum number of platforms. Thus, the Chrome security model no longer implies language confidentiality in the renderer process.
Specter has come a long way and highlighted the merits of collaboration between developers in industry and academia. So far, white hats are ahead of black. We still do not know about any real attack, with the exception of curious experimenters and professional researchers who are developing gadgets to prove the concept. New variants of these vulnerabilities continue to appear and this will continue for some time. We continue to monitor these threats and take them seriously.
Like many programmers, we also thought that safe languages ​​provide the correct abstraction border, not allowing well-typed programs to read arbitrary memory. Sadly, this turned out to be a mistake - this guarantee does not correspond to today's equipment. Of course, we still believe that safe languages ​​have more engineering advantages, and there is a future for them, but ... they flow a little on today's equipment.
Interested readers can delve into the topic and get more detailed information in our
scientific article .