I wanted to re-raise the discussions around preventing 'hidden unsafe', meaning Rust code in the unsafe
dialect that doesn’t require the direct use of the unsafe
keyword nearby.
It may be tempting to dismiss these concerns because the patterns are so contrived that they would never organically appear in non-malicious code, but the angle I’m coming from is that they could be used to hide subtle backdoors in Rust code (underhanded Rust contest anyone?).
I'm also not talking about hiding vulnerabilities in safe Rust code; obviously in a security review it’s not sufficient to just grep for unsafe
to find vulnerabilities since safe code can be vulnerable too, but at least you would expect to find all of the unsafe Rust by doing this... otherwise, what’s the point of the keyword?
I’ve talked about unsafe
macros being the main technique in a previous thread, but it’s now closed, so I’m creating a new one so I can add new thoughts on an idea to combine this with the include
built-in macro for more chaos.
Essentially, the simplest, shortest, ‘default’, way people use expr
with unsafe
in macros allows the unsafe
block to spread, meaning that a malicious actor can use someone else's macro to hide their own unsafe
statements without needing to introduce their own corresponding unsafe
blocks.
A clearer example than originally shown (although there is some irony that some of the initial replies missed the hidden get_unchecked
in the original example): if you naively create a mmap
wrapper macro that takes a size, it allows any of the callers to hide their own unsafe code in the macro arguments without needing their own unsafe blocks:
Crate:
use libc::*;
#[macro_export]
macro_rules! alloc_pages {
($length:expr) => (
unsafe {
mmap(0 as *mut c_void, $length, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0)
}
);
}
Malicious code containing 'hidden unsafe’ (dereferencing an arbitrary pointer):
fn main() {
// Intended use of library
let p = alloc_pages!(0x4000);
println!("{:p}", p);
// Unsafely dereferencing raw pointer without using unsafe keyword!
let p2 = alloc_pages!(*(0x41414141 as *mut usize));
println!("{:p}", p2);
}
Again, this violates the idea that you can simply grep for unsafe
to find all of the unsafe Rust in a project. Not only that, but it was pointed out that more advanced tools like Cargo-Geiger and even #![forbid(unsafe_code)]
do not detect/forbid the hidden unsafe dereference. I can’t understand how anyone wouldn’t consider this a bug, but even assuming the possibility of the existence of ‘legitimate’ use-cases of this as a feature, should they really be prioritised over violating the ability to easily search for unsafe Rust? I think a breaking change is warranted.
I would like for that example to no longer compile, without an additional unsafe block:
alloc_pages!(unsafe { *(0x41414141 as *mut usize) } );
I believe there would be real appeal to hiding backdoors this way. If you are reviewing Rust code, it would be very easy to quickly assume certain lines are safe under the rationale that an unsafe
keyword would be nearby if it were doing anything sketchy. For example, a variable assignment like x = y
would look extremely innocuous if there were no unsafe
keyword drawing attention to it, but if it happens to be against a global variable (disabling #[warn(non_upper_case_globals)]
to disguise further) and this introduces an exploitable data race condition, this could be extremely easy for a review to miss.
It’s particularly annoying because Rust macros are supposed to be more sophisticated than in C where expressions are just ‘pasted’ and hope for the best. For example, Rust macros do solve the common C mistake where pasting an expression could lead to unintended order of operations; with an input like 1 + 2
a macro that does input * 2
will always get 6
, as opposed to 5
like it may be in an equivalent C macro. Why can’t macros also be smarter against pasting code into unsafe
blocks by default, essentially creating unintended implicit unsafe
blocks everywhere?
I wanted to add a handful of real examples to back up that people do indeed write macros this way. I don’t blame these authors; I think the language is at fault here:
These are all protected at least against external code introducing unsafe
since they are not exposed via #[macro_export]
.
Anyway, what I wanted to add to the discussion today is that the include
macro has the potential to copy these macros into a context where they can be abused. The idea would be an attacker could include
some ‘utils’ file from another library as an unusual and bad, but not overly suspicious from a security review, practice. They would then be able to abuse these macros, which were originally thought to be safe from abuse due to not being exposed, to introduce their own hidden unsafe. Again, if the unsafe code they introduce is as simple as a variable assignment that happens to be on a global, would you really be confident that a reviewer would spot that without an unsafe
keyword nearby?
Fortunately(?) I couldn’t get the include
macro to work against source files that happen to contain documentation, which covers the 3 examples I listed above - but this definitely shouldn’t be considered as any kind of durable mitigation, and I think the idea still has potential (and we should consider solutions). Playground:
use rand_core; // 0.6.2
include!("/playground/.cargo/registry/src/github.com-1ecc6299db9ec823/rand_core-0.6.2/src/impls.rs");
(In the context of a malicious library, Cargo thankfully standardizes library directory structure, and so we can just use a predictable relative path to another library "../../rand_core-0.6.2/src/impls.rs"
instead).
error[E0753]: expected outer doc comment
--> src/../.cargo/registry/src/github.com-1ecc6299db9ec823/rand_core-0.6.2/src/impls.rs:9:1
|
9 | //! Helper functions for implementing `RngCore` functions.
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: inner doc comments like this (starting with `//!` or `/*!`) can only appear before items
There may be a bunch of other unscrupulous opportunities for include
to activate potentially unreviewed code such as a non-code file like the LICENSE that obviously won’t have been reviewed as Rust code. What would be truly evil would be to have a documentation file “Unsafe guidelines” that contains examples of unsafe Rust, but is crafted to be a fully valid Rust source file and can be subtly include
d and abused deep in some ‘util’ macro in another crate.
UPDATE: I managed to find a crate that doesn't have documentation, so isn't affected by the above compiler error. I was able to produce a working Playground demo for the include!
idea - gaining access to unsound macros that are not supposed to be exposed, and then abusing them to introduce unsafe behavior (accessing global mutable without our own unsafe
block). This macro was just using ident
instead of expr
so we can't introduce arbitrary code, but I think it's still interesting.