From 5d9206057281f73d27a4c8ddd4bc018529542f77 Mon Sep 17 00:00:00 2001 From: zjp Date: Thu, 31 Jul 2025 14:27:23 +0000 Subject: [PATCH 01/41] RFC: safety-tags --- text/0000-safety-tags.md | 469 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 469 insertions(+) create mode 100644 text/0000-safety-tags.md diff --git a/text/0000-safety-tags.md b/text/0000-safety-tags.md new file mode 100644 index 00000000000..5eacfc35fa0 --- /dev/null +++ b/text/0000-safety-tags.md @@ -0,0 +1,469 @@ +- Feature Name: safety_tag +- Start Date: 2025-07-29 +- RFC PR: [rust-lang/rfcs#0000](https://github.com/rust-lang/rfcs/pull/0000) +- Rust Issue: [rust-lang/rust#0000](https://github.com/rust-lang/rust/issues/0000) + +# Summary +[summary]: #summary + +This RFC introduces a concise safety-comment convention for unsafe code in libstd-adjacent crates: +tag every unsafe function and call with `#[safety { SP1, SP2 }]`. + +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, but we expect Clippy and rust-analyzer to enforce tag checks +and provide first-class IDE support. + +[compiles]: https://play.rust-lang.org/?version=nightly&mode=debug&edition=2024&gist=056cbe626a7cc92a317e38e9f54cb1fb + +```rust +#![feature(custom_inner_attributes)] +#![clippy::safety(invariant::ValidPtr)] // 💡 + + +pub mod invariant { + #[clippy::safety::tag] + pub fn ValidPtr() {} +} + +#[clippy::safety { ValidPtr }] // 💡 +pub unsafe fn read(ptr: *const T) {} + +fn main() { + #[clippy::safety { ValidPtr }] // 💡 + unsafe { read(&()) }; +} +``` + +# Motivation +[motivation]: #motivation + +To avoid the misuse of unsafe code, Rust developers are encouraged to provide clear safety comments +for unsafe APIs. While these comments are generally human-readable, they can be ambiguous and +laborious to write. Even the current best practices in the Rust standard library are somewhat ad hoc +and informal. Moreover, safety comments are often repetitive and may be perceived as less important +than the code itself, which makes them error-prone and increases the risk that reviewers may +overlook inaccuracies or missing safety requirements. + +For instance, a severe problem may arise if the safety requirements of an API change over time: +downstream users may be unaware of such changes and thus be exposed to security risks. Therefore, we +propose to improve the current practice of writing safety comments by making them checkable through +a system of safety tags. These tags are designed to be: + +* Compatible with existing safety documentation: Safety tags should be expressive enough to + represent current safety comments, especially as rendered in today's rustdoc HTML pages. +* Usable by compiler tools for safety checking: If no safety tags are provided for an unsafe API, + lints should be emitted to remind developers to provide safety requirements. If a safety tag is + declared for an unsafe API but not discharged at a callsite, lints should be emitted to alert + developers about potentially overlooked safety requirements. + +# Guide-level explanation +[guide-level-explanation]: #guide-level-explanation + +## Syntax of Safety Tags + +Syntax of a safety tag is defined as follows: + +```text +SafetyTag -> `#` `[` `clippy::safety` `{` Tags `}` `]` + +Tags -> Tag (`;` Tag)* + +Tag -> ID (`,` ID)* (`:` LiteralString)? + +ID -> SingleIdent | SimplePath +``` + +Here are some tag examples: + +```rust +#[clippy::safety { SP }] +#[clippy::safety { SP1, SP2 }] + +#[clippy::safety { SP1: "reason" }] +#[clippy::safety { SP1: "reason"; SP2: "reason" }] + +#[clippy::safety { SP1, SP2: "shared reason for the two SPs" }] +#[clippy::safety { SP1, SP2: "shared reason for the two SPs"; SP3 }] +#[clippy::safety { SP3; SP1, SP2: "shared reason for the two SPs" }] +``` + +`#[clippy::safety]` is a [tool attribute] that you attach to an unsafe function (or to an expression +that performs unsafe calls). Take [`ptr::read`]: its safety comment lists three requirements, so we +create three corresponding tags on the function declaration and mark each one off at the call site. + +```rust +#[clippy::safety { ValidPtr, Aligned, Initialized }] // defsite +pub unsafe fn read(ptr: *const T) -> T { ... } + +#[clippy::safety { ValidPtr, Aligned, Initialized }] // callsite +unsafe { read(ptr) }; +``` + +We can also attach comments for a tag or a group of tags to clarify how safety requirements are met: + +```rust +#[clippy::safety { + InBounded, ValidNum: "`n` won't exceed isize::MAX here, so `p.add(n)` is fine"; + ValidPtr, Aligned, Initialized: "addr range p..p+n is property initialized" +}] +for _ in 0..n { + unsafe { + p = p.add(1); + c ^= p.read(); + } +} +``` + +[tool attribute]: https://doc.rust-lang.org/reference/attributes.html#tool-attributes +[`ptr::read`]: https://doc.rust-lang.org/std/ptr/fn.read.html + +Every safety tag declared on a function must appear in `#[clippy::safety { ... }]` together with an +optional reason; any omission triggers a warning-by-default diagnostic that lists the missing tags +and explains each one: + +```rust +unsafe { ptr::read(ptr) } +``` + +```rust +warning: `ValidPtr`, `Aligned`, `Initialized` tags are missing. Add them to `#[clippy::safety { }]`. + --> file.rs:xxx:xxx + | +LLL | unsafe { ptr::read(ptr) } + | ^^^^^^^^^^^^^^^^^^^^^^^^^ This unsafe call requires these safety tags. + | + = NOTE: See core::ptr::invariants::ValidPtr + = NOTE: See core::ptr::invariants::Aligned + = NOTE: See core::ptr::invariants::Initialized +``` + +The process of verifying whether a tag is absent is referred to as tag discharge. + +## Safety Tags as Ordinary Items + +Before tagging a function, we must declare them as ordinary items with `#[clippy::safety::tag]` such +as [uninhabited] types or plain functions: + +```rust +#[clippy::safety::tag] +enum ValidPtr {} + +#[clippy::safety::tag] +fn Aligned() {} +``` + +Tags live in their own [type namespace] carry item-level [scopes] and obey [visibility] rules, +keeping the system modular and collision-free. Since they are never referenced directly as real +items inside the safety-macro, importing them uses a dedicated syntax: + +```rust +#![clippy::safety { UseTree })] +``` + +[`UseTree`] follows the exact grammar of the `use` declaration. Some examples: + +```rust +// at the top of module: +#![clippy::safety { core::ptr::invariants::* }] +#![clippy::safety { core::ptr::invariants::ValidPtr }] +#![clippy::safety { core::ptr::invariants::{ValidPtr, Aligned} }] +``` + +That's to say: +* Tags declared or re-exported in the current module are automatically in scope: no import required. +* Tags from other modules must be brought in with the inner-tool attribute shown above. +* Tags are visible and available to downstream crates whenever their declaration paths are public. +* Attempting to import a tag from a private module is a **hard error**. +* Referencing a tag that has never been declared is also a **hard error**. + +[uninhabited]: https://doc.rust-lang.org/reference/glossary.html#uninhabited +[type namespace]: https://doc.rust-lang.org/reference/names/namespaces.html +[item scopes]: https://doc.rust-lang.org/reference/names/scopes.html#item-scopes +[visibility]: https://doc.rust-lang.org/reference/visibility-and-privacy.html +[`UseTree`]: https://doc.rust-lang.org/reference/items/use-declarations.html + +Tags are treated as items so rustdoc can render their documentation and hyperlink tag references. +And rust-analyzer can offer **full IDE support**: completion, go-to-definition/declaration, and +doc-hover. + +Tags constitute a public API; therefore, any alteration to their declaration or definition must be +evaluated against [Semantic Versioning][semver]. +* Adding a tag is a **minor** change. +* Removing a tag is a **major** change. + +To give dependent crates time to migrate, mark obsolete tag items with `#[deprecated]`. Clippy will +surface the deprecation warning whenever the tag is used. + +[semver]: https://doc.rust-lang.org/cargo/reference/semver.html + +# Reference-level explanation +[reference-level-explanation]: #reference-level-explanation + +## Unstable Features + +Currently, safety tags requires the following unstable features +* `#![feature(proc_macro_hygiene, stmt_expr_attributes)]` for tagging statements or expressions. +* `#![feature(custom_inner_attributes)]` for `#![clippy::safety {}]` imports + +Since the safety-tag mechanism is implemented primarily in Clippy and rust-analyzer, no additional +support is required from rustc. + +But We ask the libs team to adopt safety tags for all public `unsafe` APIs in libstd, along with +their call sites. To enable experimentation, a nightly-only library feature +`#![feature(safety_tags)]` should be introduced and remain unstable until the design is finalized. + +## Implementation in Clippy + +Procedure: + +1. Scan the crate for every item marked `#[clippy::safety::tag]`; cache the compiled tag metadata of + upstream dependencies under `target/` for later queries. +2. Validate every `#![clippy::safety { ... }]` import by a reachability analysis that ensures every + referenced tag is defined and accessible. +3. Verify that every unsafe call carries the required safety tags: + * Resolve the callee, collect its declared tags, then walk outward from the call site until the + function’s own signature confirms these tags are listed in its `#![clippy::safety { ... }]` + attribute. + * Tags are only discharged inside or onto an `unsafe fn`; it's an error to tag a safe function. + * If an unsafe call lacks any required tag, emit a diagnostic whose severity (warning or error) + is governed by the configured Clippy lint level. + +Libraries in `rust-lang/rust` must enforce tag checking as a hard error, guaranteeing that every tag +definition and discharge is strictly valid. + +## Implementation in Rust Analyzer + +Safety-tag analysis requirements: + +* Harvest every item marked `#[clippy::safety::tag]`, including those pulled in from dependencies. +* Offer path completion for `#![clippy::safety { ... }]`. +* Offer tag-name completion for `#[clippy::safety]` on unsafe functions, let-statements, or expressions. +* Validate all tags inside `#[clippy::safety]`, and support “go-to-definition” plus inline documentation hover. + +# Drawbacks +[drawbacks]: #drawbacks + +Even though safety tags are machine-readable, their correctness still hinges on human review: +developers can silence Clippy by discharging tags without verifying the underlying obligations. + +# Rationale and alternatives +[rationale-and-alternatives]: #rationale-and-alternatives + +## Alternatives from IRLO + +There are alternative discussion or Pre-RFCs on IRLO: + +* 2023-10: [Ability to call unsafe functions without curly brackets](https://internals.rust-lang.org/t/ability-to-call-unsafe-functions-without-curly-brackets/19635/22) + * This is a discussion about make single unsafe call simpler, so the idea evolved into tczajka's Pre-RFC. + * But the idea and syntax from Scottmcm's comments are very enlightening to our RFC. +* 2024-10: [Detect and Fix Overscope unsafe Block](https://internals.rust-lang.org/t/detect-and-fix-overscope-unsafe-block/21660/19) + * The OP is about safe code scope in big unsafe block, which is not discussed in our RFC. + * But scottmcm's comments are good inspiration for our RFC. +* 2024-12: [Pre-RFC: Unsafe reasons](https://internals.rust-lang.org/t/pre-rfc-unsafe-reasons/22093) proposed by chrefr + * This is a good improvement on abstracting safety comments into a single, machine-readable and + checkable identifier. However, it doesn't specify arguments and lacks more fine-grained string + interpolation for detailing unsafe reasons. + * It also requests big changes on language and compiler change, while safety tags in our RFC is lightweight +* 2025-02: [Pre-RFC: Single function call `unsafe`](https://internals.rust-lang.org/t/pre-rfc-single-function-call-unsafe/22343) proposed by tczajka + * The practice of using a single unsafe call is good, but the postfix `.unsafe` requires more + compiler support and does not offer suggestions for improving safe comments. + * Our RFC, however, supports annotating safety tags on any expression, including single calls. +* 2025-05: [Pre-RFC: Granular Unsafe Blocks - A more explicit and auditable approach](https://internals.rust-lang.org/t/pre-rfc-granular-unsafe-blocks-a-more-explicit-and-auditable-approach/23022) proposed by Redlintles + * The safety categories suggested are overly broad. In contrast, the safety properties outlined in + our RFC are more granular and semantics-specific. +* 2025-07: [Unsafe assertion invariants](https://internals.rust-lang.org/t/unsafe-assertion-invariants/23206) + * It’s a good idea to embed safety requirements into doc comments, which aligns with one of the + goals in our RFC. +* 2025-07: [Pre-RFC: Safety Property System](https://internals.rust-lang.org/t/pre-rfc-safety-property-system/23252) proposed by vague + * It's a draft of our current proposal, but more focused on custom linter's design. + * The critical parts have already been refined from Clippy’s perspective. + +## Alternatives from Rust for Linux + +More importantly, our proposal is a big improvement to these proposals, which Rust for Linux care +more about: +* 2024-09: [Rust Safety Standard: Increasing the Correctness of unsafe Code][Rust Safety Standard] + proposed by Benno Lossin + * These slides outline the motivations and objectives of safety-documentation standardization — + exactly what our proposal aims to deliver. + * They omit implementation details; nevertheless, Predrag (see next line) and we remain faithful + to their intent. +* 2024-10: [Automated checking of unsafe code requirements](https://hackmd.io/@predrag/ByVBjIWlyx) + proposed by Predrag + * Predrag’s proposal focuses on structured safety comments, entity references, requirement + discharges, and the careful handling of soundness hazards when safety rules evolve. Most are + compatible with our proposal. + * The principal divergence is syntactic: Predrag embeds the rules in doc- and line-comments, which + remain highly readable for both humans and tools, yet are discarded early by the compiler. This + makes retrieving a rule for a specific expression far harder than with + [`stmt_expr_attributes`](https://github.com/rust-lang/rust/issues/15701). + +Originally, we only focus on libstd's common safety propeties ([paper]), but noticed the RustWeek +[meeting note] in zulipchat. Thus [tag-std#3](https://github.com/Artisan-Lab/tag-std/issues/3) is +opened to support Rust for Linux on safety standard. + +[meeting note]: https://hackmd.io/@qnR1-HVLRx-dekU5dvtvkw/SyUuR6SZgx +[Rust Safety Standard]: https://kangrejos.com/2024/Rust%20Safety%20Standard.pdf +[paper]: https://arxiv.org/abs/2504.21312 + +# Prior art +[prior-art]: #prior-art + +Currently, there are efforts on introducing contracts and formal verification into Rust: +* [contracts](https://rust-lang.github.io/rust-project-goals/2024h2/Contracts-and-invariants.html): + the lang experiment has been implemented since + [rust#128044](https://github.com/rust-lang/rust/issues/128044). +* [verify-rust-std] pursues applying formal verification tools to libstd. Also see Rust Foundation + [announcement][vrs#ann], project goals during [2024h2] and [2025h1]. + +While safety tags are less formally verified, intended to be a check list on safety requirements. + +[verify-rust-std]: https://github.com/model-checking/verify-rust-std +[vrs#ann]: https://foundation.rust-lang.org/news/rust-foundation-collaborates-with-aws-initiative-to-verify-rust-standard-libraries/ +[2024h2]: https://rust-lang.github.io/rust-project-goals/2024h2/std-verification.html +[2025h1]: https://rust-lang.github.io/rust-project-goals/2025h1/std-contracts.html + +# Unresolved questions +[unresolved-questions]: #unresolved-questions + +## Tagging on Unsafe Traits and Impls + +We can extend safety definitions to unsafe traits and require discharges in unsafe trait impls. + +Crates with heavy unsafe-trait usage will likely need. We’d welcome more minds on this. + +## Tagging on Datastructures + +We believe safety obligations are almost always imposed by unsafe functions, so tagging a struct, +enum, or union is neither needed nor permitted. + +## Tagging on Unsafe Fields + +Unsafe fields are already declared and accessed with the `unsafe` keyword, often accompanied by +safety comments. We could extend safety tags to cover unsafe fields as well, both in their +definitions and at every access point they are discharged. + +[unsafe field]: https://github.com/rust-lang/rfcs/pull/3458 + +# Future possibilities +[future-possibilities]: #future-possibilities + +## Generate Safety Docs from Tags + +We can take structured safety comments one step further by turning the explanatory prose into +explicit tag reasons. + +For `ptr::read`, the existing comments are replaced with safety tags: + +```rust +/// * `src` must be [valid] for reads. +/// * `src` must be properly aligned. Use [`read_unaligned`] if this is not the case. +/// * `src` must point to a properly initialized value of type `T`. +pub const unsafe fn read(src: *const T) -> T { ... } +``` + +```rust +#[safety { + ValidPtr: "`src` must be [valid] for reads"; + Aligned: "`src` must be properly aligned. Use [`read_unaligned`] if this is not the case"; + Initialized: "`src` must point to a properly initialized value of type `T`" +}] +pub const unsafe fn read(src: *const T) -> T { ... } +``` + +`#[safety]` becomes a procedural macro that expands to both `#[doc]` attributes and the +`#[clippy::safety]` attribute. + +```rust +/// # Safety +/// +/// - ValidPtr: `src` must be [valid] for reads +/// - Aligned: `src` must be properly aligned. Use [`read_unaligned`] if this is not the case +/// - Initialized: `src` must point to a properly initialized value of type `T` +#[clippy::safety { ValidPtr, Aligned, Initialized }] +pub const unsafe fn read(src: *const T) -> T { ... } +``` + +## `any { Option1, Option2 }` Tags + +Sometimes it’s useful to declare a set of safety tags on an unsafe function while discharging only +one of them. + +For instance, `ptr::read` could expose the group `any { DropCheck, CopyType }` and then discharge +either `DropCheck` or `CopyType` at the call site, depending on the concrete type `T`. + +Another instance is `<*const T>::as_ref`, whose safety doc states that the caller must guarantee +“the pointer is either null or safely convertible to a reference.” This can be expressed as +`#[clippy::safety { any { Null, ValidPtr2Ref } }]`, allowing the caller to discharge whichever tag +applies. + +## Entity References and Code Review Enhancement + +To cut boilerplate or link related code locations, we introduce `#[clippy::safety::ref(...)]`, which +establishes a two-way reference: + +An example of this is [IntoIter::try_fold][vec_deque] of VecDeque, using `#[ref]` for short: + +[vec_deque]: https://github.com/rust-lang/rust/blob/ebd8557637b33cc09b6ee8273f3154d5d3af6a15/library/alloc/src/collections/vec_deque/into_iter.rs#L104 + +```rust +fn try_fold(&mut self, mut init: B, mut f: F) -> R + impl<'a, T, A: Allocator> Drop for Guard<'a, T, A> { + #[ref(try_fold)] // 💡 ptr::read below relies on this drop impl + fn drop(&mut self) { ... } + } + ... + + init = head.iter().map(|elem| { + guard.consumed += 1; + + #[ref(try_fold)] // 💡 + #[safety { + ValidPtr, Aligned, Initialized, + DropCheck: "Because we incremented `guard.consumed`, the deque \ + effectively forgot the element, so we can take ownership." + }] + unsafe { ptr::read(elem) } + }) + .try_fold(init, &mut f)?; + + tail.iter().map(|elem| { + guard.consumed += 1; + + #[ref(try_fold)] // 💡 No longer to write SAFETY: Same as above. + unsafe { ptr::read(elem) } + }) + .try_fold(init, &mut f) +} + +fn try_rfold(&mut self, mut init: B, mut f: F) -> R { + impl<'a, T, A: Allocator> Drop for Guard<'a, T, A> { + #[ref(try_fold)] // 💡 + fn drop(&mut self) { ... } + } + ... + + init = tail.iter().map(|elem| { + guard.consumed += 1; + + #[ref(try_fold)] // 💡 No longer to write SAFETY: See `try_fold`'s safety comment. + unsafe { ptr::read(elem) } + }) + .try_rfold(init, &mut f)?; + + head.iter().map(|elem| { + guard.consumed += 1; + + #[ref(try_fold)] // 💡 No longer to write SAFETY: Same as above. + unsafe { ptr::read(elem) } + }) + .try_rfold(init, &mut f) +} +``` + +These `#[ref]` annotations act as cross-references that nudge developers to inspect every linked +site. When either end or the code around it changes, reviewers are instantly aware of all affected +locations and can verify that every referenced safety requirement is still satisfied. + From 7ccafe10b786fbc9fc11845f7c3c8d5dd17cd338 Mon Sep 17 00:00:00 2001 From: zjp Date: Thu, 31 Jul 2025 14:38:31 +0000 Subject: [PATCH 02/41] safety-tags: styling --- text/0000-safety-tags.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/text/0000-safety-tags.md b/text/0000-safety-tags.md index 5eacfc35fa0..44720fce5b0 100644 --- a/text/0000-safety-tags.md +++ b/text/0000-safety-tags.md @@ -21,7 +21,6 @@ and provide first-class IDE support. #![feature(custom_inner_attributes)] #![clippy::safety(invariant::ValidPtr)] // 💡 - pub mod invariant { #[clippy::safety::tag] pub fn ValidPtr() {} @@ -239,14 +238,16 @@ Safety-tag analysis requirements: * Harvest every item marked `#[clippy::safety::tag]`, including those pulled in from dependencies. * Offer path completion for `#![clippy::safety { ... }]`. -* Offer tag-name completion for `#[clippy::safety]` on unsafe functions, let-statements, or expressions. -* Validate all tags inside `#[clippy::safety]`, and support “go-to-definition” plus inline documentation hover. +* Offer tag-name completion for `#[clippy::safety]` on unsafe functions, let-statements, or + expressions. +* Validate all tags inside `#[clippy::safety]`, and support “go-to-definition” plus inline + documentation hover. # Drawbacks [drawbacks]: #drawbacks Even though safety tags are machine-readable, their correctness still hinges on human review: -developers can silence Clippy by discharging tags without verifying the underlying obligations. +developers can silence Clippy by discharging tags without verifying underlying safety requirements. # Rationale and alternatives [rationale-and-alternatives]: #rationale-and-alternatives @@ -318,7 +319,7 @@ Currently, there are efforts on introducing contracts and formal verification in * [verify-rust-std] pursues applying formal verification tools to libstd. Also see Rust Foundation [announcement][vrs#ann], project goals during [2024h2] and [2025h1]. -While safety tags are less formally verified, intended to be a check list on safety requirements. +While safety tags are less formally verified and intended to be a check list on safety requirements. [verify-rust-std]: https://github.com/model-checking/verify-rust-std [vrs#ann]: https://foundation.rust-lang.org/news/rust-foundation-collaborates-with-aws-initiative-to-verify-rust-standard-libraries/ @@ -336,7 +337,7 @@ Crates with heavy unsafe-trait usage will likely need. We’d welcome more minds ## Tagging on Datastructures -We believe safety obligations are almost always imposed by unsafe functions, so tagging a struct, +We believe safety requirements are almost always imposed by unsafe functions, so tagging a struct, enum, or union is neither needed nor permitted. ## Tagging on Unsafe Fields From c0495e3cb4d93d5b0cbe72a31edc38c217d05723 Mon Sep 17 00:00:00 2001 From: zjp Date: Thu, 31 Jul 2025 15:06:26 +0000 Subject: [PATCH 03/41] safety-tags: add previous zulipchat link on Safety Property System --- text/0000-safety-tags.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/text/0000-safety-tags.md b/text/0000-safety-tags.md index 44720fce5b0..d626db77899 100644 --- a/text/0000-safety-tags.md +++ b/text/0000-safety-tags.md @@ -278,8 +278,9 @@ There are alternative discussion or Pre-RFCs on IRLO: * It’s a good idea to embed safety requirements into doc comments, which aligns with one of the goals in our RFC. * 2025-07: [Pre-RFC: Safety Property System](https://internals.rust-lang.org/t/pre-rfc-safety-property-system/23252) proposed by vague - * It's a draft of our current proposal, but more focused on custom linter's design. - * The critical parts have already been refined from Clippy’s perspective. + * It's a draft of our current proposal, but more focused on custom linter's design. Also see + [this thread in opsem channel](https://rust-lang.zulipchat.com/#narrow/channel/136281-t-opsem/topic/Safety.20Property.20System/with/530679491). + * The critical parts have already been refined from Clippy’s perspective in current proposal. ## Alternatives from Rust for Linux From 2004fd43c3a5f8b53fee7dd45f6758045e9d9bfa Mon Sep 17 00:00:00 2001 From: zjp Date: Thu, 31 Jul 2025 15:23:44 +0000 Subject: [PATCH 04/41] safety-tags: add tag discharge of unsafe encapsulation --- text/0000-safety-tags.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/text/0000-safety-tags.md b/text/0000-safety-tags.md index d626db77899..4dc1ec79def 100644 --- a/text/0000-safety-tags.md +++ b/text/0000-safety-tags.md @@ -140,6 +140,16 @@ LLL | unsafe { ptr::read(ptr) } The process of verifying whether a tag is absent is referred to as tag discharge. +Note that it's allowed to discharge tags of unsafe callees onto the unsafe caller for unsafe +encapsulation: + +```rust +#[clippy::safety { ValidPtr, Aligned, Initialized }] // ✅ +unsafe fn constructor() -> T { + unsafe { read(...) } +} +``` + ## Safety Tags as Ordinary Items Before tagging a function, we must declare them as ordinary items with `#[clippy::safety::tag]` such @@ -209,7 +219,7 @@ Currently, safety tags requires the following unstable features Since the safety-tag mechanism is implemented primarily in Clippy and rust-analyzer, no additional support is required from rustc. -But We ask the libs team to adopt safety tags for all public `unsafe` APIs in libstd, along with +But we ask the libs team to adopt safety tags for all public `unsafe` APIs in libstd, along with their call sites. To enable experimentation, a nightly-only library feature `#![feature(safety_tags)]` should be introduced and remain unstable until the design is finalized. From 009041965e27376dd570dfdb57883a21ff172a15 Mon Sep 17 00:00:00 2001 From: zjp Date: Thu, 31 Jul 2025 15:27:35 +0000 Subject: [PATCH 05/41] safety-tags: typo --- text/0000-safety-tags.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/text/0000-safety-tags.md b/text/0000-safety-tags.md index 4dc1ec79def..3733573643a 100644 --- a/text/0000-safety-tags.md +++ b/text/0000-safety-tags.md @@ -138,10 +138,10 @@ LLL | unsafe { ptr::read(ptr) } = NOTE: See core::ptr::invariants::Initialized ``` -The process of verifying whether a tag is absent is referred to as tag discharge. +The process of verifying whether a tag is present is referred to as tag discharge. Note that it's allowed to discharge tags of unsafe callees onto the unsafe caller for unsafe -encapsulation: +delegation or propogation: ```rust #[clippy::safety { ValidPtr, Aligned, Initialized }] // ✅ From 4b6bdb236c1326b914250d5339539f735c64654f Mon Sep 17 00:00:00 2001 From: zjp Date: Thu, 31 Jul 2025 15:57:34 +0000 Subject: [PATCH 06/41] safety-tags: styling --- text/0000-safety-tags.md | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/text/0000-safety-tags.md b/text/0000-safety-tags.md index 3733573643a..1a3c8840411 100644 --- a/text/0000-safety-tags.md +++ b/text/0000-safety-tags.md @@ -12,7 +12,7 @@ tag every unsafe function and call with `#[safety { SP1, SP2 }]`. 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, but we expect Clippy and rust-analyzer to enforce tag checks +The following snippet [compiles] today, but we expect Clippy and Rust-Analyzer to enforce tag checks and provide first-class IDE support. [compiles]: https://play.rust-lang.org/?version=nightly&mode=debug&edition=2024&gist=056cbe626a7cc92a317e38e9f54cb1fb @@ -105,7 +105,7 @@ We can also attach comments for a tag or a group of tags to clarify how safety r ```rust #[clippy::safety { InBounded, ValidNum: "`n` won't exceed isize::MAX here, so `p.add(n)` is fine"; - ValidPtr, Aligned, Initialized: "addr range p..p+n is property initialized" + ValidPtr, Aligned, Initialized: "addr range p..p+n is properly initialized from aligned memory" }] for _ in 0..n { unsafe { @@ -189,12 +189,12 @@ That's to say: [uninhabited]: https://doc.rust-lang.org/reference/glossary.html#uninhabited [type namespace]: https://doc.rust-lang.org/reference/names/namespaces.html -[item scopes]: https://doc.rust-lang.org/reference/names/scopes.html#item-scopes +[scopes]: https://doc.rust-lang.org/reference/names/scopes.html#item-scopes [visibility]: https://doc.rust-lang.org/reference/visibility-and-privacy.html [`UseTree`]: https://doc.rust-lang.org/reference/items/use-declarations.html Tags are treated as items so rustdoc can render their documentation and hyperlink tag references. -And rust-analyzer can offer **full IDE support**: completion, go-to-definition/declaration, and +And Rust-Analyzer can offer **full IDE support**: completion, go-to-definition/declaration, and doc-hover. Tags constitute a public API; therefore, any alteration to their declaration or definition must be @@ -216,7 +216,7 @@ Currently, safety tags requires the following unstable features * `#![feature(proc_macro_hygiene, stmt_expr_attributes)]` for tagging statements or expressions. * `#![feature(custom_inner_attributes)]` for `#![clippy::safety {}]` imports -Since the safety-tag mechanism is implemented primarily in Clippy and rust-analyzer, no additional +Since the safety-tag mechanism is implemented primarily in Clippy and Rust-Analyzer, no additional support is required from rustc. But we ask the libs team to adopt safety tags for all public `unsafe` APIs in libstd, along with @@ -308,9 +308,9 @@ more about: discharges, and the careful handling of soundness hazards when safety rules evolve. Most are compatible with our proposal. * The principal divergence is syntactic: Predrag embeds the rules in doc- and line-comments, which - remain highly readable for both humans and tools, yet are discarded early by the compiler. This - makes retrieving a rule for a specific expression far harder than with - [`stmt_expr_attributes`](https://github.com/rust-lang/rust/issues/15701). + remain highly readable for humans, but not so much for tools, because line-comments are + discarded early by the compiler. This makes retrieving a rule for a specific expression far + harder than with [`stmt_expr_attributes`](https://github.com/rust-lang/rust/issues/15701). Originally, we only focus on libstd's common safety propeties ([paper]), but noticed the RustWeek [meeting note] in zulipchat. Thus [tag-std#3](https://github.com/Artisan-Lab/tag-std/issues/3) is @@ -403,18 +403,18 @@ pub const unsafe fn read(src: *const T) -> T { ... } Sometimes it’s useful to declare a set of safety tags on an unsafe function while discharging only one of them. -For instance, `ptr::read` could expose the group `any { DropCheck, CopyType }` and then discharge -either `DropCheck` or `CopyType` at the call site, depending on the concrete type `T`. +For instance, `ptr::read` could expose the grouped tag `any { DropCheck, CopyType }` and then +discharge either `DropCheck` or `CopyType` at the call site, depending on the concrete type `T`. Another instance is `<*const T>::as_ref`, whose safety doc states that the caller must guarantee -“the pointer is either null or safely convertible to a reference.” This can be expressed as +“the pointer is either null or safely convertible to a reference”. This can be expressed as `#[clippy::safety { any { Null, ValidPtr2Ref } }]`, allowing the caller to discharge whichever tag applies. ## Entity References and Code Review Enhancement -To cut boilerplate or link related code locations, we introduce `#[clippy::safety::ref(...)]`, which -establishes a two-way reference: +To cut boilerplate or link related code locations, we introduce `#[clippy::safety::ref(...)]` which +establishes a two-way reference. An example of this is [IntoIter::try_fold][vec_deque] of VecDeque, using `#[ref]` for short: From 567b998eda10b067fe81b8b6b83c22fa93f2cd4a Mon Sep 17 00:00:00 2001 From: zjp Date: Fri, 1 Aug 2025 01:22:34 +0000 Subject: [PATCH 07/41] safety-tags: fix link --- text/0000-safety-tags.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/text/0000-safety-tags.md b/text/0000-safety-tags.md index 1a3c8840411..49390c793b2 100644 --- a/text/0000-safety-tags.md +++ b/text/0000-safety-tags.md @@ -344,7 +344,7 @@ While safety tags are less formally verified and intended to be a check list on We can extend safety definitions to unsafe traits and require discharges in unsafe trait impls. -Crates with heavy unsafe-trait usage will likely need. We’d welcome more minds on this. +Crates with heavy unsafe-trait usage likely needs the extension. We’d welcome more minds on this. ## Tagging on Datastructures @@ -353,11 +353,11 @@ enum, or union is neither needed nor permitted. ## Tagging on Unsafe Fields -Unsafe fields are already declared and accessed with the `unsafe` keyword, often accompanied by -safety comments. We could extend safety tags to cover unsafe fields as well, both in their -definitions and at every access point they are discharged. +[Unsafe fields] are declared and accessed with the `unsafe` keyword, often accompanied by safety +comments. We could extend safety tags to cover unsafe fields as well, both in their definitions and +at every access point they are discharged. -[unsafe field]: https://github.com/rust-lang/rfcs/pull/3458 +[Unsafe fields]: https://github.com/rust-lang/rfcs/pull/3458 # Future possibilities [future-possibilities]: #future-possibilities From e9ec420d024f0e34709bc600d916ab5a58cacf90 Mon Sep 17 00:00:00 2001 From: zjp Date: Fri, 1 Aug 2025 01:24:43 +0000 Subject: [PATCH 08/41] safety-tags: clarify semver rules --- text/0000-safety-tags.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/text/0000-safety-tags.md b/text/0000-safety-tags.md index 49390c793b2..bb24a65706b 100644 --- a/text/0000-safety-tags.md +++ b/text/0000-safety-tags.md @@ -199,11 +199,11 @@ doc-hover. Tags constitute a public API; therefore, any alteration to their declaration or definition must be evaluated against [Semantic Versioning][semver]. -* Adding a tag is a **minor** change. -* Removing a tag is a **major** change. +* Adding a tag declaration or definition is a **minor** change. +* Removing a tag declaration or definition is a **major** change. To give dependent crates time to migrate, mark obsolete tag items with `#[deprecated]`. Clippy will -surface the deprecation warning whenever the tag is used. +surface the deprecation warning whenever the tag is used w.r.t definitions and discharges. [semver]: https://doc.rust-lang.org/cargo/reference/semver.html From 3da20c9869c10a19b7a667bb18cbe5dd3f04cac4 Mon Sep 17 00:00:00 2001 From: zjp Date: Fri, 1 Aug 2025 01:26:55 +0000 Subject: [PATCH 09/41] safety-tags: #[ref] needs clippy reporting --- text/0000-safety-tags.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/text/0000-safety-tags.md b/text/0000-safety-tags.md index bb24a65706b..82f353a6905 100644 --- a/text/0000-safety-tags.md +++ b/text/0000-safety-tags.md @@ -477,5 +477,6 @@ fn try_rfold(&mut self, mut init: B, mut f: F) -> R { These `#[ref]` annotations act as cross-references that nudge developers to inspect every linked site. When either end or the code around it changes, reviewers are instantly aware of all affected -locations and can verify that every referenced safety requirement is still satisfied. +locations that Clippy reports and thus can assess if every referenced safety requirement is still +satisfied. From 719342e8152c640ab186e19e4db8ef82b340a3fa Mon Sep 17 00:00:00 2001 From: zjp Date: Fri, 1 Aug 2025 01:36:45 +0000 Subject: [PATCH 10/41] safety-tags: #![clippy::safety] => #![clippy::safety::use] --- text/0000-safety-tags.md | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/text/0000-safety-tags.md b/text/0000-safety-tags.md index 82f353a6905..2da7a3e0d2e 100644 --- a/text/0000-safety-tags.md +++ b/text/0000-safety-tags.md @@ -19,7 +19,7 @@ and provide first-class IDE support. ```rust #![feature(custom_inner_attributes)] -#![clippy::safety(invariant::ValidPtr)] // 💡 +#![clippy::safety::use(invariant::*)] // 💡 pub mod invariant { #[clippy::safety::tag] @@ -165,19 +165,22 @@ fn Aligned() {} Tags live in their own [type namespace] carry item-level [scopes] and obey [visibility] rules, keeping the system modular and collision-free. Since they are never referenced directly as real -items inside the safety-macro, importing them uses a dedicated syntax: +items, we propose importing them uses a dedicated syntax: inner-styled or outer-styled +`clippy::safety::use` tool attribute on modules: ```rust -#![clippy::safety { UseTree })] +#![clippy::safety::use { UseTree })] // {} signifies a delimiter here, thus () also works ``` [`UseTree`] follows the exact grammar of the `use` declaration. Some examples: ```rust -// at the top of module: -#![clippy::safety { core::ptr::invariants::* }] -#![clippy::safety { core::ptr::invariants::ValidPtr }] -#![clippy::safety { core::ptr::invariants::{ValidPtr, Aligned} }] +#[clippy::safety::use { core::ptr::invariants::* }] +mod foo; + +mod bar { + #![clippy::safety::use { core::ptr::invariants::{ValidPtr, Aligned} }] +} ``` That's to say: @@ -214,7 +217,7 @@ surface the deprecation warning whenever the tag is used w.r.t definitions and d Currently, safety tags requires the following unstable features * `#![feature(proc_macro_hygiene, stmt_expr_attributes)]` for tagging statements or expressions. -* `#![feature(custom_inner_attributes)]` for `#![clippy::safety {}]` imports +* `#![feature(custom_inner_attributes)]` for `#![clippy::safety::use(...)]` imports Since the safety-tag mechanism is implemented primarily in Clippy and Rust-Analyzer, no additional support is required from rustc. @@ -233,7 +236,7 @@ Procedure: referenced tag is defined and accessible. 3. Verify that every unsafe call carries the required safety tags: * Resolve the callee, collect its declared tags, then walk outward from the call site until the - function’s own signature confirms these tags are listed in its `#![clippy::safety { ... }]` + function’s own signature confirms these tags are listed in its `#![clippy::safety::use(...)]` attribute. * Tags are only discharged inside or onto an `unsafe fn`; it's an error to tag a safe function. * If an unsafe call lacks any required tag, emit a diagnostic whose severity (warning or error) @@ -246,11 +249,11 @@ definition and discharge is strictly valid. Safety-tag analysis requirements: -* Harvest every item marked `#[clippy::safety::tag]`, including those pulled in from dependencies. -* Offer path completion for `#![clippy::safety { ... }]`. -* Offer tag-name completion for `#[clippy::safety]` on unsafe functions, let-statements, or - expressions. -* Validate all tags inside `#[clippy::safety]`, and support “go-to-definition” plus inline +* Harvest every item marked `#[clippy::safety::tag]`, including those pulled in from dependencies. +* Offer tag path completion for `#![clippy::safety::use(...)]`. +* Offer tag name completion for `#[clippy::safety { ... }]` on unsafe functions, let-statements, or + expressions. +* Validate all tags inside `#[clippy::safety { ... }]`, and support “go-to-definition” plus inline documentation hover. # Drawbacks @@ -299,7 +302,7 @@ more about: * 2024-09: [Rust Safety Standard: Increasing the Correctness of unsafe Code][Rust Safety Standard] proposed by Benno Lossin * These slides outline the motivations and objectives of safety-documentation standardization — - exactly what our proposal aims to deliver. + exactly what our proposal aims to deliver. * They omit implementation details; nevertheless, Predrag (see next line) and we remain faithful to their intent. * 2024-10: [Automated checking of unsafe code requirements](https://hackmd.io/@predrag/ByVBjIWlyx) From 421ea8d49a73a385e4fc57246d9c18e0bd285d3a Mon Sep 17 00:00:00 2001 From: zjp Date: Fri, 1 Aug 2025 01:43:30 +0000 Subject: [PATCH 11/41] safety-tags: fix tag in loop snippet --- text/0000-safety-tags.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/text/0000-safety-tags.md b/text/0000-safety-tags.md index 2da7a3e0d2e..9a87934fe1f 100644 --- a/text/0000-safety-tags.md +++ b/text/0000-safety-tags.md @@ -104,13 +104,13 @@ We can also attach comments for a tag or a group of tags to clarify how safety r ```rust #[clippy::safety { - InBounded, ValidNum: "`n` won't exceed isize::MAX here, so `p.add(n)` is fine"; ValidPtr, Aligned, Initialized: "addr range p..p+n is properly initialized from aligned memory" + InBounded, ValidNum: "`n` won't exceed isize::MAX here, so `p.add(n)` is fine"; }] for _ in 0..n { unsafe { - p = p.add(1); c ^= p.read(); + p = p.add(1); } } ``` From a0294a85a0bd1aa32ae182620407aa97926743f5 Mon Sep 17 00:00:00 2001 From: zjp Date: Fri, 1 Aug 2025 01:47:51 +0000 Subject: [PATCH 12/41] safety-tags: clippy::safety::use => clippy::safety::r#use MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit error: expected identifier, found keyword `use` --> src/main.rs:2:20 | 2 | #![clippy::safety::use(invariant::ValidPtr)] // 💡 | ^^^ expected identifier, found keyword --- text/0000-safety-tags.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/text/0000-safety-tags.md b/text/0000-safety-tags.md index 9a87934fe1f..4709e14cf42 100644 --- a/text/0000-safety-tags.md +++ b/text/0000-safety-tags.md @@ -15,11 +15,11 @@ requirement into a single, check-off reminder. The following snippet [compiles] today, but we expect Clippy and Rust-Analyzer to enforce tag checks and provide first-class IDE support. -[compiles]: https://play.rust-lang.org/?version=nightly&mode=debug&edition=2024&gist=056cbe626a7cc92a317e38e9f54cb1fb +[compiles]: https://play.rust-lang.org/?version=nightly&mode=debug&edition=2024&gist=2f49a8b255b8c066ffd5e3157a70b821 ```rust #![feature(custom_inner_attributes)] -#![clippy::safety::use(invariant::*)] // 💡 +#![clippy::safety::r#use(invariant::*)] // 💡 pub mod invariant { #[clippy::safety::tag] @@ -166,20 +166,20 @@ fn Aligned() {} Tags live in their own [type namespace] carry item-level [scopes] and obey [visibility] rules, keeping the system modular and collision-free. Since they are never referenced directly as real items, we propose importing them uses a dedicated syntax: inner-styled or outer-styled -`clippy::safety::use` tool attribute on modules: +`clippy::safety::r#use` tool attribute on modules: ```rust -#![clippy::safety::use { UseTree })] // {} signifies a delimiter here, thus () also works +#![clippy::safety::r#use { UseTree })] // {} signifies a delimiter here, thus () also works ``` [`UseTree`] follows the exact grammar of the `use` declaration. Some examples: ```rust -#[clippy::safety::use { core::ptr::invariants::* }] +#[clippy::safety::r#use { core::ptr::invariants::* }] mod foo; mod bar { - #![clippy::safety::use { core::ptr::invariants::{ValidPtr, Aligned} }] + #![clippy::safety::r#use { core::ptr::invariants::{ValidPtr, Aligned} }] } ``` @@ -217,7 +217,7 @@ surface the deprecation warning whenever the tag is used w.r.t definitions and d Currently, safety tags requires the following unstable features * `#![feature(proc_macro_hygiene, stmt_expr_attributes)]` for tagging statements or expressions. -* `#![feature(custom_inner_attributes)]` for `#![clippy::safety::use(...)]` imports +* `#![feature(custom_inner_attributes)]` for `#![clippy::safety::r#use(...)]` imports Since the safety-tag mechanism is implemented primarily in Clippy and Rust-Analyzer, no additional support is required from rustc. @@ -236,7 +236,7 @@ Procedure: referenced tag is defined and accessible. 3. Verify that every unsafe call carries the required safety tags: * Resolve the callee, collect its declared tags, then walk outward from the call site until the - function’s own signature confirms these tags are listed in its `#![clippy::safety::use(...)]` + function’s own signature confirms these tags are listed in its `#![clippy::safety::r#use(...)]` attribute. * Tags are only discharged inside or onto an `unsafe fn`; it's an error to tag a safe function. * If an unsafe call lacks any required tag, emit a diagnostic whose severity (warning or error) @@ -250,7 +250,7 @@ definition and discharge is strictly valid. Safety-tag analysis requirements: * Harvest every item marked `#[clippy::safety::tag]`, including those pulled in from dependencies. -* Offer tag path completion for `#![clippy::safety::use(...)]`. +* Offer tag path completion for `#![clippy::safety::r#use(...)]`. * Offer tag name completion for `#[clippy::safety { ... }]` on unsafe functions, let-statements, or expressions. * Validate all tags inside `#[clippy::safety { ... }]`, and support “go-to-definition” plus inline From 39dec42194ad1ad8b0e2ff14bafad94a104c4d83 Mon Sep 17 00:00:00 2001 From: zjp-CN Date: Fri, 1 Aug 2025 18:11:14 +0800 Subject: [PATCH 13/41] Update text/0000-safety-tags.md Co-authored-by: kennytm --- text/0000-safety-tags.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/text/0000-safety-tags.md b/text/0000-safety-tags.md index 4709e14cf42..4145ac01490 100644 --- a/text/0000-safety-tags.md +++ b/text/0000-safety-tags.md @@ -104,7 +104,7 @@ We can also attach comments for a tag or a group of tags to clarify how safety r ```rust #[clippy::safety { - ValidPtr, Aligned, Initialized: "addr range p..p+n is properly initialized from aligned memory" + ValidPtr, Aligned, Initialized: "addr range p..p+n is properly initialized from aligned memory"; InBounded, ValidNum: "`n` won't exceed isize::MAX here, so `p.add(n)` is fine"; }] for _ in 0..n { From e2e5a116722e2623ea1bcf65843bf1a6265342a7 Mon Sep 17 00:00:00 2001 From: zjp Date: Fri, 1 Aug 2025 22:54:49 +0800 Subject: [PATCH 14/41] safety-tags: Should Tags Take Arguments? --- text/0000-safety-tags.md | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/text/0000-safety-tags.md b/text/0000-safety-tags.md index 4145ac01490..0c500e44059 100644 --- a/text/0000-safety-tags.md +++ b/text/0000-safety-tags.md @@ -343,6 +343,39 @@ While safety tags are less formally verified and intended to be a check list on # Unresolved questions [unresolved-questions]: #unresolved-questions +## Should Tags Take Arguments? + +If a tag takes arguments, how should the tag declared? An uninhabited enum doesn't express +correct semantics anymore. The closest item is a function with arguments. But it's brings +many problems, like +* What argument types should be on such tag declaration functions? Do we need to declare extra +types specifically for such tag declaration? +* Is there a argument check on tag definitions on unsafe functions? Are we reinventing contracts +system again? +* Or just only keep uninhabited enums as tag declaration, and allow any arguments for all tags, +but don't check the validity arguments. Tag arguments enhance precision of safety operation, +but no checks on them avoid any complicated interaction with type system. We can make a compromise. + +When a tag needs parameters, we must decide what its declaration looks like. An uninhabited enum +can no longer express “this tag carries data”, so the nearest legal item is a function whose +parameters represent the tag’s arguments. Unfortunately, this immediately raises design questions: + +1. **Argument types** Which types are allowed in the declaration? Do we have to introduce new, + purpose-built types just so the compiler can see them at the use-site? + +2. **Definition-side checking** Will `unsafe fn` definitions be obliged to supply arguments that + type-check against the declaration? If so, are we quietly reinventing a full contract system? + +I'd like to propose a solution or rather compromise here by trading strict precision for simplicity: + +We could keep uninhabited enums as the only formal declaration and allow *any* arguments at use +sites, skipping all validation. Tag arguments would still refine the description of an unsafe +operation, but they are never type checked. + +Also see [this][tag-args] RFC discussion for our thoughts. + +[tag-args]: https://github.com/rust-lang/rfcs/pull/3842#discussion_r2246551643 + ## Tagging on Unsafe Traits and Impls We can extend safety definitions to unsafe traits and require discharges in unsafe trait impls. From 80cf23dad9206851906c7b0503a3fcb143227a8c Mon Sep 17 00:00:00 2001 From: zjp Date: Sat, 2 Aug 2025 00:20:50 +0800 Subject: [PATCH 15/41] safety-tags: rephrase motivations --- text/0000-safety-tags.md | 83 ++++++++++++++++++++++++++++++++-------- 1 file changed, 67 insertions(+), 16 deletions(-) diff --git a/text/0000-safety-tags.md b/text/0000-safety-tags.md index 0c500e44059..d1d556d99c0 100644 --- a/text/0000-safety-tags.md +++ b/text/0000-safety-tags.md @@ -38,28 +38,77 @@ fn main() { # Motivation [motivation]: #motivation +## Safety Invariants: Forgotten by Authors, Hidden from Reviewers + To avoid the misuse of unsafe code, Rust developers are encouraged to provide clear safety comments -for unsafe APIs. While these comments are generally human-readable, they can be ambiguous and -laborious to write. Even the current best practices in the Rust standard library are somewhat ad hoc -and informal. Moreover, safety comments are often repetitive and may be perceived as less important +for unsafe APIs. Safety comments are often repetitive and may be perceived as less important than the code itself, which makes them error-prone and increases the risk that reviewers may overlook inaccuracies or missing safety requirements. -For instance, a severe problem may arise if the safety requirements of an API change over time: -downstream users may be unaware of such changes and thus be exposed to security risks. Therefore, we -propose to improve the current practice of writing safety comments by making them checkable through -a system of safety tags. These tags are designed to be: +Recent Rust issues [#134496],[#135805], and [#135009] illustrate the problem: several libstd APIs +silently rely on the global allocator, yet their safety comments never state the crucial invariant +that any raw pointer passed to them must refer to memory allocated by that same global allocator. +The pattern is always the same. + +```rust +// Ptr is possibly from another allocation. +pub unsafe fn from_raw(ptr: *const T) -> Self { + // SmartPoiner can be Box, Arc, Rc, and Weak. + unsafe { SmartPoiner::from_raw_in(ptr, Global) } +} +``` + +[#134496]: https://github.com/rust-lang/rust/pull/134496 +[#135805]: https://github.com/rust-lang/rust/pull/135805 +[#135009]: https://github.com/rust-lang/rust/pull/135009 + +Even if the safety documentation is complete, two problems remain: + +- When *writing* the call, the author may forget the inline safety comment that proves every + invariant has been identified and upheld. +- When *reviewing* the call, the absence of such a comment forces the auditor to reconstruct the + required invariants from scratch, with no assurance that the author considered them at all. + +## Granular Unsafe: How Small Is Too Small? + +The unsafe block faces a built-in tension: +- **Precision** demands the smallest possible scope—hence proposals for prefix or postfix `unsafe` + operators that wrap a single unsafe call (see “[Alternatives from IRLO]” for such proposals). +- **Completeness** demands the opposite: unsafe code often depends on surrounding safe (or other + unsafe) code to satisfy its safety invariants, so the scope that must be considered “safe” + balloons outward. + +[Alternatives from IRLO]: #IRLO -* Compatible with existing safety documentation: Safety tags should be expressive enough to - represent current safety comments, especially as rendered in today's rustdoc HTML pages. -* Usable by compiler tools for safety checking: If no safety tags are provided for an unsafe API, - lints should be emitted to remind developers to provide safety requirements. If a safety tag is - declared for an unsafe API but not discharged at a callsite, lints should be emitted to alert - developers about potentially overlooked safety requirements. +## Safety Invariants Have No Semver + +A severe problem may arise if the safety requirements of an API change over time: downstream users +may be unaware of such changes and thus be exposed to security risks. + +## Formal Contracts, Casual Burden + +[Contracts][contracts] excel at enforcing safety invariants rigorously, but they demand the +precision as well as overhead of formal verification, making them too heavy for everyday projects. # Guide-level explanation [guide-level-explanation]: #guide-level-explanation +We propose **checkable safety comments** via Clippy’s new **safety-tag** system, addressing today’s +ad-hoc practice with four concrete gains: + +1. **Shared clarity**. Authors attach a short tag above every unsafe operation; reviewers instantly + see which invariant must hold and where it is satisfied. + +2. **Semantic granularity**. Tags can label a single unsafe call, a loop body, or the entire + caller - no longer constrained by the visual boundaries of `unsafe {}`. + +3. **Versioned contracts**. Tags are crate-level items; any change to their declaration or + definition is a **semver-breaking** API change, so safety invariants evolve explicitly. + +4. **Lightweight checking**. Clippy only matches tag paths. No heavyweight formal proofs, keeping + the system easy to adopt and understand. + + ## Syntax of Safety Tags Syntax of a safety tag is defined as follows: @@ -267,6 +316,8 @@ developers can silence Clippy by discharging tags without verifying underlying s ## Alternatives from IRLO + + There are alternative discussion or Pre-RFCs on IRLO: * 2023-10: [Ability to call unsafe functions without curly brackets](https://internals.rust-lang.org/t/ability-to-call-unsafe-functions-without-curly-brackets/19635/22) @@ -327,14 +378,14 @@ opened to support Rust for Linux on safety standard. [prior-art]: #prior-art Currently, there are efforts on introducing contracts and formal verification into Rust: -* [contracts](https://rust-lang.github.io/rust-project-goals/2024h2/Contracts-and-invariants.html): - the lang experiment has been implemented since - [rust#128044](https://github.com/rust-lang/rust/issues/128044). +* [contracts]: the lang experiment has been implemented since [rust#128044]. * [verify-rust-std] pursues applying formal verification tools to libstd. Also see Rust Foundation [announcement][vrs#ann], project goals during [2024h2] and [2025h1]. While safety tags are less formally verified and intended to be a check list on safety requirements. +[contracts]: https://rust-lang.github.io/rust-project-goals/2024h2/Contracts-and-invariants.html +[rust#128044]: https://github.com/rust-lang/rust/issues/128044 [verify-rust-std]: https://github.com/model-checking/verify-rust-std [vrs#ann]: https://foundation.rust-lang.org/news/rust-foundation-collaborates-with-aws-initiative-to-verify-rust-standard-libraries/ [2024h2]: https://rust-lang.github.io/rust-project-goals/2024h2/std-verification.html From 392d5fc248187cd4512c0f6f0cf9372e2d529a19 Mon Sep 17 00:00:00 2001 From: zjp Date: Sat, 2 Aug 2025 17:37:21 +0800 Subject: [PATCH 16/41] safety-tags: replace clippy namespace with safety namesapce Also * distinguish safety::requires and safety::checked * replace safety::r#use with safety::import * only allow uninhabited enum as tag item --- text/0000-safety-tags.md | 166 ++++++++++++++++++++++++--------------- 1 file changed, 101 insertions(+), 65 deletions(-) diff --git a/text/0000-safety-tags.md b/text/0000-safety-tags.md index d1d556d99c0..94427448161 100644 --- a/text/0000-safety-tags.md +++ b/text/0000-safety-tags.md @@ -7,30 +7,29 @@ [summary]: #summary This RFC introduces a concise safety-comment convention for unsafe code in libstd-adjacent crates: -tag every unsafe function and call with `#[safety { SP1, SP2 }]`. +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, but we expect Clippy and Rust-Analyzer to enforce tag checks -and provide first-class IDE support. +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. -[compiles]: https://play.rust-lang.org/?version=nightly&mode=debug&edition=2024&gist=2f49a8b255b8c066ffd5e3157a70b821 +[compiles]: https://play.rust-lang.org/?version=nightly&mode=debug&edition=2024&gist=8b22aebccf910428008c4423c436d81e ```rust -#![feature(custom_inner_attributes)] -#![clippy::safety::r#use(invariant::*)] // 💡 +#![safety::import(invariant::*)] // 💡 pub mod invariant { - #[clippy::safety::tag] - pub fn ValidPtr() {} + #[safety::declare_tag] // 💡 + pub enum ValidPtr() {} } -#[clippy::safety { ValidPtr }] // 💡 +#[safety::requires { ValidPtr }] // 💡 pub unsafe fn read(ptr: *const T) {} fn main() { - #[clippy::safety { ValidPtr }] // 💡 + #[safety::checked { ValidPtr }] // 💡 unsafe { read(&()) }; } ``` @@ -72,7 +71,7 @@ Even if the safety documentation is complete, two problems remain: ## Granular Unsafe: How Small Is Too Small? The unsafe block faces a built-in tension: -- **Precision** demands the smallest possible scope—hence proposals for prefix or postfix `unsafe` +- **Precision** demands the smallest possible scope, hence proposals for prefix or postfix `unsafe` operators that wrap a single unsafe call (see “[Alternatives from IRLO]” for such proposals). - **Completeness** demands the opposite: unsafe code often depends on surrounding safe (or other unsafe) code to satisfy its safety invariants, so the scope that must be considered “safe” @@ -102,19 +101,32 @@ ad-hoc practice with four concrete gains: 2. **Semantic granularity**. Tags can label a single unsafe call, a loop body, or the entire caller - no longer constrained by the visual boundaries of `unsafe {}`. -3. **Versioned contracts**. Tags are crate-level items; any change to their declaration or - definition is a **semver-breaking** API change, so safety invariants evolve explicitly. +3. **Versioned invariants**. Tags are real items; any change to their declaration or definition is a + **semver-breaking** API change, so safety invariants evolve explicitly. 4. **Lightweight checking**. Clippy only matches tag paths. No heavyweight formal proofs, keeping the system easy to adopt and understand. +## `#[safety]` Tool Attribute and Namespace + +Safety tags have no effect on compilation; they merely document safety invariants. Therefore, we +propose that `#[safety]` be implicitly registered for every crate. + +```rust +#![feature(register_tool)] +#![register_tool(safety)] +``` ## Syntax of Safety Tags Syntax of a safety tag is defined as follows: ```text -SafetyTag -> `#` `[` `clippy::safety` `{` Tags `}` `]` +SafetyTag -> `#` `[` `safety::` Operation Object `]` + +Operation -> requires | checked + +Object -> `[` Tags `]` | `(` Tags `)` | `{` Tags `}` Tags -> Tag (`;` Tag)* @@ -126,33 +138,37 @@ ID -> SingleIdent | SimplePath Here are some tag examples: ```rust -#[clippy::safety { SP }] -#[clippy::safety { SP1, SP2 }] +#[safety::requires { SP }] +#[safety::requires { SP1, SP2 }] -#[clippy::safety { SP1: "reason" }] -#[clippy::safety { SP1: "reason"; SP2: "reason" }] +#[safety::checked { SP1: "reason" }] +#[safety::checked { SP1: "reason"; SP2: "reason" }] -#[clippy::safety { SP1, SP2: "shared reason for the two SPs" }] -#[clippy::safety { SP1, SP2: "shared reason for the two SPs"; SP3 }] -#[clippy::safety { SP3; SP1, SP2: "shared reason for the two SPs" }] +#[safety::checked { SP1, SP2: "shared reason for the two SPs" }] +#[safety::checked { SP1, SP2: "shared reason for the two SPs"; SP3 }] +#[safety::checked { SP3; SP1, SP2: "shared reason for the two SPs" }] ``` -`#[clippy::safety]` is a [tool attribute] that you attach to an unsafe function (or to an expression -that performs unsafe calls). Take [`ptr::read`]: its safety comment lists three requirements, so we -create three corresponding tags on the function declaration and mark each one off at the call site. +`#[safety]` is a tool attribute with two forms to operate on safety invariants: +* `safety::requires` is placed on an unsafe function’s signature to state the safety invariants that +callers must uphold; +* `safety::checked` is placed on an expression or let-statement that wraps an unsafe call. + +Take [`ptr::read`] as an example: its safety comment lists three requirements, so we create three +corresponding tags on the function declaration and mark each one off at the call site. ```rust -#[clippy::safety { ValidPtr, Aligned, Initialized }] // defsite +#[safety::requires { ValidPtr, Aligned, Initialized }] // defsite pub unsafe fn read(ptr: *const T) -> T { ... } -#[clippy::safety { ValidPtr, Aligned, Initialized }] // callsite +#[safety::checked { ValidPtr, Aligned, Initialized }] // callsite unsafe { read(ptr) }; ``` We can also attach comments for a tag or a group of tags to clarify how safety requirements are met: ```rust -#[clippy::safety { +#[safety::checked { ValidPtr, Aligned, Initialized: "addr range p..p+n is properly initialized from aligned memory"; InBounded, ValidNum: "`n` won't exceed isize::MAX here, so `p.add(n)` is fine"; }] @@ -167,16 +183,17 @@ for _ in 0..n { [tool attribute]: https://doc.rust-lang.org/reference/attributes.html#tool-attributes [`ptr::read`]: https://doc.rust-lang.org/std/ptr/fn.read.html -Every safety tag declared on a function must appear in `#[clippy::safety { ... }]` together with an -optional reason; any omission triggers a warning-by-default diagnostic that lists the missing tags -and explains each one: +When calling an unsafe function, tags defined on it must be present in `#[safety::checked]` or +`#[safety::requires]` together with an optional reason; any omission triggers a warning-by-default +diagnostic that lists the missing tags and explains each one: ```rust unsafe { ptr::read(ptr) } ``` ```rust -warning: `ValidPtr`, `Aligned`, `Initialized` tags are missing. Add them to `#[clippy::safety { }]`. +warning: `ValidPtr`, `Aligned`, `Initialized` tags are missing. Add them to `#[safety::checked]` or + `#[safety::requires]` if you're sure these invariants are satisfied. --> file.rs:xxx:xxx | LLL | unsafe { ptr::read(ptr) } @@ -193,42 +210,62 @@ Note that it's allowed to discharge tags of unsafe callees onto the unsafe calle delegation or propogation: ```rust -#[clippy::safety { ValidPtr, Aligned, Initialized }] // ✅ -unsafe fn constructor() -> T { - unsafe { read(...) } +#[safety::requires { ValidPtr, Aligned, Initialized }] // ✅ +unsafe fn delegation(ptr: *const T) -> T { + unsafe { read(ptr) } +} +``` + +Partial discharges are also allowed: + +```rust +#[safety::requires { + ValidPtr, Initialized: "ensure the allocation spans at least size_of::() bytes past ptr" +}] +unsafe fn propogation(ptr: *const T) -> T { + let align = mem::align_of::(); + let addr = ptr as usize; + let aligned_addr = (addr + align - 1) & !(align - 1); + + #[safety::checked { Aligned: "alignment of ptr has be adjusted" }] + unsafe { read(ptr) } } ``` ## Safety Tags as Ordinary Items -Before tagging a function, we must declare them as ordinary items with `#[clippy::safety::tag]` such -as [uninhabited] types or plain functions: +Before tagging a function, we must declare them using `#[safety::declare_tag]` as an [uninhabited] +enum whose value is never constructed: ```rust -#[clippy::safety::tag] +#[safety::declare_tag] enum ValidPtr {} - -#[clippy::safety::tag] -fn Aligned() {} ``` Tags live in their own [type namespace] carry item-level [scopes] and obey [visibility] rules, -keeping the system modular and collision-free. Since they are never referenced directly as real -items, we propose importing them uses a dedicated syntax: inner-styled or outer-styled -`clippy::safety::r#use` tool attribute on modules: +keeping the system modular and collision-free. + +However, tag items are only used in safety tool attribute and never really used in user own code, we +propose importing them uses a dedicated syntax: inner-styled or outer-styled `safety::import` tool +attribute on modules and takes [`UseTree`] whose grammar is shared with that in `use` declaration. +Some examples: ```rust -#![clippy::safety::r#use { UseTree })] // {} signifies a delimiter here, thus () also works -``` +// outer-styled import: import tag Bar to foo module +#[safety::import(crate::invariants::Bar)] +mod foo; -[`UseTree`] follows the exact grammar of the `use` declaration. Some examples: +// inner-styled import: equivalent to the above, +// but must enable #![feature(custom_inner_attributes)] +mod foo { #![safety::import(crate::invariants::Bar)] } -```rust -#[clippy::safety::r#use { core::ptr::invariants::* }] +// Below are examples to import multiple tags into scope. + +#[safety::import { core::ptr::invariants::* }] mod foo; mod bar { - #![clippy::safety::r#use { core::ptr::invariants::{ValidPtr, Aligned} }] + #![safety::import { core::ptr::invariants::{ValidPtr, Aligned} }] } ``` @@ -266,7 +303,7 @@ surface the deprecation warning whenever the tag is used w.r.t definitions and d Currently, safety tags requires the following unstable features * `#![feature(proc_macro_hygiene, stmt_expr_attributes)]` for tagging statements or expressions. -* `#![feature(custom_inner_attributes)]` for `#![clippy::safety::r#use(...)]` imports +* `#![feature(custom_inner_attributes)]` for `#![safety::import(...)]`. Since the safety-tag mechanism is implemented primarily in Clippy and Rust-Analyzer, no additional support is required from rustc. @@ -279,14 +316,13 @@ their call sites. To enable experimentation, a nightly-only library feature Procedure: -1. Scan the crate for every item marked `#[clippy::safety::tag]`; cache the compiled tag metadata of +1. Scan the crate for every item marked `#[safety::declare_tag]`; cache the compiled tag metadata of upstream dependencies under `target/` for later queries. -2. Validate every `#![clippy::safety { ... }]` import by a reachability analysis that ensures every - referenced tag is defined and accessible. +2. Validate every tags in `#![safety::import]` through a reachability analysis to ensure the paths + are accessible. 3. Verify that every unsafe call carries the required safety tags: * Resolve the callee, collect its declared tags, then walk outward from the call site until the - function’s own signature confirms these tags are listed in its `#![clippy::safety::r#use(...)]` - attribute. + function’s own signature confirms these tags are listed in `#[safety::requires]`. * Tags are only discharged inside or onto an `unsafe fn`; it's an error to tag a safe function. * If an unsafe call lacks any required tag, emit a diagnostic whose severity (warning or error) is governed by the configured Clippy lint level. @@ -298,12 +334,12 @@ definition and discharge is strictly valid. Safety-tag analysis requirements: -* Harvest every item marked `#[clippy::safety::tag]`, including those pulled in from dependencies. -* Offer tag path completion for `#![clippy::safety::r#use(...)]`. -* Offer tag name completion for `#[clippy::safety { ... }]` on unsafe functions, let-statements, or - expressions. -* Validate all tags inside `#[clippy::safety { ... }]`, and support “go-to-definition” plus inline - documentation hover. +* Harvest every item marked `#[safety::declare_tag]`, including those pulled in from dependencies. +* Offer tag path completion for `#![safety::import]`. +* Offer tag name and path completion for `#[safety::requires]` on unsafe functions, and + `#[safety::checked]` on let-statements, or expressions. +* Validate all tags inside `#[safety::{requires,checked}]`, and support “go-to-definition” plus + inline documentation hover. # Drawbacks [drawbacks]: #drawbacks @@ -473,7 +509,7 @@ pub const unsafe fn read(src: *const T) -> T { ... } ``` `#[safety]` becomes a procedural macro that expands to both `#[doc]` attributes and the -`#[clippy::safety]` attribute. +`#[safety::requires]` attribute. ```rust /// # Safety @@ -481,7 +517,7 @@ pub const unsafe fn read(src: *const T) -> T { ... } /// - ValidPtr: `src` must be [valid] for reads /// - Aligned: `src` must be properly aligned. Use [`read_unaligned`] if this is not the case /// - Initialized: `src` must point to a properly initialized value of type `T` -#[clippy::safety { ValidPtr, Aligned, Initialized }] +#[safety::requires { ValidPtr, Aligned, Initialized }] pub const unsafe fn read(src: *const T) -> T { ... } ``` @@ -495,12 +531,12 @@ discharge either `DropCheck` or `CopyType` at the call site, depending on the co Another instance is `<*const T>::as_ref`, whose safety doc states that the caller must guarantee “the pointer is either null or safely convertible to a reference”. This can be expressed as -`#[clippy::safety { any { Null, ValidPtr2Ref } }]`, allowing the caller to discharge whichever tag +`#[safety::requires { any { Null, ValidPtr2Ref } }]`, allowing the caller to discharge whichever tag applies. ## Entity References and Code Review Enhancement -To cut boilerplate or link related code locations, we introduce `#[clippy::safety::ref(...)]` which +To cut boilerplate or link related code locations, we introduce `#[safety::ref(...)]` which establishes a two-way reference. An example of this is [IntoIter::try_fold][vec_deque] of VecDeque, using `#[ref]` for short: From 88c3525b44bf91c8690a1296cf3de7d525e4ecf8 Mon Sep 17 00:00:00 2001 From: zjp Date: Sat, 2 Aug 2025 21:20:35 +0800 Subject: [PATCH 17/41] safety-tags: alternative named argument syntax --- text/0000-safety-tags.md | 44 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/text/0000-safety-tags.md b/text/0000-safety-tags.md index 94427448161..189a3a233f8 100644 --- a/text/0000-safety-tags.md +++ b/text/0000-safety-tags.md @@ -271,7 +271,8 @@ mod bar { That's to say: * Tags declared or re-exported in the current module are automatically in scope: no import required. -* Tags from other modules must be brought in with the inner-tool attribute shown above. +* To use tags defined in other modules or crates, attach the `safety::import` attribute to current + module. * Tags are visible and available to downstream crates whenever their declaration paths are public. * Attempting to import a tag from a private module is a **hard error**. * Referencing a tag that has never been declared is also a **hard error**. @@ -482,6 +483,47 @@ at every access point they are discharged. [Unsafe fields]: https://github.com/rust-lang/rfcs/pull/3458 +## Alternatives of Syntax for `requires` and `checked` + +@kennytm suggested [named arguments] in such tags: + +```rust +// Alternative1: change our `:` to `=` before reasons. +#[safety::checked(Tag1 = "reason1", Tag2 = "reason2", Tag3)] +// This would be ugly for tag arguments (if we allow them) and grouped tags. +#[safety::checked(Tag1(arg1) = "reason1", Tag2(arg2) = "reason2", Tag3(arg3))] +// Ambiguous here: reason for Tag2 or (Tag1, Tag2)? +#[safety::checked(Tag1, Tag2 = "reason for what???", Tag3(arg3))] +``` + +The improved version groups tags within single attribute and uses the `reason` field to explain why +invariants are satisfied. The idea is from [lint reasons] + +```rust +// Alternative2: each groupe of tags is in single attribute +#[safety::checked(Tag1, reason = "reason1")] +#[safety::checked(Tag1, Tag2, reason = "reason for Tag1 and Tag2")] +#[safety::checked(Tag1(arg1), reason = "reason for Tag1 and Tag2")] +``` + +Downside of alternative2 is discharge of single tag results in verbose syntax and lines of code: + +```rust +// Must discharge separate tags in separate attributes: +#[safety::checked(Tag1, reason = "reason1")] +#[safety::checked(Tag2, reason = "reason2")] +#[safety::checked(Tag3, reason = "reason3")] +``` + +By comparison, our proposed syntax is + +```rust +#[safety::checked { Tag1: "reason1", Tag2: "reason2", Tag3: "reason3" }] +``` + +[named arguments]: https://github.com/rust-lang/rfcs/pull/3842#discussion_r2247342603 +[lint reasons]: https://doc.rust-lang.org/reference/attributes/diagnostics.html#lint-reasons + # Future possibilities [future-possibilities]: #future-possibilities From 731a5ba5fbbe9e3aef2f2706cd1b1db84f943ae6 Mon Sep 17 00:00:00 2001 From: zjp Date: Sat, 2 Aug 2025 22:56:02 +0800 Subject: [PATCH 18/41] safety-tag: Rationale for the Proposed Implementation --- text/0000-safety-tags.md | 40 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/text/0000-safety-tags.md b/text/0000-safety-tags.md index 189a3a233f8..4856b1a19b2 100644 --- a/text/0000-safety-tags.md +++ b/text/0000-safety-tags.md @@ -307,7 +307,7 @@ Currently, safety tags requires the following unstable features * `#![feature(custom_inner_attributes)]` for `#![safety::import(...)]`. Since the safety-tag mechanism is implemented primarily in Clippy and Rust-Analyzer, no additional -support is required from rustc. +support is required from rustc except registering `safety` tool in every crate. But we ask the libs team to adopt safety tags for all public `unsafe` APIs in libstd, along with their call sites. To enable experimentation, a nightly-only library feature @@ -351,6 +351,29 @@ developers can silence Clippy by discharging tags without verifying underlying s # Rationale and alternatives [rationale-and-alternatives]: #rationale-and-alternatives +## Rationale for the Proposed Implementation + +We argued in the [guide-level-explanation] why safety tags are necessary; here we justify the *way* +they are proposed to implemente. + +1. Keep the compiler untouched. Tag checking is an API-style lint, not a language feature. All the + `#[safety::*]` information is available in the HIR, so a linter (rather than the compiler) is the + right place to enforce the rules. + +2. Re-use the existing linter. We prototyped the checks in [safety-tool], but a stand-alone tool + cannot scale: project may pin its own toolchain, so we would need one binary per toolchain. We + already maintain three separate feature-gated builds just for verify-rust-std, rust-for-linux, + and Asterinas. Once the proposal is standardised, the only sustainable path is to upstream the + lint pass into Clippy. Project building on stable or nightly toolchains since then will get the + checks automatically whenever it runs Clippy. Moreover, if `rust-lang/rust` repo has already + undergone Clippy CI, so no extra tooling is required for tag checking on the standard libraries. + +3. IDE integration. The same reasoning applies to the language server. A custom server would again + be tied to internal APIs and specific toolchains. Extending rust-analyzer is therefore the only + practical way to give users first-class IDE support. + +[safety-tool]: https://github.com/Artisan-Lab/tag-std/blob/main/safety-tool + ## Alternatives from IRLO @@ -496,8 +519,8 @@ at every access point they are discharged. #[safety::checked(Tag1, Tag2 = "reason for what???", Tag3(arg3))] ``` -The improved version groups tags within single attribute and uses the `reason` field to explain why -invariants are satisfied. The idea is from [lint reasons] +[Lint reasons] inspires the following improved form that groups tags within single attribute and +uses the `reason` field to explain why invariants are satisfied. ```rust // Alternative2: each groupe of tags is in single attribute @@ -522,11 +545,17 @@ By comparison, our proposed syntax is ``` [named arguments]: https://github.com/rust-lang/rfcs/pull/3842#discussion_r2247342603 -[lint reasons]: https://doc.rust-lang.org/reference/attributes/diagnostics.html#lint-reasons +[Lint reasons]: https://doc.rust-lang.org/reference/attributes/diagnostics.html#lint-reasons # Future possibilities [future-possibilities]: #future-possibilities +## Better Rustdoc Rendering + +Because tags are surfaced as real API items, rustdoc can give `#[safety::declare_tag]`–annotated, +uninhabited enums (the tag items) special treatment: it renders compact pages for them and +establishes bidirectional links between tag items and unsafe functions requiring the tags. + ## Generate Safety Docs from Tags We can take structured safety comments one step further by turning the explanatory prose into @@ -563,6 +592,9 @@ pub const unsafe fn read(src: *const T) -> T { ... } pub const unsafe fn read(src: *const T) -> T { ... } ``` +With support for tag arguments, safety documentation can be made more precise and contextual by +dynamically injecting the argument values into the reason strings. + ## `any { Option1, Option2 }` Tags Sometimes it’s useful to declare a set of safety tags on an unsafe function while discharging only From 39b17e261a6994df1fd21a7466879a1265e646b4 Mon Sep 17 00:00:00 2001 From: zjp Date: Sat, 2 Aug 2025 23:20:38 +0800 Subject: [PATCH 19/41] safety-tags: teams support for this RFC --- text/0000-safety-tags.md | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/text/0000-safety-tags.md b/text/0000-safety-tags.md index 4856b1a19b2..908be98ee38 100644 --- a/text/0000-safety-tags.md +++ b/text/0000-safety-tags.md @@ -302,12 +302,13 @@ surface the deprecation warning whenever the tag is used w.r.t definitions and d ## Unstable Features -Currently, safety tags requires the following unstable features +Currently, safety tags requires the following features * `#![feature(proc_macro_hygiene, stmt_expr_attributes)]` for tagging statements or expressions. * `#![feature(custom_inner_attributes)]` for `#![safety::import(...)]`. +* registering `safety` tool in every crate to create safety namespace. Since the safety-tag mechanism is implemented primarily in Clippy and Rust-Analyzer, no additional -support is required from rustc except registering `safety` tool in every crate. +significant support is required from rustc. But we ask the libs team to adopt safety tags for all public `unsafe` APIs in libstd, along with their call sites. To enable experimentation, a nightly-only library feature @@ -356,7 +357,7 @@ developers can silence Clippy by discharging tags without verifying underlying s We argued in the [guide-level-explanation] why safety tags are necessary; here we justify the *way* they are proposed to implemente. -1. Keep the compiler untouched. Tag checking is an API-style lint, not a language feature. All the +1. It's linter's work. Tag checking is an API-style lint, not a language feature. All the `#[safety::*]` information is available in the HIR, so a linter (rather than the compiler) is the right place to enforce the rules. @@ -372,6 +373,15 @@ they are proposed to implemente. be tied to internal APIs and specific toolchains. Extending rust-analyzer is therefore the only practical way to give users first-class IDE support. +We therefore seek approvals from the following teams: + +1. **Library team** – to allow the tagging of unsafe operations and to expose tag items as public + APIs. +2. **Clippy team** – to integrate tag checking into the linter. +3. **Rust-Analyzer team** – to add IDE support for tags. +4. **Compiler team** – to reserve the `safety` namespace and gate the feature via + `#![feature(safety_tags)]`. + [safety-tool]: https://github.com/Artisan-Lab/tag-std/blob/main/safety-tool ## Alternatives from IRLO From 2130878acbbc38a481c1aa18734f364d2076ad93 Mon Sep 17 00:00:00 2001 From: zjp Date: Sat, 2 Aug 2025 23:36:00 +0800 Subject: [PATCH 20/41] safety-tags: Why Not Structured Safety Comments? --- text/0000-safety-tags.md | 34 +++++++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/text/0000-safety-tags.md b/text/0000-safety-tags.md index 908be98ee38..ba4f34262f9 100644 --- a/text/0000-safety-tags.md +++ b/text/0000-safety-tags.md @@ -416,7 +416,7 @@ There are alternative discussion or Pre-RFCs on IRLO: [this thread in opsem channel](https://rust-lang.zulipchat.com/#narrow/channel/136281-t-opsem/topic/Safety.20Property.20System/with/530679491). * The critical parts have already been refined from Clippy’s perspective in current proposal. -## Alternatives from Rust for Linux +## Safety Standard Proposal from Rust for Linux More importantly, our proposal is a big improvement to these proposals, which Rust for Linux care more about: @@ -424,8 +424,14 @@ more about: proposed by Benno Lossin * These slides outline the motivations and objectives of safety-documentation standardization — exactly what our proposal aims to deliver. - * They omit implementation details; nevertheless, Predrag (see next line) and we remain faithful - to their intent. + * They omit implementation details; nevertheless, Predrag (see next section) and we remain + faithful to their intent. + +[meeting note]: https://hackmd.io/@qnR1-HVLRx-dekU5dvtvkw/SyUuR6SZgx +[Rust Safety Standard]: https://kangrejos.com/2024/Rust%20Safety%20Standard.pdf + +## Why Not Structured Safety Comments? + * 2024-10: [Automated checking of unsafe code requirements](https://hackmd.io/@predrag/ByVBjIWlyx) proposed by Predrag * Predrag’s proposal focuses on structured safety comments, entity references, requirement @@ -435,14 +441,24 @@ more about: remain highly readable for humans, but not so much for tools, because line-comments are discarded early by the compiler. This makes retrieving a rule for a specific expression far harder than with [`stmt_expr_attributes`](https://github.com/rust-lang/rust/issues/15701). +* 2025-07: [Reddit Post: Safety Property System](https://internals.rust-lang.org/t/pre-rfc-safety-property-system/23252/22) + discussed by Matthieum -Originally, we only focus on libstd's common safety propeties ([paper]), but noticed the RustWeek -[meeting note] in zulipchat. Thus [tag-std#3](https://github.com/Artisan-Lab/tag-std/issues/3) is -opened to support Rust for Linux on safety standard. +Our proposed syntax looks closer to structured comments: -[meeting note]: https://hackmd.io/@qnR1-HVLRx-dekU5dvtvkw/SyUuR6SZgx -[Rust Safety Standard]: https://kangrejos.com/2024/Rust%20Safety%20Standard.pdf -[paper]: https://arxiv.org/abs/2504.21312 +```rust +#[safety { + ValidPtr, Align, Initialized: "`self.head_tail()` returns two slices to live elements."; + NotOwned: "because we incremented..."; +}] +unsafe { ptr::read(elem) } +``` + +```rust +// SAFETY +// - ValidPtr, Aligned, Initialized: `self.head_tail()` returns two slices to live elements. +// - NotOwned: because we incremented... +``` # Prior art [prior-art]: #prior-art From b077573c87122bb0972182d55a45648dca6853fd Mon Sep 17 00:00:00 2001 From: zjp Date: Sat, 2 Aug 2025 23:50:56 +0800 Subject: [PATCH 21/41] safety-tags: Encapsulate Declaration with define_safety_tag! --- text/0000-safety-tags.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/text/0000-safety-tags.md b/text/0000-safety-tags.md index ba4f34262f9..bb462ffba94 100644 --- a/text/0000-safety-tags.md +++ b/text/0000-safety-tags.md @@ -573,6 +573,14 @@ By comparison, our proposed syntax is [named arguments]: https://github.com/rust-lang/rfcs/pull/3842#discussion_r2247342603 [Lint reasons]: https://doc.rust-lang.org/reference/attributes/diagnostics.html#lint-reasons +## Encapsulate Tag Item Declaration with `define_safety_tag!` + +@clarfonthey [suggested][tool macro] a `define_safety_tag!` tool macro which will unlikely happen. + +But I think it'd be necessary and handy to hide tag declarations in some cases. + +[tool macro]: https://github.com/rust-lang/rfcs/pull/3842#discussion_r2245923920 + # Future possibilities [future-possibilities]: #future-possibilities From d90644780ccacef5c3810c76a41b7d95493ae3b0 Mon Sep 17 00:00:00 2001 From: zjp Date: Sun, 3 Aug 2025 08:59:33 +0800 Subject: [PATCH 22/41] safety-tags: minor clarification --- text/0000-safety-tags.md | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/text/0000-safety-tags.md b/text/0000-safety-tags.md index bb462ffba94..c5f87807618 100644 --- a/text/0000-safety-tags.md +++ b/text/0000-safety-tags.md @@ -355,7 +355,7 @@ developers can silence Clippy by discharging tags without verifying underlying s ## Rationale for the Proposed Implementation We argued in the [guide-level-explanation] why safety tags are necessary; here we justify the *way* -they are proposed to implemente. +to implement them. 1. It's linter's work. Tag checking is an API-style lint, not a language feature. All the `#[safety::*]` information is available in the HIR, so a linter (rather than the compiler) is the @@ -367,10 +367,10 @@ they are proposed to implemente. and Asterinas. Once the proposal is standardised, the only sustainable path is to upstream the lint pass into Clippy. Project building on stable or nightly toolchains since then will get the checks automatically whenever it runs Clippy. Moreover, if `rust-lang/rust` repo has already - undergone Clippy CI, so no extra tooling is required for tag checking on the standard libraries. + undergone Clippy CI, no extra tooling is required for tag checking on the standard libraries. 3. IDE integration. The same reasoning applies to the language server. A custom server would again - be tied to internal APIs and specific toolchains. Extending rust-analyzer is therefore the only + be tied to internal APIs and specific toolchains. Extending Rust-Analyzer is therefore the only practical way to give users first-class IDE support. We therefore seek approvals from the following teams: @@ -380,7 +380,7 @@ We therefore seek approvals from the following teams: 2. **Clippy team** – to integrate tag checking into the linter. 3. **Rust-Analyzer team** – to add IDE support for tags. 4. **Compiler team** – to reserve the `safety` namespace and gate the feature via - `#![feature(safety_tags)]`. + `#![feature(safety_tags)]` for the namespace and tag APIs in standard libraries. [safety-tool]: https://github.com/Artisan-Lab/tag-std/blob/main/safety-tool @@ -418,8 +418,6 @@ There are alternative discussion or Pre-RFCs on IRLO: ## Safety Standard Proposal from Rust for Linux -More importantly, our proposal is a big improvement to these proposals, which Rust for Linux care -more about: * 2024-09: [Rust Safety Standard: Increasing the Correctness of unsafe Code][Rust Safety Standard] proposed by Benno Lossin * These slides outline the motivations and objectives of safety-documentation standardization — @@ -458,6 +456,7 @@ unsafe { ptr::read(elem) } // SAFETY // - ValidPtr, Aligned, Initialized: `self.head_tail()` returns two slices to live elements. // - NotOwned: because we incremented... +unsafe { ptr::read(elem) } ``` # Prior art @@ -495,19 +494,19 @@ but no checks on them avoid any complicated interaction with type system. We can When a tag needs parameters, we must decide what its declaration looks like. An uninhabited enum can no longer express “this tag carries data”, so the nearest legal item is a function whose -parameters represent the tag’s arguments. Unfortunately, this immediately raises design questions: +parameters represent the tag’s arguments. Unfortunately, this immediately raises design questions: -1. **Argument types** Which types are allowed in the declaration? Do we have to introduce new, - purpose-built types just so the compiler can see them at the use-site? +1. **Argument types**. Which types are allowed in the declaration? Do we have to introduce new + arguments types for each tag declaration? -2. **Definition-side checking** Will `unsafe fn` definitions be obliged to supply arguments that - type-check against the declaration? If so, are we quietly reinventing a full contract system? +2. **Definition-side checking**. Will tag operations `safety::requires` and `safety::checked` + type-check against these arguments? If so, are we quietly reinventing a full contract system? I'd like to propose a solution or rather compromise here by trading strict precision for simplicity: -We could keep uninhabited enums as the only formal declaration and allow *any* arguments at use -sites, skipping all validation. Tag arguments would still refine the description of an unsafe -operation, but they are never type checked. +We could keep uninhabited enums as the only tag declaration and allow *any* arguments at use sites +without validation. Tag arguments would still refine the description of an unsafe operation, but +they are never type checked. Also see [this][tag-args] RFC discussion for our thoughts. @@ -577,7 +576,8 @@ By comparison, our proposed syntax is @clarfonthey [suggested][tool macro] a `define_safety_tag!` tool macro which will unlikely happen. -But I think it'd be necessary and handy to hide tag declarations in some cases. +But I think it'd be good and handy to hide tag declaration details. But this means +there is a `define_safety_tag!` in libcore for any crate to use. [tool macro]: https://github.com/rust-lang/rfcs/pull/3842#discussion_r2245923920 From 078d22c806db19c5a05dae70801f2e6ade3a2ae6 Mon Sep 17 00:00:00 2001 From: zjp Date: Sun, 3 Aug 2025 14:27:26 +0800 Subject: [PATCH 23/41] =?UTF-8?q?safety-tags:=20summarize=20safety=20attri?= =?UTF-8?q?butes=20with=20=F0=9F=92=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- text/0000-safety-tags.md | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/text/0000-safety-tags.md b/text/0000-safety-tags.md index c5f87807618..1d1b808c316 100644 --- a/text/0000-safety-tags.md +++ b/text/0000-safety-tags.md @@ -18,18 +18,18 @@ and Rust-Analyzer to enforce tag checks and provide first-class IDE support. [compiles]: https://play.rust-lang.org/?version=nightly&mode=debug&edition=2024&gist=8b22aebccf910428008c4423c436d81e ```rust -#![safety::import(invariant::*)] // 💡 +#![safety::import(invariant::*)] // 💡 import tag items pub mod invariant { - #[safety::declare_tag] // 💡 + #[safety::declare_tag] // 💡 declare a tag item only on uninhabited enum pub enum ValidPtr() {} } -#[safety::requires { ValidPtr }] // 💡 +#[safety::requires { ValidPtr }] // 💡 define tags on unsafe functions pub unsafe fn read(ptr: *const T) {} fn main() { - #[safety::checked { ValidPtr }] // 💡 + #[safety::checked { ValidPtr }] // 💡 discharge tags on unsafe calls unsafe { read(&()) }; } ``` @@ -506,11 +506,18 @@ I'd like to propose a solution or rather compromise here by trading strict preci We could keep uninhabited enums as the only tag declaration and allow *any* arguments at use sites without validation. Tag arguments would still refine the description of an unsafe operation, but -they are never type checked. +they are never type checked. An example: -Also see [this][tag-args] RFC discussion for our thoughts. +```rust +#[safety::define_safety_tag( + args = [ "p", "T", "len" ], + desc = "pointer `{p}` must be valid for reading and writing the `sizeof({T})*{n}` memory from it" +)] +enum ValidPtr {} -[tag-args]: https://github.com/rust-lang/rfcs/pull/3842#discussion_r2246551643 +#[safety::requires { ValidPtr(ptr) }] +unsafe fn foo(ptr: *const T) -> T { ... } +``` ## Tagging on Unsafe Traits and Impls From 6f9577d21bafc57eb093a213959aa01d1749b3c5 Mon Sep 17 00:00:00 2001 From: zjp Date: Sun, 3 Aug 2025 17:13:08 +0800 Subject: [PATCH 24/41] safety-tags: explain how safety::checked is validated on unsafe operation Other major clarification: * safety::checked is mandatory in unsafe delegation example * discussion on precedence of grouped tags --- text/0000-safety-tags.md | 114 +++++++++++++++++++++++++++++++-------- 1 file changed, 91 insertions(+), 23 deletions(-) diff --git a/text/0000-safety-tags.md b/text/0000-safety-tags.md index 1d1b808c316..fbd344d5479 100644 --- a/text/0000-safety-tags.md +++ b/text/0000-safety-tags.md @@ -98,8 +98,9 @@ ad-hoc practice with four concrete gains: 1. **Shared clarity**. Authors attach a short tag above every unsafe operation; reviewers instantly see which invariant must hold and where it is satisfied. -2. **Semantic granularity**. Tags can label a single unsafe call, a loop body, or the entire - caller - no longer constrained by the visual boundaries of `unsafe {}`. +2. **Semantic granularity**. Tags must label a single unsafe call, or an expression contains single + unsafe call. No longer constrained by the visual boundaries of `unsafe {}`. This sidesteps the + precision vs completeness tension of unsafe blocks, and zeros in on real unsafe operations. 3. **Versioned invariants**. Tags are real items; any change to their declaration or definition is a **semver-breaking** API change, so safety invariants evolve explicitly. @@ -146,7 +147,6 @@ Here are some tag examples: #[safety::checked { SP1, SP2: "shared reason for the two SPs" }] #[safety::checked { SP1, SP2: "shared reason for the two SPs"; SP3 }] -#[safety::checked { SP3; SP1, SP2: "shared reason for the two SPs" }] ``` `#[safety]` is a tool attribute with two forms to operate on safety invariants: @@ -168,13 +168,16 @@ unsafe { read(ptr) }; We can also attach comments for a tag or a group of tags to clarify how safety requirements are met: ```rust -#[safety::checked { - ValidPtr, Aligned, Initialized: "addr range p..p+n is properly initialized from aligned memory"; - InBounded, ValidNum: "`n` won't exceed isize::MAX here, so `p.add(n)` is fine"; -}] for _ in 0..n { unsafe { + #[safety::checked { ValidPtr, Aligned, Initialized: + "addr range p..p+n is properly initialized from aligned memory" + }] c ^= p.read(); + + #[safety::checked { InBounded, ValidNum: + "`n` won't exceed isize::MAX here, so `p.add(n)` is fine" + }] p = p.add(1); } } @@ -183,8 +186,19 @@ for _ in 0..n { [tool attribute]: https://doc.rust-lang.org/reference/attributes.html#tool-attributes [`ptr::read`]: https://doc.rust-lang.org/std/ptr/fn.read.html -When calling an unsafe function, tags defined on it must be present in `#[safety::checked]` or -`#[safety::requires]` together with an optional reason; any omission triggers a warning-by-default + +NOTE: the follwing syntax is valid, but probably bad in practice, since people may leave a typo `,` +to accidentally group tags when they mean `;` to separate them. Thus I'd just suggest forbiding +such syntax to avoid surprising unsafe delegation which will be disscused in next section. + +```rust +#[safety::checked { SP3; SP1, SP2: "shared reason for the two SPs" }] +``` + +## Discharge Tags + +When calling an unsafe function, tags defined by `#[safety::requires]` on it must be present in +`#[safety::checked]` together with an optional reason; any omission triggers a warning-by-default diagnostic that lists the missing tags and explains each one: ```rust @@ -210,28 +224,67 @@ Note that it's allowed to discharge tags of unsafe callees onto the unsafe calle delegation or propogation: ```rust -#[safety::requires { ValidPtr, Aligned, Initialized }] // ✅ +#[safety::requires { ValidPtr, Aligned, Initialized }] +unsafe fn propogation(ptr: *const T) -> T { + #[safety::checked { ValidPtr, Aligned, Initialized }] + unsafe { read(ptr) } +} +``` + +Tags defined on an unsafe function must be fully discharged at callsites. No partial discharge: + +```rust +#[safety::requires { ValidPtr, Initialized }] unsafe fn delegation(ptr: *const T) -> T { + #[safety::checked { Aligned }] // 💥 Error: Tags are not fully discharged. unsafe { read(ptr) } } ``` -Partial discharges are also allowed: +For such partial unsafe delegations, please fully discharge tags on the callee and define needed +tags on the caller. ```rust #[safety::requires { ValidPtr, Initialized: "ensure the allocation spans at least size_of::() bytes past ptr" }] -unsafe fn propogation(ptr: *const T) -> T { +unsafe fn delegation(ptr: *const T) -> T { let align = mem::align_of::(); let addr = ptr as usize; let aligned_addr = (addr + align - 1) & !(align - 1); - #[safety::checked { Aligned: "alignment of ptr has be adjusted" }] + #[safety::checked { + Aligned: "alignment of ptr has be adjusted"; + ValidPtr, Initialized: "delegated to the caller" + }] unsafe { read(ptr) } } ``` +In this delegation case, you're able to declare a new meaningful tag for ValidPtr and Initialized +invariants, and define the new tag on `delegation` function. This practice extends to partial unsafe +delegation of multiple tag discharges: + +```rust +#[safety::declare_tag] +enum MyInvaraint {} // Invariants of A and C, but could be a more contextual name. + +#[safety::requires { MyInvaraint }] +unsafe fn delegation() { + unsafe { + #[safety::checked { A: "delegated to the caller"; B }] + foo(); + #[safety::checked { C: "delegated to the caller"; D }] + bar(); + } +} +``` + +Note that the tag group matters here, because we want to convey `MyInvaraint` represents A and C. If +someone doesn't pay attention to the group and writes `B, A: "..."` instead of separating B from A +with `B; A: "..."`, the `MyInvaraint` will be inaccurate. So I'd just propose grouped tags precede +tags without reasons. + ## Safety Tags as Ordinary Items Before tagging a function, we must declare them using `#[safety::declare_tag]` as an [uninhabited] @@ -318,16 +371,31 @@ their call sites. To enable experimentation, a nightly-only library feature Procedure: -1. Scan the crate for every item marked `#[safety::declare_tag]`; cache the compiled tag metadata of - upstream dependencies under `target/` for later queries. -2. Validate every tags in `#![safety::import]` through a reachability analysis to ensure the paths - are accessible. -3. Verify that every unsafe call carries the required safety tags: - * Resolve the callee, collect its declared tags, then walk outward from the call site until the - function’s own signature confirms these tags are listed in `#[safety::requires]`. - * Tags are only discharged inside or onto an `unsafe fn`; it's an error to tag a safe function. - * If an unsafe call lacks any required tag, emit a diagnostic whose severity (warning or error) - is governed by the configured Clippy lint level. +1. Scan the crate for uninhabited enums marked `#[safety::declare_tag]`; cache the compiled tag + metadata of upstream dependencies under `target/` for later queries. + * Raise an error if `declare_tag` is on other items, and ask readers to tag uninhabited enum. +2. Resolve and validate tags in `#![safety::import]` through a reachability analysis to ensure the + paths are accessible. +3. Validate `#[safety::requires]` only appears on unsafe functions if the attribute exists. + +4. Validate `#[safety::checked]` on HIR nodes whose `ExprKind` is one of + - **direct unsafe nodes**: `Call`, `MethodCall` that invoke an unsafe function/method, or + - **indirect unsafe nodes**: `Block` (unsafe), `Let`, `Assign`, `AssignOp`. + + Algorithm for every function body: + 1. Walk the HIR; whenever an unsafe `Call`/`MethodCall` is encountered, record the unsafe callee + and its nearest ancestor that is an *indirect unsafe node*. + 2. If the callee is annotated with safety tags, require that **either** the call itself **or** + its recorded ancestor carries `#[safety::checked]`. + 3. Any node that carries `#[safety::checked]` must contain **exactly one** unsafe call/method; + otherwise emit a diagnostic. *(We intentionally stop at this simple rule; splitting complex + unsafe expressions into separate annotated nodes is considered good style.)* + 4. Diagnostics are emitted at the current Clippy lint level (warning or error). + +5. Resolve and validate tags in `#[safety::requires]` correspond to tag declarations. +6. Resolve and validate tags in `#[safety::checked]` correspond to tag declarations and definitions. + +[HIR ExprKind]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_hir/hir/enum.ExprKind.html Libraries in `rust-lang/rust` must enforce tag checking as a hard error, guaranteeing that every tag definition and discharge is strictly valid. From 7e0daffb510c7302e76ffe77bf0b9517cdd0447b Mon Sep 17 00:00:00 2001 From: zjp Date: Sun, 3 Aug 2025 18:15:46 +0800 Subject: [PATCH 25/41] safety-tags: minor adjustment --- text/0000-safety-tags.md | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/text/0000-safety-tags.md b/text/0000-safety-tags.md index fbd344d5479..47d62c9bf48 100644 --- a/text/0000-safety-tags.md +++ b/text/0000-safety-tags.md @@ -220,8 +220,8 @@ LLL | unsafe { ptr::read(ptr) } The process of verifying whether a tag is present is referred to as tag discharge. -Note that it's allowed to discharge tags of unsafe callees onto the unsafe caller for unsafe -delegation or propogation: +Now consider forwarding invariants of unsafe callees onto the unsafe caller for unsafe delegation or +propogation: ```rust #[safety::requires { ValidPtr, Aligned, Initialized }] @@ -231,7 +231,7 @@ unsafe fn propogation(ptr: *const T) -> T { } ``` -Tags defined on an unsafe function must be fully discharged at callsites. No partial discharge: +Tags defined on an unsafe function must be **fully** discharged at callsites. No partial discharge: ```rust #[safety::requires { ValidPtr, Initialized }] @@ -567,12 +567,12 @@ parameters represent the tag’s arguments. Unfortunately, this immediately rais 1. **Argument types**. Which types are allowed in the declaration? Do we have to introduce new arguments types for each tag declaration? -2. **Definition-side checking**. Will tag operations `safety::requires` and `safety::checked` - type-check against these arguments? If so, are we quietly reinventing a full contract system? +2. **Type checking**. Will tag operations `safety::requires` and `safety::checked` type-check + against these arguments? If so, are we quietly reinventing a full contract system? I'd like to propose a solution or rather compromise here by trading strict precision for simplicity: -We could keep uninhabited enums as the only tag declaration and allow *any* arguments at use sites +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: @@ -585,6 +585,9 @@ enum ValidPtr {} #[safety::requires { ValidPtr(ptr) }] unsafe fn foo(ptr: *const T) -> T { ... } + +#[safety::checked { ValidPtr(p) }] +unsafe { bar(p) } ``` ## Tagging on Unsafe Traits and Impls From cb7dbd7f0029ff29a21b45e84ccd4db65ff1ce23 Mon Sep 17 00:00:00 2001 From: zjp Date: Sun, 3 Aug 2025 18:36:32 +0800 Subject: [PATCH 26/41] safety-tags: semantic granularity with future entity-reference also add https://github.com/rust-lang/rfcs/pull/3768 safety blocks to alternative proposals --- text/0000-safety-tags.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/text/0000-safety-tags.md b/text/0000-safety-tags.md index 47d62c9bf48..8349bb0d787 100644 --- a/text/0000-safety-tags.md +++ b/text/0000-safety-tags.md @@ -101,13 +101,17 @@ ad-hoc practice with four concrete gains: 2. **Semantic granularity**. Tags must label a single unsafe call, or an expression contains single unsafe call. No longer constrained by the visual boundaries of `unsafe {}`. This sidesteps the precision vs completeness tension of unsafe blocks, and zeros in on real unsafe operations. + * To enable truly semantic checking, we envision an [entity-reference] system that meticulously + traces every unsafe operation that could break an invariant in source code. 3. **Versioned invariants**. Tags are real items; any change to their declaration or definition is a - **semver-breaking** API change, so safety invariants evolve explicitly. + *semver-breaking* API change, so safety invariants evolve explicitly. 4. **Lightweight checking**. Clippy only matches tag paths. No heavyweight formal proofs, keeping the system easy to adopt and understand. +[entity-reference]: #entity-reference + ## `#[safety]` Tool Attribute and Namespace Safety tags have no effect on compilation; they merely document safety invariants. Therefore, we @@ -473,6 +477,8 @@ There are alternative discussion or Pre-RFCs on IRLO: * The practice of using a single unsafe call is good, but the postfix `.unsafe` requires more compiler support and does not offer suggestions for improving safe comments. * Our RFC, however, supports annotating safety tags on any expression, including single calls. +* 2025-01: [RFC: Add safe blocks](https://github.com/rust-lang/rfcs/pull/3768) + * This is a continum of discussion of 2024-10, focusing on visual granularity. * 2025-05: [Pre-RFC: Granular Unsafe Blocks - A more explicit and auditable approach](https://internals.rust-lang.org/t/pre-rfc-granular-unsafe-blocks-a-more-explicit-and-auditable-approach/23022) proposed by Redlintles * The safety categories suggested are overly broad. In contrast, the safety properties outlined in our RFC are more granular and semantics-specific. @@ -722,6 +728,8 @@ applies. ## Entity References and Code Review Enhancement + + To cut boilerplate or link related code locations, we introduce `#[safety::ref(...)]` which establishes a two-way reference. From ad2d8d7eef0f1b671238df42f38d1d8fc4087a6e Mon Sep 17 00:00:00 2001 From: zjp Date: Sun, 3 Aug 2025 23:29:22 +0800 Subject: [PATCH 27/41] safety-tags: minor fixes --- text/0000-safety-tags.md | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/text/0000-safety-tags.md b/text/0000-safety-tags.md index 8349bb0d787..e1fce362bcb 100644 --- a/text/0000-safety-tags.md +++ b/text/0000-safety-tags.md @@ -6,7 +6,7 @@ # Summary [summary]: #summary -This RFC introduces a concise safety-comment convention for unsafe code in libstd-adjacent crates: +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 @@ -102,7 +102,7 @@ ad-hoc practice with four concrete gains: unsafe call. No longer constrained by the visual boundaries of `unsafe {}`. This sidesteps the precision vs completeness tension of unsafe blocks, and zeros in on real unsafe operations. * To enable truly semantic checking, we envision an [entity-reference] system that meticulously - traces every unsafe operation that could break an invariant in source code. + traces every unsafe related operation that could break an invariant in source code. 3. **Versioned invariants**. Tags are real items; any change to their declaration or definition is a *semver-breaking* API change, so safety invariants evolve explicitly. @@ -477,7 +477,7 @@ There are alternative discussion or Pre-RFCs on IRLO: * The practice of using a single unsafe call is good, but the postfix `.unsafe` requires more compiler support and does not offer suggestions for improving safe comments. * Our RFC, however, supports annotating safety tags on any expression, including single calls. -* 2025-01: [RFC: Add safe blocks](https://github.com/rust-lang/rfcs/pull/3768) +* 2025-01: [RFC: Add safe blocks](https://github.com/rust-lang/rfcs/pull/3768) by Aversefun * This is a continum of discussion of 2024-10, focusing on visual granularity. * 2025-05: [Pre-RFC: Granular Unsafe Blocks - A more explicit and auditable approach](https://internals.rust-lang.org/t/pre-rfc-granular-unsafe-blocks-a-more-explicit-and-auditable-approach/23022) proposed by Redlintles * The safety categories suggested are overly broad. In contrast, the safety properties outlined in @@ -555,17 +555,6 @@ While safety tags are less formally verified and intended to be a check list on ## Should Tags Take Arguments? -If a tag takes arguments, how should the tag declared? An uninhabited enum doesn't express -correct semantics anymore. The closest item is a function with arguments. But it's brings -many problems, like -* What argument types should be on such tag declaration functions? Do we need to declare extra -types specifically for such tag declaration? -* Is there a argument check on tag definitions on unsafe functions? Are we reinventing contracts -system again? -* Or just only keep uninhabited enums as tag declaration, and allow any arguments for all tags, -but don't check the validity arguments. Tag arguments enhance precision of safety operation, -but no checks on them avoid any complicated interaction with type system. We can make a compromise. - When a tag needs parameters, we must decide what its declaration looks like. An uninhabited enum can no longer express “this tag carries data”, so the nearest legal item is a function whose parameters represent the tag’s arguments. Unfortunately, this immediately raises design questions: @@ -583,7 +572,7 @@ without validation. Tag arguments would still refine the description of an unsaf they are never type checked. An example: ```rust -#[safety::define_safety_tag( +#[safety::declare_tag( args = [ "p", "T", "len" ], desc = "pointer `{p}` must be valid for reading and writing the `sizeof({T})*{n}` memory from it" )] From 35aa734601a1dd5828aa693aeb1ceeb577c794b4 Mon Sep 17 00:00:00 2001 From: zjp Date: Fri, 1 Aug 2025 02:13:59 +0000 Subject: [PATCH 28/41] safety-tags: clarify #[ref] support from clippy and RA --- text/0000-safety-tags.md | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/text/0000-safety-tags.md b/text/0000-safety-tags.md index e1fce362bcb..670a60c72b9 100644 --- a/text/0000-safety-tags.md +++ b/text/0000-safety-tags.md @@ -722,14 +722,14 @@ applies. To cut boilerplate or link related code locations, we introduce `#[safety::ref(...)]` which establishes a two-way reference. -An example of this is [IntoIter::try_fold][vec_deque] of VecDeque, using `#[ref]` for short: +An example of this is [`IntoIter::try_fold`][vec_deque] of VecDeque, using `#[ref]` for short: [vec_deque]: https://github.com/rust-lang/rust/blob/ebd8557637b33cc09b6ee8273f3154d5d3af6a15/library/alloc/src/collections/vec_deque/into_iter.rs#L104 ```rust fn try_fold(&mut self, mut init: B, mut f: F) -> R impl<'a, T, A: Allocator> Drop for Guard<'a, T, A> { - #[ref(try_fold)] // 💡 ptr::read below relies on this drop impl + #[ref(try_fold)] // 💡 unsafety of ptr::read below relies on this drop impl fn drop(&mut self) { ... } } ... @@ -739,7 +739,7 @@ fn try_fold(&mut self, mut init: B, mut f: F) -> R #[ref(try_fold)] // 💡 #[safety { - ValidPtr, Aligned, Initialized, + ValidPtr, Aligned, Initialized; DropCheck: "Because we incremented `guard.consumed`, the deque \ effectively forgot the element, so we can take ownership." }] @@ -781,8 +781,10 @@ fn try_rfold(&mut self, mut init: B, mut f: F) -> R { } ``` -These `#[ref]` annotations act as cross-references that nudge developers to inspect every linked -site. When either end or the code around it changes, reviewers are instantly aware of all affected -locations that Clippy reports and thus can assess if every referenced safety requirement is still -satisfied. +These `#[ref]` tags act as cross-references that nudge developers to inspect every linked site. When +either end or the code around it changes, reviewers are instantly aware of all affected locations +and thus can assess if every referenced safety requirement is still satisfied. +Clippy can generate a diff-style report that pinpoints every location where changes to referenced +HIR nodes occur between two commits or crate versions, enabling more focused code reviews. To +improve dev experiences, Rust-Analyzer can retrieve every ref sites from a given ref tag object. From 82a9c2c09fbf8360bb27e6f112e96e3684437a8e Mon Sep 17 00:00:00 2001 From: zjp Date: Mon, 4 Aug 2025 15:05:10 +0000 Subject: [PATCH 29/41] remove safety::import; named arguments in safety::{requires,checked} --- text/0000-safety-tags.md | 269 +++++++++++---------------------------- 1 file changed, 76 insertions(+), 193 deletions(-) diff --git a/text/0000-safety-tags.md b/text/0000-safety-tags.md index 670a60c72b9..1202524c97e 100644 --- a/text/0000-safety-tags.md +++ b/text/0000-safety-tags.md @@ -15,21 +15,21 @@ 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. -[compiles]: https://play.rust-lang.org/?version=nightly&mode=debug&edition=2024&gist=8b22aebccf910428008c4423c436d81e +[compiles]: https://play.rust-lang.org/?version=nightly&mode=debug&edition=2024&gist=6eb0e47c416953da1f2470b11417e69a ```rust -#![safety::import(invariant::*)] // 💡 import tag items - -pub mod invariant { - #[safety::declare_tag] // 💡 declare a tag item only on uninhabited enum - pub enum ValidPtr() {} -} +#[safety::requires { // 💡 define safety tags on an unsafe function + ValidPtr = "src must be valid 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(ptr: *const T) { } -#[safety::requires { ValidPtr }] // 💡 define tags on unsafe functions -pub unsafe fn read(ptr: *const T) {} fn main() { - #[safety::checked { ValidPtr }] // 💡 discharge tags on unsafe calls + #[safety::checked { // 💡 discharge safety tags on an unsafe call + ValidPtr, Aligned, Initialized = "optional reason" + }] unsafe { read(&()) }; } ``` @@ -133,39 +133,36 @@ Operation -> requires | checked Object -> `[` Tags `]` | `(` Tags `)` | `{` Tags `}` -Tags -> Tag (`;` Tag)* +Tags -> Tag (`,` Tag)* `,`? -Tag -> ID (`,` ID)* (`:` LiteralString)? +Tag -> ID (`=` LiteralString)? -ID -> SingleIdent | SimplePath +ID -> SingleIdent ``` Here are some tag examples: ```rust #[safety::requires { SP }] -#[safety::requires { SP1, SP2 }] +#[safety::requires { SP1 = "description1", SP2 = "description2" }] -#[safety::checked { SP1: "reason" }] -#[safety::checked { SP1: "reason"; SP2: "reason" }] - -#[safety::checked { SP1, SP2: "shared reason for the two SPs" }] -#[safety::checked { SP1, SP2: "shared reason for the two SPs"; SP3 }] +#[safety::checked { SP }] +#[safety::checked { SP1 = "description1", SP2 = "description2" }] ``` `#[safety]` is a tool attribute with two forms to operate on safety invariants: * `safety::requires` is placed on an unsafe function’s signature to state the safety invariants that -callers must uphold; -* `safety::checked` is placed on an expression or let-statement that wraps an unsafe call. + callers must uphold; +* `safety::checked` is placed on an expression that wraps an unsafe call. Take [`ptr::read`] as an example: its safety comment lists three requirements, so we create three corresponding tags on the function declaration and mark each one off at the call site. ```rust -#[safety::requires { ValidPtr, Aligned, Initialized }] // defsite +#[safety::requires { ValidPtr, Aligned, Initialized }] // defsite or definition pub unsafe fn read(ptr: *const T) -> T { ... } -#[safety::checked { ValidPtr, Aligned, Initialized }] // callsite +#[safety::checked { ValidPtr, Aligned, Initialized }] // callsite or discharge unsafe { read(ptr) }; ``` @@ -174,12 +171,12 @@ We can also attach comments for a tag or a group of tags to clarify how safety r ```rust for _ in 0..n { unsafe { - #[safety::checked { ValidPtr, Aligned, Initialized: + #[safety::checked { ValidPtr, Aligned, Initialized = "addr range p..p+n is properly initialized from aligned memory" }] c ^= p.read(); - #[safety::checked { InBounded, ValidNum: + #[safety::checked { InBounded, ValidNum = "`n` won't exceed isize::MAX here, so `p.add(n)` is fine" }] p = p.add(1); @@ -190,15 +187,6 @@ for _ in 0..n { [tool attribute]: https://doc.rust-lang.org/reference/attributes.html#tool-attributes [`ptr::read`]: https://doc.rust-lang.org/std/ptr/fn.read.html - -NOTE: the follwing syntax is valid, but probably bad in practice, since people may leave a typo `,` -to accidentally group tags when they mean `;` to separate them. Thus I'd just suggest forbiding -such syntax to avoid surprising unsafe delegation which will be disscused in next section. - -```rust -#[safety::checked { SP3; SP1, SP2: "shared reason for the two SPs" }] -``` - ## Discharge Tags When calling an unsafe function, tags defined by `#[safety::requires]` on it must be present in @@ -222,7 +210,7 @@ LLL | unsafe { ptr::read(ptr) } = NOTE: See core::ptr::invariants::Initialized ``` -The process of verifying whether a tag is present is referred to as tag discharge. +The process of verifying whether a tag is checked is referred to as tag discharge. Now consider forwarding invariants of unsafe callees onto the unsafe caller for unsafe delegation or propogation: @@ -258,8 +246,8 @@ unsafe fn delegation(ptr: *const T) -> T { let aligned_addr = (addr + align - 1) & !(align - 1); #[safety::checked { - Aligned: "alignment of ptr has be adjusted"; - ValidPtr, Initialized: "delegated to the caller" + Aligned: "alignment of ptr has be adjusted"; + ValidPtr, Initialized: "delegated to the caller" }] unsafe { read(ptr) } } @@ -276,84 +264,46 @@ enum MyInvaraint {} // Invariants of A and C, but could be a more contextual nam #[safety::requires { MyInvaraint }] unsafe fn delegation() { unsafe { - #[safety::checked { A: "delegated to the caller"; B }] + #[safety::checked { A: "delegated to the caller's MyInvaraint"; B }] foo(); - #[safety::checked { C: "delegated to the caller"; D }] + #[safety::checked { C: "delegated to the caller's MyInvaraint"; D }] bar(); } } ``` -Note that the tag group matters here, because we want to convey `MyInvaraint` represents A and C. If -someone doesn't pay attention to the group and writes `B, A: "..."` instead of separating B from A -with `B; A: "..."`, the `MyInvaraint` will be inaccurate. So I'd just propose grouped tags precede -tags without reasons. +Note that discharing a tag that is not defined will raise a hard error. -## Safety Tags as Ordinary Items +## Safety Tags are Part of an Unsafe Function -Before tagging a function, we must declare them using `#[safety::declare_tag]` as an [uninhabited] -enum whose value is never constructed: +Tags are extra information of unsafe functions, so rustdoc can render documentation of tags, +displaying each tag and its optional description below function's doc. Rust-Analyzer can also offer +**full IDE support**: completion, go-to-definition, and doc-hover. -```rust -#[safety::declare_tag] -enum ValidPtr {} -``` +Tags constitute a public API; therefore, any alteration to their definition must be evaluated +against [Semantic Versioning][semver]. +* Adding a tag definition is a **minor** change. +* Removing a tag definition is a **major** change. Renaming a tag definition is a two-step + operation of removal and addition, bringing a major change due to removal. -Tags live in their own [type namespace] carry item-level [scopes] and obey [visibility] rules, -keeping the system modular and collision-free. +[semver]: https://doc.rust-lang.org/cargo/reference/semver.html -However, tag items are only used in safety tool attribute and never really used in user own code, we -propose importing them uses a dedicated syntax: inner-styled or outer-styled `safety::import` tool -attribute on modules and takes [`UseTree`] whose grammar is shared with that in `use` declaration. -Some examples: +To give dependent crates time to migrate an outdated tag definition, use `@deprecated` in tag +description. Clippy will emit a deprecation warning whenever the tag is used in `safety::checked`. ```rust -// outer-styled import: import tag Bar to foo module -#[safety::import(crate::invariants::Bar)] -mod foo; - -// inner-styled import: equivalent to the above, -// but must enable #![feature(custom_inner_attributes)] -mod foo { #![safety::import(crate::invariants::Bar)] } - -// Below are examples to import multiple tags into scope. - -#[safety::import { core::ptr::invariants::* }] -mod foo; +#[safety::requires { + NewTag = "description", + Tag = "@deprecated explain why this tag is discouraged or what tag shoud be used instead", +}] +unsafe fn deprecate_a_tag() {} -mod bar { - #![safety::import { core::ptr::invariants::{ValidPtr, Aligned} }] -} +// warning: Tag is deprecated, copy description to here. +// error: NewTag is not discharged. +#[safety::requires { Tag }] +unsafe { deprecate_a_tag() } ``` -That's to say: -* Tags declared or re-exported in the current module are automatically in scope: no import required. -* To use tags defined in other modules or crates, attach the `safety::import` attribute to current - module. -* Tags are visible and available to downstream crates whenever their declaration paths are public. -* Attempting to import a tag from a private module is a **hard error**. -* Referencing a tag that has never been declared is also a **hard error**. - -[uninhabited]: https://doc.rust-lang.org/reference/glossary.html#uninhabited -[type namespace]: https://doc.rust-lang.org/reference/names/namespaces.html -[scopes]: https://doc.rust-lang.org/reference/names/scopes.html#item-scopes -[visibility]: https://doc.rust-lang.org/reference/visibility-and-privacy.html -[`UseTree`]: https://doc.rust-lang.org/reference/items/use-declarations.html - -Tags are treated as items so rustdoc can render their documentation and hyperlink tag references. -And Rust-Analyzer can offer **full IDE support**: completion, go-to-definition/declaration, and -doc-hover. - -Tags constitute a public API; therefore, any alteration to their declaration or definition must be -evaluated against [Semantic Versioning][semver]. -* Adding a tag declaration or definition is a **minor** change. -* Removing a tag declaration or definition is a **major** change. - -To give dependent crates time to migrate, mark obsolete tag items with `#[deprecated]`. Clippy will -surface the deprecation warning whenever the tag is used w.r.t definitions and discharges. - -[semver]: https://doc.rust-lang.org/cargo/reference/semver.html - # Reference-level explanation [reference-level-explanation]: #reference-level-explanation @@ -361,7 +311,6 @@ surface the deprecation warning whenever the tag is used w.r.t definitions and d Currently, safety tags requires the following features * `#![feature(proc_macro_hygiene, stmt_expr_attributes)]` for tagging statements or expressions. -* `#![feature(custom_inner_attributes)]` for `#![safety::import(...)]`. * registering `safety` tool in every crate to create safety namespace. Since the safety-tag mechanism is implemented primarily in Clippy and Rust-Analyzer, no additional @@ -375,14 +324,8 @@ their call sites. To enable experimentation, a nightly-only library feature Procedure: -1. Scan the crate for uninhabited enums marked `#[safety::declare_tag]`; cache the compiled tag - metadata of upstream dependencies under `target/` for later queries. - * Raise an error if `declare_tag` is on other items, and ask readers to tag uninhabited enum. -2. Resolve and validate tags in `#![safety::import]` through a reachability analysis to ensure the - paths are accessible. -3. Validate `#[safety::requires]` only appears on unsafe functions if the attribute exists. - -4. Validate `#[safety::checked]` on HIR nodes whose `ExprKind` is one of +1. Validate `#[safety::requires]` only appears on unsafe functions if the attribute exists. +2. Validate `#[safety::checked]` on HIR nodes whose `ExprKind` is one of - **direct unsafe nodes**: `Call`, `MethodCall` that invoke an unsafe function/method, or - **indirect unsafe nodes**: `Block` (unsafe), `Let`, `Assign`, `AssignOp`. @@ -394,10 +337,8 @@ Procedure: 3. Any node that carries `#[safety::checked]` must contain **exactly one** unsafe call/method; otherwise emit a diagnostic. *(We intentionally stop at this simple rule; splitting complex unsafe expressions into separate annotated nodes is considered good style.)* - 4. Diagnostics are emitted at the current Clippy lint level (warning or error). - -5. Resolve and validate tags in `#[safety::requires]` correspond to tag declarations. -6. Resolve and validate tags in `#[safety::checked]` correspond to tag declarations and definitions. + 4. Make sure tags in `#[safety::checked]` correspond to their definitions. + 5. Diagnostics are emitted at the current Clippy lint level (warning or error). [HIR ExprKind]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_hir/hir/enum.ExprKind.html @@ -406,14 +347,11 @@ definition and discharge is strictly valid. ## Implementation in Rust Analyzer -Safety-tag analysis requirements: +Safety-tag analysis requirements: offer tag name completion, go-to-definition and inline +documentation hover in `#[safety::checked]` as per tag definitions on unsafe calls. -* Harvest every item marked `#[safety::declare_tag]`, including those pulled in from dependencies. -* Offer tag path completion for `#![safety::import]`. -* Offer tag name and path completion for `#[safety::requires]` on unsafe functions, and - `#[safety::checked]` on let-statements, or expressions. -* Validate all tags inside `#[safety::{requires,checked}]`, and support “go-to-definition” plus - inline documentation hover. +Maybe some logics on safety tags like collecting tag definitions need to be extracted to a shared +crate for both Clippy and Rust-Analyzer to use. # Drawbacks [drawbacks]: #drawbacks @@ -555,33 +493,29 @@ While safety tags are less formally verified and intended to be a check list on ## Should Tags Take Arguments? -When a tag needs parameters, we must decide what its declaration looks like. An uninhabited enum -can no longer express “this tag carries data”, so the nearest legal item is a function whose -parameters represent the tag’s arguments. Unfortunately, this immediately raises design questions: - -1. **Argument types**. Which types are allowed in the declaration? Do we have to introduce new - arguments types for each tag declaration? +When a tag needs arguments to refine context in discharging, we must decide what its definition +looks like. Unfortunately, this immediately raises design questions: +1. **Argument types**. Which types are allowed in the definition? 2. **Type checking**. Will tag operations `safety::requires` and `safety::checked` type-check against these arguments? If so, are we quietly reinventing a full contract system? I'd like to propose a solution or rather compromise here by trading strict precision for simplicity: -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: +We could 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: ```rust -#[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) }] +#[safety::requires { + ValidPtr = { + args = [ "p", "T", "len" ], + desc = "pointer `{p}` must be valid for \ + reading and writing the `sizeof({T})*{n}` memory from it" + } +}] unsafe fn foo(ptr: *const T) -> T { ... } -#[safety::checked { ValidPtr(p) }] +#[safety::checked { ValidPtr(p) }] // p will not be type-checked unsafe { bar(p) } ``` @@ -604,56 +538,6 @@ at every access point they are discharged. [Unsafe fields]: https://github.com/rust-lang/rfcs/pull/3458 -## Alternatives of Syntax for `requires` and `checked` - -@kennytm suggested [named arguments] in such tags: - -```rust -// Alternative1: change our `:` to `=` before reasons. -#[safety::checked(Tag1 = "reason1", Tag2 = "reason2", Tag3)] -// This would be ugly for tag arguments (if we allow them) and grouped tags. -#[safety::checked(Tag1(arg1) = "reason1", Tag2(arg2) = "reason2", Tag3(arg3))] -// Ambiguous here: reason for Tag2 or (Tag1, Tag2)? -#[safety::checked(Tag1, Tag2 = "reason for what???", Tag3(arg3))] -``` - -[Lint reasons] inspires the following improved form that groups tags within single attribute and -uses the `reason` field to explain why invariants are satisfied. - -```rust -// Alternative2: each groupe of tags is in single attribute -#[safety::checked(Tag1, reason = "reason1")] -#[safety::checked(Tag1, Tag2, reason = "reason for Tag1 and Tag2")] -#[safety::checked(Tag1(arg1), reason = "reason for Tag1 and Tag2")] -``` - -Downside of alternative2 is discharge of single tag results in verbose syntax and lines of code: - -```rust -// Must discharge separate tags in separate attributes: -#[safety::checked(Tag1, reason = "reason1")] -#[safety::checked(Tag2, reason = "reason2")] -#[safety::checked(Tag3, reason = "reason3")] -``` - -By comparison, our proposed syntax is - -```rust -#[safety::checked { Tag1: "reason1", Tag2: "reason2", Tag3: "reason3" }] -``` - -[named arguments]: https://github.com/rust-lang/rfcs/pull/3842#discussion_r2247342603 -[Lint reasons]: https://doc.rust-lang.org/reference/attributes/diagnostics.html#lint-reasons - -## Encapsulate Tag Item Declaration with `define_safety_tag!` - -@clarfonthey [suggested][tool macro] a `define_safety_tag!` tool macro which will unlikely happen. - -But I think it'd be good and handy to hide tag declaration details. But this means -there is a `define_safety_tag!` in libcore for any crate to use. - -[tool macro]: https://github.com/rust-lang/rfcs/pull/3842#discussion_r2245923920 - # Future possibilities [future-possibilities]: #future-possibilities @@ -702,7 +586,7 @@ pub const unsafe fn read(src: *const T) -> T { ... } With support for tag arguments, safety documentation can be made more precise and contextual by dynamically injecting the argument values into the reason strings. -## `any { Option1, Option2 }` Tags +## Discharge One Tag from `any = { Option1, Option2 }` Sometimes it’s useful to declare a set of safety tags on an unsafe function while discharging only one of them. @@ -712,8 +596,8 @@ discharge either `DropCheck` or `CopyType` at the call site, depending on the co Another instance is `<*const T>::as_ref`, whose safety doc states that the caller must guarantee “the pointer is either null or safely convertible to a reference”. This can be expressed as -`#[safety::requires { any { Null, ValidPtr2Ref } }]`, allowing the caller to discharge whichever tag -applies. +`#[safety::requires { any = { Null, ValidPtr2Ref } }]`, allowing the caller to discharge whichever +tag applies. ## Entity References and Code Review Enhancement @@ -738,10 +622,9 @@ fn try_fold(&mut self, mut init: B, mut f: F) -> R guard.consumed += 1; #[ref(try_fold)] // 💡 - #[safety { - ValidPtr, Aligned, Initialized; - DropCheck: "Because we incremented `guard.consumed`, the deque \ - effectively forgot the element, so we can take ownership." + #[safety { ValidPtr, Aligned, Initialized, DropCheck = + "Because we incremented `guard.consumed`, the deque \ + effectively forgot the element, so we can take ownership." }] unsafe { ptr::read(elem) } }) From 75f85f3de95264ed6f177a98d74c2c9b52929eac Mon Sep 17 00:00:00 2001 From: zjp Date: Tue, 5 Aug 2025 00:45:24 +0800 Subject: [PATCH 30/41] add "Tagging More Unsafe Operations" --- text/0000-safety-tags.md | 39 +++++++++++++++++++++------------------ 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/text/0000-safety-tags.md b/text/0000-safety-tags.md index 1202524c97e..66735e9d2e4 100644 --- a/text/0000-safety-tags.md +++ b/text/0000-safety-tags.md @@ -519,24 +519,27 @@ unsafe fn foo(ptr: *const T) -> T { ... } unsafe { bar(p) } ``` -## Tagging on Unsafe Traits and Impls - -We can extend safety definitions to unsafe traits and require discharges in unsafe trait impls. - -Crates with heavy unsafe-trait usage likely needs the extension. We’d welcome more minds on this. - -## Tagging on Datastructures - -We believe safety requirements are almost always imposed by unsafe functions, so tagging a struct, -enum, or union is neither needed nor permitted. - -## Tagging on Unsafe Fields - -[Unsafe fields] are declared and accessed with the `unsafe` keyword, often accompanied by safety -comments. We could extend safety tags to cover unsafe fields as well, both in their definitions and -at every access point they are discharged. - -[Unsafe fields]: https://github.com/rust-lang/rfcs/pull/3458 +## Tagging More Unsafe Operations + +There are several other unsafe operations other than unsafe calls. We can extend safety tags in the +following cases: + +1. Dereferencing a raw pointer: we can define such operation to have `Deref` tag, thus users will + have to discharge `Deref` when this operation happens. (Is Deref tag enough?) +2. Reading or writing a mutable or external static variable: we can relax `requires` to such static + items, so tags can be defined on them as well as be discharged on such operation happens. +3. Accessing a field of a union or an [unsafe field]: we can extend tag definitions to such field, + so tags must be discharged at every access point. +4. Calling some kinds of safe functions like ones marked with a target_feature or an unsafe + attribute or in an extern block: the definition and discharge rules is the same as that of + ordinary unsafe functions. +5. Implementing an unsafe trait: we can extend safety definitions to unsafe traits and require + discharges in unsafe trait impls. + +[unsafe fields]: https://github.com/rust-lang/rfcs/pull/3458 + +But we believe safety requirements are almost mostly imposed by unsafe functions, so tagging a +struct, enum, or union is neither needed nor permitted. # Future possibilities [future-possibilities]: #future-possibilities From 2711e17f4a84cab7b4d10205dc8f0c265217f95e Mon Sep 17 00:00:00 2001 From: zjp Date: Tue, 5 Aug 2025 01:04:36 +0800 Subject: [PATCH 31/41] minor fixes --- text/0000-safety-tags.md | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/text/0000-safety-tags.md b/text/0000-safety-tags.md index 66735e9d2e4..90da2a822d6 100644 --- a/text/0000-safety-tags.md +++ b/text/0000-safety-tags.md @@ -274,11 +274,7 @@ unsafe fn delegation() { Note that discharing a tag that is not defined will raise a hard error. -## Safety Tags are Part of an Unsafe Function - -Tags are extra information of unsafe functions, so rustdoc can render documentation of tags, -displaying each tag and its optional description below function's doc. Rust-Analyzer can also offer -**full IDE support**: completion, go-to-definition, and doc-hover. +## Safety Tags are a Part of an Unsafe Function Tags constitute a public API; therefore, any alteration to their definition must be evaluated against [Semantic Versioning][semver]. @@ -528,15 +524,15 @@ following cases: have to discharge `Deref` when this operation happens. (Is Deref tag enough?) 2. Reading or writing a mutable or external static variable: we can relax `requires` to such static items, so tags can be defined on them as well as be discharged on such operation happens. -3. Accessing a field of a union or an [unsafe field]: we can extend tag definitions to such field, - so tags must be discharged at every access point. +3. Accessing a union field or an [unsafe field]: we can extend tag definitions to such field, so + tags must be discharged at every access point. 4. Calling some kinds of safe functions like ones marked with a target_feature or an unsafe attribute or in an extern block: the definition and discharge rules is the same as that of ordinary unsafe functions. 5. Implementing an unsafe trait: we can extend safety definitions to unsafe traits and require discharges in unsafe trait impls. -[unsafe fields]: https://github.com/rust-lang/rfcs/pull/3458 +[unsafe field]: https://github.com/rust-lang/rfcs/pull/3458 But we believe safety requirements are almost mostly imposed by unsafe functions, so tagging a struct, enum, or union is neither needed nor permitted. @@ -546,9 +542,9 @@ struct, enum, or union is neither needed nor permitted. ## Better Rustdoc Rendering -Because tags are surfaced as real API items, rustdoc can give `#[safety::declare_tag]`–annotated, -uninhabited enums (the tag items) special treatment: it renders compact pages for them and -establishes bidirectional links between tag items and unsafe functions requiring the tags. +Because tags are surfaced as a part of API, rustdoc can render documentation of tags by displaying +each tag name, its optional description, and possible deprecated state below the tagged function or +other unsafe item. ## Generate Safety Docs from Tags From 33826b10f32ffdceaf0982c90045eb6e747c5f2c Mon Sep 17 00:00:00 2001 From: zjp Date: Tue, 5 Aug 2025 09:05:16 +0800 Subject: [PATCH 32/41] force description in tag definitions; fix `:` by `=` --- text/0000-safety-tags.md | 93 +++++++++++++++++----------------------- 1 file changed, 40 insertions(+), 53 deletions(-) diff --git a/text/0000-safety-tags.md b/text/0000-safety-tags.md index 90da2a822d6..826e4e1bbc5 100644 --- a/text/0000-safety-tags.md +++ b/text/0000-safety-tags.md @@ -19,13 +19,12 @@ and Rust-Analyzer to enforce tag checks and provide first-class IDE support. ```rust #[safety::requires { // 💡 define safety tags on an unsafe function - ValidPtr = "src must be valid for reads", + ValidPtr = "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(ptr: *const T) { } - fn main() { #[safety::checked { // 💡 discharge safety tags on an unsafe call ValidPtr, Aligned, Initialized = "optional reason" @@ -104,7 +103,7 @@ ad-hoc practice with four concrete gains: * To enable truly semantic checking, we envision an [entity-reference] system that meticulously traces every unsafe related operation that could break an invariant in source code. -3. **Versioned invariants**. Tags are real items; any change to their declaration or definition is a +3. **Versioned invariants**. Tags are real items; any change to their definition is a *semver-breaking* API change, so safety invariants evolve explicitly. 4. **Lightweight checking**. Clippy only matches tag paths. No heavyweight formal proofs, keeping @@ -135,19 +134,7 @@ Object -> `[` Tags `]` | `(` Tags `)` | `{` Tags `}` Tags -> Tag (`,` Tag)* `,`? -Tag -> ID (`=` LiteralString)? - -ID -> SingleIdent -``` - -Here are some tag examples: - -```rust -#[safety::requires { SP }] -#[safety::requires { SP1 = "description1", SP2 = "description2" }] - -#[safety::checked { SP }] -#[safety::checked { SP1 = "description1", SP2 = "description2" }] +Tag -> SingleIdent (`=` LiteralString)? ``` `#[safety]` is a tool attribute with two forms to operate on safety invariants: @@ -158,15 +145,20 @@ Here are some tag examples: Take [`ptr::read`] as an example: its safety comment lists three requirements, so we create three corresponding tags on the function declaration and mark each one off at the call site. +Note that a tag definition must contain human text to describe safety requirements for readers to +understand them and Clippy to emit good error messages. + ```rust -#[safety::requires { ValidPtr, Aligned, Initialized }] // defsite or definition +#[safety::requires { // defsite or definition + ValidPtr = "definition1", Aligned = "definition2", Initialized = "definition3" +}] pub unsafe fn read(ptr: *const T) -> T { ... } -#[safety::checked { ValidPtr, Aligned, Initialized }] // callsite or discharge +#[safety::checked { ValidPtr, Aligned, Initialized }] // callsite or discharge unsafe { read(ptr) }; ``` -We can also attach comments for a tag or a group of tags to clarify how safety requirements are met: +We can also attach comments for a tag to clarify how safety requirements are met in callsites: ```rust for _ in 0..n { @@ -199,15 +191,15 @@ unsafe { ptr::read(ptr) } ```rust warning: `ValidPtr`, `Aligned`, `Initialized` tags are missing. Add them to `#[safety::checked]` or - `#[safety::requires]` if you're sure these invariants are satisfied. + once these invariants are confirmed to be satisfied. --> file.rs:xxx:xxx | LLL | unsafe { ptr::read(ptr) } | ^^^^^^^^^^^^^^^^^^^^^^^^^ This unsafe call requires these safety tags. | - = NOTE: See core::ptr::invariants::ValidPtr - = NOTE: See core::ptr::invariants::Aligned - = NOTE: See core::ptr::invariants::Initialized + = NOTE: ValidPtr = "definition1" + = NOTE: Aligned = "definition2" + = NOTE: Initialized = "definition3" ``` The process of verifying whether a tag is checked is referred to as tag discharge. @@ -216,7 +208,7 @@ Now consider forwarding invariants of unsafe callees onto the unsafe caller for propogation: ```rust -#[safety::requires { ValidPtr, Aligned, Initialized }] +#[safety::requires { ValidPtr = "...", Aligned = "...", Initialized = "..." }] unsafe fn propogation(ptr: *const T) -> T { #[safety::checked { ValidPtr, Aligned, Initialized }] unsafe { read(ptr) } @@ -226,7 +218,7 @@ unsafe fn propogation(ptr: *const T) -> T { Tags defined on an unsafe function must be **fully** discharged at callsites. No partial discharge: ```rust -#[safety::requires { ValidPtr, Initialized }] +#[safety::requires { ValidPtr = "...", Aligned = "...", Initialized = "..." }] unsafe fn delegation(ptr: *const T) -> T { #[safety::checked { Aligned }] // 💥 Error: Tags are not fully discharged. unsafe { read(ptr) } @@ -237,17 +229,15 @@ For such partial unsafe delegations, please fully discharge tags on the callee a tags on the caller. ```rust -#[safety::requires { - ValidPtr, Initialized: "ensure the allocation spans at least size_of::() bytes past ptr" -}] +#[safety::requires { ValidPtr = "...", Initialized = "..." }] unsafe fn delegation(ptr: *const T) -> T { let align = mem::align_of::(); let addr = ptr as usize; let aligned_addr = (addr + align - 1) & !(align - 1); #[safety::checked { - Aligned: "alignment of ptr has be adjusted"; - ValidPtr, Initialized: "delegated to the caller" + Aligned = "alignment of ptr has be adjusted", + ValidPtr, Initialized = "delegated to the caller" }] unsafe { read(ptr) } } @@ -258,15 +248,12 @@ invariants, and define the new tag on `delegation` function. This practice exten delegation of multiple tag discharges: ```rust -#[safety::declare_tag] -enum MyInvaraint {} // Invariants of A and C, but could be a more contextual name. - -#[safety::requires { MyInvaraint }] +#[safety::requires { MyInvaraint = "Invariants of A and C, but could be a more contextual name." }] unsafe fn delegation() { unsafe { - #[safety::checked { A: "delegated to the caller's MyInvaraint"; B }] + #[safety::checked { A = "delegated to the caller's MyInvaraint", B }] foo(); - #[safety::checked { C: "delegated to the caller's MyInvaraint"; D }] + #[safety::checked { C = "delegated to the caller's MyInvaraint", D }] bar(); } } @@ -290,7 +277,7 @@ description. Clippy will emit a deprecation warning whenever the tag is used in ```rust #[safety::requires { NewTag = "description", - Tag = "@deprecated explain why this tag is discouraged or what tag shoud be used instead", + Tag = "@deprecated Explain why this tag is discouraged or what tag shoud be used instead", }] unsafe fn deprecate_a_tag() {} @@ -527,7 +514,7 @@ following cases: 3. Accessing a union field or an [unsafe field]: we can extend tag definitions to such field, so tags must be discharged at every access point. 4. Calling some kinds of safe functions like ones marked with a target_feature or an unsafe - attribute or in an extern block: the definition and discharge rules is the same as that of + attribute or in an extern block: the definition and discharge rules is the same as those of ordinary unsafe functions. 5. Implementing an unsafe trait: we can extend safety definitions to unsafe traits and require discharges in unsafe trait impls. @@ -562,9 +549,9 @@ pub const unsafe fn read(src: *const T) -> T { ... } ```rust #[safety { - ValidPtr: "`src` must be [valid] for reads"; - Aligned: "`src` must be properly aligned. Use [`read_unaligned`] if this is not the case"; - Initialized: "`src` must point to a properly initialized value of type `T`" + ValidPtr = "`src` must be [valid] for reads"; + Aligned = "`src` must be properly aligned. Use [`read_unaligned`] if this is not the case"; + Initialized = "`src` must point to a properly initialized value of type `T`" }] pub const unsafe fn read(src: *const T) -> T { ... } ``` @@ -578,7 +565,7 @@ pub const unsafe fn read(src: *const T) -> T { ... } /// - ValidPtr: `src` must be [valid] for reads /// - Aligned: `src` must be properly aligned. Use [`read_unaligned`] if this is not the case /// - Initialized: `src` must point to a properly initialized value of type `T` -#[safety::requires { ValidPtr, Aligned, Initialized }] +#[safety::requires { ValidPtr = "...", Aligned = "...", Initialized = "..." }] pub const unsafe fn read(src: *const T) -> T { ... } ``` @@ -605,14 +592,14 @@ tag applies. To cut boilerplate or link related code locations, we introduce `#[safety::ref(...)]` which establishes a two-way reference. -An example of this is [`IntoIter::try_fold`][vec_deque] of VecDeque, using `#[ref]` for short: +An example of this is [`IntoIter::try_fold`][vec_deque] of VecDeque: [vec_deque]: https://github.com/rust-lang/rust/blob/ebd8557637b33cc09b6ee8273f3154d5d3af6a15/library/alloc/src/collections/vec_deque/into_iter.rs#L104 ```rust fn try_fold(&mut self, mut init: B, mut f: F) -> R impl<'a, T, A: Allocator> Drop for Guard<'a, T, A> { - #[ref(try_fold)] // 💡 unsafety of ptr::read below relies on this drop impl + #[safety::ref(try_fold)] // 💡 unsafety of ptr::read below relies on this drop impl fn drop(&mut self) { ... } } ... @@ -620,8 +607,8 @@ fn try_fold(&mut self, mut init: B, mut f: F) -> R init = head.iter().map(|elem| { guard.consumed += 1; - #[ref(try_fold)] // 💡 - #[safety { ValidPtr, Aligned, Initialized, DropCheck = + #[safety::ref(try_fold)] // 💡 + #[safety::checked { ValidPtr, Aligned, Initialized, DropCheck = "Because we incremented `guard.consumed`, the deque \ effectively forgot the element, so we can take ownership." }] @@ -632,7 +619,7 @@ fn try_fold(&mut self, mut init: B, mut f: F) -> R tail.iter().map(|elem| { guard.consumed += 1; - #[ref(try_fold)] // 💡 No longer to write SAFETY: Same as above. + #[safety::ref(try_fold)] // 💡 No longer to write SAFETY: Same as above. unsafe { ptr::read(elem) } }) .try_fold(init, &mut f) @@ -640,7 +627,7 @@ fn try_fold(&mut self, mut init: B, mut f: F) -> R fn try_rfold(&mut self, mut init: B, mut f: F) -> R { impl<'a, T, A: Allocator> Drop for Guard<'a, T, A> { - #[ref(try_fold)] // 💡 + #[safety::ref(try_fold)] // 💡 fn drop(&mut self) { ... } } ... @@ -648,7 +635,7 @@ fn try_rfold(&mut self, mut init: B, mut f: F) -> R { init = tail.iter().map(|elem| { guard.consumed += 1; - #[ref(try_fold)] // 💡 No longer to write SAFETY: See `try_fold`'s safety comment. + #[safety::ref(try_fold)] // 💡 No longer to write SAFETY: See `try_fold`'s safety comment. unsafe { ptr::read(elem) } }) .try_rfold(init, &mut f)?; @@ -656,16 +643,16 @@ fn try_rfold(&mut self, mut init: B, mut f: F) -> R { head.iter().map(|elem| { guard.consumed += 1; - #[ref(try_fold)] // 💡 No longer to write SAFETY: Same as above. + #[safety::ref(try_fold)] // 💡 No longer to write SAFETY: Same as above. unsafe { ptr::read(elem) } }) .try_rfold(init, &mut f) } ``` -These `#[ref]` tags act as cross-references that nudge developers to inspect every linked site. When -either end or the code around it changes, reviewers are instantly aware of all affected locations -and thus can assess if every referenced safety requirement is still satisfied. +These `#[safety::ref]` tags act as cross-references that nudge developers to inspect every linked +site. When either end or the code around it changes, reviewers are instantly aware of all affected +locations and thus can assess if every referenced safety requirement is still satisfied. Clippy can generate a diff-style report that pinpoints every location where changes to referenced HIR nodes occur between two commits or crate versions, enabling more focused code reviews. To From 3aa44b4292bb8f92a06185544d86d4e98390c088 Mon Sep 17 00:00:00 2001 From: zjp Date: Tue, 5 Aug 2025 09:24:03 +0800 Subject: [PATCH 33/41] add @deprecated disgnostics; major change to add or remove tags --- text/0000-safety-tags.md | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/text/0000-safety-tags.md b/text/0000-safety-tags.md index 826e4e1bbc5..d58efe237c5 100644 --- a/text/0000-safety-tags.md +++ b/text/0000-safety-tags.md @@ -265,13 +265,15 @@ Note that discharing a tag that is not defined will raise a hard error. Tags constitute a public API; therefore, any alteration to their definition must be evaluated against [Semantic Versioning][semver]. -* Adding a tag definition is a **minor** change. -* Removing a tag definition is a **major** change. Renaming a tag definition is a two-step - operation of removal and addition, bringing a major change due to removal. +* Adding a tag definition is a **major** change, because new tag is missing. +* Removing a tag definition is a **major** change, because the tag doesn't exist. +* Renaming a tag definition is a **major** change, because it's the result of removal and addition. +* Marking or cancelling `@deprecated` in tag definition description is a *minor* change, because + the tag is still able to be checked. [semver]: https://doc.rust-lang.org/cargo/reference/semver.html -To give dependent crates time to migrate an outdated tag definition, use `@deprecated` in tag +To grant dependent crates time to migrate an outdated tag definition, use `@deprecated` in tag description. Clippy will emit a deprecation warning whenever the tag is used in `safety::checked`. ```rust @@ -321,7 +323,8 @@ Procedure: otherwise emit a diagnostic. *(We intentionally stop at this simple rule; splitting complex unsafe expressions into separate annotated nodes is considered good style.)* 4. Make sure tags in `#[safety::checked]` correspond to their definitions. - 5. Diagnostics are emitted at the current Clippy lint level (warning or error). + 5. If tags marked `@deprecated` in definitions are successfully checked, emit diagnostics. + 6. Diagnostics are emitted at the current Clippy lint level (warning or error). [HIR ExprKind]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_hir/hir/enum.ExprKind.html From 27a7b0d68e69e386e18c63cbd96b8f7ab7affd3d Mon Sep 17 00:00:00 2001 From: zjp-CN Date: Tue, 5 Aug 2025 15:15:09 +0800 Subject: [PATCH 34/41] minor fix Co-authored-by: kennytm --- text/0000-safety-tags.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/text/0000-safety-tags.md b/text/0000-safety-tags.md index d58efe237c5..1f60a125089 100644 --- a/text/0000-safety-tags.md +++ b/text/0000-safety-tags.md @@ -285,7 +285,7 @@ unsafe fn deprecate_a_tag() {} // warning: Tag is deprecated, copy description to here. // error: NewTag is not discharged. -#[safety::requires { Tag }] +#[safety::checked { Tag }] unsafe { deprecate_a_tag() } ``` @@ -443,9 +443,9 @@ There are alternative discussion or Pre-RFCs on IRLO: Our proposed syntax looks closer to structured comments: ```rust -#[safety { - ValidPtr, Align, Initialized: "`self.head_tail()` returns two slices to live elements."; - NotOwned: "because we incremented..."; +#[safety::checked { + ValidPtr, Align, Initialized = "`self.head_tail()` returns two slices to live elements.", + NotOwned = "because we incremented...", }] unsafe { ptr::read(elem) } ``` From 5dffa30b514f27e00dc362e1a1854cfc65f8b073 Mon Sep 17 00:00:00 2001 From: zjp Date: Wed, 6 Aug 2025 14:15:51 +0800 Subject: [PATCH 35/41] remove `@deprecated`; swap the order on Versioned invariants and Semantic granularity --- text/0000-safety-tags.md | 45 ++++++++++++++-------------------------- 1 file changed, 15 insertions(+), 30 deletions(-) diff --git a/text/0000-safety-tags.md b/text/0000-safety-tags.md index 1f60a125089..1b48855a2fb 100644 --- a/text/0000-safety-tags.md +++ b/text/0000-safety-tags.md @@ -67,6 +67,11 @@ Even if the safety documentation is complete, two problems remain: - When *reviewing* the call, the absence of such a comment forces the auditor to reconstruct the required invariants from scratch, with no assurance that the author considered them at all. +## Safety Invariants Have No Semver + +A severe problem may arise if the safety requirements of an API change over time: downstream users +may be unaware of such changes and thus be exposed to security risks. + ## Granular Unsafe: How Small Is Too Small? The unsafe block faces a built-in tension: @@ -78,11 +83,6 @@ The unsafe block faces a built-in tension: [Alternatives from IRLO]: #IRLO -## Safety Invariants Have No Semver - -A severe problem may arise if the safety requirements of an API change over time: downstream users -may be unaware of such changes and thus be exposed to security risks. - ## Formal Contracts, Casual Burden [Contracts][contracts] excel at enforcing safety invariants rigorously, but they demand the @@ -97,18 +97,20 @@ ad-hoc practice with four concrete gains: 1. **Shared clarity**. Authors attach a short tag above every unsafe operation; reviewers instantly see which invariant must hold and where it is satisfied. -2. **Semantic granularity**. Tags must label a single unsafe call, or an expression contains single +2. **Versioned invariants**. Tags are a part of API; any change to their definition is a + *semver-breaking* API change, so safety invariants evolve explicitly. + +3. **Semantic granularity**. Tags must label a single unsafe call, or an expression contains single unsafe call. No longer constrained by the visual boundaries of `unsafe {}`. This sidesteps the precision vs completeness tension of unsafe blocks, and zeros in on real unsafe operations. + * It's viable to extend tagging to [more unsafe operations] beyond unsafe calls. * To enable truly semantic checking, we envision an [entity-reference] system that meticulously traces every unsafe related operation that could break an invariant in source code. -3. **Versioned invariants**. Tags are real items; any change to their definition is a - *semver-breaking* API change, so safety invariants evolve explicitly. - 4. **Lightweight checking**. Clippy only matches tag paths. No heavyweight formal proofs, keeping the system easy to adopt and understand. +[more unsafe operations]: #tagging-more-unsafe-ops [entity-reference]: #entity-reference ## `#[safety]` Tool Attribute and Namespace @@ -190,7 +192,7 @@ unsafe { ptr::read(ptr) } ``` ```rust -warning: `ValidPtr`, `Aligned`, `Initialized` tags are missing. Add them to `#[safety::checked]` or +warning: `ValidPtr`, `Aligned`, `Initialized` tags are missing. Add them to `#[safety::checked]` once these invariants are confirmed to be satisfied. --> file.rs:xxx:xxx | @@ -268,27 +270,9 @@ against [Semantic Versioning][semver]. * Adding a tag definition is a **major** change, because new tag is missing. * Removing a tag definition is a **major** change, because the tag doesn't exist. * Renaming a tag definition is a **major** change, because it's the result of removal and addition. -* Marking or cancelling `@deprecated` in tag definition description is a *minor* change, because - the tag is still able to be checked. [semver]: https://doc.rust-lang.org/cargo/reference/semver.html -To grant dependent crates time to migrate an outdated tag definition, use `@deprecated` in tag -description. Clippy will emit a deprecation warning whenever the tag is used in `safety::checked`. - -```rust -#[safety::requires { - NewTag = "description", - Tag = "@deprecated Explain why this tag is discouraged or what tag shoud be used instead", -}] -unsafe fn deprecate_a_tag() {} - -// warning: Tag is deprecated, copy description to here. -// error: NewTag is not discharged. -#[safety::checked { Tag }] -unsafe { deprecate_a_tag() } -``` - # Reference-level explanation [reference-level-explanation]: #reference-level-explanation @@ -323,8 +307,7 @@ Procedure: otherwise emit a diagnostic. *(We intentionally stop at this simple rule; splitting complex unsafe expressions into separate annotated nodes is considered good style.)* 4. Make sure tags in `#[safety::checked]` correspond to their definitions. - 5. If tags marked `@deprecated` in definitions are successfully checked, emit diagnostics. - 6. Diagnostics are emitted at the current Clippy lint level (warning or error). + 5. Diagnostics are emitted at the current Clippy lint level (warning or error). [HIR ExprKind]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_hir/hir/enum.ExprKind.html @@ -507,6 +490,8 @@ unsafe { bar(p) } ## Tagging More Unsafe Operations + + There are several other unsafe operations other than unsafe calls. We can extend safety tags in the following cases: From 6d0d064f4f8614f0cc3eaec2e23f21133ced555e Mon Sep 17 00:00:00 2001 From: zjp-CN Date: Wed, 6 Aug 2025 18:41:08 +0800 Subject: [PATCH 36/41] minor fix Co-authored-by: Julien Cretin --- text/0000-safety-tags.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/text/0000-safety-tags.md b/text/0000-safety-tags.md index 1b48855a2fb..a2939d9ad1b 100644 --- a/text/0000-safety-tags.md +++ b/text/0000-safety-tags.md @@ -107,7 +107,7 @@ ad-hoc practice with four concrete gains: * To enable truly semantic checking, we envision an [entity-reference] system that meticulously traces every unsafe related operation that could break an invariant in source code. -4. **Lightweight checking**. Clippy only matches tag paths. No heavyweight formal proofs, keeping +4. **Lightweight checking**. Clippy only matches tags. No heavyweight formal proofs, keeping the system easy to adopt and understand. [more unsafe operations]: #tagging-more-unsafe-ops From c73221b360dad19b59d12fc44739e3e1f74b97d1 Mon Sep 17 00:00:00 2001 From: zjp Date: Wed, 6 Aug 2025 20:04:41 +0800 Subject: [PATCH 37/41] clarify semver on definition change --- text/0000-safety-tags.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/text/0000-safety-tags.md b/text/0000-safety-tags.md index a2939d9ad1b..b6a8651c8fb 100644 --- a/text/0000-safety-tags.md +++ b/text/0000-safety-tags.md @@ -268,7 +268,13 @@ Note that discharing a tag that is not defined will raise a hard error. Tags constitute a public API; therefore, any alteration to their definition must be evaluated against [Semantic Versioning][semver]. * Adding a tag definition is a **major** change, because new tag is missing. -* Removing a tag definition is a **major** change, because the tag doesn't exist. +* Removing a tag definition is a **minor** change. The tag doesn't exist anymore, and discharing + an undefined tag just emits a warning-by-default diagnostic. +* Changing the definition of a tag in a way that *requires more*, is a **major** change, because + callsites only checked the weaker requirement for this tag. However, it's strongly discouraged to + change the tag in such way, as newly added requirements may blindly discharged by callsites. +* Changing the definition of a tag in an *equivalent* or in a way that *requires less* (the old tag + implies the new tag), is a **minor** change. * Renaming a tag definition is a **major** change, because it's the result of removal and addition. [semver]: https://doc.rust-lang.org/cargo/reference/semver.html From 21e54312487def28899ee05fb0e046ce4c246c2b Mon Sep 17 00:00:00 2001 From: zjp Date: Wed, 6 Aug 2025 20:59:25 +0800 Subject: [PATCH 38/41] Auto Generate Safety Docs from Tags; multiple attrs on the same node --- text/0000-safety-tags.md | 129 ++++++++++++++++++++++++--------------- 1 file changed, 80 insertions(+), 49 deletions(-) diff --git a/text/0000-safety-tags.md b/text/0000-safety-tags.md index b6a8651c8fb..f38be9d084a 100644 --- a/text/0000-safety-tags.md +++ b/text/0000-safety-tags.md @@ -279,6 +279,54 @@ against [Semantic Versioning][semver]. [semver]: https://doc.rust-lang.org/cargo/reference/semver.html +NOTE: +* `requires` or `checked` can be specified multiple times, and they will be merged together. + * Duplicate tags in `requires` will trigger errors. + * Duplicate tags in `checked` will trigger warning-by-default diagnostics. +* the scope of a tag is limited to the defined unsafe function, so identical tag name on different + unsafe functions won't affect with each other. + +## Auto Generate Safety Docs from Tags + +Since tag definitions duplicate safety comments, we propose `rustdoc` can recognize +`#[safety::requires]` attributes and render them into safety docs. + +For `ptr::read`, the existing comments are replaced with safety tags: + +```rust +/// # Safety +/// Behavior is undefined if any of the following conditions are violated: +/// * `src` must be [valid] for reads. +/// * `src` must be properly aligned. Use [`read_unaligned`] if this is not the case. +/// * `src` must point to a properly initialized value of type `T`. +/// # Examples +pub const unsafe fn read(src: *const T) -> T { ... } +``` + +```rust +/// # Safety +/// Behavior is undefined if any of the following conditions are violated: +#[safety::requires { + ValidPtr = "`src` must be [valid] for reads"; + Aligned = "`src` must be properly aligned. Use [`read_unaligned`] if this is not the case"; + Initialized = "`src` must point to a properly initialized value of type `T`" +}] +/// # Examples +pub const unsafe fn read(src: *const T) -> T { ... } +``` + +Each `Tag = "desc"` item is rendered as `` `Tag`: desc `` list item. + +```rust +/// # Safety +/// Behavior is undefined if any of the following conditions are violated: +/// * `ValidPtr`: `src` must be [valid] for reads. +/// * `Aligned`: `src` must be properly aligned. Use [`read_unaligned`] if this is not the case. +/// * `Initialized`: `src` must point to a properly initialized value of type `T`. +/// # Examples +pub const unsafe fn read(src: *const T) -> T { ... } +``` + # Reference-level explanation [reference-level-explanation]: #reference-level-explanation @@ -300,6 +348,7 @@ their call sites. To enable experimentation, a nightly-only library feature Procedure: 1. Validate `#[safety::requires]` only appears on unsafe functions if the attribute exists. + - Merge tags in multiple `requires` on the same function. Emit error if tag name duplicates. 2. Validate `#[safety::checked]` on HIR nodes whose `ExprKind` is one of - **direct unsafe nodes**: `Call`, `MethodCall` that invoke an unsafe function/method, or - **indirect unsafe nodes**: `Block` (unsafe), `Let`, `Assign`, `AssignOp`. @@ -312,8 +361,10 @@ Procedure: 3. Any node that carries `#[safety::checked]` must contain **exactly one** unsafe call/method; otherwise emit a diagnostic. *(We intentionally stop at this simple rule; splitting complex unsafe expressions into separate annotated nodes is considered good style.)* - 4. Make sure tags in `#[safety::checked]` correspond to their definitions. - 5. Diagnostics are emitted at the current Clippy lint level (warning or error). + 4. Merge tags in multiple `checked` on the same node. Emit a diagnostic if tag name duplicates. + 5. Make sure checked tags correspond to their definitions. Emit a diagnostic if the tag doesn't + have a definition on the call. + 6. Diagnostics are emitted at the current Clippy lint level (warning or error). [HIR ExprKind]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_hir/hir/enum.ExprKind.html @@ -328,6 +379,26 @@ documentation hover in `#[safety::checked]` as per tag definitions on unsafe cal Maybe some logics on safety tags like collecting tag definitions need to be extracted to a shared crate for both Clippy and Rust-Analyzer to use. +## Implementation in Rustdoc + +Treat `#[safety::requires]` tool attributes on unsafe functions as `#[doc]` attributes, and extract +tag names and definitions to render as item list: + +```rust +#[safety::requires(Tag1 = "definition1")] +#[safety::requires(Tag2 = "definition2")] +``` + +will be rendered if in markdown syntax + +```md +* `Tag1`: definition1 +* `Tag2`: definition2 +``` + +It'd be good if tag names have a special css class like background color to be attractive. Tag +styling is not required in this RFC, and can be implemented later as an improvement. + # Drawbacks [drawbacks]: #drawbacks @@ -358,12 +429,17 @@ to implement them. be tied to internal APIs and specific toolchains. Extending Rust-Analyzer is therefore the only practical way to give users first-class IDE support. +4. Avoid safety comment duplication. Tag definitions and safety requirements share identical prose, + so we only need one way to render them. Generating safety docs through safety tags prevents + verbosity and inconsistency. + We therefore seek approvals from the following teams: 1. **Library team** – to allow the tagging of unsafe operations and to expose tag items as public APIs. -2. **Clippy team** – to integrate tag checking into the linter. -3. **Rust-Analyzer team** – to add IDE support for tags. +2. **Clippy team** – to integrate tag checking into the linter. +3. **Rust-Analyzer team** – to add IDE support for tags. +3. **Rustdoc team** – to render tags to docs. 4. **Compiler team** – to reserve the `safety` namespace and gate the feature via `#![feature(safety_tags)]` for the namespace and tag APIs in standard libraries. @@ -521,51 +597,6 @@ struct, enum, or union is neither needed nor permitted. # Future possibilities [future-possibilities]: #future-possibilities -## Better Rustdoc Rendering - -Because tags are surfaced as a part of API, rustdoc can render documentation of tags by displaying -each tag name, its optional description, and possible deprecated state below the tagged function or -other unsafe item. - -## Generate Safety Docs from Tags - -We can take structured safety comments one step further by turning the explanatory prose into -explicit tag reasons. - -For `ptr::read`, the existing comments are replaced with safety tags: - -```rust -/// * `src` must be [valid] for reads. -/// * `src` must be properly aligned. Use [`read_unaligned`] if this is not the case. -/// * `src` must point to a properly initialized value of type `T`. -pub const unsafe fn read(src: *const T) -> T { ... } -``` - -```rust -#[safety { - ValidPtr = "`src` must be [valid] for reads"; - Aligned = "`src` must be properly aligned. Use [`read_unaligned`] if this is not the case"; - Initialized = "`src` must point to a properly initialized value of type `T`" -}] -pub const unsafe fn read(src: *const T) -> T { ... } -``` - -`#[safety]` becomes a procedural macro that expands to both `#[doc]` attributes and the -`#[safety::requires]` attribute. - -```rust -/// # Safety -/// -/// - ValidPtr: `src` must be [valid] for reads -/// - Aligned: `src` must be properly aligned. Use [`read_unaligned`] if this is not the case -/// - Initialized: `src` must point to a properly initialized value of type `T` -#[safety::requires { ValidPtr = "...", Aligned = "...", Initialized = "..." }] -pub const unsafe fn read(src: *const T) -> T { ... } -``` - -With support for tag arguments, safety documentation can be made more precise and contextual by -dynamically injecting the argument values into the reason strings. - ## Discharge One Tag from `any = { Option1, Option2 }` Sometimes it’s useful to declare a set of safety tags on an unsafe function while discharging only From 2fb3d0c691f49b988602a00b5f439a404e38c4b7 Mon Sep 17 00:00:00 2001 From: zjp Date: Fri, 8 Aug 2025 10:26:49 +0800 Subject: [PATCH 39/41] minor fixes and clarification on semver --- text/0000-safety-tags.md | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/text/0000-safety-tags.md b/text/0000-safety-tags.md index f38be9d084a..07708a30098 100644 --- a/text/0000-safety-tags.md +++ b/text/0000-safety-tags.md @@ -15,7 +15,7 @@ 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. -[compiles]: https://play.rust-lang.org/?version=nightly&mode=debug&edition=2024&gist=6eb0e47c416953da1f2470b11417e69a +[compiles]: https://play.rust-lang.org/?version=nightly&mode=debug&edition=2024&gist=322dbd93610aca05db49382802c732c3 ```rust #[safety::requires { // 💡 define safety tags on an unsafe function @@ -100,9 +100,9 @@ ad-hoc practice with four concrete gains: 2. **Versioned invariants**. Tags are a part of API; any change to their definition is a *semver-breaking* API change, so safety invariants evolve explicitly. -3. **Semantic granularity**. Tags must label a single unsafe call, or an expression contains single - unsafe call. No longer constrained by the visual boundaries of `unsafe {}`. This sidesteps the - precision vs completeness tension of unsafe blocks, and zeros in on real unsafe operations. +3. **Semantic granularity**. Tags must label a single unsafe call, or an expression containing + single unsafe call. No longer constrained by the visual boundaries of `unsafe {}`. This sidesteps + the precision vs completeness tension of unsafe blocks, and zeros in on real unsafe operations. * It's viable to extend tagging to [more unsafe operations] beyond unsafe calls. * To enable truly semantic checking, we envision an [entity-reference] system that meticulously traces every unsafe related operation that could break an invariant in source code. @@ -220,7 +220,7 @@ unsafe fn propogation(ptr: *const T) -> T { Tags defined on an unsafe function must be **fully** discharged at callsites. No partial discharge: ```rust -#[safety::requires { ValidPtr = "...", Aligned = "...", Initialized = "..." }] +#[safety::requires { ValidPtr = "...", Initialized = "..." }] unsafe fn delegation(ptr: *const T) -> T { #[safety::checked { Aligned }] // 💥 Error: Tags are not fully discharged. unsafe { read(ptr) } @@ -270,12 +270,18 @@ against [Semantic Versioning][semver]. * Adding a tag definition is a **major** change, because new tag is missing. * Removing a tag definition is a **minor** change. The tag doesn't exist anymore, and discharing an undefined tag just emits a warning-by-default diagnostic. -* Changing the definition of a tag in a way that *requires more*, is a **major** change, because - callsites only checked the weaker requirement for this tag. However, it's strongly discouraged to - change the tag in such way, as newly added requirements may blindly discharged by callsites. +* Renaming a tag definition is a **major** change, because it's the result of removal and addition. * Changing the definition of a tag in an *equivalent* or in a way that *requires less* (the old tag implies the new tag), is a **minor** change. -* Renaming a tag definition is a **major** change, because it's the result of removal and addition. +* Changing the definition of a tag in a way that *requires more*, is a **major** change, because + callsites only checked the weaker requirement for this tag. + * However, adding more safety requirements to an existing tag definition is strongly discouraged: + call sites that were blindly compiled against the old definition may unsoundly assume the new, + weaker requirements still hold. + * Instead, replace the tag with a distinct name. This guarantees downstream crates notice the + change. It's a potential hazard to reuse the tag name back in the future, due to the same reason + stated above. Renaming the unsafe function to keep the original tag name for new definition is + also good, because tags are scoped to their defining function. [semver]: https://doc.rust-lang.org/cargo/reference/semver.html From bfb6304fa2caeb9f03cf209891c57fb15bb34168 Mon Sep 17 00:00:00 2001 From: zjp Date: Fri, 8 Aug 2025 10:34:05 +0800 Subject: [PATCH 40/41] minor fixes: discharge of undefined tags warns by default --- text/0000-safety-tags.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/text/0000-safety-tags.md b/text/0000-safety-tags.md index 07708a30098..563e766567d 100644 --- a/text/0000-safety-tags.md +++ b/text/0000-safety-tags.md @@ -250,18 +250,18 @@ invariants, and define the new tag on `delegation` function. This practice exten delegation of multiple tag discharges: ```rust -#[safety::requires { MyInvaraint = "Invariants of A and C, but could be a more contextual name." }] +#[safety::requires { MyInvariant = "Invariants of A and C, but could be a more contextual name." }] unsafe fn delegation() { unsafe { - #[safety::checked { A = "delegated to the caller's MyInvaraint", B }] + #[safety::checked { A = "delegated to the caller's MyInvariant", B }] foo(); - #[safety::checked { C = "delegated to the caller's MyInvaraint", D }] + #[safety::checked { C = "delegated to the caller's MyInvariant", D }] bar(); } } ``` -Note that discharing a tag that is not defined will raise a hard error. +Note that discharing a tag that is not defined will raise a warning-by-default lint. ## Safety Tags are a Part of an Unsafe Function @@ -289,7 +289,7 @@ NOTE: * `requires` or `checked` can be specified multiple times, and they will be merged together. * Duplicate tags in `requires` will trigger errors. * Duplicate tags in `checked` will trigger warning-by-default diagnostics. -* the scope of a tag is limited to the defined unsafe function, so identical tag name on different +* the scope of a tag is limited to the defining unsafe function, so identical tag name on different unsafe functions won't affect with each other. ## Auto Generate Safety Docs from Tags From 0405d98a08ecdd60b74f53557f60af36c46a0a4d Mon Sep 17 00:00:00 2001 From: zjp Date: Fri, 8 Aug 2025 20:42:58 +0800 Subject: [PATCH 41/41] minor fix --- text/0000-safety-tags.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/text/0000-safety-tags.md b/text/0000-safety-tags.md index 563e766567d..c19b433632c 100644 --- a/text/0000-safety-tags.md +++ b/text/0000-safety-tags.md @@ -91,6 +91,8 @@ precision as well as overhead of formal verification, making them too heavy for # Guide-level explanation [guide-level-explanation]: #guide-level-explanation +## From Motivation to Solution: Bridging the Identified Gaps + We propose **checkable safety comments** via Clippy’s new **safety-tag** system, addressing today’s ad-hoc practice with four concrete gains: @@ -103,7 +105,7 @@ ad-hoc practice with four concrete gains: 3. **Semantic granularity**. Tags must label a single unsafe call, or an expression containing single unsafe call. No longer constrained by the visual boundaries of `unsafe {}`. This sidesteps the precision vs completeness tension of unsafe blocks, and zeros in on real unsafe operations. - * It's viable to extend tagging to [more unsafe operations] beyond unsafe calls. + * It's viable to extend tags to [more unsafe operations] beyond unsafe calls. * To enable truly semantic checking, we envision an [entity-reference] system that meticulously traces every unsafe related operation that could break an invariant in source code. @@ -290,14 +292,14 @@ NOTE: * Duplicate tags in `requires` will trigger errors. * Duplicate tags in `checked` will trigger warning-by-default diagnostics. * the scope of a tag is limited to the defining unsafe function, so identical tag name on different - unsafe functions won't affect with each other. + unsafe functions won't affect each other. ## Auto Generate Safety Docs from Tags Since tag definitions duplicate safety comments, we propose `rustdoc` can recognize `#[safety::requires]` attributes and render them into safety docs. -For `ptr::read`, the existing comments are replaced with safety tags: +For `ptr::read`, replace the existing comments with safety tags: ```rust /// # Safety