Using kernel-provided capabilities (like Mach ports or Unix file descriptors) directly might have too much overhead if the design uses a lot of them. But even if that’s true, it should be possible to implement unforgeable capabilities in userland using a trusted broker process.

It’s a classic design mistake to represent IPC access rights using secrets instead of capabilities. Secrets seem more convenient to work with: they’re just data, which can be encoded and transformed like any other data, whereas capabilities need to be kept separate throughout multiple levels of abstraction, from your IPC wrapper functions down to the low-level message sending primitives.

And secrets are theoretically secure, if you do everything right. After all, in the related domain of network services, there is no trusted broker to represent capabilities, so you have to use secrets in various forms, like MAC-ed and encrypted cookies, or TLS certificates. And it mostly works out in practice.

But secrets are risky. Even with network services, you have the fundamental downside: the security model is compromised if an attacker can merely read data they shouldn’t, like the MAC key or the TLS private key, rather than having to modify data. That greatly increases the impact of vulnerabilities that only allow reads – especially if, like in this exploit, you can only read a small amount of random data, rather than whatever data you want.

When it comes to IPC, using secrets is even riskier for multiple reasons. First, an attacker is very ‘close’ to the target, running on a different process in the same machine, which means they’re in a much better position to try to leak memory using side-channel attacks. Although the full power of hardware side-channel attacks has only recently been exposed, weaker side-channel attacks have been a known threat for a long time. There are also pure-software timing side channels, i.e. where the software does a different amount of work depending on a condition, which would let an attacker guess the value of the condition even if the CPU itself executed instructions in constant time. Second, IPC communications protocols tend to be lower-level, e.g. using shared memory, and the trusted side is often written in an unsafe language like (as here) C++. In contrast, network protocols and servers can typically afford to be somewhat higher-level because the network itself introduces a bunch of overhead anyway (something that also makes them more resistant to. But low-level means greater risk, especially for low-level vulnerability categories like memory disclosure (or, for that matter, memory corruption). Not that network services can’t be vulnerable to memory disclosure – consider Heartbleed or Cloudbleed – but it’s more likely with IPC.

Third, without getting too far into the weeds, the objectives are often somewhat different. With a network service, leaking random data is often a pretty good attack by itself, especially if the service directly handles data for many users. With IPC, the attacker usually needs to use a separate exploit to even get in position to interact with the IPC interface, so ‘only’ being able to read data is sort of a waste; you really want to escalate privileges. This is only a rule of thumb and isn’t always true – sometimes network services are only accessible after compromising a different network service; sometimes IPC is intentionally exposed to untrusted code. But it’s a factor.

The most important difference, though, is what I already said: with IPC you don’t have to use secrets. They’re risky in both network services and IPC, but with IPC you can use a trusted capability broker instead, and so you should. Capabilities are a bit less convenient, not being pure data. but precisely for that reason they’re harder to screw up, harder to leak by accident.