The checkpoint that needed no driver
4 October 2026 · Windows internals · Secure Kernel from the root, part 5 of 10
The result in part 4 needed test-signing on, Secure Boot off, HVCI off and a driver of our own loaded. A standard Hyper-V checkpoint needs none of that, and it carries VTL1 — pages and page-table root.
Part 4 ended on a working route and an uncomfortable price. Reading a guest's Secure Kernel from the root partition takes a kernel driver issuing hypercalls, which means test-signing on, Secure Boot off and HVCI off — a deliberately weakened host, and not a configuration to leave running. The capability was real and the audience for it was one bench.
So the next question was not can this be done but how much setup does a user need. It has a sharp form: is there a source containing VTL1 pages that needs no driver? A guest kernel crash dump cannot be one — the guest's own NT cannot read VTL1 memory, which is the same refusal part 4 measured from outside, so it cannot write those pages into a dump. A Hyper-V saved state is written by the host, and that makes it the candidate.
It works, and it works in the strong form: the page-table root comes out of the capture rather than being carried in from a previous run. By the end of this part the reads are four MCP tools over a file that can be copied off the host and read anywhere.
The shape of the result in one line. A standard checkpoint of a VBS guest, read through Microsoft's own
vmsavedstatedumpprovider.dll, reproduces every landmark the driver-and-hypercall route found — the Secure Kernel base, its debugger data block, its loaded-module list, its six VTL1 modules — with no driver, no test-signing, no hypercall and no VM consulted. The gate that matters is the file's ACL, not the Hyper-V role.
The instrument was already on the machine
vmsavedstatedumpprovider.dll, from the Windows Kit's bin\10.0.26100.0\x64, declared in VmSavedStateDump.h beside it. It is VTL-aware by declaration — GetGuestEnabledVirtualTrustLevels, GetEnabledVirtualTrustLevels, ForceActiveVirtualTrustLevel, IsActiveVirtualTrustLevelEnabled — and the whole of this gate is whether that declaration extends to VTL1's memory and its CR3.
Two details of the probe are there to keep it from being confidently wrong. It reads the REGISTER_ID enum out of the SDK header rather than hand-counting to X64_RegisterCr3 (it is 46); and it reads CR0, CR4 and EFER beside CR3, so that a long-mode-consistent control-register set says the indexing is right rather than off by one. An index that is quietly wrong returns a number, and a number is what this whole route is trying to establish.
The captures are standard checkpoints — CheckpointType = Standard, so memory is included — of both lab guests, ~1.7 GB each, taken ten seconds apart. Both guests stayed Running throughout and were Operating normally afterwards.
The same boot, two routes
Every part 4 landmark reproduces. The left column is the live route — a test-signed driver plus HvCallGetVpRegisters — and the right is a file.
| landmark | part 4, live (driver + hypercall) | this gate, saved state (neither) |
|---|---|---|
VTL1 CR3 |
0x1201000 |
0x1201000 |
| PML4 self-map index | 388 | 388 |
| present entries in the PML4 | 26 | 26 |
| non-zero bytes in that page | 123 | 123 |
| first present entry's byte offset | 0x850 |
0x850 |
| reads to walk the page tables | 167 | 166 |
| leaf pages | 11,326 | 11,326 |
securekernel.exe |
VA 0xFFFFF80220D89000, GPA 0x00CD0000 |
identical |
| identified against the on-disk image | 18 sections, 0x94DED27F, 0x175000 |
identical |
KdDebuggerDataBlock |
+0x1335E0, Size 0x3A0 |
identical |
SkLoadedModuleList |
+0x127770 |
identical |
| VTL1 modules | 6, named | identical — same six, same bases, same sizes |
The one cell that is not a repeat is 166 against 167, and it is not worth chasing: part 4's own breakdown — 11 PDPTs, 24 PDs, 130 PTs, plus the root — sums to 166.
And a second translator agrees, which is what makes either believable. GuestVirtualAddressToPhysicalAddress at the forced VTL1, handed Secure Kernel's base VA, returns 0x00CD0000 — the GPA the four-level descent had already reached from the CR3. That is Microsoft's code and not ours, sharing only the capture. It also hands the next gate a differential oracle that costs nothing.
Four KDBG tags, three of them noise — which is the argument for reporting Size rather than matching it, and is part 8 §4 in its original habitat. The three carry a Size of 0x2C058948, 0 and 0, with KernBase values that are plainly instruction bytes. The fourth carries Size 0x3A0 and a KernBase equal to the base the PE walk established independently. An exact-match needle built from a remembered constant would have returned the noise, or nothing at all.
The root has to come from the capture, and this is where we learned it
The other checkpoint on the bench is of the same guest on a different boot, and reading it is the control that says this is a file being read rather than a channel to the running guest.
| capture of 25 September | capture of 26 September | |
|---|---|---|
VTL1 CR3 |
0x107593000 |
0x1201000 |
| PML4 self-map index | 309 | 388 |
| present entries | 29 | 26 |
securekernel.exe VA |
0xFFFFF8070EDA9000 |
0xFFFFF80220D89000 |
securekernel.exe GPA |
0x00CD0000 |
0x00CD0000 |
KdDebuggerDataBlock |
+0x1335E0, Size 0x3A0 |
same |
SkLoadedModuleList |
+0x127770 |
same |
| VTL1 modules | the same six | the same six |
| leaf pages from the walk | 16,437 | 11,326 |
So the VTL1 CR3 is not reboot-stable, and part 4's own landmark table said it was. That table compared two boots, found the value identical, and recorded "repeated across a reboot: yes". A third boot has a different one. Nothing part 4 measured falls — it measured what it measured — but the generalisation does, and it is the exact generalisation an implementer would have hard-coded. The image-relative offsets are the coordinates that survive. The root has to be read, every time.
A later capture sharpened this further, and in a way that makes "read it" the contract rather than a precaution: a third checkpoint carries the same CR3 value as the first with a different self-map index — 434 against 388 — and a different Secure Kernel GPA. So the root is not merely unstable across reboots; equal roots do not even identify the same layout.
A seventh PE image appears on the earlier boot, identity-identical to symcryptk.dll (0xD000, 0x98293ECD) at a VA outside the module list's range and not named by it. Recorded rather than explained, and it is the counterexample that makes the identification rule non-obvious: matching the disk image says the bytes are that image, not that this VA is the base it was loaded at. A duplicate mapping matches on all three fields, and which of two mappings the walk reaches first is prefix order. So the probe tries every matching candidate until one is confirmed, and reports the attempt for each.
Confirmation is two independent things agreeing. A KDBG block whose KernBase equals the candidate's base is necessary and not sufficient: the block is populated selectively — 26 of its 116 qwords — so a PsLoadedModuleList that is stale, zero or read at the wrong alignment still yields plausible names and sizes rather than an error. A candidate is accepted only when the list walked from that field has a first entry whose DllBase is the candidate's base. On every capture taken here, exactly one of the four tags passes both.
The control, and the self-map that eats walks
Part 3's VBS-off twin, same procedure, both captures:
| VBS on | VBS off | |
|---|---|---|
GetGuestEnabledVirtualTrustLevels |
0b11 |
0b1 |
ForceActiveVirtualTrustLevel(vp0, 1) |
succeeds, VTL enabled | refused, 0xC0370509 |
| pages scanned physically | 32,768 | 32,768 |
| PE image headers | 86–101 | 100–143 |
matching securekernel.exe |
1, GPA 0x00CD0000 |
0 |
KDBG tags |
4 | 0 |
The two PE-header counts move and the other rows do not, which is worth saying rather than picking one. Those are three captures of the same boot across two hours, and a running guest's physical memory changes between them — so a count of PE headers in a fixed 32,768-page window is a reading of what happened to be resident, not a property of the guest. What is stable is what the control actually rests on: 1 against 0 matching images, 4 against 0 tags, and the direction of the header count, the VBS-off guest yielding more every time. That direction is the positive arm (part 8 §8): a scan that found nothing anywhere would be indistinguishable from a broken one.
The negatives are earned rather than assumed, which is a separate claim and now a measured one: every capture reports reads.failed: 0 with no unreadable page in either scan. A scan that had quietly skipped some fraction of 32,768 pages would have produced the same zero, and until that round nothing in the output could tell the two apart.
The refusal is named, not silent. 0xC0370509 is VM_SAVED_STATE_DUMP_E_VP_VTL_NOT_ENABLED — the provider ships a typed error for exactly this condition, and the probe keeps it apart from a query that fails after a switch that worked. One handler over both would let a provider that cannot return some register report a guest as having no Secure Kernel, which is the same collapse of refused into absent that the next part's read seam is built against.
Two readings about the walk itself, because Secure Kernel's VTL1 page tables are recursively self-mapped — one physical page is a PML4, a PDPT, a PD and a PT depending on the route taken to it:
| guarded walk, 26 Sep | guarded walk, 25 Sep | a sweep that follows the self-map | |
|---|---|---|---|
| table reads | 166 | 179 | 215 |
| tables decoded | 166 | 215 — 36 served at more than one level | — |
| alias prefixes skipped | 6,773 | 7,803 | — |
| leaf mappings / distinct pages | 11,326 / ~4,200 | 16,437 / 4,545 | — |
| worst-case fan-in | — | — | one PD referenced 1023×, one PT 2300×, the root present at all four levels |
The third column is a deliberate diagnostic rather than the production walk: it does not skip the self-map entry, which is how it reaches the page tables as mapped data and shows why enumerating every prefix is combinatorial rather than merely expensive.
A review finding here was right about the fact and wrong about the remedy, and both are worth keeping. The fact: the walk expands each table once per level, and before this round it did not say what that skipped — so it now counts alias prefixes, 6,773 of them, reported beside the leaves. The remedy proposed was to cut cycles by descent path instead, so an aliased table is walked again under each prefix. That was built and measured: it turned a 166-read, 11,326-leaf walk that identifies the image into one that exhausted a 200,000-leaf budget over 509 distinct pages and identified nothing. On a self-mapped tree, the complete VA enumeration it asks for is the thing that cannot terminate usefully.
Two smaller readings from the same instrumentation. No large-page mapping appears in this build's VTL1 tables (every leaf 4 KiB), which is why an address-masking defect in the large-page path stayed latent until review found it by reading: in a large PDPTE or PDE, bit 12 is the PAT flag rather than the low bit of the frame, so masking at 4 KiB granularity lands a page high. And the KDBG half of the physical scan was taught to search across page boundaries — a PE image is page-aligned so a per-page MZ test cannot miss one, but a data block sits at an arbitrary offset, and roughly 1.4% of placements straddle a boundary invisibly. Pairing each page with its physical successor turned up a fifth tag on the earlier capture, which is another coincidental byte sequence. The control still has none.
It is a file, not a live channel
The capture was copied out of D:\Hyper-V\Virtual Machines\Snapshots into a scratch directory and read there with --vmrs and no VM named, so neither WMI nor Hyper-V was consulted. Same CR3 0x107593000, same Secure Kernel, same six modules, same KDBG. The running guest's VTL1 CR3 at that moment was 0x1201000, and the copy insists on 0x107593000 — so the source is demonstrably the bytes on disk.
Capture on the Hyper-V host, analyse anywhere. And nothing was loaded to do it: no driver service exists on that host, and the CodeIntegrity log's most recent entries are from boot with none during any of these runs — worth checking rather than assuming, because part 3's driver-free probe failed precisely by installing a driver while configured not to.
Is a checkpoint carrying VTL1 something to report?
Asked directly, because the reasonable reaction to "a checkpoint hands over Secure Kernel" is to wonder whether Microsoft should hear about it. The evidence says this is documented behaviour inside the boundary VBS claims, on three readings:
- The SDK documents the capability in terms. At
ForceActiveVirtualTrustLevel: "Forces the current Virtual Trust Level of a given virtual processor. This is useful to force register state to and virtual address translation to come from a different VTL." Beside it, four more VTL functions and aGetGuestOsInfothat takes a VTL parameter. Reading a capture's VTL1 register state is an API surface Microsoft shipped, commented and versioned. - The mitigation for the at-rest half exists and was switched off here.
EncryptStateAndVmMigrationTrafficisFalseon both lab guests. A mitigation being offered is design intent stated out loud — and whether it covers VTL1 was then measured, below. - The boundary VBS claims is VTL0 → VTL1 inside the guest. For a guest that is not hardware-isolated, the host partition is inside the TCB — which parts 3 and 4 demonstrate from the other direction, having read the same pages from a running guest with a driver in the root. A guest's VTL1 is the host's to protect.
That third reading used to end "and every route here needs Hyper-V Administrator", which is false. The live routes do. A capture is a file, and the --vmrs route needs nothing but read access to it — so the file's ACL is the gate rather than the Hyper-V role. The sentence contradicted a measurement three paragraphs later in its own document.
Which brings up the one of three reportable cases that is true on this bench today: D:\Hyper-V\Virtual Machines\Snapshots\<id>.vmrs, 1,984,630,784 bytes, grants BUILTIN\Users: ReadAndExecute and NT AUTHORITY\Authenticated Users: Modify. Both ACEs carry the ID (inherited) flag and match D:\'s root ACL, which is the Windows default for a non-system volume; the two ACEs Hyper-V does add are the non-inherited ones. So Hyper-V adds what it needs and does not strip what it inherited, and a VM whose storage sits on a default-ACL'd data volume has its guest RAM readable by any authenticated local user. That is a configuration hazard rather than a product defect — the storage path is the administrator's choice — and it is worth fixing on any bench that keeps VM files off the system volume.
The third case, a hardware-isolated guest (SEV-SNP / TDX) where the host is outside the TCB by construction, is out of scope and nothing here reaches one.
The encryption arm, which answered one question and found a different defect
The interesting reportable case is the first: does VTL1 come out of a capture taken with EncryptStateAndVmMigrationTraffic = $true? If it did, encryption would not be covering what it claims.
It does not. The mitigation holds. But it does not hold by refusing — LoadSavedStateFile never returns:
0xC0000409 __fastfail, uncatchable
Subcode: 0x7 FAST_FAIL_FATAL_APP_EXIT (the CRT's abort())
vmsavedstatedumpprovider!abort +0xD569
vmsavedstatedumpprovider!terminate +0x19716
vmsavedstatedumpprovider!gsl::details::terminate +0x40235
vmsavedstatedumpprovider!PartitionStateParser::GetPartitionStateVirtualProcessors +0x3F294
vmsavedstatedumpprovider!VmSavedStateDumpContentProvider::… +0x3AFCC
vmsavedstatedumpprovider!LoadSavedStateFile +0x35FFA
… the caller's frames …
So LoadSavedStateFile constructs the content provider, which parses the partition state, and GetPartitionStateVirtualProcessors trips a Guidelines Support Library contract. GSL's handler calls std::terminate → abort → __fastfail, and the process is gone: no try/except, no SEH handler and no catch_unwind in the caller sees it.
0xC0000409 is not a diagnosis. Its legacy name is STATUS_STACK_BUFFER_OVERRUN and it is the status Windows raises for every __fastfail, so the name says nothing on its own — this record said "the corruption being detected" for exactly one commit. What says why is the subcode, and subcode 7 is a deliberate exit from a contract check. No memory corruption is involved and none is claimed.
Two controls make the trigger specific rather than "this parser gives up loudly":
- A fresh plaintext capture of the same guest, taken minutes later, reads normally with the identical command. That attributes the failure to the setting rather than to the capture being new.
- Corrupt, truncated and random input are all refused cleanly, with
0x80070570ERROR_FILE_CORRUPT. The encrypted path specifically is the one that is not handled.
And the file was measured rather than inferred about: the encrypted and plaintext captures share the container magic 14 20 28 01 and the field at +0x08, while payload entropy is 8.000 bits/byte against 7.246 — a well-formed container whose body is at maximal entropy. That reading matters more than the provider's behaviour, because the arm ran on the owning host with the VM's key protector available: in that context a successful read would have been authorized decryption and would have said nothing about how the bytes are stored. Provider-level reachability could never have been the evidence.
So: the VBS question came back negative, and the arm surfaced an ordinary robustness defect in a Microsoft SDK component — a documented API that terminates its caller on an input Hyper-V itself wrote, from a supported configuration, using the setting Microsoft documents for protecting saved state. A tool that enumerates a host's checkpoints is killed by one of them and cannot report which file did it. Reported via Feedback Hub, 27 September 2026, against component version 10.0.26100.7705, with a twelve-line repro checked in so a later SDK can be re-tested on two facts: that LoadSavedStateFile returns at all, and that if it fails it does so with an HRESULT.
One practical note on running that arm: Set-VMSecurity -EncryptStateAndVmMigrationTraffic $true is refused on a running VM, so it needs the guest powered off. It is a scheduled lab mutation, not a probe.
The decode, as code, with the source as a parameter
That settles the source. The rest of this part is turning it into something other than a Python probe.
src/sk.rs is the decode — the guarded walk, PE identification, the KdDebuggerDataBlock decode, the SkLoadedModuleList walk — expressed over a source seam whose whole contract is read(gpa, len), root() and max_read(). src/savedstate.rs implements it for a capture. The root arrives as a register read rather than as a constant, which the different-boot table above is the measured reason for.
Run against the third-boot checkpoint, every landmark reproduces from an independent implementation in a different language:
| landmark | the Python probe | src/sk.rs |
|---|---|---|
VTL1 CR3 |
0x107593000 |
0x107593000 |
| root page: present entries / self-map index | 29 / 309 | 29 / 309 |
securekernel.exe base VA |
0xFFFFF8070EDA9000 |
0xFFFFF8070EDA9000 |
KdDebuggerDataBlock |
+0x1335E0, Size 0x3A0 |
+0x1335E0, Size 0x3A0 |
SkLoadedModuleList |
+0x127770 |
+0x127770 |
| VTL1 modules | the same six | the same six, same sizes |
KDBG tags found / accepted |
4 / 1 | 4 / 1 |
| table reads / decodes | 179 / 215 | 179 / 215 |
Two checks the probe did not run, and they are why this is worth more than a second opinion. The structural route — find a loader entry whose DllBase is the identified base with SizeOfImage sixteen bytes later, then follow its Blink — found the list head 0xFFFFF8070EED0770, which is the same head the debugger data block names, reached without reading the block at all. And the provider's own translator agreed with the walk on 373 of 373 pages of the image, with 0 pages mapped by one and not the other. The sample is taken from the image's SizeOfImage rather than from the walk's own leaves, deliberately: comparing on the addresses the walk found would only ask whether we agree about what we found, and a page the walk missed would be invisible to it.
For a later build to be compared against: 16,437 leaf mappings over 4,545 distinct pages, 7,803 alias prefixes counted as unexpanded, 0 malformed entries, 0 unreadable tables, 18,253 physical reads of which 0 failed, 74,764,288 bytes.
And 34 synthetic tests, six of them mutation-verified, pinning the rules — the self-map, the large-page frame mask, a tag straddling a page boundary, a stale list under a matching KernBase, poison against zeros. That is a guard against regression, not more evidence about Windows.
Symbols, with no debuggee anywhere
The decode searches the capture for its two landmarks. The obvious cross-check is to ask a securekernel.pdb that has never seen that capture where the same landmarks are.
The unknown worth settling first was whether image-only resolution is available at all, since every symbol method in the sibling dbgscope crate assumes a session with a target. It is: DbgEng accepts a PE image as a target in its own right. OpenDumpFileWide on C:\Windows\System32\securekernel.exe gives a session with exactly one module at the image's own ImageBase 0x140000000, and .reload /f fetches the PDB from the public store. So this needed no new engine primitive and no execute text hatch, and the whole of its own work is arithmetic: rebasing from 0x140000000 onto the base the walk found.
| landmark | found by searching the capture | read out of the PDB |
|---|---|---|
KdDebuggerDataBlock RVA |
+0x1335E0 |
+0x1335E0 |
SkLoadedModuleList RVA |
+0x127770 |
+0x127770 |
KdDebuggerDataBlock in the guest |
0xFFFFF8070EEDC5E0 |
0xFFFFF8070EEDC5E0 |
SkLoadedModuleList in the guest |
0xFFFFF8070EED0770 |
0xFFFFF8070EED0770 |
Asked the other way round as well, which is a separate engine call and can fail differently: the engine names each guest address with displacement 0. That matters because GetNameByOffset answers with the nearest preceding symbol at any address in the module, so a non-zero displacement is a miss dressed as a hit.
So the module list now has a third route, and it is the only one needing no debugger data block. The tag scan cannot find SkLoadedModuleList — it is a bare LIST_ENTRY with no signature to search for — which is why part 4 read it out of the block and why the structural Blink walk exists. A PDB names it directly. The scan stays primary, because a host with no symbol store, or a build whose PDB is not served, has only that route.
Four things this settles in the negative, and the fourth is the one that cost review rounds:
- The public
securekernel.pdbcarries no type information.dt securekernel!_LIST_ENTRYis not found, every data symbol prints= <no type information>, and fourGetTypeIdprobes come backE_NOINTERFACE— the engine declining to service type queries for this module at all, rather than four names it searched for and did not find. Structure walks over VTL1 stay hand-decoded. A finite set of name probes cannot prove a PDB has no types, which is why the report prints the engine's own reason for each rather than a verdict — and that reason is what makes four negatives worth more than a sample of four. It also retires a promise made one part earlier: part 4 recorded that dropping the DbgEng route would cost "PDB type resolution and structure formatting". Those were never available to be given up. What recovers completely is the names. - The convenience API lies about it.
SymbolKind::has_type_inforeadsDEBUG_SYMTYPE_PDBas private type information, and this module issymbols: pdbwith no types. The engine does not distinguish a stripped public PDB from a private one. Ask for a type; do not ask the kind. - Rebasing inside the engine is available and is a trap.
.reload /i securekernel.exe=fffff8070eda9000,175000with.exepathset does load the image a second time at the guest's base and resolve the same PDB there — but the name collides, so the second module issecurekernel_exeand both answer tosecurekernel!. With the pair loaded,? securekernel!KdDebuggerDataBlockanswers0x1401335E0: the preferred base. That is why the rebase is arithmetic outside the engine. - A host with no symbols is refused, and what that refusal reads was measured. Pointed at a symbol path reaching no store, the module comes back
Exportrather thanDeferred— DbgEng falls back to the image's export table. The first draft accepted a still-deferred module on the grounds thatDeferredmeans "nothing has looked yet"; after the probe it does not, and three review rounds were the cost of that distinction. A module left deferred could load its PDB on the first landmark query — that is, after the only check that the PDB belongs to this image. So the open now issues one deliberately-failing lookup to settle the module, then requires a provider. Refusing costs nothing: with symbols failing to load, neither landmark is reachable from roughly 280 exported names. What is still unmeasured is a PDB that is served and wrong.
Four tools on a file
Both halves were reachable only from a command-line role. Making them MCP tools meant answering three questions that had been deferred on purpose, and the answers are all where things go.
open_sk_capture, sk_modules, sk_read_memory, sk_symbol, in a --tools securekernel group.
The engine lives in a worker, and the decisive argument is measured rather than architectural. Keeping DbgEng out of the process that serves MCP was already this server's rule, which settles the symbol half and says nothing about the decode — which needs no engine at all. What settles that is the encryption arm above: a vendor DLL that __fastfails on an input the caller supplies cannot be loaded beside the session registry. In a worker it costs the one session that named the capture. The robustness defect found by accident turned into the load-bearing argument for a process boundary.
One session handle carries both halves, with symbols opt-in on it, because the symbols are the capture's only through the image the mapping was identified against, and the rebase needs the base the decode found. Two handles would be two halves of a join nothing checks.
And the debugger tools are refused on such a session — by an allow-list, in both directions, in the one funnel every call passes. The reason is specific: the engine in that worker holds securekernel.exe as an image, so read_memory there would read the file and answer as though it had read the guest — and that does not fail. It returns bytes. The refusal names what the engine is actually holding.
That refusal is also what keeps the answers stable. Nothing in the session can run a command, so the target cannot be replaced under the symbols, and the capture does not change — which is why the whole decode travels with the open, counters included, rather than being re-read. Every figure from the gates above came back unchanged over MCP, plus one thing the command-line role could not show: sk_read_memory at the data block's own address returns the self-pointing LIST_ENTRY, then KDBG, then 0x3A0, then 0xFFFFF8070EDA9000 — three of the decode's conclusions read back as bytes through a different path, with the engine naming that address beside them.
The control arm is the other half: the VBS-off twin's capture reports partition VTLs 0x1 against 0x3, the provider refusing the VTL switch by name, and the session opens carrying that as its reason and refuses every read with the same sentence — while its symbols load normally. A run where the engine had symbols and the capture had no VTL1 must not read like a run where neither half worked.
Thirteen new unit tests and four in the smoke harness, every guard mutation-verified — including the kind gate three ways: accept every debugger op on a capture, accept every capture op elsewhere, and replace the allow-list with a deny-list, where the row that then goes through is the one that was not thought of.
Two costs recorded rather than absorbed. The surface grew 7,501 B of model-visible context across 63 → 67 tools, paid by every caller because the default surface is every tool; whether a group should be able to sit outside that default became its own open item rather than a decision made quietly. And the tools/list payload turned out to contain a multiplication: holding the report inline in the shared target summary inlined its schema into all seven openers' $defs, measuring 341,057 B. Moving it into an outcome of its own took 50,844 B back off the wire for a change no client can observe.
What this changes, and what it does not
The audience. An operator with no test-signing, no weakened Code Integrity and no loaded driver can read a captured guest's Secure Kernel — base, modules, memory, symbols — on a machine that is not the host. That decides who can do this work rather than whether it can be done.
It does not retire the driver. The live route is still the only one that reads a guest as it runs. What this adds is a second source with a much lower setup cost.
And it is a snapshot. There is no execution control here, and if anything a fixed capture makes the inspector-versus-debugger question sharper rather than answering it. Everything in parts 1 through 5 reads Secure Kernel. Nothing in them stops it.
That is the next question, and it was the one the series had been deferring since part 1: the hypervisor has a VTL1 debug port that is up, and nothing has ever connected to it. A receiver with no sender.