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