Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@ All notable changes to this project are documented here. The format follows
Only a positive `MEM_RESERVE`/`MEM_FREE` excuses a gap — a failed query, a run that cannot
advance, an unnamed state and a source that cannot be asked are each conservative, so the
excuse is never granted by an absence of evidence. The kernel pool walk is not asked at all
and is unchanged.
and is unchanged. Which also fixes what `Unreadable` *means*: it is now everything that could
not be established as empty, including every case nothing could be asked about, so it is the
conservative bucket rather than a claim that the target has the memory.

A free chunk whose middle the allocator decommitted is the same question reached another way:
it runs past the committed extent it starts in, and `walk_vs` emitted no span for it and
Expand Down
9 changes: 8 additions & 1 deletion docs/unknown-not-absent.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,12 @@ Three states hide behind that one observation, and the failed read does not sepa
| Committed and paged out, or absent from a dump | The target has this memory and the walk did not see it. A real gap. |
| Committed and present | Not a gap at all — it read. |

`Uncommitted` is the first row and **only** the first row, because only it can be established. The
other two, and every case where no commitment query could be made at all — a kernel walk, a dump
recording no memory information, a query that failed — stay `Unreadable`. So `Unreadable` is the
conservative bucket rather than a claim that the target has the memory, and prose about it that
says *memory the process has* is overstating exactly the half that was not measured.

The allocator's own records can answer the first distinction: a page range descriptor's
`CommittedPageCount`, a VS subsegment's `CommitBitmap`, an LFH subsegment's commit state at
`CommitStateOffset`. That is three structures, each of which moves between builds, to learn
Expand All @@ -112,7 +118,8 @@ runs past the committed extent it starts in, and `walk_vs` emitted no span for i
geometry and state, both of which are known there — the header was read, the size came out of
it and passed the subsegment bound, the state comes from the free tree — and the only thing
missing is the chunk's *contents*, which no span carries. So where the tail holds nothing the
chunk is reported; where it is memory the process has, it is not, and now the walk says so.
chunk is reported; where that could not be established — memory the process has, or memory
nothing could be asked about — it is not, and now the walk says so.

Measured on a live 26200 process (`sihost`, four Segment Heaps, 19,448 chunks, 2026-09-24): all
48 gaps were `MEM_RESERVE`, both controls — an allocated chunk and a free one — were
Expand Down
10 changes: 6 additions & 4 deletions src/heap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,9 @@ pub enum HeapState {
Allocated,
ReusableFree,
CachedFree,
/// Memory the walk could not read that the process does have; see
/// [`PoolState::Unreadable`].
/// A span the walk could not read and nothing established was empty — which includes both
/// memory the process does have and memory nothing could be asked about. The conservative
/// bucket rather than a claim; see [`PoolState::Unreadable`].
Unreadable,
/// Address space in a heap region with no pages behind it; see
/// [`PoolState::Uncommitted`]. Not a gap in the walk's coverage — a reserved subsegment
Expand Down Expand Up @@ -182,8 +183,9 @@ pub struct HeapWalkReport {
pub total_chunks: usize,
pub allocated_chunks: usize,
pub diagnostic_count: usize,
/// Spans the walk could not read and the process **does** have; each one clears
/// [`WalkCoverage::complete`].
/// Spans the walk could not read and nothing established were empty; each one clears
/// [`WalkCoverage::complete`]. See [`HeapState::Unreadable`] for why that is not the same as
/// memory the process has.
pub unreadable_gaps: usize,
/// Spans with no pages behind them — reserved subsegment tails and the like, which the
/// memory manager confirmed rather than the walk assumed.
Expand Down
14 changes: 11 additions & 3 deletions src/pool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,18 @@ pub enum PoolState {
Allocated,
ReusableFree,
CachedFree,
/// Memory the walk could not read, and which the target *has*: a page that is committed
/// and paged out, one missing from a dump, or one the debugger refused. Something may have
/// been there, so this is a hole in the walk's coverage and clears
/// A span the walk could not read and **nothing established was empty**. Something may have
/// been there, so it is a hole in the walk's coverage and clears
/// [`query::WalkCoverage::complete`].
///
/// **It is the conservative bucket, not a claim that the target has the memory.** Two very
/// different things land here: a page the memory manager confirms is committed — paged out,
/// missing from a dump, refused by the debugger — and a page no commitment query could be
/// made about at all, which is every kernel walk and any target
/// [`snapshot::PoolMemory::committed_run`] answers `None` for. Only
/// [`Self::Uncommitted`] carries a positive answer; this one carries the absence of one, and
/// reading it as confirmed memory would turn *we could not tell* into a fact about the
/// target.
Unreadable,
/// Address space inside a region with **no pages behind it** — reserved, or committed and
/// since released — as the target's memory manager says, not as the walk inferred from
Expand Down
Loading