Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 19 additions & 3 deletions msipackage/package.wix.in
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,9 @@
<RemoveFile Id="CleanUpWSLShortCut" Directory="ProgramMenuFolder" Name="WSL" On="uninstall"/>
<File Id="wsl.exe" Name="wsl.exe" Source="${PACKAGE_INPUT_DIR}/wsl.exe" KeyPath="yes">
<Shortcut Id="WSLShortcut" Name="WSL" Description="Windows Subsystem for Linux" Arguments="--cd ~" Advertise="yes" Directory="ProgramMenuFolder" Icon="wsl.ico">
<ShortcutProperty Key="System.AppUserModel.ID" Value="Microsoft.WSL"/>
<ShortcutProperty Key="System.AppUserModel.ToastActivatorCLSID" Value="{2B9C59C3-98F1-45C8-B87B-12AE3C7927E8}"/>
<!-- ShortcutProperty entries removed to prevent Warning 1946 dialog popups.
Properties are set by the RepairShortcutProperties custom action with retry logic.
See: microsoft/WSL#11276, microsoft/WSL#13469 -->
</Shortcut>
</File>

Expand Down Expand Up @@ -285,7 +286,8 @@
<RemoveFile Id="CleanUpWSLSettingsShortCutNonServer" Directory="ProgramMenuFolder" Name="WSLSettings" On="uninstall"/>
<File Id="wslsettings.exe_nonserver" Source="${PACKAGE_INPUT_DIR}/wslsettings/wslsettings.exe" KeyPath="yes" ShortName="kyk8fs6a.exe">
<Shortcut Id="WSLSettingsShortcutNonServer" Name="WSL Settings" Description="Windows Subsystem for Linux Settings" Advertise="yes" Directory="ProgramMenuFolder" Icon="wsl.ico">
<ShortcutProperty Key="System.AppUserModel.IsSystemComponent" Value="true"/>
<!-- ShortcutProperty removed to prevent Warning 1946 dialog popup.
Set by RepairShortcutProperties custom action instead. -->
</Shortcut>
</File>
<!-- Protocol registration -->
Expand Down Expand Up @@ -454,6 +456,17 @@
Execute="deferred"
/>

<!-- Repair shortcut properties that may fail due to sharing violations (Warning 1946).
Runs after CreateShortcuts to re-apply properties with retry logic via COM IPropertyStore.
See: microsoft/WSL#11276, microsoft/WSL#13469 -->
<CustomAction Id="RepairShortcutProperties"
Impersonate="no"
BinaryRef="wslinstall.dll"
DllEntry="RepairShortcutProperties"
Return="ignore"
Execute="deferred"
/>
Comment thread
yeelam-gordon marked this conversation as resolved.

<CustomAction Id="RegisterLspCategories"
Impersonate="no"
BinaryRef="wslinstall.dll"
Expand Down Expand Up @@ -537,6 +550,9 @@

<Custom Action="FinalizeInstall" After="PublishFeatures"/>

<!-- Repair shortcut properties after CreateShortcuts has run. Only during install/upgrade, not uninstall. -->
<Custom Action="RepairShortcutProperties" After="FinalizeInstall" Condition='((not REMOVE~="ALL") or WIX_UPGRADE_DETECTED) and (not UPGRADINGPRODUCTCODE)' />

</InstallExecuteSequence>

<!-- Don't show a 'Modify' button in settings since there is nothing to modify -->
Expand Down
3 changes: 2 additions & 1 deletion src/windows/wslinstall/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@ target_link_libraries(wslinstall
common
legacy_stdio_definitions
Crypt32.lib
sfc.lib)
sfc.lib
propsys.lib)
166 changes: 166 additions & 0 deletions src/windows/wslinstall/DllMain.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,18 @@ Module Name:
#include <winrt/Windows.Foundation.Collections.h>
#include <winrt/windows.management.deployment.h>
#include <Sfc.h>
#include <propkey.h>
#include <propvarutil.h>
#include <ShlObj.h>
#include <ShObjIdl.h>
#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<MSIHANDLE, decltype(MsiCloseHandle), &MsiCloseHandle>;

using namespace wsl::windows::common::registry;
Expand Down Expand Up @@ -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<ShortcutPropertyEntry> 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<IShellLinkW> shellLink;
RETURN_IF_FAILED(CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&shellLink)));

wil::com_ptr<IPersistFile> 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<IPropertyStore> propertyStore;
RETURN_IF_FAILED(shellLink->QueryInterface(IID_PPV_ARGS(&propertyStore)));

Comment thread
yeelam-gordon marked this conversation as resolved.
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
{
Expand Down
1 change: 1 addition & 0 deletions src/windows/wslinstall/wslinstall.def
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ EXPORTS
DeprovisionMsix
WslValidateInstallation
WslFinalizeInstallation
RepairShortcutProperties
InstallMsix
InstallMsixAsUser
RegisterLspCategories
Expand Down
3 changes: 2 additions & 1 deletion test/windows/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
124 changes: 124 additions & 0 deletions test/windows/InstallerTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ Module Name:

#include "precomp.h"
#include <Sfc.h>
#include <propkey.h>
#include <propvarutil.h>
#include <ShObjIdl.h>

#include "Common.h"
#include "registry.hpp"
Expand Down Expand Up @@ -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<IShellLinkW> shellLink;
VERIFY_SUCCEEDED(CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&shellLink)));

wil::com_ptr<IPersistFile> 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<IPropertyStore> 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<VARTYPE>(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<VARTYPE>(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<RepairShortcutPropertiesFn>(
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<IShellLinkW> shellLink;
VERIFY_SUCCEEDED(CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&shellLink)));

wil::com_ptr<IPersistFile> 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<IPropertyStore> 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<VARTYPE>(VT_LPWSTR));
VERIFY_ARE_EQUAL(std::wstring(pvAppId.pwszVal), L"Microsoft.WSL");
}
};
Loading