Hacker News new | past | comments | ask | show | jobs | submit login

A true enough statement, but "Rust" is unnecessarily specific. Dependencies are getting scary in general. Supply chain attacks are no longer hypothetical, they're here and have been for a while.

If I were designing a new language I think I'd be very interested in putting some sort of capability system in so I can confine entire library trees safely, and libraries can volunteer somehow what capabilities they need/offer. I think it would need to be a new language if for no other reason than ecosystems will need to be written with the concept in them from the beginning.

For instance, consider an "image loading library". In most modern languages such libraries almost invariably support loading images from a file, directly, for convenience if nothing else. In a language that supported this concept of capabilities it would be necessary to support loading them from a stream, so either the image library would need you to supply it a stream unconditionally, or if the capability support is more rich, you could say "I don't want you to be able to load files" in your manifest or something and the compiler would block the "LoadFromFile(filename)" function at compile time. Multiply that out over an entire ecosystem and I think this would be hard to retrofit. It's hugely backwards incompatible if it is done correctly, it would be a de facto fork of the entire ecosystem.

I honestly don't see any other solution to this in the long term, except to create a world where the vast majority of libraries become untargetable in supply chain attacks because they can't open sockets or read files and are thus useless to attackers, and we can reduce our attack surface to just the libraries that truly need the deep access. And I think if a language came out with this design, you'd be surprised at how few things need the dangerous permissions.

Even a culture of minimizing dependencies is just delaying the inevitable. We've been seeing Go packages getting supply-chain-attacked and it getting into people's real code bases, and that community is about as hostile to large dependency trees as any can be and still function. It's not good enough.






You want a special purpose language.

In your particular example of image loading, you want WUFFS. https://github.com/google/wuffs

In WUFFS most programs are impossible. Their "Hello, world" doesn't print hello world because it literally can't do that. It doesn't even have a string type, and it has no idea how to do I/O so that's both elements of the task ruled out. It can however, Wrangle Untrusted File Formats Safely which is its sole purpose.

I believe there should be more special purpose languages like this, as opposed to the General Purpose languages most of us learn. If your work needs six, sixteen or sixty WUFFS libraries to load different image formats, that's all fine because categorically they don't do anything outside their box. Yet, they're extremely fast because since they can't do anything bad by definition they don't need those routine "Better not do anything bad" checks you'd write in a language like C or the compiler would add in a language like Rust, and because they vectorize very nicely.


Java and the .NET Framework had partial trust/capabilities mechanisms decades ago. No one really used them and they were deprecated/removed.

It was not bad, but without memory/cpu isolates, it was pretty useless. The JSR for isolation got abandoned when Sun went belly up.

It was more like no one used them correctly.

Wouldn't that mean they were poorly implemented. If no one uses something correctly, seems like that isn't a problem with the people but the thing.

I don't think so. Software is maybe the only "engineering" discipline where it is considered okay to use mainstream tools incorrectly and then blame the tools.

Do the “mainstream” tools change every five years in other disciplines?

To be fair they only change when chasing trends, consumers don't care how software is written, provided it does the job.

Which goes both ways, it can be a Gtk+ application written in C, or Electron junk, as long as it works, they will use it.


Partially yes, hence why they got removed, the official messaging being OS security primitives are a better way.

I don't think retrofitting existing languages/ecosystems is necessarily a lost cause. Static enforcement requires rewrites, but runtime enforcement gets you most of the benefit at a much lower cost.

As long as all library code is compiled/run from source, a compiler/runtime can replace system calls with wrappers that check caller-specific permissions, and it can refuse to compile or insert runtime panics if the language's escape hatches would be used. It can be as safe as the language is safe, so long as you're ok with panics when the rules are broken.

It'd take some work to document and distribute capability profiles for libraries that don't care to support it, but a similar effort was proven possible with TypeScript.


I actually started working on a tool like that for fun, at each syscall it would walk back up the stack and check which shared object a function was from and compare that to a policy until it found something explicitly allowed or denied. I don't think it would necessarily be bulletproof enough to trust fully but it was fun to write.

Maybe we need a stronger culture of Sans-IO dependencies in general. To the point of pointing out and criticising like it happens with bad practices and dark patterns. A new lib (which shouldn't be used its own file access code) is announced in HN, and the first comment: "why do you do your own IO?"

Edit - note it's just tongue in cheek. Obviously libraries being developed against the public approval wouldn't be much of a good metric. Although I do agree that a bit more common culture of the Sans-IO principles would be a good thing.


I love this idea and I hope I get to work on it someday. I've wanted this ever since I was a starry-eyed teenager on IRC listening to Darius Bacon explain his capability-based OS idea, aptly called "Vapor".

I think it could be possible in Rust with a linter, something like https://github.com/geiger-rs/cargo-geiger . The Rust compiler has some unsoundness issues such as https://github.com/rust-lang/rust/issues/84366 . Those would need fixing or linter coverage.


I've thought about this (albeit not for that long) and it seems like you'd need a non-trivial revamp of how we communicate with the operating system. For instance, allowing a library to "read from a stream" sounds safe until you realize they might be using the same syscalls as reading from a file!

I love this idea. There is some reminiscence of this in Rust, but it's opt in and based on convention, and only for `unsafe` code. Specifically, there's a trend of libraries using `#![deny(unsafe_code)]` (which will cause a compilation error if there is any `unsafe` code in the current crate), and then advertising this to their users. But there's no enforcement, and the library can still add `#[allow(unsafe_Code)]` to specific functions.

Perhaps a capability system could work like the current "feature" flags, but for the standard library, which would mean they could be computed transitively.


FYI: `#[forbid(_)]` cannot be bypassed by the affected code (without a never-to-be-stabilised nightly feature meant to be used only in `std` macros).

https://doc.rust-lang.org/rustc/lints/levels.html


Ah right, forgot about forbid!

I don't think you need to get very complex to design a language that protects libraries from having implicit system access. If the only place that can import system APIs is in the entry program, then by design libraries need to use dependency injection to facilitate explicit passing of capabilities.

One can take just about any existing language and add this constraint, the problem however is it would break the existing ecosystem of libraries.


If you want this today, Haskell might be the only choice.

Yes, there is a sense in which Haskell's "effect systems" are "capability systems". My effect system, Bluefin, models capabilities as values that you explicitly pass around. You can't do I/O unless you have the "IOE" capability, for example.

https://hackage.haskell.org/package/bluefin


That's one hell of a task. First question is how fine-grained your capability system will be. Both in terms of capabilities and who they are granted for. Not fine-grained enough and everything will need everything, e.g. access to various clocks could be used to DoS you or as a side channel attack. Unsafe memory access might speed up your image parsing but kills all safety. Similar problems with scope. If per dependency, forces library authors to remove useful functionality or break up their library into tiny pieces. If per function and module you'll have a hard time auditing it all. Lastly, it's a huge burden on devs to accurately communicate why their library/function needs a specific capability. We know from JavaScript engines, containerization and WASM runtimes what's actually required for running untrusted code. The overhead is just to large to do it for each function call.

Is there anything in existence which has a version of this idea? It makes a ton of sense to me, but you are right that it would be practically impossible to do in a current language.


Austral is a really cool experiment and I love how much effort was put into the spec which you've linked to. It explains the need for capabilities and linear types, and how they interact, really well.


Yes, but you can't enforce this at the language level if your objective is security (at least not for natively-compiled languages). You need OS-level support for capabilities, which some OSes do provide (SeL4, Fuchsia). But if you're in a VM rather than native code then you can enforce capabilities, which is what Wasm does with WASI.

Wasm + wasi let you define hard boundaries between components with explicit interfaces, might be loosely along these lines?

.NET Framework, windows only, (non .NET, aka .NET Core)

Capslock sort of does this with go https://github.com/google/capslock

Interesting. I hadn't seen it yet. I'll check out how fine-grained it really is. My first concern would (naturally) be network calls, but calling a local service should ideally is distinguishable from calling some address that does not originate in the top level.

If anyone ever check this thread: it works well. Use the json output, and it'll show the call path for each "capability" it detects (network, arbitrary code execution, ...). I use this on the output to organize it into a spreadsheet and scan quickly:

    jq -r '.capabilityInfo[] | [.capability, .depPath | split(" ") | reverse | join(" ")] | @tsv'

TypeScript ecosystem supports this! An environment without e.g. file operations will simply miss classes that are needed for it, and your compilation will fail.

Doesn't Haskell do this to some degree with the IO monad? Functions that are not supposed to do IO directly simply have a more specific type signature, like taking in a stream and returning a buffer for example.

Yes, although it can be violated by unsafePerformIO and friends. Haskell's is not an "assured" system.

What if I pass in components from one library with permissions to another library that doesn't have those permissions?

> I think it would need to be a new language [..]

Languages (plural) ... no single language will work for everyone.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: