An idea to mitigate attacks through malicious crates

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]

It's roughly as unsafe as unsafe, and the lang team has plans to fix it, so I wouldn't fixate on #[no_mangle].

Here’s hoping for time to read the rest of this thread more carefully, but I want to relay some experience in this area…

When I saw trust-dns came out, I started PR 71 object capability discipline audit. I used tag_safe to do it. I tagged functions such as std::io::_print as not ocap-safe, and added:

#[req_safe(ocap)]
fn ocap_main(argv: std::env::Args) {

and let the tag_safe static analysis plug-in find violations.

Maybe this sort of thing is built-in to the compiler by now.

It is of course easier to deal with ocap-safety at the crate level, but… for example… I used to maintain sqlite bindings, and they’re kinda useless without the ability to open an sqlite file, and the sqlite API opens files using pathname strings, not file descriptors, so I would have had to build an sqlite virtual filesystem, which would be a pain, so I punted. But I did put access to ambient authority in a separate module: https://github.com/dckc/rust-sqlite3/blob/master/src/access/mod.rs It has a TODO:

//! *TODO: move `mod access` to its own crate so that linking to `sqlite3` doesn't
//! bring in this ambient authority.*

I’m really happy to see work in this area. I have been wishing for it since back in 2012:

1 Like

To the extent there are direct Rust call graph dependencies, since I'm suggesting a feature which requires explicit whitelisting based on conditional compilation, programs which don't correctly whitelist the necessary dependencies will fail to compile.

As for other OS-level side-channels or privilege escalations, especially involving other programs running on the same system, I do not think that is something Rust can or should guard against. Does being able to write to the filesystem provide an RCE vector? An awful lot of the time yes. Does that mean there isn't value in splitting up these various subsystems? I think that's debatable. For example:

...that's kind of the point. And to be fair, there are potential ways to escalate from this capability to RCE, especially if there are insecure services bound to a localhost interface which these programs can access (e.g. Redis).

But if we grant a crate the authority to use std/net, should it automatically be able to modify the filesystem and execute programs? My personal inclination would be no.

I get your point. My fear here is that by having a different capability name, we would lead people to assume that std/net can only ever access the net. We know it's not true, but not everybody does.

Let's deal with the conditional compilation or other possible mechanisms later :slight_smile:

RCE by definition always involves the network, but to me the "network" part isn't the problem, it's the "code execution" part that's the problem, and that's a problem with the vulnerable application which is reachable over the network, not the ability to talk on a network.

As a case study, browsers confer to HTML/JS apps the ability to make a wide range of network requests directly from a user's computer. This has been used in practice for RCE in conjunction with a vulnerable service.

Does this mean we should treat network access as RCE and say... build an exec() feature into browsers which would allow JavaScript programs to run arbitrary code?

I hope you can see how these two things aren't equivalent.

I'm bringing up conditional compilation in the context of my very similar proposal, which I made a few days before yours (that said I'm still glad you're thinking along the same lines):

Namely, I proposed leaning on cargo features as the fundamental modeling dimension for "permissions". You are proposing a completely separate mechanism.

I think using cargo features for this purpose not only provides reuse of an existing mechanism, but also provides for a simpler, more flexible solution which works for both std and crates alike which nicely answers questions like "if we grant permission X, doesn't that also transitively grant permission Y?".

Using conditional compilation as the mechanism by which these dependency relationships are gated means if we ever get them wrong, programs won't compile. In this way, it's not possible for side effects/ambient authority to "sneak in" via logic bugs (other than bugs in the "cargo feature" conditional compilation logic, which is hopefully fairly robust at this point).

Let me clarify this: I don't care about the mechanism yet. I want to specify what property we need to guarantee. How we do it and how we store and present the results will come later.

I fully realize that they are not equivalent. I also suspect that if we differentiate sys/net, sys/fs and sys/command, some developers are going to assume "oh, it doesn't have sys/net, so it cannot access the network", "oh, it doesn't have sys/fs so it cannot modify a file". Both assumptions sound true, are false and require knowledge that many developers do not have to make the difference.

What would you think of starting with io (or some similar label) and possibly later defining io as a shortcut for [sys/net, sys/fs, sys/command]?

Then why are you proposing we do as much?

I'm a fan of misuse resistance, but I'm also a fan of least authority, and I think if you fail to achieve the latter, this proposal provides little value.

I think the potential risks of confusion over privilege escalation paths between fine-grained subsystem permissions matter less than conferring blanket RCE authority to crates which do not need it.

You're suggesting lumping "network access" together with permissions that confer "take of my computer" authority. I don't think that makes sense. I would prefer something more fine-grained than that.

Well, least authority assumes that you can make a difference between your capabilities. How do you differentiate between sys/net, sys/fs and sys/command, exactly?

In my proposal, each usage of unsafe would need to be tied to a corresponding "unsafe feature".

Supposing a std/net "unsafe feature", some of the relevant unsafe code is here:

https://github.com/rust-lang/rust/blob/master/src/libstd/sys_common/net.rs

As a concrete example:

would need to be updated to something like:

#[cfg(unsafe_feature = "net")]
pub fn setsockopt<T>(sock: &Socket, opt: c_int, val: c_int,
                     payload: T) -> io::Result<()> {
    unsafe {
        let payload = &payload as *const T as *const c_void;
        cvt(c::setsockopt(*sock.as_inner(), opt, val, payload,
                          mem::size_of::<T>() as c::socklen_t))?;
        Ok(())
    }
}

...and this would need to be performed on a case-by-case basis for each usage of unsafe.

Doing it this way ensures if we miss anything, the compiler will catch it, either in the form of an "untagged" unsafe block, or a compile error.

Thanks for the example, but at this stage, I don’t really need code. Rather, I’d like to know how many unsafe_features would you have and what is the intended difference between them.

The unsafe, no_mangle, sys/fs, and sys/command capabilities described so far are all synonyms for “crate can do anything” (via inline assembly, #28179, proc/$pid/mem, and opening a shell respectively). They all mean exactly the same thing.