Skip to content

Commit 9e58e07

Browse files
Overhaul of "Behavior not considered unsafe"
Give this section of the reference a new paint job :3 Note: I’m not entirely sure if this section should even exist permanently. It doesn’t really fit in with strictly defining Rust’s behavior as such, since it describes lots of legal behavior that is simply surprising to users. It may fit better in the nomicon or a later part of the book. On the other hand, neither of those would suffer from the improvements made here. - Slightly rename the section to clarify that we refer to generally undesirable behavior. - Adopt language to authoritative spec language (do not use we, do not refer to implementations, use as specific of a language as possible) - Link to standard library - Link to applicable other sections - Use admonitions for existing text where applicable - Insert rule identifiers everywhere - Write a complete section for deadlocks, including a deadlocking code example - Expand the logic bug example with Hash and Eq into a full code example
1 parent 2d9827f commit 9e58e07

File tree

3 files changed

+103
-41
lines changed

3 files changed

+103
-41
lines changed

src/SUMMARY.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@
123123
- [Unsafety](unsafety.md)
124124
- [The `unsafe` keyword](unsafe-keyword.md)
125125
- [Behavior considered undefined](behavior-considered-undefined.md)
126-
- [Behavior not considered unsafe](behavior-not-considered-unsafe.md)
126+
- [Undesirable behavior not considered unsafe](behavior-not-considered-unsafe.md)
127127

128128
- [Constant Evaluation](const_eval.md)
129129

Lines changed: 98 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,115 @@
1-
# Behavior not considered `unsafe`
1+
r[not-unsafe]
2+
# Undesirable behavior not considered `unsafe`
23

3-
The Rust compiler does not consider the following behaviors _unsafe_,
4-
though a programmer may (should) find them undesirable, unexpected,
5-
or erroneous.
4+
> [!NOTE]
5+
> This section is a non-exhaustive description of undesirable safe behavior. By nature, it cannot encompass all possible undesired safe behavior and does not claim to do so. Any behavior not defined as [unsafe] or [undefined] is safe by definition.
66
7-
- Deadlocks
7+
Rust does not consider the following behaviors _[unsafe]_, though a programmer may (or should) find them undesirable, unexpected, or erroneous.
8+
9+
r[not-unsafe.resource-leaks]
810
- Leaks of memory and other resources
11+
r[not-unsafe.abort]
912
- Exiting without calling destructors
10-
- Exposing randomized base addresses through pointer leaks
13+
r[not-unsafe.aslr-bypass]
14+
- Exposing randomized executable base addresses through pointer leaks
15+
16+
r[not-unsafe.deadlocks]
17+
## Deadlocks and livelocks
18+
19+
Deadlocks occur when certain tasks cannot proceed executing as they are waiting on at least one other resource held by another task. In the simplest case, two tasks 1 and 2 will deadlock when task 1 holds lock A, task 2 holds lock B, task 1 is waiting to acquire lock B, and task 2 is waiting to acquire lock A. Since both are requesting the lock that the other is holding, none can make any progress and they are deadlocked.
20+
21+
<!-- no_run: this program intentionally deadlocks and therefore does not terminate -->
22+
23+
```rust,no_run
24+
# use std::sync::{Arc, Mutex};
25+
# use std::thread;
26+
# use std::time::Duration;
27+
#
28+
# #[allow(unused)]
29+
# fn main() {
30+
let lock_a = Arc::new(Mutex::new(()));
31+
let lock_b = Arc::new(Mutex::new(()));
32+
33+
let task1 = {
34+
# let lock_a = lock_a.clone();
35+
# let lock_b = lock_b.clone();
36+
thread::spawn(move || {
37+
let obtained_lock_a = lock_a.lock().unwrap();
38+
// Give process 2 some time to lock B.
39+
thread::sleep(Duration::from_secs(1));
40+
let obtained_lock_b = lock_b.lock().unwrap();
41+
})
42+
};
1143
44+
let task2 = {
45+
# let lock_a = lock_a.clone();
46+
# let lock_b = lock_b.clone();
47+
thread::spawn(move || {
48+
let obtained_lock_b = lock_b.lock().unwrap();
49+
thread::sleep(Duration::from_secs(1));
50+
let obtained_lock_a = lock_a.lock().unwrap();
51+
})
52+
};
53+
54+
// Neither of these calls will ever return due to the deadlock.
55+
task1.join();
56+
task2.join();
57+
# }
58+
```
59+
60+
In general, determining whether a program has deadlocked requires to solve the [Halting problem], which is impossible. Even though many instances of deadlocks can be detected automatically, doing so is not always practical. Regardless, many multitasking systems, and especially async frameworks such as [tokio], provide good deadlock detection capabilities.
61+
62+
r[not-unsafe.livelocks]
63+
Livelocks are a related issue where no real progress is made in a group of tasks, yet they technically continue to run. For instance, using non-blocking synchronization primitives like spinlocks or atomic variables can quickly lead to livelocks. This is in opposition to deadlocks, where tasks are blocked on resource acquisition, which is relatively easy to discern. Therefore, livelocks are much harder to detect than deadlocks, but equally undesirable.
64+
65+
r[not-unsafe.integer-overflow]
1266
## Integer overflow
1367

14-
If a program contains arithmetic overflow, the programmer has made an
15-
error. In the following discussion, we maintain a distinction between
16-
arithmetic overflow and wrapping arithmetic. The first is erroneous,
17-
while the second is intentional.
68+
If a program contains arithmetic overflow, the programmer has made an error. There is a distinction between arithmetic overflow and _wrapping arithmetic_. The first is erroneous, while the second is intentional.
1869

19-
When the programmer has enabled `debug_assert!` assertions (for
20-
example, by enabling a non-optimized build), implementations must
21-
insert dynamic checks that `panic` on overflow. Other kinds of builds
22-
may result in `panics` or silently wrapped values on overflow, at the
23-
implementation's discretion.
70+
r[not-unsafe.integer-overflow.panic]
71+
When the configuration option [debug_assertions] is enabled (for example, by enabling a non-optimized build), dynamic checks are inserted that `panic` on overflow.
2472

25-
In the case of implicitly-wrapped overflow, implementations must
26-
provide well-defined (even if still considered erroneous) results by
27-
using two's complement overflow conventions.
73+
r[not-unsafe.integer-overflow.silent-wrapping]
74+
Other kinds of builds may result in [panic]s or silently wrapped values on overflow. In the case of implicitly-wrapped overflow, the results are well-defined (even if still considered erroneous) by using two's complement overflow conventions.
2875

29-
The integral types provide inherent methods to allow programmers
30-
explicitly to perform wrapping arithmetic. For example,
31-
`i32::wrapping_add` provides two's complement, wrapping addition.
76+
r[not-unsafe.integer-overflow.intentional-wrapping]
77+
The [integer types] provide inherent methods to allow explicitly performing wrapping arithmetic. For example, [`i32::wrapping_add`] provides two's complement, wrapping addition for 32-bit signed integers.
3278

33-
The standard library also provides a `Wrapping<T>` newtype which
34-
ensures all standard arithmetic operations for `T` have wrapping
35-
semantics.
79+
The standard library also provides a [`Wrapping<T>`](`core::num::Wrapping<T>`) newtype which ensures all standard arithmetic operations for `T` have wrapping semantics.
3680

37-
See [RFC 560] for error conditions, rationale, and more details about
38-
integer overflow.
81+
> [!NOTE]
82+
> See [RFC 560] for error conditions, rationale, and more details about integer overflow.
3983
84+
r[not-unsafe.logic]
4085
## Logic errors
4186

42-
Safe code may impose extra logical constraints that can be checked
43-
at neither compile-time nor runtime. If a program breaks such
44-
a constraint, the behavior may be unspecified but will not result in
45-
undefined behavior. This could include panics, incorrect results,
46-
aborts, and non-termination. The behavior may also differ between
47-
runs, builds, or kinds of build.
48-
49-
For example, implementing both `Hash` and `Eq` requires that values
50-
considered equal have equal hashes. Another example are data structures
51-
like `BinaryHeap`, `BTreeMap`, `BTreeSet`, `HashMap` and `HashSet`
52-
which describe constraints on the modification of their keys while
53-
they are in the data structure. Violating such constraints is not
54-
considered unsafe, yet the program is considered erroneous and
55-
its behavior unpredictable.
87+
Safe code may impose extra logical constraints that can be checked at neither compile-time nor runtime. If a program breaks such a constraint, the behavior may be _unspecified_ but will not result in [undefined] behavior. This could include [panic]s, incorrect results, aborts, and non-termination. The behavior may also differ between runs, builds, or kinds of build.
88+
89+
> [!EXAMPLE]
90+
> Implementing both [`Hash`](`core::hash::Hash`) and [`Eq`] requires that values considered equal have equal hashes. This promise is broken in the following code, and using `Wrapper` in types like [`HashMap`](`std::collections::HashMap`) will lead to unexpected behavior.
91+
>
92+
> <!-- no_run: exposing unpredictable HashMap behavior reliably (and in an understandable way) is hard -->
93+
>
94+
> ```rust,no_run
95+
> use std::hash::{Hash, Hasher};
96+
>
97+
> #[derive(PartialEq, Eq)]
98+
> struct Wrapper(i32);
99+
>
100+
> impl Hash for Wrapper {
101+
> fn hash<H>(&self, hasher: &mut H) where H: Hasher {
102+
> Hash::hash(&0i32, hasher);
103+
> }
104+
> }
105+
> ```
106+
107+
Another example are data structures like [`BinaryHeap`](`alloc::collections::binary_heap::BinaryHeap`), [`BTreeMap`](`alloc::collections::btree_map::BTreeMap`), [`BTreeSet`](`alloc::collections::btree_set::BTreeSet`), [`HashMap`](`std::collections::HashMap`), and [`HashSet`](`std::collections::HashSet`), which describe constraints on the modification of their keys while they are in the data structure. Violating such constraints is not considered unsafe, yet the program is considered erroneous and its behavior unpredictable.
56108
57109
[RFC 560]: https://github.com/rust-lang/rfcs/blob/master/text/0560-integer-overflow.md
110+
[unsafe]: safety.unsafe-ops
111+
[undefined]: undefined
112+
[debug_assertions]: cfg.debug_assertions
113+
[integer types]: type.numeric.int
114+
[Halting problem]: https://en.wikipedia.org/wiki/Halting_problem
115+
[tokio]: https://tokio.rs/

src/unsafety.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ r[safety.unsafe-extern]
3333
r[safety.unsafe-attribute]
3434
- Applying an [unsafe attribute] to an item.
3535

36+
> [!NOTE]
37+
> A lot of undesirable behavior is [not considered unsafe].
38+
3639
[^extern-2024]: Prior to the 2024 edition, extern blocks were allowed to be declared without `unsafe`.
3740

3841
[`extern`]: items/external-blocks.md
@@ -42,3 +45,4 @@ r[safety.unsafe-attribute]
4245
[raw pointer]: types/pointer.md
4346
[unsafe trait]: items/traits.md#unsafe-traits
4447
[unsafe attribute]: attributes.md
48+
[not considered unsafe]: not-unsafe

0 commit comments

Comments
 (0)