csvfs.sys — TOCTOU Race Condition / Use-After-Free (CWE-362, CWE-416) in CsvCamSetSecurityInfo fixed
KB5078752
1. Overview
| Field | Value |
|---|---|
| Unpatched binary | csvfs_unpatched.sys |
| Patched binary | csvfs_patched.sys |
| Overall similarity | 0.9921 |
| Matched functions | 1766 |
| Changed functions | 1 |
| Identical functions | 1765 |
| Unmatched (unpatched) | 0 |
| Unmatched (patched) | 0 |
Verdict: The patch fixes a single, surgical defect in csvfs!CsvCamSetSecurityInfo (sub_1C00BCAAC) — an unlocked read of the global EncryptionManager pointer that can be freed concurrently by an exclusive writer, producing a TOCTOU / Use-After-Free on a 0x28-byte PagedPool ('CVCA') Encryption object.
2. Vulnerability Summary
Finding #1 — TOCTOU / Use-After-Free in CsvCamSetSecurityInfo (sub_1C00BCAAC)
- Severity: High
- Vulnerability class: CWE-362 (Race Condition) / CWE-416 (Use-After-Free)
- Affected function:
csvfs!CsvCamSetSecurityInfo(sub_1C00BCAAC,0x1C00BCAACin both builds) - Affected object: The global
Encryption*pointerEncryptionManager(?EncryptionManager@@3PEAVEncryption@@EA), a 0x28-byte object, tag'CVCA'(0x41435643).
Root cause:
CsvCamSetSecurityInfo updates the global Encryption signing manager used for Cluster Shared Volumes. It touches two globals:
EncryptionManager— a small (0x28-byte)Encryptionobject (the unprotected read).ReplayCacheManagerObject— a larger (0x1010-byte) object (already read underSigningLockshared in both builds).
When the caller supplies a non-empty encryption input (*(context + 0xe8) != 0), the unpatched version reads EncryptionManager and dereferences its stored-size (+0x20) and stored-buffer (+0x18) fields, then memcmp's the caller's input buffer (context + 0xec) against the stored buffer — all without acquiring SigningLock. Later in the same function, the exclusive-write path takes KeEnterCriticalRegion + ExAcquireResourceExclusiveLite on SigningLock (?SigningLock@@3PEAVRWLock@@EA), frees the old object via Encryption::scalar deleting destructor` (??_GEncryption,sub_1C0058100→ExFreePoolWithTag`), and replaces the global with a freshly allocated object. Because the read side never shares the lock, the writer can free the object mid-read, leaving the reader holding a dangling pointer.
The patch wraps the read+dereference in RWLock::AcquireShared (?AcquireShared@RWLock@@QEAAXXZ) / RWLock::ReleaseShared (?ReleaseShared@RWLock@@QEAAXXZ) — the shared side of SigningLock — closing the window. Two incidental codegen differences accompany the fix and are not themselves security changes: the entry-cached eax copy of *(context+0xe8) is dropped in favor of an in-memory re-read (cmp [rsi+0xe8], eax), and the length test changes from test eax, eax; jz to cmp [rcx+0xe8], ebp; jbe. For a length field (never negative) the two tests are equivalent — the jbe form does not reject any additional value.
Note on the shared-lock helper:
RWLock::AcquireShared(0x1C0018818) andRWLock::ReleaseShared(0x1C0018850) are new functions in the patched build — they do not exist in the unpatched build. Their bodies hardcode the globalSigningLock:AcquireSharedperformsmov rbx, cs:SigningLock; KeEnterCriticalRegion; ExAcquireResourceSharedLite([rbx], 1), andReleaseSharedperformsExReleaseResourceLite([SigningLock]); KeLeaveCriticalRegion. Because they reference the global directly, the caller'sthis(rcx) is irrelevant — every call acquires the sameSigningLockshared. In the unpatched build this exact shared sequence was emitted inline for theReplayCacheManagerObjectread; the patch factors it into these helpers and additionally applies it to the previously-unprotectedEncryptionManagerread. At the address0x1C0018818the unpatched build instead contains__GSHandlerCheck(an unrelated function relocated a few bytes later in the patched build to make room for the new helpers).
Attacker-reachable entry point and data flow:
GsDriverEntry(sub_1C00BF010) — driver entry stub.DriverEntry(sub_1C00BF078) — driver initialization (init path, not the runtime dispatch of this call).CsvFsNotificationQueueInitialize(sub_1C004800C).CsvFsHandleMessage(sub_1C0005F50) — message dispatcher; callsCsvFsHandleSetSecurityInfo.CsvFsHandleSetSecurityInfo(sub_1C0046974) — validates buffers, then callsCsvCamSetSecurityInfoat0x1C00469D6.CsvCamSetSecurityInfo(sub_1C00BCAAC) — vulnerableEncryptionManagerupdate.
The context field at +0xe8 is the caller-supplied encryption input length; +0xec is the input buffer, compared via memcmp against the stored buffer read from the unlocked EncryptionManager.
3. Pseudocode Diff
// ============ UNPATCHED (vulnerable) ============
int32_t len = *(context + 0xe8); // cached in eax at function entry
if (len != 0) // signed != 0 test
{
Encryption* rdx_1 = EncryptionManager; // <<<< NO LOCK — unprotected global read
r14 = 1;
if (rdx_1) {
int32_t size = *(rdx_1 + 0x20); // <<<< UAF: deref stored size (+0x20)
if (len == size &&
memcmp(context + 0xec, // caller input
*(rdx_1 + 0x18), // <<<< UAF: deref stored buffer (+0x18)
size) == 0)
r14 = 0; // input matches existing manager
}
}
// ...later in this function:
// KeEnterCriticalRegion();
// ExAcquireResourceExclusiveLite(*SigningLock, 1);
// if (EncryptionManager) __GEncryption(EncryptionManager); // <<<< ExFreePoolWithTag of old object
// EncryptionManager = new_object;
// ExReleaseResourceLite(*SigningLock);
// KeLeaveCriticalRegion();
// ============ PATCHED (fixed) ============
if (*(context + 0xe8) > 0) // unsigned > 0 test (cmp [rcx+0xe8], ebp; jbe)
{
r14 = 1;
RWLock::AcquireShared(); // <<<< SHARED LOCK ACQUIRE (SigningLock shared)
Encryption* rdx_1 = EncryptionManager; // read under lock
if (rdx_1)
{
int32_t size = *(rdx_1 + 0x20); // safe deref under shared lock
if (*(context + 0xe8) == size && // <<<< re-read from memory (no stale eax)
memcmp(context + 0xec, *(rdx_1 + 0x18), size) == 0)
r14 = 0;
}
RWLock::ReleaseShared(); // <<<< SHARED LOCK RELEASE
// ...allocate replacement / exclusive swap as before...
}
What the diff tells us:
- The unpatched read of
EncryptionManagerhas noKeEnterCriticalRegion/ExAcquireResourceSharedLitearound it. - The exclusive writer (same function, later in execution, or another thread) does correctly take
ExAcquireResourceExclusiveLiteonSigningLockand frees the object inside that critical region — but that lock is meaningless when the reader never shares it. - The patch adds the matching shared-lock acquire/release (
RWLock::AcquireShared/ReleaseShared). The accompanying re-read of the length from memory and thetest→cmp/jbecodegen change are incidental and functionally equivalent for a length value; the lock is the security-relevant change.
4. Assembly Analysis
4.1 Unpatched — sub_1C00BCAAC (CsvCamSetSecurityInfo)
0x1C00BCAAC: mov [rsp+0x8], rbx
0x1C00BCAB1: mov [rsp+0x10], rbp
0x1C00BCAB6: mov [rsp+0x18], rsi
0x1C00BCABB: push rdi
0x1C00BCABC: push r14
0x1C00BCABE: push r15
0x1C00BCAC0: sub rsp, 0x30
0x1C00BCAC4: mov eax, [rcx+0xe8] ; cache input length in eax <-- PATCH REMOVES THIS CACHE
0x1C00BCACA: xor ebp, ebp
0x1C00BCACC: xor r14b, r14b
0x1C00BCACF: mov rsi, rcx
0x1C00BCAD2: mov r15b, 0x1
0x1C00BCAD5: test eax, eax ; signed != 0 <-- PATCH uses 'cmp [rcx+0xe8], ebp; jbe'
0x1C00BCAD7: jz 0x1C00BCC2E
; === RACE WINDOW START — NO LOCK ===
0x1C00BCADD: mov rdx, cs:EncryptionManager ; <<<< UNLOCKED read of Encryption global
0x1C00BCAE4: mov r14b, r15b
0x1C00BCAE7: test rdx, rdx
0x1C00BCAEA: jz 0x1C00BCB63
0x1C00BCAEC: mov ecx, [rdx+0x20] ; <<<< UAF SINK: deref stored size (+0x20)
0x1C00BCAEF: cmp eax, ecx
0x1C00BCAF1: jnz 0x1C00BCB3A
0x1C00BCAF3: mov rdx, [rdx+0x18] ; <<<< UAF SINK: deref stored buffer (+0x18)
0x1C00BCAF7: mov r8d, ecx ; Size
0x1C00BCAFA: lea rcx, [rsi+0xec] ; Buf1 (caller input)
0x1C00BCB01: call memcmp ; sub_1C0018950 — compares input vs stored buffer
; --- Exclusive-lock path that frees the object Thread A may be reading ---
0x1C00BCBD0: mov rbx, cs:SigningLock
0x1C00BCBD7: call KeEnterCriticalRegion
0x1C00BCBE3: mov rcx, [rbx] ; Resource
0x1C00BCBE6: mov dl, 0x1
0x1C00BCBE8: call ExAcquireResourceExclusiveLite
0x1C00BCBF4: mov rcx, cs:EncryptionManager ; old object
0x1C00BCBFB: test rcx, rcx
0x1C00BCC00: call ??_GEncryption ; <<<< scalar deleting destructor -> ExFreePoolWithTag (sub_1C0058100)
0x1C00BCC0C: mov cs:EncryptionManager, rdi ; replace global with new object
0x1C00BCC16: call ExReleaseResourceLite
0x1C00BCC22: call KeLeaveCriticalRegion
; --- ReplayCacheManagerObject read — CORRECTLY under SigningLock SHARED in BOTH versions ---
0x1C00BCC2E: mov rbx, cs:SigningLock
0x1C00BCC35: call KeEnterCriticalRegion
0x1C00BCC41: mov rcx, [rbx]
0x1C00BCC44: mov dl, 0x1
0x1C00BCC46: call ExAcquireResourceSharedLite ; <-- SHARED LOCK
0x1C00BCC52: mov rax, cs:ReplayCacheManagerObject
...
0x1C00BCCD0: call ExReleaseResourceLite
0x1C00BCCDC: call KeLeaveCriticalRegion
; --- Free helper ??_GEncryption (sub_1C0058100) ---
0x1C0058109: call ?ReleaseAll@Encryption@@AEAAXXZ ; Encryption::ReleaseAll
0x1C0058113: call ExFreePoolWithTag ; deallocate the object
4.2 Patched — relevant change block
0x1C00BCACF: cmp [rcx+0xe8], ebp ; unsigned > 0
0x1C00BCAD5: jbe 0x1C00BCC40
0x1C00BCADE: call ?AcquireShared@RWLock@@QEAAXXZ ; <<<< SigningLock shared acquire (sub_1C0018818)
0x1C00BCAE3: mov rdx, cs:EncryptionManager ; read under lock
0x1C00BCAEA: test rdx, rdx
0x1C00BCAED: jz 0x1C00BCB67
0x1C00BCAEF: mov eax, [rdx+0x20] ; safe deref under lock
0x1C00BCAF2: cmp [rsi+0xe8], eax ; re-read context+0xe8 from memory
0x1C00BCAFA: mov rdx, [rdx+0x18]
0x1C00BCB08: call memcmp ; sub_1C00189C0
...
0x1C00BCB67: call ?ReleaseShared@RWLock@@QEAAXXZ ; <<<< SigningLock shared release (sub_1C0018850)
4.3 Key takeaways from the assembly
- The unprotected read is a single
mov rdx, cs:EncryptionManagerwith no precedingKeEnterCriticalRegionand no precedingExAcquireResource*/RWLock::AcquireSharedcall. - The exclusive-write path does correctly serialize against itself (it takes both critical region and exclusive resource) — but the missing shared-lock on the read makes the entire exclusive critical region useless for protecting against the racing reader.
- The
ReplayCacheManagerObjectread shows the correct pattern already in place:KeEnterCriticalRegion → ExAcquireResourceSharedLite → read → ExReleaseResourceLite → KeLeaveCriticalRegion. The patch replicates this (via theRWLockwrapper) for theEncryptionManagerread. - Address/symbol reuse across builds:
0x1C0018818=RWLock::AcquireShared(patched) vs__GSHandlerCheck(unpatched);sub_1C0018950(unpatched) /sub_1C00189C0(patched) =memcmp;sub_1C0058100=Encryption::scalar deleting destructor` (??_GEncryption`).
5. Trigger Conditions
- Target environment. The victim machine must have the Cluster Shared Volume File System driver loaded — i.e., a node of a Windows Server Failover Cluster with at least one CSV enabled. Verify with
fltmc filters(look forcsvfs) orsc query csvfs. - Volume handle. Open a handle reaching the CSV set-security-info path.
- Message routing. Drive a set-security-info operation that routes through
CsvFsHandleMessage(sub_1C0005F50) →CsvFsHandleSetSecurityInfo(sub_1C0046974). - Input validation.
CsvFsHandleSetSecurityInfo(sub_1C0046974) validates the input before callingCsvCamSetSecurityInfo. - Trigger flag. The context field at
+0xe8(encryption input length) must be> 0— supply a non-empty encryption input so theEncryptionManagerpath is taken. - Racing threads. Fire at least two concurrent operations from separate threads targeting the same CSV volume. Ideally many threads (8–32) to amplify the probability of hitting the narrow race window.
- Race window. Thread A executes
0x1C00BCADD(read pointer) →0x1C00BCAEC(deref +0x20) →0x1C00BCAF3(deref +0x18) →0x1C00BCB01(call memcmp). Thread B must enter the exclusive-write path (0x1C00BCBD0→0x1C00BCC00) and free the object Thread A read. - Widen the window. Pin threads to specific CPUs via
SetThreadAffinityMask, lower priority on the racing thread, or useNtYieldExecutionto encourage preemption. Stagger the threads so Thread B reaches the free path while Thread A is between the read and the first deref. - Confirm. If no pool reclamation occurs: BSOD
PAGE_FAULT_IN_NONPAGED_AREA (0x50)or an invalid pool read, faulting address inside a freed PagedPool block, callercsvfs!CsvCamSetSecurityInfo+0x40. With Driver Verifier Special Pool enabled: bug checkDRIVER_VERIFIER_DETECTED_VIOLATIONorSPECIAL_POOL_DETECTED_MEMORY_CORRUPTION, with parameter 1 = the freed pool address. If the freed slot is reclaimed with controlled data, the reader instead dereferences an attacker-influenced+0x18pointer and+0x20size insidememcmp; note thememcmpresult is consumed only as a match/no-match boolean (it setsr14), so it is not a data-return channel.
6. Exploit Primitive & Development Notes
Primitive. The bug is a narrow-window UAF on a 0x28-byte PagedPool allocation tagged 'CVCA' (0x41435643) holding an Encryption object. After a concurrent thread frees the object at 0x1C00BCC00, the racing reader dereferences the freed object's stored-size (+0x20) and stored-buffer pointer (+0x18) at 0x1C00BCAEC / 0x1C00BCAF3 and passes them to memcmp at 0x1C00BCB01.
Demonstrable impact:
- Denial of service (kernel crash). If the freed 0x28 block is unmapped or poisoned (e.g., Driver Verifier Special Pool), the dereference at
0x1C00BCAECfaults, bugchecking the machine. This is the directly reproducible outcome. - Attacker-influenced dereference. If the freed slot is reclaimed with controlled data before the dereference, the
+0x18pointer and+0x20size read from the reclaimed object are attacker-influenced. The size (+0x20) becomes thememcmplength and the pointer (+0x18) becomes thememcmpsource, somemcmpreadssizebytes from an attacker-influenced address — an out-of-bounds read against kernel memory. Thememcmpresult is consumed only as a match/no-match boolean that decides whether the manager is rebuilt (r14); it is not returned to the caller, so this is not by itself an information-disclosure channel.
Constraints on weaponization (not demonstrated here):
- Winning the race requires two concurrent invocations reaching the
EncryptionManagerpath (*(context+0xe8) > 0) on the same globals, with one freeing the object during the other's read window (0x1C00BCADD→0x1C00BCB01). The operation runs atPASSIVE_LEVEL. - Turning the attacker-influenced dereference into anything beyond a crash requires reclaiming the freed 0x28 PagedPool slot with controlled contents, which is not established against these binaries and is out of scope for this report.
7. Debugger PoC Playbook
Below is the playbook for an analyst with kd/WinDbg attached to the unpatched driver, building a PoC for the UAF in CsvCamSetSecurityInfo.
7.1 Symbols / module setup
!load mex ; (optional) helpful
.reload /f csvfs.sys=............
lm csvfs ; confirm base
u csvfs!CsvCamSetSecurityInfo L40 ; sanity check entry
Replace ............ with the loaded base — typically obtainable from lm csvfs.
7.2 Breakpoints
| # | Command | Why |
|---|---|---|
| 1 | bp csvfs!CsvCamSetSecurityInfo |
Function entry. rcx = arg1 = security context. Check [rcx+0xe8] (input length). |
| 2 | bp csvfs!CsvCamSetSecurityInfo+0x31 (0x1C00BCADD) |
Unlocked read of EncryptionManager. rdx will hold the Encryption object pointer — the dangling-pointer-in-waiting. |
| 3 | bp csvfs!CsvCamSetSecurityInfo+0x40 (0x1C00BCAEC) |
Vulnerable dereference mov ecx, [rdx+0x20] (stored size). If the pool has been reclaimed, ecx contains attacker-controlled or stale data. |
| 4 | bp csvfs!CsvCamSetSecurityInfo+0x47 (0x1C00BCAF3) |
Second vulnerable dereference mov rdx, [rdx+0x18] (stored buffer). |
| 5 | bp csvfs!CsvCamSetSecurityInfo+0x154 (0x1C00BCC00) |
The free: call ??_GEncryption. rcx = object being freed. Compare against the rdx value captured at BP #2 on the racing thread. |
| 6 | bp csvfs!__GEncryption (sub_1C0058100) |
The Encryption scalar deleting destructor (ReleaseAll then ExFreePoolWithTag). Confirms the pool address freed. |
A conditional break to skip the boring path:
bp csvfs!CsvCamSetSecurityInfo ".if (poi(@rcx+0xe8) == 0) { gc } .else { .echo ENTERING VULNERABLE PATH; kn; }"
7.3 What to inspect at each breakpoint
| BP | Register / Expression | Meaning |
|---|---|---|
| #1 | rcx, [rcx+0xe8], [rcx+0xe0], [rcx+0xe4] |
Security context, input length, volume IDs. |
| #2 | rdx, !pool rdx 2 |
The EncryptionManager pointer just read unlocked. Tag should be CVCA, size 0x28. |
| #3 | ecx after mov, !pte rdx |
The stored size read from the potentially freed object. |
| #5 | rcx |
The pointer being freed. If this equals the rdx from BP #2 on another thread → UAF confirmed. |
| Global watch | EncryptionManager |
The Encryption global pointer itself. Break whenever any thread reads/writes it. |
Useful pool inspection commands:
!poolfind 0x41435643 4 ; enumerate all live 'CVCA' allocations
!pool <faulting_addr> 2 ; tag / size of faulting address
7.4 Key instruction offsets
0x1C00BCAC4—mov eax, [rcx+0xe8]— caches the input length ineax. (Patch removes this caching.)0x1C00BCAD5—test eax, eax— signed!= 0predicate. (Patch:cmp [rcx+0xe8], ebp; jbe— unsigned> 0.)0x1C00BCADD—mov rdx, cs:EncryptionManager— UNLOCKED global read. (Patch insertscall ?AcquireShared@RWLockat0x1C00BCADEjust before.)0x1C00BCAEC—mov ecx, [rdx+0x20]— UAF sink #1 (stored size).0x1C00BCAF3—mov rdx, [rdx+0x18]— UAF sink #2 (stored buffer).0x1C00BCB01—call memcmp(sub_1C0018950) — consumes the leaked/reclaimed data.0x1C00BCC00—call ??_GEncryption(sub_1C0058100) — Encryption scalar deleting destructor /ExFreePoolWithTagof the old object.0x1C00BCC0C—mov cs:EncryptionManager, rdi— replaces global after free.
7.5 Trigger setup (from user mode)
- Ensure CSV is enabled and
csvfsis loaded:fltmc filtersfrom an elevated cmd. - Open a handle reaching the CSV set-security-info path.
- Drive a set-security-info operation that routes to
CsvFsHandleSetSecurityInfoviaCsvFsHandleMessage(sub_1C0005F50) with a non-empty encryption input so the context field+0xe8is> 0. - Race: spawn 16–32 threads all issuing the same operation against the same volume, at high rate. Optionally throttle the racing thread with
SetThreadAffinityMaskto a different logical processor. - Observe: the BSOD or, with controlled reclamation, the controlled
+0x18/+0x20data feedingmemcmp.
7.6 Expected observations
- Crash without reclamation: Bug check
0x50 PAGE_FAULT_IN_NONPAGED_AREA.Parameter 2= freed PagedPool address. Call stack ends incsvfs!CsvCamSetSecurityInfo+0x40(0x1C00BCAEC) reading[rdx+0x20]. - Crash with Special Pool: Bug check
0xC4 DRIVER_VERIFIER_DETECTED_VIOLATION(code0x20use-after-free) or0xCC SPECIAL_POOL_DETECTED_MEMORY_CORRUPTION. Parameter 1 = pool address. - No crash (controlled reuse):
memcmpruns against attacker-supplied+0x18/+0x20— an attacker-directed kernel read.
7.7 Struct / offset notes
Security context (arg1) layout:
+0x000 : (header / context fields)
+0x0e0 : volume identifier A (32-bit, used to build ReplayCacheManager)
+0x0e4 : volume identifier B (32-bit)
+0x0e8 : encryption input length (int32) — must be > 0 to hit the EncryptionManager path
+0x0ec : encryption input buffer (compared via memcmp)
Encryption object (EncryptionManager), size 0x28, tag 'CVCA':
+0x18 : stored buffer pointer (Buf2 for memcmp)
+0x20 : stored size (int32; compared with *(context+0xe8) and used as memcmp Size)
SigningLock : ?SigningLock@@3PEAVRWLock@@EA — a C++ RWLock wrapping an ERESOURCE.
exclusive = ExAcquireResourceExclusiveLite (guards the free)
shared = RWLock::AcquireShared / ReleaseShared (added by the patch around the read)
Pool tag constant : 0x41435643 ('CVCA')
8. Changed Functions — Full Triage
Only one function changed between the two binaries.
CsvCamSetSecurityInfo (sub_1C00BCAAC, similarity 0.9638, change_type = security_relevant)
- What changed: The unprotected read+dereference of the global
EncryptionManagerpointer was wrapped inRWLock::AcquireShared/RWLock::ReleaseShared(the shared side ofSigningLock). Incidentally, the entry-cachedeaxcopy of*(context+0xe8)was replaced by an in-memory re-read, and the length test changed fromtest eax, eax; jztocmp [rcx+0xe8], ebp; jbe; for a length value these two tests are equivalent and are not security-relevant on their own. - Helper functions introduced by the patch:
RWLock::AcquireShared(0x1C0018818) andRWLock::ReleaseShared(0x1C0018850) exist only in the patched build. Their bodies reference the globalSigningLockdirectly (AcquireShared:KeEnterCriticalRegion+ExAcquireResourceSharedLite([SigningLock], 1);ReleaseShared:ExReleaseResourceLite+KeLeaveCriticalRegion). In the unpatched build this identical shared-lock sequence was emitted inline around theReplayCacheManagerObjectread; the patch factors it into these two helpers, reuses them for that read and theUpdateGlobalSequenceNumberpath, and newly applies them to the previously-unprotectedEncryptionManagerread. - Address/symbol reuse: At
0x1C0018818the unpatched build contains__GSHandlerCheck(an unrelated function that the patched build relocates a few bytes later to0x1C0018888to make room for the new helpers).sub_1C0058100=Encryption::scalar deleting destructor` (??_GEncryption), whose body callsEncryption::ReleaseAll(0x1C0058109) thenExFreePoolWithTag(0x1C0058113).sub_1C0018950(unpatched) /sub_1C00189C0(patched) =memcmp`.
CsvCamSetSecurityInfo is the only function whose behavior changed; the two RWLock helpers are the newly-emitted code that implements the fix.
9. Unmatched Functions
- Removed: none.
- Added:
RWLock::AcquireShared(0x1C0018818) andRWLock::ReleaseShared(0x1C0018850). These two helper functions exist only in the patched build; they encapsulate theSigningLockshared acquire/release sequence that the unpatched build emitted inline. (The aggregate table in §1 pairs functions by address, so these two are counted against the__GSHandlerCheck/__GSHandlerCheckCommonfunctions that occupy those addresses in the unpatched build rather than reported as unmatched; by content they are genuinely new.)
No added sanitizers and no removed mitigations. The behavioral fix is confined to CsvCamSetSecurityInfo: a SigningLock shared lock (via the two new helpers) is added around the previously-unprotected EncryptionManager read. This makes the diff easy to audit and confirms that the patch targets the UAF in CsvCamSetSecurityInfo.
10. Confidence & Caveats
Confidence: High.
- The diff is unambiguous: one function, two inserted
RWLock::AcquireShared/ReleaseSharedcalls around a read that was previously unlocked, plus a tightened predicate and a re-read from memory. The pattern mirrors the protection already present on theReplayCacheManagerObjectread, which serves as an internal reference implementation. - The assembly at
0x1C00BCADDthrough0x1C00BCB01is unambiguously an unprotected read-then-deref-then-memcmp, and0x1C00BCC00is unambiguously a free of the same object under aSigningLockexclusive lock the reader does not share. - The vulnerable function is reachable from
CsvFsHandleMessage(sub_1C0005F50) →CsvFsHandleSetSecurityInfo(sub_1C0046974) →CsvCamSetSecurityInfo.
Assumptions to verify manually before PoC:
- Message/FSCTL code. Confirm via dynamic tracing which user-mode operation routes into
CsvFsHandleSetSecurityInfo(case handling insideCsvFsHandleMessage). - Privilege requirement. Whether a low-privileged user can reach the set-security-info path, or whether local-administrators-only access is required, depends on the cluster's security descriptor. Verify with
!sdon the device object. - Pre-conditioning
*(context+0xe8). Confirm what supplies the non-empty encryption input that flips this length positive. - Pool-reclamation primitive choice. The 0x28 PagedPool size class is small enough for many spray primitives; the optimal one depends on the target Windows build and pool allocator.
- Race timing. The window is a few instructions plus the
memcmp. On a multi-core system the race is winnable but requires many threads or careful CPU pinning. Validate empirically with DV Special Pool first to confirm reachability, then disable DV for the real attempt. - Address changes. The addresses reflect this specific build. If you reverse a different build, rebase — the pattern (unlocked
EncryptionManagerread vsSigningLock-protected exclusive free) is what matters.