mrxsmb.sys — reparse-tag classification tweak and SMB-over-QUIC teardown feature-staging
KB5075941
1. Overview
| Field | Value |
|---|---|
| Unpatched Binary | mrxsmb_unpatched.sys |
| Patched Binary | mrxsmb_patched.sys |
| Overall Similarity | 96.05% |
| Matched Functions | 1094 |
| Changed Functions | 59 |
| Identical Functions | 1035 |
| Unmatched (Unpatched) | 0 |
| Unmatched (Patched) | 0 |
Verdict: No delivered security fix was found in this diff. The two changes that were initially suspected as security-relevant do not survive verification against the two builds:
- In
FsRtlValidateReparsePointBufferExthe only logic change is the removal of a single tag-acceptance predicate term. It does not touch the symbolic-link / mount-point offset-and-length validation at all (that code is byte-identical in both builds), and it applies only to a class of reserved-bit reparse tags that are never subjected to a field walk. The net effect is a reparse-tag classification change, not a bounds fix, and if anything it is a relaxation (more tags accepted as valid). - In
SmbQuicDisconnectConnectionthe patched build adds a new push-lock-guarded teardown-notification callback. There is no pre-existing unlocked callback in the unpatched build for it to protect; the lock and the callback are introduced together as new SMB-over-QUIC teardown code (feature-staging), not a fix to an existing race.
The remainder of the diff is servicing/feature churn: SMB-over-QUIC feature code (including wholesale removal of the QUIC certificate-mapping / server-suffix feature), WIL feature-staging helpers (wil_details_*, Feature_*), WPP/ETW tracing GUID and event-id changes, and struct-offset relocations. None of it adds or tightens a bounds, length, overflow, type, or lock check on SMB-server-supplied data.
2. Vulnerability Summary
Finding #1 — Reparse-tag acceptance predicate change (No security-relevant change)
- Severity: None (downgraded from High)
- Vulnerability Class: None demonstrable. Classified as a behavioral change to reparse-tag acceptance.
- Affected Function:
FsRtlValidateReparsePointBufferEx(unpatched @0x1C0034840, patched @0x1C0032620)
What actually changed:
FsRtlValidateReparsePointBufferEx validates REPARSE_DATA_BUFFER structures. Early in the function a predicate decides whether a reparse tag is eligible for the tag-specific validation block (if it is not eligible, the function returns STATUS_IO_REPARSE_TAG_NOT_HANDLED, 0xC0000276). The unpatched predicate is:
(tag & 0xFFF0000) == 0
&& (unsigned)tag > 2
&& (tag & 0xC0000000) != 0x40000000 // <-- present only in the unpatched build
&& (tag & 0x30000000) != 0x30000000
&& tag != 0xA0000019
The patched build removes the single term (tag & 0xC0000000) != 0x40000000. Everything else in the function — including the entire symbolic-link and mount-point offset/length accumulation using RtlULongAdd and the associated bounds comparisons — is identical between the two builds (the only other deltas are WPP tracing GUID names).
Why this is not the claimed OOB fix:
- The removed term only distinguishes tags where bit 31 is clear and bit 30 is set (the
0x40000000reserved-bit pattern).IO_REPARSE_TAG_SYMLINK(0xA000000C) andIO_REPARSE_TAG_MOUNT_POINT(0xA0000003) both have bit 31 set, sotag & 0xC0000000 == 0x80000000for them — the removed term never excluded symlink or mount-point tags in either build. The symlink/mount-point validation path is therefore unaffected. - A reparse tag matching the
0x40000000pattern is not a symbolic link or mount point, so inside the block it reaches neither field-walk handler. It falls through toreturn STATUS_REPARSE (0x80000005)without any offset/length arithmetic or memory access. - Direction: removing a term from the accepting
&&chain makes the predicate true for more tags, so the patched build treats these reserved-bit tags as a valid generic reparse point instead of returning "tag not handled". That is a relaxation of classification, not a tightening, and it involves no reachable out-of-bounds read or write.
There is no demonstrable memory-safety primitive here in either build.
Finding #2 — Added push-lock-guarded QUIC teardown callback (No security-relevant change)
- Severity: None (downgraded from Medium)
- Vulnerability Class: None demonstrable. Classified as SMB-over-QUIC teardown feature code added in the patched build.
- Affected Function:
SmbQuicDisconnectConnection(unpatched @0x1C0046A74, patched @0x1C0046464)
What actually changed:
The unpatched SmbQuicDisconnectConnection only emits WPP trace events and, guarded by a NULL check on *(a1+0x70), calls the MsQuic connection-shutdown vtable slot (MsQuic + 0x88) through the CFG guard dispatcher. It contains no push lock and no callback-object dereference.
The patched build adds a new block after the shutdown call:
ExAcquirePushLockExclusiveEx(a1 + 0xA8, 0);
if ( *(_DWORD *)(a1 + 0xB4) != 0 ) {
v2 = *(_DWORD ***)(a1 + 0xB8);
v3 = *(unsigned int *)(a1 + 0xB0);
(*v2)[45] = 0; // clear a field in the target object
((...)v2[3])(v2, 0xC000020C, v3); // invoke slot 3 with STATUS_CONNECTION_DISCONNECTED
}
ExReleasePushLockExclusiveEx(a1 + 0xA8, 0);
Why this is not the claimed UAF fix:
- The push lock (
ExAcquirePushLockExclusiveEx/ExReleasePushLockExclusiveEx) and the callback invocation are introduced together as new code. The unpatched build has no callback dereference ata1+0xB8and no vtable-slot call at this site, so there is no pre-existing unlocked callback for the lock to protect. - The status value passed to the new callback is
0xC000020C(STATUS_CONNECTION_DISCONNECTED), consistent with new teardown-notification logic being staged for the SMB-over-QUIC transport, not a retrofit of synchronization onto an existing race.
This is added feature/teardown code, not a fix to a demonstrable use-after-free in the unpatched build.
3. Code Diff
Finding #1 — FsRtlValidateReparsePointBufferEx
// UNPATCHED @ 0x1C0034840 and PATCHED @ 0x1C0032620
// The tag-acceptance predicate differs by exactly one term:
// UNPATCHED:
if ( (a2->ReparseTag & 0xFFF0000) == 0
&& (unsigned int)ReparseTag > 2
&& (ReparseTag & 0xC0000000) != 0x40000000 // removed in patched
&& (ReparseTag & 0x30000000) != 0x30000000
&& ReparseTag != -1610612711 ) // 0xA0000019
{ ... }
// PATCHED:
if ( (a2->ReparseTag & 0xFFF0000) == 0
&& (unsigned int)ReparseTag > 2
&& (ReparseTag & 0x30000000) != 0x30000000
&& ReparseTag != -1610612711 )
{ ... }
// The symbolic-link (0xA000000C) and mount-point (0xA0000003) handlers inside
// the block, including every RtlULongAdd offset/length accumulation and bounds
// comparison, are IDENTICAL in both builds.
Finding #2 — SmbQuicDisconnectConnection
// UNPATCHED @ 0x1C0046A74 (WPP trace omitted):
if ( *(_QWORD *)(a1 + 0x70) != 0 )
(*(void (__fastcall **)(_QWORD, __int64))(MsQuic + 0x88))(*(_QWORD *)(a1 + 0x70), 1);
return 0;
// No push lock, no callback dereference at a1+0xB8.
// PATCHED @ 0x1C0046464 (WPP trace omitted):
(*(void (__fastcall **)(_QWORD, __int64))(MsQuic + 0x88))(*(_QWORD *)(a1 + 0x70), 1);
ExAcquirePushLockExclusiveEx(a1 + 0xA8, 0); // ADDED (new code)
if ( *(_DWORD *)(a1 + 0xB4) != 0 ) { // ADDED
v2 = *(_DWORD ***)(a1 + 0xB8);
v3 = *(unsigned int *)(a1 + 0xB0);
(*v2)[45] = 0;
((void (__fastcall *)(_DWORD **, __int64, __int64))v2[3])(v2, 0xC000020C, v3);
}
ExReleasePushLockExclusiveEx(a1 + 0xA8, 0); // ADDED
return 0;
4. Assembly Analysis
Finding #1 — FsRtlValidateReparsePointBufferEx
The removed predicate term is present in the unpatched disassembly and absent from the patched disassembly:
; UNPATCHED mrxsmb_unpatched.sys : FsRtlValidateReparsePointBufferEx @ 0x1C0034840
00000001C0034919 and eax, 0C0000000h ; tag & 0xC0000000
00000001C003491E cmp eax, 40000000h ; == 0x40000000 ? (removed in patched)
00000001C0034987 cmp ecx, 0A000000Ch ; IO_REPARSE_TAG_SYMLINK
00000001C0034CB5 cmp ecx, 0A0000003h ; IO_REPARSE_TAG_MOUNT_POINT
; PATCHED mrxsmb_patched.sys : FsRtlValidateReparsePointBufferEx @ 0x1C0032620
; No 'and eax,0C0000000h / cmp eax,40000000h' pair exists in the function.
00000001C0032755 cmp ecx, 0A000000Ch ; IO_REPARSE_TAG_SYMLINK
00000001C0032A83 cmp ecx, 0A0000003h ; IO_REPARSE_TAG_MOUNT_POINT
The 0x40000000 comparison appears exactly once in the unpatched function and zero times in the patched function; the symlink and mount-point tag comparisons appear in both. This confirms the change is limited to the tag-acceptance predicate and does not alter the symlink/mount-point field validation.
Finding #2 — SmbQuicDisconnectConnection
The unpatched function contains no push-lock calls; its only guarded indirect call is the MsQuic shutdown vtable dispatch:
; UNPATCHED mrxsmb_unpatched.sys : SmbQuicDisconnectConnection @ 0x1C0046A74
00000001C0046AB1 call WPP_SF_
00000001C0046AE8 call WPP_SF_q
00000001C0046B06 call cs:__guard_dispatch_icall_fptr ; MsQuic shutdown vtable call
00000001C0046B35 call WPP_SF_
; No ExAcquirePushLockExclusiveEx / ExReleasePushLockExclusiveEx anywhere.
The patched function introduces the push lock and the new status-carrying callback:
; PATCHED mrxsmb_patched.sys : SmbQuicDisconnectConnection @ 0x1C0046464
00000001C0046508 call cs:__imp_ExAcquirePushLockExclusiveEx
00000001C0046524 mov edx, 0C000020Ch ; STATUS_CONNECTION_DISCONNECTED
00000001C0046549 call cs:__imp_ExReleasePushLockExclusiveEx
Both push-lock calls exist only in the patched build (count: unpatched 0, patched 2), confirming the lock and the callback block are added together as new code rather than wrapped around pre-existing logic.
5. Trigger Conditions
No reachable trigger exists for a memory-safety issue in either finding.
- Finding #1: the changed predicate only reclassifies certain reserved-bit reparse tags (bit 31 clear, bit 30 set) between
STATUS_IO_REPARSE_TAG_NOT_HANDLEDandSTATUS_REPARSE. These tags reach no field-walk handler and no offset/length arithmetic, so no craftedREPARSE_DATA_BUFFERproduces an out-of-bounds access via this change. The symlink/mount-point validation that a malicious server could target is identical in both builds. - Finding #2: the added block is new teardown-notification code guarded by a newly added push lock; there is no unlocked-callback window in the unpatched build to race.
6. Exploit Primitive & Development Notes
No exploit primitive is supported by the binaries. Neither change introduces or removes a reachable out-of-bounds read/write, integer-overflow-to-undersized-allocation, or use-after-free on SMB-server-controlled data. There is no basis for a kernel read/write primitive, KASLR defeat, pool-grooming strategy, or privilege-escalation chain, so none is described.
7. Debugger Playbook
Because no security-relevant primitive is present, there is no proof-of-concept to reproduce. The relevant address anchors for confirming the actual (non-security) changes are:
- Finding #1:
mrxsmb!FsRtlValidateReparsePointBufferEx— unpatched0x1C0034919/0x1C003491E(and eax,0C0000000h/cmp eax,40000000h, absent in the patched build at0x1C0032620). - Finding #2:
mrxsmb!SmbQuicDisconnectConnection— patched0x1C0046508(ExAcquirePushLockExclusiveEx),0x1C0046524(mov edx,0C000020Ch),0x1C0046549(ExReleasePushLockExclusiveEx); none of these exist in the unpatched function at0x1C0046A74.
8. Changed Functions — Full Triage
Security-Relevant
None. No changed function adds or tightens a bounds, length, overflow, type, or lock check on SMB-server-supplied data.
Behavioral / Cosmetic (representative)
| Function | Similarity | Note |
|---|---|---|
FsRtlValidateReparsePointBufferEx |
0.987 | Removed one tag-acceptance predicate term ((tag & 0xC0000000) != 0x40000000). Reparse-tag classification change; symlink/mount-point validation identical. Not security-relevant. |
SmbQuicDisconnectConnection |
0.690 | Added a push-lock-guarded teardown-notification callback (new SMB-over-QUIC teardown code). No pre-existing unlocked callback existed. Not a UAF fix. |
FsRtlValidateChangeNotifyBuffer |
— | The standalone FsRtlValidateChangeNotifyBufferEx/FsRtlValidateRelativeFileName helpers were inlined into a single validator; the patched inlined form performs no more validation than the unpatched pair. Shared OS library refactor, not an mrxsmb attacker-data tightening. |
SmbCompressionDecompress |
— | Refactor: the unpatched server-claimed-size check *a2 > a5 is removed, but the patched call passes the true output buffer size a5 to RtlDecompressBufferEx2 directly, so the decompress write remains bounded by a5 in both builds. Equivalent safety, not a tightening. |
sub_1C0021F50 |
0.329 | Code reordering artifact (request-sending vs notification dispatch). Not security-relevant. |
sub_1C006AF70 |
0.786 | Connection setup; added __security_cookie. Standard hardening; no logical change. |
sub_1C00147B0 |
0.719 | Reconnection logic; struct offset shifts. Refactoring only. |
sub_1C0048E24 |
0.852 | IOCTL buffer alloc; overflow-checked size math replaced with direct arithmetic on values capped upstream. Not a regression. |
sub_1C00204E8 |
0.818 | Capability-negotiation bitmask constant changes. Protocol bookkeeping. |
sub_1C001651C |
0.634 | Session state processing; inlined status-code lookup. Equivalent behavior. |
sub_1C0010520 |
0.909 | Connection establishment; offset shifts and added negotiation gate. Not security-relevant. |
sub_1C0016824 |
0.828 | Status update with telemetry; ETW offsets changed. |
sub_1C00151F4 |
0.901 | SMB response magic checks; offset shifts only. |
sub_1C0011E60 |
0.821 | Multi-connection dispatch; struct offset shifts. |
sub_1C002009C |
0.882 | Capability flag interlocked ops; arg removed; logic simplified. |
sub_1C000E09C |
0.905 | Object cleanup; rundown-protection wait reordered; memset size changed. Teardown-ordering refactor. |
SmbCryptoAllocateAndPopulateCipherSuiteOrder |
0.643 | Cipher-suite allocation; same size formula, cleaner error path. |
sub_1C007E078 |
0.360 | Feature-configuration init; __security_cookie added; WIL feature-staging integration. Servicing churn. |
Servicing / feature-staging cluster: SMB-over-QUIC feature code (including removal of the QUIC certificate-mapping and server-suffix feature — ValidateSmbQuicActionBufferAndGetServerSuffix, SmbQuicGetUnicodeServerSuffix, SmbQuic*Certificate*, SmbQuic*Configuration* no longer present in the patched build), WIL feature-staging helpers (wil_details_*, Feature_*__private_IsEnabled*), WPP/ETW tracing GUID and event-id changes (McTemplate*, WPP_SF_*), and struct-offset relocations account for the remainder of the changed functions. Removing the QUIC cert-mapping feature reduces attack surface but adds no new check, so it is not a delivered security fix.
9. Unmatched Functions
| Direction | Count | Notes |
|---|---|---|
| Removed (unpatched only) | 0 | Per the matched-function accounting. |
| Added (patched only) | 0 | Per the matched-function accounting. |
Note: at the source-symbol level the patched build drops the SMB-over-QUIC certificate-mapping / server-suffix helper functions and gains a small number of WIL feature-staging helpers; these are servicing/feature changes and do not represent a security fix.
10. Confidence & Caveats
Overall Confidence: High that neither change is a security fix.
- Finding #1 was verified by a full body diff of
FsRtlValidateReparsePointBufferExacross both builds: the sole logic delta is the removed(tag & 0xC0000000) != 0x40000000term, confirmed in the disassembly (0x1C0034919/0x1C003491Epresent only in the unpatched build). The symlink/mount-point offset/length validation is identical, so the previously suspected out-of-bounds primitive does not exist. - Finding #2 was verified by reading both function bodies and disassembly: the unpatched
SmbQuicDisconnectConnectionhas no push lock and no callback dereference, so the added push-lock/callback block is new teardown code rather than a retrofit onto a pre-existing race. - An independent function-by-function review of the security-relevant-looking changed functions (validation, parsing of server responses, allocation-size and copy-length computation, receive/decompress/decrypt paths) found no function where the patched build adds or tightens a bounds/length/overflow/type/lock check on attacker-controlled data.