- Feature Name: cargo_safetyrails
- Start Date: 2017-07-13
- RFC PR: (leave this empty)
- Rust Issue: (leave this empty)
Summary
This RFC proposes Cargo Safety Rails, allowing crates to ensure that dependent crates don’t use unsafe language features.
By declaring a dependency “untrusted”, the user tells cargo to compile it with the -F unsafe_code
option, preventing the dependency from using the unsafe
keyword.
Motivation
Type safety allows the programmer to have strong guarantees about what incorporated code can, and cannot, do. For example, if I incorporate a Rust library, I don’t have to worry about it accessing arbitrary memory, modifying my private variables etc. However, this is only true as long as the library code doesn’t use unsafe features of the language.
Currently, unsafe features are guarded in Rust with the unsafe
keyword: If I want to do something that might violate type-safety (or borrow rules), I have to wrap that code in unsafe
. In addition, the compiler let’s us prevent a crate from using the unsafe
keyword entirely by either using the -F unsafe_code
compiler flag or adding #![forbid(unsafe_code)]
to the crate’s top-level module.
It is also possible to ask Cargo to compile all code with the unsafe
keyword forbidden by setting the RUSTCFLAGS
environment variable.
But what if some of my dependencies should be allowed to unsafe
(like a trusted binding to a C-library, a hardware interface layer or data structure that achieves high efficiency through some memory tricks)? In theory I could vendor every one of my dependencies and manually add #![forbid(unsafe_code)]
to them, but that imposes a high barrier for updating exactly the dependencies I should be able to update with relatively little auditing. Moreover, as dependency trees become larger, this becomes unwieldy.
The safety rails feature would, instead, allow Cargo users to specify, declaratively, which dependencies the don’t trust completely and be confident they will not be allowed to use unsafe language features, without having to audit each version of the code, while allowing trusted dependencies to do so. It would also result in a explicit list in the Cargo.toml
file of dependencies that deserve extra scrutiny.
Side note: I believe @josh did some analysis of crates.io and found that a relatively small number of crates currently use unsafe features, and those that do are often C-bindings. This would imply that a feature allowing a Cargo use to disable unsafe
for some dependencies would actually be usable with crates.io immediately.
Detailed design
Since this is a Pre-RFC, I would appreciate feedback on this initial design proposal. I’ve tried to come up with a design that doesn’t change the experience for Cargo users who wouldn’t use this feature (e.g. dependencies should be trusted by default).
User Interface
The dependency section of the manifest format has a new, optional, field called “safety”, which can either be “trusted”, “untrusted” or “inherit”:
[dependencies.awesome_widget]
features = [ ... ]
safety = "untrusted"
[dependencies.carefully_written_c_binding]
features = [ ... ]
safety = "trusted"
Semantics
When a dependency with “untrusted” safety
, cargo adds it and all of it’s dependencies to a list of crates that will be compiled with -F unsafe_code
.
Setting the safety
of a dependency to “trusted” will remove it from this list, meaning the user trusts this crate to use the unsafe
features, regardless of whether it’s used directly or through a dependency.
A dependency with safety “inherit” is trusted by default, unless it is in the dependency graph of an “untrusted” dependency.
The core
crate is always trusted.
Propogation
Safety specifications in “trusted” dependencies can only serve to constrain the list set of trusted crates. That is, only the manifest file for the artifact being compiled can constrain the declare a dependency “trusted” and have it take affect, but any “trusted” dependency can declare it’s dependency “untrusted”.
“Untrusted” dependencies have no affect on the safety of their dependencies since their dependencies are untrusted by default, and they should not be allowed to add “trusted” signifiers.
How We Teach This
The terms “trusted” and “untrusted” were chosen to be distinct from “safe”/“unsafe”, but to have a similarly intuitive meaning. In particular, the declaration:
safety = "trusted"
Means that the user trusts the dependency to preserve Rust safety semantics, even if it happens to use unsafe features, while
safety = "untrusted"
means the user doesn’t trust the dependency to preserve type-safety, and wants to compiler/build system to vouch that it doesn’t use the unsafe
keyword.
In this sense, this feature should be presented as a way of enforcing an existing concept (type-safety and the unsafe
keyword guard) in the build system.
This feature does rely on a relatively nuanced understanding of type-safety, how it can be broken, and how it can be preserved despite using the unsafe
keyword. So, presentation probably has to rely on the reader having intermediate familiarity with both the language and Cargo. Since the design makes the feature transparent to users who don’t use it, it’s probably OK to defer explanation to more hidden parts of the documentation.
Drawbacks
It’s not clear yet how many applications could actually use this kind of feature, since it’s not clear how much of crates.io can be compiled without unsafe (though initial results imply that a lot of it).
Moreover, this design might require users to list a large number of dependencies they don’t directly depend on only to be able to mark them “trusted” because their dependencies use them. Over time, as dependency graphs change, this could result in a large list of unused dependencies.
Alternatives
I’m not entirely sure! That’s what I’m asing y’all for!
My main thought is that the design uses a default trusted model in order to maintain compatibility with existing crates. An untrusted by default might yield a cleaner design with more concise Cargo.toml
. It might be possible to get the best of both worlds with a Cargo flag that enables the feature (so, without the flag, all crates are effectively trusted).
Unresolved questions
Are there crates, other than core
that should be trusted by default? std
and collections
are likely candidates, but I wonder how that impacts std-less bare-metal applications.