Skip to content

Allow volatile access to non-Rust memory, including address 0 #141260

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
165 changes: 95 additions & 70 deletions library/core/src/ptr/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@
//! undefined behavior to perform two concurrent accesses to the same location from different
//! threads unless both accesses only read from memory. Notice that this explicitly
//! includes [`read_volatile`] and [`write_volatile`]: Volatile accesses cannot
//! be used for inter-thread synchronization.
//! be used for inter-thread synchronization, regardless of whether it is acting on
//! Rust memory or not.
//! * The result of casting a reference to a pointer is valid for as long as the
//! underlying object is live and no reference (just raw pointers) is used to
//! access the same memory. That is, reference and pointer accesses cannot be
Expand Down Expand Up @@ -112,6 +113,10 @@
//! fully contiguous (i.e., has no "holes"), there is no guarantee that this
//! will not change in the future.
//!
//! Allocated objects must behave like "normal" memory: in particular, reads must not have
//! side-effects, and writes must become visible to other threads using the usual synchronization
//! primitives.
//!
//! For any allocated object with `base` address, `size`, and a set of
//! `addresses`, the following are guaranteed:
//! - For all addresses `a` in `addresses`, `a` is in the range `base .. (base +
Expand Down Expand Up @@ -1744,54 +1749,63 @@ pub const unsafe fn write_unaligned<T>(dst: *mut T, src: T) {
}
}

/// Performs a volatile read of the value from `src` without moving it. This
/// leaves the memory in `src` unchanged.
///
/// Volatile operations are intended to act on I/O memory, and are guaranteed
/// to not be elided or reordered by the compiler across other volatile
/// operations.
///
/// # Notes
///
/// Rust does not currently have a rigorously and formally defined memory model,
/// so the precise semantics of what "volatile" means here is subject to change
/// over time. That being said, the semantics will almost always end up pretty
/// similar to [C11's definition of volatile][c11].
///
/// The compiler shouldn't change the relative order or number of volatile
/// memory operations. However, volatile memory operations on zero-sized types
/// (e.g., if a zero-sized type is passed to `read_volatile`) are noops
/// and may be ignored.
/// Performs a volatile read of the value from `src` without moving it.
///
/// Rust does not currently have a rigorously and formally defined memory model, so the precise
/// semantics of what "volatile" means here is subject to change over time. That being said, the
/// semantics will almost always end up pretty similar to [C11's definition of volatile][c11].
///
/// Volatile operations are intended to act on I/O memory. As such, they are considered externally
/// observable events (just like syscalls), and are guaranteed to not be elided or reordered by the
/// compiler across other externally observable events. With this in mind, there are two cases of
/// usage that need to be distinguished:
///
/// - When a volatile operation is used for memory inside an [allocation], it behaves exactly like
/// [`read`], except for the additional guarantee that it won't be elided or reordered (see
/// above). This implies that the operation will actually access memory and not e.g. be lowered to
/// a register access or stack pop. Other than that, all the usual rules for memory accesses
/// apply. In particular, just like in C, whether an operation is volatile has no bearing
/// whatsoever on questions involving concurrent access from multiple threads. Volatile accesses
/// behave exactly like non-atomic accesses in that regard.
///
/// - Volatile operations, however, may also be used access memory that is _outside_ of any Rust
/// allocation. The main use-case is CPU and peripheral registers that must be accessed via an I/O
/// memory mapping, most commonly at fixed addresses reserved by the hardware. These often have
/// special semantics associated to their manipulation, and cannot be used as general purpose
/// memory. Here, any address value is possible, including 0 and [`usize::MAX`], so long as the
/// semantics of such a read are well-defined by the target hardware. The access must not trap. It
/// can (and usually will) cause side-effects, but those must not affect Rust-allocated memory in
/// in any way. In this use-case, the pointer does *not* have to be [valid] for reads.
///
/// Note that volatile memory operations on zero-sized types (e.g., if a zero-sized type is passed
/// to `read_volatile`) are noops and may be ignored.
///
/// [c11]: http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1570.pdf
/// [allocation]: crate::ptr#allocated-object
///
/// # Safety
///
/// Behavior is undefined if any of the following conditions are violated:
///
/// * `src` must be [valid] for reads.
/// * `src` must be either [valid] for reads, or it must point to memory outside of all Rust
/// allocations and reading from that memory must:
/// - not trap, and
/// - not cause any memory inside a Rust allocation to be modified.
///
/// * `src` must be properly aligned.
///
/// * `src` must point to a properly initialized value of type `T`.
/// * Reading from `src` must produce a properly initialized value of type `T`.
///
/// Like [`read`], `read_volatile` creates a bitwise copy of `T`, regardless of
/// whether `T` is [`Copy`]. If `T` is not [`Copy`], using both the returned
/// value and the value at `*src` can [violate memory safety][read-ownership].
/// However, storing non-[`Copy`] types in volatile memory is almost certainly
/// incorrect.
/// Like [`read`], `read_volatile` creates a bitwise copy of `T`, regardless of whether `T` is
/// [`Copy`]. If `T` is not [`Copy`], using both the returned value and the value at `*src` can
/// [violate memory safety][read-ownership]. However, modeling volatile memory with non-[`Copy`]
/// types is almost certainly incorrect.
///
/// Note that even if `T` has size `0`, the pointer must be properly aligned.
///
/// [valid]: self#safety
/// [read-ownership]: read#ownership-of-the-returned-value
///
/// Just like in C, whether an operation is volatile has no bearing whatsoever
/// on questions involving concurrent access from multiple threads. Volatile
/// accesses behave exactly like non-atomic accesses in that regard. In particular,
/// a race between a `read_volatile` and any write operation to the same location
/// is undefined behavior.
///
/// # Examples
///
/// Basic usage:
Expand All @@ -1813,63 +1827,75 @@ pub unsafe fn read_volatile<T>(src: *const T) -> T {
unsafe {
ub_checks::assert_unsafe_precondition!(
check_language_ub,
"ptr::read_volatile requires that the pointer argument is aligned and non-null",
"ptr::read_volatile requires that the pointer argument is aligned",
(
addr: *const () = src as *const (),
align: usize = align_of::<T>(),
is_zst: bool = T::IS_ZST,
) => ub_checks::maybe_is_aligned_and_not_null(addr, align, is_zst)
) => ub_checks::maybe_is_aligned(addr, align)
);
intrinsics::volatile_load(src)
}
}

/// Performs a volatile write of a memory location with the given value without
/// reading or dropping the old value.
///
/// Volatile operations are intended to act on I/O memory, and are guaranteed
/// to not be elided or reordered by the compiler across other volatile
/// operations.
///
/// `write_volatile` does not drop the contents of `dst`. This is safe, but it
/// could leak allocations or resources, so care should be taken not to overwrite
/// an object that should be dropped.
///
/// Additionally, it does not drop `src`. Semantically, `src` is moved into the
/// location pointed to by `dst`.
///
/// # Notes
///
/// Rust does not currently have a rigorously and formally defined memory model,
/// so the precise semantics of what "volatile" means here is subject to change
/// over time. That being said, the semantics will almost always end up pretty
/// similar to [C11's definition of volatile][c11].
///
/// The compiler shouldn't change the relative order or number of volatile
/// memory operations. However, volatile memory operations on zero-sized types
/// (e.g., if a zero-sized type is passed to `write_volatile`) are noops
/// and may be ignored.
/// Performs a volatile write of a memory location with the given value without reading or dropping
/// the old value.
///
/// Rust does not currently have a rigorously and formally defined memory model, so the precise
/// semantics of what "volatile" means here is subject to change over time. That being said, the
/// semantics will almost always end up pretty similar to [C11's definition of volatile][c11].
///
/// Volatile operations are intended to act on I/O memory. As such, they are considered externally
/// observable events (just like syscalls), and are guaranteed to not be elided or reordered by the
/// compiler across other externally observable events. With this in mind, there are two cases of
/// usage that need to be distinguished:
///
/// - When a volatile operation is used for memory inside an [allocation], it behaves exactly like
/// [`write()`], except for the additional guarantee that it won't be elided or reordered (see
/// above). This implies that the operation will actually access memory and not e.g. be lowered to
/// a register access or stack pop. Other than that, all the usual rules for memory accesses
/// apply. In particular, just like in C, whether an operation is volatile has no bearing
/// whatsoever on questions involving concurrent access from multiple threads. Volatile accesses
/// behave exactly like non-atomic accesses in that regard.
///
/// - Volatile operations, however, may also be used access memory that is _outside_ of any Rust
/// allocation. The main use-case is CPU and peripheral registers that must be accessed via an I/O
/// memory mapping, most commonly at fixed addresses reserved by the hardware. These often have
/// special semantics associated to their manipulation, and cannot be used as general purpose
/// memory. Here, any address value is possible, including 0 and [`usize::MAX`], so long as the
/// semantics of such a write are well-defined by the target hardware. The access must not trap.
/// It can (and usually will) cause side-effects, but those must not affect Rust-allocated memory
/// in any way. In this use-case, the pointer does *not* have to be [valid] for writes.
///
/// Note that volatile memory operations on zero-sized types (e.g., if a zero-sized type is passed
/// to `write_volatile`) are noops and may be ignored.
///
/// `write_volatile` does not drop the contents of `dst`. This is safe, but it could leak
/// allocations or resources, so care should be taken not to overwrite an object that should be
/// dropped when operating on Rust memory.
///
/// Additionally, it does not drop `src`. Semantically, `src` is moved into the location pointed to
/// by `dst`.
///
/// [c11]: http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1570.pdf
/// [allocation]: crate::ptr#allocated-object
///
/// # Safety
///
/// Behavior is undefined if any of the following conditions are violated:
///
/// * `dst` must be [valid] for writes.
/// * `dst` must be either [valid] for writes, or it must point to memory outside of all Rust
/// allocations and writing to that memory must:
/// - not trap, and
/// - not cause any memory inside a Rust allocation to be modified.
///
/// * `dst` must be properly aligned.
///
/// * `src` must be a properly initialized value of type `T`.
///
/// Note that even if `T` has size `0`, the pointer must be properly aligned.
///
/// [valid]: self#safety
///
/// Just like in C, whether an operation is volatile has no bearing whatsoever
/// on questions involving concurrent access from multiple threads. Volatile
/// accesses behave exactly like non-atomic accesses in that regard. In particular,
/// a race between a `write_volatile` and any other operation (reading or writing)
/// on the same location is undefined behavior.
///
/// # Examples
///
/// Basic usage:
Expand All @@ -1893,12 +1919,11 @@ pub unsafe fn write_volatile<T>(dst: *mut T, src: T) {
unsafe {
ub_checks::assert_unsafe_precondition!(
check_language_ub,
"ptr::write_volatile requires that the pointer argument is aligned and non-null",
"ptr::write_volatile requires that the pointer argument is aligned",
(
addr: *mut () = dst as *mut (),
align: usize = align_of::<T>(),
is_zst: bool = T::IS_ZST,
) => ub_checks::maybe_is_aligned_and_not_null(addr, align, is_zst)
) => ub_checks::maybe_is_aligned(addr, align)
);
intrinsics::volatile_store(dst, src);
}
Expand Down
19 changes: 19 additions & 0 deletions library/core/src/ub_checks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,25 @@ pub(crate) const fn maybe_is_aligned_and_not_null(
)
}

/// Checks whether `ptr` is properly aligned with respect to the given alignment.
///
/// In `const` this is approximate and can fail spuriously. It is primarily intended
/// for `assert_unsafe_precondition!` with `check_language_ub`, in which case the
/// check is anyway not executed in `const`.
#[inline]
#[rustc_allow_const_fn_unstable(const_eval_select)]
pub(crate) const fn maybe_is_aligned(ptr: *const (), align: usize) -> bool {
// This is just for safety checks so we can const_eval_select.
const_eval_select!(
@capture { ptr: *const (), align: usize } -> bool:
if const {
true
} else {
ptr.is_aligned_to(align)
}
)
}

#[inline]
pub(crate) const fn is_valid_allocation_size(size: usize, len: usize) -> bool {
let max_len = if size == 0 { usize::MAX } else { isize::MAX as usize / size };
Expand Down
4 changes: 1 addition & 3 deletions tests/ui/precondition-checks/read_volatile.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//@ run-fail
//@ compile-flags: -Copt-level=3 -Cdebug-assertions=no -Zub-checks=yes
//@ error-pattern: unsafe precondition(s) violated: ptr::read_volatile requires
//@ revisions: null misaligned
//@ revisions: misaligned

#![allow(invalid_null_arguments)]

Expand All @@ -11,8 +11,6 @@ fn main() {
let src = [0u16; 2];
let src = src.as_ptr();
unsafe {
#[cfg(null)]
ptr::read_volatile(ptr::null::<u8>());
#[cfg(misaligned)]
ptr::read_volatile(src.byte_add(1));
}
Expand Down
6 changes: 1 addition & 5 deletions tests/ui/precondition-checks/write_volatile.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
//@ run-fail
//@ compile-flags: -Copt-level=3 -Cdebug-assertions=no -Zub-checks=yes
//@ error-pattern: unsafe precondition(s) violated: ptr::write_volatile requires
//@ revisions: null misaligned

#![allow(invalid_null_arguments)]
//@ revisions: misaligned

use std::ptr;

fn main() {
let mut dst = [0u16; 2];
let mut dst = dst.as_mut_ptr();
unsafe {
#[cfg(null)]
ptr::write_volatile(ptr::null_mut::<u8>(), 1u8);
#[cfg(misaligned)]
ptr::write_volatile(dst.byte_add(1), 1u16);
}
Expand Down
Loading