Skip to content

RFC: Safety Tags#3842

Open
zjp-CN wants to merge 52 commits intorust-lang:masterfrom
safer-rust:safety-tags
Open

RFC: Safety Tags#3842
zjp-CN wants to merge 52 commits intorust-lang:masterfrom
safer-rust:safety-tags

Conversation

@zjp-CN
Copy link

@zjp-CN zjp-CN commented Jul 31, 2025

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 zjp-CN requested a review from ia0 August 1, 2025 06:44
Co-authored-by: kennytm <kennytm@gmail.com>
@Alexendoo
Copy link
Member

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

@zjp-CN
Copy link
Author

zjp-CN commented Sep 7, 2025

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
Copy link
Author

zjp-CN commented Sep 16, 2025

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. @ 💬


#[safety::checked(
aligned = "alignment of ptr has be adjusted",
valid_ptr, initialized = "delegated to the caller"
Copy link
Author

@zjp-CN zjp-CN Sep 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(As an enhanced example from my comment in #3458 (comment) of RFC unsafe fields )

I'm wondering that there should be a specific #[safety::delegated] attribute the same as #[safety::checked], but more handy and concise to use:

#[safety::requires(
  // bad comments by mentioning things in release build, but that's what the current implementation is in libstd
  no_more_than_cap = "Caller must ensure the new_len <= cap (in release mode)",
  valid_T_all_in_length_range = "Please also make sure all T in 0..=new_len are valid, since this call can't ensure this!"
)]
unsafe fn set_len(&mut self, new_len: usize) {
  debug_assert!(new_len <= self.capacity());
  #[safety::delegated(
    no_more_than_cap, valid_T_all_in_length_range
  )]
  unsafe { self.len = new_len }
}

Tags in delegated are merged into checked, so both can coexist and work well.


What I mean handy is that we can omit definition texts in such context, because safety requirements are first introduced on unsafe field declaration or unsafe function being called, so it's verbose to duplicate them when without extra context attached:

struct Vec<T> {
  #[safety::requires(
    no_more_than_cap = "0 <= len <= cap",
    valid_T_all_in_length_range = "all elements in the Vec<T> between 0 and len are valid T"
  )]
  unsafe len: usize,
  ...
}

// The same safety requirements as on `unsafe len` field are rendered here
// because safety tags need support from rustdoc.
#[safety::requires(no_more_than_cap, valid_T_all_in_length_range)]
unsafe fn set_len(&mut self, new_len: usize) {
  debug_assert!(new_len <= self.capacity());
  #[safety::delegated(no_more_than_cap, valid_T_all_in_length_range)]
  unsafe { self.len = new_len }
}

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another reason to have #[safety::delegated] is that #[safety::requires] must incorporate all tags delegated from the fn body. Must means that a diagnostic happens if such one tag is missing.

checked won't have such delegation checks when listing delegated tags in requires.

Copy link
Author

@zjp-CN zjp-CN Sep 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Paste a totally semantic-unsafety-oriented example given in my comment on RFC unsafe fields:

struct Vec<T> {
  #[safety::requires(any { 
    good_on_read,
    hazard_on_mut(valid_T_all_in_length_range, no_more_than_cap)
  })]
  unsafe len: usize,
}

pub fn len(&self) -> usize {
    #[safety::checked(good_on_read)]
    unsafe { self.len }
}

#[safety::requires(hazard_on_mut)]
unsafe fn set_len(&mut self, new_len: usize) {
  #[safety::delegated(hazard_on_mut)]
  unsafe { self.len = new_len }
}

This is an improvement to #3842 (comment) as well.

(I don't really suggest supporting safety tags beyond visual unsafe operations too far though.)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you should at least add the unsafe back to the len field, I don't think it is supposed that #[safety::requires] can be applicable on things declared safe.

Copy link
Author

@zjp-CN zjp-CN Sep 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The snippet is more to clippy PR danger_not_accepted.

Safety tags for unsafe fields are already stated to be supported.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@zjp-CN

i mean your declaration of len is not an unsafe field at all, not whether about the attribute supports unsafe field or not.

struct Vec<T> {
  #[safety::requires(any { 
    good_on_read,
    hazard_on_mut(valid_T_all_in_length_range, no_more_than_cap)
  })]
  len: usize,
//^ NO UNSAFE IN SIGHT
}

Copy link
Author

@zjp-CN zjp-CN Sep 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

danger_not_accepted doesn't require unsafe.

But to avoid further unnecessary misinterpretation, I've just modified it the way you asked.

@TimTheBig
Copy link

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

@zjp-CN
Copy link
Author

zjp-CN commented Dec 21, 2025

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
Copy link
Author

zjp-CN commented Jan 31, 2026

We have developed a Web-based interface for auditing safety properties, featuring

  • Call graph visualization with tags
  • Overview of tag specifications and usage frequency
  • Interactive exogenous function inspection
  • Streamlined auditing panels (tag-derived docs/original docs/source code/MIR)

Current support extends to the Standard Library (core, alloc, and std) and Asterinas' ostd crate. Although the tag set is still preliminary, it effectively demonstrates the framework's visualization capabilities.

The application is hosted as a static webpage, and the repo is here.

@joshlf
Copy link
Contributor

joshlf commented Feb 1, 2026

Heads up that the unsafe fields RFC (which is referenced in this RFC) has now been merged.

cc @jswrenn

@zjp-CN
Copy link
Author

zjp-CN commented Mar 8, 2026

I just noticed a case where safety invariants in libcore changed in a semver-breaking manner as introduced in rust-lang/rust#141260 and stablized since 1.90: for read_volatile/write_volatile on latest stable vs an old version such as 1.88, the safety doc section differs

-src must be valid for reads.
+src must be either valid for reads, or it must point to memory outside of all Rust allocations 
+and reading from that memory must:
+* not trap, and
+* not cause any memory inside a Rust allocation to be modified.

Since maintaining invariant semver is a core motivation for safety tags, relying solely on release notes is insufficient; automated tool checks are essential to track such changes effectively.

-#[safety::requires(valid_ptr(src))]
+#[safety::requires(any(valid_ptr(src), not_rust_allocation(src)))]

@madsmtm
Copy link
Contributor

madsmtm commented Mar 9, 2026

I just noticed a case where safety invariants in libcore changed in a semver-breaking manner

To be clear, the std change was not semver-breaking, it relaxed the necessary preconditions. Users that did:

#[safety::checked(valid_ptr(ptr))]
unsafe { ptr.read_volatile() };

Would get no warning both before and after Rust 1.88.

@zjp-CN
Copy link
Author

zjp-CN commented Mar 9, 2026

To be clear, the std change was not semver-breaking, it relaxed the necessary preconditions.
Would get no warning both before and after Rust 1.88.

Thanks for the clarification! There's a contradiction in semver for this case:

  • "Adding a tag definition is a major change, because new tag is missing." as per the RFC; not_rust_allocation is an added tag, so it obeys the rule.
    • For a crate which relies on the newer std version and the not_rust_allocation tag of read_volatile, what happens if the crate is compiled with an old std/rustc version? The tag doesn't exist at all and tag check would warn againt it and fail due to the missing valid_ptr.
  • However, not_rust_allocation is also a subtag of any which means any one of the argument tags can satisfy the tag check, and in this sense it's a newly added argument/option instead of a tag, so the change is minor.
    • Note that any tag is a magic tag implemented in the prototype tool, but keeps a future possibility in the RFC.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

T-lang Relevant to the language team, which will review and decide on the RFC. T-libs-api Relevant to the library API team, which will review and decide on the RFC. T-rustdoc Relevant to rustdoc team, which will review and decide on the RFC.

Projects

None yet

Development

Successfully merging this pull request may close these issues.