My understanding is that linguistic-level sandboxing is not really possible. Capability abstraction doesn’t improve security unless capabilities are actually enforced at runtime, by the runtime.
To give two examples:
cap-std doesn’t help you ensure that deps are safe. Nothing prevents a dep from, eg, using inline assembly to make a write syscall directly.
deno doesn’t allow disk access by default. If you don’t pass --allow-net, no dependency will be able to touch the network. At the same time, there are no linguistic abstractions to express capabilities. (https://deno.land/manual/getting_started/permissions)
Is there a canonical blog post explaining that you can’t generally add security to “allow-all” runtime by layering abstraction on top (as folks would most likely find a hole somewhere), and that instead security should start with adding unforgeable capabilities at the runtime level? It seems to be a very common misconception, cap-std is suggested as a fix in many similar threads.
Sandboxing is certainly possible, with some caveats.
You don’t need any runtime enforcement: unforgeable capabilities (in the sense of object capabilities) can be created with, for example, a private constructor. With a (package/module) private constructor, only your own package can hand out capabilities, and no one else is allowed to create them.
cap-std doesn’t help you ensure that deps are safe.
That is true, in the sense that no dependency is forced to use cap-std itself. But, if we assumed for a second that cap-std was the rust standard library, then all dependencies would need to go through it to do anything useful.
Nothing prevents a dep from, eg, using inline assembly to make a write syscall directly.
This can also be prevented by making inline assembly impossible to use without possesing a capability. You can do the same for FFI: all FFI function invokations have to take a FFI capability. With regards to the rust-specific unsafe blocks, you can either do the same (capabilities) or compiler-level checks: no dependencies of mine can use unsafe blocks unless I grant them explicit permission (through a compiler flag, for example).
Is there a canonical blog post explaining that you can’t generally add security to “allow-all” runtime by layering abstraction on top […] and that instead security should start with adding unforgeable capabilities at the runtime level?
I would go the other way, and recommend Capability Myths Demolished, which shows that object capabilities are enough to enforce proper security and that they can support irrevocability.
With a (package/module) private constructor, only your own package can hand out capabilities, and no one else is allowed to create them.
This doesn’t generally work-out in practice: linguistic abstractions of privacy are not usually sufficiently enforced by the runtime. In Java/JavaScript you often can use reflection to get the stuff you are not supposed to get. In Rust, you can just cast a number to a function pointer and call that.
I would sum up it as follows: languages protect their abstractions, and good languages make it impossible to accidentally break them. However, practical languages include escape hatches for deliberate circumventing of abstractions. In the presence of such escapes, we cannot rely on linguistic abstractions for security. Java story is a relevant case study: https://orbilu.uni.lu/bitstream/10993/38190/1/paper.txt.
Now, if you design a language with water-tight abstractions, this can work, but I’d probably call the result a “runtime” rather than a language. WASM, for example, can implement capabilities in a proper way, and Rust would run on WASM, using cap-std as n API for runtime. The security properties won’t be in cap-std, they’ll be in WASM.
This can also be prevented by making inline assembly impossible to use without possesing a capability
I don’t think this general approach would work for Rust. In Rust, unsafe is the defining feature of the language. Moving along these lines would make Rust more like Java in terms of expressiveness, and probably won’t actually improve security (ie, the same class of exploits from the linked paper would work).
I would go the other way, and recommend Capability Myths Demolished
Thanks, going to read that, will report back if I shift my opinions!
EDIT: it seems that the paper is entirely orthogonal to what I am trying to say. The paper argues that cap model is better that ACL model. I agree with that! What I am saying is that you can’t implement the model on the language level. That is, I predict that even if Java used capability objects instead of security manager, it would have been exploitable more or less in the same way, as exploits breaking ACL would also break capabilities.
Go used to have a model where you could prohibit the use of package unsafe and syscall to try to get security. App Engine, for example, used this. But my understanding is that they ended up abandoning it as unworkable.
Your points are sharp. Note that there was an attempt to make Java capability-safe (Joe-E), and it ended up becoming E because taming Java was too much work. Note also that there was a similar attempt for OCaml (Emily), and it was better at retaining the original language’s behavior, because OCaml is closer than Java to capability-safety.
ECMAScript is almost capability-safe. There are some useful tools, and there have been attempts to define safe subsets like Secure ECMAScript. But you’re right that, just like with memory-safety, a language that is almost capability-safe is not capability-safe.
While you’re free to think of languages like E as runtimes, I would think of E as a language and individual implementations like E-on-Java or E-on-CL as runtimes.
Rather than remove the package, wouldn’t it make more sense to replace it in order to poison it? That way every build pulling it could get a build error and a notice about the issue.
Removing the package will also cause build errors for the (apparently very few) projects relying on it. Someone investigating why their build is failing will likely come across the info from the crates.io maintainers about this.
In general, we recommend regularly auditing your dependencies, and only depending on crates whose author you trust.
Ok thanks?
I can do this in C++, because the language makes it impossible to ship libraries that have dependencies and you don’t get the exponential blowup, but the Rust ecosystem makes this completely intractable.
Last time I messed with rust, I wrote a very basic piece of software (connect to Discord, FlashWindow if anyone says your name, like IRC highlights) and it pulled in over 1 million lines of dependencies. Even if I could audit that, what can I do if I find a crate I don’t like?
C/C++ stuff has dependencies, and sometimes enormous amounts of them. This mantra that dependency tree hell is an NPM/Cargo/etc. specific problem is - frankly - ridiculous. I can’t audit all of, say, Boost or LibreSSL much better than I can audit, say, Tokio or RustTLS. Arguably, it’s harder to audit something like LibreSSL than RustTLS on account of the memory management, but that’s a vastly different discussion. Let’s imagine I’m instead talking about NodeJS’s “lodash” or something, which I also haven’t read every line of.
But you have package managers as a line of defense with C/C++ and most other popular languages. I don’t know why this is so hard to understand for many people.
Perhaps in some ways, the argument can be made that one big (but composable/de-composable), thoughtfully managed library with high-bar for entry (like Boost) – in longer term is significantly better, than 100s or 1000s of crates or NPM packages (also helps that the quality of library informs upcoming language standards).
The article you linked, to me, reads a bit more like false dichotomy (as it does not take into account the maturity of both technical and release process maturity characteristics of the C and C++ libraries).
These complaints are valid, but my argument is that they’re also not NEW, and they’re certainly not unique to Rust. Rather the difference in dependencies between something written in Rust vs. “traditional” C or C++ is that on Unix systems, all these dependencies are still there, just handled by the system instead of the compiler directly
Or… Use something like cap-std to reduce ambient authority like access to the network.
My understanding is that linguistic-level sandboxing is not really possible. Capability abstraction doesn’t improve security unless capabilities are actually enforced at runtime, by the runtime.
To give two examples:
--allow-net
, no dependency will be able to touch the network. At the same time, there are no linguistic abstractions to express capabilities. (https://deno.land/manual/getting_started/permissions)Is there a canonical blog post explaining that you can’t generally add security to “allow-all” runtime by layering abstraction on top (as folks would most likely find a hole somewhere), and that instead security should start with adding unforgeable capabilities at the runtime level? It seems to be a very common misconception, cap-std is suggested as a fix in many similar threads.
Sandboxing is certainly possible, with some caveats.
You don’t need any runtime enforcement: unforgeable capabilities (in the sense of object capabilities) can be created with, for example, a private constructor. With a (package/module) private constructor, only your own package can hand out capabilities, and no one else is allowed to create them.
That is true, in the sense that no dependency is forced to use
cap-std
itself. But, if we assumed for a second thatcap-std
was the rust standard library, then all dependencies would need to go through it to do anything useful.This can also be prevented by making inline assembly impossible to use without possesing a capability. You can do the same for FFI: all FFI function invokations have to take a FFI capability. With regards to the rust-specific
unsafe
blocks, you can either do the same (capabilities) or compiler-level checks: no dependencies of mine can useunsafe
blocks unless I grant them explicit permission (through a compiler flag, for example).I would go the other way, and recommend Capability Myths Demolished, which shows that object capabilities are enough to enforce proper security and that they can support irrevocability.
This doesn’t generally work-out in practice: linguistic abstractions of privacy are not usually sufficiently enforced by the runtime. In Java/JavaScript you often can use reflection to get the stuff you are not supposed to get. In Rust, you can just cast a number to a function pointer and call that.
I would sum up it as follows: languages protect their abstractions, and good languages make it impossible to accidentally break them. However, practical languages include escape hatches for deliberate circumventing of abstractions. In the presence of such escapes, we cannot rely on linguistic abstractions for security. Java story is a relevant case study: https://orbilu.uni.lu/bitstream/10993/38190/1/paper.txt.
Now, if you design a language with water-tight abstractions, this can work, but I’d probably call the result a “runtime” rather than a language. WASM, for example, can implement capabilities in a proper way, and Rust would run on WASM, using cap-std as n API for runtime. The security properties won’t be in cap-std, they’ll be in WASM.
I don’t think this general approach would work for Rust. In Rust,
unsafe
is the defining feature of the language. Moving along these lines would make Rust more like Java in terms of expressiveness, and probably won’t actually improve security (ie, the same class of exploits from the linked paper would work).Thanks, going to read that, will report back if I shift my opinions!
EDIT: it seems that the paper is entirely orthogonal to what I am trying to say. The paper argues that cap model is better that ACL model. I agree with that! What I am saying is that you can’t implement the model on the language level. That is, I predict that even if Java used capability objects instead of security manager, it would have been exploitable more or less in the same way, as exploits breaking ACL would also break capabilities.
Go used to have a model where you could prohibit the use of package unsafe and syscall to try to get security. App Engine, for example, used this. But my understanding is that they ended up abandoning it as unworkable.
Your points are sharp. Note that there was an attempt to make Java capability-safe (Joe-E), and it ended up becoming E because taming Java was too much work. Note also that there was a similar attempt for OCaml (Emily), and it was better at retaining the original language’s behavior, because OCaml is closer than Java to capability-safety.
ECMAScript is almost capability-safe. There are some useful tools, and there have been attempts to define safe subsets like Secure ECMAScript. But you’re right that, just like with memory-safety, a language that is almost capability-safe is not capability-safe.
While you’re free to think of languages like E as runtimes, I would think of E as a language and individual implementations like E-on-Java or E-on-CL as runtimes.
porquoi no los dos?
Rather than remove the package, wouldn’t it make more sense to replace it in order to poison it? That way every build pulling it could get a build error and a notice about the issue.
Removing the package will also cause build errors for the (apparently very few) projects relying on it. Someone investigating why their build is failing will likely come across the info from the crates.io maintainers about this.
Ok thanks?
I can do this in C++, because the language makes it impossible to ship libraries that have dependencies and you don’t get the exponential blowup, but the Rust ecosystem makes this completely intractable.
Last time I messed with rust, I wrote a very basic piece of software (connect to Discord, FlashWindow if anyone says your name, like IRC highlights) and it pulled in over 1 million lines of dependencies. Even if I could audit that, what can I do if I find a crate I don’t like?
I think this is a false dichotomy, as outlined by (as just one example) https://wiki.alopex.li/LetsBeRealAboutDependencies
C/C++ stuff has dependencies, and sometimes enormous amounts of them. This mantra that dependency tree hell is an NPM/Cargo/etc. specific problem is - frankly - ridiculous. I can’t audit all of, say, Boost or LibreSSL much better than I can audit, say, Tokio or RustTLS. Arguably, it’s harder to audit something like LibreSSL than RustTLS on account of the memory management, but that’s a vastly different discussion. Let’s imagine I’m instead talking about NodeJS’s “lodash” or something, which I also haven’t read every line of.
But you have package managers as a line of defense with C/C++ and most other popular languages. I don’t know why this is so hard to understand for many people.
some C++ libraries have very high standards for contribution to them and the overall review + release process. C++ Boost is one of them ( https://www.boost.org/development/requirements.html ).
Perhaps in some ways, the argument can be made that one big (but composable/de-composable), thoughtfully managed library with high-bar for entry (like Boost) – in longer term is significantly better, than 100s or 1000s of crates or NPM packages (also helps that the quality of library informs upcoming language standards).
The article you linked, to me, reads a bit more like false dichotomy (as it does not take into account the maturity of both technical and release process maturity characteristics of the C and C++ libraries).