Skip to content

Ptr::iter allows creating aliasing exclusive Ptrs, leading to mutable reference aliasing #3419

@squiffy

Description

@squiffy

I found a soundness issue with Ptr::iter where calling it twice on the same Exclusive Ptr lets you get aliasing &mut references, which Miri confirms as UB.

Reproduction

Cargo.toml:

[package]
name = "ptr_iter_repro"
version = "0.1.0"
edition = "2024"

[dependencies]
zerocopy = "0.8.49"

src/main.rs:

use zerocopy::Ptr;

fn main() {
    let mut arr = [0u8, 1, 2, 3];
    let p = Ptr::from_mut(&mut arr[..]);
    let a = p.iter().next().unwrap();
    let b = p.iter().next().unwrap();

    let m1: &mut u8 = a.as_mut();
    let m2: &mut u8 = b.as_mut();

    *m1 = 10;
    *m2 = 20;
    *m1 = 30;

    println!("m1={} m2={}", *m1, *m2);
}
$ cargo miri run                                                                                                                                       06:54:33
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.00s
     Running `/Users/squiffy/.rustup/toolchains/nightly-2025-07-20-aarch64-apple-darwin/bin/cargo-miri runner target/miri/aarch64-apple-darwin/debug/ptr_iter_repro`
error: Undefined Behavior: attempting a write access using <834> at alloc240[0x0], but that tag does not exist in the borrow stack for this location
  --> src/main.rs:12:5
   |
12 |     *m1 = 10;
   |     ^^^^^^^^ this error occurs as part of an access at alloc240[0x0..0x1]
   |
   = help: this indicates a potential bug in the program: it performed an invalid operation, but the Stacked Borrows rules it violated are still experimental
   = help: see https://github.com/rust-lang/unsafe-code-guidelines/blob/master/wip/stacked-borrows.md for further information
help: <834> was created by a Unique retag at offsets [0x0..0x1]
  --> src/main.rs:9:23
   |
9  |     let m1: &mut u8 = a.as_mut();
   |                       ^^^^^^^^^^
help: <834> was later invalidated at offsets [0x0..0x1] by a Unique retag
  --> src/main.rs:10:23
   |
10 |     let m2: &mut u8 = b.as_mut();
   |                       ^^^^^^^^^^
   = note: BACKTRACE (of the first span):
   = note: inside `main` at src/main.rs:12:5: 12:13

note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace

error: aborting due to 1 previous error

Root Cause

The issue is that Ptr::iter takes &self:

pub fn iter(&self) -> impl Iterator<Item = Ptr<'a, T, I>> { ... }

Since it borrows rather than consumes, nothing stops you from calling it multiple times on the same Exclusive Ptr. Each call hands back fresh Ptr<'a, T, (Exclusive, ...)> values that all point to the same memory, so the exclusivity invariant is quietly broken.

Suggested Fix

I think the cleanest fix would be to split the method into shared/exclusive variants, similar to how std does iter vs iter_mut. Specifically:

  • iter(&self): constrained to Shared aliasing only
  • iter_mut(self): for Exclusive, consuming the Ptr so it can't be reused

Alternatively, iter could just consume self whenever the aliasing invariant is Exclusive.

Versions

  • zerocopy 0.8.49
  • rustc nightly (Miri)
  • macOS / aarch64

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions