heap/
The allocator layer. It turns any dump source into a HeapLayout, meaning
chunks, free lists, regions, evidence and a per-byte role projection, without
callers knowing which allocator produced the heap. For the researcher view, see
Heap allocator awareness.
Module map
Module |
Responsibility |
|---|---|
|
Plain data types: |
|
|
|
|
|
|
|
Name / module / OS signals per allocator; |
|
The built-in descriptors ( |
|
|
|
One parser module per allocator family. Each exports |
Consumers outside the package:
Module |
Uses the layer for |
|---|---|
|
The six |
|
Projecting N layouts into slab coordinates ( |
|
|
|
The |
|
Persisting |
|
The |
Data flow
flowchart LR
Src[DumpSource<br/>.msl · ELF core · regioned raw · flat raw] --> Plat[platform_of<br/>memory_map_of]
Plat --> Ctx[DetectContext<br/>lazy platform · regions · modules · memory]
Ctx --> Det[detect_in_context<br/>name signals ⊕ content probe]
Det --> Order[attempt order<br/>hint, or ranked candidates, then generic]
Order --> Parse[descriptor.parse<br/>VaMemory reads]
Parse --> Layout[HeapLayout<br/>chunks · free lists · coverage]
Layout --> Runs[role_runs lo..hi]
Runs --> Tools[app/tools_heap<br/>CLI · web · MCP · library]
Runs --> Proj[consensus_heap<br/>slab projection + merge]
Proj --> Cand[candidate_heap<br/>annotate / mask]
Layout --> Hits[heap_hits<br/>locate-key annotation]
Source → platform and memory map.
platform_ofasks each source in the way that source can answer.memory_map_ofreturns oneMappedRegionshape (va, size, perms, name, module_path, captured) whatever the format.Detect.
DetectContextcomputes each field on first use, so a detector that only needs region names never builds aVaMemory. A built-in detector scores name signals (weights summed, capped at 1.0) and combines them with its parser’s content probe as independent evidence:1 − (1 − names) · (1 − probe).run_detectorclamps scores to[0, 1]and turns a raising detector into a 0 score with adetector_errorevidence line, so one bad plugin cannot break detection.Parse with fallback.
analyze_heaptries the hinted allocator, or else every candidate with a positive score in rank order, and alwaysgenericlast. The first layout that has chunks wins. If none has chunks, the first layout that parsed at all is used. Every attempt is recorded incoverage["analysis"].Role runs.
HeapChunk.role_segmentspaints default layers (slack, then header, then user or free data, ortop/allocator_metafor a whole chunk) and then the parser’srolesoverrides on top. An example override is the tcachenext/keywords asFREE_LINK.HeapLayout.role_runsclips these to a window, never lets them overlap, and coalesces adjacent equal runs.Consensus projection. See below.
Registry and plugin contract
@dataclass(frozen=True)
class AllocatorDescriptor:
kind: AllocatorKind
detect: Callable[[DetectContext], DetectScore]
parse: Optional[Callable[[VaMemory, DetectContext, ParseOptions], HeapLayout]] = None
priority: int = 0
levels: FrozenSet[str] = frozenset({"L0"})
partial: bool = False
heuristic: bool = False
The registry is keyed by
AllocatorKind. Registering a kind that is already present replaces it, so a plugin can override a built-in. Built-ins register withreplace=False, so a plugin registered earlier is never overwritten.registered_allocators()sorts by descendingpriorityand breaks ties by registration order. Built-in priorities: ptmalloc2 / scudo / mallocng 50, dlmalloc 40, jemalloc 30, generic 0.Loading is lazy and happens once per process: built-ins first, then
load_entry_point_registrations("memdiver.allocators"). An entry point that resolves to a module is imported, and one that resolves to a callable is invoked.parse=Nonedeclares a detect-only allocator.analyze_heaprecordsno parser registeredand moves on.partial/heuristic/levelsare surfaced verbatim in every summary payload and in the UI badges.
See Adding an allocator for a worked plugin.
Caching
analyze_heap keeps a process-wide LRU of 8 layouts (CACHE_SIZE), keyed by
the resolved dump path, st_mtime_ns, st_size, the source’s region-filter
note, the allocator hint, base_va, max_chunks and regions. It follows the
same pattern as app/reader_cache.py.
A source that cannot be stat’ed is not cached, and neither is a pathless source.
Encrypted sources, whether keyed or locked, bypass the cache. A layout derived from decrypted bytes must never be served to a caller without the key.
Cached layouts are shared objects, so treat them as read-only.
clear_heap_cache()drops every entry.
The frontend keeps its own cache. See UI layer.
Coordinate spaces
Three coordinate systems meet in the heap layer. Mixing them up is the most common bug.
Space |
Who uses it |
Notes |
|---|---|---|
Virtual address (VA) |
Parsers, |
What the allocator itself stores. Kernel-half VAs (≥ 2⁶³) are kept as |
View offset |
Producers ( |
Offset in a byte view: |
Slab offset |
Consensus, |
Index into the consensus vector. Aligned builds map via |
VaMemory reads by VA in three modes:
Sparse
vaview (.msl) throughread_range_with_validity. A page the dump never captured yieldsNoneinstead of zero filler.Dense
vasprojection (gcore, gdb/lldb regioned raw), after checking that the whole range lies in contiguous captured runs.Flat (
RawDumpSource, or any source with an explicitbase_va):offset = va - base_va.
Every read returns exactly n captured bytes or None. Parsers treat None
as “not captured” and skip ahead, recording that in coverage.
Consensus projection
engine/consensus_heap.compute_consensus_heap(cm, sources, allocator):
Builds one VA → slab map per dump. Aligned builds use their
msl_layoutrows. Flat builds clip the source’s capturedvasruns to the slab, and a dump with no VA map is not projected. When no dump has a VA map, the result isConsensusHeap.unavailable("no_va_coordinate").Projects each dump’s
role_runsthrough its map into(starts, lengths, roles)pieces.Merges the pieces with run-based numpy interval arithmetic (nothing loops per byte). A byte takes role R when every dump with a non-
NONErole there agrees, and isMIXEDotherwise.agreecounts the dumps whose role equals the anchor’s (the first projected dump),NONEincluded.Projects the anchor’s chunks into a
HeapChunkIndex(slab_starts, sizes, states, agree).
Bytes no dump assigns a role are not stored. HeapRoleRuns.window paints them
with fill, which is NONE for a computed result and UNCOVERED for an
unavailable one. save_heap_npz / load_heap_npz persist runs and chunks
together in heap_roles.npz, with the chunk arrays under a chunk_ key prefix.
engine/candidate_heap.py consumes the runs and chunk index:
region_heap_annotation builds the CandidateRegion.heap block, and
metadata_mask plus heap_score_adjustment implement mask. The heap layer
is imported only when a caller passes heap runs, so candidate_pipeline stays
heap-free under off.
aligned_window_from_vector adds heap_status, heap_roles (one role code
per byte, parallel to classes; 255 = uncovered) and a heap_role_names
legend whenever the consensus vector carries heap roles.
UI layer
The heap mode is described from the user’s side in the web UI tour. Its architecture:
App mode.
APP_MODES = ["verification", "exploration", "heap"]instores/app-store.ts. Heap mode is exploration plus the heap panels, so every consumer that gates on “not verification” treats it like exploration.useHeapModeLayermirrors “heap mode and an active dump” into the heap store and turns the layer off again when the mode or dump changes.stores/heap-store.tsholds per-dump state (detection, summary, a chunk-page cache keyed by filters, view and page, free lists,layerEnabled,selectedChunk). It fetches throughapi/heap.ts(/api/heap/*). A per-dump generation counter turns a response that arrives after an allocator change into a no-op, so a stale answer cannot overwrite the new one.Role windows. The hex layer is deliberately not a set of
highlightedRegions. The left-walk lookup inhighlight-utilscollapses on a 100 KiB top chunk. Instead the store fetches/api/heap/rolesin windows aligned toHEAP_ROLE_WINDOW_SIZE(64 KiB), keyed by(dumpPath, view, windowStart), and keeps at mostHEAP_MAX_ROLE_WINDOWS(48) of them. Every window arrival bumpsroleWindowsVersion, which is the single value a viewer subscribes to.Binary-search lookup.
utils/heap-roles.tsindexes each window’s sorted, non-overlapping runs. The run covering a byte is the last one whose start is ≤ the byte, found with oneupperBound(O(log n)). The answer is one packed number per byte: the role code in the low byte, OR-ed withHEAP_CHUNK_START_BIT(0x100) at a chunk start. The memoisedHexRowtherefore allocates nothing per cell.Ground classes. Roles map to
heap-header,heap-user,heap-freelink,heap-free,heap-meta,heap-topandheap-slack, which are registered incomponents/hex/ground-classes.tswith rules instyles/hex.cssand--md-hl-heap-*tokens for the light, dark and high-contrast themes instyles/globals.css. The chunk-start tick (heap-chunk-start) is a box-shadow, not a ground, so it combines with any role color.HexRowasks the heap layer only after the existing highlights.Panels.
HeapOverview(side tab),HeapPanel(bottom tab: chunk table, size histogram, free lists, chunk inspector) andHeapLegend/HeapLayerTogglein the hex toolbar. Navigation goes throughheap-navigation.ts. Aview_offsetgoes straight tohex-store.scrollToOffset, and a bare VA is first translated by/api/heap/chunk-at.i18n. Every string lives in the
heapnamespace (i18n/locales/en/heap.json).Consensus overlay.
OVERLAY_RENDER_MODESgains"heap", which paints the aligned window’sheap_roleswith the same ground classes plusheap-mixedfor bytes the dumps disagree on. It is disabled, with a localized reason, wheneverheap_statusis notok.OverlayByteInspectorshows the inspected byte’s role. The candidate table adds chunk-state, residual and offset-in-chunk columns when candidates carry aheapblock, andHeapPolicySelectsetsheap_policyin the candidate filters and the pipeline’s search-reduce thresholds.