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 — returns true if any variant has OwnedHeap::True.
  • maybe_heapowner: Checks whether a non-heap-owning type could become a heap owner after monomorphization — returns true if any variant has OwnedHeap::False with 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 — true means the corresponding generic parameter may contribute to heap ownership when monomorphized (used for maybe_heapowner queries).

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:

  1. It contains PhantomData<T> where T is a raw generic type parameter (not buried behind *mut, &, etc.). This establishes ownership intent — PhantomData indicates the struct logically owns T.
  2. It also contains a pointer field (raw pointer or reference to any type). Without a pointer, PhantomData alone is not sufficient — there must be actual memory that the PhantomData "owns" on behalf of T.

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 KindLayout Behavior
Array / TupleRecursively 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 variantEncodes only the fields of the given variant
ParamMarked as owned, requirement = true
RawPtr / RefMarked 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])
  • String owns heap memory (via its internal Vec<u8>).
  • Vec<T, A> owns heap memory; T does not affect ownership; A may (different allocators could own heap).
  • Unique<T> (the internal pointer wrapper) is a heap unit per PhantomData convention, but does not propagate ownership to T.
  • 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: rCanary uses OwnedHeapAnalyzer to identify which types need deallocation tracking, and uses Encoder::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.