Chapter 5.6. Owned Heap Analysis
Owned heap analysis determines whether a Rust type owns heap-allocated memory. This information is essential for memory leak detection and other analyses that must distinguish stack-only types from those requiring deallocation. The module lives at rapx/src/analysis/ownedheap_analysis/.
Overview
A type is classified as a heap owner if it directly or transitively contains a heap unit — a struct that combines a raw pointer to T with a PhantomData<T> marker, following Rust's ownership convention. For example, Vec<T> is a heap owner because its internal RawVec<T> contains both NonNull<T> (the pointer) and PhantomData<T> (the marker).
The analysis traverses all ADTs reachable from local crate function bodies, applies four phases top-down, and produces an OHAResultMap keyed by DefId.
OwnedHeapAnalysis Trait
#![allow(unused)] fn main() { pub trait OwnedHeapAnalysis: Analysis { fn get_all_items(&self) -> OHAResultMap; fn is_heapowner<'tcx>(hares: OHAResultMap, ty: Ty<'tcx>) -> Result<bool, &'static str> { ... } fn maybe_heapowner<'tcx>(hares: OHAResultMap, ty: Ty<'tcx>) -> Result<bool, &'static str> { ... } } }
get_all_items: Returns the full analysis result.is_heapowner: Checks whether a concrete (monomorphized) type owns heap memory — returnstrueif any variant hasOwnedHeap::True.maybe_heapowner: Checks whether a non-heap-owning type could become a heap owner after monomorphization — returnstrueif any variant hasOwnedHeap::Falsewith at least one type parameter flagged as owning.
Result Format
#![allow(unused)] fn main() { pub type OHAResultMap = HashMap<DefId, Vec<(OwnedHeap, Vec<bool>)>>; pub enum OwnedHeap { False = 0, // never owns heap memory True = 1, // owns heap memory Unknown = 2, // not yet analyzed } }
Each DefId maps to a Vec of variants (one for structs, one per variant for enums). Each variant is (OwnedHeap, Vec<bool>):
OwnedHeap: whether this variant directly owns heap memory.Vec<bool>: per-type-parameter flags —truemeans the corresponding generic parameter may contribute to heap ownership when monomorphized (used formaybe_heapownerqueries).
For example, Vec<T, A> returns (True, [false, true]): the Vec itself is a heap owner; T does not affect ownership; A (allocator) may.
Quick Usage
cargo rapx analyze owned-heap
In code:
#![allow(unused)] fn main() { let mut analyzer = OwnedHeapAnalyzer::new(tcx); analyzer.run(); let result = analyzer.get_all_items(); rap_info!("{}", OHAResultMapWrapper(result)); }
The Four-Phase Pipeline
OwnedHeapAnalyzer::start() in default.rs proceeds through four phases. First, it visits all MIR bodies reachable from the local crate, collects all ADT types, and records their DefIds. Then:
Phase 1: Raw Generic Extraction (extract_raw_generic)
For each ADT, determines which type parameters appear as raw generics — directly embedded in a field without being wrapped in *mut, &, or a heap-owning container. Raw generics may contribute to heap ownership after monomorphization; non-raw generics (behind pointers) do not.
The IsolatedParam type visitor walks each field type and sets record[i] = true when type parameter i appears directly (nested inside tuples, arrays, or other ADT wrappers are recursively explored). The result is a Vec<bool> per variant.
Given struct Example<A, B, T, S> {
a: A, // A appears raw → record[0] = true
b: (i32, (f64, B)), // B appears raw → record[1] = true
c: [[(S) ; 1] ; 2], // S appears raw → record[3] = true
d: Vec<T>, // T is inside Vec, not raw → record[2] = false
}
Result: (False, [true, true, false, true])
Phase 2: Generic Propagation (extract_raw_generic_prop)
Propagates raw-generic flags upward through nested ADTs. When a field is itself a generic ADT (e.g., X<A> inside Example<A, ...>), the raw-generic flags of the inner ADT are transferred to the outer ADT's type parameters. This handles cases like:
struct X<A> { a: A }
// X<A>: (False, [true]) — A is raw
struct Y<B> { a: (i32, (f64, B)), b: X<i32> }
// Y<B>: (False, [true]) — B is raw; X<i32> contributes nothing (i32 is not a param)
struct Example<A, B, T, S> { a: X<A>, b: (i32, (f64, B)), c: [[(S);1];2], d: Vec<T> }
// After propagation: (False, [true, true, false, true])
// X<A> propagates: A is raw → outer's A flag = true
The IsolatedParamPropagation visitor handles this: when it encounters Adt(field_adt, substs), it looks up the field ADT's raw-generic flags and propagates them through the substitution mapping (which inner param maps to which outer param) into the outer record.
Phase 3: Phantom Unit Detection (extract_phantom_unit)
Identifies heap units — structs that are the fundamental building blocks of heap ownership. A struct is a heap unit if and only if:
- It contains
PhantomData<T>whereTis a raw generic type parameter (not buried behind*mut,&, etc.). This establishes ownership intent — PhantomData indicates the struct logically ownsT. - It also contains a pointer field (raw pointer or reference to any type). Without a pointer,
PhantomDataalone is not sufficient — there must be actual memory that the PhantomData "owns" on behalf ofT.
The FindPtr visitor recursively scans all fields looking for TyKind::RawPtr or TyKind::Ref. If both conditions are satisfied, the variant is promoted to OwnedHeap::True.
struct Foo<T> { // Vec's internal RawVec equivalent
ptr: NonNull<T>, // pointer field ✓
_marker: PhantomData<T>, // PhantomData with raw T ✓
}
// → (True, [false]) — heap unit, T does not affect ownership
struct Proxy3<'a, T> {
_p: *mut T,
_marker: PhantomData<&'a T>, // PhantomData holds &T, not raw T
}
// → (False, [false, false]) — NOT a heap unit
Phase 4: Heap Propagation (extract_heap_prop)
Propagates OwnedHeap::True flags upward through the type tree. Starting from leaf heap units discovered in Phase 3, the HeapPropagation visitor marks an ADT as a heap owner if any of its fields is a known heap owner.
struct Proxy2<T> { _p: *mut T, _marker: PhantomData<T> }
// Phase 3: (True, [false]) — heap unit
struct Proxy5<T> { _x: Proxy2<T> }
// Phase 4: Proxy2 is True → Proxy5 becomes (True, [false]) — heap owner via Proxy2
struct Proxy4<T> { _x: T }
// Phase 4: no heap unit in fields → stays (False, [true]) — only a heap owner when T is one
The visitor short-circuits: once any field returns True, the entire ADT is marked True and traversal stops. Enum variants are handled independently — only structs participate in propagation (enum ownership depends on the active variant at runtime, handled separately in rCanary).
Ownership Layout Encoding
Beyond the boolean heap-owner classification, the Encoder struct in default.rs produces an OwnershipLayoutResult for each field of a type. This is used by the memory leak detector (rCanary) to determine, at runtime, which fields of a drop-in-progress struct need deallocation.
Encoder::encode() matches on the type kind:
| Type Kind | Layout Behavior |
|---|---|
Array / Tuple | Recursively encodes the field ownership of each element |
Adt(struct) | Recursively encodes each field; if any field is a heap owner, the outer struct requires deallocation |
Adt(enum) with variant | Encodes only the fields of the given variant |
Param | Marked as owned, requirement = true |
RawPtr / Ref | Marked as non-owning (pointer itself does not own the pointee), requirement = true |
The result is a Vec<OwnedHeap> per field plus flags for whether the type requires runtime deallocation checking.
Examples
Basic Proxy Types
The test at rapx/tests/analyze/ownedheap_proxy/src/main.rs defines five proxy structs:
#![allow(unused)] fn main() { struct Proxy1<T> { _p: *mut T } // No PhantomData → (False, [false]) // Has a pointer but no ownership intent struct Proxy2<T> { _p: *mut T, _marker: PhantomData<T> } // PhantomData + pointer → (True, [false]) // Heap unit: owns memory of type T struct Proxy3<'a, T> { _p: *mut T, _marker: PhantomData<&'a T> } // PhantomData holds reference, not raw T → (False, [false, false]) struct Proxy4<T> { _x: T } // No heap unit, but T is raw generic → (False, [true]) // Becomes heap owner when T is one (e.g., Proxy4<Vec<i32>>) struct Proxy5<T> { _x: Proxy2<T> } // Proxy2 is heap unit → (True, [false]) // Inherits heap ownership from Proxy2 }
Standard Library Types
cargo rapx analyze owned-heap
Type: std::string::String: (1, [])
Type: std::vec::Vec<T/#0, A/#1>: (1, [0,1])
Type: std::ptr::Unique<T/#0>: (1, [0])
Type: std::alloc::Global: (0, [])
Type: std::ptr::NonNull<T/#0>: (0, [0])
Type: std::marker::PhantomData<T/#0>: (0, [0])
Stringowns heap memory (via its internalVec<u8>).Vec<T, A>owns heap memory;Tdoes not affect ownership;Amay (different allocators could own heap).Unique<T>(the internal pointer wrapper) is a heap unit per PhantomData convention, but does not propagate ownership toT.NonNull<T>is just a pointer — no PhantomData → not a heap unit.PhantomData<T>itself is not a heap owner.
Relationship to Other Modules
- Memory Leak Detection:
rCanaryusesOwnedHeapAnalyzerto identify which types need deallocation tracking, and usesEncoder::encode()to determine field-level ownership layouts for runtime drop analysis. - SafeDrop: Uses heap ownership information to reason about whether a pointer aliases memory subject to deallocation.