Skip to content

[Coding Rule RST] prevent torn reads and writes #332

@rcseacord

Description

@rcseacord

.. guideline:: gui_TornReadWrite
:category: Concurrency
:status: Draft
:rule:

Prevent torn reads and writes during concurrent shared data access

.. rationale:: gui_TornReadWrite

A torn read (or torn write) occurs when a memory operation is not atomic at the hardware level,
allowing one thread to observe a partially-updated value written by another thread.
On x86-64, for example, naturally-aligned accesses up to 8 bytes are guaranteed atomic by the hardware.
However, tearing can occur when:

  • Misaligned access crosses a cache line boundary — A 64-bit value straddling a 64-byte cache line boundary may be written in two separate bus transactions, allowing another core to observe an intermediate state.

  • Access size exceeds native word width — Operations on 128-bit values (e.g., u128) are not atomic on most architectures without special instructions.

  • Compiler optimizations — Without proper atomic or volatile semantics, the compiler may split or reorder memory accesses.

In safety-critical systems, torn reads can cause:

  • Corrupted sensor readings leading to incorrect control decisions
  • Invalid state machine transitions
  • Violated invariants in concurrent data structures
  • Non-deterministic behavior that is extremely difficult to reproduce and diagnose

The Rust memory model, following C++11, does not guarantee any atomicity for non-atomic types accessed concurrently.
Data races on non-atomic types are undefined behavior, regardless of hardware atomicity guarantees.

.. non_compliant_example:: gui_TornReadWrite

In this example, a u64 is shared between threads using raw pointers without atomic operations.
Even if the value happens to be aligned, this constitutes a data race and is undefined behavior.
If the value crosses a cache line boundary, torn reads become likely:

.. code-block:: rust

  use std::thread;

  static mut SHARED: u64 = 0;

  fn main() {
      // UNSAFE: Data race - undefined behavior
      let writer = thread::spawn(|| {
          for _ in 0..1_000_000 {
              unsafe {
                  // Non-atomic write to shared data
                  SHARED = 0xFFFF_FFFF_FFFF_FFFF;
                  SHARED = 0;
              }
          }
      });

      let reader = thread::spawn(|| {
          for _ in 0..1_000_000 {
              unsafe {
                  // Non-atomic read - may observe torn value
                  let val = SHARED;
                  // val could be 0, 0xFFFFFFFFFFFFFFFF,
                  // or a torn value like 0x00000000FFFFFFFF
                  process(val);
              }
          }
      });

      writer.join().unwrap();
      reader.join().unwrap();
  }

.. compliant_example:: gui_TornReadWrite

The corrected example uses AtomicU64 to ensure all accesses are atomic and free from tearing:

.. code-block:: rust

  use std::sync::atomic::{AtomicU64, Ordering};
  use std::thread;

  static SHARED: AtomicU64 = AtomicU64::new(0);

  fn main() {
      let writer = thread::spawn(|| {
          for _ in 0..1_000_000 {
              // Atomic write - guaranteed not to tear
              SHARED.store(0xFFFF_FFFF_FFFF_FFFF, Ordering::Relaxed);
              SHARED.store(0, Ordering::Relaxed);
          }
      });

      let reader = thread::spawn(|| {
          for _ in 0..1_000_000 {
              // Atomic read - guaranteed to observe a complete value
              let val = SHARED.load(Ordering::Relaxed);
              // val is guaranteed to be either 0 or 0xFFFFFFFFFFFFFFFF
              process(val);
          }
      });

      writer.join().unwrap();
      reader.join().unwrap();
  }

For larger types or complex data structures, use a Mutex or RwLock:

.. code-block:: rust

  use std::sync::{Arc, RwLock};
  use std::thread;

  #[derive(Clone, Default)]
  struct SensorData {
      timestamp: u64,
      position: [f64; 3],
      velocity: [f64; 3],
  }

  fn main() {
      let data = Arc::new(RwLock::new(SensorData::default()));

      let data_writer = Arc::clone(&data);
      let writer = thread::spawn(move || {
          loop {
              let reading = read_sensor();
              // Lock ensures entire struct is updated atomically
              let mut guard = data_writer.write().unwrap();
              *guard = reading;
          }
      });

      let data_reader = Arc::clone(&data);
      let reader = thread::spawn(move || {
          loop {
              // Lock ensures entire struct is read atomically
              let guard = data_reader.read().unwrap();
              let snapshot = guard.clone();
              drop(guard);
              process(snapshot);
          }
      });
  }

.. bibliography:: gui_TornReadWrite

.. list-table::
:widths: 10 90

  * - :cite:t:`rustonomicon-atomics`
    - "Atomics." *The Rustonomicon*. Accessed December 18, 2025. https://doc.rust-lang.org/nomicon/atomics.html.
  * - :cite:t:`rust-reference-data-races`
    - "Behavior considered undefined: Data races." *The Rust Reference*. Accessed December 18, 2025. https://doc.rust-lang.org/reference/behavior-considered-undefined.html.
  * - :cite:t:`intel-sdm-vol3`
    - Intel Corporation. *Intel® 64 and IA-32 Architectures Software Developer's Manual, Volume 3A: System Programming Guide*. Section 9.1.1, "Guaranteed Atomic Operations." 2024.
  * - :cite:t:`boehm-2005`
    - Boehm, Hans-J. "Threads Cannot Be Implemented as a Library." *ACM SIGPLAN Notices* 40, no. 6 (June 2005): 261–268. https://doi.org/10.1145/1065010.1065042.
  * - :cite:t:`adve-gharachorloo-1996`
    - Adve, Sarita V., and Kourosh Gharachorloo. "Shared Memory Consistency Models: A Tutorial." *Computer* 29, no. 12 (December 1996): 66–76.

Metadata

Metadata

Assignees

Labels

coding guidelineAn issue related to a suggestion for a coding guideline

Type

No type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions