Skip to content

Packaged temp path ACL reapplication on %TEMP%\WinGet causes recursive security traversal and severe slowdown #6162

@mamoreau-devolutions

Description

@mamoreau-devolutions

When running in packaged context, temp path initialization appears to reapply a protected inheritable ACL on %TEMP%\WinGet every time the temp path is resolved. On a large existing cache tree, that single ACL application causes Windows to recursively traverse and reprocess security on a very large subtree, which shows up in ProcMon as a large burst of metadata and security events and adds significant latency.

This does not appear to be caused by a semantic difference in the final ACL between packaged and unpackaged paths. The resulting ACL shape is effectively the same. The cost appears to come from applying it at different roots:

  • packaged temp root: %TEMP%\WinGet
  • unpackaged temp root: %TEMP%\WinGet\defaultState

Observed Behavior

In ProcMon, the fast and slow traces look qualitatively similar. The slow case does not look like a different logical workflow. It looks like the same security-heavy workflow repeated over a much larger subtree.

Common event patterns in both traces:

  • repeated QueryDirectory
  • repeated QueryBasicInformationFile
  • repeated QueryStandardInformationFile
  • repeated QueryNameInformationFile
  • repeated Owner, DACL, DACL Protected
  • repeated NO MORE FILES
  • handles opened with security-write style access consistent with ACL application

What stands out is scale, not sequence.

Relevant Code Paths

Temp path retrieval goes through src/AppInstallerSharedLib/Public/winget/Filesystem.h, which always calls InitializeAndGetPathTo(...).

InitializeAndGetPathTo(...) in src/AppInstallerSharedLib/Filesystem.cpp creates the directory and then unconditionally calls ApplyACL() when ACL metadata exists.

ApplyACL() builds explicit ACEs with inheritable flags and applies a protected DACL via SetNamedSecurityInfoW.

Packaged vs unpackaged temp roots are selected in src/AppInstallerCommonCore/Runtime.cpp:

  • packaged temp root %TEMP%\WinGet
  • unpackaged temp root %TEMP%\WinGet\defaultState

Cache paths resolve through temp in src/AppInstallerCommonCore/FileCache.cpp, so even cache path resolution can trigger temp-root ACL application before any actual file read/write occurs.

Why This Looks Like ACL Propagation and Not Cache Enumeration

The cache code itself is path-based and direct. It does not intentionally walk the whole manifest/package tree just to service a lookup.

The broad ProcMon activity is therefore more consistent with Windows recursively reprocessing inherited security after one root SetNamedSecurityInfoW call than with winget logically enumerating all cached manifests/packages.

Measured Tree Sizes

Current tree sizes on the test machine:

PATH=C:\Users\mamoreau\AppData\Local\Temp\WinGet
EXISTS=True
DIRS=56171
FILES=673

PATH=C:\Users\mamoreau\AppData\Local\Temp\WinGet\defaultState
EXISTS=True
DIRS=704
FILES=310

The directory count ratio is about 79.8x.

Additional subtree counts gathered during investigation:

C:\Users\mamoreau\AppData\Local\Temp\WinGet\cache\V2_M\...\manifests
dirs=31293 files=170

C:\Users\mamoreau\AppData\Local\Temp\WinGet\cache\V2_PVD\...\packages
dirs=24122 files=181

C:\Users\mamoreau\AppData\Local\Temp\WinGet\defaultState\cache\V2_M\...\manifests
dirs=415 files=141

C:\Users\mamoreau\AppData\Local\Temp\WinGet\defaultState\cache\V2_PVD\...\packages
dirs=282 files=141

Independent Reproduction Outside winget

To isolate the effect from winget logic, I copied %TEMP%\WinGet to %TEMP%\WinGet2 and used a PowerShell P/Invoke repro that mirrors the packaged temp ACL call shape:

  • owner = current user
  • explicit ACEs for current user, Builtin Administrators, and Local System
  • SET_ACCESS
  • inheritance = OBJECT_INHERIT_ACE | CONTAINER_INHERIT_ACE
  • security info = OWNER_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION | PROTECTED_DACL_SECURITY_INFORMATION
  • retry without owner if owner already matches current user

Measured results:

TARGET=C:\Users\mamoreau\AppData\Local\Temp\WinGet2
EXISTS=True
EXIT=SUCCESS
ELAPSED_MS=23788.601
OWNER=DEVOLUTIONS\mamoreau
SDDL=O:S-1-5-21-...G:DUD:PAI(A;OICIIO;GA;;;SY)(A;;FA;;;SY)(A;;FA;;;BA)(A;OICIIO;GA;;;BA)(A;OICIIO;GA;;;S-1-5-21-...)(A;;FA;;;S-1-5-21-...)

TARGET=C:\Users\mamoreau\AppData\Local\Temp\WinGet2\defaultState
EXISTS=True
EXIT=SUCCESS
ELAPSED_MS=298.577
OWNER=DEVOLUTIONS\mamoreau
SDDL=O:S-1-5-21-...G:DUD:PAI(A;OICIIO;GA;;;SY)(A;;FA;;;SY)(A;;FA;;;BA)(A;OICIIO;GA;;;BA)(A;OICIIO;GA;;;S-1-5-21-...)(A;;FA;;;S-1-5-21-...)

Comparison:

RootMs:23788.601
DefaultStateMs:298.577
Ratio:79.673

The resulting owner and SDDL were effectively the same. The time delta tracks the subtree size delta very closely.

This strongly suggests the slowdown is caused by applying the same protected inheritable ACL over a much larger subtree, not by a difference in final ACL semantics.

Key Conclusion

A single packaged-style SetNamedSecurityInfoW call on %TEMP%\WinGet is sufficient to reproduce the same class of slowdown outside of winget itself. That makes the issue primarily about ACL application scope and unconditional reapplication, not source search or cache layout logic.

Likely Root Cause

InitializeAndGetPathTo() currently re-applies ACLs whenever a path has ACL metadata. There does not appear to be:

  • a short-circuit that checks whether owner/DACL/protection already match the desired state
  • a process-level memoization that avoids reapplying the ACL repeatedly for the same temp root

As a result, resolving PathName::Temp in packaged mode can repeatedly trigger expensive recursive security propagation over %TEMP%\WinGet.

Possible Fix Directions

  • Avoid applying ACLs unconditionally on every temp path resolution.
  • Add a fast short-circuit to skip SetNamedSecurityInfoW when owner/DACL/protection already match the desired state.
  • Memoize successful ACL application per process for the temp root, if correctness allows it.
  • Narrow ACL application scope so packaged mode does not target the large shared %TEMP%\WinGet root when a smaller subtree would suffice.

Why This Matters

The issue scales with cache size. On machines with large %TEMP%\WinGet trees, a normal packaged operation that merely resolves a temp path can incur large, repeated ACL traversal cost and generate very large ProcMon traces, even when the logical file operation itself is small.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Area-PerformanceIssue related to CPU or memory performanceIssue-FeatureThis is a feature request for the Windows Package Manager client.
    No fields configured for Feature.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions