# cleanmgr.exe Patch Diff — CVE-2025-21420

| | |
|---|---|
| Binary | cleanmgr.exe (Disk Space Cleanup Manager) |
| Pre-patch version | 10.0.26100.1882 |
| Post-patch version | 10.0.26100.3194 |
| KB | KB5051987 |
| CVE | CVE-2025-21420 — Improper Link Resolution Before File Access / Link Following (CWE-59), Elevation of Privilege, CVSS 7.8 |
| Diff tool | ghidriff (Ghidra VersionTrackingDiff engine) |
| Functions changed | 10 with code changes (out of 1,068 total, 95.55% avg similarity) |
| Functions added | 123 (WIL Controlled-Feature-Rollout plumbing for the new flag) |

## Summary

The fix is a single, targeted addition to `WinMainT` (the disk-cleanup entry
routine). The patched build imports `SetProcessMitigationPolicy` — an import
that does **not** exist in the vulnerable 1882 build at all — and calls it
right after COM is initialized, behind a new Controlled-Feature-Rollout flag
(`Feature_3318489400`):

```c
CleanupMgrInfo::m_hInstance = param_1;
bVar2 = wil::details::FeatureImpl<Feature_3318489400>::__private_IsEnabled(...);
if (bVar2) {
    local_1d98 = local_1d98 | 1;
    iVar4 = SetProcessMitigationPolicy(0x10,&local_1d98,4);  // 0x10 = 16 = ProcessRedirectionTrustPolicy
    uVar11 = 0;
    if (iVar4 == 0) goto LAB_1;   // hard-fail: bail out if the policy can't be set
}
```

`0x10` is `ProcessRedirectionTrustPolicy` (value 16 in the
`PROCESS_MITIGATION_POLICY` enum), and setting bit 0 of the policy struct
(`local_1d98 | 1`) enables **Redirection Guard** for the process. Redirection
Guard makes the kernel refuse to follow filesystem junctions/symlinks created
by a lower-privileged user when a higher-privileged process walks a path. If
the call fails, the patched code jumps straight to cleanup and refuses to
continue — the tool will not run without the mitigation active.

That is the whole vulnerability class. `cleanmgr.exe` is launched by the
`SilentCleanup` scheduled task, which runs with highest privileges. As the ZDI
and enigma0x3 write-ups (and moiz-2x's PoC) describe, an unprivileged user can
pre-stage `C:\$Windows.~WS`, `C:\ESD\Windows`, `C:\ESD\Download` with junctions
and drop dummy files, then trigger `SilentCleanup`. The privileged cleanup then
deletes/moves content through the attacker's junction — redirected at
`C:\Config.msi` — turning an arbitrary folder-contents-delete into a full
SYSTEM elevation. Enabling Redirection Guard blocks the junction from being
followed, which severs the primitive.

**Confidence:** high. The added import + gated `SetProcessMitigationPolicy(0x10, …)`
call inside the privileged entry path maps one-to-one onto the CWE-59
"link following" classification and matches the PoC's own root-cause
observation (`MitigationPolicy` set to 16). The `if (iVar4 == 0) goto LAB_1`
hard-fail shows the mitigation is treated as mandatory, not best-effort.

## Functions changed

### WinMainT

| | |
|---|---|
| Address | 140006614 -> 14000955c |
| Change type | code, length, address, called |
| Similarity | 0.91 (b_ratio) |
| Mitigation added | **Yes** — `SetProcessMitigationPolicy(ProcessRedirectionTrustPolicy)`, gated by `Feature_3318489400` |

Before (pre-patch): after `SHCoInitialize()` succeeds, the routine went
straight into command-line parsing with no process-level mitigation:

```c
// before (pre-patch)
local_1d88 = SHCoInitialize();
uVar10 = uVar16;
if (-1 < local_1d88) {
    puVar17 = &local_1d94;
    local_1d98 = 0;
    local_1d94 = 0;
    CleanupMgrInfo::m_hInstance = param_1;
    iVar3 = ParseCommandLine(param_3,&local_1d98,puVar17);
    // ... no SetProcessMitigationPolicy anywhere ...
}
```

After (post-patch): the Redirection Guard opt-in is inserted between
`SHCoInitialize()` and `ParseCommandLine()`, behind the feature flag, and
aborts on failure:

```c
// after (post-patch)
local_1d84 = SHCoInitialize();
uVar11 = uVar16;
if (-1 < local_1d84) {
    CleanupMgrInfo::m_hInstance = param_1;
    bVar2 = wil::details::FeatureImpl<Feature_3318489400>::__private_IsEnabled(...);
    if (bVar2) {
        local_1d98 = local_1d98 | 1;
        iVar4 = SetProcessMitigationPolicy(0x10,&local_1d98,4);
        uVar11 = 0;
        if (iVar4 == 0) goto LAB_1;   // bail if the mitigation cannot be applied
    }
    puVar14 = &local_1d94;
    local_1d90 = 0;
    local_1d94 = 0;
    iVar4 = ParseCommandLine(param_3,&local_1d90,puVar14);
    // ...
}
```

The `called` list confirms it: post-patch `WinMainT` gains
`API-MS-WIN-CORE-PROCESSTHREADS-L1-1-1.DLL::SetProcessMitigationPolicy` and
`wil::details::FeatureImpl<Feature_3318489400>::__private_IsEnabled`; the
pre-patch list has neither.

### Other modified functions

The remaining nine code-changed functions (`CreateStringFromDrive`,
`wil::details::WilFailureNotifyWatchers`, `FailFast_Unexpected`,
`ProcessLocalStorage<…>` destructors, `ReportFailure_Hr<3>`, etc.) are all WIL
result-macro / feature-state machinery pulled in as a side-effect of adding the
first Controlled-Feature-Rollout flag to this binary. They are plumbing, not
the fix.

### New functions (feature-flag plumbing for `Feature_3318489400`)

123 added functions — `FeatureImpl<Feature_3318489400>::GetCachedFeatureEnabledState`,
`GetCurrentFeatureEnabledState`, `__private_IsEnabled`, plus the WIL
`EnabledStateManager` / `FeatureStateManager` / `ProcessLocalStorage` support
classes. Standard WIL CFR accessors introduced because this is the first
feature-flagged code path in `cleanmgr.exe`.

---

<sub>Source: ghidriff diff of cleanmgr-2025-01.exe (10.0.26100.1882, pre-patch)
vs cleanmgr-2025-02.exe (10.0.26100.3194, post-patch) —
[download pre](/data/patch_diffs/binaries/cleanmgr-2025-01.exe) /
[download post](/data/patch_diffs/binaries/cleanmgr-2025-02.exe). Binaries
extracted from WinSxS (not on the public symbol server). PoC and independent
root-cause analysis: https://github.com/moiz-2x/CVE-2025-21420_POC</sub>
