An idea to mitigate attacks through malicious crates

I probably do. At this stage, I'm not sure how we could both annotate functions/data structures/modules with capabilities-as-feature-flags and still compile the crate accurately and contaminate client crates.

I wonder if there’s a 80% solution here. Maybe we don’t need this for big crates, but could find a version to increase confidence in the many little utility crates that aren’t worth using if you need to audit them.

As a trivial example, I’d never use the default crate if I needed to code audit it – it’s way easier to just re-implement myself. But it also very obviously does nothing scary, in a way that would be nice to expose (and re-confirm when building).

2 Likes

I believe that this is the kind of thing that can be fine-tuned later.

I'm not sure this is a precedent. Nobody has ever broken into my unlocked car, but that doesn't mean it is a reasonable security policy. How many people have attempted to use an OCaml extention in the MMM web browser to compromise a system?

1 Like

I think a CSRNG meets the requirements for a lang item, namely:

  • Already an important part of the core language
  • Low-level primitive with a large variety of potential platform-specific backends which need a common interface (especially on embedded). We're talking something where on embedded core Rust might need to talk directly to a specific hardware intrinsic (e.g. via a C library)
  • Trouble wrapping it up in the crate ecosystem, especially in a way that permits reuse between libcore/libstd and crates

If you have further thoughts on this matter, I'd suggest discussing it on `OsRng` crate? (or optional PRNG dependencies) · Issue #648 · rust-random/rand · GitHub

1 Like

The main problems with existing unsafe lints:

Unless there are additional unsafe lints I'm unaware of in the core language, I feel the existing mechanisms aren't suitable for and are somewhat orthogonal to the proposed goals in this thread.

cargo rustc --min-lint-unsafe_code=forbid (or something - the inverse of what cargo does to lints currently).

additionally: the ability to force the compiler to enable/disable feature flags in a way that code can’t override.

Nightly feature (names) (#![feature(..)]) are not “stable”, never have been, and never should be. If you’re suggesting using these features to “turn off” features, a) it wouldn’t work for capabilities (most of the language is still #[stable(feature = "rust1", since = "1.0.0")], including the “dangerous” things in std::fs, std::io, etc.) and b) we explicitly don’t want to opt-out of these features, especially if said opt-out is transitive; it’d just lead to language fragmentation, and all of the complaints around default binding modes (match ergonomics) made it clear that this is a firm position that the core team hold.

(If I’m mistaken, and you’re referring to cargo features: the stdlib can’t have cargo features, as both it’s not a cargo dependency and it’s not recompiled locally, plus it goes against the current position of features being transitively additive.)

you enable/disable per-crate and it’s opt-in. and it only applies to the library side of things. and it only applies to the stable features.

my point is the compiler already implements feature flags, we just need to make them usable for this.

fragmentation is a good thing actually.

I think lints should always have non-transitive forms, but clearly many like #![forbid(unsafe_code)] need transitive forms as well. Also yes some attributes like #[no_mangle] should be considered, if not unsafe, at least sensitive.

Of all the things you named, the only one which isn't under the umbrella of unsafe is no_mangle. However, if you look at my "unsafe features" proposal, you'll see I have suggested gating no_mangle using "unsafe features" as well.

I think that deciding how to represent crate capabilities is secondary at this stage. The most important part is to determine what we wish to represent.

What do people think of the following?

  1. Unsafe code
    • unsafe blocks require a capability cap_unsafe;
    • unsafe impl data structures require a capability cap_unsafe;
    • unsafe or #[no_mangle] function definitions do not require any capability;
    • calling a #[no_mange] function requires a capability cap_unsafe;
  2. I/O code
    • calling a function or method defined in std::fs requires cap_io;
    • calling a function or method defined in std::net requires cap_io;
    • calling a function or method defined in std::command requires cap_io;
  3. Crate-level capabilities
    • if any of your code requires a capability c, the entire crate requires capability c;
    • (we do not examine targets doc, test);
  4. White-listing
    • A whitelist is shipped with the tool. This whitelist contains libraries that are absolved from containing capability c.
    • The whitelist contains the stdlib and possibly a few blessed libraries at specific versions.
    • A mechanism will need to be defined at a later stage to introduce additional libraries + versions in the whitelist. This mechanism is beyond the scope of the current work and probably involves audits.
1 Like

Everything should be explicit and non-transitive.

You should specify which crates a dependency is allowed to depend on.

You should specify what that dependency is directly allowed to do, that isn’t covered by its dependencies.

Dependencies of dependencies can be regulated separately.

Feature flags (compiler/std), crate features, and better rustc-args-based lint control are the main source-wise control features. Everything else is Cargo.

Even better: 90% of this is already implemented. In rustc, all that’s missing is finer-grained feature flags, which is almost as trivial as documentation changes, and the ability to disallow them with rustc args. That’s it. The system for disallowing them is already in place, we just need to expose it to cargo/CLI.

My suggestion is arbitrary labels on unsafe code in the form of arbitrary Cargo features. This has the advantage of being extremely flexible while also reusing existing language functionality (i.e. cargo features).

This is immensely coarser grained than what I'd want, to the point I don't think it's useful. Something like cap_unsafe grants unbounded authority, or as @kornel put it "crate can do whatever it wants". My proposal adds specific feature labels to each usage of unsafe to avoid any sort of unbounded authority like this.

Within the unsafe-features scheme I was proposing, I'd rather see something like this:

  • calling a function or method defined in std::fs requires the std/fs "unsafe feature".
  • calling a function or method defined in std::net requires the std/net "unsafe feature".
  • calling a function or method defined in std::command requires the std/command "unsafe feature".

At the very least, I think something should be able to talk on the network without being able to write to the filesystem or execute commands.

What happens when the dependencies of the latter change, or their transitive dependencies?

I'm failing to see how a non-transitive mechanism can provide any sort of useful security guarantees.

You specify each dependency and so on. It’s not transitive in the sense that specifying “foo” doesn’t specify “foo/bar” unless you also explicitly specify “foo/bar”.

Transitive, in the context of dependencies, means dependencies of dependencies are pulled in automatically. It’s not transitive because you need to manually specify everything.

restrictions on “foo” don’t apply to “foo/bar”, useful if “bar” is e.g. a sandbox.

Did you mean #[no_mangle], and why is calling a #[no_mangle] function unsafe. As I understand it, all it does is allow the function to be exported for ffi purposes and used in other languages. There doesn't seem to be a good reason to require a capability for this.

  • calling a function or method defined in std::fs requires the std/fs “unsafe feature”.
  • calling a function or method defined in std::net requires the std/net “unsafe feature”.
  • calling a function or method defined in std::command requires the std/command “unsafe feature”.

I'm pretty sure that we cannot enforce the difference. std::command can easily read/write from a file or the network. On Unix, std::fs can easily read/write from the network and std::net can communicate with other processes, etc.

1 Like

Yes, I did mean #[no_mangle]. I wrote this because of issue 28179, but once this is entually fixed at the compiler/linker-level, we could remove this restriction.

1 Like

Pretty sure you will want to block defining a #[no_mangle] function (or any other item) as well, as mentioned in the linked issue you can overwrite other functions just by defining something as #[no_mangle]