-
Notifications
You must be signed in to change notification settings - Fork 7.5k
fix: Improve install scope detection to prevent parallel installations #44112
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
fix: Improve install scope detection to prevent parallel installations #44112
Conversation
Fixes microsoft#44071 The previous logic in get_current_install_scope() had a flaw where if HKLM registry key open failed (due to permissions or corruption), it would fall back to checking HKCU. If HKCU had a stale 'perUser' value, the updater would download the per-user installer for a per-machine installation, causing parallel installations. Changes: - Check HKLM for explicit 'perMachine' InstallScope value first - Only check HKCU for 'perUser' if HKLM doesn't have the value - Add fallback detection based on executable path (LocalAppData = per-user) - Proper resource cleanup with RegCloseKey in all code paths
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR improves install scope detection logic to prevent parallel PowerToys installations caused by stale registry values. The fix changes the detection order to check HKLM for per-machine installs first, then HKCU for per-user installs, and finally falls back to file path analysis when registry keys are missing or ambiguous. This addresses issue #44071 where auto-update could download the wrong installer type.
Key Changes:
- Restructured registry check order: HKLM → HKCU → file path fallback
- Added explicit per-machine check in HKLM before per-user check in HKCU
- Introduced file path analysis as final fallback to detect LocalAppData installations
| return InstallScope::PerMachine; | ||
| std::wstring data; | ||
| data.resize(dataSize / sizeof(wchar_t)); | ||
| if (RegGetValueW(hklmKey, nullptr, L"InstallScope", RRF_RT_REG_SZ, nullptr, &data[0], &dataSize) == ERROR_SUCCESS) |
Copilot
AI
Dec 6, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] Use data.data() instead of &data[0] for better code modernization and consistency. While &data[0] works, data.data() is the preferred C++11+ approach for accessing the underlying buffer of a string.
if (RegGetValueW(hklmKey, nullptr, L"InstallScope", RRF_RT_REG_SZ, nullptr, data.data(), &dataSize) == ERROR_SUCCESS)| nullptr, | ||
| &data[0], | ||
| &dataSize) != ERROR_SUCCESS) | ||
| if (RegGetValueW(hkcuKey, nullptr, L"InstallScope", RRF_RT_REG_SZ, nullptr, &data[0], &dataSize) == ERROR_SUCCESS) |
Copilot
AI
Dec 6, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] Use data.data() instead of &data[0] for better code modernization and consistency, same as the HKLM case.
if (RegGetValueW(hkcuKey, nullptr, L"InstallScope", RRF_RT_REG_SZ, nullptr, data.data(), &dataSize) == ERROR_SUCCESS)| if (RegGetValueW(hkcuKey, nullptr, L"InstallScope", RRF_RT_REG_SZ, nullptr, &data[0], &dataSize) == ERROR_SUCCESS) | |
| if (RegGetValueW(hkcuKey, nullptr, L"InstallScope", RRF_RT_REG_SZ, nullptr, data.data(), &dataSize) == ERROR_SUCCESS) |
| if (path.find(L"\\appdata\\local\\") != std::wstring::npos) | ||
| { | ||
| return InstallScope::PerUser; |
Copilot
AI
Dec 6, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Incomplete path check: The fallback logic only checks for \appdata\local\ but doesn't verify that it's actually within the user's profile. A malicious or unusual installation could have a path like C:\SomePath\appdata\local\PowerToys which would incorrectly match as PerUser.
Additionally, the default Program Files paths can vary (e.g., Program Files (x86), non-English Windows with localized folder names, or custom installation drives).
Consider checking against known environment variables or using stricter pattern matching:
wchar_t* localAppData = nullptr;
if (_wdupenv_s(&localAppData, nullptr, L"LOCALAPPDATA") == 0 && localAppData != nullptr)
{
std::wstring localAppDataPath(localAppData);
std::transform(localAppDataPath.begin(), localAppDataPath.end(), localAppDataPath.begin(), ::towlower);
free(localAppData);
if (path.find(localAppDataPath) == 0)
{
return InstallScope::PerUser;
}
}This ensures the path actually starts with the user's LocalAppData folder.
| if (path.find(L"\\appdata\\local\\") != std::wstring::npos) | |
| { | |
| return InstallScope::PerUser; | |
| wchar_t* localAppData = nullptr; | |
| if (_wdupenv_s(&localAppData, nullptr, L"LOCALAPPDATA") == 0 && localAppData != nullptr) | |
| { | |
| std::wstring localAppDataPath(localAppData); | |
| std::transform(localAppDataPath.begin(), localAppDataPath.end(), localAppDataPath.begin(), ::towlower); | |
| free(localAppData); | |
| // Ensure trailing backslash for proper prefix match | |
| if (!localAppDataPath.empty() && localAppDataPath.back() != L'\\') | |
| { | |
| localAppDataPath += L'\\'; | |
| } | |
| if (path.find(localAppDataPath) == 0) | |
| { | |
| return InstallScope::PerUser; | |
| } |
| if (GetModuleFileNameW(nullptr, modulePath, MAX_PATH) > 0) | ||
| { | ||
| std::wstring path(modulePath); | ||
| std::transform(path.begin(), path.end(), path.begin(), ::towlower); |
Copilot
AI
Dec 6, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing include: std::transform is used on line 92, but <algorithm> is not included in this header file. This will cause a compilation error.
Add the required include at the top of the file:
#include <algorithm>| HKEY hklmKey{}; | ||
| if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, INSTALL_SCOPE_REG_KEY, 0, KEY_READ, &hklmKey) == ERROR_SUCCESS) | ||
| { | ||
| // Open HKCU key | ||
| HKEY perUserKey{}; | ||
| if (RegOpenKeyExW(HKEY_CURRENT_USER, | ||
| INSTALL_SCOPE_REG_KEY, | ||
| 0, | ||
| KEY_READ, | ||
| &perUserKey) != ERROR_SUCCESS) | ||
| DWORD dataSize{}; | ||
| if (RegGetValueW(hklmKey, nullptr, L"InstallScope", RRF_RT_REG_SZ, nullptr, nullptr, &dataSize) == ERROR_SUCCESS) | ||
| { | ||
| // both keys are missing | ||
| return InstallScope::PerMachine; | ||
| std::wstring data; | ||
| data.resize(dataSize / sizeof(wchar_t)); | ||
| if (RegGetValueW(hklmKey, nullptr, L"InstallScope", RRF_RT_REG_SZ, nullptr, &data[0], &dataSize) == ERROR_SUCCESS) | ||
| { | ||
| RegCloseKey(hklmKey); | ||
| if (data.find(L"perMachine") != std::wstring::npos) | ||
| { | ||
| return InstallScope::PerMachine; | ||
| } | ||
| } | ||
| else | ||
| { | ||
| RegCloseKey(hklmKey); | ||
| } | ||
| } | ||
| else | ||
| { | ||
| DWORD dataSize{}; | ||
| if (RegGetValueW( | ||
| perUserKey, | ||
| nullptr, | ||
| L"InstallScope", | ||
| RRF_RT_REG_SZ, | ||
| nullptr, | ||
| nullptr, | ||
| &dataSize) != ERROR_SUCCESS) | ||
| { | ||
| // HKCU key is missing | ||
| RegCloseKey(perUserKey); | ||
| return InstallScope::PerMachine; | ||
| } | ||
| RegCloseKey(hklmKey); | ||
| } | ||
| } |
Copilot
AI
Dec 6, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The codebase uses a RAII pattern with detail::on_exit for managing registry keys (see lines 187, 231, 261 in this file). This approach is more maintainable and exception-safe than manually closing keys at multiple exit points.
Consider refactoring to use this pattern:
HKEY hklmKey{};
if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, INSTALL_SCOPE_REG_KEY, 0, KEY_READ, &hklmKey) == ERROR_SUCCESS)
{
detail::on_exit closeKey{ [hklmKey] { RegCloseKey(hklmKey); } };
DWORD dataSize{};
if (RegGetValueW(hklmKey, nullptr, L"InstallScope", RRF_RT_REG_SZ, nullptr, nullptr, &dataSize) == ERROR_SUCCESS)
{
std::wstring data;
data.resize(dataSize / sizeof(wchar_t));
if (RegGetValueW(hklmKey, nullptr, L"InstallScope", RRF_RT_REG_SZ, nullptr, data.data(), &dataSize) == ERROR_SUCCESS)
{
if (data.find(L"perMachine") != std::wstring::npos)
{
return InstallScope::PerMachine;
}
}
}
}This eliminates the need for multiple explicit RegCloseKey calls and prevents resource leaks.
| HKEY hkcuKey{}; | ||
| if (RegOpenKeyExW(HKEY_CURRENT_USER, INSTALL_SCOPE_REG_KEY, 0, KEY_READ, &hkcuKey) == ERROR_SUCCESS) | ||
| { | ||
| DWORD dataSize{}; | ||
| if (RegGetValueW(hkcuKey, nullptr, L"InstallScope", RRF_RT_REG_SZ, nullptr, nullptr, &dataSize) == ERROR_SUCCESS) | ||
| { | ||
| std::wstring data; | ||
| data.resize(dataSize / sizeof(wchar_t)); | ||
|
|
||
| if (RegGetValueW( | ||
| perUserKey, | ||
| nullptr, | ||
| L"InstallScope", | ||
| RRF_RT_REG_SZ, | ||
| nullptr, | ||
| &data[0], | ||
| &dataSize) != ERROR_SUCCESS) | ||
| if (RegGetValueW(hkcuKey, nullptr, L"InstallScope", RRF_RT_REG_SZ, nullptr, &data[0], &dataSize) == ERROR_SUCCESS) | ||
| { | ||
| // HKCU key is missing | ||
| RegCloseKey(perUserKey); | ||
| return InstallScope::PerMachine; | ||
| RegCloseKey(hkcuKey); | ||
| if (data.find(L"perUser") != std::wstring::npos) | ||
| { | ||
| return InstallScope::PerUser; | ||
| } | ||
| } | ||
| RegCloseKey(perUserKey); | ||
|
|
||
| if (data.contains(L"perUser")) | ||
| else | ||
| { | ||
| return InstallScope::PerUser; | ||
| RegCloseKey(hkcuKey); | ||
| } | ||
| } | ||
| else | ||
| { | ||
| RegCloseKey(hkcuKey); | ||
| } | ||
| } |
Copilot
AI
Dec 6, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as the HKLM case - consider using the detail::on_exit RAII pattern for managing the registry key, which is the established pattern in this codebase:
HKEY hkcuKey{};
if (RegOpenKeyExW(HKEY_CURRENT_USER, INSTALL_SCOPE_REG_KEY, 0, KEY_READ, &hkcuKey) == ERROR_SUCCESS)
{
detail::on_exit closeKey{ [hkcuKey] { RegCloseKey(hkcuKey); } };
DWORD dataSize{};
if (RegGetValueW(hkcuKey, nullptr, L"InstallScope", RRF_RT_REG_SZ, nullptr, nullptr, &dataSize) == ERROR_SUCCESS)
{
std::wstring data;
data.resize(dataSize / sizeof(wchar_t));
if (RegGetValueW(hkcuKey, nullptr, L"InstallScope", RRF_RT_REG_SZ, nullptr, data.data(), &dataSize) == ERROR_SUCCESS)
{
if (data.find(L"perUser") != std::wstring::npos)
{
return InstallScope::PerUser;
}
}
}
}
This comment has been minimized.
This comment has been minimized.
81c3fd4 to
1754d8d
Compare
Summary
Fixes #44071 - Auto-update creates parallel PowerToys installations.
PR Checklist
Problem
get_current_install_scope() had flawed logic - if HKLM open fails, it checks HKCU. If HKCU has stale perUser value, dowloads wrong installer causing parallel installs.
Solution