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
49 changes: 49 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,55 @@ All notable changes to this project are documented here. The format follows

### Added

- **Exception events are readable as values.** `DebugEngine::last_event` returns a `DebugEvent` —
kind, engine process and thread, and, when the event carried one, an `ExceptionRecord` with the
code, flags, faulting address and parameters. That is `.exr -1` typed, and it is the user-mode
counterpart to `bug_check`: on a target stopped by a fault it is the record that stopped it.
`Ok(None)` where the engine has seen no event, which is any engine before its first wait —
including a dump `open_dump` has *named* but nothing has pumped. That case is `None` rather than
an error because the engine does not fail it: it answers `S_OK` with kind `0` and `DEBUG_ANY_ID`
for both ids, and kind `0` is not a `DEBUG_EVENT_*` value.
`ExceptionRecord::parameters` arrives already cut to the record's own `NumberParameters` (and
clamped to the fifteen slots there are), because the count is the field that tells the two shapes
of a `0xc0000409` apart — one parameter is the CRT's `abort`, three is WIL's, whose second is the
`HRESULT` — and a leftover read as a parameter would answer the question wrongly rather than
cosmetically.
- `DebugEngine::stored_event` returns the event a dump was **written for**, with the register
context it was written with, as an opaque `ThreadContext`. Unlike `last_event` it does not move:
it still answers after a caller has stepped, gone, or changed threads. `Ok(None)` where there is
no stored event — every live target, and every dump not written for a fault, including kernel
crash dumps, whose bug check `ReadBugCheckData` reads instead. That is read off the engine's own
refusal (`E_UNEXPECTED`, measured on both) rather than probed for, so a genuine failure still
reaches the caller as one.
- `DebugEngine::stack_frames_from` walks the stack a recorded context was in, which is what
`.ecxr; k` produces without `.ecxr`'s effect on the session: the caller's selected thread and
frame are left exactly where they were, so a triage built on it is still a read. **What makes it
differ from `stack_frames` is the selected thread and only that** — measured on a two-thread
fail-fast dump, after `~1s` the other walk returns the parked thread's six frames while this one
still returns the crash's twelve, while `.frame`, `.cxr` and `.ecxr` move neither, since they
change the symbol scope and `GetStackTrace` walks from the thread's registers.
A `ThreadContext` carries the target it was read from and is refused
(`DbgEngError::ContextFromAnotherTarget`) by an engine that no longer holds it, exactly as
`set_scope` refuses a stale `Scope` — and for a sharper reason: a stale scope points the session
somewhere visibly wrong, while a stale context comes back as frames, which is an answer a caller
cannot tell from the right one.

### Fixed

- **An opener that replaces the session now reissues the target identity.** Only `end_session` did,
so a caller who opened dump A, saved a `Scope` or a `ThreadContext`, and opened dump B *through
the same engine* got the same identity back — and the stale registers were accepted against the
new target. The three openers that replace a session (`open_dump`, which `open_trace` delegates
to, and both kernel attaches) already funnel through `forget_the_previous_session`, so the
reissue lives there rather than in each of them: "the previous session is gone" now means all of
what it says, and the next opener gets it without remembering to.
- `examples/stored_event_probe.rs`, the measurements behind the three. It also caught the one that
would otherwise have shipped: `GetStoredEventInformation` does **not** refuse a context buffer
that is too small the way `GetScope` does. It truncates — offered 716 bytes for an x64 dump it
writes 716, reports 716 and returns success, and the damage surfaces three calls later when
`GetContextStackTrace` rejects the truncated context with `E_INVALIDARG`. So the context ladder
here starts *above* every real `CONTEXT` rather than below it, and grows only on the one signal
the call gives that there was more to write.
- Breakpoints can be **set**, not only listed. `DebugEngine::set_breakpoint` and
`set_breakpoint_bounded` take a `BreakpointSpec` — a location (`BreakpointAt::Address` or
`::Expression`), an optional command, match thread, pass count, one-shot flag, and a `DataWatch`
Expand Down
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,5 +103,6 @@ There is no build script and no assembler step: both left with the exploitation
- `examples/session_fuzz.rs` — randomised command sequences against a live session, checking after every step that the engine either still holds a target and answers or says it holds none. Seeded, so a failing sequence replays. Run it after touching anything in the wait/settle/guard seam: at seed 1 it finds the pre-fix half-dead session in 4 rounds of 8, and 150 rounds of 14 steps are clean on the fix
- `examples/interrupt_provenance.rs` — whether a post-wait `GetInterrupt` tells a request that *ended* a wait from one that did not. It does: a request the wait consumed reads `[false; 5]` afterwards, one nothing consumed reads `[true, false, …]`, two back to back are one flag rather than two, and one lodged after the wait it was too late for is readable and belongs to the **next** operation. That makes it a forward signal and not a backward one, which is what #136 stage 3 would need to attribute a break to the operation it landed on rather than the one it was aimed at. Nothing depends on it yet — stage 2's scoping needs no engine state — and #136 asked for the measurement before anything did. Undocumented by Microsoft, one engine, one host: re-run it before building on it, and mind the DLL note below
- `examples/breakpoint_probe.rs` — the measurements behind the breakpoint API: eager resolution and its cost, that the bound is reachable, that `bp` deduplicates where the engine does not, what a duplicate costs, and that a command string survives unescaped. Run it after touching that seam. **Copy the engine DLLs into `target/debug/examples/`, not `target/debug/`** — an example loads from its own directory, so otherwise it gets System32's `dbgeng.dll`, which without `symsrv.dll`/`msdia140.dll` resolves exported symbols from the export table and defers everything else. Nothing errors; the timings simply collapse to milliseconds and the run measures the wrong engine
- `examples/stored_event_probe.rs` — the measurements behind `last_event`, `stored_event` and `stack_frames_from`: how the engine says a target has no stored event (`E_UNEXPECTED`, on a live process and a kernel crash dump alike), that a kernel crash dump has none at all so `bug_check` and this do not overlap, and that what makes the two stack walks differ is the **selected thread** and nothing else — `.frame`, `.cxr` and `.ecxr` move neither. Run it after touching the event or stack-walk seam. Its main finding is a trap worth knowing before writing any `GetStoredEventInformation` call: unlike `GetScope` it does **not** refuse a context buffer that is too small, it truncates and returns success, and the damage only surfaces when `GetContextStackTrace` rejects the result. Needs a user-mode fault dump, and a **two-thread** one for the last question — a single-threaded dump agrees with itself in every state it can be put in, which nearly recorded "no difference" as the answer
- `README.md` — user-facing overview, the `!dbgscope.poolmap` extension, and usage sketches
- `.cursor/rules/*.mdc` — Cursor editor rules; they defer to this file for build commands and the module map
Loading
Loading