Generalized address-space isolation
Protecting data with ASI
Speculative-execution vulnerabilities come about when the CPU can be fooled into accessing arbitrary memory in a speculative mode, bypassing checks that (presumably) exist in the code to prevent such access. Whenever it becomes clear that the CPU has predicted wrongly, the effects of the speculative execution will be undone, but traces will be left behind in various hardware caches. Hostile code can look for those traces and use them to exfiltrate data that would otherwise not be accessible to an attacker.
These attacks cannot work, however, against memory that is not accessible when the attack is underway. That is why kernel page-table isolation is effective against Meltdown; if the kernel's memory is not mapped while an attacker's code can run, it cannot be exposed during speculative execution. Keeping the kernel's address space unmapped is sufficient to protect against Meltdown exploits running in user space.
Spectre attacks, instead, normally target the system while it is running in kernel mode and all of kernel memory is mapped, so the current kernel page-table isolation implementation offers no protection there. But the kernel almost never needs access to all of its address space, and often accesses almost none of it. Thus the appeal of greater use of address-space isolation: by walling the kernel off from even its own memory when there is no need for that memory, ASI can eliminate many possible attacks.
There are other ways of blocking Spectre attacks, some of which are implemented in the kernel now, but many of the current Spectre mitigations are expensive and incomplete. Flushing the memory caches on every return to user space will block many exploits, but at a significant run-time cost, for example. Administrators must also disable simultaneous multi-threading (SMT) — which can hurt performance badly — or leave open the possibility of attacks from a sibling CPU. If ASI can be made sufficiently effective at blocking attacks, it might be possible to dispense with the current mitigations and gain some CPU performance back.
One place where better address-space isolation could help is in the area of virtualization. Virtual machines running under KVM will often need to trap back into the host kernel to carry out various tasks, but those requests can usually be handled without access to most of the kernel's address space. Since virtual machines might well be running malicious code, and they may be running on mixed-tenant systems, protecting against Spectre attacks from that source has long been of special interest. So it is not surprising that the current ASI patches originate from cloud providers (Oracle originally, Google now) and address KVM in particular, even though the mechanism has been designed to be more general than that.
Sensitive and non-sensitive memory
The core idea behind this patch set is in the concept of "address-space-isolation classes", each of which describes a specific security context. The unrestricted class is for the kernel with full access to the entire address space — the way the kernel works now, in other words. The restricted classes are defined as a subset of the unrestricted class. Any page-table mapping that exists in the restricted classes is identical to the same mapping in the unrestricted class, but the restricted classes lack mappings for much of the sensitive data that is mapped in the unrestricted class.
A system running with ASI can be expected to be running in a restricted class just about any time that user-space code is running. The KVM-specific ASI class will be entered, for example, before giving control to the kernel running in the guest system. A kernel page-table isolation class (if it existed — the patch set describes the possibility but does not contain an implementation), instead, would be entered before returning to user space on the host system. But an important aspect of ASI is that restricted classes can also be used when running in the kernel if access to sensitive data is not required. Thus, for example, the kernel should be able to handle many KVM-related tasks without ever leaving the KVM ASI class.
There are three levels of sensitivity defined in the patch set for data stored in memory:
- "Sensitive" memory, which should never be leaked out of the kernel.
- "Locally non-sensitive" memory, which is harmless if leaked to the currently running process, but cannot be allowed to leak further.
- "Globally non-sensitive" memory can be leaked far and wide without unpleasant consequences.
When address-space isolation is in use, sensitive memory is only mapped while the kernel is running and, even then, only if the kernel actually needs it. Globally non-sensitive memory can remain mapped all the time. Unlike the other two classes, locally non-sensitive memory is different for every process; it can be mapped while the current process is running but is not mapped into any other process's address space.
These memory classifications apply for all of the restricted ASI classes; if there are many of those classes, they all restrict the kernel's address space in the same way. In other words, memory that is sensitive in one restricted ASI class is sensitive in all of them. The difference between ASI classes comes down to how much of user space is mapped and a set of hooks that are run whenever the kernel enters or exits one of those classes. Entry into the KVM class, for example, requires flushing the memory caches to frustrate any Spectre attack that may be running in the virtual machine. If the current kernel page-table isolation mechanism were implemented in this scheme, that class would not need to do a cache flush on entry, since removing the kernel's address-space mappings is sufficient.
There is an interesting decision built into this patch set: if the kernel tries to access sensitive data while running under a restricted ASI class, a processor trap will occur. One possible reaction at that point would be a kernel oops, which would certainly prevent speculative attacks against that data. The ASI patch set, instead, will exit the current ASI class and go into the unrestricted mode in this case, allowing the access to proceed. This response should be sufficient to block speculative access to that data (since speculative execution will simply stop rather than causing a trap) while, at the same time, not getting in the way of legitimate kernel accesses.
One reason for this approach should be reasonably clear: the kernel's address space is huge, and nobody could ever hope to properly determine the sensitivity of every data structure the kernel uses and mark it accordingly. Beyond that, somebody would have to find all of the places where the kernel must exit any restricted class to work with the sensitive data. Even if somebody did achieve all of that, there would be no hope of maintaining it going forward. This architecture thus embodies an acknowledgment that it will never be possible to properly mark all data and all places where sensitive data must be used, so it will always be necessary to make things work anyway.
Classifying memory
Still, an attempt must be made to try to properly mark at least the most sensitive and most frequently accessed data; much of the 47-part patch set is focused on that task. For dynamically allocated memory, there is a new set of GFP flags (__GFP_GLOBAL_NONSENSITIVE and __GFP_LOCAL_NONSENSITIVE) for marking allocations that do not hold sensitive contents. Calls to the page or slab allocator can use those flags to put the resulting memory into the desired class; memory allocated with vmalloc() can also be classified in this manner.
Internally, the kernel maintains two sets of page tables for its own address space. The unrestricted tables are the same as they appear in current kernels; included therein is the "direct map" that makes all of physical memory available in the kernel's address space. The restricted page tables start with almost no mappings at all. Whenever globally non-sensitive allocations are made, the mappings for the allocated pages are copied into that second set of page tables at the same addresses. When running in a restricted mode, the second page set of page tables is made active in place of the first, providing access to the non-sensitive data but not to any sensitive data.
Handling locally non-sensitive allocations is a bit trickier. When the kernel is running in a restricted mode, only the globally non-sensitive part of the direct map is actually present in the page tables. Since this data is globally non-sensitive, there is a single restricted table that is used for all processes. But the locally non-sensitive mappings must be unique to each process, so they cannot live in a single, global page table. That raises the question of where, in the address space, those mappings can go.
The solution that was chosen was to duplicate the direct mapping, so that now the kernel has two complete mappings for all of physical memory. In a restricted mode, the first mapping contains the globally non-sensitive data, as before. Locally non-sensitive allocations, instead, are set up using the second mapping. Once again, only the pages that have been specifically allocated as locally non-sensitive are mapped in the restricted version of this range, and each process has its own version of this mapping. As a result, the locally non-sensitive data for the running process is accessible even in the restricted mode, but it is not accessible from any other process.
That solves the problem, at the cost of halving the amount of physical memory that can be managed by the kernel, since each physical page must now be mapped twice.
For static variables, there is a separate set of flags with names like __asi_not_sensitive and __asi_not_sensitive_readmostly that can be added to the declaration. As one might expect, there is no way to declare static variables as being locally non-sensitive. Static per-CPU variables add another level of complexity, leading to declaration macros with concise names like DEFINE_PER_CPU_SHARED_ALIGNED_ASI_NOT_SENSITIVE().
The developers have not attempted to mark every allocation and declaration properly; as noted above, that is not a task that a rational person (or even a kernel developer) would attempt. But they have spent some time running with the patch set and making note of which accesses caused traps and forced an exit into the unrestricted mode. By identifying and marking the busiest, non-sensitive data structures, they were able to reduce the overall performance impact of this mechanism.
Isolating KVM
With all that infrastructure in place, along with a mechanism to allow controlled mapping of user-space memory into the restricted context, it becomes possible to set up address-space isolation for KVM in particular, and to simultaneously disable some of the expensive Spectre mitigations. Whenever the kernel gives control to the guest, it ensures that the KVM ASI mode is enabled; on return to the kernel, an exit to the unrestricted mode may or may not be performed, depending on what the kernel has to do. If that exit can be avoided (because no sensitive data need be accessed), the cost of cache flushes can be avoided. Meanwhile, with luck, even a hostile guest system will be unable to exploit Spectre vulnerabilities in the host kernel.
For this protection to be complete, though, the kernel must protect itself against not only the guest, but also against a hostile process running on an SMT sibling. In the full implementation, if the cost of simply disabling SMT is to be avoided, that means "stunning" the sibling — suspending its execution — while the kernel is running in the unrestricted address space. When the kernel returns to KVM, the sibling CPU must then be resumed ("unstunned"); the kernel must also flush the memory caches to prevent any data leakage. The implementation of sibling stunning is not part of this patch set; as with the stunning of siblings in the real world, there are potential consequences that must be considered first. In this case, the interaction between stunning and the scheduler has not yet been fully reviewed, so this work has not yet been posted.
The end result of all this is a first look at a form of ASI that could
increase both safety and performance for systems running guests with KVM,
and which could be extended to cover any number of other situations where
ASI might be indicated. As of this writing, the patch series has received
no review
comments at all, which may be a result of the size and complexity of the
work as a whole. It does represent an interesting set of tradeoffs; it can
improve both performance and security, but at the cost of a lot of code
churn and ongoing maintenance of sensitivity annotations. In a world where
one might hope that, before too long, hardware will no longer contain
Spectre vulnerabilities, this cost may well appear to be too high. If,
instead, we believe that Spectre may haunt us for a long time yet, though,
the calculation could well be different.
| Index entries for this article | |
|---|---|
| Kernel | Memory management/Address-space isolation |
| Kernel | Security/Meltdown and Spectre |
