Skip to content

v0.1.0 — first release

Choose a tag to compare

@glslang glslang released this 29 Aug 13:01
· 211 commits to main since this release
Immutable release. Only release title and notes can be modified.

crates.io · docs.rs

[dependencies]
dbgscope = "0.1.0"

First release. dbgscope gives typed access to a WinDbg/DbgEng debug session, and kernel-pool
and user-heap walkers built on one. The organising rule, and the thing to know before reading
the API, is that every answer carries what the answering cost — see
Unknown, not absent.

Added — debug sessions

  • DebugEngine over IDebugClient6 / IDebugControl4 / IDebugDataSpaces4 /
    IDebugSymbols3, owning its own session (new, Default) or borrowing an existing WinDbg
    client (from_windbg_client, from_client_interface, and their try_ forms). owns_session
    governs teardown, so a borrowed client is never ended.
  • Openers for every target DbgEng handles: attach_kernel, attach_local_kernel,
    launch_process, attach_process, open_dump, open_trace. The two post-mortem openers
    commit the session and leave the pump to the caller: the engine has no current process or
    thread — and GetNumberRegisters answers 0x8000FFFF — until wait_for_event has run.
  • connect(remote_options) for an existing debugging server, so an extension can load out of
    process.
  • Two-step opening on the four live openers, each a thin x_begin()?.wait(). x_begin
    performs only the side effect that creates or claims the target and returns a PendingTarget;
    wait completes the initial break. The split lets a caller distinguish "nothing happened,
    retry is clean" from "the target exists and only the wait failed" — opposite recoveries, since
    re-running the second spawns a second process, attaches twice, or re-dials a live KD link.
    Dropping a guard is safe and non-blocking: deferred input buffers are parked on the engine
    rather than the guard, because CreateProcessWide reads the command line at the next
    WaitForEvent.
  • Per-process teardown. DbgEng holds several user-mode targets at once and EndSession
    takes one flag for the whole session, so provenance is recorded per pid at open time. A
    process this engine attached to is detached individually before the session ends — otherwise
    a passive end kills somebody else's service. A live kernel is resumed and actively detached,
    or it stays frozen at its last break with one CPU halted. Failures are per process and
    reported without stopping the teardown.
  • launch_process uses DEBUG_ONLY_THIS_PROCESS | CREATE_NEW_CONSOLE, so a console target
    cannot inherit a host's stdout — which may be a JSON-RPC channel.

Added — execution control

  • execute_command_bounded, execute_and_wait, settle and run_to_address: bounded waits
    with a condition-variable watchdog behind them. The watchdog stops the moment it is disarmed
    rather than at the end of a poll interval, so a bound costs nothing until it is reached.
  • CommandRun { output, cut_short, target_gone } — the output and whether the command
    finished. A String alone cannot answer "did this run?", and an Err would discard the
    output, which on an interrupted search is all there was.
  • Interruption::Deadline { after_ms } distinguished from Interruption::OnRequest, because
    the advice differs and only the first needs saying. The origin is decided by the watchdog's
    own flag, not by the shared interrupt bit that the watchdog also sets.
  • RunToOutcome names four endings — Hit, StoppedElsewhere { stopped_at }, Timeout,
    TargetGone — rather than a boolean.
  • InterruptHandle: a Send + Sync handle that Ctrl+Breaks an engine from another thread, over
    SetInterrupt, the one DbgEng call documented as safe there. It holds an owned interface
    reference, so it may outlive the DebugEngine it came from.
  • A target that ends is an ending, not a failure. A debuggee running to completion is
    reported as CommandRun::target_gone / RunToOutcome::TargetGone, each keeping the output the
    run captured — the module loads, the breakpoint banner, an embedded script's prints, none of
    which a successor will print again. It is terminal, and callers are told so.
  • Nothing runs on an engine with no debuggee. Driving DbgEng without a target faults inside
    it with a STATUS_ACCESS_VIOLATION that catch_unwind cannot trap, so execute_command,
    execute_command_bounded, execute_and_wait and run_to_address all refuse first.

Added — typed session state

  • register_values returning RegisterValue, decoded once from DEBUG_VALUE's own tag:
    Int, Float, Bytes for x87 and vector registers that no f64 can hold, and
    Unavailable for state a minidump does not carry — which is not 0.
  • register_descriptions for the whole register description rather than one flag of it.
  • modules, unloaded_modules, module, module_at, module_identity, module_pdb,
    module_symbol_file. SymbolKind keeps Deferred separate from None and preserves an
    unrecognised provider as Other(u32); has_type_info() answers the narrow question the pool
    walker actually asks.
  • stack_frames, bug_check as the engine's five values, breakpoints as BreakpointInfo,
    disassemble as Instruction records with a line the split does not recognise kept whole
    rather than guessed at.
  • Scope save and restore: scope, set_scope, scope_guard and the ScopeGuard RAII type,
    for running a command that moves the debugger's scope — measured: !analyze -v discards a
    frame or .ecxr context the caller had selected. A Scope carries the target identity it was
    read from and is refused rather than applied to a later target.
  • Memory and symbols: read_memory, valid_virtual_region, symbol_offset, type_id,
    type_size, field_offset, field_type_and_offset, field_names, set_symbol_path,
    append_symbol_path, reload_symbols.
  • Breakpoints: the Breakpoint<'a> RAII type, BreakpointCallback, and
    DebugEventContextCallbacks for event-driven handling.

Added — kernel pool

  • pool::query: find_tag, chunk_at with immediate neighbours, tag_census,
    snapshot_report, and cache invalidation hooks for a host that resumes or replaces a target.
  • PoolAnswer<T> pairs every answer with the PoolSnapshotReport of the walk it came from, so
    a count and a coverage figure can never be drawn from two different walks — which is what
    happens otherwise, since an incomplete walk is deliberately not cached.
  • WalkCoverage { Complete, BudgetExpired, Partial }, computed in one place from the walk's own
    two bits. Not a bool, because a walk that ran out of time reaches more of the pool if given
    more, and one that met unreadable regions reports the same gaps however long it runs.
  • PoolWalk with cached(), refreshed(), within(Duration) and unbounded(), plus
    impl From<bool> so existing refresh: bool call sites are unchanged and pick up
    DEFAULT_WALK_BUDGET (120s). A walk that runs out of its budget is not an error: it returns
    the chunks it reached with the coverage saying so.
  • PoolDiagnostics groups complaints by shape — the message with every number standing in for
    itself — keeping DIAGNOSTIC_EXAMPLES (8) verbatim per shape and the totals as numbers.
    emitted() describes the walk; examples().len() describes the struct, and on a busy target
    the two differ by two orders of magnitude.
  • WalkStalls, refused_chunks and unplaced_bytes size what conservative decoding cost, so a
    refusal to guess is auditable rather than invisible.
  • Both tag forms: one to four ASCII bytes, or the raw form 0x plus eight hex digits in memory
    order, so it reads in the same direction as the printed tag (Tgsm is 0x5467736d). The two
    cannot collide — the raw form is exactly ten characters and a printed tag at most four.
    display_is_ambiguous and display_round_trips separate the two distinct ways a rendering
    fails, and tag_label is the one rule every output site prints through.
  • PoolKind's eight variants are not collapsed to paged/nonpaged, because crossing one of those
    boundaries creates false holes. PoolState::Unreadable is distinct from allocated and from
    the two free states, and chunk_at returns Ok(None) for "not covered by the snapshot",
    which is a different answer from "it is a free hole".
  • find_tag indexes allocated chunks only: a freed chunk's tag is not reliably preserved by the
    allocator, so returning freed chunks by tag would be inventing information.

Added — user-mode Segment Heap

  • heap: list, allocations, chunk_at, census, diagnostics, diagnostics_for_heap,
    with HeapWalk mirroring PoolWalk and HeapAnswer<T> mirroring PoolAnswer<T>.
  • HeapScope names the roots that were skipped and why — nt_heaps_skipped,
    unknown_heaps_skipped, unreadable_heaps_skipped — rather than reporting only the ones that
    worked.
  • Shared page-segment, LFH, VS, backend and large-allocation decoding with the pool walker,
    because the two allocators are the same machinery either side of the ring boundary.
  • allocator::LayoutProvenance carries the image, PDB and a fingerprint of every resolved type
    size and field offset actually used — deliberately with no build-number policy in it.
  • requested_size is Option<u64>, set only where allocator metadata validates it, rather than
    guessed from capacity.

Added — WinDbg extension

  • !dbgscope.poolmap, built from the cdylib crate type, over the same walker and the same
    caches as pool::query, so the interactive and programmatic entry points cannot drift apart.
  • -tag (either form), -paged / -nonpaged, -refresh, and an address argument for detail
    on the allocation or hole containing it.
  • DML colours and clickable address links where WinDbg accepts DML; meaningful ASCII glyphs and
    a legend where it is stripped or the output is captured as plain text.
  • The extension lets a walk run to completion, because there is an operator at a prompt who can
    Ctrl+Break. pool::query cannot assume anyone is watching the clock, which is why its walks
    carry a budget — and a host that is watching can cancel one through interrupt_handle(),
    which the walk polls and reports as PoolQueryError::Interrupted.

Known limitations

  • Windows only. The public surface calls Windows APIs with no #[cfg] gating; the crate is
    not designed to build elsewhere. docs.rs is configured for the MSVC targets accordingly.
  • Pool walking is x64 only, because the allocator encodings it consults are. The rest of the
    crate builds for ARM64, and CI covers both.
  • Windows 10 19H1 or later allocator algorithms, and full private type information for nt.
    Symbols must be on the debugger host — PDBs are never fetched from a target over KD — and
    resolving them needs msdia140.dll beside the engine. Without it, a kernel dump presents not
    as missing symbols but as memory reads failing.
  • A live-kernel walk is normally incomplete. Paged pool is partly on disk, and a page the
    memory manager has paged out cannot be read through the debugger either.
  • KERNEL_ATTACH_WAIT_MS bounds less than it looks like. The watchdog works by
    SetInterrupt, which only reaches a target that has connected, so it caps a
    connected-but-unresponsive target and nothing else. One that never dials in — powered off,
    wrong key, not booted with bcdedit /debug on — blocks past the bound indefinitely.
  • Snapshots are snapshots. The walkers examine current allocations, reusable frees and
    cached/delay-free spans while the target is stopped. They install no allocation breakpoints
    and reconstruct no history.
  • Per-session paged heaps are outside the initial pool-map scope, and the command says so.
  • Pre-1.0. The dbgeng surface is large and expected to change; breaking changes may land
    in any 0.x release.