Skip to content

Commit 8a8717e

Browse files
committed
fix: don't panic on volatile access to null
According to https://discourse.llvm.org/t/rfc-volatile-access-to-non-dereferenceable-memory-may-be-well-defined/86303/4, LLVM allows volatile operations on null and handles it correctly. This should be allowed in Rust as well, because I/O memory may be hard-coded to address 0 in some cases, like the AVR chip ATtiny1626. A test case that ensured a failure when passing null to volatile was removed, since it's now valid. Due to the addition of `maybe_is_aligned` to `ub_checks`, `maybe_is_aligned_and_not_null` was refactored to use it. docs: revise restrictions on volatile operations A distinction between usage on Rust memory vs. non-Rust memory was introduced. Documentation was reworded to explain what that means, and make explicit that: - No trapping can occur from volatile operations; - On Rust memory, all safety rules must be respected; - On Rust memory, the primary difference from regular access is that volatile always involves a memory dereference; - On Rust memory, the only data affected by an operation is the one pointed to in the argument(s) of the function; - On Rust memory, provenance follows the same rules as non-volatile access; - On non-Rust memory, any address known to not contain Rust memory is valid (including 0 and usize::MAX); - On non-Rust memory, no Rust memory may be affected (it is implicit that any other non-Rust memory may be affected, though, even if not referenced by the pointer). This should be relevant when, for example, reading register A causes a flag to change in register B, or writing to A causes B to change in some way. Everything affected mustn't be inside an allocation. - On non-Rust memory, provenance is irrelevant and a pointer with none can be used in a valid way. fix: don't lint null as UB for volatile Also remove a now-unneeded `allow` line. fix: additional wording nits
1 parent bf5e6cc commit 8a8717e

File tree

7 files changed

+127
-138
lines changed

7 files changed

+127
-138
lines changed

compiler/rustc_lint/src/ptr_nulls.rs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -160,12 +160,10 @@ impl<'tcx> LateLintPass<'tcx> for PtrNullChecks {
160160
let (arg_indices, are_zsts_allowed): (&[_], _) = match diag_name {
161161
sym::ptr_read
162162
| sym::ptr_read_unaligned
163-
| sym::ptr_read_volatile
164163
| sym::ptr_replace
165164
| sym::ptr_write
166165
| sym::ptr_write_bytes
167-
| sym::ptr_write_unaligned
168-
| sym::ptr_write_volatile => (&[0], true),
166+
| sym::ptr_write_unaligned => (&[0], true),
169167
sym::slice_from_raw_parts | sym::slice_from_raw_parts_mut => (&[0], false),
170168
sym::ptr_copy
171169
| sym::ptr_copy_nonoverlapping

library/core/src/ptr/mod.rs

Lines changed: 93 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@
2828
//! undefined behavior to perform two concurrent accesses to the same location from different
2929
//! threads unless both accesses only read from memory. Notice that this explicitly
3030
//! includes [`read_volatile`] and [`write_volatile`]: Volatile accesses cannot
31-
//! be used for inter-thread synchronization.
31+
//! be used for inter-thread synchronization, regardless of whether they are acting on
32+
//! Rust memory or not.
3233
//! * The result of casting a reference to a pointer is valid for as long as the
3334
//! underlying allocation is live and no reference (just raw pointers) is used to
3435
//! access the same memory. That is, reference and pointer accesses cannot be
@@ -114,6 +115,10 @@
114115
//! fully contiguous (i.e., has no "holes"), there is no guarantee that this
115116
//! will not change in the future.
116117
//!
118+
//! Allocations must behave like "normal" memory: in particular, reads must not have
119+
//! side-effects, and writes must become visible to other threads using the usual synchronization
120+
//! primitives.
121+
//!
117122
//! For any allocation with `base` address, `size`, and a set of
118123
//! `addresses`, the following are guaranteed:
119124
//! - For all addresses `a` in `addresses`, `a` is in the range `base .. (base +
@@ -2021,54 +2026,61 @@ pub const unsafe fn write_unaligned<T>(dst: *mut T, src: T) {
20212026
}
20222027
}
20232028

2024-
/// Performs a volatile read of the value from `src` without moving it. This
2025-
/// leaves the memory in `src` unchanged.
2026-
///
2027-
/// Volatile operations are intended to act on I/O memory, and are guaranteed
2028-
/// to not be elided or reordered by the compiler across other volatile
2029-
/// operations.
2030-
///
2031-
/// # Notes
2032-
///
2033-
/// Rust does not currently have a rigorously and formally defined memory model,
2034-
/// so the precise semantics of what "volatile" means here is subject to change
2035-
/// over time. That being said, the semantics will almost always end up pretty
2036-
/// similar to [C11's definition of volatile][c11].
2037-
///
2038-
/// The compiler shouldn't change the relative order or number of volatile
2039-
/// memory operations. However, volatile memory operations on zero-sized types
2040-
/// (e.g., if a zero-sized type is passed to `read_volatile`) are noops
2041-
/// and may be ignored.
2042-
///
2043-
/// [c11]: http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1570.pdf
2029+
/// Performs a volatile read of the value from `src` without moving it.
2030+
///
2031+
/// Volatile operations are intended to act on I/O memory. As such, they are considered externally
2032+
/// observable events (just like syscalls, but less opaque), and are guaranteed to not be elided or
2033+
/// reordered by the compiler across other externally observable events. With this in mind, there
2034+
/// are two cases of usage that need to be distinguished:
2035+
///
2036+
/// - When a volatile operation is used for memory inside an [allocation], it behaves exactly like
2037+
/// [`read`], except for the additional guarantee that it won't be elided or reordered (see
2038+
/// above). This implies that the operation will actually access memory and not e.g. be lowered to
2039+
/// reusing data from a previous read. Other than that, all the usual rules for memory accesses
2040+
/// apply (including provenance). In particular, just like in C, whether an operation is volatile
2041+
/// has no bearing whatsoever on questions involving concurrent accesses from multiple threads.
2042+
/// Volatile accesses behave exactly like non-atomic accesses in that regard.
2043+
///
2044+
/// - Volatile operations, however, may also be used to access memory that is _outside_ of any Rust
2045+
/// allocation. In this use-case, the pointer does *not* have to be [valid] for reads. This is
2046+
/// typically used for CPU and peripheral registers that must be accessed via an I/O memory
2047+
/// mapping, most commonly at fixed addresses reserved by the hardware. These often have special
2048+
/// semantics associated to their manipulation, and cannot be used as general purpose memory.
2049+
/// Here, any address value is possible, including 0 and [`usize::MAX`], so long as the semantics
2050+
/// of such a read are well-defined by the target hardware. The provenance of the pointer is
2051+
/// irrelevant, and it can be created with [`without_provenance`]. The access must not trap. It
2052+
/// can cause side-effects, but those must not affect Rust-allocated memory in any way. This
2053+
/// access is still not considered [atomic], and as such it cannot be used for inter-thread
2054+
/// synchronization.
2055+
///
2056+
/// Note that volatile memory operations where T is a zero-sized type are noops and may be ignored.
2057+
///
2058+
/// [allocation]: crate::ptr#allocated-object
2059+
/// [atomic]: crate::sync::atomic#memory-model-for-atomic-accesses
20442060
///
20452061
/// # Safety
20462062
///
2063+
/// Like [`read`], `read_volatile` creates a bitwise copy of `T`, regardless of whether `T` is
2064+
/// [`Copy`]. If `T` is not [`Copy`], using both the returned value and the value at `*src` can
2065+
/// [violate memory safety][read-ownership]. However, storing non-[`Copy`] types in volatile memory
2066+
/// is almost certainly incorrect.
2067+
///
20472068
/// Behavior is undefined if any of the following conditions are violated:
20482069
///
2049-
/// * `src` must be [valid] for reads.
2070+
/// * `src` must be either [valid] for reads, or it must point to memory outside of all Rust
2071+
/// allocations and reading from that memory must:
2072+
/// - not trap, and
2073+
/// - not cause any memory inside a Rust allocation to be modified.
20502074
///
20512075
/// * `src` must be properly aligned.
20522076
///
2053-
/// * `src` must point to a properly initialized value of type `T`.
2054-
///
2055-
/// Like [`read`], `read_volatile` creates a bitwise copy of `T`, regardless of
2056-
/// whether `T` is [`Copy`]. If `T` is not [`Copy`], using both the returned
2057-
/// value and the value at `*src` can [violate memory safety][read-ownership].
2058-
/// However, storing non-[`Copy`] types in volatile memory is almost certainly
2059-
/// incorrect.
2077+
/// * Reading from `src` must produce a properly initialized value of type `T`.
20602078
///
20612079
/// Note that even if `T` has size `0`, the pointer must be properly aligned.
20622080
///
20632081
/// [valid]: self#safety
20642082
/// [read-ownership]: read#ownership-of-the-returned-value
20652083
///
2066-
/// Just like in C, whether an operation is volatile has no bearing whatsoever
2067-
/// on questions involving concurrent access from multiple threads. Volatile
2068-
/// accesses behave exactly like non-atomic accesses in that regard. In particular,
2069-
/// a race between a `read_volatile` and any write operation to the same location
2070-
/// is undefined behavior.
2071-
///
20722084
/// # Examples
20732085
///
20742086
/// Basic usage:
@@ -2090,63 +2102,70 @@ pub unsafe fn read_volatile<T>(src: *const T) -> T {
20902102
unsafe {
20912103
ub_checks::assert_unsafe_precondition!(
20922104
check_language_ub,
2093-
"ptr::read_volatile requires that the pointer argument is aligned and non-null",
2105+
"ptr::read_volatile requires that the pointer argument is aligned",
20942106
(
20952107
addr: *const () = src as *const (),
20962108
align: usize = align_of::<T>(),
2097-
is_zst: bool = T::IS_ZST,
2098-
) => ub_checks::maybe_is_aligned_and_not_null(addr, align, is_zst)
2109+
) => ub_checks::maybe_is_aligned(addr, align)
20992110
);
21002111
intrinsics::volatile_load(src)
21012112
}
21022113
}
21032114

2104-
/// Performs a volatile write of a memory location with the given value without
2105-
/// reading or dropping the old value.
2106-
///
2107-
/// Volatile operations are intended to act on I/O memory, and are guaranteed
2108-
/// to not be elided or reordered by the compiler across other volatile
2109-
/// operations.
2110-
///
2111-
/// `write_volatile` does not drop the contents of `dst`. This is safe, but it
2112-
/// could leak allocations or resources, so care should be taken not to overwrite
2113-
/// an object that should be dropped.
2114-
///
2115-
/// Additionally, it does not drop `src`. Semantically, `src` is moved into the
2116-
/// location pointed to by `dst`.
2117-
///
2118-
/// # Notes
2119-
///
2120-
/// Rust does not currently have a rigorously and formally defined memory model,
2121-
/// so the precise semantics of what "volatile" means here is subject to change
2122-
/// over time. That being said, the semantics will almost always end up pretty
2123-
/// similar to [C11's definition of volatile][c11].
2124-
///
2125-
/// The compiler shouldn't change the relative order or number of volatile
2126-
/// memory operations. However, volatile memory operations on zero-sized types
2127-
/// (e.g., if a zero-sized type is passed to `write_volatile`) are noops
2128-
/// and may be ignored.
2129-
///
2130-
/// [c11]: http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1570.pdf
2115+
/// Performs a volatile write of a memory location with the given value without reading or dropping
2116+
/// the old value.
2117+
///
2118+
/// Volatile operations are intended to act on I/O memory. As such, they are considered externally
2119+
/// observable events (just like syscalls), and are guaranteed to not be elided or reordered by the
2120+
/// compiler across other externally observable events. With this in mind, there are two cases of
2121+
/// usage that need to be distinguished:
2122+
///
2123+
/// - When a volatile operation is used for memory inside an [allocation], it behaves exactly like
2124+
/// [`write`][write()], except for the additional guarantee that it won't be elided or reordered
2125+
/// (see above). This implies that the operation will actually access memory and not e.g. be
2126+
/// lowered to a register access. Other than that, all the usual rules for memory accesses apply
2127+
/// (including provenance). In particular, just like in C, whether an operation is volatile has no
2128+
/// bearing whatsoever on questions involving concurrent access from multiple threads. Volatile
2129+
/// accesses behave exactly like non-atomic accesses in that regard.
2130+
///
2131+
/// - Volatile operations, however, may also be used to access memory that is _outside_ of any Rust
2132+
/// allocation. In this use-case, the pointer does *not* have to be [valid] for writes. This is
2133+
/// typically used for CPU and peripheral registers that must be accessed via an I/O memory
2134+
/// mapping, most commonly at fixed addresses reserved by the hardware. These often have special
2135+
/// semantics associated to their manipulation, and cannot be used as general purpose memory.
2136+
/// Here, any address value is possible, including 0 and [`usize::MAX`], so long as the semantics
2137+
/// of such a write are well-defined by the target hardware. The provenance of the pointer is
2138+
/// irrelevant, and it can be created with [`without_provenance`]. The access must not trap. It
2139+
/// can cause side-effects, but those must not affect Rust-allocated memory in any way. This
2140+
/// access is still not considered [atomic], and as such it cannot be used for inter-thread
2141+
/// synchronization.
2142+
///
2143+
/// Note that volatile memory operations on zero-sized types (e.g., if a zero-sized type is passed
2144+
/// to `write_volatile`) are noops and may be ignored.
2145+
///
2146+
/// `write_volatile` does not drop the contents of `dst`. This is safe, but it could leak
2147+
/// allocations or resources, so care should be taken not to overwrite an object that should be
2148+
/// dropped when operating on Rust memory. Additionally, it does not drop `src`. Semantically, `src`
2149+
/// is moved into the location pointed to by `dst`.
2150+
///
2151+
/// [allocation]: crate::ptr#allocated-object
2152+
/// [atomic]: crate::sync::atomic#memory-model-for-atomic-accesses
21312153
///
21322154
/// # Safety
21332155
///
21342156
/// Behavior is undefined if any of the following conditions are violated:
21352157
///
2136-
/// * `dst` must be [valid] for writes.
2158+
/// * `dst` must be either [valid] for writes, or it must point to memory outside of all Rust
2159+
/// allocations and writing to that memory must:
2160+
/// - not trap, and
2161+
/// - not cause any memory inside a Rust allocation to be modified.
21372162
///
21382163
/// * `dst` must be properly aligned.
21392164
///
21402165
/// Note that even if `T` has size `0`, the pointer must be properly aligned.
21412166
///
21422167
/// [valid]: self#safety
21432168
///
2144-
/// Just like in C, whether an operation is volatile has no bearing whatsoever
2145-
/// on questions involving concurrent access from multiple threads. Volatile
2146-
/// accesses behave exactly like non-atomic accesses in that regard. In particular,
2147-
/// a race between a `write_volatile` and any other operation (reading or writing)
2148-
/// on the same location is undefined behavior.
2149-
///
21502169
/// # Examples
21512170
///
21522171
/// Basic usage:
@@ -2170,12 +2189,11 @@ pub unsafe fn write_volatile<T>(dst: *mut T, src: T) {
21702189
unsafe {
21712190
ub_checks::assert_unsafe_precondition!(
21722191
check_language_ub,
2173-
"ptr::write_volatile requires that the pointer argument is aligned and non-null",
2192+
"ptr::write_volatile requires that the pointer argument is aligned",
21742193
(
21752194
addr: *mut () = dst as *mut (),
21762195
align: usize = align_of::<T>(),
2177-
is_zst: bool = T::IS_ZST,
2178-
) => ub_checks::maybe_is_aligned_and_not_null(addr, align, is_zst)
2196+
) => ub_checks::maybe_is_aligned(addr, align)
21792197
);
21802198
intrinsics::volatile_store(dst, src);
21812199
}

library/core/src/ub_checks.rs

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -120,13 +120,25 @@ pub(crate) const fn maybe_is_aligned_and_not_null(
120120
align: usize,
121121
is_zst: bool,
122122
) -> bool {
123+
// This is just for safety checks so we can const_eval_select.
124+
maybe_is_aligned(ptr, align) && (is_zst || !ptr.is_null())
125+
}
126+
127+
/// Checks whether `ptr` is properly aligned with respect to the given alignment.
128+
///
129+
/// In `const` this is approximate and can fail spuriously. It is primarily intended
130+
/// for `assert_unsafe_precondition!` with `check_language_ub`, in which case the
131+
/// check is anyway not executed in `const`.
132+
#[inline]
133+
#[rustc_allow_const_fn_unstable(const_eval_select)]
134+
pub(crate) const fn maybe_is_aligned(ptr: *const (), align: usize) -> bool {
123135
// This is just for safety checks so we can const_eval_select.
124136
const_eval_select!(
125-
@capture { ptr: *const (), align: usize, is_zst: bool } -> bool:
137+
@capture { ptr: *const (), align: usize } -> bool:
126138
if const {
127-
is_zst || !ptr.is_null()
139+
true
128140
} else {
129-
ptr.is_aligned_to(align) && (is_zst || !ptr.is_null())
141+
ptr.is_aligned_to(align)
130142
}
131143
)
132144
}

tests/ui/lint/invalid_null_args.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,9 @@ unsafe fn null_ptr() {
5858
let _a: A = ptr::read_unaligned(ptr::null_mut());
5959
//~^ ERROR calling this function with a null pointer is undefined behavior
6060

61+
// These two should *not* fire the lint.
6162
let _a: A = ptr::read_volatile(ptr::null());
62-
//~^ ERROR calling this function with a null pointer is undefined behavior
6363
let _a: A = ptr::read_volatile(ptr::null_mut());
64-
//~^ ERROR calling this function with a null pointer is undefined behavior
6564

6665
let _a: A = ptr::replace(ptr::null_mut(), v);
6766
//~^ ERROR calling this function with a null pointer is undefined behavior
@@ -82,8 +81,8 @@ unsafe fn null_ptr() {
8281
ptr::write_unaligned(ptr::null_mut(), v);
8382
//~^ ERROR calling this function with a null pointer is undefined behavior
8483

84+
// This one should *not* fire the lint.
8585
ptr::write_volatile(ptr::null_mut(), v);
86-
//~^ ERROR calling this function with a null pointer is undefined behavior
8786

8887
ptr::write_bytes::<usize>(ptr::null_mut(), 42, 0);
8988
//~^ ERROR calling this function with a null pointer is undefined behavior

0 commit comments

Comments
 (0)