Skip to content

Commit 55a275c

Browse files
committed
Merge RFC 3325: Unsafe attributes
The FCP for RFC 3325 completed on 2024-04-04 with a disposition to merge. Let's merge it.
2 parents a57b4bc + 5d148ba commit 55a275c

File tree

1 file changed

+208
-0
lines changed

1 file changed

+208
-0
lines changed

text/3325-unsafe-attributes.md

+208
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
- Feature Name: `unsafe_attributes`
2+
- Start Date: 2022-10-11
3+
- RFC PR: [rust-lang/rfcs#3325](https://github.com/rust-lang/rfcs/pull/3325)
4+
- Tracking Issue: [rust-lang/rust#123757](https://github.com/rust-lang/rust/issues/123757)
5+
6+
# Summary
7+
[summary]: #summary
8+
9+
Consider some attributes 'unsafe', so that they must only be used like this:
10+
11+
```rust
12+
#[unsafe(no_mangle)]
13+
```
14+
15+
# Motivation
16+
[motivation]: #motivation
17+
18+
Some of our attributes, such as `no_mangle`, can be used to
19+
[cause Undefined Behavior without any `unsafe` block](https://github.com/rust-lang/rust/issues/28179).
20+
If this was regular code we would require them to be placed in an `unsafe {}`
21+
block, but since they are attributes that makes less sense. Hence we need a
22+
concept of 'unsafe attributes' and accompanying syntax to declare that one is
23+
aware of the UB risks here (and it might be good to add a SAFETY comment
24+
explaining why this use of the attribute is fine).
25+
26+
# Guide-level explanation
27+
[guide-level-explanation]: #guide-level-explanation
28+
29+
*Example explanation for `no_mangle`; the other attributes need something similar.*
30+
31+
When declaring a function like this
32+
33+
```rust
34+
#[no_mangle]
35+
pub fn write(...) { ... }
36+
```
37+
38+
this will cause Rust to generate a globally visible function with the
39+
linker/export name `write`. As consequence of that, other code that wants to
40+
call the
41+
[POSIX `write` function](https://pubs.opengroup.org/onlinepubs/9699919799/functions/write.html) might
42+
end up calling this other `write` instead. This can easily lead to Undefined
43+
Behavior:
44+
- The other `write` might have the wrong signature, so arguments are passed
45+
incorrectly.
46+
- The other `write` might not have the expected behavior of
47+
[write](https://man7.org/linux/man-pages/man2/write.2.html), causing code
48+
relying on this behavior to misbehave.
49+
50+
To avoid this, when declaring a function `no_mangle`, it is important that the
51+
name of the function does not clash with other globally named functions. Similar
52+
to how `unsafe { ... }` blocks are used to acknowledge that this code is
53+
dangerous and needs manual checking, `unsafe(no_mangle)` acknowledges that
54+
`no_mangle` is dangerous and needs to be manually checked for correctness:
55+
56+
```rust
57+
// SAFETY: there is no other global function of this name
58+
#[unsafe(no_mangle)]
59+
pub fn my_own_write(...) { ... }
60+
```
61+
62+
Note that when writing a library crate, it is in general not possible to make
63+
claims like "there is no other global function of this name". This is a
64+
fundamental limitation of the global linking namespace, and not something Rust
65+
currently is able to overcome. Libraries that make such assumptions should
66+
ideally document somewhere publicly that they consider some namespace, i.e.
67+
every function starting with `_mycrate__`, to be reserved for their exclusive
68+
use.
69+
70+
# Reference-level explanation
71+
[reference-level-explanation]: #reference-level-explanation
72+
73+
Some attributes (e.g. `no_mangle`, `export_name`, `link_section` -- see
74+
[here](https://github.com/rust-lang/rust/issues/82499) for a more complete list)
75+
are considered "unsafe" attributes. An unsafe attribute must only be used inside
76+
`unsafe(...)` in the attribute declaration, like
77+
78+
```rust
79+
#[unsafe(no_mangle)]
80+
```
81+
82+
For backwards compatibility reasons, using these attributes outside of
83+
`unsafe(...)` is just a lint, not a hard error. The lint is called
84+
`unsafe_attr_outside_unsafe`. Initially, this lint will be allow-by-default.
85+
Unsafe attributes that are added in the future can hard-require `unsafe` from
86+
the start since the backwards compatibility concern does not apply to them.
87+
The 2024 edition is also expected to increase the severity of this lint,
88+
possibly even making it a hard error.
89+
90+
Syntactically, for each unsafe attribute `attr`, we now also accept
91+
`unsafe(attr)` anywhere that `attr` can be used (in particular, inside
92+
`cfg_attr`). `unsafe` cannot be nested, cannot contain `cfg_attr`, and cannot
93+
contain any other (non-unsafe) attributes. Only a single attribute can be used
94+
inside `unsafe`, i.e., `unsafe(foo, bar)` is invalid.
95+
96+
The `deny(unsafe_code)` lint denies the use of unsafe attributes both inside and
97+
outside of `unsafe(...)` blocks. (That lint currently has special handling to
98+
deny these attributes. Once there is a general notion of 'unsafe attributes' as
99+
proposed by this RFC, that special handling should no longer be needed.)
100+
101+
The `unsafe(...)` attribute block is required even for functions declared inside
102+
an `unsafe` block. That is, the following is an error:
103+
104+
```rust
105+
fn outer() {
106+
unsafe {
107+
#[no_mangle]
108+
fn write() {}
109+
}
110+
}
111+
```
112+
113+
This matches the fact that expression-level unsafety is not inherited for items
114+
declared inside other items.
115+
116+
# Drawbacks
117+
[drawbacks]: #drawbacks
118+
119+
I think if we had thought of this around Rust 1.0, then this would be rather
120+
uncontroversial. As things stand now, this proposal will cause a lot of churn
121+
since all existing uses of these unsafe attributes need to be adjusted. The
122+
warning for using unsafe attributes outside `unsafe(...)` should probably have
123+
an auto-fix available to help ease the transition here.
124+
125+
# Rationale and alternatives
126+
[rationale-and-alternatives]: #rationale-and-alternatives
127+
128+
- **Nothing.** We could do nothing at all, and live with the status quo. However
129+
then we will not be able to fix issues like
130+
[`no_mangle` being unsound](https://github.com/rust-lang/rust/issues/28179),
131+
which is one of the oldest open soundness issues.
132+
- **Rename.** We could just rename the attributes to `unsafe_no_mangle` etc.
133+
However that is inconsistent with how we approach `unsafe` on expressions, and
134+
feels much less systematic and much more ad-hoc.
135+
- **`deny(unsafe_code)`.** We already
136+
[started the process](https://github.com/rust-lang/rust/issues/82499) of
137+
rejecting these attributes when `deny(unsafe_code)` is used. We could say that
138+
is enough. However the RFC authors thinks that is insufficient, since only few
139+
crates use that lint, and since it is the wrong default for Rust (users have
140+
to opt-in to a soundness-critical diagnostic -- that's totally against the
141+
"safety by default" goal of Rust). This RFC says that yes, `deny(unsafe_code)`
142+
should deny those attributes, but we should go further and require an explicit
143+
`unsafe(...)` attribute block for them to be used at all.
144+
- **Item-level unsafe blocks.** We could find some way to have 'unsafe blocks'
145+
around entire functions or modules. However, those would go against the usual
146+
goal of keeping `unsafe` blocks small. Big `unsafe` blocks risk accidentally
147+
calling an unsafe operation in there without even realizing it.
148+
- **Other syntax.** Obviously we could pick a different syntax for the same
149+
concept, but this seems like the most natural marriage of the idea of unsafe
150+
blocks from regular code, and the existing attributes syntax.
151+
152+
# Prior art
153+
[prior-art]: #prior-art
154+
155+
We have `unsafe` blocks; this is basically the same thing for the "attributes
156+
DSL".
157+
158+
In the attribute DSL, we already have a "nesting" construct: `cfg_attr`. That
159+
allows terms like
160+
`#[cfg_attr(debug_assertions, deny(unsafe_code), allow(unused))]`, so there is
161+
precedent for having a list of attributes inside a single attribute.
162+
163+
I don't know of other languages that would distinguish safe and unsafe
164+
attributes.
165+
166+
# Unresolved questions
167+
[unresolved-questions]: #unresolved-questions
168+
169+
- **Different lint staging.** The lint on using existing unsafe attributes like
170+
`no_mangle` outside `unsafe(...)` could be staged in various ways: it could be
171+
warn-by-default to start or we wait a while before to do that, it could be
172+
edition-dependent, it might eventually be deny-by-default or even a hard error
173+
on some editions -- there are lots of details here, which can be determined
174+
later during the process.
175+
176+
# Future possibilities
177+
[future-possibilities]: #future-possibilities
178+
179+
- **Unsafe attribute proc macros.** We could imagine something like
180+
```
181+
#[proc_macro_attribute(require_unsafe)]
182+
fn spoopy(args: TokenStream, input: TokenStream) -> TokenStream {…}
183+
```
184+
to declare that an attribute proc macro is unsafe to use, and must only
185+
occur as an unsafe macro. Such an unsafe-to-use attribute proc macro must
186+
declare in a comment what its safety requirements are. (This is the `unsafe`
187+
from `unsafe fn`, whereas the rest of the RFC is using the `unsafe` from
188+
`unsafe { ... }`.)
189+
- **Unsafe derive.** We could use `#[unsafe(derive(Trait))]` to derive an
190+
`unsafe impl` where the deriving macro itself cannot check all required safety
191+
conditions (i.e., this is 'unsafe to derive').
192+
- **Unsafe tool attributes.** Same as above, but for tool attributes.
193+
- **Unsafe attributes on statements.** For now, the only unsafe attributes we
194+
have don't make sense on the statement level. Once we do have unsafe statement
195+
attributes, we need to figure out whether inside `unsafe {}` blocks one still
196+
needs to also write `unsafe(...)`.
197+
- **Lists and nesting.** We could specify that `unsafe(...)` may contain a list
198+
of arbitrary attributes (including safe ones), may be nested, and may contain
199+
`cfg_attr` that gets expanded appropriately. However that could make it tricky
200+
to consistently support non-builtin unsafe attributes in the future, so the
201+
RFC proposes to not do that yet. The current approach is forward-compatible
202+
with allowing lists and nesting in the future.
203+
- **Unsafe crates.** Some attributes' requirements cannot be fully discharged
204+
locally. For instance, if a lib crate uses `no_mangle`, this really puts a
205+
burden on *the author of the final binary* to ensure that the symbol dos not
206+
conflict. In the future it would be better if rust tooling could automatically
207+
surface a such requirements to downstream code, for example by an automatic
208+
"unsafe attributes used" listing in a crate's generated rustdoc.

0 commit comments

Comments
 (0)