name: rust-smart-pointers
description: Detect and fix smart-pointer and interior-mutability bugs in Rust. Covers Box (heap allocation, recursive types, trait objects), Deref/DerefMut coercions, Drop, Rc (shared single-thread ownership), Arc (atomic multi-thread), RefCell (interior mutability, runtime borrow check, BorrowMutError panics), Cell, RwLock, Weak (non-owning back-pointers), and reference-cycle memory leaks. Auto-triggers when: Rc<RefCell<…>> combinations appear in code review, RefCell::borrow_mut call sites are present, recursive enum/struct definitions need sizing, !Send compiler errors mention Rc, Arc usage needs review, or Weak::upgrade patterns need analysis. Use when this capability is needed.
metadata:
author: adelabdelgawad
Smart Pointers & Interior Mutability
Rust smart pointers implement Deref/DerefMut (transparent dereferencing) and Drop (deterministic cleanup). Unlike plain references, they own the data they point to and carry extra metadata. Choosing the wrong pointer type causes either a compile error (Rc across threads), a runtime panic (RefCell double-borrow), or a silent memory leak (Rc cycle with no Weak).
Book source: The Rust Programming Language, Chapter 15 — Smart Pointers
When to Use
Invoke this skill when:
- An
Rc<RefCell<…>> combination appears in code review — check for reference cycles and missing Weak back-pointers
- A
RefCell::borrow_mut call site is present — confirm no two mutable guards overlap in scope
- A recursive enum or struct definition needs sizing — ensure
Box<T> is used to break the infinite-size cycle
- A
!Send compiler error mentions Rc — the fix is Arc with appropriate synchronization
Arc<Mutex<T>> usage needs review — confirm shared mutable state is actually concurrent; plain &mut T may suffice
- A
Weak::upgrade call appears — verify the upgrade result is handled rather than unwrapped unconditionally
Which Pointer When
| Need | Use | Notes |
|---|
| Heap-allocate a sized value | Box<T> | Zero overhead beyond the allocation |
| Recursive / self-referential type | Box<T> | Breaks infinite-size cycle with one level of indirection |
Trait object (dyn Trait) | Box<dyn Trait> | Fixed-size pointer to heap-allocated erased type |
| Multiple read-only owners, single thread | Rc<T> | Cheap clone; !Send, !Sync |
| Multiple read-only owners, multi-thread | Arc<T> | Atomic ref-count; Send + Sync when T: Send + Sync |
| Interior mutability, single thread | RefCell<T> | Panics on borrow rule violation at runtime |
| Copy types, single thread, no borrow guard | Cell<T> | Cheaper than RefCell; get/set, no references |
| Shared mutability, multi-thread | Arc<Mutex<T>> or Arc<RwLock<T>> | Mutex blocks; RwLock allows concurrent reads |
| Back-pointer / observer without ownership | Weak<T> | Upgrade returns Option<Rc<T>>; never keeps value alive |
Core Idioms
Box for recursive types
enum List {
Cons(i32, Box<List>),
Nil,
}
enum List {
Cons(i32, List),
Nil,
}
The Book (ch15-01): the Cons variant's size becomes i32 + pointer-width. Without Box the type has no finite representation.
Rc for shared read-only ownership
use std::rc::Rc;
let a = Rc::new(vec![1, 2, 3]);
let b = Rc::clone(&a);
let c = Rc::clone(&a);
let b = a.clone();
Weak for parent/back-pointer to break cycles
use std::rc::{Rc, Weak};
use std::cell::RefCell;
struct Node {
parent: RefCell<Weak<Node>>,
children: RefCell<Vec<Rc<Node>>>,
}
struct Node {
parent: RefCell<Rc<Node>>,
children: RefCell<Vec<Rc<Node>>>,
}
The Book (ch15-06): parent→child is Rc (ownership); child→parent is Weak (non-ownership). upgrade() returns None once the parent is dropped — no dangling pointer, no leak.
RefCell runtime-borrow risk
use std::cell::RefCell;
let data = RefCell::new(vec![1, 2, 3]);
{
let mut w = data.borrow_mut();
w.push(4);
}
let r = data.borrow();
let mut w1 = data.borrow_mut();
let mut w2 = data.borrow_mut();
The Book (ch15-05): RefCell tracks a dynamic borrow count. Acquiring a second mutable guard while one is still live panics — the compile-time borrowing rules are enforced at runtime instead.
Forbidden Patterns
Forbidden 1 — Rc<RefCell> Cycle with No Weak (Memory Leak)
Forbidden:
enum List {
Cons(i32, RefCell<Rc<List>>),
Nil,
}
impl List {
fn tail(&self) -> Option<&RefCell<Rc<List>>> {
match self { List::Cons(_, t) => Some(t), List::Nil => None }
}
}
let a = Rc::new(List::Cons(5, RefCell::new(Rc::new(List::Nil))));
let b = Rc::new(List::Cons(10, RefCell::new(Rc::clone(&a))));
if let Some(tail) = a.tail() {
*tail.borrow_mut() = Rc::clone(&b);
}
Why (Book ch15-06): Rc<T> cleans up only when strong_count reaches 0. A cycle means each node holds a strong reference to the other; strong_count never falls to 0 even after all user-facing bindings are dropped. The heap allocation leaks for the lifetime of the process.
Fix — change the back-pointer field to RefCell<Weak<List>>:
use std::rc::{Rc, Weak};
use std::cell::RefCell;
enum List {
Cons(i32, RefCell<Weak<List>>),
Nil,
}
impl List {
fn tail(&self) -> Option<&RefCell<Weak<List>>> {
match self { List::Cons(_, t) => Some(t), List::Nil => None }
}
}
let a = Rc::new(List::Cons(5, RefCell::new(Weak::new())));
let b = Rc::new(List::Cons(10, RefCell::new(Rc::downgrade(&a))));
if let Some(tail) = a.tail() {
*tail.borrow_mut() = Rc::downgrade(&b);
}
for f in $(grep -rlE 'Rc<RefCell|RefCell<Rc|Rc::new\(.*RefCell::new|RefCell::new\(.*Rc::new' src/); do
grep -qE 'Weak<|Rc::downgrade' "$f" || echo "WARN: $f has Rc+RefCell but no Weak — check for cycles"
done
Forbidden 2 — RefCell::borrow_mut While Another Borrow Is Live (BorrowMutError Panic)
Forbidden:
let cell = RefCell::new(0i32);
let mut w1 = cell.borrow_mut();
let mut w2 = cell.borrow_mut();
Why (Book ch15-05): RefCell<T> enforces Rust's borrow rules dynamically. The RefMut<T> guard increments an internal mutable-borrow counter on acquisition. A second borrow_mut() call while the counter is non-zero panics immediately — this is a runtime crash, not a compile error.
Fix: Narrow guard lifetimes so they don't overlap. Drop explicitly with drop(w1) if needed, or use a block scope.
grep -rno 'borrow_mut()' src/ | cut -d: -f1 | sort | uniq -d
Forbidden 3 — Rc Shared Across Threads (!Send Violation)
Forbidden:
use std::rc::Rc;
use std::thread;
let shared = Rc::new(vec![1, 2, 3]);
let clone = Rc::clone(&shared);
thread::spawn(move || {
println!("{:?}", clone);
});
Why (Book ch15-04, ch16-04): Rc<T> is only for single-threaded use. The compiler enforces !Send on Rc<T> — moving an Rc clone to another thread is a hard compile error. As ch16-04 explains, if two threads updated the reference count at the same time, the count could corrupt. The fix is Arc<T>, which uses atomic operations and is Send + Sync when T: Send + Sync.
Fix:
use std::sync::Arc;
let shared = Arc::new(vec![1, 2, 3]);
let clone = Arc::clone(&shared);
thread::spawn(move || println!("{:?}", clone));
grep -rln 'Rc::' src/ | xargs grep -lE 'thread::spawn|tokio::spawn' 2>/dev/null
Forbidden 4 — Arc<Mutex> Where a Plain &mut or Single Owner Suffices (Needless Sync Overhead)
Forbidden:
async fn process(data: Arc<Mutex<Vec<u8>>>) {
let mut guard = data.lock().unwrap();
guard.push(42);
}
Why (Book ch16-03): Arc uses atomic reference counting — more expensive than Rc because atomics carry a performance penalty not present in single-threaded counting. Mutex adds a lock acquisition on every access. When the value has a single owner at a time, &mut T or owned T carries the same safety guarantee at zero overhead. As ch16-03 notes, thread safety comes with a performance cost you should only pay when you actually need concurrent access. Unnecessary Arc<Mutex> obscures the true ownership model and hides opportunities for better parallelism.
Fix: Pass &mut Vec<u8> or owned Vec<u8> when there is genuinely one concurrent accessor. Introduce Arc<Mutex<T>> only when shared mutable state across concurrent tasks is unavoidable.
grep -rnE 'Arc<Mutex<|Arc::new\(Mutex::new' src/ | grep -vE 'spawn|rayon|crossbeam'
Forbidden 5 — Box Where a Plain Value or Borrow Suffices
Forbidden:
fn add_one(x: Box<i32>) -> Box<i32> {
Box::new(*x + 1)
}
struct Config {
name: Box<String>,
}
Why: Box<T> adds a heap allocation whose only benefit is indirection or enabling an unsized type. For Copy types or types that already own heap data (String, Vec<T>), wrapping in Box adds a pointer-indirection level with no benefit — String already allocates on the heap; the Box just adds another pointer hop. The Clippy lint clippy::box_collection flags Box<String>, Box<Vec<T>>, and similar double-heap patterns automatically.
Fix: Use the type directly. Use Box<T> only when the value must live on the heap (recursive type, large struct transferred by ownership across a move-heavy call graph, or Box<dyn Trait>).
grep -rnE 'Box<String>|Box<Vec<|Box<HashMap<|Box<BTreeMap<' src/
Forbidden 6 — .clone() on Rc Misread as Deep Clone
Forbidden:
let original = Rc::new(expensive_computation());
let copy = original.clone();
Why (Book ch15-04): Rc::clone only increments the reference count — O(1) with no heap allocation. Calling .clone() on an Rc<T> has the same effect but is visually indistinguishable from a deep clone. Code reviewers and profiler annotations will flag .clone() as a potential performance concern; Rc::clone(&val) makes the intent explicit and silent to analysis tools looking for expensive clones.
Fix:
let copy = Rc::clone(&original);
grep -rlnE 'Rc::|Arc::|Rc<|Arc<' src/ | xargs grep -n '\.clone()' 2>/dev/null | grep -vE 'Rc::clone|Arc::clone'
Forbidden 7 — Cell or RefCell for Data That Is Never Mutated
Forbidden:
struct Config {
max_retries: Cell<u32>,
timeout_ms: RefCell<u64>,
}
Why (Book ch15-05): Cell<T> and RefCell<T> exist to provide interior mutability — the ability to mutate through a shared (&self) reference when the compiler cannot verify the borrow rules statically. If the value is never mutated after construction, the indirection and dynamic borrow tracking are pure overhead. The Cell/RefCell wrapper also signals to readers that mutation is expected, causing unnecessary cognitive load during code review.
Fix: Use plain fields, const, or static for immutable data. Reserve Cell/RefCell for fields that genuinely need to be mutated through a shared reference (lazy initialization, mock recorders, arena allocators).
grep -rnE 'Cell<|RefCell<' src/
grep -rnE 'borrow_mut\(\)|\.set\(|\.replace\(' src/
Book References
Verification Hooks
Run this sweep to catch all seven forbidden patterns in one pass:
for f in $(grep -rlE 'Rc<RefCell|RefCell<Rc|Rc::new\(.*RefCell::new|RefCell::new\(.*Rc::new' src/); do
grep -qE 'Weak<|Rc::downgrade' "$f" || echo "WARN F1: $f — Rc+RefCell without Weak"
done
grep -rno 'borrow_mut()' src/ | cut -d: -f1 | sort | uniq -d | sed 's/^/WARN F2: /'
grep -rln 'Rc::' src/ | xargs grep -lE 'thread::spawn|tokio::spawn' 2>/dev/null | sed 's/^/WARN F3: /'
grep -rnE 'Arc<Mutex<|Arc::new\(Mutex::new' src/ | grep -vE 'spawn|rayon|crossbeam' | sed 's/^/WARN F4: /'
grep -rnE 'Box<String>|Box<Vec<|Box<HashMap<|Box<BTreeMap<' src/ | sed 's/^/WARN F5: /'
grep -rlnE 'Rc::|Arc::|Rc<|Arc<' src/ | xargs grep -n '\.clone()' 2>/dev/null | grep -vE 'Rc::clone|Arc::clone' | sed 's/^/WARN F6: /'
for f in $(grep -rlE 'Cell<|RefCell<' src/);
grep -qE ||
Related Skills
- rust-ownership-borrowing — foundational ownership and lifetime rules that smart pointers extend
- rust-concurrency — Mutex, RwLock, channels, and Send/Sync bounds that pair with Arc
- rust-lifetimes — lifetime annotations that interact with Deref coercions and borrow scopes
- rust-error-handling — how
RefCell::borrow_mut panics propagate through server fn and handler call stacks (see also leptos-hydration-discipline Forbidden 9: Rc/RefCell held across .await must become Arc/drop-before-await)
Source: adelabdelgawad/rust-fullstack-agents — distributed by TomeVault.