mup.sys — WIL feature-staging gate removed from UNC hardening prefix-table packing
KB5074109
1. Overview
- Unpatched Binary:
mup_unpatched.sys - Patched Binary:
mup_patched.sys - Overall Similarity Score: 0.9757
- Diff Statistics: The single behaviorally interesting change is in
MupiPackUncHardeningPrefixTableForFsctl. In addition, a cluster of WIL feature-staging / usage-reporting helper functions present in the unpatched build is absent from the patched build (dead-code elimination once the gate below was removed). The tool's headline "1 changed, 0 unmatched" therefore undercounts the removed library functions. - Verdict: No security-relevant change. The patch removes a WIL feature-staging gate (
Feature_447750459__private_IsEnabledDeviceUsageNoInline) that selected between two nearly-identical code paths inside the UNC hardening prefix-table packing routine, hardwiring the formerly feature-enabled path and eliminating the now-dead feature machinery. This is staged-feature/library churn, not a fix. The gate is a cached in-memory read, so the earlier framing as "blocking I/O inside a held push lock" does not hold.
2. Change Summary
- Severity: None (no security-relevant change)
- Nature: WIL feature-staging gate removal + dead-code elimination of the associated feature usage-reporting helpers.
- Affected Function:
MupiPackUncHardeningPrefixTableForFsctl(0x1C001657Cunpatched,0x14001557Cpatched)
What actually changed:
The unpatched MupiPackUncHardeningPrefixTableForFsctl calls Feature_447750459__private_IsEnabledDeviceUsageNoInline (0x1C0003CC4) once per prefix-table entry inside the enumeration loop. The return value selects between an "if" path (feature reported disabled) and an "else" path (feature reported enabled) that compute the same 16-byte-aligned output offset. The patched build deletes the call and keeps only the formerly feature-enabled path, and restructures the loop so the get-next call sits at the top of the loop with a restart flag instead of a get-first-before-loop / get-next-at-bottom shape. The two shapes are semantically equivalent.
Because that call site was the only consumer of the Feature_447750459 "DeviceUsage" feature, the patched build no longer contains the feature accessor, its fallback, or the WIL usage-reporting helpers it pulled in (see Section 8).
Why the earlier "blocking I/O in lock" framing is wrong:
Feature_447750459__private_IsEnabledDeviceUsageNoInline is a compiler-generated WIL feature-staging accessor. Its steady-state path is a plain read of the cached in-memory global Feature_447750459__private_featureState, a bit test, and a return — no lock, no WNF state query, no registry access, no I/O. There is no blocking operation performed while the push lock is held, so the claimed denial-of-service / deadlock condition does not exist.
3. Call Chain
The enumeration path itself is reachable, and is described here only to show the gate's context:
MupFsControl(0x1C001A0D0) — FSCTL dispatch; compares the control code against0x100038at0x1C001A0F8.MupFsctlGetUncHardeningPrefixTable(0x1C00154BC) — prepares the output buffer and calls the packer at0x1C00154F8.MupiPackUncHardeningPrefixTableForFsctl(0x1C001657C) — acquiresExAcquirePushLockExclusiveExonqword_1C000B218, enumerates prefix-table entries viaMupiGetFirstOrNextUncHardeningPrefixEntry, and (unpatched only) calls the feature gate per iteration.Feature_447750459__private_IsEnabledDeviceUsageNoInline(0x1C0003CC4) — cached feature-state read (removed in the patched build).
4. The feature gate is a cached read, not I/O
; ---- Feature_447750459__private_IsEnabledDeviceUsageNoInline @ 0x1C0003CC4 (unpatched) ----
00000001C0003CC4 sub rsp, 28h
00000001C0003CC8 and [rsp+28h+arg_0], 0
00000001C0003CCE mov eax, cs:Feature_447750459__private_featureState ; cached in-memory global
00000001C0003CD4 mov dword ptr [rsp+28h+arg_0], eax
00000001C0003CD8 test al, 10h ; bit 0x10 = "already cached"
00000001C0003CDA jz short loc_1C0003CE1
00000001C0003CDC and eax, 1 ; cached: return enabled bit
00000001C0003CDF jmp short loc_1C0003CF0
00000001C0003CE1 mov rcx, [rsp+28h+arg_0]
00000001C0003CE6 mov edx, 3
00000001C0003CEB call Feature_447750459__private_IsEnabledFallback ; one-time init only
00000001C0003CF0 add rsp, 28h
00000001C0003CF4 retn
The common path never leaves this function's stack frame. The Fallback branch runs at most once to populate the cache; it is not a per-iteration blocking operation and it acquires no push lock that the caller holds.
5. Assembly Analysis of the changed function
Unpatched loop head with the gate:
; ---- MupiPackUncHardeningPrefixTableForFsctl @ 0x1C001657C (unpatched) ----
00000001C00165CA call cs:__imp_ExAcquirePushLockExclusiveEx ; acquire on qword_1C000B218
00000001C00165D6 mov dl, 1
00000001C00165D8 lea rcx, stru_1C000B220
00000001C00165DF call MupiGetFirstOrNextUncHardeningPrefixEntry ; get first (before loop)
00000001C00165E4 mov r15, rax
00000001C00165E7 test rax, rax
00000001C00165EA jz loc_1C00166D6
00000001C00165F0 lea ebx, [rdi+0Fh] ; offset + 15 (16-byte align)
00000001C00165F3 call Feature_447750459__private_IsEnabledDeviceUsageNoInline ; the gate (removed)
00000001C00165F8 test eax, eax
00000001C00165FA jnz short loc_1C001662E ; enabled -> "else" path
; "if" (disabled) path: overflow -> edi=0xFFFFFFFF then STATUS_BUFFER_TOO_SMALL
00000001C00165FC cmp ebx, edi
00000001C00165FE jb loc_1C00166C2 ; loc_..C2: mov edi,0FFFFFFFFh
; "else" (enabled) path: overflow -> STATUS_BUFFER_TOO_SMALL, edi untouched
00000001C001662E cmp ebx, edi
00000001C0016630 jb loc_1C00166CC ; loc_..CC: mov ebx,0C0000095h
Patched loop with the gate deleted and get-next hoisted to the top:
; ---- MupiPackUncHardeningPrefixTableForFsctl @ 0x14001557C (patched) ----
00000001400155C6 call cs:__imp_ExAcquirePushLockExclusiveEx ; acquire on qword_14000A1F8
00000001400155D2 mov dl, 1
00000001400155D4 lea rcx, stru_14000A200
00000001400155DB call MupiGetFirstOrNextUncHardeningPrefixEntry ; get-next at loop top
00000001400155E0 mov rcx, rax
00000001400155E3 test rax, rax
00000001400155E6 jz loc_14001568F
00000001400155EC lea eax, [rdi+0Fh] ; no feature call
00000001400155EF cmp eax, edi
00000001400155F1 jb loc_14001568A ; overflow -> STATUS_BUFFER_TOO_SMALL
00000001400155F7 and eax, 0FFFFFFF0h
...
0000000140015674 jmp loc_1400155DB ; restart flag now dl=0
The patched code is the unpatched "else" (feature-enabled) path with the branch and duplicate arm removed.
6. The only behavioral difference, and why it is not reachable
The "if" (feature-disabled) and "else" (feature-enabled) paths differ in exactly one place: when the 32-bit expression offset + 0xF wraps (i.e. offset is within 15 of 0xFFFFFFFF), the disabled path additionally writes *OutputSize = 0xFFFFFFFF (mov edi, 0FFFFFFFFh at 0x1C00166C2) before returning STATUS_BUFFER_TOO_SMALL, whereas the enabled/patched path leaves *OutputSize unchanged and returns the same status. Both return an error; neither reads or writes out of bounds.
Reaching that branch requires the accumulated output offset (rdi, a running sum of per-entry NextEntryOffset values) to approach 4 GB, which the 32-bit output buffer size (arg2, r12d) and the entry copy logic never allow. It is dead defensive code, not an attacker-reachable primitive. Aligning both paths is a cosmetic cleanup, not a security-relevant strictness change, and it is not a regression in either direction.
7. Trigger Conditions
The enumeration is reached from user mode by issuing FSCTL 0x100038 (via NtFsControlFile / DeviceIoControl) against the MUP device, which routes through MupFsControl to MupFsctlGetUncHardeningPrefixTable and then the packer. Doing so exercises the changed function but produces no security-relevant difference between the two builds: the gate it removes is a cached read, and the aligned-offset result is identical for any input a caller can supply.
8. Changed / Removed Functions — Full Triage
MupiPackUncHardeningPrefixTableForFsctl(0x1C001657C->0x14001557C, Change Type: WIL feature-staging churn)- Removed the per-iteration call to
Feature_447750459__private_IsEnabledDeviceUsageNoInline. - Collapsed the two feature-selected arms into the single formerly-enabled path.
- Restructured the enumeration loop from get-first-then-get-next-at-bottom to get-next-at-top with a restart flag.
-
Register allocation: the output-size pointer (
arg4) moved fromrsi(spilled and reloaded from stack at0x1C00166D1in the unpatched build) to the non-volatiler13. This is an optimizer artifact of removing the branch, not a fix. -
Removed in the patched build (dead-code elimination, present only in unpatched):
Feature_447750459__private_IsEnabledDeviceUsageNoInline(0x1C0003CC4),Feature_447750459__private_IsEnabledFallback(0x1C0003CFC),wil_details_FeatureReporting_IncrementOpportunityInCache(0x1C000414C),wil_details_FeatureReporting_IncrementUsageInCache(0x1C0004224),wil_details_FeatureReporting_RecordUsageInCache(0x1C0004308),wil_details_FeatureReporting_ReportUsageToService(0x1C0004488),wil_details_FeatureReporting_ReportUsageToServiceDirect(0x1C0004504),wil_details_FeatureStateCache_ReevaluateCachedFeatureEnabledState(0x1C00045F0),wil_details_FeatureStateCache_TryEnableDeviceUsageFastPath(0x1C0004700),wil_details_GetCurrentFeatureEnabledState(0x1C0004754),wil_details_IsEnabledFallback(0x1C00048B4),wil_details_MapReportingKind(0x1C0004944). These are WIL feature-staging / usage-reporting helpers that became unreachable once the single gate consumer was removed. None are security-relevant.
9. Independent Diff Note
The report's tool summary states "1 changed, 0 unmatched." An independent content-matched comparison shows one changed function plus the ~12 removed WIL helpers listed above. No changed function outside this WIL feature-staging cluster was found, and none of the removed helpers implements a security check; their removal is a consequence of compiling out the feature gate, not an omitted fix.
10. Confidence & Caveats
- Confidence Level: High. The removed call target is named
Feature_447750459__private_IsEnabledDeviceUsageNoInlineand reads a cached feature-state global; the loop bodies match on both sides except for the flighting branch and the unreachable overflow-guard detail; the removed helpers are the WIL feature-staging support functions for that feature. - Result: No security-relevant change. The push lock is real and is held across the enumeration, but nothing blocking runs inside it in either build. There is no CWE-667 / CWE-400 condition, no reachable memory-safety issue, and no direction in which the patched build is meaningfully stricter.