Tobias Keller·
DeepSeek R1 is my second reviewer for unsafe Rust now. This prompt got it to spot an aliasing bug in a ring buffer that two human reviewers had already approved.
A soundness-focused review protocol for unsafe Rust: verifies each block's safety contract against the documented invariants instead of vibes.
Unsafe Rust Block Review
You are reviewing unsafe Rust for soundness. Not style, not performance — soundness. Every unsafe block is a claim that the programmer upholds invariants the compiler cannot check; your job is to find where that claim is false.
Code:
{{code}}
Documented invariants and safety comments from the module:
{{invariants}}
For EACH unsafe block, produce this analysis:
1. **Obligation inventory.** List the exact safety contracts of every unsafe operation used (raw pointer deref, `get_unchecked`, `transmute`, `from_raw_parts`, FFI call, `Send`/`Sync` impl). Quote the requirement from the standard library docs from memory — validity, alignment, aliasing, initialization, lifetime.
2. **Discharge check.** For each obligation: is it discharged locally (visible in this function), structurally (by a type invariant maintained elsewhere — name the maintaining code), or NOT discharged? An obligation discharged by 'the caller is careful' on a non-unsafe fn is a soundness hole — flag it.
3. **Aliasing analysis.** Trace every path where a `&mut` and any other reference or raw pointer could observe the same memory while the `&mut` is live. Pay attention to reborrows through `self`, pointer arithmetic that can wrap into another allocation's range, and references materialized from raw pointers inside loops.
4. **Panic and unwind safety.** If any code between acquiring the invariant-breaking state and restoring it can panic, what does the unwinder observe? Check Drop impls.
5. **Adversarial instantiation.** Construct the most hostile safe-code caller you can: zero-sized types, `T: Drop` with a panicking drop, maximum-length inputs, concurrent access if the type is `Sync`. Show the concrete call sequence that triggers UB, or state why none exists.
Verdict per block: SOUND / UNSOUND (with the UB-triggering sequence) / UNPROVEN (obligation exists that neither code nor comments discharge). Suggest the minimal fix — often a debug_assert, a tightened bound, or making a function `unsafe` and documenting the real contract.
Ergebnisse
## Review: `RingBuffer::push_slice` — 2 unsafe blocks
### Block 1 (lines 47–52): `ptr::copy_nonoverlapping` into the tail region
**1. Obligations:** `copy_nonoverlapping` requires: src valid for `len` reads, dst valid for `len` writes, both properly aligned, and the regions must NOT overlap. `dst = self.buf.as_mut_ptr().add(self.tail)` additionally requires the resulting pointer stays within (or one past) the allocation.
**2. Discharge check:** validity and alignment are discharged structurally by `buf: Box<[MaybeUninit<T>]>` — fine. The in-bounds obligation on `.add(self.tail)` relies on the invariant `tail < cap` maintained in `advance_tail()`. Verified: `advance_tail` masks with `cap - 1`... but only when `cap.is_power_of_two()`. Your `with_capacity` rounds up to a power of two — **however `with_capacity_exact` (line 19) does not**, and it sets the same `cap`. Obligation NOT discharged for buffers created via that constructor. **UNPROVEN → effectively UNSOUND**: `with_capacity_exact(6)` then pushing 7 elements computes `tail & 5` = wrong slot but in-bounds by luck for some values; `tail & (cap-1)` with cap=6 masks to 0..=5 — in-bounds, but slot aliasing occurs (see Block 2).
**3. Aliasing (the real bug):** with non-power-of-two cap, the mask makes `head` and `tail` land on the same slot while `len < cap`, so `push_slice`'s `copy_nonoverlapping` dst can overlap the region a concurrently-borrowed `peek()` slice refers to. `peek` returns `&[T]` tied to `&self`; `push_slice` takes `&mut self`, so the borrow checker saves you in safe code — **but** `iter_unchecked` (line 88) materializes a `&[T]` from a raw pointer with lifetime `'static`-ish laundering via `slice::from_raw_parts(p, len)` and a transmuted lifetime. That slice can be alive across a `push_slice`. Concrete UB sequence:
```rust
let mut rb = RingBuffer::with_capacity_exact(6);
rb.push_slice(&[1,2,3]);
let s = unsafe { rb.iter_unchecked() }; // &[i32] escapes
rb.push_slice(&[9,9,9,9]); // writes overlap s
let _ = s[0]; // read of mutated memory: UB
```
**4. Panic safety:** `T: Clone` path clones before the copy; a panicking clone leaves `tail` un-advanced — state stays consistent. OK.
**Verdicts:** Block 1: UNSOUND via `with_capacity_exact` + `iter_unchecked` interaction. Block 2 (`iter_unchecked`): UNSOUND as a safe-callable fn — the lifetime transmute hands out an unbounded borrow.
**Minimal fixes:** (1) make `with_capacity_exact` round up or store a `mask` computed only from power-of-two caps, with `debug_assert!(cap.is_power_of_two())` in `advance_tail`; (2) mark `iter_unchecked` as `unsafe fn` with a documented no-mutation-while-borrowed contract, or return the borrow tied to `&self` properly.
Modell: DeepSeek R1
26 Likes14 SavesScore: 20
2 Kommentare
Jonas Weber·
The adversarial instantiation step is what separates this from 'looks fine to me' reviews. Constructing the hostile caller is exactly what humans skip when tired.
Emily Chen·
I write zero unsafe Rust and still read the whole output like a thriller. The escaped-borrow sequence is beautifully horrible.
