Remix.run Logo
crazygringo 4 days ago

Totally agreed, and I'm surprised this idea hasn't become more mainstream yet.

If a package wants to access the filesystem, shell, OS API's, sockets, etc., those should be permissions you have to explicitly grant in your code.

mike_hearn 4 days ago | parent | next [-]

It's harder than it looks. I wrote an essay exploring why here:

https://blog.plan99.net/why-not-capability-languages-a8e6cbd...

crazygringo 4 days ago | parent | next [-]

Thanks, it's great to see all the issues you raise.

On the other hand, it seems about as hard as I was imagining. I take for granted that it has to be a new language -- you obviously can't add it on top of Python, for example. And obviously it isn't compatible with things like global monkeypatching.

But if a language's built-in functions are built around the idea from the ground up, it seems entirely feasible. Particularly if you make the limits entirely around permissions around data communication -- with disk, sockets, APIs, hardware like webcams and microphones, and "god" permissions like shell or exec commands -- and not about trying to merely constrain resource usage around things like CPU, memory, etc.

If a package is blowing up your memory or CPU, you'll catch it quickly and usually the worst it can do is make your service unavailable. The risk to focus on should be exclusively data access+exfiltration and external data modification, as far as I can tell. A package shouldn't be able to wipe your user folder or post program data to a URL at all unless you give it permission. Which means no filesystem or network calls, no shell access, no linked programs in other languages, etc.

Groxx 4 days ago | parent | prev | next [-]

tbh none of that sounds particularly bad, nor do I think capabilities are necessary (but obviously useful).

we could literally just take Go and categorize on "imports risky package" and we'd have a better situation than we have now, and it would encourage library design that isolates those risky accesses so people don't worry about them being used. even that much should have been table stakes over a decade ago.

and like:

>No language has such an object or such interfaces in its standard library, and in fact “god objects” are viewed as violating good object oriented design.

sure they do. that's dependency injection, and you'd probably delegate it to a dependency injector (your god object) that resolves permissions. plus go already has an object for it that's passed almost everywhere: context.

perfect isn't necessary. what we have now very nearly everywhere is the most extreme example of "yolo", almost anything would be an improvement.

mike_hearn 3 days ago | parent [-]

Yes, dependency injection can help although injectors don't have any understanding of whether an object really needs a dependency. But that's not a god object in the sense it's normally meant. For one, it's injecting different objects :)

Groxx 3 days ago | parent [-]

to be clear, I mean that the DI container/whatever is "the god object" - it holds essentially every dependency and every piece of your own code, knows how to construct every single one, and knows what everything needs. it's the biggest and most complicatedly-intertwined thing in pretty much any application, and it works so well that people forget it exists or how it works, and carrying permission-objects through that on a library level would be literally trivial because all of them already do everything needed.

hence: doesn't sound too bad

"truly needs": currently, yes. but that seems like a fairly easy thing to address with library packaging systems and a language that supports that. static analysis and language design to support it can cover a lot (e.g. go is limited enough that you can handle some just from scanning imports), and "you can ask for something you don't use, it just means people are less likely to use your library" for the exceptions is hardly a problem compared to our current "you already have every permission and nobody knows it".

mike_hearn 3 days ago | parent [-]

Yes, I do agree that integration with DI is one way to make progress on this problem that hasn't been tried before.

ryukafalz 4 days ago | parent | prev [-]

Thanks, this was a good overview of some of the challenges involved with designing a capability language.

I think I need to read up more on how to deal with (avoiding) changes to your public APIs when doing dependency injection, because that seems like basically what you're doing in a capability-based module system. I feel like there has to be some way to make such a system more ergonomic and make the common case of e.g. "I just want to give this thing the ability to make any HTTP request" easy, while still allowing for flexibility if you want to lock that down more.

mike_hearn 3 days ago | parent [-]

In Java DI you can add dependencies without changing your public API using field injection. But really there needs to be a language with integrated DI. A lot of the pain of using DI comes from the way it's been strapped on the side.

int_19h 3 days ago | parent | prev | next [-]

This exact idea has already been mainstream. Both Java and .NET used to have mechanisms like that, e.g.: https://en.wikipedia.org/wiki/Code_Access_Security

Groxx 2 days ago | parent [-]

"it exists as a niche feature that few use and fewer understand" isn't exactly "mainstream" IMO (it's significantly less common from what I've seen than manual classloader shenanigans, for example). But yes, it's nice that it exists, and I wish it were used more - it'd catch low-effort stuff like this one was.

darthwalsh 2 days ago | parent [-]

No, C# had it: past tense. CAS was neutered in .NET Framework 4.0 then removed in dotnet core.

Groxx 2 days ago | parent [-]

alas. don't suppose you know of any good articles on why it's removed? I'd be curious about the reasoning / challenges.

there are some rather obvious challenges, but a huge amount of the ones I've run across end up looking mostly like "it's hard to add to an existing language" which is extremely understandable, but hardly a blocker for new ones.

int_19h a day ago | parent [-]

I don't know if there were any articles specifically detailing it, but from blog posts at the time the clear message was that they didn't consider the intended security guarantees to be possible to uphold in practice, so much so that "CAS and appdomains shouldn't be considered a security boundary".

crdrost 4 days ago | parent | prev [-]

This was one of Doug Crockford's big bugaboos since The Good Parts and JSLint and Yahoo days—the idea that lexical scope aka closures give you an unprecedented ability to actually control I/O because you can say

    function main(io) {
        const result = somethingThatRequiresHttp(io.fetch);
        // ...
    }
and as long as you don't put I/O in global scope (i.e. window.fetch) but do an injection into the main entrypoint, that entrypoint gets to control what everyone else can do. I could for example do

    function main(io) {
      const result = something(readonlyFetch(onlyOurAPI(io.fetch))
    }
    function onlyOurAPI(fetch) {
      return (...args) => {
        const test = /^https:\/\/api.mydomain.example\//.exec(args[0]);
        if (test == null) {
          throw new ValueError("must only communicate with our API");
        }
        return fetch(..args);
      }
    }
    function readonlyFetch(fetch) { /* similar but allowlist only GET/HEAD methods */ }
I vaguely remember him being really passionate about "JavaScript lets you do this, we should all program in JavaScript" at the time... these days he's much more likely to say "JavaScript doesn't have any way to force you to do this and close off all the exploits from the now-leaked global scope, we should never program in JavaScript."

Shoutout to Ryan Dahl and Deno, where you write `#!/usr/bin/env deno --allow-net=api.mydomain.example` at the start of your shell script to accomplish something similar.

In my amateur programming-conlang hobby that will probably never produce anything joyful to anyone other than me, one of those programming languages has a notion of sending messages to "message-spaces" and I shamelessly steal Doug's idea -- message-spaces have handles that you can use to communicate with them, your I/O is a message sent to your main m-space containing a bunch of handles, you can then pattern-match on that message and make a new handle for a new m-space, provisioned with a pattern-matcher that only listens for, say, HTTP GET/HEAD events directed at the API, and forwards only those to the I/O handle. So then when I give this new handle to someone, they have no way of knowing that it's not fully I/O capable, requests they make to the not-API just sit there blackholed until you get an alert "there are too many unread messages in this m-space" and peek in to see why.