I had some thoughts about capabilities. This is a very half assed post that I wrote up to get my thoughts out so that I could go back to work.

OK so capabilities are cool. They’re essentially named tokens that, if you possess, give you some right. They can be delegated by telling someone about that name, which makes them very powerful.

The “security” of a capability is enforced by the inability to forge one - as an example, imagine I host a sensitive document on S3 at public-bucket/<uuid>. If you don’t have the “list” iam permission that uuid acts as a capability - its name connotes permission. I can tell you the name, and now you have permission. If you could “forge” the name by guessing it it wouldn’t protect me at all.

So that’s capabilities. What about rust?

Well, let’s imagine a capability system.

pub cap Io;

fn reads_file(path: &str) -> String 
    requires: Io
{
    std::fs::read_to_string(path).unwrap()
}

I’ve declared a new public capability, Io, and I’ve required that capability for reads_file.

Here’s what calling that looks like:

fn read_config() -> String
    requires: Io
{
    reads_file("config.toml")
}

fn main() 
    requires: Arbitrary
{
    dbg!(read_config());
}

Notably, read_config doesn’t have to pass anything into reads_file - so long as it “requires” the Io capability reads_file is callable. In essenence ‘read_config’ is delegating Io implicity.

Further, while main may have the Arbitrary capability, which denotes “all” capabilities, once you’re in read_config you drop everything except for what’s required. In this way capabilities are automatically narrowed throughout your program.

More on that ‘Arbitrary’ later.

You might be thinking “wow that looks familiar”. This is just a generalized ‘unsafe’, I think.

pub cap Unsafe;
fn does_unsafe_things() requires: Unsafe {}

unsafe is a capability in a sense, but it’s forgable. And that actually is a super important property. It lets us write unsafe code and wrap it in safe code - otherwise all of rust would be ‘unsafe’.

So let’s take a look at our code again, but with forgery.

fn read_config() -> String
    forges: Io
{
    reads_file("config.toml")
}

A new keyword appears - forges. This is a declaration of capabilities that we don’t inherit from the caller, instead we magically forge Io out of nowhere, and we can now use that capability.

Forgery, like wrapping any unsafe function in safe, would have to be heavily scrutinized. Are you sure you have that capability?

Forgery also makes capabilities backwards compatible, right? Functions before the next edition would all just forge their capabilities. I would suggest a special capability, Arbitrary.

fn read_config() -> String
    forges: Arbitrary
{
    reads_file("config.toml")
}

By default all past editions would forge Arbitrary, which itself would encompass all capabilities. In new editions, you’d have to declare your capabilities everywhere.

There are some pretty obvious questions though. Are capabilities polymorphic? If read_config takes R: Read, do I need a capability to read from a backing file? Honestly, idk.

How do we compose capabilities? Maybe

cap Io: IoRead + IoWrite;

What if we want capabilities to be parameterized? Like cap Io<&Path> ? The short answer is, I have no idea, I told you this was just to get my thoughts out.

Anyway, some scattered thoughts here, since people have been talking about language capabilities. Koka is doing some cool stuff in this area, but I haven’t dug into it enough.

There are some cool implications here. In theory if you know all of a programs capabilities you can generated sandboxes for them at compile time. You can force callers to abide by arbitrary constraints. You could even have caps like “Panics” etc.



blog comments powered by Disqus

Published

11 May 2022

Categories