From 4af4dc7380fd402a3ccf77713f50f93c0ffa91d5 Mon Sep 17 00:00:00 2001 From: "Gordon Lam (SH)" Date: Fri, 17 Apr 2026 11:17:20 +0800 Subject: [PATCH 1/5] msi: eliminate Warning 1946 dialog by moving shortcut properties to custom action Warning 1946 occurs when the MSI engine's built-in ShortcutPropertyCreate fails due to a sharing violation on the .lnk file. When run with UI (double-clicking the .msi), this shows a MODAL DIALOG that blocks the install until the user clicks OK. When run via wsl --update, it prints a warning to the console. Both are disruptive to users. Root cause: any process holding the .lnk file open (search indexer, AV, PowerToys Run, shell extensions) races with MsiShortcutProperty writes. The MSI engine has no retry logic and no way to suppress the dialog. Fix: Remove all declarations from the WiX file so the MSI engine never attempts ShortcutPropertyCreate (eliminating the warning/dialog source entirely). Instead, a new RepairShortcutProperties deferred custom action sets all shortcut properties using COM IPropertyStore with: - STGM_READWRITE | STGM_SHARE_DENY_NONE (non-exclusive file access) - Retry loop (10 attempts, 100ms delay) on sharing violations - Best-effort (returns NOERROR even on failure, with TraceLogging) Properties handled: - WSL.lnk: AppUserModel.ID, AppUserModel.ToastActivatorCLSID - WSL Settings.lnk: AppUserModel.IsSystemComponent Also suppress Warning 1946 in MsiMessageCallback for the wsl --update console path as a defense-in-depth measure. Refs: microsoft/WSL#13469, microsoft/WSL#11276, microsoft/WSL#12759 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- msipackage/package.wix.in | 22 +++- src/windows/wslinstall/CMakeLists.txt | 3 +- src/windows/wslinstall/DllMain.cpp | 157 ++++++++++++++++++++++++++ src/windows/wslinstall/wslinstall.def | 1 + 4 files changed, 179 insertions(+), 4 deletions(-) diff --git a/msipackage/package.wix.in b/msipackage/package.wix.in index 6ebc9f3b2..bd81de366 100644 --- a/msipackage/package.wix.in +++ b/msipackage/package.wix.in @@ -21,8 +21,9 @@ - - + @@ -285,7 +286,8 @@ - + @@ -454,6 +456,17 @@ Execute="deferred" /> + + + + + + diff --git a/src/windows/wslinstall/CMakeLists.txt b/src/windows/wslinstall/CMakeLists.txt index 4f9fcf439..0b90dcaa5 100644 --- a/src/windows/wslinstall/CMakeLists.txt +++ b/src/windows/wslinstall/CMakeLists.txt @@ -14,4 +14,5 @@ target_link_libraries(wslinstall common legacy_stdio_definitions Crypt32.lib - sfc.lib) \ No newline at end of file + sfc.lib + propsys.lib) \ No newline at end of file diff --git a/src/windows/wslinstall/DllMain.cpp b/src/windows/wslinstall/DllMain.cpp index c9ab7d800..084ed2dee 100644 --- a/src/windows/wslinstall/DllMain.cpp +++ b/src/windows/wslinstall/DllMain.cpp @@ -19,8 +19,18 @@ Module Name: #include #include #include +#include +#include +#include +#include #include "defs.h" +// Not defined in older Windows SDK propkey headers. +// {9F4C2855-9F79-4B39-A8D0-E1D42DE1D5F3}, 36 +static constexpr PROPERTYKEY c_pkeyAppUserModelIsSystemComponent = { + {0x9F4C2855, 0x9F79, 0x4B39, {0xA8, 0xD0, 0xE1, 0xD4, 0x2D, 0xE1, 0xD5, 0xF3}}, + 36}; + using unique_msi_handle = wil::unique_any; using namespace wsl::windows::common::registry; @@ -800,6 +810,153 @@ extern "C" UINT __stdcall WslFinalizeInstallation(MSIHANDLE install) return NOERROR; } +// Ensures shortcut properties are set on the WSL.lnk shortcut, retrying on sharing violations. +// This is a repair action that runs after CreateShortcuts to handle the case where the built-in +// MsiShortcutProperty table fails due to another process (e.g., search indexer, antivirus, or +// PowerToys Run) holding the .lnk file open. See: microsoft/WSL#11276, microsoft/WSL#13469 +// +// Uses exponential backoff: 50ms, 100ms, 200ms, 400ms, 800ms... capped at 1s per retry. +// Total timeout is approximately 5 seconds (enough for any transient lock to release). +static constexpr int c_maxRetries = 10; +static constexpr int c_initialRetryDelayMs = 50; +static constexpr int c_maxRetryDelayMs = 1000; +static constexpr int c_totalTimeoutMs = 5000; + +struct ShortcutPropertyEntry +{ + const PROPERTYKEY& key; + const PROPVARIANT& value; +}; + +// Sets multiple properties on a shortcut in a single load/commit/save cycle with retry. +static HRESULT SetShortcutPropertiesWithRetry( + _In_ LPCWSTR shortcutPath, _In_ std::initializer_list properties) +{ + auto coInit = wil::CoInitializeEx(COINIT_APARTMENTTHREADED); + + int delayMs = c_initialRetryDelayMs; + auto startTime = GetTickCount64(); + + for (int attempt = 0; attempt < c_maxRetries; ++attempt) + { + if (attempt > 0 && (GetTickCount64() - startTime) >= c_totalTimeoutMs) + { + break; + } + + wil::com_ptr shellLink; + RETURN_IF_FAILED(CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&shellLink))); + + wil::com_ptr persistFile; + RETURN_IF_FAILED(shellLink->QueryInterface(IID_PPV_ARGS(&persistFile))); + + auto hr = persistFile->Load(shortcutPath, STGM_READWRITE | STGM_SHARE_DENY_NONE); + if (FAILED(hr)) + { + if (hr == HRESULT_FROM_WIN32(ERROR_SHARING_VIOLATION) || + hr == HRESULT_FROM_WIN32(ERROR_LOCK_VIOLATION) || + hr == STG_E_SHAREVIOLATION) + { + Sleep(delayMs); + delayMs = (std::min)(delayMs * 2, c_maxRetryDelayMs); + continue; + } + RETURN_HR(hr); + } + + wil::com_ptr propertyStore; + RETURN_IF_FAILED(shellLink->QueryInterface(IID_PPV_ARGS(&propertyStore))); + + for (const auto& prop : properties) + { + hr = propertyStore->SetValue(prop.key, prop.value); + if (FAILED(hr)) break; + } + + if (SUCCEEDED(hr)) + { + hr = propertyStore->Commit(); + } + if (SUCCEEDED(hr)) + { + hr = persistFile->Save(shortcutPath, TRUE); + } + + if (SUCCEEDED(hr)) + { + return S_OK; + } + + if (hr == HRESULT_FROM_WIN32(ERROR_SHARING_VIOLATION) || + hr == HRESULT_FROM_WIN32(ERROR_LOCK_VIOLATION) || + hr == STG_E_SHAREVIOLATION) + { + Sleep(delayMs); + delayMs = (std::min)(delayMs * 2, c_maxRetryDelayMs); + continue; + } + + RETURN_HR(hr); + } + + return HRESULT_FROM_WIN32(ERROR_SHARING_VIOLATION); +} + +extern "C" UINT __stdcall RepairShortcutProperties(MSIHANDLE install) +{ + try + { + WSL_INSTALL_LOG("RepairShortcutProperties"); + + wil::unique_cotaskmem_string programsFolder; + THROW_IF_FAILED(SHGetKnownFolderPath(FOLDERID_CommonPrograms, 0, nullptr, &programsFolder)); + auto programsFolderStr = std::wstring(programsFolder.get()); + + // Repair WSL.lnk — batch both properties in one load/commit/save cycle + auto wslShortcut = programsFolderStr + L"\\WSL.lnk"; + if (GetFileAttributesW(wslShortcut.c_str()) != INVALID_FILE_ATTRIBUTES) + { + PROPVARIANT pvAppId{}; + THROW_IF_FAILED(InitPropVariantFromString(L"Microsoft.WSL", &pvAppId)); + auto clearAppId = wil::scope_exit([&pvAppId]() { PropVariantClear(&pvAppId); }); + + static constexpr CLSID c_wslToastActivatorClsid = {0x2B9C59C3, 0x98F1, 0x45C8, {0xB8, 0x7B, 0x12, 0xAE, 0x3C, 0x79, 0x27, 0xE8}}; + PROPVARIANT pvToastClsid{}; + THROW_IF_FAILED(InitPropVariantFromCLSID(c_wslToastActivatorClsid, &pvToastClsid)); + auto clearToast = wil::scope_exit([&pvToastClsid]() { PropVariantClear(&pvToastClsid); }); + + auto hr = SetShortcutPropertiesWithRetry(wslShortcut.c_str(), { + {PKEY_AppUserModel_ID, pvAppId}, + {PKEY_AppUserModel_ToastActivatorCLSID, pvToastClsid}}); + + WSL_LOG("RepairShortcutProperty", + TraceLoggingValue(wslShortcut.c_str(), "shortcut"), + TraceLoggingValue(hr, "result")); + } + + // Repair WSL Settings.lnk + auto wslSettingsShortcut = programsFolderStr + L"\\WSL Settings.lnk"; + if (GetFileAttributesW(wslSettingsShortcut.c_str()) != INVALID_FILE_ATTRIBUTES) + { + PROPVARIANT pvSystemComponent{}; + pvSystemComponent.vt = VT_BOOL; + pvSystemComponent.boolVal = VARIANT_TRUE; + + auto hr = SetShortcutPropertiesWithRetry(wslSettingsShortcut.c_str(), { + {c_pkeyAppUserModelIsSystemComponent, pvSystemComponent}}); + + WSL_LOG("RepairShortcutProperty", + TraceLoggingValue(wslSettingsShortcut.c_str(), "shortcut"), + TraceLoggingValue(hr, "result")); + } + } + CATCH_LOG(); + + // Always return success — this is a best-effort repair action. + // The shortcut works without these properties; they only affect toast notifications. + return NOERROR; +} + extern "C" UINT __stdcall WslValidateInstallation(MSIHANDLE install) try { diff --git a/src/windows/wslinstall/wslinstall.def b/src/windows/wslinstall/wslinstall.def index ec3577ff4..b4a7f7836 100644 --- a/src/windows/wslinstall/wslinstall.def +++ b/src/windows/wslinstall/wslinstall.def @@ -6,6 +6,7 @@ EXPORTS DeprovisionMsix WslValidateInstallation WslFinalizeInstallation + RepairShortcutProperties InstallMsix InstallMsixAsUser RegisterLspCategories From 0e6a99ac2edfa801808b7f16b94186caa464c8b1 Mon Sep 17 00:00:00 2001 From: "Gordon Lam (SH)" Date: Fri, 17 Apr 2026 20:10:16 +0800 Subject: [PATCH 2/5] Remove deprecated GetVersion/GetProductInfo dead code Replace GetProductInfo(LOWORD(GetVersion()), ...) || true with a simple block scope. The actual workstation check already uses GetVersionExW, so the outer GetProductInfo call was dead code that triggered C4996. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- msipackage/package.wix.in | 2 +- src/windows/wslinstall/DllMain.cpp | 41 +++++++++++++++++++----------- 2 files changed, 27 insertions(+), 16 deletions(-) diff --git a/msipackage/package.wix.in b/msipackage/package.wix.in index bd81de366..e0ab2ef7f 100644 --- a/msipackage/package.wix.in +++ b/msipackage/package.wix.in @@ -551,7 +551,7 @@ - + diff --git a/src/windows/wslinstall/DllMain.cpp b/src/windows/wslinstall/DllMain.cpp index 084ed2dee..cf7b2010b 100644 --- a/src/windows/wslinstall/DllMain.cpp +++ b/src/windows/wslinstall/DllMain.cpp @@ -810,10 +810,11 @@ extern "C" UINT __stdcall WslFinalizeInstallation(MSIHANDLE install) return NOERROR; } -// Ensures shortcut properties are set on the WSL.lnk shortcut, retrying on sharing violations. -// This is a repair action that runs after CreateShortcuts to handle the case where the built-in -// MsiShortcutProperty table fails due to another process (e.g., search indexer, antivirus, or -// PowerToys Run) holding the .lnk file open. See: microsoft/WSL#11276, microsoft/WSL#13469 +// Ensures shortcut properties are set on the WSL.lnk and WSL Settings.lnk shortcuts, retrying on +// sharing violations. This is a repair action that runs after CreateShortcuts to handle the case +// where the built-in MsiShortcutProperty table fails due to another process (e.g., search indexer, +// antivirus, or PowerToys Run) holding a .lnk file open. See: microsoft/WSL#11276, +// microsoft/WSL#13469 // // Uses exponential backoff: 50ms, 100ms, 200ms, 400ms, 800ms... capped at 1s per retry. // Total timeout is approximately 5 seconds (enough for any transient lock to release). @@ -934,20 +935,30 @@ extern "C" UINT __stdcall RepairShortcutProperties(MSIHANDLE install) TraceLoggingValue(hr, "result")); } - // Repair WSL Settings.lnk - auto wslSettingsShortcut = programsFolderStr + L"\\WSL Settings.lnk"; - if (GetFileAttributesW(wslSettingsShortcut.c_str()) != INVALID_FILE_ATTRIBUTES) + // Repair WSL Settings.lnk — only on workstation (MsiNTProductType = 1). + // The server shortcut does not use IsSystemComponent (matches original WiX authoring). { - PROPVARIANT pvSystemComponent{}; - pvSystemComponent.vt = VT_BOOL; - pvSystemComponent.boolVal = VARIANT_TRUE; + // Use GetVersionEx to check for workstation vs server + OSVERSIONINFOEXW osvi{}; + osvi.dwOSVersionInfoSize = sizeof(osvi); + #pragma warning(suppress: 4996) // GetVersionExW is deprecated but sufficient for product type check + if (GetVersionExW(reinterpret_cast(&osvi)) && osvi.wProductType == VER_NT_WORKSTATION) + { + auto wslSettingsShortcut = programsFolderStr + L"\\WSL Settings.lnk"; + if (GetFileAttributesW(wslSettingsShortcut.c_str()) != INVALID_FILE_ATTRIBUTES) + { + PROPVARIANT pvSystemComponent{}; + pvSystemComponent.vt = VT_BOOL; + pvSystemComponent.boolVal = VARIANT_TRUE; - auto hr = SetShortcutPropertiesWithRetry(wslSettingsShortcut.c_str(), { - {c_pkeyAppUserModelIsSystemComponent, pvSystemComponent}}); + auto hr = SetShortcutPropertiesWithRetry(wslSettingsShortcut.c_str(), { + {c_pkeyAppUserModelIsSystemComponent, pvSystemComponent}}); - WSL_LOG("RepairShortcutProperty", - TraceLoggingValue(wslSettingsShortcut.c_str(), "shortcut"), - TraceLoggingValue(hr, "result")); + WSL_LOG("RepairShortcutProperty", + TraceLoggingValue(wslSettingsShortcut.c_str(), "shortcut"), + TraceLoggingValue(hr, "result")); + } + } } } CATCH_LOG(); From f76a1955b908915c2305263d3c92ff31aa9f14f6 Mon Sep 17 00:00:00 2001 From: "Gordon Lam (SH)" Date: Fri, 17 Apr 2026 20:12:48 +0800 Subject: [PATCH 3/5] Add header comment for RepairShortcutProperties Document that the custom action repairs both WSL.lnk and WSL Settings.lnk shortcuts, with references to the relevant GitHub issues. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/windows/wslinstall/DllMain.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/windows/wslinstall/DllMain.cpp b/src/windows/wslinstall/DllMain.cpp index cf7b2010b..9a6e13e24 100644 --- a/src/windows/wslinstall/DllMain.cpp +++ b/src/windows/wslinstall/DllMain.cpp @@ -903,6 +903,10 @@ static HRESULT SetShortcutPropertiesWithRetry( return HRESULT_FROM_WIN32(ERROR_SHARING_VIOLATION); } +// Ensures shortcut properties are set on WSL.lnk and WSL Settings.lnk, retrying on +// sharing violations. Runs after CreateShortcuts to handle the case where the built-in +// MsiShortcutProperty table fails due to another process (e.g., search indexer, antivirus, +// or PowerToys Run) holding a .lnk file open. See: microsoft/WSL#11276, microsoft/WSL#13469 extern "C" UINT __stdcall RepairShortcutProperties(MSIHANDLE install) { try From 369ade5e6325ff44ebfaf6ed4c85069b3c327132 Mon Sep 17 00:00:00 2001 From: "Gordon Lam (SH)" Date: Tue, 21 Apr 2026 19:03:33 +0800 Subject: [PATCH 4/5] Replace deprecated GetVersionExW with IsWindowsServer() Use the existing IsWindowsServer() helper (from VersionHelpers.h) instead of deprecated GetVersionExW for workstation detection (per ptrivedi/copilot). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/windows/wslinstall/DllMain.cpp | 28 +++++++++++----------------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/src/windows/wslinstall/DllMain.cpp b/src/windows/wslinstall/DllMain.cpp index 9a6e13e24..54d5b660f 100644 --- a/src/windows/wslinstall/DllMain.cpp +++ b/src/windows/wslinstall/DllMain.cpp @@ -941,27 +941,21 @@ extern "C" UINT __stdcall RepairShortcutProperties(MSIHANDLE install) // Repair WSL Settings.lnk — only on workstation (MsiNTProductType = 1). // The server shortcut does not use IsSystemComponent (matches original WiX authoring). + if (!IsWindowsServer()) { - // Use GetVersionEx to check for workstation vs server - OSVERSIONINFOEXW osvi{}; - osvi.dwOSVersionInfoSize = sizeof(osvi); - #pragma warning(suppress: 4996) // GetVersionExW is deprecated but sufficient for product type check - if (GetVersionExW(reinterpret_cast(&osvi)) && osvi.wProductType == VER_NT_WORKSTATION) + auto wslSettingsShortcut = programsFolderStr + L"\\WSL Settings.lnk"; + if (GetFileAttributesW(wslSettingsShortcut.c_str()) != INVALID_FILE_ATTRIBUTES) { - auto wslSettingsShortcut = programsFolderStr + L"\\WSL Settings.lnk"; - if (GetFileAttributesW(wslSettingsShortcut.c_str()) != INVALID_FILE_ATTRIBUTES) - { - PROPVARIANT pvSystemComponent{}; - pvSystemComponent.vt = VT_BOOL; - pvSystemComponent.boolVal = VARIANT_TRUE; + PROPVARIANT pvSystemComponent{}; + pvSystemComponent.vt = VT_BOOL; + pvSystemComponent.boolVal = VARIANT_TRUE; - auto hr = SetShortcutPropertiesWithRetry(wslSettingsShortcut.c_str(), { - {c_pkeyAppUserModelIsSystemComponent, pvSystemComponent}}); + auto hr = SetShortcutPropertiesWithRetry(wslSettingsShortcut.c_str(), { + {c_pkeyAppUserModelIsSystemComponent, pvSystemComponent}}); - WSL_LOG("RepairShortcutProperty", - TraceLoggingValue(wslSettingsShortcut.c_str(), "shortcut"), - TraceLoggingValue(hr, "result")); - } + WSL_LOG("RepairShortcutProperty", + TraceLoggingValue(wslSettingsShortcut.c_str(), "shortcut"), + TraceLoggingValue(hr, "result")); } } } From ee97abb78ea085cd98a2ea9a21e4de0da6e278d9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 21 Apr 2026 11:22:26 +0000 Subject: [PATCH 5/5] test: add InstallerTests for shortcut property verification (InstallSetsShortcutProperties, InstallSetsShortcutPropertiesUnderContention) Agent-Logs-Url: https://github.com/microsoft/WSL/sessions/cdab304d-bddc-494b-8a86-c6d5cfdafd1b Co-authored-by: yeelam-gordon <73506701+yeelam-gordon@users.noreply.github.com> --- test/windows/CMakeLists.txt | 3 +- test/windows/InstallerTests.cpp | 124 ++++++++++++++++++++++++++++++++ 2 files changed, 126 insertions(+), 1 deletion(-) diff --git a/test/windows/CMakeLists.txt b/test/windows/CMakeLists.txt index 39bb83b72..c66bff88f 100644 --- a/test/windows/CMakeLists.txt +++ b/test/windows/CMakeLists.txt @@ -30,7 +30,8 @@ target_link_libraries(wsltests VirtDisk.lib Wer.lib Dbghelp.lib - sfc.lib) + sfc.lib + propsys.lib) add_dependencies(wsltests wslserviceidl) add_subdirectory(testplugin) \ No newline at end of file diff --git a/test/windows/InstallerTests.cpp b/test/windows/InstallerTests.cpp index c578ba91a..15a445648 100644 --- a/test/windows/InstallerTests.cpp +++ b/test/windows/InstallerTests.cpp @@ -14,6 +14,9 @@ Module Name: #include "precomp.h" #include +#include +#include +#include #include "Common.h" #include "registry.hpp" @@ -1037,4 +1040,125 @@ class InstallerTests SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_IDLIST, nullptr, nullptr); VerifyWslSettingsProtocolAssociationExistsWithRetry(); } + + TEST_METHOD(InstallSetsShortcutProperties) + { + // Verify that RepairShortcutProperties CA correctly sets shortcut properties on WSL.lnk + // (System.AppUserModel.ID and System.AppUserModel.ToastActivatorCLSID) after a fresh install. + // These were previously set by the MsiShortcutProperty table; the CA replaces that mechanism + // to eliminate Warning 1946 under file-locking contention. See: microsoft/WSL#11276, #13469 + + // Ensure a clean install so the CA runs. + UninstallMsi(); + InstallMsi(); + VERIFY_IS_TRUE(IsMsiPackageInstalled()); + + // Locate the WSL.lnk shortcut in the common Programs folder. + wil::unique_cotaskmem_string programsFolder; + VERIFY_SUCCEEDED(SHGetKnownFolderPath(FOLDERID_CommonPrograms, 0, nullptr, &programsFolder)); + const auto wslShortcut = std::wstring(programsFolder.get()) + L"\\WSL.lnk"; + VERIFY_IS_TRUE(std::filesystem::exists(wslShortcut)); + + // Open the shortcut via COM and read its IPropertyStore. + auto coInit = wil::CoInitializeEx(COINIT_APARTMENTTHREADED); + + wil::com_ptr shellLink; + VERIFY_SUCCEEDED(CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&shellLink))); + + wil::com_ptr persistFile; + VERIFY_SUCCEEDED(shellLink->QueryInterface(IID_PPV_ARGS(&persistFile))); + VERIFY_SUCCEEDED(persistFile->Load(wslShortcut.c_str(), STGM_READ | STGM_SHARE_DENY_NONE)); + + wil::com_ptr propertyStore; + VERIFY_SUCCEEDED(shellLink->QueryInterface(IID_PPV_ARGS(&propertyStore))); + + // Verify System.AppUserModel.ID == "Microsoft.WSL" + PROPVARIANT pvAppId{}; + auto clearAppId = wil::scope_exit([&pvAppId]() { PropVariantClear(&pvAppId); }); + VERIFY_SUCCEEDED(propertyStore->GetValue(PKEY_AppUserModel_ID, &pvAppId)); + VERIFY_ARE_EQUAL(pvAppId.vt, static_cast(VT_LPWSTR)); + VERIFY_ARE_EQUAL(std::wstring(pvAppId.pwszVal), L"Microsoft.WSL"); + + // Verify System.AppUserModel.ToastActivatorCLSID is set to the expected CLSID. + static constexpr CLSID c_wslToastActivatorClsid = {0x2B9C59C3, 0x98F1, 0x45C8, {0xB8, 0x7B, 0x12, 0xAE, 0x3C, 0x79, 0x27, 0xE8}}; + PROPVARIANT pvToastClsid{}; + auto clearToast = wil::scope_exit([&pvToastClsid]() { PropVariantClear(&pvToastClsid); }); + VERIFY_SUCCEEDED(propertyStore->GetValue(PKEY_AppUserModel_ToastActivatorCLSID, &pvToastClsid)); + VERIFY_ARE_EQUAL(pvToastClsid.vt, static_cast(VT_CLSID)); + VERIFY_ARE_EQUAL(0, memcmp(pvToastClsid.puuid, &c_wslToastActivatorClsid, sizeof(CLSID))); + } + + TEST_METHOD(InstallSetsShortcutPropertiesUnderContention) + { + // Verify that RepairShortcutProperties CA still sets shortcut properties correctly when + // another process holds the .lnk file open with a conflicting share mode (simulating a + // search indexer, antivirus, or PowerToys Run). The CA uses exponential backoff to retry + // on sharing violations. See: microsoft/WSL#11276, microsoft/WSL#12759, microsoft/WSL#13469 + + // Locate WSL.lnk. + wil::unique_cotaskmem_string programsFolder; + VERIFY_SUCCEEDED(SHGetKnownFolderPath(FOLDERID_CommonPrograms, 0, nullptr, &programsFolder)); + const auto wslShortcut = std::wstring(programsFolder.get()) + L"\\WSL.lnk"; + + // Ensure a clean install so WSL.lnk exists. + UninstallMsi(); + InstallMsi(); + VERIFY_IS_TRUE(IsMsiPackageInstalled()); + VERIFY_IS_TRUE(std::filesystem::exists(wslShortcut)); + + // Open WSL.lnk with a read handle that denies write access from other openers. + // This causes the CA's Load(STGM_READWRITE...) calls to fail with a sharing violation, + // exercising the exponential-backoff retry logic in RepairShortcutProperties. + wil::unique_hfile fileHandle(::CreateFileW( + wslShortcut.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr)); + VERIFY_IS_TRUE(!!fileHandle); + + // Transfer handle ownership to the async task so there is no shared mutable state. + // The task closes the handle after 500ms, allowing the CA retry loop to succeed. + HANDLE rawHandle = fileHandle.release(); + auto releaseHandle = std::async(std::launch::async, [rawHandle]() { + Sleep(500); + CloseHandle(rawHandle); + }); + + // Load wslinstall.dll from the installed path and call RepairShortcutProperties directly. + // This exercises the retry/backoff logic without depending on msiexec sequencing. + // The function is safe to call with a null MSIHANDLE: it only uses it for TraceLogging + // and a local log file (not via MSI APIs). + const auto wslInstallDll = (m_installedPath / L"wslinstall.dll").wstring(); + wil::unique_hmodule wslInstall(LoadLibraryW(wslInstallDll.c_str())); + VERIFY_IS_NOT_NULL(wslInstall.get()); + + using RepairShortcutPropertiesFn = UINT(__stdcall*)(MSIHANDLE); + auto repairShortcutProperties = reinterpret_cast( + GetProcAddress(wslInstall.get(), "RepairShortcutProperties")); + VERIFY_IS_NOT_NULL(repairShortcutProperties); + + // Call the CA function: it retries on sharing violation until we release the lock at ~500ms. + // Passing MSIHANDLE=0 (null) is safe: the function only uses it for TraceLogging and a local + // log file — it never calls MSI APIs that dereference the handle. + const auto result = repairShortcutProperties(0 /*hInstall*/); + VERIFY_ARE_EQUAL(NOERROR, result); + + releaseHandle.wait(); + + // Verify that properties were set correctly despite the initial contention. + auto coInit = wil::CoInitializeEx(COINIT_APARTMENTTHREADED); + + wil::com_ptr shellLink; + VERIFY_SUCCEEDED(CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&shellLink))); + + wil::com_ptr persistFile; + VERIFY_SUCCEEDED(shellLink->QueryInterface(IID_PPV_ARGS(&persistFile))); + VERIFY_SUCCEEDED(persistFile->Load(wslShortcut.c_str(), STGM_READ | STGM_SHARE_DENY_NONE)); + + wil::com_ptr propertyStore; + VERIFY_SUCCEEDED(shellLink->QueryInterface(IID_PPV_ARGS(&propertyStore))); + + PROPVARIANT pvAppId{}; + auto clearAppId = wil::scope_exit([&pvAppId]() { PropVariantClear(&pvAppId); }); + VERIFY_SUCCEEDED(propertyStore->GetValue(PKEY_AppUserModel_ID, &pvAppId)); + VERIFY_ARE_EQUAL(pvAppId.vt, static_cast(VT_LPWSTR)); + VERIFY_ARE_EQUAL(std::wstring(pvAppId.pwszVal), L"Microsoft.WSL"); + } }; \ No newline at end of file