ksrext.sys — loader-callback synchronization refactor
KB5087538
1. Overview
| Field | Value |
|---|---|
| Unpatched binary | ksrext_unpatched.sys |
| Patched binary | ksrext_patched.sys |
| Overall similarity | 0.3722 (low — substantial refactoring) |
| Matched functions | 119 |
| Changed functions | 69 |
| Identical functions | 50 |
| Unmatched (either direction) | 0 / 0 |
Verdict: The change to the Soft Restart loader-callback interface is an architectural refactor, not a security fix. In both builds the teardown path (KsrRegisterLoaderCallbacks(NULL)) and the API dispatch path are correctly serialized against each other, so no race condition or NULL-pointer dereference is reachable. The unpatched build holds the loader-interface push lock shared across the entire dispatch (pointer read and indirect call) while the teardown path holds the exclusive variant of the same lock; the reader/writer lock already prevents the teardown from clearing a pointer while any dispatch is in progress. The patched build replaces this "lock held across the callback" model with a reference-count-plus-drain model and stores built-in default implementations instead of the previous NULL sentinel. Both models are race-free.
2. Change Summary
Finding 1 — Loader-callback interface synchronization refactor in KsrRegisterLoaderCallbacks
| Attribute | Detail |
|---|---|
| Severity | None (no security-relevant change) |
| Class | Synchronization refactor / expanded callback contract |
| Function | KsrRegisterLoaderCallbacks (exported; unpatched @ 0x1C0007D40, patched @ 0x14000B080) |
| Entry point | Exported kernel API KsrRegisterLoaderCallbacks, used by the Windows Soft Restart loader subsystem |
What the code actually does
ksrext.sys keeps a set of named loader-callback function-pointer globals: KsrpQueryMetadataRoutinePointer, KsrpPersistMemoryPartitionRoutinePointer, KsrpFreePersistentMemoryBlockRoutinePointer, KsrpClaimPersistentMemoryRoutinePointer, KsrpFreePersistentMemoryRoutinePointer, KsrpPersistMemoryWithMetadataRoutinePointer, and KsrpEnumeratePersistentMemoryRoutinePointer. KsrRegisterLoaderCallbacks populates them from the caller's registration structure when arg1 != NULL, and clears/resets them when arg1 == NULL.
Every exported dispatch API (KsrQueryMetadata, KsrPersistMemoryPartition, KsrFreePersistedMemoryBlock, KsrClaimPersistedMemory, KsrFreePersistedMemory, KsrPersistMemoryWithMetadata) reads the relevant pointer and, if it is non-NULL, calls through it with a CFG-guarded indirect call. If the pointer is NULL, the dispatch falls back to the driver's own built-in implementation (the KsrPd* routines) under KsrpPdDatabaseLock. NULL is a valid sentinel meaning "loader not registered, use built-in path" — it is not a crash condition.
Why there is no race (unpatched build)
In every unpatched dispatch function, the pointer read and the indirect call both occur while holding KsrpLoaderInterfaceLock shared:
KsrQueryMetadata:ExAcquirePushLockSharedEx(KsrpLoaderInterfaceLock)@0x1C00072EE, pointer read @0x1C00072FA, guarded call @0x1C0007368,ExReleasePushLockSharedEx@0x1C0007379.KsrFreePersistedMemoryBlock: shared acquire @0x1C0007C7A, pointer read @0x1C0007C86, guarded call @0x1C0007CD7, shared release @0x1C0007CE8.KsrClaimPersistedMemory,KsrFreePersistedMemory,KsrPersistMemoryWithMetadata,KsrPersistMemoryPartition: same acquire-read-call-release-under-shared-lock structure.
The teardown path in KsrRegisterLoaderCallbacks clears the pointers while holding the exclusive variant of the same KsrpLoaderInterfaceLock (acquired @ 0x1C0007DB3, pointers cleared @ 0x1C0007EDA–0x1C0007F04, released @ 0x1C0007F88). Because a push lock's exclusive owner cannot coexist with any shared holder, the teardown cannot zero a pointer while a dispatch holds the shared lock across its check-and-call. This is the standard, correct reader/writer serialization; no thread can observe a non-NULL pointer and then call through a freshly-zeroed one.
What changed in the patched build
The patched build does not add a missing guard; it changes the locking discipline:
- Reference count instead of lock-held-across-callback. Patched dispatch functions call
KsrpAcquireLoaderInterfaceReference(@0x14000B5DC), which takesKsrpLoaderInterfaceLockshared only long enough tolock inc KsrpLoaderInterfaceRefCount, then releases it. The callback then runs without the push lock held, andKsrpReleaseLoaderInterfaceReference(@0x14000BDE4) decrements the count afterward. This avoids holding a push lock across a potentially long loader callback. - Drain at teardown. Before clearing the pointers, patched
KsrRegisterLoaderCallbacksruns a drain loop (0x14000B0FD–0x14000B16D): it checksKsrpLoaderInterfaceRefCountunder the exclusive lock and, while non-zero, releases the lock and blocks inExBlockOnAddressPushLockonKsrpLoaderInterfaceRefCount/KsrpLoaderInterfaceEventuntil the count reaches zero. - Default implementations instead of NULL. The unregister path now stores the driver's built-in database routines (
KsrDbQueryMetadata,KsrDbPersistMemoryPartition,KsrDbUnpersistMemoryPartition,KsrDbClaimPersistedPages,KsrDbFreePersistedPages,KsrDbFreePersistedMemoryBlock,KsrDbEnumeratePersistedPages,KsrDbPersistPagesWithMetadata) into the routine pointers (@0x14000B2D4–0x14000B34C) instead of zeroing them. These are real implementations, not no-op stubs; the effect is that dispatch always calls through a valid pointer and no longer needs a NULL fallback branch. - Expanded callback contract. The minimum registration-structure size grows from
0x58(unpatchedcmp ecx, 58h@0x1C0007D65) to0x88(patchedcmp ecx, 88h@0x14000B0B3), and the register path reads additional callback fields at offsets0x58–0x80(KsrpPullRemoteFileRoutinePointer,KsrpSaveKeyToBufferPointer,KsrpIsBugcheckActive,KsrpOpenKeyForBugCheckRecoveryPointer,KsrpSetSystemBootMetadata, and an unpersist routine at[rdi+80h]). This is feature growth of the loader interface.
ExBlockOnAddressPushLock is not new to the patched build: the unpatched KsrpWaitForPartitionsRestored (@ 0x1C00091E4) already calls it (@ 0x1C0009230) to wait on the boot-time KsrpPartitionRestoreInProgress flag. That routine waits for partition restoration to finish; it is not an in-flight-dispatch counter.
3. Pseudocode Diff
KsrRegisterLoaderCallbacks — Unregistration Path
// UNPATCHED (@ 0x1C0007D40) — clears pointers under exclusive lock
NTSTATUS KsrRegisterLoaderCallbacks(void *arg1)
{
if (arg1 && (*(uint32_t*)arg1 < 0x58 ... )) { /* size/flag checks */ }
KeEnterCriticalRegion();
KsrpWaitForPartitionsRestored(); // waits for boot-time restore
ExAcquirePushLockExclusiveEx(&KsrpLoaderInterfaceLock, 0);
if (arg1 == NULL) { // unregister
if (!KsrpLoaderRegistered) KeBugCheckEx(0x70, 0x201, 0, 0, 0);
KsrpLoaderRegistered = 0;
KsrpPersistMemoryWithMetadataRoutinePointer = 0; // seven pointers
KsrpPersistMemoryPartitionRoutinePointer = 0; // set to 0 by
KsrpClaimPersistentMemoryRoutinePointer = 0; // individual movs
KsrpFreePersistentMemoryRoutinePointer = 0;
KsrpFreePersistentMemoryBlockRoutinePointer= 0;
KsrpEnumeratePersistentMemoryRoutinePointer= 0;
KsrpQueryMetadataRoutinePointer = 0;
// ZwQuerySystemInformation(0x96 ...) + KsrpPdInitializeDatabase under KsrpPdDatabaseLock
}
ExReleasePushLockExclusiveEx(&KsrpLoaderInterfaceLock, 0);
KeLeaveCriticalRegion();
}
// PATCHED (@ 0x14000B080) — drains a refcount, stores default impls
NTSTATUS KsrRegisterLoaderCallbacks(void *arg1)
{
if (arg1 && (*(uint32_t*)arg1 < 0x88 ... )) { /* larger struct required */ }
KeEnterCriticalRegion();
KsrpWaitForPartitionsRestored();
for (;;) { // drain in-flight references
ExAcquirePushLockExclusiveEx(&KsrpLoaderInterfaceLock, 0);
if (KsrpLoaderInterfaceRefCount == 0) break;
ExReleasePushLockExclusiveEx(&KsrpLoaderInterfaceLock, 0);
while (KsrpLoaderInterfaceRefCount > 0)
ExBlockOnAddressPushLock(&KsrpLoaderInterfaceEvent,
&KsrpLoaderInterfaceRefCount, &n, 4, 0);
}
if (arg1 == NULL) { // unregister
if (!KsrpLoaderRegistered) KeBugCheckEx(0x70, 0x201, 0, 0, 0);
KsrpLoaderRegistered = 0;
KsrpQueryMetadataRoutinePointer = KsrDbQueryMetadata; // default
KsrpPersistMemoryPartitionRoutinePointer = KsrDbPersistMemoryPartition;
KsrpUnpersistMemoryPartitionRoutinePointer = KsrDbUnpersistMemoryPartition;
KsrpClaimPersistentMemoryRoutinePointer = KsrDbClaimPersistedPages;
KsrpFreePersistentMemoryRoutinePointer = KsrDbFreePersistedPages;
KsrpFreePersistentMemoryBlockRoutinePointer= KsrDbFreePersistedMemoryBlock;
KsrpEnumeratePersistentMemoryRoutinePointer= KsrDbEnumeratePersistedPages;
KsrpPersistMemoryWithMetadataRoutinePointer= KsrDbPersistPagesWithMetadata;
// ZwQuerySystemInformation(0x96 ...) + KsrDbpAttachDatabase under KsrpPartitionLock
}
ExReleasePushLockExclusiveEx(&KsrpLoaderInterfaceLock, 0);
KeLeaveCriticalRegion();
}
Representative Dispatch Pattern
// UNPATCHED KsrQueryMetadata (@ 0x1C0007290) — check-and-call fully under shared lock
NTSTATUS KsrQueryMetadata(...)
{
KeEnterCriticalRegion();
ExAcquirePushLockSharedEx(&KsrpLoaderInterfaceLock, 0); // shared held...
void *p = KsrpQueryMetadataRoutinePointer;
if (p != NULL)
r = guard_dispatch_icall(p)(...); // ...across the call
else { // NULL = valid fallback
ExAcquirePushLockExclusiveEx(&KsrpPdDatabaseLock, 0);
r = KsrPdQueryRegionMetadata(...); // built-in implementation
ExReleasePushLockExclusiveEx(&KsrpPdDatabaseLock, 0);
}
ExReleasePushLockSharedEx(&KsrpLoaderInterfaceLock, 0); // released after call
KeLeaveCriticalRegion();
return r;
}
// PATCHED KsrQueryMetadata (@ 0x14000AE40) — refcount instead of lock-across-callback
NTSTATUS KsrQueryMetadata(...)
{
KeEnterCriticalRegion();
KsrpAcquireLoaderInterfaceReference(); // shared lock only to inc refcount
void *p = KsrpQueryMetadataRoutinePointer; // always non-NULL now
r = guard_dispatch_icall(p)(...); // callback runs w/o lock
KsrpReleaseLoaderInterfaceReference(); // dec refcount
KeLeaveCriticalRegion();
return r;
}
4. Assembly Analysis
Unpatched KsrRegisterLoaderCallbacks unregister path (@ 0x1C0007D40)
00000001C0007DA5 call KsrpWaitForPartitionsRestored
00000001C0007DAC lea rcx, KsrpLoaderInterfaceLock
00000001C0007DB3 call cs:__imp_ExAcquirePushLockExclusiveEx ; exclusive acquire
00000001C0007DBF test rdi, rdi ; arg1 == NULL?
00000001C0007DC2 jz loc_1C0007EA2 ; -> unregister
...
00000001C0007EA2 cmp cs:KsrpLoaderRegistered, bl
00000001C0007EA8 jnz short loc_1C0007ECB
00000001C0007EBE call cs:__imp_KeBugCheckEx ; not registered -> bugcheck
00000001C0007ECD mov cs:KsrpLoaderRegistered, bl ; bl = 0
00000001C0007EDA mov cs:KsrpPersistMemoryWithMetadataRoutinePointer, rbx ; rbx = 0
00000001C0007EE1 mov cs:KsrpPersistMemoryPartitionRoutinePointer, rbx
00000001C0007EE8 mov cs:KsrpClaimPersistentMemoryRoutinePointer, rbx
00000001C0007EEF mov cs:KsrpFreePersistentMemoryRoutinePointer, rbx
00000001C0007EF6 mov cs:KsrpFreePersistentMemoryBlockRoutinePointer, rbx
00000001C0007EFD mov cs:KsrpEnumeratePersistentMemoryRoutinePointer, rbx
00000001C0007F04 mov cs:KsrpQueryMetadataRoutinePointer, rbx
The seven pointers are cleared by seven individual mov stores of the zeroed register rbx, all under the exclusive KsrpLoaderInterfaceLock. There is no memset/rep stosb and no contiguous fixed-size table zeroing in this function.
Patched KsrRegisterLoaderCallbacks drain + default-impl store (@ 0x14000B080)
000000014000B0F6 call KsrpWaitForPartitionsRestored
000000014000B117 mov r9d, 4
000000014000B126 lea rdx, KsrpLoaderInterfaceRefCount
000000014000B12D lea rcx, KsrpLoaderInterfaceEvent
000000014000B134 call cs:__imp_ExBlockOnAddressPushLock ; drain wait
000000014000B140 mov eax, cs:KsrpLoaderInterfaceRefCount
000000014000B14B jg short loc_14000B117
000000014000B14F lea rcx, KsrpLoaderInterfaceLock
000000014000B156 call cs:__imp_ExAcquirePushLockExclusiveEx ; exclusive after drain
000000014000B162 mov eax, cs:KsrpLoaderInterfaceRefCount
000000014000B16D jnz short loc_14000B0FD ; re-drain if raced up
...
000000014000B2D4 lea rax, KsrDbPersistPagesWithMetadata
000000014000B2E1 mov cs:KsrpPersistMemoryWithMetadataRoutinePointer, rax ; default impl
000000014000B2EF lea rax, KsrDbPersistMemoryPartition
000000014000B2F8 mov cs:KsrpPersistMemoryPartitionRoutinePointer, rax
000000014000B2FF lea rax, KsrDbUnpersistMemoryPartition
000000014000B306 mov cs:KsrpUnpersistMemoryPartitionRoutinePointer, rax
000000014000B30D lea rax, KsrDbClaimPersistedPages
000000014000B314 mov cs:KsrpClaimPersistentMemoryRoutinePointer, rax
000000014000B31B lea rax, KsrDbFreePersistedPages
000000014000B322 mov cs:KsrpFreePersistentMemoryRoutinePointer, rax
000000014000B329 lea rax, KsrDbFreePersistedMemoryBlock
000000014000B330 mov cs:KsrpFreePersistentMemoryBlockRoutinePointer, rax
000000014000B337 lea rax, KsrDbEnumeratePersistedPages
000000014000B33E mov cs:KsrpEnumeratePersistentMemoryRoutinePointer, rax
000000014000B345 lea rax, KsrDbQueryMetadata
000000014000B34C mov cs:KsrpQueryMetadataRoutinePointer, rax
Unpatched dispatch (KsrQueryMetadata @ 0x1C0007290) — lock spans check and call
00000001C00072EE call cs:__imp_ExAcquirePushLockSharedEx ; KsrpLoaderInterfaceLock (shared)
00000001C00072FA mov rax, cs:KsrpQueryMetadataRoutinePointer
00000001C0007301 test rax, rax
00000001C0007304 jnz short loc_1C0007352 ; non-NULL -> call via ptr
00000001C000730F call cs:__imp_ExAcquirePushLockExclusiveEx ; NULL -> built-in path
00000001C0007334 call KsrPdQueryRegionMetadata ; (KsrpPdDatabaseLock)
00000001C0007368 call cs:__guard_dispatch_icall_fptr ; guarded indirect call
00000001C0007379 call cs:__imp_ExReleasePushLockSharedEx ; released after call
Patched dispatch reference helper (KsrpAcquireLoaderInterfaceReference @ 0x14000B5DC)
000000014000B5E2 lea rcx, KsrpLoaderInterfaceLock
000000014000B5E9 call cs:__imp_ExAcquirePushLockSharedEx
000000014000B5F5 lock inc cs:KsrpLoaderInterfaceRefCount ; atomic increment
000000014000B5FE lea rcx, KsrpLoaderInterfaceLock
000000014000B605 call cs:__imp_ExReleasePushLockSharedEx
Key Differences at a Glance
| Aspect | Unpatched | Patched |
|---|---|---|
| Dispatch serialization | KsrpLoaderInterfaceLock held shared across pointer read and indirect call |
Shared lock held only to lock inc KsrpLoaderInterfaceRefCount; callback runs without the lock |
| Teardown serialization | KsrpLoaderInterfaceLock held exclusive while clearing pointers |
Drain KsrpLoaderInterfaceRefCount to zero via ExBlockOnAddressPushLock, then exclusive lock |
| Pointer teardown value | 0 (NULL sentinel; dispatch falls back to KsrPd* built-in) |
Built-in KsrDb* default implementations |
| Registration struct min size | 0x58 |
0x88 (new callback fields at 0x58–0x80) |
| Race between teardown and dispatch | Prevented by reader/writer lock | Prevented by refcount drain |
Both models are race-free; the pointer is never observed non-NULL and then called after being cleared.
5. Reachability
-
Load state:
ksrext.sysis the Soft Restart kernel extension, loaded on supported systems when the feature is enabled. -
Entry point:
KsrRegisterLoaderCallbacksis an export with no internal callers; it is invoked by the Windows Soft Restart loader subsystem during registration/unregistration. The dispatch APIs (KsrQueryMetadata,KsrPersistMemory*,KsrFreePersisted*,KsrClaimPersistedMemory) are the driver's exported service routines. -
Concurrency: Even with concurrent calls to a dispatch API on one thread and
KsrRegisterLoaderCallbacks(NULL)on another, the shared/exclusiveKsrpLoaderInterfaceLock(unpatched) or the reference-count drain (patched) serializes teardown against in-progress dispatch. A dispatch that reads a non-NULL pointer holds the shared lock until after its indirect call returns, so teardown cannot clear that pointer mid-call. -
NULL is not a fault: When the loader is unregistered, the routine pointers are either NULL (unpatched, handled by the built-in
KsrPd*fallback branch) or set to the built-inKsrDb*implementations (patched). Neither state produces a NULL indirect call.
No attacker-reachable crash or memory-safety primitive follows from this change.
6. Exploit Primitive & Development Notes
No exploit primitive is present. The claimed teardown-versus-dispatch NULL-pointer dereference is not reachable in either build:
- The unpatched dispatch functions read the routine pointer and perform the indirect call entirely under
KsrpLoaderInterfaceLockheld shared; the teardown clears the pointers only under the exclusive variant of the same lock. The reader/writer lock makes the two mutually exclusive. - A NULL routine pointer in the unpatched build is a valid state that routes dispatch to the driver's built-in
KsrPd*implementation, not a dereference of address 0.
The patched changes (reference-count drain, built-in default implementations, larger registration contract) are a robustness/architecture refactor and a feature expansion of the loader interface, not a security fix. There is no denial-of-service, information-disclosure, or code-execution primitive to develop.
7. Debugger Notes
The following symbols and addresses describe the relevant code paths for confirmation. They do not correspond to any exploitable condition.
| Symbol / Address | Purpose |
|---|---|
0x1C0007D40 |
KsrRegisterLoaderCallbacks (unpatched); unregister path at 0x1C0007EA2 |
0x14000B080 |
KsrRegisterLoaderCallbacks (patched); drain loop 0x14000B0FD–0x14000B16D, default-impl stores 0x14000B2D4–0x14000B34C |
0x1C00091E4 |
KsrpWaitForPartitionsRestored (unpatched); waits on KsrpPartitionRestoreInProgress via ExBlockOnAddressPushLock @ 0x1C0009230 |
0x14000B5DC |
KsrpAcquireLoaderInterfaceReference (patched); lock inc KsrpLoaderInterfaceRefCount @ 0x14000B5F5 |
0x14000BDE4 |
KsrpReleaseLoaderInterfaceReference (patched) |
0x1C0007290 |
KsrQueryMetadata (unpatched); shared lock 0x1C00072EE, pointer read 0x1C00072FA, guarded call 0x1C0007368, release 0x1C0007379 |
0x14000AE40 |
KsrQueryMetadata (patched); reference acquire 0x14000AE85, guarded call 0x14000AEA7, reference release 0x14000AEAE |
KsrpLoaderInterfaceLock |
Reader/writer push lock guarding the routine-pointer globals |
KsrpLoaderInterfaceRefCount / KsrpLoaderInterfaceEvent |
Patched in-flight reference count and drain wait object |
KsrpLoaderRegistered |
Registration flag (1 = registered, 0 = unregistered) |
To confirm the serialization: disassemble any unpatched dispatch export and observe that ExAcquirePushLockSharedEx(KsrpLoaderInterfaceLock) precedes the routine-pointer read and ExReleasePushLockSharedEx follows the guarded indirect call, with the exclusive acquisition in KsrRegisterLoaderCallbacks targeting the same lock.
8. Changed Functions — Full Triage
Loader-interface synchronization refactor (not a security fix)
| Function | Similarity | Note |
|---|---|---|
KsrRegisterLoaderCallbacks |
0.74 | Teardown changed from clearing pointers to NULL under the exclusive KsrpLoaderInterfaceLock to draining KsrpLoaderInterfaceRefCount and storing built-in KsrDb* default implementations. Registration-structure minimum size grew 0x58 → 0x88 with new callback fields. Both builds are race-free. |
KsrQueryMetadata |
0.12 | Dispatch changed from "shared lock held across the indirect call" to "acquire/release a reference count around the call." Pointer is non-NULL in the patched model (defaults to KsrDbQueryMetadata), so the NULL fallback branch is removed. |
KsrFreePersistedMemoryBlock |
0.81 | Same dispatch refactor (KsrpFreePersistentMemoryBlockRoutinePointer). |
KsrPersistMemoryPartition |
0.58 | Same dispatch refactor (KsrpPersistMemoryPartitionRoutinePointer); lock usage restructured around the reference count. |
KsrClaimPersistedMemory |
0.64 | Same dispatch refactor. |
KsrFreePersistedMemory |
0.74 | Same dispatch refactor. |
KsrPersistMemoryWithMetadata |
0.67 | Same dispatch refactor. |
KsrpDestoryPartitionList (sub_1C0009A70) |
0.976 | Lock acquisition adjusted as part of the surrounding refactor; caller-side locking changed. No reachable memory-safety impact. |
KsrpAllocatePagesFromMemoryRange (sub_1C00088A0) |
0.83 | Page-mapping loop (MmMapLockedPagesWithReservedMapping) extracted to a helper in the patched build. Behavior preserved. |
Cosmetic / equivalent changes
sub_1C00084A4(sim 0.967): flag test changed from& 0x10to a sign-based< 0check — semantically identical.sub_1C000BE1C(sim 0.967): SSE-inlinedmemsetreplacing a call — equivalent behavior.
Note on the low overall similarity (0.3722): most of the 69 changed functions reflect the loader-interface architecture change (routine-pointer globals now carry built-in defaults, dispatch uses a reference count, the registration contract grew) plus general recompilation. None of this constitutes a security fix; the pre-existing reader/writer serialization already prevented the teardown-versus-dispatch race.
9. Unmatched Functions
| Direction | Count | Notes |
|---|---|---|
| Removed (unpatched only) | 0 | None |
| Added (patched only) | 0 | None |
All function changes are matched pairs. The patched KsrpAcquireLoaderInterfaceReference / KsrpReleaseLoaderInterfaceReference reference helpers and the KsrDb* default implementations are matched against existing regions rather than reported as standalone additions.
10. Confidence & Caveats
Confidence: High (no security-relevant change)
- The unpatched dispatch functions were verified to hold
KsrpLoaderInterfaceLockshared across both the routine-pointer read and the guarded indirect call, and the teardown to hold the same lock exclusive while clearing the pointers. The reader/writer lock prevents the teardown-versus-dispatch race in the unpatched build. - A NULL routine pointer in the unpatched build is handled by an explicit fallback to the built-in
KsrPd*implementation, so it is not a dereference of address 0. - The patched build's
ExBlockOnAddressPushLockdrain andKsrDb*default implementations replace the lock-across-callback model;ExBlockOnAddressPushLockitself already existed in the unpatched build insideKsrpWaitForPartitionsRestored.
Notes
- The registration-structure minimum-size increase (
0x58→0x88) and the added callback fields reflect an expanded loader contract, not a security boundary change. KsrpWaitForPartitionsRestoredwaits on the boot-timeKsrpPartitionRestoreInProgressflag; it is not an in-flight-dispatch counter, and the unpatched teardown's use of it is unrelated to dispatch serialization (which the push lock provides).
End of report.