The impact of memory safety on sandboxing
Sandboxing and memory safety are generally considered two orthogonal, and therefore complementary, approaches to improving security. Memory safety reduces the likelihood of a vulnerability being introduced, and sandboxing reduces the impact if a vulnerability is exploited. However, I think this over-simplifies what sandboxing looks like in sophisticated multi-process architectures like web browsers and leads us to miss the potential implications of ubiquitous memory safety on sandboxing.
Sandboxing generally has two similar, yet distinct, purposes. The first is to reduce the semantic privileges a process has, and the second is to reduce the attack surface accessible to a process. These are abstract, so let’s consider some examples:
- If we take away a process’s ability to read files from the file system this reduces its semantic privileges.
- If we take away a process’s ability to make the
futex
syscall this reduces the kernel’s attack surface that’s accessible to it.
These seem similar, in that they reduce the post-sandboxed-code-execution vectors for an attacker, but they’re not! The former is enforcing the threat-model of our process: if websites aren’t supposed to be able to access the file system, then this makes that true even in the face of a code execution vulnerability. The latter, however, is to protect against the possibility that the kernel might have a vulnerability in its implementation of futex
.
In practice, to implement either of these sandboxing policies in an existing project, you start by figuring out if anything in your process relies on the ability to do these, and if so, you find ways to make it stop (often by moving that code into a different process, and using IPC to ask that process to do the operation). But what if we were exceptionally confident that there were no vulnerabilities in futex
? Then the hassle of auditing our process for futex
usage, and wiring up the IPC for it, would be a lot of effort for every little gain.
Let’s take a bigger example: imagine an API (a syscall or IPC to a privileged process) that lets the caller play an MP3, provided as bytes, through the system’s speakers. Websites are allowed to play sounds, but only sometimes – individual tabs can be muted, as can origins. If we deny access to the play MP3 API to our browser’s renderer processes and instead require them to make IPC requests of the browser parent process to play audio (which would then enforce tab and origin mutes) then we have a semantic privilege reduction. The mute rules are now enforced, even in the face of a compromised renderer. However, in practice the risk that someone would exploit a renderer process just to Rick-roll the user seems remote, the far greater risk is that the play-MP3 API has a vulnerability that can be triggered by the MP3’s bytes, and gives an attacker arbitrary code execution in an even higher privileged context (the kernel or a system daemon).
Which returns us again to the question of: If the likelihood of a vulnerability in the audio playing API was incredibly low, what would we do differently? I think the answer is that we’d just grant the renderer process access to it, rather than building our own APIs to route audio playing through the browser parent process. This is because our real motivation in restricting access to the play-MP3 API was attack-surface reduction, not preventing processes from playing audio. By allowing the play-MP3 API, we’d be sacrificing the semantic security, but in exchange we’d be saving ourselves a lot of time, effort, and complexity in our own IPC, likely a good trade-off.
So what would it take to give ourselves the confidence that these APIs were exceedingly unlikely to have vulnerabilities? First, we’d need them to be implemented in memory safe languages. The data is exceptionally clear that the majority of kernel and OS daemon vulnerabilities are due to memory unsafety. But, on average, memory unsafety is “only” 70% of vulnerabilities, so just using a memory safe programming language means we still have 30% of the vulnerabilities. That’s too much risk to change our sandboxing strategy. One might argue that by freeing up a security team’s time to look for only that 30%, they’d be more productive in finding vulnerabilities before they ship, but this is speculative (it seems plausible though). What if there was a way to identify attack surfaces where it was likely that closer to 100% of vulnerabilities would be memory unsafety? For example, if an API never accepts or returns a file path, is it less likely to have logic vulnerabilities? How much less? I don’t have an answer to this question, so it’s my call to action: As we begin to move towards a world with more memory safety, how can we analyze the design of attack surfaces to know if they’re the sort that lend themselves to the possibility of high confidence that they don’t have vulnerabilities?
It’s worth stating explicitly: at the margin, reducing attack surface is always good for security. However, security teams have many demands on their time and many potential projects to work on. The question we always need to answer is: does this project provide more security gains than something else I could do. Reducing attack surface by cutting off vulnerability-prone APIs (like win32k
) is a very high return-on-investment project, reducing attack surface by cutting off an API which you already have very high confidence has no vulnerabilities is not. This is compounded by the fact that many attack surface reduction efforts introduce complexity, because they replace calls to system APIs with cross-process IPC operations.
As long as our kernels, system daemons, and other privileged attack surfaces are written in memory unsafe languages, there will be a strong reason to pursue aggressive attack-surface reduction from our sandboxes. However, in a world where our attack surfaces are built on the foundations of memory safety, we may be able to reconsider our assumptions about what attack surface reductions are good time-investments for security and engineering teams. This should not, however, mean we abandon attack surface reduction, but rather take the opportunity to further analyze the types of surfaces and APIs that are most likely to present risks. Right now, every API presents risks due to memory unsafety, but in the future, I hope we’ll be able to identify more specific risk factors, and thus invest our time more wisely.