rfcs icon indicating copy to clipboard operation
rfcs copied to clipboard

RFC: Safety Tags

Open zjp-CN opened this issue 5 months ago • 15 comments

Summary

This RFC introduces a concise safety-comment convention for unsafe code in standard libraries: tag every public unsafe function with #[safety::requires] and call with #[safety::checked].

Safety tags refine today’s safety-comment habits: a featherweight syntax that condenses every requirement into a single, check-off reminder.

The following snippet compiles today if we enable enough nightly features, but we expect Clippy and Rust-Analyzer to enforce tag checks and provide first-class IDE support.

#[safety::requires( // 💡 define safety tags on an unsafe function
    valid_ptr = "src must be [valid](https://doc.rust-lang.org/std/ptr/index.html#safety) for reads",
    aligned = "src must be properly aligned, even if T has size 0",
    initialized = "src must point to a properly initialized value of type T"
)]
pub unsafe fn read<T>(ptr: *const T) { }

fn main() {
    #[safety::checked( // 💡 discharge safety tags on an unsafe call
        valid_ptr, aligned, initialized = "optional reason"
    )]
    unsafe { read(&()) };
}

Rendered

zjp-CN avatar Jul 31 '25 14:07 zjp-CN

Really appreciate feedbacks from all of you! I've made big revisions and resolved all conversations. I’d value any further thoughts on the updated document.

The main two concerns I see are

We could keep uninhabited enums as the only tag declaration and allow any arguments in tag usage without validation. Tag arguments would still refine the description of an unsafe operation, but they are never type checked. An example:

#[safety::declare_tag(
  args = [ "p", "T", "len" ],
  desc = "pointer `{p}` must be valid for reading and writing the `sizeof({T})*{n}` memory from it"
)]
enum ValidPtr {}

#[safety::requires { ValidPtr(ptr) }]
unsafe fn foo<T>(ptr: *const T) -> T { ... }

#[safety::checked { ValidPtr(p) }]
unsafe { bar(p) }

Semantic granularity: SemanticTags must label a single unsafe call, or an expression contains single unsafe call. No longer constrained by the visual boundaries of unsafe {}.

zjp-CN avatar Aug 03 '25 10:08 zjp-CN

An unsafe counter maybe useful, and easier to maintain, so this code would hard error on something_unsafe_4:

unsafe(3) {
   something_unsafe_1( something_unsafe_2(something_unsafe_3()), something_unsafe_4() );  
}

This code would error even if the (3) were changed to a (5) too, but hypothetically you could've some #[allow(undercounted_unsafe)], although not sure that's really necessary.

unsafe(explain) would always print a warning that says exactly what safety violations it permitted, so you remove it or change it to a number to compile without warnings.

We could eventually default unsafe to unsafe(1) and provide an unsafe(*) or whatever.

Now there could be changes in how the compiler counts unsafe code, which maybe an issue for tags too, so then the unsafe code becomes impacted by editions, not sure if that's a problem.

burdges avatar Aug 03 '25 14:08 burdges

Do you indicate tags work bad on nested unsafe calls? @burdges

Tags are for single unsafe call, meaning nested unsafe calls have to be flatten as assignments:

#[safety::checked(...)] // 💥error: ambiguous to discharge invariants for multiple unsafe calls: call1 and call2 
unsafe { call1(call2()) }

// Tag as follows
unsafe {
    #[safety::checked(...)]
    let val2 = call2();
    #[safety::checked(...)]
    call1(val2)
}

But Rust allows attributes on nested expressions, so you could also try tagging like this, which rarely happens in reality tho.

unsafe {
    #[safety::checked { ValidPtr: "outer" }]
    read(&
        #[safety::checked { ValidPtr: "inner" }]
        read(&())
    ) 
};

Edit: the second will be not supported. See this conversation.

zjp-CN avatar Aug 03 '25 14:08 zjp-CN

@zjp-CN > Do you indicate tags work bad on nested unsafe calls?

I've no opinion on this RFC, or the previous one, or when one should create some static analysis plugin interface.

I'm only pointing out that an unsafe counter achieves lots, without increasing the langauge complexity much, so imho most developers would use an unsafe counter, not a claim we can make for more complex schemes. That doesn't say those schemes do not have a place.

burdges avatar Aug 03 '25 20:08 burdges

I agree forcing checked clauses to be on unsafe calls might be too restrictive (in particular in comparison with current practices of having unsafe comments on unsafe blocks). However this unsafe counter seems a completely different proposal, which doesn't address most of the concerns this RFC address (like catching unsafe functions that change their requirements between 2 major versions).

Maybe a mix between a previous version of this RFC and the current one would be better, namely that checked clauses can be written anywhere between the unsafe call itself and the closest unsafe block containing it.

ia0 avatar Aug 04 '25 08:08 ia0

IIUC unsafe counter is a practice to alert unsafe operations are not checked against their safety requirements.

It's a piece of linter work: undocumented_unsafe_blocks, but on unsafe operations (calls, and other unsafety), so maybe name it undocumented_unsafe_operations.

Discharge of safety tags can be considered a rigorous undocumented_unsafe_operations lint as @ia0 points out, and a design on machine-readable/checkable safety requirements.

checked clauses can be written anywhere between the unsafe call itself and the closest unsafe block containing it.

checked can be attached to an unsafe block as long as the block containing single unsafe call (no restriction on how many safe calls are in it).

zjp-CN avatar Aug 04 '25 08:08 zjp-CN

checked can be attached to an unsafe block as long as the block containing single unsafe call (no restriction on how many safe calls are in it).

Right. What I meant was to also permit this when there are multiple unsafe calls within the unsafe block (or expression) where the checked clause is present. The semantics would be that the checked clause needs to contain the union (with or without duplicates, TBD) of the unsafe call tags. That's not "clean", but it gives some amount of freedom to users that could help in such situations where multiple unsafe calls are chained. This is a trade-off essentially.

ia0 avatar Aug 04 '25 09:08 ia0

checked clause needs to contain the union (with or without duplicates, TBD) of the unsafe call tags. That's not "clean"

Yeah, I thought about that too. And it's basically suggesting

#[safety::checked(TagForCall1, TagForCall2)]
unsafe { call1(call2()) }

// or
#[safety::checked(TagForCall1, TagForCall2)]
unsafe { call1(); call2(); }

It's not allowed in this RFC due to exactly what you said, instead split inner results into assignments as mentioned above.

zjp-CN avatar Aug 04 '25 09:08 zjp-CN

It's not allowed in this RFC due to exactly what you said

Ok, so there's no trade-off. That's fine for me, I'm just noticing that some may find this too strict. But then writing unsafe code is not necessarily supposed to be an enjoyable experience, so biasing towards "safety"/"precision" to the detriment of "user experience" is understandable. It just needs to be a deliberate choice and not an oversight of the consequences.

ia0 avatar Aug 04 '25 09:08 ia0

Not too relevant to this RFC, but I just saw an OSDI paper Paralegal: Practical Static Analysis for Privacy Bugs (repo) introduces markers as a key abstraction for mapping the policy onto the program. Basically it maps English texts to lightweight attributes in source code to find bugs.

zjp-CN avatar Aug 04 '25 09:08 zjp-CN

This flavor of thing would be nice to introduce more structure (and the possibility for automated tooling) into complex unsafe code bases like bevy_ecs, which I help maintain.

alice-i-cecile avatar Aug 06 '25 05:08 alice-i-cecile

https://github.com/rust-lang/rust-clippy/pull/11600 was a related effort on the clippy side that stalled on inactivity, though not specific to unsafe

Alexendoo avatar Sep 07 '25 01:09 Alexendoo

rust-lang/rust-clippy#11600 was a related effort on the clippy side that stalled on inactivity, though not specific to unsafe

Thanks for the link. I’ve expanded the “Alternatives” section with a side-by-side comparison.

zjp-CN avatar Sep 07 '25 07:09 zjp-CN

I've left some comments in rust-for-linux channel on zulip:

we've proposed this RFC safety tags (structured safety comments but in tool attribute syntax) in rust-lang repo to achieve the following goals:

  1. annotate safety properties on public unsafe APIs in libcore/libstd: downsteam crates like R4L will benefit from these tags
  2. tag checking will be implemented in clippy: R4L will just gain the SP checks out of the box; we'd like to implement and maintain such feature in clippy, but currently it seems no interest from clippy and R4L: #clippy > Discussion with R4L
  3. support tag LSP in Rust-Analyzer to have hover-doc, jump-to-definition, and auto-completion on safety tags for better dev-ex
  4. generate safety API documentation from safety tags to avoid replication: we had a small chat with @GuillaumeGomez in RustChinaConf2025, and he'd like to have rustdoc recognize such safety attributes in rendering

src: #rust-for-linux > Provide the meaning of "Valid" to Understand. @ 💬

zjp-CN avatar Sep 16 '25 15:09 zjp-CN

Allowing an unstructured safety comment like #[safety::checked("<reason>")] would be nice to allow clippy to auto port over // SAFETY: <reason> comments, maybe with a warn lint that encourages you to structure it

TimTheBig avatar Oct 01 '25 19:10 TimTheBig

We presented a talk on safety tags last Sunday. The talk is in Chinese, but the slides are in English.

The presentation introduced safety-tool, which standardizes safety requirements and annotations into a machine-readable attribute syntax. It checks unsafe function declarations and calls to ensure that safety properties are without any omissions. The idea of simplifying the text of safety requirements is in line with the goals of the Rust for Linux Safety Standard RFC, both advocating for the establishment of a shared vocabulary and the use of consistent terminology to express safety requirements in daily development. In addition, the presentation demonstrated the RFC#3842 safety tags proposed to the Rust official team and community, suggesting that the standard library support safety tags and Clippy implement the tag check. It also shared the initial tagging results on the Rust for Linux and Asterinas codebase, as well as further plans.

zjp-CN avatar Dec 21 '25 06:12 zjp-CN