diff --git a/compiler/rustc_lint/src/ptr_nulls.rs b/compiler/rustc_lint/src/ptr_nulls.rs index 826bce2c31506..b2fa0fba76d98 100644 --- a/compiler/rustc_lint/src/ptr_nulls.rs +++ b/compiler/rustc_lint/src/ptr_nulls.rs @@ -160,12 +160,10 @@ impl<'tcx> LateLintPass<'tcx> for PtrNullChecks { let (arg_indices, are_zsts_allowed): (&[_], _) = match diag_name { sym::ptr_read | sym::ptr_read_unaligned - | sym::ptr_read_volatile | sym::ptr_replace | sym::ptr_write | sym::ptr_write_bytes - | sym::ptr_write_unaligned - | sym::ptr_write_volatile => (&[0], true), + | sym::ptr_write_unaligned => (&[0], true), sym::slice_from_raw_parts | sym::slice_from_raw_parts_mut => (&[0], false), sym::ptr_copy | sym::ptr_copy_nonoverlapping diff --git a/library/core/src/ptr/mod.rs b/library/core/src/ptr/mod.rs index 81bf6778b05db..d8f07aecdf5ea 100644 --- a/library/core/src/ptr/mod.rs +++ b/library/core/src/ptr/mod.rs @@ -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 allocation is live and no reference (just raw pointers) is used to //! access the same memory. That is, reference and pointer accesses cannot be @@ -114,6 +115,10 @@ //! fully contiguous (i.e., has no "holes"), there is no guarantee that this //! will not change in the future. //! +//! Allocations 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 allocation 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 + @@ -2021,54 +2026,66 @@ pub const unsafe fn write_unaligned(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, but less opaque), 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 +/// reusing data from a previous read, such as from a register on which previous load of that +/// memory was performed. Other than that, all the usual rules for memory accesses apply +/// (including provenance). 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 to access memory that is _outside_ of any Rust +/// allocation. In this use-case, the pointer does *not* have to be [valid] for reads. This is +/// typically used for 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 provenance of the pointer is +/// irrelevant, and it can be created with [`without_provenance`]. The access must not trap. It +/// can cause side-effects, but those must not affect Rust-allocated memory in in any way. This +/// access is still not considered [atomic], and as such it cannot be used for inter-thread +/// synchronization. Note that volatile memory operations where T is a zero-sized type are noops +/// and may be ignored. /// /// [c11]: http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1570.pdf +/// [allocation]: crate::ptr#allocated-object +/// [atomic]: crate::sync::atomic#memory-model-for-atomic-accesses /// /// # Safety /// +/// 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. +/// /// 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`. -/// -/// 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. +/// * Reading from `src` must produce a properly initialized value of type `T`. /// /// 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: @@ -2090,50 +2107,70 @@ pub unsafe fn read_volatile(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::(), - 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 +/// (including provenance). 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 to access memory that is _outside_ of any Rust +/// allocation. In this use-case, the pointer does *not* have to be [valid] for writes. This is +/// typically used for 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 provenance of the pointer is +/// irrelevant, and it can be created with [`without_provenance`]. The access must not trap. It +/// can cause side-effects, but those must not affect Rust-allocated memory in any way. This +/// access is still not considered [atomic], and as such it cannot be used for inter-thread +/// synchronization. +/// +/// 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 +/// [atomic]: crate::sync::atomic#memory-model-for-atomic-accesses /// /// # 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. /// @@ -2141,12 +2178,6 @@ pub unsafe fn read_volatile(src: *const T) -> T { /// /// [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: @@ -2170,12 +2201,11 @@ pub unsafe fn write_volatile(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::(), - 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); } diff --git a/library/core/src/ub_checks.rs b/library/core/src/ub_checks.rs index a7caaeb95cdba..b809294cfcee7 100644 --- a/library/core/src/ub_checks.rs +++ b/library/core/src/ub_checks.rs @@ -120,13 +120,25 @@ pub(crate) const fn maybe_is_aligned_and_not_null( align: usize, is_zst: bool, ) -> bool { + // This is just for safety checks so we can const_eval_select. + maybe_is_aligned(ptr, align) && (is_zst || !ptr.is_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, is_zst: bool } -> bool: + @capture { ptr: *const (), align: usize } -> bool: if const { - is_zst || !ptr.is_null() + true } else { - ptr.is_aligned_to(align) && (is_zst || !ptr.is_null()) + ptr.is_aligned_to(align) } ) } diff --git a/tests/ui/lint/invalid_null_args.rs b/tests/ui/lint/invalid_null_args.rs index f40f06a0d3624..402a0beebaf04 100644 --- a/tests/ui/lint/invalid_null_args.rs +++ b/tests/ui/lint/invalid_null_args.rs @@ -58,11 +58,6 @@ unsafe fn null_ptr() { let _a: A = ptr::read_unaligned(ptr::null_mut()); //~^ ERROR calling this function with a null pointer is undefined behavior - let _a: A = ptr::read_volatile(ptr::null()); - //~^ ERROR calling this function with a null pointer is undefined behavior - let _a: A = ptr::read_volatile(ptr::null_mut()); - //~^ ERROR calling this function with a null pointer is undefined behavior - let _a: A = ptr::replace(ptr::null_mut(), v); //~^ ERROR calling this function with a null pointer is undefined behavior @@ -82,12 +77,6 @@ unsafe fn null_ptr() { ptr::write_unaligned(ptr::null_mut(), v); //~^ ERROR calling this function with a null pointer is undefined behavior - ptr::write_volatile(ptr::null_mut(), v); - //~^ ERROR calling this function with a null pointer is undefined behavior - - ptr::write_bytes::(ptr::null_mut(), 42, 0); - //~^ ERROR calling this function with a null pointer is undefined behavior - // with indirections let const_ptr = null_ptr as *const u8; let _a: u8 = ptr::read(const_ptr); diff --git a/tests/ui/lint/invalid_null_args.stderr b/tests/ui/lint/invalid_null_args.stderr index 11c6270cfb78e..f38a62dd2c1c4 100644 --- a/tests/ui/lint/invalid_null_args.stderr +++ b/tests/ui/lint/invalid_null_args.stderr @@ -166,26 +166,6 @@ LL | let _a: A = ptr::read_unaligned(ptr::null_mut()); error: calling this function with a null pointer is undefined behavior, even if the result of the function is unused --> $DIR/invalid_null_args.rs:61:17 | -LL | let _a: A = ptr::read_volatile(ptr::null()); - | ^^^^^^^^^^^^^^^^^^^-----------^ - | | - | null pointer originates from here - | - = help: for more information, visit and - -error: calling this function with a null pointer is undefined behavior, even if the result of the function is unused - --> $DIR/invalid_null_args.rs:63:17 - | -LL | let _a: A = ptr::read_volatile(ptr::null_mut()); - | ^^^^^^^^^^^^^^^^^^^---------------^ - | | - | null pointer originates from here - | - = help: for more information, visit and - -error: calling this function with a null pointer is undefined behavior, even if the result of the function is unused - --> $DIR/invalid_null_args.rs:66:17 - | LL | let _a: A = ptr::replace(ptr::null_mut(), v); | ^^^^^^^^^^^^^---------------^^^^ | | @@ -194,7 +174,7 @@ LL | let _a: A = ptr::replace(ptr::null_mut(), v); = help: for more information, visit and error: calling this function with a null pointer is undefined behavior, even if the result of the function is unused - --> $DIR/invalid_null_args.rs:69:5 + --> $DIR/invalid_null_args.rs:64:5 | LL | ptr::swap::(ptr::null_mut(), &mut v); | ^^^^^^^^^^^^^^^---------------^^^^^^^^^ @@ -204,7 +184,7 @@ LL | ptr::swap::(ptr::null_mut(), &mut v); = help: for more information, visit and error: calling this function with a null pointer is undefined behavior, even if the result of the function is unused - --> $DIR/invalid_null_args.rs:71:5 + --> $DIR/invalid_null_args.rs:66:5 | LL | ptr::swap::(&mut v, ptr::null_mut()); | ^^^^^^^^^^^^^^^^^^^^^^^---------------^ @@ -214,7 +194,7 @@ LL | ptr::swap::(&mut v, ptr::null_mut()); = help: for more information, visit and error: calling this function with a null pointer is undefined behavior, even if the result of the function is unused - --> $DIR/invalid_null_args.rs:74:5 + --> $DIR/invalid_null_args.rs:69:5 | LL | ptr::swap_nonoverlapping::(ptr::null_mut(), &mut v, 0); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^---------------^^^^^^^^^^^^ @@ -224,7 +204,7 @@ LL | ptr::swap_nonoverlapping::(ptr::null_mut(), &mut v, 0); = help: for more information, visit and error: calling this function with a null pointer is undefined behavior, even if the result of the function is unused - --> $DIR/invalid_null_args.rs:76:5 + --> $DIR/invalid_null_args.rs:71:5 | LL | ptr::swap_nonoverlapping::(&mut v, ptr::null_mut(), 0); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^---------------^^^^ @@ -234,7 +214,7 @@ LL | ptr::swap_nonoverlapping::(&mut v, ptr::null_mut(), 0); = help: for more information, visit and error: calling this function with a null pointer is undefined behavior, even if the result of the function is unused - --> $DIR/invalid_null_args.rs:79:5 + --> $DIR/invalid_null_args.rs:74:5 | LL | ptr::write(ptr::null_mut(), v); | ^^^^^^^^^^^---------------^^^^ @@ -244,7 +224,7 @@ LL | ptr::write(ptr::null_mut(), v); = help: for more information, visit and error: calling this function with a null pointer is undefined behavior, even if the result of the function is unused - --> $DIR/invalid_null_args.rs:82:5 + --> $DIR/invalid_null_args.rs:77:5 | LL | ptr::write_unaligned(ptr::null_mut(), v); | ^^^^^^^^^^^^^^^^^^^^^---------------^^^^ @@ -254,27 +234,7 @@ LL | ptr::write_unaligned(ptr::null_mut(), v); = help: for more information, visit and error: calling this function with a null pointer is undefined behavior, even if the result of the function is unused - --> $DIR/invalid_null_args.rs:85:5 - | -LL | ptr::write_volatile(ptr::null_mut(), v); - | ^^^^^^^^^^^^^^^^^^^^---------------^^^^ - | | - | null pointer originates from here - | - = help: for more information, visit and - -error: calling this function with a null pointer is undefined behavior, even if the result of the function is unused - --> $DIR/invalid_null_args.rs:88:5 - | -LL | ptr::write_bytes::(ptr::null_mut(), 42, 0); - | ^^^^^^^^^^^^^^^^^^^^^^^^^^---------------^^^^^^^^ - | | - | null pointer originates from here - | - = help: for more information, visit and - -error: calling this function with a null pointer is undefined behavior, even if the result of the function is unused - --> $DIR/invalid_null_args.rs:93:18 + --> $DIR/invalid_null_args.rs:82:18 | LL | let _a: u8 = ptr::read(const_ptr); | ^^^^^^^^^^^^^^^^^^^^ @@ -287,7 +247,7 @@ LL | let null_ptr = ptr::null_mut(); | ^^^^^^^^^^^^^^^ error: calling this function with a null pointer is undefined behavior, even if the result of the function is unused - --> $DIR/invalid_null_args.rs:100:5 + --> $DIR/invalid_null_args.rs:89:5 | LL | std::slice::from_raw_parts::<()>(ptr::null(), 0); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^-----------^^^^ @@ -297,7 +257,7 @@ LL | std::slice::from_raw_parts::<()>(ptr::null(), 0); = help: for more information, visit and error: calling this function with a null pointer is undefined behavior, even if the result of the function is unused - --> $DIR/invalid_null_args.rs:102:5 + --> $DIR/invalid_null_args.rs:91:5 | LL | std::slice::from_raw_parts::(ptr::null(), 0); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^-----------^^^^ @@ -307,7 +267,7 @@ LL | std::slice::from_raw_parts::(ptr::null(), 0); = help: for more information, visit and error: calling this function with a null pointer is undefined behavior, even if the result of the function is unused - --> $DIR/invalid_null_args.rs:104:5 + --> $DIR/invalid_null_args.rs:93:5 | LL | std::slice::from_raw_parts_mut::<()>(ptr::null_mut(), 0); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^---------------^^^^ @@ -317,7 +277,7 @@ LL | std::slice::from_raw_parts_mut::<()>(ptr::null_mut(), 0); = help: for more information, visit and error: calling this function with a null pointer is undefined behavior, even if the result of the function is unused - --> $DIR/invalid_null_args.rs:106:5 + --> $DIR/invalid_null_args.rs:95:5 | LL | std::slice::from_raw_parts_mut::(ptr::null_mut(), 0); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^---------------^^^^ @@ -326,5 +286,5 @@ LL | std::slice::from_raw_parts_mut::(ptr::null_mut(), 0); | = help: for more information, visit and -error: aborting due to 31 previous errors +error: aborting due to 27 previous errors diff --git a/tests/ui/precondition-checks/read_volatile.rs b/tests/ui/precondition-checks/read_volatile.rs index ada8932c398ce..d858e6b939ac0 100644 --- a/tests/ui/precondition-checks/read_volatile.rs +++ b/tests/ui/precondition-checks/read_volatile.rs @@ -1,9 +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 - -#![allow(invalid_null_arguments)] +//@ revisions: misaligned use std::ptr; @@ -11,8 +9,6 @@ fn main() { let src = [0u16; 2]; let src = src.as_ptr(); unsafe { - #[cfg(null)] - ptr::read_volatile(ptr::null::()); #[cfg(misaligned)] ptr::read_volatile(src.byte_add(1)); } diff --git a/tests/ui/precondition-checks/write_volatile.rs b/tests/ui/precondition-checks/write_volatile.rs index 0d5ecb014b3d9..ddc882be4323f 100644 --- a/tests/ui/precondition-checks/write_volatile.rs +++ b/tests/ui/precondition-checks/write_volatile.rs @@ -1,9 +1,7 @@ //@ 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; @@ -11,8 +9,6 @@ fn main() { let mut dst = [0u16; 2]; let mut dst = dst.as_mut_ptr(); unsafe { - #[cfg(null)] - ptr::write_volatile(ptr::null_mut::(), 1u8); #[cfg(misaligned)] ptr::write_volatile(dst.byte_add(1), 1u16); }