diff --git a/msipackage/package.wix.in b/msipackage/package.wix.in
index 6ebc9f3b2..e0ab2ef7f 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..54d5b660f 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,162 @@ extern "C" UINT __stdcall WslFinalizeInstallation(MSIHANDLE install)
return NOERROR;
}
+// 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).
+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);
+}
+
+// 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
+ {
+ 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 — only on workstation (MsiNTProductType = 1).
+ // The server shortcut does not use IsSystemComponent (matches original WiX authoring).
+ if (!IsWindowsServer())
+ {
+ 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
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