From 9e58e076717deef865fcf69d4f44b6c7a29048bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?kleines=20Filmr=C3=B6llchen?= Date: Fri, 18 Jul 2025 00:29:17 +0200 Subject: [PATCH 1/2] Overhaul of "Behavior not considered unsafe" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/SUMMARY.md | 2 +- src/behavior-not-considered-unsafe.md | 138 ++++++++++++++++++-------- src/unsafety.md | 4 + 3 files changed, 103 insertions(+), 41 deletions(-) diff --git a/src/SUMMARY.md b/src/SUMMARY.md index cbee8c018..f47b342fa 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -123,7 +123,7 @@ - [Unsafety](unsafety.md) - [The `unsafe` keyword](unsafe-keyword.md) - [Behavior considered undefined](behavior-considered-undefined.md) - - [Behavior not considered unsafe](behavior-not-considered-unsafe.md) + - [Undesirable behavior not considered unsafe](behavior-not-considered-unsafe.md) - [Constant Evaluation](const_eval.md) diff --git a/src/behavior-not-considered-unsafe.md b/src/behavior-not-considered-unsafe.md index 94b7d57de..9ef0ac06a 100644 --- a/src/behavior-not-considered-unsafe.md +++ b/src/behavior-not-considered-unsafe.md @@ -1,57 +1,115 @@ -# Behavior not considered `unsafe` +r[not-unsafe] +# Undesirable behavior not considered `unsafe` -The Rust compiler does not consider the following behaviors _unsafe_, -though a programmer may (should) find them undesirable, unexpected, -or erroneous. +> [!NOTE] +> 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. -- Deadlocks +Rust does not consider the following behaviors _[unsafe]_, though a programmer may (or should) find them undesirable, unexpected, or erroneous. + +r[not-unsafe.resource-leaks] - Leaks of memory and other resources +r[not-unsafe.abort] - Exiting without calling destructors -- Exposing randomized base addresses through pointer leaks +r[not-unsafe.aslr-bypass] +- Exposing randomized executable base addresses through pointer leaks + +r[not-unsafe.deadlocks] +## Deadlocks and livelocks + +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. + + + +```rust,no_run +# use std::sync::{Arc, Mutex}; +# use std::thread; +# use std::time::Duration; +# +# #[allow(unused)] +# fn main() { +let lock_a = Arc::new(Mutex::new(())); +let lock_b = Arc::new(Mutex::new(())); + +let task1 = { +# let lock_a = lock_a.clone(); +# let lock_b = lock_b.clone(); + thread::spawn(move || { + let obtained_lock_a = lock_a.lock().unwrap(); + // Give process 2 some time to lock B. + thread::sleep(Duration::from_secs(1)); + let obtained_lock_b = lock_b.lock().unwrap(); + }) +}; +let task2 = { +# let lock_a = lock_a.clone(); +# let lock_b = lock_b.clone(); + thread::spawn(move || { + let obtained_lock_b = lock_b.lock().unwrap(); + thread::sleep(Duration::from_secs(1)); + let obtained_lock_a = lock_a.lock().unwrap(); + }) +}; + +// Neither of these calls will ever return due to the deadlock. +task1.join(); +task2.join(); +# } +``` + +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. + +r[not-unsafe.livelocks] +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. + +r[not-unsafe.integer-overflow] ## Integer overflow -If a program contains arithmetic overflow, the programmer has made an -error. In the following discussion, we maintain a distinction between -arithmetic overflow and wrapping arithmetic. The first is erroneous, -while the second is intentional. +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. -When the programmer has enabled `debug_assert!` assertions (for -example, by enabling a non-optimized build), implementations must -insert dynamic checks that `panic` on overflow. Other kinds of builds -may result in `panics` or silently wrapped values on overflow, at the -implementation's discretion. +r[not-unsafe.integer-overflow.panic] +When the configuration option [debug_assertions] is enabled (for example, by enabling a non-optimized build), dynamic checks are inserted that `panic` on overflow. -In the case of implicitly-wrapped overflow, implementations must -provide well-defined (even if still considered erroneous) results by -using two's complement overflow conventions. +r[not-unsafe.integer-overflow.silent-wrapping] +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. -The integral types provide inherent methods to allow programmers -explicitly to perform wrapping arithmetic. For example, -`i32::wrapping_add` provides two's complement, wrapping addition. +r[not-unsafe.integer-overflow.intentional-wrapping] +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. -The standard library also provides a `Wrapping` newtype which -ensures all standard arithmetic operations for `T` have wrapping -semantics. +The standard library also provides a [`Wrapping`](`core::num::Wrapping`) newtype which ensures all standard arithmetic operations for `T` have wrapping semantics. -See [RFC 560] for error conditions, rationale, and more details about -integer overflow. +> [!NOTE] +> See [RFC 560] for error conditions, rationale, and more details about integer overflow. +r[not-unsafe.logic] ## Logic errors -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 panics, incorrect results, -aborts, and non-termination. The behavior may also differ between -runs, builds, or kinds of build. - -For example, implementing both `Hash` and `Eq` requires that values -considered equal have equal hashes. Another example are data structures -like `BinaryHeap`, `BTreeMap`, `BTreeSet`, `HashMap` and `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. +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. + +> [!EXAMPLE] +> 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. +> +> +> +> ```rust,no_run +> use std::hash::{Hash, Hasher}; +> +> #[derive(PartialEq, Eq)] +> struct Wrapper(i32); +> +> impl Hash for Wrapper { +> fn hash(&self, hasher: &mut H) where H: Hasher { +> Hash::hash(&0i32, hasher); +> } +> } +> ``` + +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. [RFC 560]: https://github.com/rust-lang/rfcs/blob/master/text/0560-integer-overflow.md +[unsafe]: safety.unsafe-ops +[undefined]: undefined +[debug_assertions]: cfg.debug_assertions +[integer types]: type.numeric.int +[Halting problem]: https://en.wikipedia.org/wiki/Halting_problem +[tokio]: https://tokio.rs/ diff --git a/src/unsafety.md b/src/unsafety.md index 352edec47..c29bc5fe9 100644 --- a/src/unsafety.md +++ b/src/unsafety.md @@ -33,6 +33,9 @@ r[safety.unsafe-extern] r[safety.unsafe-attribute] - Applying an [unsafe attribute] to an item. +> [!NOTE] +> A lot of undesirable behavior is [not considered unsafe]. + [^extern-2024]: Prior to the 2024 edition, extern blocks were allowed to be declared without `unsafe`. [`extern`]: items/external-blocks.md @@ -42,3 +45,4 @@ r[safety.unsafe-attribute] [raw pointer]: types/pointer.md [unsafe trait]: items/traits.md#unsafe-traits [unsafe attribute]: attributes.md +[not considered unsafe]: not-unsafe From 234ae3ec552fcc61598f845bc56e8d7e01ab76a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?kleines=20Filmr=C3=B6llchen?= Date: Sun, 27 Jul 2025 18:20:42 +0200 Subject: [PATCH 2/2] Expand the rest of the non-unsafe behaviors into paragraphs with examples --- src/behavior-not-considered-unsafe.md | 60 +++++++++++++++++++++++---- 1 file changed, 51 insertions(+), 9 deletions(-) diff --git a/src/behavior-not-considered-unsafe.md b/src/behavior-not-considered-unsafe.md index 9ef0ac06a..7d62af75c 100644 --- a/src/behavior-not-considered-unsafe.md +++ b/src/behavior-not-considered-unsafe.md @@ -6,13 +6,6 @@ r[not-unsafe] Rust does not consider the following behaviors _[unsafe]_, though a programmer may (or should) find them undesirable, unexpected, or erroneous. -r[not-unsafe.resource-leaks] -- Leaks of memory and other resources -r[not-unsafe.abort] -- Exiting without calling destructors -r[not-unsafe.aslr-bypass] -- Exposing randomized executable base addresses through pointer leaks - r[not-unsafe.deadlocks] ## Deadlocks and livelocks @@ -62,6 +55,52 @@ In general, determining whether a program has deadlocked requires to solve the [ r[not-unsafe.livelocks] 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. +r[not-unsafe.resource-leaks] +## Leaks of memory and other resources + +A leaking resource is one that is never freed, even when it is no longer needed. Among the resources commonly leaked are memory, file descriptors, and networking sockets. Resource leaks may cause the program to accumulate unused resources until certain constraints by the environment are reached, such as the system running out of memory (OOM). In this case, a variety of non-normal behavior can occur, such as program termination by the operating system, common for OOM conditions, or failure to acquire further resources of the given type, common for file descriptors and sockets. + +> [!EXAMPLE] +> Using reference counting via [Rc](`std::rc::Rc`), it is possible to construct a reference cycle, where two data structures point at each other via `Rc`. Since both hold a reference to the other, the `Rc`s always have a reference count of at least 1, and their inner values are never dropped: a memory leak. Note that while implementing the following scenario requires some unsafe code in the standard library, the safety conditions are never violated, and the program is correct as far as `Rc`'s and `RefCell`'s unsafe code is concerned. +> +> ```rust +> use std::cell::RefCell; +> use std::rc::Rc; +> +> struct Node(Option>>); +> +> impl Node { +> fn link_to_other(&mut self, other: Rc>) { +> self.0 = Some(other); +> } +> } +> +> fn main() { +> let a = Rc::new(RefCell::new(Node(None))); +> let b = Rc::new(RefCell::new(Node(None))); +> a.borrow_mut().link_to_other(b.clone()); +> b.borrow_mut().link_to_other(a.clone()); +> // a and b are never deallocated, even when dropped here. +> } +> ``` + +r[not-unsafe.abort] +## Exiting without calling destructors + +When [panicking], a program may abort execution; that is, exit immediately without further activities. There are several instances in which abort on panic can happen, such as a double panic or with a compiler option to always use the abort strategy. + +Unlike unwinding, which runs [destructors] for each object in each unwound frame, aborting does not run any destructors in any frame. Any code relying on certain destructors to run might therefore not function correctly. + +r[not-unsafe.aslr-bypass] +## Exposing randomized executable base addresses through pointer leaks + +[Address space layout randomization] (ASLR) is a security mitigation which moves program segments to randomized (virtual) memory addresses each time the program is started. Without knowing the absolute positions of code or data in a program, this complicates binary exploitation. + +> [!NOTE] +> Whether the executable has ASLR support depends on the target, compiler flags, linker options, and more. Embedded targets usually do not support ASLR at all, while it is commonplace for "normal" targets on modern operating systems. Rust does not guarantee ASLR support, though the compiler and target documentation should specify their ASLR support. + +When a pointer is leaked that has a fixed known offset to a segment base address, such as a [function pointer], an attacker can derive the base address regardless of ASLR, usually bypassing its protections. A leak may occur in many ways, such as by displaying the pointer’s numeric value to the user. + r[not-unsafe.integer-overflow] ## Integer overflow @@ -103,13 +142,16 @@ Safe code may impose extra logical constraints that can be checked at neither co > } > } > ``` - -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. +> +> Related, 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`) 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. [RFC 560]: https://github.com/rust-lang/rfcs/blob/master/text/0560-integer-overflow.md [unsafe]: safety.unsafe-ops [undefined]: undefined +[panicking]: panic [debug_assertions]: cfg.debug_assertions [integer types]: type.numeric.int [Halting problem]: https://en.wikipedia.org/wiki/Halting_problem [tokio]: https://tokio.rs/ +[Address space layout randomization]: https://en.wikipedia.org/wiki/Address_space_layout_randomization +[function pointer]: type.fn-pointer