Ambient Authority: The Root of all Evil
Not so many years ago I had the privilege of working on the Midori Research Operating System and it was one of the most educational two years of my life. I kind of took the job because I hoped that would happen. During that time I learned a phrase I hadn’t encountered before that helped me to put a name on something bad that had been previously nameless. Those two words were of course Ambient Authority. It’s possible you’ve also not heard those words. So let me give you some sense of why it’s such a problem and why getting rid of it is so good.
Everyone knows (“It is known”) that global variables are “bad”. Lots of global variables makes things very confusing because when your important state is in globals you have this problem that any function you might call at any point might change your globals and so any call puts all of your state at risk.
Ambient Authority is like that… only generalized. The problem is that many (Most? Basically all?) operating systems and runtimes (the WASM runtime is an exception!) use global functions to give you access to their features. This is a problem because if your key functions are globals then basically any function you call at any point might do, well, anything. If it wanted to pop up a message box you can’t really stop it. The comparison function in quicksort which is only supposed to use the two pointers its given might spin up threads and start mining bitcoin. Probably a bad idea.
Importantly, if the code goes awry, and is attacked, the attacker has full access to all the ambient functions. In Most Popular Operating Systems(TM) there are oodles of them. And many of them give you access to even more functions.
Midori tried to address this by making everything “capability based”. Meaning that to do anything you had to first have a handle to something that offered that service. Since we used C# we did this basically with interfaces. Everything was an interface. You started with the interfaces for the capabilities you requested and they spread only exactly as far as you wanted them to.
The upshot that that, for instance, things like opening a file simply cannot be done by code that is hashing some object because you need the interface to the file system to open a file and the (e.g.) object hasher you’re using never gets that interface. You can, with local reasoning, and simple static analysis of signatures, get a clear idea what capabilities any given piece of code might have. You don’t have to worry that some idiot is going to start re-initializing the localization functions mid-hash — and that wouldn’t be the craziest thing I’ve seen people attempt to do in a hashing function.
Now, maybe you don’t want to go all-in on objects and interfaces. You can accomplish basically the same thing (but with slightly less easy mocking) using global functions and handles. So to get a handle to a file you first have to get a handle to the file system.
Note that it was normal for the interface to be a lie. In many cases the “file system” would let you open exactly this one file you’re supposed to open and any other file would mysteriously be not found. Why the entire C drive seems to be empty except for the one file and there are no other drives at all. Go figure.
You can do similar things with handles and shims but it’s easier to understand as interfaces.
Now if you do this all the way down, and you basically banish global variables or at least seriously limit their visibility (not that hard) you get this amazing ability to reason over what code might run where. You know what any give piece of code might do from a system perspective because you know what handles you gave it and what you can do with those handles. You can easily (and its normal to do this) strip away even more capability by making contracts that accept already resolved handles. For instance, you don’t make APIs that accept file names or paths, they get a stream that has already been opened for them. They can read and close.
Most good code is already layered like this, but the layering is so voluntary that any piece of code might decide to go renegade at any moment. Some well-meaning engineer decides it’s ok to use this global function (like adding a critical section) and suddenly everything breaks. Importantly, an attacker can force the issue. Yes, even things like making a thread or acquiring a mutex require a capability.
Midori didn’t succeed, other than being really educational I guess. But that doesn’t mean it has nothing to teach us. Local reasoning, very short proof of correctness, minimal authority, and of course a memory-safe runtime, didn’t eliminate all bugs or security problems. But it sure took a lot of dumb off the table.