1. Overview

  • Unpatched Binary: mrxsmb_unpatched.sys
  • Patched Binary: mrxsmb_patched.sys
  • Overall Similarity: 0.9536
  • Diff Statistics: Matched: 1246 | Changed: 44 | Identical: 1202 | Unmatched (Unpatched): 0 | Unmatched (Patched): 0
  • Verdict: The patch adds ExBlockOnAddressPushLock synchronization to the SMB redirector's session-entry lookup so that a thread which matches a session entry whose initialization has not completed waits for the constructing thread instead of proceeding. The worst demonstrable outcome of the unsynchronized code is a NULL/invalid-pointer dereference in the credential check (a local bugcheck / denial of service). The change is delivered behind the WIL feature gate Feature_Servicing_AlternativePort_Dcr, with the pre-patch code path retained verbatim as SmbCeFindSessionEntry_Old — a staged rollout.

2. Vulnerability Summary

Finding: Use of a not-fully-initialized session entry in the VNetRoot lookup

  • Severity: Low
  • Vulnerability Class: Race condition on a partially-initialized object; demonstrable impact is a NULL pointer dereference (CWE-476, arising from CWE-362)
  • Affected Function: SmbCeFindSessionEntry (unpatched @ 0x140008700) / new SmbCeFindSessionEntry (patched @ 0x1400334F0); the unpatched behavior is retained in the patched build as SmbCeFindSessionEntry_Old (@ 0x1400476D4)

Root Cause: The unpatched SmbCeFindSessionEntry walks the server entry's session list (head at server_entry+0x210) under the exclusive spinlock at server_entry+0x780. It matches an entry by state (+0xc <= 5), a non-zero session refcount (+0x1a8), and SessionID/LogonID (+0x60/+0x64 or +0x68/+0x6c), increments the reference counts, releases the spinlock, and then calls CheckCredentials with the entry's security-context pointer at +0x80 and, on the session-setup path, SeAccessCheck.

The unpatched code has no notion of an "initialization complete" flag on the entry. The patched build adds two per-entry flags (+0x230 session-initialized, +0x231 transport-initialized) and a pushlock (+0x238), and inserts ExBlockOnAddressPushLock wait loops: a thread that matches an entry whose +0x230/+0x231 flag is not yet set releases the spinlock, blocks on the pushlock until the constructing thread signals completion, then re-acquires the spinlock and re-checks. The constructing side signals completion in SmbCeDecrementActiveVNetRootContextsOnSession (patched @ 0x14001BA30), which sets +0x231 and calls ExUnblockOnAddressPushLockEx(entry+0x238, 0).

Demonstrable impact: The construction routine SmbCeFindOrConstructSessionEntry (unpatched @ 0x1400081E0) populates the security-context pointer (+0x80), the match keys (+0x60..+0x6c), and the session refcount (+0x1a8) before it links the new entry into the searchable +0x210 list, and it performs that link under the same server_entry+0x780 spinlock that the finder holds. A concurrent finder therefore cannot observe the entry in the list with an unset +0x80. The initialization that the patch waits on is a later stage (session/transport setup, which performs network I/O after the entry is already listed). On the unsynchronized path the only unguarded dereference of the security-context pointer is inside CheckCredentials at 0x140008A56 (cmp [rax+0x38], rdx, where rax is the +0x80 pointer): if that pointer is NULL for the matched entry, this faults and bugchecks. The SeAccessCheck path is not a sink — it explicitly null-checks +0x80 (at 0x1400088CF) and the descriptor at +0x38 (at 0x1400088D8) before calling SeAccessCheck.

The reachable impact is therefore a local kernel bugcheck (denial of service) if the timing race is won. There is no attacker-controlled memory content (the entry is zero-initialized pool), so this is not an authentication bypass, not an information leak, not a memory-corruption primitive, and not remotely reachable.

Attacker-Reachable Entry Point & Data Flow: 1. User-mode NtCreateFile / CreateFile on a UNC network path (e.g., \\server\share\file). 2. MRxSmbCreateVNetRoot (RDBSS minirdr tree-connect callback). 3. SmbCeFindOrConstructVNetRootContext (@ 0x1400079F0). 4. SmbCeFindOrConstructSessionEntry (@ 0x1400081E0). 5. SmbCeFindSessionEntry (@ 0x140008700) — the lookup that lacks the initialization wait.

3. Pseudocode Diff

// --- UNPATCHED SmbCeFindSessionEntry @ 0x140008700 ---
// Under the server-entry spinlock, on a matched entry:
if (entry->state <= 5 && entry->session_refcount != 0 &&
    entry->session_id == target_sid && entry->logon_id == target_lid) {
    entry->vnr_refcount_active++;   // +0x1ac
    entry->session_refcount++;      // +0x1a8
    _InterlockedAdd(&entry->refcount, 1);   // +0x8
    ExReleaseSpinLockExclusive(server_entry + 0x780, OldIrql);

    // No wait for initialization completion:
    CheckCredentials(entry->security_context /* +0x80 */, arg2, flag);
    // ... SeAccessCheck path below null-checks +0x80 and [+0x80]+0x38 first ...
}

// --- PATCHED SmbCeFindSessionEntry @ 0x1400334f0 ---
if (matched(entry)) {
    if (!entry->session_init /* +0x230 */) {          // NEW
        _InterlockedAdd(&entry->refcount, 1);
        ExReleaseSpinLockExclusive(server_entry + 0x780, OldIrql);
        if (prev) SmbCeDereferenceSessionEntryEx(prev, 0);
        do {                                          // NEW: block until initialized
            ExBlockOnAddressPushLock(entry + 0x238, entry + 0x230, &v, 1, ...);
        } while (!entry->session_init);
        ExAcquireSpinLockExclusive(server_entry + 0x780);
    }
    if (entry->session_init || entry->state <= 5) {
        if (!entry->transport_init /* +0x231 */) {    // NEW: same wait on +0x231
            do { ExBlockOnAddressPushLock(entry + 0x238, entry + 0x231, &v, 1, ...); }
            while (!entry->transport_init);
        }
        // ... proceed to refcount + CheckCredentials on a fully-initialized entry ...
    }
}

4. Assembly Analysis

The block below is the vulnerable lookup path in the unpatched binary. The spinlock is released before the credential check; there is no wait for an initialization flag.

; --- UNPATCHED SmbCeFindSessionEntry @ 0x140008700 ---
0x140008750  lea   rcx, [rdi+780h]                  ; server-entry spinlock
0x140008757  call  cs:__imp_ExAcquireSpinLockExclusive
0x140008773  lea   rdx, [rbx+210h]                  ; session-entry list head
0x140008780  mov   rbx, [rdx]                        ; first entry
0x140008797  cmp   dword ptr [rbx+0Ch], 5            ; state <= 5 gate
0x14000879B  jg    0x140008972
0x1400087A1  mov   r9d, [rbx+1A8h]                   ; session refcount
0x1400087A8  test  r9d, r9d
0x1400087AB  jz    0x140008972                       ; skip if zero
0x1400087B4  cmp   [rbx+60h], ecx                    ; match SessionID ([rsi+48h])
0x1400087C0  cmp   [rbx+64h], eax                    ; match LogonID   ([rsi+4Ch])
0x1400087C9  inc   dword ptr [rbx+1ACh]              ; VnrCtxts++
0x1400087E8  mov   [rbx+1A8h], r9d                   ; session refcount++
0x140008805  lock xadd [rbx+8], r8d                  ; refcount++ (atomic)
0x14000882C  lea   rcx, [rdi+780h]
0x140008841  call  cs:__imp_ExReleaseSpinLockExclusive   ; spinlock released
; --- no ExBlockOnAddressPushLock; no wait on any init flag ---
0x14000884D  mov   rcx, [rbx+80h]                    ; security-context pointer
0x14000885B  call  CheckCredentials                  ; @ 0x1400089FC
0x14000888F  mov   rax, [rbx+80h]                    ; security-context pointer
0x1400088CF  test  rax, rax                          ; null-check +0x80
0x1400088D2  jz    0x1400088E1
0x1400088D4  mov   rcx, [rax+38h]                    ; security descriptor
0x1400088D8  test  rcx, rcx                          ; null-check descriptor
0x1400088DB  jnz   0x14005A4FE
0x14005A53B  call  cs:__imp_SeAccessCheck            ; only reached with non-NULL SD

The unguarded dereference is in CheckCredentials (@ 0x1400089FC), which receives the +0x80 pointer as its first argument:

; --- CheckCredentials @ 0x1400089FC (identical logic in both builds) ---
0x140008A12  mov   rax, rcx                          ; rax = security-context (+0x80)
0x140008A2F  test  rax, rax
0x140008A32  jz    0x140008A3C                        ; only the [rax+8] read is guarded
0x140008A48  test  r8b, r8b                           ; arg3
0x140008A4D  mov   rdx, [r14+38h]
0x140008A56  cmp   [rax+38h], rdx                     ; NULL-deref sink if rax == 0

The patched SmbCeFindSessionEntry (@ 0x1400334F0) inserts the wait before this point:

; --- PATCHED SmbCeFindSessionEntry @ 0x1400334F0 ---
0x1400335DF  movzx eax, byte ptr [rbx+230h]           ; session-init flag
0x1400335E8  jnz   0x1400336F9                          ; already initialized -> proceed
0x140033694  lea   rdx, [rbx+230h]
0x14003369B  lea   rcx, [rbx+238h]                      ; per-entry pushlock
0x1400336A2  call  cs:__imp_ExBlockOnAddressPushLock    ; wait for +0x230
0x140033709  movzx eax, byte ptr [rbx+231h]             ; transport-init flag
0x1400337CB  lea   rdx, [rbx+231h]
0x1400337D2  call  cs:__imp_ExBlockOnAddressPushLock    ; wait for +0x231

5. Trigger Conditions

  1. Session Setup: Attacker operates within a standard local user logon session (no special privilege required, but local code execution is required — the path is not remotely reachable).
  2. Target: A UNC path (e.g., \\SERVER\SHARE) whose tree-connect drives MRxSmbCreateVNetRoot.
  3. Race Generation: Multiple threads in the same logon session call CreateFileW / NtCreateFile on the same UNC path concurrently, released together via a barrier, so that one thread is still completing session/transport setup of a newly constructed session entry while another matches it.
  4. Fault Window: The second thread proceeds to CheckCredentials on the matched entry before setup completes. If the entry's security-context pointer (+0x80) is NULL at that instant, the cmp [rax+0x38] at 0x140008A56 faults.
  5. Observable Effect: A kernel bugcheck (e.g., PAGE_FAULT_IN_NONPAGED_AREA, 0x50). No unauthorized access is granted — the SeAccessCheck path null-checks its inputs and cannot be reached with a NULL/garbage descriptor.

6. Exploit Primitive & Development Notes

  • Primitive: None beyond a local denial of service. The matched entry is zero-initialized pool memory; its contents are not attacker-controlled, so there is no read/write primitive, no information leak, and no way to forge a security descriptor. The SeAccessCheck call site guards both +0x80 and the descriptor at +0x38, so it cannot be coerced into evaluating attacker data.
  • Reachability caveat: Because the construction routine sets +0x80 and the match keys before publishing the entry into the searchable list under the same spinlock, even the NULL-dereference outcome depends on the security-context pointer being legitimately NULL for the matched entry (e.g., a credential-less session) at the moment of the race. This is a narrow, timing-dependent condition and is the reason the change is best characterized as concurrency hardening with a local-DoS worst case rather than a memory-safety exploitation primitive.
  • Rollout note: The fix is gated by Feature_Servicing_AlternativePort_Dcr; when the feature is disabled the caller dispatches to SmbCeFindSessionEntry_Old, which is byte-for-byte the pre-patch behavior. This is a staged rollout, not an unconditional fix.

7. Debugger PoC Playbook

To observe the race on an unpatched system with a kernel debugger attached:

  • Breakpoints:
  • mrxsmb!SmbCeFindSessionEntry+0xc9 (0x1400087C9) — a matching entry has been found and its refcount is being incremented. rbx is the session entry. Inspect rbx+0x80 (security-context pointer) and rbx+0x1a8 (session refcount).
  • mrxsmb!SmbCeFindSessionEntry+0x14d (0x14000884D) — mov rcx, [rbx+0x80], immediately after the spinlock release, feeding the security-context pointer into CheckCredentials.
  • mrxsmb!CheckCredentials+0x5a (0x140008A56) — cmp [rax+0x38], rdx, the NULL-dereference sink when rax (the +0x80 pointer) is zero.

  • Registers & Memory to Watch:

  • rbx = matched session entry.
  • rbx+0x80 = security-context pointer (crash candidate at CheckCredentials+0x5a).
  • rbx+0x1a8 = session refcount (match gate).
  • rsi+0x48 / rsi+0x4c = target SessionID / LogonID used for matching.

  • Key Offsets:

  • 0x140008841: ExReleaseSpinLockExclusive — end of the locked region.
  • 0x14000885b: call CheckCredentials.
  • 0x140008A56: NULL-deref sink inside CheckCredentials.

  • Trigger Setup:

  • Set the breakpoints above.
  • Spawn 10+ threads in one process, gated on a CreateEvent barrier.
  • Have all threads call CreateFile(L"\\\\SERVER\\SHARE\\test", ...) for the same share.
  • Signal the event to release them simultaneously; a slow-negotiating server widens the setup window.

  • Expected Observation: If the race is won with a credential-less entry, a bugcheck (e.g. 0x50) occurs at 0x140008A56. The SeAccessCheck call at 0x14005A53B is not reached with an invalid descriptor because of the null checks at 0x1400088CF/0x1400088D8.

8. Changed Functions — Full Triage

The diff is dominated by the Feature_Servicing_AlternativePort_Dcr rollout (alternative-port / transport-address plumbing) and a few unrelated WIL feature stagings (Feature_Servicing_AllowFileIdZeroOnShareRoot, Feature_Servicing_SmbCompressionOverQuicFix). Every pre-patch behavior that the feature changes is retained verbatim under an _Old twin and selected by a runtime feature check, i.e. staged rollout.

  • SmbCeFindSessionEntry (@ 0x140008700) → new SmbCeFindSessionEntry (@ 0x1400334F0), old retained as SmbCeFindSessionEntry_Old (@ 0x1400476D4) (Security-relevant): Adds ExBlockOnAddressPushLock waits on initialization flags +0x230/+0x231 before using a matched session entry. Worst demonstrable impact of the unsynchronized path is a local NULL-deref bugcheck. See the finding above.
  • SmbCeDecrementActiveVNetRootContextsOnSession (unpatched @ 0x140008F60 / patched @ 0x14001BA30) (Security-relevant, companion): The patched version sets the transport-init flag +0x231 and calls ExUnblockOnAddressPushLockEx(entry+0x238, 0) to wake threads blocked in the new lookup. The unpatched version has no such wake.
  • SmbCeFindOrConstructSessionEntry (unpatched @ 0x1400081E0 / patched @ 0x14001A980) (Behavioral): Dispatches to SmbCeFindSessionEntry vs SmbCeFindSessionEntry_Old via Feature_Servicing_AlternativePort_Dcr; passes an extra output parameter for the new path.
  • MRxSmbValidateAndGetTransportParameters (@ 0x140030D30) (Behavioral, not security-relevant): The pre-patch parser is retained verbatim as MRxSmbValidateAndGetTransportParameters_Old (@ 0x14003E2D0); the feature path adds a boolean gate and the new MRxSmbValidateIncomingPortsAgainstEffective port-consistency check. The parsed "TransportInfo" EA and the +0x344 target belong to the caller's own outbound connection; no privilege boundary is crossed. See Finding 2 in the companion report.
  • SmbCeInitializeConnectionInfo (@ various) (Behavioral): Adds a pointer-overflow guard and dialect/port-aware selection via the alternative-port helpers; no standalone vulnerability fix.
  • RxCeEstablishConnection / RxCeInitializeConnectionCallOutContext / VctEstablishConnection (Behavioral): Each retained as an _Old twin and split into helper routines for the alternative-port path (feature-gated refactor).
  • RxCeGetPortFrom{Policy,Registry}ServerTransportPortMappings (Behavioral): Retained as _Old twins; the feature path uses the new SmbCeGetPortFrom{Policy,Registry}... and port-scope lookups.
  • sub_140009D00, sub_14001CEEC, MRxSmbProcessClientMoveNotification, sub_140030080, sub_14000A850, MRxSmbCreateVNetRoot, sub_14008A220/sub_14008D5A0 (Behavioral/cosmetic): Dialect/port-aware dispatch, structure-offset adjustments, and minor guard additions consistent with the expanded transport structures.

9. Unmatched Functions

New functions introduced by the feature rollout include SmbCeFindSessionEntry_Old, MRxSmbValidateAndGetTransportParameters_Old, MRxSmbValidateIncomingPortsAgainstEffective, the SmbCe*PortScopeEntry* / SmbCeComputeEffectivePort* / SmbCeGetPortFrom* helpers, and the RxCe*_Old / Vct*_Old twins. All are alternative-port / feature-staging plumbing.

10. Confidence & Caveats

  • Confidence: High on the mechanism and direction: the patched build adds ExBlockOnAddressPushLock waits on new +0x230/+0x231 flags in the session-entry lookup, with the constructing side signaling via ExUnblockOnAddressPushLockEx, and the pre-patch path retained as SmbCeFindSessionEntry_Old.
  • Impact rating: Low. The reachable worst case is a local kernel bugcheck (DoS). The SeAccessCheck call site null-checks its inputs, so there is no authentication bypass; the entry is zero-initialized pool, so there is no information-leak or memory-corruption primitive; and the path is local-only. Even the NULL-deref requires winning a narrow timing window against an entry whose security-context pointer is legitimately NULL.
  • Rollout: The change is feature-gated (Feature_Servicing_AlternativePort_Dcr) with the old path retained, i.e. a staged rollout rather than an unconditional fix.