1. Overview

Field Value
Unpatched binary mrxsmb_unpatched.sys
Patched binary mrxsmb_patched.sys
Overall similarity 0.9898
Matched functions 1230
Changed functions 6 (behavioural cluster); remaining textual diffs are data-symbol relocations
Identical functions 1224
Unmatched (unpatched → patched) 0 / 0

Verdict: The patch removes the Feature_3679766840__private_IsEnabledDeviceUsageNoInline() feature-staging gate across a cluster of SMB-over-QUIC functions and, in SmbQuicInitiateAsynchronousConnect, replaces a buffer-reuse code path with an unconditional "query-first" call to MsQuicConnectionGetParam (NULL output buffer, zeroed stack-local size). This is a defensive/hardening refactor. The behaviour that the earlier draft framed as an exploitable stale-pointer write is not demonstrable against these binaries: the connection block is zero-initialized at allocation, and the cached buffer pointer/size fields are always kept consistent (allocated-and-sized together, freed-and-nulled together). No reachable memory-safety primitive was found, so no CWE/severity is assigned.


2. Change Summary

Finding 1 — SmbQuicInitiateAsynchronousConnect buffer handling (informational, no demonstrable vulnerability)

  • Severity: None (defense-in-depth hardening)
  • Function: SmbQuicInitiateAsynchronousConnect (unpatched base 0x14008F110, patched base 0x14008F100)
  • Class: Refactor of an output-buffer allocation path; removal of a feature-staging gate

What the code actually does

SmbQuicInitiateAsynchronousConnect operates on a connection block passed in as *(a1+0x40) (call it blk; the same pointer is rbx in the disassembly). The block is allocated by SmbMmAllocateServerTransport via ExAllocatePool2(0x40, 664, 'SmCe') — pool flag 0x40 does not set the uninitialized bit, so the whole block, including the two fields of interest, is zero-filled on allocation.

Two fields matter: - blk+0xE0 — a cached output buffer pointer (pool tag 'SmQu', 0x75516D53). - blk+0xE8 — the ULONG size associated with that buffer.

The function queries a QUIC connection parameter (level 0x5000018) through the MsQuic dispatch table at vtable offset +0x20.

Unpatched. The first MsQuicConnectionGetParam call is gated by Feature_3679766840: - Feature disabled (default): pass the block's cached buffer *(blk+0xE0) as the output buffer and blk+0xE8 as the size in/out pointer. On the very first connect these are 0/0, so MsQuic returns STATUS_BUFFER_TOO_SMALL without writing; the function then allocates a 'SmQu' buffer of the returned size, stores it into blk+0xE0, and retries. This is a buffer-reuse path: a buffer allocated on one pass can be reused on a later pass. - Feature enabled: pass a NULL output buffer and a zeroed stack-local size, then allocate and retry.

Patched. The feature gate is gone. The first call is always made with a NULL output buffer and a zeroed stack-local size, the function allocates a fresh 'SmQu' buffer of the returned size, calls again with that fresh buffer, and only then stores the buffer/size into blk+0xE0/blk+0xE8. The cached buffer is never handed to the API before validation.

Why this is not a demonstrable vulnerability

The earlier framing was that *(blk+0xE0) could be a stale/freed/uninitialized pointer that MsQuic would then write into. That premise is not supported by the binaries:

  1. Zero-initialized block. The block comes from ExAllocatePool2(0x40, 664, 'SmCe') (no uninitialized flag), so blk+0xE0 and blk+0xE8 are 0 on first use. A NULL buffer / zero size yields STATUS_BUFFER_TOO_SMALL with no write — identical outcome to the patched query-first path.
  2. Buffer and size stay consistent. The only writes to blk+0xE0 set it either to a freshly allocated buffer (with blk+0xE8 set to its size) or to 0 (with blk+0xE8 set to 0 on the failure path). There is no path that leaves blk+0xE0 non-NULL with an inconsistent size.
  3. Every free nulls the field. The 'SmQu' buffer at blk+0xE0 is freed in two places — the local failure path (ExFreePoolWithTag immediately followed by blk+0xE0 = 0, blk+0xE8 = 0) and SmbQuicTearDown (ExFreePoolWithTag followed by a memset that zeroes the field). No free leaves a dangling pointer in blk+0xE0.

No code path was found that re-enters this function on a block whose blk+0xE0 holds a freed pointer, and the block is not observed to be used uninitialized. The patch therefore reads as defense-in-depth: it stops trusting a stored output-buffer pointer and always queries the size first with a NULL buffer.


3. Decompilation Diff

// ===== UNPATCHED: SmbQuicInitiateAsynchronousConnect (feature-gated) =====
// blk = *(a1 + 0x40);  v15 -> connection handle at blk+0x70
Feature_3679766840__private_IsEnabledDeviceUsageNoInline();
v43 = 0;                                   // stack-local size
v25 = nullptr;
IsEnabled = Feature_3679766840__private_IsEnabledDeviceUsageNoInline();
v27 = *v15;                                // connection handle
v29 = *(GetParam)(MsQuic + 32);
if ( IsEnabled == 0 ) {                    // default: reuse cached buffer/size
    v31 = (unsigned int *)(blk + 232);     // &blk[0xE8]
    v30 = v29(v27, 83886104, blk + 232, *(_QWORD *)(blk + 224));  // cached buffer *(blk+0xE0)
} else {                                   // feature on: query-first
    v30 = v29(v27, 83886104, (__int64)&v43, 0);                   // NULL buffer, stack size
    v31 = (unsigned int *)(blk + 232);
}
if ( v30 == 0xC0000023 /*STATUS_BUFFER_TOO_SMALL*/ ) {
    if ( Feature_3679766840...() != 0 ) {
        v33 = ExAllocatePool2(66, v43, 0x75516D53);               // stack size
        ...
    } else {
        v38 = ExAllocatePool2(66, *v31, 0x75516D53);              // blk[0xE8] size
        *(_QWORD *)(blk + 224) = v38;                             // store buffer
        ...
    }
    v39 = (*GetParam)(*v15, 83886104, v37, v36);                  // retry with buffer
}

// ===== PATCHED: unconditional query-first =====
v31 = 0;                                                          // stack-local size
v22 = (*GetParam)(*v12, 83886104, &v31, 0);                      // NULL buffer, stack size
if ( v22 == 0xC0000023 /*STATUS_BUFFER_TOO_SMALL*/ ) {
    v23 = ExAllocatePool2(66, v31, 0x75516D53);                  // fresh allocation
    if ( v23 == 0 ) goto fail;
    v25 = (*GetParam)(*v12, 83886104, &v31, v23);                // retry with fresh buffer
    if ( v25 >= 0 ) {
        *(_QWORD *)(v1 + 224) = v24;                             // store buffer *after* success
        *(_DWORD *)(v1 + 232) = v31;                             // store size
        v31 = 0;
    } else {
        ExFreePoolWithTag(v24, 0x75516D53);
    }
}

Difference: the unpatched default branch reuses the stored buffer *(blk+0xE0) on the first call; the patched code never does — it always queries with a NULL buffer and a stack-local size, allocates fresh, and stores into blk+0xE0/blk+0xE8 only after a successful second call. The Feature_3679766840 gate is removed.


4. Assembly Analysis

Unpatched SmbQuicInitiateAsynchronousConnect — first MsQuicConnectionGetParam call

000000014008FA07  call    Feature_3679766840__private_IsEnabledDeviceUsageNoInline
000000014008FA0C  and     [rsp+78h+arg_0], 0        ; stack-local size = 0
000000014008FA16  call    Feature_3679766840__private_IsEnabledDeviceUsageNoInline
000000014008FA1B  mov     rcx, [r12]                ; rcx = connection handle
000000014008FA1F  test    eax, eax                  ; feature enabled?
000000014008FA21  mov     rax, cs:MsQuic
000000014008FA28  mov     r15d, 5000018h            ; QUIC param level
000000014008FA2E  mov     edx, r15d
000000014008FA31  mov     rax, [rax+20h]            ; MsQuic vtable +0x20 = GetParam
000000014008FA35  jz      short loc_14008FA50       ; feature OFF -> reuse cached buffer
000000014008FA37  xor     r9d, r9d                  ; feature ON: r9 = NULL buffer
000000014008FA3A  lea     r8, [rsp+78h+arg_0]       ; r8 = &stack size
000000014008FA42  call    _guard_dispatch_icall     ; GetParam(handle, 0x5000018, &stacksize, NULL)
000000014008FA47  lea     r14, [rbx+0E8h]
000000014008FA4E  jmp     short loc_14008FA66
000000014008FA50  mov     r9, [rbx+0E0h]            ; feature OFF: r9 = cached buffer *(blk+0xE0)
000000014008FA57  lea     r14, [rbx+0E8h]           ; r14 = &blk[0xE8]
000000014008FA5E  mov     r8, r14                   ; r8 = &blk[0xE8] (size in/out)
000000014008FA61  call    _guard_dispatch_icall     ; GetParam(handle, 0x5000018, &blk[0xE8], cached buffer)
000000014008FA66  mov     r9d, eax
000000014008FA69  cmp     eax, 0C0000023h           ; STATUS_BUFFER_TOO_SMALL?
000000014008FA6E  jnz     loc_14008FC20
000000014008FA74  call    Feature_3679766840__private_IsEnabledDeviceUsageNoInline
000000014008FA79  mov     ebp, 75516D53h            ; pool tag 'SmQu'
000000014008FA7E  mov     ecx, 42h                  ; POOL_FLAG_NON_PAGED | POOL_FLAG_UNINITIALIZED
000000014008FA83  mov     r8d, ebp
000000014008FA86  test    eax, eax
000000014008FA88  jz      short loc_14008FAF6       ; feature OFF -> alloc using blk[0xE8] size
000000014008FA8A  mov     edx, [rsp+78h+arg_0]      ; feature ON: size from stack local
000000014008FA91  call    cs:__imp_ExAllocatePool2
; ...
000000014008FAF6  mov     edx, [r14]                ; feature OFF: size = *(blk+0xE8) (set by MsQuic)
000000014008FAF9  call    cs:__imp_ExAllocatePool2
000000014008FB05  mov     [rbx+0E0h], rax           ; store fresh buffer into blk[0xE0]
; ... retry GetParam with the fresh buffer ...

Patched SmbQuicInitiateAsynchronousConnect — unconditional query-first

000000014008F923  and     dword ptr [rbx+0A8h], 0
000000014008F92A  lea     r8, [rsp+78h+arg_0]       ; r8 = &stack size
000000014008F932  and     [rsp+78h+arg_0], 0        ; stack size = 0
000000014008F93A  mov     ebp, 5000018h             ; QUIC param level
000000014008F93F  mov     rax, cs:MsQuic
000000014008F946  xor     r9d, r9d                  ; r9 = NULL buffer (always)
000000014008F949  mov     rcx, [r15]                ; connection handle
000000014008F94C  mov     edx, ebp
000000014008F953  mov     rax, [rax+20h]            ; GetParam
000000014008F957  call    _guard_dispatch_icall     ; GetParam(handle, 0x5000018, &stacksize, NULL)
000000014008F95F  cmp     eax, 0C0000023h           ; STATUS_BUFFER_TOO_SMALL?
000000014008F964  jnz     loc_14008FA6D
000000014008F96A  mov     edx, [rsp+78h+arg_0]      ; size from stack local
000000014008F971  mov     r12d, 75516D53h           ; pool tag 'SmQu'
000000014008F97A  mov     ecx, 42h
000000014008F97F  call    cs:__imp_ExAllocatePool2  ; fresh allocation
; ...
000000014008F9D1  mov     rax, cs:MsQuic
000000014008F9D8  lea     r8, [rsp+78h+arg_0]
000000014008F9E3  mov     r9, rsi                   ; r9 = fresh buffer
000000014008F9EC  call    _guard_dispatch_icall     ; retry GetParam with fresh buffer
; ...
000000014008FA4C  mov     [rbx+0E0h], rsi           ; store buffer *after* success
000000014008FA5A  mov     [rbx+0E8h], eax           ; store size

Diff takeaway: The unpatched default branch loads the cached buffer pointer from blk+0xE0 and hands it to MsQuic on the first call; the patched code always uses r9 = NULL with a zeroed stack-local size, allocates a fresh buffer, and stores the buffer/size only after the retry succeeds. The Feature_3679766840 A/B gate is removed entirely (the function itself is no longer present in the patched binary).


5. Reachability

SmbQuicInitiateAsynchronousConnect is reached when a user-mode operation resolves a UNC path bound to SMB-over-QUIC transport, routed through RDBSS/mrxsmb.sys connection establishment (RxCeInitiateConnectRequest → per-transport callout → SmbQuicInitiateAsynchronousConnect). The connection block is freshly built and zero-allocated per connect callout (RxCeBuildConnectionOverMultipleTransports / RxCepBuildConnectCalloutsForTdiSmbMmAllocateServerTransport).

The parameter data that MsQuicConnectionGetParam(level=0x5000018) returns is influenced by the QUIC peer (the SMB server). In both builds the buffer that receives this data is a correctly sized 'SmQu' allocation obtained from the size MsQuic reports, so the write length matches the allocation. No attacker-controlled write-primitive was identified in either build.


6. Exploit Assessment

No exploit primitive is demonstrable. The output buffer handed to MsQuicConnectionGetParam is, in both builds, a heap allocation sized to the value MsQuic returns for parameter 0x5000018; the write stays within that allocation. In the unpatched default path the first call may pass the block's cached blk+0xE0 buffer, but that field is either 0 (fresh, zero-initialized block) or a valid buffer whose recorded size matches, and it is nulled whenever the buffer is freed. There is no reachable stale/dangling/uninitialized pointer, no attacker-controlled length/target mismatch, and therefore no pool-overflow or use-after-free primitive to develop.


7. Debugger Notes

For observing the change on the unpatched target:

bp mrxsmb!SmbQuicInitiateAsynchronousConnect            ; entry; blk = *(rcx+0x40)
bp mrxsmb!SmbQuicInitiateAsynchronousConnect+0x940      ; 0x14008FA50 - feature-OFF: mov r9,[rbx+0xE0]
bp mrxsmb!SmbQuicInitiateAsynchronousConnect+0x951      ; 0x14008FA61 - GetParam (feature-OFF path)
bp mrxsmb!SmbQuicInitiateAsynchronousConnect+0x959      ; 0x14008FA69 - STATUS_BUFFER_TOO_SMALL compare

What to inspect: - At 0x14008FA50, dq @rbx+0xE0 l1 and dd @rbx+0xE8 l1. On a fresh connect these are 0/0, so the call at 0x14008FA61 receives a NULL buffer and returns STATUS_BUFFER_TOO_SMALL without writing. - Register contract at the 0x14008FA61 indirect call: rcx = connection handle (*(blk+0x70)), edx = 0x5000018, r8 = &blk[0xE8] (size in/out), r9 = cached buffer pointer. - On the patched target the equivalent site (0x14008F957) always shows r9 = 0 and r8 = &[rsp+...] (a stack local); there is no load of a cached buffer before the first call.

Struct/offset notes for SmbQuicInitiateAsynchronousConnect: - Connection block blk = *(a1+0x40) — pool tag 'SmCe' (0x65436D53), allocated by SmbMmAllocateServerTransport via ExAllocatePool2(0x40, 664, ...) (zero-initialized). - blk+0x70 — MsQuic connection handle. - blk+0xE0 — cached output buffer pointer for MsQuicConnectionGetParam (pool tag 'SmQu', 0x75516D53). - blk+0xE8 — output size (ULONG) for the same call. - MsQuic dispatch table +0x20 slot — MsQuicConnectionGetParam; parameter level 0x5000018.


8. Changed Functions — Triage

Function Type Note
SmbQuicInitiateAsynchronousConnect behavioural / hardening Removes the Feature_3679766840 gate; replaces the cached-buffer-reuse path with an unconditional query-first (NULL buffer, stack-local size) call. Not a demonstrable vulnerability (see §2).
SmbQuicDisconnectEvent behavioural Removes Feature_3679766840 gating of ETW/WPP tracing; WPP GUID refresh (WPP_29dfa564…WPP_204de8b894eb30a604d55fc295d80353). Not security-relevant.
SmbQuicConnEventPeerCertificateReceived behavioural Feature-gate removal / ETW tracing unification; WPP GUID refresh. Not security-relevant.
SmbQuicAsynchronousConnectCompletion behavioural Feature-gate removal / ETW tracing unification. Not security-relevant.
RxCeCompleteConnectRequest behavioural Feature-gate removal / ETW tracing unification; rundown-protection release accounting simplified alongside the gate removal. Signature-type inference differences are decompiler artifacts. Not security-relevant.
DriverEntry cosmetic WPP GUID updates; minor address shifts. Not security-relevant.

Independent whole-binary diff. Comparing the two builds function-by-function (matching by content across relocations) shows the behavioural changes are confined to the SMB-over-QUIC cluster above, all driven by removing two feature-staging gates (Feature_3679766840… and Feature_Servicing_EnableSMBHardeningTelemetry…) plus WPP GUID refreshes. The gate functions themselves and their WIL feature-staging infrastructure (wil_details_*, wil_InitializeFeatureStaging) are removed/updated as a consequence. Telemetry helpers (EEL_*, EdpEnforcementLog_SmbAccessDenied) and WPP SOCKADDR trace helpers changed as part of the same servicing/telemetry churn. Every other function that differs textually (SmbWskReceiveEvent, VctIndReceive, VctIndDataReady, MRxSmbDevFcbXXXControlFile, SeEncodeSamClaimsBlob/SeEncodeSamClaimsSet, SmbMmGetMaxBufferLength, SmbCeCompleteVNetRootContextInitialization, RxSmbdGetSMBDirectServicePath, MRxSmbGetStatistics, WitnessUpcall*) differs only by relocated global-data symbol addresses (e.g. dword_1400666F8dword_1400666E8) or statistics-counter layout — identical logic, no behavioural change. No security-relevant change was omitted from this report.


9. Unmatched Functions

  • Added: none.
  • Removed (relative to the QUIC cluster): the Feature_3679766840… and Feature_Servicing_EnableSMBHardeningTelemetry… staging-gate helpers are no longer present. No sanitizer or bounds check was removed; no new mitigation function was added.

10. Confidence

Confidence: High that the described change exists as decoded above, and high that it is a defensive/hardening refactor rather than a fix for a demonstrable memory-safety flaw. The connection block is zero-initialized, the blk+0xE0/blk+0xE8 buffer/size pair is kept consistent, and every free of the buffer nulls the field, so no stale/dangling/uninitialized pointer reaches the MsQuicConnectionGetParam call in the unpatched build. The remaining changed functions are feature-staging/telemetry/WPP servicing churn and data-symbol relocations with no security impact.