|
|
Log in / Subscribe / Register

Generalized address-space isolation

By Jonathan Corbet
March 3, 2022
The disclosure of the Meltdown and Spectre vulnerabilities put a spotlight on the risks that come with sharing address spaces too widely. Even if the protection mechanisms provided by the hardware should prevent access to sensitive data, those vulnerabilities can often be used to leak that data anyway. So, from the beginning, mitigation strategies have included reducing the sharing of address spaces, but there is more that could be done and ongoing interest in doing so. Now, this patch set posted by Junaid Shahid (containing work from Ofir Weisse and inspired by earlier patches from Alexandre Chartre) shows what would be required to create a general address-space isolation (ASI) mechanism for the kernel.

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
KernelMemory management/Address-space isolation
KernelSecurity/Meltdown and Spectre


to post comments

Generalized address-space isolation

Posted Mar 4, 2022 2:14 UTC (Fri) by pabs (subscriber, #43278) [Link] (1 responses)

Could this be used for isolating different parts of Linux from each other in a microkernel-esq way? for instance to restrict network drivers from "accidentally" calling into block device code.

Generalized address-space isolation

Posted Mar 4, 2022 6:29 UTC (Fri) by jorgegv (subscriber, #60484) [Link]

I was thinking exactly this when I started reading the article: slowly, but surely, moving to a microkernel architecture...

:-D

Generalized address-space isolation

Posted Mar 4, 2022 2:56 UTC (Fri) by calumapplepie (guest, #143655) [Link]

I look forward to the article in a few years about how this (or a similar) mechanism is becoming easy enough to work with that it can be enabled in the "enforcement"/"oopsing" mode, rather than just letting any stray accesses through. It'd be a major hardening step for the kernel: out-of-bounds accesses and most ROP gadgets would be much weaker, as will whatever the next big hijacking technique is.

Generalized address-space isolation

Posted Mar 4, 2022 6:19 UTC (Fri) by developer122 (guest, #152928) [Link] (8 responses)

It's probably worth noting that there are multiple variants of spectre for which this will have no effect, as they aren't affected by the current memory mapping. CPU-local effects, like the spying of one concurrent hyperthread upon another in the same core is one example.

Generalized address-space isolation

Posted Mar 4, 2022 6:21 UTC (Fri) by developer122 (guest, #152928) [Link]

One of the most important things to keep in mind about spectre is that it is not one vulnerability, but a rainbow of now well over a dozen variants which work in different ways and rely on different hidden hardware buffers.

Generalized address-space isolation

Posted Mar 4, 2022 7:02 UTC (Fri) by ibukanov (subscriber, #3942) [Link] (6 responses)

As I understand Intel and other chipmakers are not going to fix Spectre bugs affecting the code sharing the address space while they may eventually fix other cases like branch predictors or at least limit them to the shared address space case.

Generalized address-space isolation

Posted Mar 4, 2022 18:26 UTC (Fri) by JoeBuck (subscriber, #2330) [Link] (5 responses)

It seems that really fixing the problem would require an architectural change. It seems that we want to have a way to distinguish processes that trust each other and should be able to work together from those that must be treated as potentially hostile, so mitigation costs are kept to a minimum. For untrusted threads/processes we want to give them no information at all about what data have been accessed by other threads.

Just tossing out an idea here:

Suppose we had a register, settable only by the kernel, that would indicate the current 'trust zone'. The value of this register would be used as part of the key when looking up data in caches, and would be stored as part of the cache line when new data are read into the cache. A process or thread would not be able to determine, by cache timing, what a process in a different zone did by determining what is in the cache. The price of this is that identical data (say, from a shared library) might have to be stored multiple times in the cache, or get pushed out. But it would be flexible: in a multithreaded application where the threads trust each other they can use the same trust zone. A browser running potentially hostile code could set up a different trust zone for it (a system call would be needed to permit this and an unprivileged process could only request a brand-new trust zone).

This wouldn't fix everything, hyperthreads assigned to the same actual core could time each other. But perhaps the OS could refuse to assign processes or threads in different trust zones to the same physical core.

Since we don't have this, the change we are discussing approximates this by giving untrusted processes a memory map that does not include data they should not access, so "trust zone" becomes "address space segment". But setting up completely disjoint memory maps will be costly.

Generalized address-space isolation

Posted Mar 4, 2022 23:07 UTC (Fri) by Paf (subscriber, #91811) [Link] (1 responses)

The comment above said “Intel is not planning to fix Spectre bugs for processes that share an address space”, and, well, there’s your definition of trusted.

Generalized address-space isolation

Posted Mar 5, 2022 2:07 UTC (Sat) by JoeBuck (subscriber, #2330) [Link]

So we might have to do it with RISC-V plus an extension. If Intel and ARM won't fix the problem, perhaps an alternative solution is possible. But it will be an expensive prospect.

Generalized address-space isolation

Posted Mar 12, 2022 18:50 UTC (Sat) by anton (subscriber, #25547) [Link] (2 responses)

Spectre is a microarchitectural problem, and can be fixed at that level: Any speculative effect must be kept speculative until the instruction is committed; e.g., no permanent cache updates, no branch predictor updates before that; keep the stuff in speculative buffers (just like architectural changes, e.g., register or memory writes), and promote them to permanent structures on instruction commit (like architectural changes).

The denizens of comp.arch worked that out a few days after Spectre was published. The architects of Intel, AMD etc. are likely just as capable, so if they don't produce Spectre-immune designs, the reason is not that it's not possible, but that they don't want to.

The reported lead time for CPUs is 5 years. Zen 4 towards the end of this year will be the first design done more than five years after AMD was notified of Spectre. We will see if AMD is unwilling to produce Spectre-immune designs then.

Unfortunately the example of Rowhammer shows that hardware companies can be unwilling to apply well-known fixes for fundamental problems.

Generalized address-space isolation

Posted Mar 17, 2022 14:21 UTC (Thu) by ksandstr (guest, #60862) [Link] (1 responses)

It's also possible that allowing speculative consumption of leaky instructions' results only after all instructions above them have been confirmed not to raise faults (e.g. segment applied & TLBs crawled for memory ops) would cost more in terms of performance than having a vulnerable pipeline and compensating with software kludges. The latter seems preferable for exactly the type of changes presented in the main article: keeping sensitive data out of Spectre's reach, thereby limiting the number of gadgets that could plausibly leak it.

Now to await the day that we can run our web browsers within a VM-equivalent container, and enjoy pre-PTI performance without.

Generalized address-space isolation

Posted Mar 17, 2022 18:41 UTC (Thu) by anton (subscriber, #25547) [Link]

It's also possible that allowing speculative consumption of leaky instructions' results only after all instructions above them have been confirmed not to raise faults (e.g. segment applied & TLBs crawled for memory ops) would cost more in terms of performance
That would certainly cost more in terms of performance than the fix that I outlined. A speculatively executed instruction can make use of the microarchitectural results of earlier speculative instructions without waiting for confirmation just like it makes use of architectural results before confirmation. The point of the fix is that, just like architectural changes are only committed if an instruction is committed, microarchitectural changes must only be commited if an instruction is committed; until then the microarchitectural result has to stay in a temporary buffer, like the architectural results do already (and have since modern OoO CPUs have been introduced in the mid-90s).

The cost in terms of performance of this fix is about zero; it does cost some area, but there might be a performance advantage from, e.g., having data in those temporary buffers in addition to the D-cache.

We probably also want to fix the resource contention side channels from speculatively executed instructions. How that affects performance depends on the fix, but I expect that there are some cases where it costs some (but not much) performance.

Concerning software mitigations, I doubt that it is possible to get software airtight against Spectre in that way. And I expect that it costs more than the resource contention fixes, and certainly more than the microarchitectural fixes. A factor 9.5 (for fft on the Pentium G4560 of thunk-inline over no retpolines (i.e., no mitigation)) is an extreme case, but at least the mitigation was easy to apply in this case (but then this mitigation does not defend against Spectre variants other than v2).

By contrast, Spectre V1 mitigations typically cost (some) performance, but also programming time, and that programming time could have been spent instead on making the program faster (or more functional, or released earlier, or ...).

In any case, given that there is no reliable software way to avoid Spectre vulnerabilities, the way to go is to fix it in hardware (and then to buy that hardware).

Generalized address-space isolation

Posted Mar 4, 2022 9:00 UTC (Fri) by taladar (subscriber, #68407) [Link] (1 responses)

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

While I agree that it would be a huge effort to properly mark everything in the kernel I feel the second half of this paragraph is too pessimistic. If there was a kernel codebase where this had already happened (or, say, a hypothetical rewrite where this was built in from the start) I think it would absolutely be possible to maintain this going forward, possibly with some tooling support.

This sounds like the kind of thing that might be a good candidate for a future Rust successor language with even more memory safety language support baked into the language. In this case annotating bits of data as sensitive and when it has to be mapped.

It sort of reminds me of the experiments in some languages for tainted data (Ruby comes to mind).

Generalized address-space isolation

Posted Mar 4, 2022 10:18 UTC (Fri) by matthias (subscriber, #94967) [Link]

> This sounds like the kind of thing that might be a good candidate for a future Rust successor language with even more memory safety language support baked into the language. In this case annotating bits of data as sensitive and when it has to be mapped.

Rust should already be powerful enough to accomplish most of this.

You can use a special allocator for sensitive data, ensuring sensitive data always lives in the correct subset of the memory. And the type system can enforce that the sensitive data is mapped whenever it is needed. Just define an operation that maps the sensitive memory and returns a zero sized struct SENSITIVE_MAPPED that proves that the sensitive memory is mapped. Now all operations on sensitive memory require that struct. Thus you cannot do operations without mapping the sensitive memory. The operation that unmaps the sensitive memory will consume the SENSITIVE_MAPPED struct.

At first, this might look like a performance impact, but as this is a zero sized struct that is not used at all in the operations (it just has to be there), there is no need for the compiler to emit any code regarding that struct. It is just a marker.

What will be hard is to enforce that the sensitive memory is not mapped when it is not needed. Probably you could use another token that proves that is returned by the unmap operation (and consumed by the map operation) that is required when you do things that can potentially leak data.

While this might not come with some runtime penalty, it requires of course much implementation effort to do all this. Probably most of the effort will be to identify everything that is sensitive.

Generalized address-space isolation

Posted Mar 4, 2022 10:45 UTC (Fri) by wtarreau (subscriber, #51152) [Link]

I think that an other approach could be related to DRAM encryption that certain CPUs support: if pages are encrypted with a task-specific key, we could imagine that depending on the task and execution level, there could be one or a few keys available that allow to access memory. In the worst case, even if the CPU was mislead into trying to prefetch data from wrong addresses, the observed data would be encrypted. One problem I'm seeing with encryption is the difficulty to share areas between tasks or with hardware devices, and I fear it could result in more copies.

Generalized address-space isolation

Posted Mar 5, 2022 22:36 UTC (Sat) by amarao (guest, #87073) [Link]

I would really like to see zero performance impact for opt-out systems. Mlfa is the most used tuning we have on all machines without untrusted code access (databases, firewalls, etc)

Generalized address-space isolation

Posted Mar 17, 2022 14:49 UTC (Thu) by ksandstr (guest, #60862) [Link]

This change appears to be doing exactly the right thing: separate data of different sensitivity level into distinct heaps, and apply phantom leak barriers in a fault handler for low-sens tagged code that regardless may hit high-sens data. Combined with syscall handling that doesn't itself represent a leak hazard, this could allow pre-PTI performance for low-sens hot paths in general. There's also an interface with the idea that unmarked allocations default to high-sens, meaning that such paths' data structures can be changed over according to just what that data itself is; so only the code that manages region accessibility need be tested for protecting against (current and future) instances of Spectre. Combined with benchmarking to validate annotation wibble, ASI could realistically claw back some of the Meltdown/Spectre losses.

Also... how come sibling hyperthreads have been permitted to run in distinct processes concurrently until now? If I'm not entirely mistaken this was a bad idea since 2004-ish when it was discovered that Netburst's early SMT could be used to capture RSA keys through instruction timing, and I was under the impression that SMT scheduling was already preventing this.


Copyright © 2022, Eklektix, Inc.
This article may be redistributed under the terms of the Creative Commons CC BY-SA 4.0 license
Comments and public postings are copyrighted by their creators.
Linux is a registered trademark of Linus Torvalds