# Unsafe boundaries and soundness

Review a small unsafe function, explain its safety rules, and check it with Miri.

Canonical: https://rust.robertdevore.com/course/23-unsafe/
Author: Robert DeVore
Technical baseline: Rust 1.98.1, edition 2024; verified 2026-09-06.

## Unsafe does not suspend Rust's rules

Unsafe operations have requirements the compiler cannot completely verify. An unsafe block makes the programmer responsible for meeting those requirements; undefined behavior remains forbidden. A safe API is sound only if no allowed safe caller can trigger undefined behavior through it.

The lab reimplements a tiny slice split for study. Production application code should use the standard `split_at_mut`. The separate lab lets us study unsafe code while forbidding it in the application.


```rust
//! Study-only unsafe boundary. In application code prefer `slice::split_at_mut`.
/// Split at a checked index, preserving exclusive access to disjoint elements.
///
/// Panics if `mid > slice.len()`. Safe callers have no extra obligations.
pub fn split<T>(slice: &mut [T], mid: usize) -> (&mut [T], &mut [T]) {
    let len = slice.len();
    assert!(mid <= len, "split index exceeds length");
    let ptr = slice.as_mut_ptr();
    // SAFETY: ptr comes from a valid exclusive slice. mid <= len keeps the
    // offset in the allocation or one-past. Both ranges contain initialized T,
    // are aligned/non-null even when empty, and partition the original range.
    // Returned lifetimes are tied to slice; the original borrow cannot be used
    // while these reborrows are live. ZSTs have disjoint logical elements even
    // when their addresses coincide. No ownership or destructor is duplicated.
    unsafe {
        (
            std::slice::from_raw_parts_mut(ptr, mid),
            std::slice::from_raw_parts_mut(ptr.add(mid), len - mid),
        )
    }
}
#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn partitions() {
        for mid in 0..=4 {
            let mut data = [0; 4];
            let (a, b) = split(&mut data, mid);
            a.fill(1);
            b.fill(2);
            assert_eq!(data[..mid], vec![1; mid]);
            assert_eq!(data[mid..], vec![2; 4 - mid]);
        }
    }
    #[test]
    fn empty_and_zst() {
        let mut empty: [u8; 0] = [];
        assert!(split(&mut empty, 0).0.is_empty());
        let mut z = [(); 4];
        let (a, b) = split(&mut z, 2);
        assert_eq!((a.len(), b.len()), (2, 2));
    }
    #[test]
    fn drops_once() {
        use std::rc::Rc;
        let token = Rc::new(());
        {
            let mut data = [token.clone(), token.clone()];
            let (a, b) = split(&mut data, 1);
            std::mem::swap(&mut a[0], &mut b[0]);
        }
        assert_eq!(Rc::strong_count(&token), 1);
    }
    #[test]
    #[should_panic(expected = "split index exceeds length")]
    fn rejects_oob() {
        split(&mut [1], 2);
    }
}
```

[View the complete source](https://github.com/robertdevore/rust.robertdevore.com/blob/main/crates/unsafe-lab/src/lib.rs)


## The safety argument

**What must remain true?** Both returned slices cover initialized elements from the original slice, stay within its allocation, retain valid alignment and provenance, and refer to disjoint logical element ranges. Their references cannot outlive the original borrow.

**Who maintains it?** The caller supplies a valid exclusive slice through a safe Rust type. The function checks the split index before pointer arithmetic. Its implementation constructs exactly the two ranges. Borrow checking relates the returned lifetimes to the input.

**Which safe callers are allowed?** Any valid mutable slice, including empty slices, zero-sized element types, and types with destructors. A split at either endpoint is valid. An out-of-range index must panic before unsafe operations. Safe callers must not need to follow extra, undocumented rules.

**How is the unsafe code kept small?** One unsafe block follows a checked boundary. The raw pointer does not escape. No allocation is freed and no element ownership is duplicated. The application never depends on this study implementation.

**What would make it unsound?** Removing the bounds check, overlapping nonempty element ranges, inventing a longer return lifetime, accepting an arbitrary unvalidated pointer, or allowing conflicting access through the original slice while the returned borrows are live.

## Aliasing is more than addresses

Zero-sized elements can share an address without representing overlapping stored bytes. A simplistic “different addresses means safe” argument would fail to cover them. Likewise, `UnsafeCell` relaxes shared-reference immutability for its contents but does not erase exclusive-reference rules or synchronize concurrent accesses.

The Reference explicitly notes that exact aliasing rules remain an active specification area. Miri checks executions under its models. A passing run does not prove all executions safe, and its models are not the final language specification. We test ordinary elements, endpoints, empty input, zero-sized elements, and drop behavior, then inspect the invariant argument separately.

## Exercise

Run the isolated lab under Miri:

```sh
rustup toolchain install nightly-2026-09-05 --profile minimal --component miri
cargo +nightly-2026-09-05 miri test -p unsafe-lab
```

Explain why no destructor may run twice and why a safe caller using `mem::forget` must not invalidate the abstraction. Do not execute deliberately undefined examples in a normal production process.

<details><summary>Solution and acceptance check</summary>

The function returns borrows, not new owners of elements. Dropping a slice reference does not drop its elements. The original owner retains destruction responsibility. Forgetting a reference does not produce conflicting access by itself; the implementation does not rely on its destructor running. The tests check selected cases. The safety argument must cover every allowed safe caller.

</details>

Sources: [undefined behavior](https://doc.rust-lang.org/reference/behavior-considered-undefined.html), [Rustonomicon](https://doc.rust-lang.org/nomicon/), [`from_raw_parts_mut`](https://doc.rust-lang.org/std/slice/fn.from_raw_parts_mut.html), and [Miri](https://github.com/rust-lang/miri).

