[PRE-RFC] Granular Unsafe Blocks - A more explicit and auditable approach

Hi everyone,

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!

Thanks for reading, [@Redlintles]

2 Likes

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.

2 Likes

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.

4 Likes

This has come up a bazillion times. How is this different from, say, [Pre-RFC] Single function call `unsafe` - #16 by scottmcm ?

I continue to think that smaller unsafe scopes is just a workaround, and we should fix it with a better system instead. See this sketch:

3 Likes

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.

2 Likes

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.

5 Likes