The UB classes an unsafe audit looks for, each with the rule, a bad example, a fixed version and how to
detect it. Authoritative sources: the Rust Reference "Behavior considered undefined", the std docs
# Safety sections, and the Rustonomicon. Rule ids (USxxx) refer to the scanners in scripts/ (see SKILL.md).
Contents
- What counts as UB
- Aliasing: Stacked Borrows and Tree Borrows
- Mutation behind a shared reference
- Invalid values
- Uninitialised memory and MaybeUninit
- mem::zeroed
- transmute pitfalls
- slice::from_raw_parts
- Vec::set_len and spare capacity
- Vec::from_raw_parts and Box::from_raw
- get_unchecked and unreachable_unchecked
- Dangling pointers from temporaries
- static mut
- Unions
- Send and Sync
- Pin projection
- Panic safety and double drop
- Quick precondition table
What counts as UB
From the Reference (abridged): data races; accessing a dangling or misaligned pointer through a place
expression; violating the aliasing rules of references and Box; mutating immutable bytes (behind &T
without UnsafeCell, or in a non-mut static/const); calling a function with the wrong ABI or unwinding
out of a function whose ABI does not allow it; producing an invalid value (even if it is never read
again); invoking compiler intrinsics or unsafe fns outside their contracts; executing code compiled with
target features the CPU lacks; incorrect inline assembly.
Library UB is separate but equally binding: violating a # Safety precondition of a std function (for
example a str that is not UTF-8, or Vec::set_len past initialised elements) lets std code make
assumptions that lead to language UB later.
Aliasing: Stacked Borrows and Tree Borrows
Rust's aliasing model is not finalised; Miri implements two candidate models:
- Stacked Borrows (Miri default): each location keeps a stack of permissions; creating a new
&mut(or using the parent) pops permissions derived from other paths, so older raw pointers become invalid. - Tree Borrows (
MIRIFLAGS=-Zmiri-tree-borrows): permissions form a tree; it accepts some patterns Stacked Borrows rejects (notably around raw pointers obtained from&mutand two-phase borrows) and is the likely basis of the eventual model.
Write code that passes both. The practical rules:
- A
&mut Tis unique while it is live: no other pointer or reference may read or write that memory while it is in use (except pointers derived from it). - Creating or using a
&mut(or the original owner) invalidates raw pointers derived before it. - Derive every raw pointer you need from one base pointer, and do not touch the owner until done.
// BAD: `r` is created after `p`, and using `p` afterwards is UB under Stacked Borrows.
fn bad() -> u32 {
let mut x = 0u32;
let p = &mut x as *mut u32;
let r = &mut x; // new unique borrow of x
unsafe { *p = 1 }; // p was derived from an older borrow: invalidated
*r += 1;
x
}
// GOOD: one base pointer; the owner is not used until the raw accesses finish.
fn good() -> u32 {
let mut x = 0u32;
let p = &raw mut x; // Rust 1.82+; use std::ptr::addr_of_mut!(x) on older compilers
// SAFETY: p points to a live local; no reference to x exists while p is used.
unsafe {
*p = 1;
*p += 1;
}
x
}Common real-world variants:
| Pattern | Problem | Fix |
|---|---|---|
let a = slice.as_mut_ptr(); let b = slice.as_mut_ptr(); then use a | second as_mut_ptr() reborrows the slice mutably, invalidating a | call as_mut_ptr() once, derive b = a.add(k) |
let p = v.as_ptr(); v.push(x); *p | reallocation frees the old buffer | re-derive after any method that may reallocate |
&mut *ptr while a &*ptr from the same pointer is still used | &mut asserts uniqueness | keep raw pointers; create references only for disjoint, non-overlapping uses |
two &mut to fields obtained via raw pointers to the whole struct | each &mut *struct_ptr claims the whole struct | project with &raw mut (*p).field and create the field reference from that |
Box<T> aliased by a raw pointer that outlives a move of the Box | moving a Box asserts uniqueness like &mut (under Stacked Borrows) | Box::into_raw and work only with the raw pointer, rebuild with from_raw |
Mutation behind a shared reference
Writing through a pointer derived from &T is UB unless the memory is inside an UnsafeCell
(directly or via Cell, RefCell, Mutex, atomics, ...). invalid_reference_casting (deny-by-default
since 1.73) catches the direct &T as *const T as *mut T write.
use std::cell::UnsafeCell;
// BAD: &self gives shared access; writing through a cast pointer is UB.
pub struct Counter { n: u64 }
impl Counter {
pub fn bump(&self) {
let p = &self.n as *const u64 as *mut u64;
unsafe { *p += 1 }; // UB
}
}
// GOOD: interior mutability; still !Sync automatically because UnsafeCell is !Sync.
pub struct Counter2 { n: UnsafeCell<u64> }
impl Counter2 {
pub fn bump(&self) {
// SAFETY: Counter2 is !Sync, so no other thread can access `n`; no reference into `n`
// escapes this method, so there is no aliasing &mut.
unsafe { *self.n.get() += 1 };
}
}Safe alternative: Cell<u64> (single thread) or AtomicU64 (shared).
Invalid values
Producing (not just reading) any of these is UB, including in a local that is never used:
| Type | Invalid |
|---|---|
bool | any byte other than 0 or 1 |
char | surrogates 0xD800..=0xDFFF and values above 0x10FFFF |
fieldless enum | a discriminant that matches no variant |
&T, &mut T, Box<T> | null, dangling, unaligned, or pointing to an invalid T |
fn pointers | null |
NonNull<T>, NonZero<u32> etc. | zero / null |
| integers, floats, raw pointers | uninitialised bytes (any bit pattern is fine once initialised) |
! (never) | any value at all |
dyn Trait pointer metadata | a vtable that is not a valid vtable for the trait |
str | non-UTF-8 bytes (library invariant: std may assume it) |
// BAD
fn to_bool_bad(b: u8) -> bool {
unsafe { std::mem::transmute(b) } // UB for b > 1
}
// GOOD
fn to_bool(b: u8) -> bool {
b != 0
}
#[derive(Debug, Clone, Copy)]
#[repr(u8)]
enum Op { Add = 0, Sub = 1 }
impl TryFrom<u8> for Op {
type Error = u8;
fn try_from(v: u8) -> Result<Self, u8> {
match v {
0 => Ok(Op::Add),
1 => Ok(Op::Sub),
other => Err(other),
}
}
}Uninitialised memory and MaybeUninit
mem::uninitialized() (deprecated since 1.39, US004) is UB for almost every type because the value is
invalid the moment it exists. MaybeUninit<T> (1.36) is the replacement: it may hold uninitialised bytes,
and assume_init is the point where you promise it is fully initialised.
use std::mem::MaybeUninit;
#[derive(Debug)]
struct Point { x: f64, y: f64 }
fn make_point() -> Point {
let mut p = MaybeUninit::<Point>::uninit();
let ptr = p.as_mut_ptr();
// SAFETY: field projection through a raw pointer creates no reference to uninitialised memory;
// both fields are written before assume_init.
unsafe {
(&raw mut (*ptr).x).write(1.0);
(&raw mut (*ptr).y).write(2.0);
p.assume_init()
}
}
// Arrays: an array of MaybeUninit needs no initialisation (inline const, 1.79+).
fn squares() -> [u64; 8] {
let mut buf = [const { MaybeUninit::<u64>::uninit() }; 8];
for (i, slot) in buf.iter_mut().enumerate() {
slot.write((i as u64) * (i as u64));
}
// SAFETY: every element was written by the loop above.
buf.map(|s| unsafe { s.assume_init() })
}Rules:
MaybeUninit::uninit().assume_init()in one expression is UB (US006, clippyuninit_assumed_init), except whenTis itselfMaybeUninitor an array of it.- Never create
&T/&mut Tto uninitialised memory; write throughas_mut_ptr()/&raw mut. assume_init_readcopies the value out: theMaybeUninitstill holds the bits; do not read twice for a non-Copytype (double drop).- Padding bytes are uninitialised: transmuting a struct with padding to
[u8; N]reads uninit memory. Usebytemuck/zerocopyderives, which reject padded types. std::array::from_fn(|i| ...)(1.63) builds arrays without any unsafe.
mem::zeroed
mem::zeroed::<T>() is sound only if the all-zero bit pattern is a valid T: integers, floats, raw
pointers, bool (false), char ('\0'), Option<&T>, Option<Box<T>>, Option<NonNull<T>>
(all None), and #[repr(C)] structs made only of such fields (typical for C structs). It is UB
for references, Box, Vec, String, NonNull, NonZero*, function pointers, and enums without a
zero discriminant (US005; rustc's invalid_value lint warns on the obvious cases).
#[repr(C)]
struct CConfig { flags: u32, timeout_ms: u32, name: *const std::ffi::c_char }
fn config_zeroed() -> CConfig {
// SAFETY: CConfig is repr(C) with integer and raw-pointer fields only, so all-zero is a
// valid value (a null `name` is what the C API documents as "default").
unsafe { std::mem::zeroed() }
}
// Usually better: no unsafe at all. (Raw pointers do not implement Default, so write it by hand.)
impl Default for CConfig {
fn default() -> Self {
Self { flags: 0, timeout_ms: 0, name: std::ptr::null() }
}
}For Rust-only structs always use Default (or a constructor) instead of zeroed.
transmute pitfalls
mem::transmute::<T, U> checks only that the sizes are equal (at compile time). Everything else is on you
(US003):
| Pitfall | Example | Use instead |
|---|---|---|
| lifetime extension | transmute::<&'a str, &'static str>(s) | redesign ownership (String, Arc<str>, Box::leak if a leak is intended) |
&T to &mut T | transmute::<&T, &mut T>(r) | always UB; use UnsafeCell-based types |
| int to bool/char/enum | transmute::<u8, bool>(b) | b != 0, char::from_u32, TryFrom |
| bytes to int | transmute::<[u8; 4], u32>(b) | u32::from_ne_bytes(b) / from_le_bytes |
| float bits | transmute::<f32, u32>(f) | f.to_bits(), f32::from_bits(u) |
| pointer casts | transmute::<*const u8, *const u32>(p) | p.cast::<u32>() |
| int to pointer | transmute::<usize, *const T>(a) | ptr::with_exposed_provenance(a) or keep the pointer (see provenance reference) |
| collections | transmute::<Vec<u32>, Vec<f32>>(v) | into_iter().map(f32::from_bits).collect(); or from_raw_parts with identical layout (clippy unsound_collection_transmute) |
repr(Rust) structs | transmute::<A, B>(a) with same fields | layout of repr(Rust) is unspecified: add #[repr(C)] or convert field by field |
| inferred types | let x: Foo = transmute(y) | always write the turbofish transmute::<Src, Dst> (clippy missing_transmute_annotations) |
transmute_copy is worse: it does not even check sizes and reads size_of::<Dst>() bytes from the
source.
slice::from_raw_parts
std::slice::from_raw_parts(data, len) requires (US013):
datanon-null and aligned forT, even whenlen == 0(useNonNull::dangling()/ptr::dangling()for empty slices; a null pointer from C must be special-cased).datavalid for reads oflen * size_of::<T>()bytes, all within one allocation.- All
lenelements initialised and valid values ofT. - The memory not mutated (except inside
UnsafeCell) for the returned lifetime; forfrom_raw_parts_mut, not accessed through any other pointer at all. len * size_of::<T>() <= isize::MAX.
/// # Safety
///
/// If `len > 0`, `ptr` must be non-null, aligned, and valid for reads of `len` initialised `u16`s that
/// are not written while the returned slice is alive.
pub unsafe fn c_array<'a>(ptr: *const u16, len: usize) -> &'a [u16] {
if len == 0 {
return &[]; // C may pass NULL for empty arrays; from_raw_parts(NULL, 0) is UB
}
debug_assert!(!ptr.is_null() && ptr.is_aligned());
// SAFETY: len > 0 and the caller guarantees the rest of the from_raw_parts contract.
unsafe { std::slice::from_raw_parts(ptr, len) }
}Since Rust 1.78 debug builds check some of these preconditions (null, alignment, size) at runtime and abort; a release build silently has UB.
Vec::set_len and spare capacity
set_len(new_len) requires new_len <= capacity() and the elements old_len..new_len to be initialised
(US007; clippy uninit_vec flags with_capacity followed by set_len).
// BAD: exposes uninitialised u8s as initialised.
fn read_bad(n: usize) -> Vec<u8> {
let mut buf = Vec::with_capacity(n);
unsafe { buf.set_len(n) };
buf
}
// GOOD (safe): zeroing is cheap compared with I/O.
fn read_zeroed(n: usize) -> Vec<u8> {
vec![0u8; n]
}
// GOOD (unsafe, when filling from a source that writes the bytes):
fn fill_from(src: &[u8]) -> Vec<u8> {
let mut buf: Vec<u8> = Vec::with_capacity(src.len());
let spare = buf.spare_capacity_mut(); // &mut [MaybeUninit<u8>], 1.60+
for (dst, &b) in spare.iter_mut().zip(src) {
dst.write(b);
}
// SAFETY: the loop wrote exactly src.len() elements (spare.len() >= src.len() by
// with_capacity), so 0..src.len() is initialised and within capacity.
unsafe { buf.set_len(src.len()) };
buf
}For FFI reads, pass buf.as_mut_ptr() and the capacity, then set_len(bytes_written) using the count the
C function reports, after checking it is <= capacity.
Vec::from_raw_parts and Box::from_raw
Vec::from_raw_parts(ptr, length, capacity) (US014) requires that ptr was allocated by the same
allocator (the global allocator for Vec<T>) with exactly the layout of capacity elements of T,
length <= capacity, and the first length elements initialised. String::from_raw_parts adds UTF-8.
Box::from_raw requires a pointer from Box::into_raw (or an allocation with the same layout from the
global allocator), and that it is reconstructed only once.
use std::mem::ManuallyDrop;
/// Splits a Vec into raw parts without freeing it.
fn into_parts(v: Vec<u32>) -> (*mut u32, usize, usize) {
let mut v = ManuallyDrop::new(v); // stable alternative to the unstable Vec::into_raw_parts
(v.as_mut_ptr(), v.len(), v.capacity())
}
/// # Safety
///
/// The arguments must come from one call to `into_parts`, and be passed here exactly once.
unsafe fn from_parts(ptr: *mut u32, len: usize, cap: usize) -> Vec<u32> {
// SAFETY: same allocator, element type, length and capacity as the original Vec (contract).
unsafe { Vec::from_raw_parts(ptr, len, cap) }
}Never: Vec::from_raw_parts(malloc_ptr, n, n) (C allocator), from_raw_parts(ptr, len, len) when the
original capacity was larger (wrong dealloc layout), Box::from_raw on a pointer to a stack value or to an
element inside a Vec.
get_unchecked and unreachable_unchecked
Out-of-bounds get_unchecked is UB even if the value is never used (US012). Measure before using it:
LLVM removes bounds checks when it can see the proof, so hoist the check.
// Usually as fast as get_unchecked: one check, then the loop has none.
fn sum_first(v: &[u64], n: usize) -> u64 {
let v = &v[..n]; // panics once if n > len
v.iter().sum()
}
/// # Safety
///
/// `idx` must be < `table.len()`.
unsafe fn lookup(table: &[u32; 256], idx: usize) -> u32 {
// SAFETY: caller guarantees idx < 256.
unsafe { *table.get_unchecked(idx) }
}std::hint::unreachable_unchecked() (US015) tells the optimiser the path is impossible; reaching it is UB.
unreachable!() costs one cold branch. std::hint::assert_unchecked(cond) (1.81) is the same contract for
a boolean condition. Use them only with a proof and a measured win.
Dangling pointers from temporaries
use std::ffi::{CString, c_char};
unsafe extern "C" {
fn puts(s: *const c_char) -> i32;
}
fn print_bad(msg: &str) {
let p = CString::new(msg).unwrap().as_ptr(); // CString dropped at end of this statement
unsafe { puts(p) }; // use-after-free
}
fn print_good(msg: &str) -> Result<(), std::ffi::NulError> {
let c = CString::new(msg)?;
// SAFETY: `c` is NUL-terminated and alive until the end of this function.
unsafe { puts(c.as_ptr()) };
Ok(())
}rustc warns (dangling_pointers_from_temporaries, formerly temporary_cstring_as_ptr) for the direct
form; the auditor flags it as US010. The same bug happens with String, Vec, format!(..).as_ptr() and
any pointer derived from a temporary.
static mut
Any access is unsafe, and any reference to a static mut is suspect: two references (one &mut) alive
at once, from two call frames or two threads, is UB. Edition 2024 makes static_mut_refs deny-by-default
(US008/US009); that includes implicit references from method calls (LOG.push(x) autoref-borrows LOG).
use std::sync::{Mutex, OnceLock};
use std::sync::atomic::{AtomicUsize, Ordering};
static HITS: AtomicUsize = AtomicUsize::new(0);
static NAMES: Mutex<Vec<String>> = Mutex::new(Vec::new()); // const Mutex::new: 1.63+
static CONFIG: OnceLock<String> = OnceLock::new(); // 1.70+
fn record(name: &str) {
HITS.fetch_add(1, Ordering::Relaxed);
NAMES.lock().unwrap().push(name.to_owned());
let _cfg = CONFIG.get_or_init(|| "default".to_owned());
}If a static mut must remain (embedded, FFI-owned buffer), only take raw pointers: &raw mut BUF
(1.82+, allowed in safe code) or addr_of_mut!(BUF), and document the single-threaded or
interrupt-disabled argument at every access.
Unions
Reading a union field is unsafe (superpower 5) and sound only if the stored bytes are a valid value of the
field read. Unions never drop their fields automatically; fields must be Copy or wrapped in
ManuallyDrop.
#[repr(C)]
union Bits {
f: f32,
u: u32,
flag: bool,
}
fn demo() -> (u32, f32) {
let b = Bits { f: 1.5 };
// SAFETY: every 32-bit pattern is a valid u32.
let u = unsafe { b.u };
let c = Bits { u: 0x3fc0_0000 };
// SAFETY: every 32-bit pattern is a valid f32 (NaNs included).
let f = unsafe { c.f };
// Reading `flag` from Bits { u: 2 } would be UB: 2 is not a valid bool.
(u, f)
}Safe alternatives: f32::to_bits/from_bits, from_ne_bytes, an enum when you need a tag.
Send and Sync
Send: safe to move to another thread. Sync: &T is safe to share between threads (T: Sync iff
&T: Send). Raw pointers, NonNull, UnsafeCell, Cell, Rc opt out automatically; an unsafe impl
opts back in and must prove it (US011).
use std::marker::PhantomData;
use std::ptr::NonNull;
// BAD: unbounded T means MyBox<Rc<u8>> becomes Send and the Rc's non-atomic count races.
pub struct MyBox<T> { ptr: NonNull<T> }
unsafe impl<T> Send for MyBox<T> {}
// GOOD: same bounds as Box<T>; PhantomData<T> tells dropck and auto traits we own a T.
pub struct MyBox2<T> { ptr: NonNull<T>, _own: PhantomData<T> }
// SAFETY: MyBox2 uniquely owns its T (like Box<T>), so it is Send/Sync exactly when T is.
unsafe impl<T: Send> Send for MyBox2<T> {}
// SAFETY: &MyBox2<T> only gives out &T, so sharing it is sound when T: Sync.
unsafe impl<T: Sync> Sync for MyBox2<T> {}Checklist for a Sync impl: every method reachable through &self that mutates must synchronise (atomics
or locks), and every &T it hands out must itself be Sync. For FFI handles, cite the C library's thread
safety statement: "handle may be used from another thread but not concurrently" means Send but not
Sync. Concurrency proofs need tests: see loom in tooling-and-testing.md.
Pin projection
Pin<&mut T> promises the pointee will not move until dropped (unless T: Unpin). Projecting to a field
(Pin<&mut Self> -> Pin<&mut Field>) is sound only if all of these hold (US016):
- The struct does not implement
Unpinunconditionally (auto-derivedUnpinis fine). - Its
Dropimpl never moves out of the pinned field (Drop::droptakes&mut self, so it could). - No method offers
&mut Field(or a way to move it out,mem::swap) fromPin<&mut Self>. - The struct is not
#[repr(packed)].
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
// GOOD: pin-project-lite generates the projection and enforces the rules.
pin_project_lite::pin_project! {
pub struct Counted<F> {
#[pin]
inner: F,
polls: u32,
}
}
impl<F: Future> Future for Counted<F> {
type Output = (F::Output, u32);
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.project(); // inner: Pin<&mut F>, polls: &mut u32
*this.polls += 1;
match this.inner.poll(cx) {
Poll::Ready(v) => Poll::Ready((v, *this.polls)),
Poll::Pending => Poll::Pending,
}
}
}The hand-written equivalent unsafe { self.map_unchecked_mut(|s| &mut s.inner) } becomes unsound the day
someone adds impl<F> Unpin for Counted<F> {} or a Drop that swaps inner. For stack pinning use
std::pin::pin!(fut) (1.68+) instead of Pin::new_unchecked(&mut fut).
Panic safety and double drop
Unsafe code must leave data structures valid if user code (clone, closures, Drop, comparison) panics.
// BAD: length covers slots that are not written yet; a panicking clone() makes Vec's Drop
// drop uninitialised memory.
fn extend_bad<T: Clone>(v: &mut Vec<T>, src: &[T]) {
v.reserve(src.len());
let old = v.len();
unsafe {
v.set_len(old + src.len());
for (i, x) in src.iter().enumerate() {
v.as_mut_ptr().add(old + i).write(x.clone());
}
}
}
// GOOD: safe and just as fast.
fn extend_good<T: Clone>(v: &mut Vec<T>, src: &[T]) {
v.extend_from_slice(src);
}Double drop: ptr::read duplicates ownership; the source must then be forgotten (mem::forget,
ManuallyDrop) or overwritten with ptr::write (which does not drop the old value). Leaking
(mem::forget) is safe, so unsafe code must never rely on a destructor running for soundness
(the "leakpocalypse" rule; see std::thread::scope for the safe design).
Quick precondition table
| Operation | Preconditions to prove |
|---|---|
*p / &*p / &mut *p | non-null, aligned, dereferenceable, valid T, aliasing for the reference's lifetime |
p.add(n) / p.offset(n) | result within the same allocation (or one past the end), no overflow of isize bytes |
p.read() / p.write(v) | as *p but no reference created; write does not drop the old value |
read_unaligned / write_unaligned | as above minus alignment |
ptr::copy_nonoverlapping(src, dst, n) | both valid for n elements, aligned, regions do not overlap |
slice::from_raw_parts(p, n) | see above: non-null/aligned even for 0, one allocation, initialised, no mutation, size limit |
str::from_utf8_unchecked(b) | b is valid UTF-8 |
String::from_utf8_unchecked(v) | v is valid UTF-8 |
Vec::set_len(n) | n <= capacity, elements ..n initialised |
Vec::from_raw_parts(p, len, cap) | global allocator, same T layout, original capacity, len <= cap, initialised |
Box::from_raw(p) | from Box::into_raw of same T, once |
MaybeUninit::assume_init | fully initialised valid value |
NonNull::new_unchecked(p) | p non-null |
char::from_u32_unchecked(u) | valid Unicode scalar value |
unwrap_unchecked() | is Some/Ok |
get_unchecked(i) | i in bounds |
unreachable_unchecked() | never reached |
Pin::new_unchecked(p) | pointee never moved again until dropped; Deref/DerefMut/Drop of the pointer type do not move it |
transmute::<A, B> | the bits of A are a valid B; lifetimes and provenance are preserved correctly |
| union field read | stored bytes valid for that field's type |
env::set_var (2024) | no other thread reads or writes the environment concurrently |
rust-borrow-checker-error-explainer
Explain and fix Rust borrow-checker, ownership, move and lifetime compiler errors: E0382 use of moved value, E0499 cannot borrow as mutable more than once, E0502 also borrowed as immutable, E0505, E0506, E0503, E0507 cannot move out of index/Rc/reference, E0515 return reference to local, E0597 does not live long enough, E0716 temporary value dropped while borrowed, E0373 closure may outlive (spawn needs move), E0521 borrowed data escapes, E0384, E0596, E0594, E0381, E0713, 'lifetime may not live long enough', RefCell 'already borrowed: BorrowMutError' panics. Ranks fixes from restructuring (split borrows, entry API, split_at_mut, get_disjoint_mut, mem::take, Option::take, indices, thread::scope) down to clone and Rc<RefCell>/Arc<Mutex>; never unsafe. Covers NLL limits (get-or-insert), iterator invalidation, closure captures, self-referential structs. Ships std-only Rust triage tools.
rust-cargo-workspace-designer
Design, review and fix Cargo workspaces: when to split a crate (compile time, boundaries, publishing, proc-macros), crates/* vs apps/ + crates/ layouts, virtual manifest vs root package, members/exclude/default-members, resolver 2 vs 3 (MSRV-aware, edition 2024), [workspace.package] and [workspace.dependencies] inheritance, [workspace.lints], root-only profiles (lto, codegen-units), additive features with dep: and weak ?/ syntax, feature unification pitfalls, internal path + version deps, publish = false, Cargo.lock and MSRV policy, xtask, CI with --locked, nextest, rust-cache, sccache, cargo-hack/deny/machete/udeps/semver-checks, and cargo build --timings. Ships a workspace auditor and crate-graph tool.
rust-error-handling-thiserror-anyhow-designer
Design and review Rust error handling with thiserror 2 and anyhow 1: library vs application split, typed error enums with #[from], #[source], #[error(transparent)] and #[non_exhaustive], ErrorKind patterns, context via .context()/.with_context(), bail!/ensure!, source() chains, {:#} and {:?} reports, RUST_BACKTRACE/RUST_LIB_BACKTRACE, Box<dyn Error + Send + Sync>, main -> anyhow::Result or ExitCode, unwrap/expect/panic policy, Option to Result, collecting iterators of Results, axum IntoResponse mapping, retryable errors, tracing, async Send bounds, testing with matches! and downcast_ref, no_std core::error::Error, and snafu, eyre/color-eyre, miette, error-stack.
rust-lifetime-annotation-advisor
Decide when Rust lifetime annotations are needed and which ones: the three elision rules, structs holding references (struct Parser<'a>), owned vs borrowed (String, &str, Cow<'a, str>, Arc<str>), one vs several lifetimes, 'a: 'b bounds, T: 'static misconceptions, Box<dyn Trait + 'a> default object lifetimes, impl Trait + '_ and edition 2024 capture rules with use<..>, variance and PhantomData, HRTB for<'a> Fn(&'a T), GATs and lending iterators, async fn and tokio::spawn 'static, self-referential structs (ouroboros, self_cell, indices, arenas), Rc/Arc to escape lifetime soup. Ships std-only Rust checkers for redundant, missing, unused and over-restrictive lifetimes.
rust-tokio-async-service-builder
Build and review production async Rust services on Tokio 1.x and axum 0.8: runtime choice (#[tokio::main] multi_thread vs current_thread, Builder), axum routing with /{id} paths, State, extractors, IntoResponse error types, axum::serve with_graceful_shutdown, tower/tower-http middleware (TraceLayer, TimeoutLayer, CorsLayer, ConcurrencyLimitLayer, RequestBodyLimitLayer), Arc<AppState>, std vs tokio Mutex, JoinSet, TaskTracker, CancellationToken, select! cancel safety, bounded mpsc, oneshot, broadcast, watch, actors, Semaphore, timeouts, retries, spawn_blocking, sqlx PgPool, reqwest Client reuse, tracing, tests with start_paused and oneshot, Dockerfile. Fixes 'future cannot be sent between threads safely', E0373, Rc or MutexGuard held across await, 'Cannot start a runtime from within a runtime', 'Path segments must not start with :'. Ships std-only Rust scanners and a compiling axum 0.8 scaffold.
rust-trait-design-reviewer
Review and design Rust traits and generic APIs: generics vs impl Trait vs dyn Trait (static vs dynamic dispatch), dyn compatibility / object safety (generic methods, Self returns, where Self: Sized, error[E0038] not dyn compatible / cannot be made into an object), associated types vs generic parameters, blanket impls, coherence and orphan rule (E0117, E0119, E0210), newtype, extension, sealed and marker traits, supertraits and trait upcasting (1.86), async fn in traits (1.75) and the Send bound problem (trait_variant, async-trait), RPITIT, GATs, From/Into/TryFrom/AsRef/Borrow, std trait checklist (Debug, Clone, PartialEq, Hash, Default, Display, Error, Send/Sync, serde feature), #[must_use], #[non_exhaustive], semver hazards, Deref abuse, builders, typestate, PhantomData, Box<dyn Error + Send + Sync>, Fn traits, mockall.