I'd like to share an idea I've been developing to improve the way we use unsafe in Rust. Before writing a full RFC, I'd love to gather some feedback from the community.
The Problem
The current unsafe {} block disables a wide range of compiler safety checks, without any distinction. This is powerful, but arguably goes against Rust’s spirit of safety and explicitness.
It's hard to audit an unsafe block — what exact risks are being taken here?
Tools like rust-analyzer, clippy, or transpilers like c2rust can’t help much without more context.
Performing safe refactoring becomes harder, especially in large or automatically generated codebases.
The Proposal: Granular unsafe Blocks
The idea is to allow developers to explicitly specify which categories of unsafe operations are permitted inside the block:
unsafe allow(deref_raw, ffi_call) {
// only raw pointer deref and FFI calls are allowed here
}
Or using a more concise form:
unsafe(deref_raw, ffi_call) {
// equivalent shorter form
}
These would coexist with the current generic unsafe {} blocks and be completely optional.
Example Safety Categories
deref_raw: dereferencing raw pointers
ffi_call: calling external functions
mutable_aliasing: mutable reference aliasing
union_access: accessing union fields
manual_drop: manual invocation of destructors
The compiler would check the contents of the block and only allow operations matching the listed categories.
Benefits
Makes unsafe blocks more readable and auditable
Allows tools and linters to better assist with unsafe code
Helps transpilers like c2rust emit more informative and safer code
Enables AI-assisted or expert-guided tooling to remove or reduce unsafe usage
Aligns with Rust’s philosophy of explicitness and safety by default
Open Questions
Should the allow(...) keyword be required, or optional?
How many categories should be exposed to users?
Should custom or future safety groups be allowed later on?
I'm still learning Rust and getting familiar with the internals, so any corrections or pointers to previous discussions are very welcome!
This is not unsafe, it is undefined behavior. Also your list is missing calls to other unsafe functions (not necessarily external functions).
This will add more text, which doesn't always make something more readable. esp. if this is taken far enough, then this would make code less readable! (by making these annotations obfuscate what's actually going on).
I don't think this is as useful as you may think unless you allow unsafe functions to declare custom safety categories. Otherwise, even a call to ptr::copy_nonoverlapping (which is just an ordinary unsafe function) would be enough to prevent using this feature.
There are only 5 operations (4 if you only count expressions) that you can only do with unsafe:
dereferencing raw pointers
calling unsafe functions (ffi functions are unsafe by default, and manual invocation of destructors is done by calling an unsafe function too)
accessing static mut variables
accessing the fields of a union
implementing unsafe traits (this is always clear because the unsafe keyword appears in a different syntactical position)
To me this does not seem very "wide", but it's true that you can do a lot with this (especially when calling unsafe functions!). However if you want to limit that you'll need to introduce a way to distinguish what different unsafe functions are for.
That said, in my opinion the difficulty in auditing unsafe blocks are others:
what are the non-local invariants that need to be upheld for it to be safe? This should be handled by // SAFETY comments, but often they are missing or not exhaustive enough. Often it's also not clear where these invariants should be even listed, in case multiple pieces of code rely on them or need to uphold them.
what are the unsafe operations in the unsafe block (especially for big ones)? Note that this is different than asking what kind of operations are in there, I'm asking e.g. which calls are actually to unsafe functions vs safe ones. This could be solved for example by having an unsafe modifies that applies to single operations and not to an entire block, so that they always mark all and only those operations that are unsafe to perform.
Pairing up requirements seems like a really good idea, though some thought is required for how to transition to this. And how they factor into semver is also important.
My thinking is that this system would have to be a lint you opt in to, which could then flip to warn by default, and then eventually with a new edition to deny by default.
But since adding reasons would be semver breaking, some careful thought is needed for how to handle the case that std (or any other library really) needs to add an extra reason down the line: mistakes happen and perhaps people didn't realise one of the required conditions originally.
Side rant: I'm sure you could invent some std only "this reason is only required from edition N", but it would be nice if std wasn't so special and other crates could also benefit from advanced semver anti-breakage tricks. Why couldn't you say that "if the dependency on me in cargo.toml is 1.2 or later this thing is unsafe" for example? That would be the crate equivalent of the getenv unsafe trickery that std does, making what minimum version people specify in their cargo.toml determine such things and avoiding needless semver breakage.
Note that this is arguably already breaking to callers: their safety proofs are incomplete, and they're similarly unsound.
But there's also a really easy fix for it. Just say that the only hard error remains the unsafe block being missing entirely, have a warn-by-default lint for not specifying any of the reasons, and a deny-by-default lint for specifying some but not all of the reasons.
That way insufficient proof would be cap-linted so you can still use out-of-date libraries if you really want and it means you can write the unsafe code without any of the reasons at first to at least run some unit tests to see if at the happy path works, then add the specific proofs to help catch anything you forgot to consider. And if you consume a library that adds these, it's just a warning that you should also start using them in your calls to that library.