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.
When running in packaged context, temp path initialization appears to reapply a protected inheritable ACL on
%TEMP%\WinGetevery 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:
%TEMP%\WinGet%TEMP%\WinGet\defaultStateObserved 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:
QueryDirectoryQueryBasicInformationFileQueryStandardInformationFileQueryNameInformationFileOwner, DACL, DACL ProtectedNO MORE FILESWhat stands out is scale, not sequence.
Relevant Code Paths
Temp path retrieval goes through
src/AppInstallerSharedLib/Public/winget/Filesystem.h, which always callsInitializeAndGetPathTo(...).InitializeAndGetPathTo(...)insrc/AppInstallerSharedLib/Filesystem.cppcreates the directory and then unconditionally callsApplyACL()when ACL metadata exists.ApplyACL()builds explicit ACEs with inheritable flags and applies a protected DACL viaSetNamedSecurityInfoW.Packaged vs unpackaged temp roots are selected in
src/AppInstallerCommonCore/Runtime.cpp:%TEMP%\WinGet%TEMP%\WinGet\defaultStateCache 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
SetNamedSecurityInfoWcall than with winget logically enumerating all cached manifests/packages.Measured Tree Sizes
Current tree sizes on the test machine:
The directory count ratio is about
79.8x.Additional subtree counts gathered during investigation:
Independent Reproduction Outside winget
To isolate the effect from winget logic, I copied
%TEMP%\WinGetto%TEMP%\WinGet2and used a PowerShell P/Invoke repro that mirrors the packaged temp ACL call shape:SET_ACCESSOBJECT_INHERIT_ACE | CONTAINER_INHERIT_ACEOWNER_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION | PROTECTED_DACL_SECURITY_INFORMATIONMeasured results:
Comparison:
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
SetNamedSecurityInfoWcall on%TEMP%\WinGetis 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:As a result, resolving
PathName::Tempin packaged mode can repeatedly trigger expensive recursive security propagation over%TEMP%\WinGet.Possible Fix Directions
SetNamedSecurityInfoWwhen owner/DACL/protection already match the desired state.%TEMP%\WinGetroot when a smaller subtree would suffice.Why This Matters
The issue scales with cache size. On machines with large
%TEMP%\WinGettrees, 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.