diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt index 20f8466f6b..a81f5862e2 100644 --- a/.github/actions/spell-check/expect.txt +++ b/.github/actions/spell-check/expect.txt @@ -440,6 +440,7 @@ DString DSVG dto DUMMYUNIONNAME +dumpbin dutil DVASPECT DVASPECTINFO @@ -710,6 +711,7 @@ HOOKPROC HORZRES HORZSIZE Hostbackdropbrush +hostfxr hostsfileeditor hotfixes hotkeycontrol @@ -1022,6 +1024,7 @@ Metadatas metafile metapackage mfc +mfcm Mgmt Microwaved middleclickaction diff --git a/Directory.Build.targets b/Directory.Build.targets index 9efab5a9a5..4ba887527a 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -65,4 +65,20 @@ + + + + + <_UnusedVCRuntimeDlls Include="$(OutDir)mfc140*.dll" /> + <_UnusedVCRuntimeDlls Include="$(OutDir)mfcm140*.dll" /> + <_UnusedVCRuntimeDlls Include="$(OutDir)vcamp140*.dll" /> + <_UnusedVCRuntimeDlls Include="$(OutDir)vcomp140*.dll" /> + + + + diff --git a/Directory.Packages.props b/Directory.Packages.props index 0650b78e61..42282b5959 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -110,8 +110,6 @@ - - diff --git a/installer/PowerToysSetupCustomActionsVNext/CustomAction.cpp b/installer/PowerToysSetupCustomActionsVNext/CustomAction.cpp index 890a9fdf6e..4a83582dc2 100644 --- a/installer/PowerToysSetupCustomActionsVNext/CustomAction.cpp +++ b/installer/PowerToysSetupCustomActionsVNext/CustomAction.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include "../../src/common/logger/logger.h" @@ -1807,6 +1808,223 @@ void initSystemLogger() } }); } +// Naming note: the *Hardlinks* names in this CA, the matching WiX CustomAction Ids +// in Product.wxs, and the manifest filename "hardlinks.txt" are kept for continuity +// with the original PR design. The implementation uses fs::copy_file -- not +// CreateHardLinkW -- because hard-links share an inode (and DACL) between root and +// WinUI3Apps, which lets MSIX sparse-package registration propagate a rich DACL onto +// the root copy of files like hostfxr.dll and break LOW-IL prevhost.exe loads, +// turning the Monaco preview pane blank. Copies create a fresh inode in WinUI3Apps so the root +// copy keeps its simple DACL. See the in-body comment for the full RCA reference. +UINT __stdcall CreateWinAppSDKHardlinksCA(MSIHANDLE hInstall) +{ + HRESULT hr = S_OK; + UINT er = ERROR_SUCCESS; + std::wstring installationFolder; + + hr = WcaInitialize(hInstall, "CreateWinAppSDKHardlinks"); + ExitOnFailure(hr, "Failed to initialize"); + hr = getInstallFolder(hInstall, installationFolder); + ExitOnFailure(hr, "Failed to get installFolder."); + + { + namespace fs = std::filesystem; + const fs::path installDir(installationFolder); + const fs::path winui3Dir = installDir / L"WinUI3Apps"; + const fs::path manifestPath = winui3Dir / L"hardlinks.txt"; + + if (!fs::exists(manifestPath)) + { + WcaLog(LOGMSG_STANDARD, "CreateWinAppSDKHardlinks: No hardlinks.txt manifest found, skipping."); + goto LExit; + } + + std::ifstream manifestFile(manifestPath); // Read as bytes, then convert UTF-8 -> wide explicitly. + std::string narrowLine; + int created = 0; + int failed = 0; + + // INSTALLFOLDER from MSI typically arrives with a trailing backslash. lexically_normal + // preserves that as an empty trailing path component, which would later make the + // per-component std::mismatch containment check below reject every legitimate entry. + // Strip any trailing separators before normalizing. + auto stripTrailingSep = [](fs::path p) { + auto s = p.native(); + while (s.size() > 1 && (s.back() == L'\\' || s.back() == L'/')) s.pop_back(); + return fs::path(s); + }; + + // Normalize once so the per-line containment check below is cheap. + const fs::path installDirNorm = stripTrailingSep(installDir).lexically_normal(); + const fs::path winui3DirNorm = stripTrailingSep(winui3Dir).lexically_normal(); + + while (std::getline(manifestFile, narrowLine)) + { + if (narrowLine.empty()) + { + continue; + } + // Strip CR if the manifest uses CRLF line endings. + if (narrowLine.back() == '\r') + { + narrowLine.pop_back(); + if (narrowLine.empty()) continue; + } + + // Manifest is written as UTF-8 (no BOM) -- convert to wide string explicitly + // rather than relying on the locale-default codecvt of std::wifstream, which is + // the ANSI code page on Windows and would silently mangle any non-ASCII path. + const int wideLen = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, narrowLine.c_str(), -1, nullptr, 0); + if (wideLen <= 0) + { + WcaLog(LOGMSG_STANDARD, "CreateWinAppSDKHardlinks: Skipping non-UTF-8 entry: %hs", narrowLine.c_str()); + failed++; + continue; + } + std::wstring fileName(static_cast(wideLen) - 1, L'\0'); + MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, narrowLine.c_str(), -1, fileName.data(), wideLen); + + // Defense-in-depth: reject manifest entries that would escape the install root + // via "..", absolute paths, or alternate stream syntax. lexically_normal collapses + // any "." / ".." / repeated separators, then std::mismatch verifies the resolved + // path is still rooted at installDir / winui3Dir respectively. + const fs::path source = (installDir / fileName).lexically_normal(); + const fs::path target = (winui3Dir / fileName).lexically_normal(); + const auto sourceIn = std::mismatch(installDirNorm.begin(), installDirNorm.end(), source.begin(), source.end()); + const auto targetIn = std::mismatch(winui3DirNorm.begin(), winui3DirNorm.end(), target.begin(), target.end()); + if (sourceIn.first != installDirNorm.end() || targetIn.first != winui3DirNorm.end()) + { + WcaLog(LOGMSG_STANDARD, "CreateWinAppSDKHardlinks: Rejecting entry outside install root: %ls", fileName.c_str()); + failed++; + continue; + } + + if (!fs::exists(source)) + { + WcaLog(LOGMSG_STANDARD, "CreateWinAppSDKHardlinks: Source not found: %ls", source.c_str()); + failed++; + continue; + } + + // Remove existing file if present (leftover from previous install) + std::error_code ec; + fs::remove(target, ec); + + // Use a regular file copy (not a hard-link). Hard-links share an + // NTFS inode -- and therefore one DACL -- between root and + // WinUI3Apps, which lets MSIX sparse-package registration + // propagate the WinUI3Apps parent's rich (Capability/Package SID) + // DACL onto the root path. That trips a kernel "stricter access + // evaluation" path that blocks LOW-IL prevhost.exe from loading + // hostfxr.dll, so File Explorer Monaco preview goes blank on + // Windows 11 23H2. Copying creates a fresh inode in WinUI3Apps, + // so the root copy keeps its simple DACL while the WinUI3Apps + // copy inherits the rich DACL from its parent (matches 0.99.1 + // behaviour). See Documents\PR-47233-Handoff.md for full RCA. + fs::copy_file(source, target, fs::copy_options::overwrite_existing, ec); + if (ec) + { + WcaLog(LOGMSG_STANDARD, "CreateWinAppSDKHardlinks: Failed to copy: %ls (%hs)", fileName.c_str(), ec.message().c_str()); + failed++; + } + else + { + created++; + } + } + + WcaLog(LOGMSG_STANDARD, "CreateWinAppSDKHardlinks: Copied %d files, %d failures", created, failed); + + // Catastrophic-case escalation: if every copy failed, the WinUI3Apps tree is + // unusable (Monaco preview / context-menu shells will break). Surface this rather + // than reporting install success. Per-file failures remain tolerated. + if (created == 0 && failed > 0) + { + hr = E_FAIL; + ExitOnFailure(hr, "All WinAppSDK file copies failed; aborting install."); + } + } + +LExit: + er = SUCCEEDED(hr) ? ERROR_SUCCESS : ERROR_INSTALL_FAILURE; + return WcaFinalize(er); +} + +UINT __stdcall DeleteWinAppSDKHardlinksCA(MSIHANDLE hInstall) +{ + HRESULT hr = S_OK; + UINT er = ERROR_SUCCESS; + std::wstring installationFolder; + + hr = WcaInitialize(hInstall, "DeleteWinAppSDKHardlinks"); + ExitOnFailure(hr, "Failed to initialize"); + hr = getInstallFolder(hInstall, installationFolder); + ExitOnFailure(hr, "Failed to get installFolder."); + + { + namespace fs = std::filesystem; + const fs::path winui3Dir = fs::path(installationFolder) / L"WinUI3Apps"; + const fs::path manifestPath = winui3Dir / L"hardlinks.txt"; + + if (!fs::exists(manifestPath)) + { + goto LExit; + } + + std::ifstream manifestFile(manifestPath); // Read as bytes; convert UTF-8 -> wide explicitly. + std::string narrowLine; + + // INSTALLFOLDER from MSI typically arrives with a trailing backslash; strip it before + // normalizing so the per-line containment check doesn't false-reject every entry. + auto stripTrailingSep = [](fs::path p) { + auto s = p.native(); + while (s.size() > 1 && (s.back() == L'\\' || s.back() == L'/')) s.pop_back(); + return fs::path(s); + }; + const fs::path winui3DirNorm = stripTrailingSep(winui3Dir).lexically_normal(); + + while (std::getline(manifestFile, narrowLine)) + { + if (narrowLine.empty()) + { + continue; + } + if (narrowLine.back() == '\r') + { + narrowLine.pop_back(); + if (narrowLine.empty()) continue; + } + + const int wideLen = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, narrowLine.c_str(), -1, nullptr, 0); + if (wideLen <= 0) + { + WcaLog(LOGMSG_STANDARD, "DeleteWinAppSDKHardlinks: Skipping non-UTF-8 entry: %hs", narrowLine.c_str()); + continue; + } + std::wstring fileName(static_cast(wideLen) - 1, L'\0'); + MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, narrowLine.c_str(), -1, fileName.data(), wideLen); + + // Defense-in-depth: reject entries whose resolved target escapes WinUI3Apps. + const fs::path target = (winui3Dir / fileName).lexically_normal(); + const auto inWinui3 = std::mismatch(winui3DirNorm.begin(), winui3DirNorm.end(), target.begin(), target.end()); + if (inWinui3.first != winui3DirNorm.end()) + { + WcaLog(LOGMSG_STANDARD, "DeleteWinAppSDKHardlinks: Rejecting entry outside WinUI3Apps: %ls", fileName.c_str()); + continue; + } + + std::error_code ec; + fs::remove(target, ec); + } + + WcaLog(LOGMSG_STANDARD, "DeleteWinAppSDKHardlinks: Cleaned up deduplicated copy files"); + } + +LExit: + er = SUCCEEDED(hr) ? ERROR_SUCCESS : ERROR_INSTALL_FAILURE; + return WcaFinalize(er); +} + // DllMain - Initialize and cleanup WiX custom action utils. extern "C" BOOL WINAPI DllMain(__in HINSTANCE hInst, __in ULONG ulReason, __in LPVOID) { diff --git a/installer/PowerToysSetupCustomActionsVNext/CustomAction.def b/installer/PowerToysSetupCustomActionsVNext/CustomAction.def index 86efe34aa6..1b8b7d55f8 100644 --- a/installer/PowerToysSetupCustomActionsVNext/CustomAction.def +++ b/installer/PowerToysSetupCustomActionsVNext/CustomAction.def @@ -36,3 +36,5 @@ EXPORTS SetBundleInstallLocationCA InstallPackageIdentityMSIXCA UninstallPackageIdentityMSIXCA + CreateWinAppSDKHardlinksCA + DeleteWinAppSDKHardlinksCA diff --git a/installer/PowerToysSetupVNext/Product.wxs b/installer/PowerToysSetupVNext/Product.wxs index 74a09972d6..8651a7d83d 100644 --- a/installer/PowerToysSetupVNext/Product.wxs +++ b/installer/PowerToysSetupVNext/Product.wxs @@ -112,6 +112,8 @@ + + @@ -124,6 +126,7 @@ + @@ -137,6 +140,7 @@ + @@ -189,8 +193,10 @@ + + diff --git a/installer/PowerToysSetupVNext/WinUI3Applications.wxs b/installer/PowerToysSetupVNext/WinUI3Applications.wxs index 4c177b960a..c0d3afb6f4 100644 --- a/installer/PowerToysSetupVNext/WinUI3Applications.wxs +++ b/installer/PowerToysSetupVNext/WinUI3Applications.wxs @@ -7,11 +7,18 @@ + + + + + + + diff --git a/installer/PowerToysSetupVNext/generateAllFileComponents.ps1 b/installer/PowerToysSetupVNext/generateAllFileComponents.ps1 index 048f587def..7273251844 100644 --- a/installer/PowerToysSetupVNext/generateAllFileComponents.ps1 +++ b/installer/PowerToysSetupVNext/generateAllFileComponents.ps1 @@ -30,6 +30,10 @@ Function Generate-FileList() { $fileInclusionList = @("*.dll", "*.exe", "*.json", "*.msix", "*.png", "*.gif", "*.ico", "*.cur", "*.svg", "index.html", "reg.js", "gitignore.js", "srt.js", "monacoSpecialLanguages.js", "customTokenThemeRules.js", "*.pri") + # MFC DLLs leak into the output via WindowsAppSDKSelfContained but no PowerToys binary imports them. + # Verified with dumpbin /dependents across all 2176 binaries — zero consumers. + $fileExclusionList += @("mfc140.dll", "mfc140u.dll", "mfcm140.dll", "mfcm140u.dll") + $dllsToIgnore = @("System.CodeDom.dll", "WindowsBase.dll") if ($fileDepsJson -eq [string]::Empty) { @@ -85,11 +89,16 @@ Function Generate-FileComponents() { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'fileList', Justification = 'variable is used in another scope')] - $fileList = $matches[2] -split ';' + $fileList = $matches[2] -split ';' | Where-Object { $_ -ne '' } return } } + if ($null -eq $fileList -or $fileList.Count -eq 0) { + # No files to generate components for — leave placeholder intact + return + } + $componentId = "$($fileListName)_Component" $componentDefs = "`r`n" @@ -154,6 +163,67 @@ Generate-FileComponents -fileListName "BaseApplicationsFiles" -wxsFilePath $PSSc #WinUI3Applications Generate-FileList -fileDepsJson "" -fileListName WinUI3ApplicationsFiles -wxsFilePath $PSScriptRoot\WinUI3Applications.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps" + +# Deduplicate: Remove files from WinUI3Apps that are identical to root (same name + same hash). +# These will be re-created as plain file copies at install time by CreateWinAppSDKHardlinksCA. +# (The CA's name is historical: it now uses fs::copy_file rather than CreateHardLinkW to avoid +# DACL contamination across the shared inode -- see CustomAction.cpp for details.) +$rootPath = "$PSScriptRoot..\..\..\$platform\Release" +$winui3Path = "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps" +$winui3WxsPath = "$PSScriptRoot\WinUI3Applications.wxs" +$winui3Wxs = Get-Content $winui3WxsPath -Raw +$manifestPath = Join-Path $winui3Path "hardlinks.txt" + +if ($winui3Wxs -match "\<\?define WinUI3ApplicationsFiles=([^?]*)\?\>") { + $winui3FileList = $matches[1] -split ';' | Where-Object { $_ -ne '' } + $hardlinkFiles = @() + + # Read the BaseApplications WXS file list so we only deduplicate files that the MSI + # is actually deploying to the install root. If a file was stripped from BaseApplications + # by an earlier step (e.g., the ImageResizer leaked-apphost workaround above), the + # install-time CA's source would be missing and both copies would disappear. + $baseAppsWxs = Get-Content $baseAppWxsPath -Raw + $baseAppsFileList = @() + if ($baseAppsWxs -match "\<\?define BaseApplicationsFiles=([^?]*)\?\>") { + $baseAppsFileList = $matches[1] -split ';' | Where-Object { $_ -ne '' } + } + + foreach ($file in $winui3FileList) { + # Skip files that were intentionally not deployed to root by the build + if ($baseAppsFileList -notcontains $file) { continue } + + $rootFile = Join-Path $rootPath $file + $winui3File = Join-Path $winui3Path $file + if ((Test-Path $rootFile) -and (Test-Path $winui3File)) { + $rootHash = (Get-FileHash $rootFile -Algorithm SHA256).Hash + $winui3Hash = (Get-FileHash $winui3File -Algorithm SHA256).Hash + if ($rootHash -eq $winui3Hash) { + $hardlinkFiles += $file + } + } + } + + if ($hardlinkFiles.Count -gt 0) { + # Remove deduplicated files from WinUI3Apps file list + $remainingFiles = $winui3FileList | Where-Object { $_ -notin $hardlinkFiles } + if ($remainingFiles.Count -eq 0) { + # All files are duplicates — keep at least a dummy entry won't be emitted + # Generate-FileComponents handles empty defines by producing no entries + $winui3Wxs = $winui3Wxs -replace "\<\?define WinUI3ApplicationsFiles=[^?]*\?\>", "" + } else { + $winui3Wxs = $winui3Wxs -replace "\<\?define WinUI3ApplicationsFiles=[^?]*\?\>", "" + } + Set-Content -Path $winui3WxsPath -Value $winui3Wxs + Write-Host "Deduplicated $($hardlinkFiles.Count) files from WinUI3Apps (will be copied at install time)" + } + + # Always write hardlinks.txt (may be empty — CA handles that gracefully) + # Write as UTF-8 without BOM so the install-time CA can read it via std::ifstream + # + MultiByteToWideChar(CP_UTF8) without dealing with PS-version-dependent default + # encodings or a leading BOM. + [System.IO.File]::WriteAllLines($manifestPath, [string[]]$hardlinkFiles, (New-Object System.Text.UTF8Encoding($false))) +} + Generate-FileComponents -fileListName "WinUI3ApplicationsFiles" -wxsFilePath $PSScriptRoot\WinUI3Applications.wxs #AdvancedPaste diff --git a/src/modules/MouseWithoutBorders/App/Class/Program.cs b/src/modules/MouseWithoutBorders/App/Class/Program.cs index 144007e92f..129fba3ce3 100644 --- a/src/modules/MouseWithoutBorders/App/Class/Program.cs +++ b/src/modules/MouseWithoutBorders/App/Class/Program.cs @@ -21,7 +21,6 @@ using System.IO.Pipes; using System.Linq; using System.Security.Authentication.ExtendedProtection; using System.Security.Principal; -using System.ServiceModel.Channels; using System.ServiceProcess; using System.Threading; using System.Threading.Tasks; diff --git a/src/modules/MouseWithoutBorders/App/Helper/MouseWithoutBordersHelper.csproj b/src/modules/MouseWithoutBorders/App/Helper/MouseWithoutBordersHelper.csproj index 4b3fc7fd50..096e1745db 100644 --- a/src/modules/MouseWithoutBorders/App/Helper/MouseWithoutBordersHelper.csproj +++ b/src/modules/MouseWithoutBorders/App/Helper/MouseWithoutBordersHelper.csproj @@ -66,7 +66,6 @@ - diff --git a/src/modules/MouseWithoutBorders/App/MouseWithoutBorders.csproj b/src/modules/MouseWithoutBorders/App/MouseWithoutBorders.csproj index 675e927334..83906924fb 100644 --- a/src/modules/MouseWithoutBorders/App/MouseWithoutBorders.csproj +++ b/src/modules/MouseWithoutBorders/App/MouseWithoutBorders.csproj @@ -214,7 +214,6 @@ - diff --git a/src/modules/MouseWithoutBorders/App/Service/MouseWithoutBordersService.csproj b/src/modules/MouseWithoutBorders/App/Service/MouseWithoutBordersService.csproj index 0decd70d38..0f522b1bad 100644 --- a/src/modules/MouseWithoutBorders/App/Service/MouseWithoutBordersService.csproj +++ b/src/modules/MouseWithoutBorders/App/Service/MouseWithoutBordersService.csproj @@ -71,7 +71,6 @@ -