diff options
Diffstat (limited to 'toolkit/mozapps/defaultagent/SetDefaultBrowser.cpp')
-rw-r--r-- | toolkit/mozapps/defaultagent/SetDefaultBrowser.cpp | 393 |
1 files changed, 387 insertions, 6 deletions
diff --git a/toolkit/mozapps/defaultagent/SetDefaultBrowser.cpp b/toolkit/mozapps/defaultagent/SetDefaultBrowser.cpp index 8bc0889e67..6d829ff045 100644 --- a/toolkit/mozapps/defaultagent/SetDefaultBrowser.cpp +++ b/toolkit/mozapps/defaultagent/SetDefaultBrowser.cpp @@ -6,17 +6,16 @@ #include <windows.h> #include <appmodel.h> #include <shlobj.h> // for SHChangeNotify and IApplicationAssociationRegistration -#include <functional> #include <timeapi.h> #include "mozilla/ArrayUtils.h" #include "mozilla/CmdLineAndEnvUtils.h" #include "mozilla/RefPtr.h" +#include "mozilla/Result.h" #include "mozilla/UniquePtr.h" #include "mozilla/WindowsVersion.h" #include "mozilla/WinHeaderOnlyUtils.h" #include "WindowsUserChoice.h" -#include "nsThreadUtils.h" #include "EventLog.h" #include "SetDefaultBrowser.h" @@ -70,6 +69,77 @@ static bool AddMillisecondsToSystemTime(SYSTEMTIME& aSystemTime, return true; } +/* + * Takes two times: the start time and the current time, and returns the number + * of seconds left before the current time hits the next minute from the start + * time. Used to check if we are within the same minute as the start time and + * how much time we have left to perform an operation in the same minute. + * + * Used with user choice hashes, which have to be written to the registry + * in the same minute as they are generated. + * + * Example 1: + * operationStartTime - 10m 20s 800ms + * currentTime - 10m 22s 0ms + * The next minute is 11, so the return value is 11m - 10m 22s 0ms, converted to + * milliseconds. + * + * Example 2: + * operationStartTime - 10m 59s 800ms + * currentTime - 11m 0s 0ms + * The next minute is 11, but the minute the operation started on was 10, so the + * time to the next minute is 0 (because the current time is already at the next + * minute). + * + * @param operationStartTime + * @param currentTime + * + * @returns the number of milliseconds left from the current time to the + * next minute from operationStartTime, or zero if the currentTime is already at + * the next minute or greater + */ +static WORD GetMillisecondsToNextMinute(SYSTEMTIME operationStartTime, + SYSTEMTIME currentTime) { + SYSTEMTIME operationStartTimeMinute = operationStartTime; + SYSTEMTIME currentTimeMinute = currentTime; + + // Zero out the seconds and milliseconds so that we can confirm they are the + // same minutes + operationStartTimeMinute.wSecond = 0; + operationStartTimeMinute.wMilliseconds = 0; + + currentTimeMinute.wSecond = 0; + currentTimeMinute.wMilliseconds = 0; + + // Convert to a 64 bit value so we can compare them directly + FILETIME fileTime1; + FILETIME fileTime2; + if (!::SystemTimeToFileTime(&operationStartTimeMinute, &fileTime1) || + !::SystemTimeToFileTime(¤tTimeMinute, &fileTime2)) { + // Error: report that there is 0 milliseconds till the next minute + return 0; + } + + // The minutes for both times have to be the same, so confirm that they are, + // and if they aren't, return 0 milliseconds to indicate that we're already + // not on the minute that operationStartTime was on + if ((fileTime1.dwLowDateTime != fileTime2.dwLowDateTime) || + (fileTime1.dwHighDateTime != fileTime2.dwHighDateTime)) { + return 0; + } + + // The minutes are the same; determine the number of milliseconds left until + // the next minute + const WORD secondsToMilliseconds = 1000; + const WORD minutesToSeconds = 60; + + // 1 minute converted to milliseconds - (the current second converted to + // milliseconds + the current milliseconds) + return (1 * minutesToSeconds * secondsToMilliseconds) - + ((currentTime.wSecond * secondsToMilliseconds) + + currentTime.wMilliseconds); +} + // Compare two SYSTEMTIMEs as FILETIME after clearing everything // below minutes. static bool CheckEqualMinutes(SYSTEMTIME aSystemTime1, @@ -149,6 +219,75 @@ static bool SetUserChoiceRegistry(const wchar_t* aExt, const wchar_t* aProgID, return true; } +struct LaunchExeErr {}; +using LaunchExeResult = + mozilla::Result<std::tuple<DWORD, mozilla::UniquePtr<wchar_t[]>>, + LaunchExeErr>; + +static LaunchExeResult LaunchExecutable(const wchar_t* exePath, int aArgsLength, + const wchar_t* const* aArgs) { + const wchar_t* args[] = {exePath}; + mozilla::UniquePtr<wchar_t[]> cmdLine(mozilla::MakeCommandLine( + mozilla::ArrayLength(args), const_cast<wchar_t**>(args), aArgsLength, + const_cast<wchar_t**>(aArgs))); + + PROCESS_INFORMATION pi; + STARTUPINFOW si = {sizeof(si)}; + si.dwFlags = STARTF_USESHOWWINDOW; + si.wShowWindow = SW_HIDE; + + if (!::CreateProcessW(exePath, cmdLine.get(), nullptr, nullptr, FALSE, 0, + nullptr, nullptr, &si, &pi)) { + HRESULT hr = HRESULT_FROM_WIN32(GetLastError()); + LOG_ERROR(hr); + return Err(LaunchExeErr()); + } + + nsAutoHandle process(pi.hProcess); + nsAutoHandle mainThread(pi.hThread); + + DWORD exitCode; + if (::WaitForSingleObject(process.get(), INFINITE) == WAIT_OBJECT_0 && + ::GetExitCodeProcess(process.get(), &exitCode)) { + return std::make_tuple(exitCode, std::move(cmdLine)); + } + + return Err(LaunchExeErr()); +} + +static bool LaunchPowershell(const wchar_t* command, + const wchar_t* powershellPath) { + const wchar_t* args[] = { + L"-NoProfile", // ensure nothing is monkeying with powershell + L"-c", + command, + }; + + return LaunchExecutable(powershellPath, mozilla::ArrayLength(args), args) + .map([](auto result) { + auto& [exitCode, regCmdLine] = result; + bool success = (exitCode == 0); + if (!success) { + LOG_ERROR_MESSAGE(L"%s returned failure exitCode %d", + regCmdLine.get(), exitCode); + } + return success; + }) + .unwrapOr(false); +} + +static bool FindPowershell(mozilla::UniquePtr<wchar_t[]>& powershellPath) { + auto exePath = mozilla::MakeUnique<wchar_t[]>(MAX_PATH + 1); + if (!ConstructSystem32Path(L"WindowsPowershell\\v1.0\\powershell.exe", + exePath.get(), MAX_PATH + 1)) { + LOG_ERROR_MESSAGE(L"Failed to construct path to powershell.exe"); + return false; + } + + powershellPath.swap(exePath); + return true; +} + /* * Set an association with a UserChoice key * @@ -165,7 +304,9 @@ static bool SetUserChoiceRegistry(const wchar_t* aExt, const wchar_t* aProgID, static bool SetUserChoice(const wchar_t* aExt, const wchar_t* aSid, const wchar_t* aProgID, bool inMsix) { if (inMsix) { - LOG_ERROR_MESSAGE(L"SetUserChoice does not work on MSIX builds."); + LOG_ERROR_MESSAGE( + L"SetUserChoice should not be called on MSIX builds. Call " + L"SetDefaultExtensionHandlersUserChoiceImplMsix instead."); return false; } @@ -298,6 +439,247 @@ nsresult SetDefaultExtensionHandlersUserChoice( return rv; } +/* + * Takes the list of file extension pairs and fills a list of program ids for + * each pair. + * + * @param aFileExtensions array of file association pairs to + * set as default, like `[ ".pdf", "FirefoxPDF" ]`. + */ +static nsresult GenerateProgramIDs(const nsTArray<nsString>& aFileExtensions, + nsTArray<nsString>& progIDs) { + for (size_t i = 0; i + 1 < aFileExtensions.Length(); i += 2) { + const wchar_t* fileExtension = aFileExtensions[i].get(); + + // Formatting the ProgID here prevents using this helper to target arbitrary + // ProgIDs. + mozilla::UniquePtr<wchar_t[]> progID; + nsresult rv = GetMsixProgId(fileExtension, progID); + if (NS_FAILED(rv)) { + LOG_ERROR_MESSAGE(L"Failed to retrieve MSIX progID for %s", + fileExtension); + return rv; + } + + progIDs.AppendElement(nsString(progID.get())); + } + + return NS_OK; +} + +/* + * Takes the list of file extension pairs and a matching list of program ids for + * each of those pairs and verifies that the system is successfully to match + * each. + * + * @param aFileExtensions array of file association pairs to set as default, + * like `[ ".pdf", "FirefoxPDF" ]`. + * @param aProgIDs array of program ids. The order of this array matches the + * file extensions parameter. + */ +static nsresult VerifyUserDefaults(const nsTArray<nsString>& aFileExtensions, + const nsTArray<nsString>& aProgIDs) { + for (size_t i = 0; i + 1 < aFileExtensions.Length(); i += 2) { + const wchar_t* fileExtension = aFileExtensions[i].get(); + + if (!VerifyUserDefault(fileExtension, aProgIDs[i / 2].get())) { + return NS_ERROR_WDBA_REJECTED; + } + } + + return NS_OK; +} + +/* + * Queries the system for the minimum resolution (or tick time) in milliseconds + * that can be used with ::Sleep. If a Sleep is not triggered with a time + * divisible by the tick time, control can return to the thread before the time + * specified to Sleep. + * + * @param defaultValue what to return if the system query fails. + */ +static UINT GetSystemSleepIntervalInMilliseconds(UINT defaultValue) { + TIMECAPS timeCapabilities; + bool timeCapsFetchSuccessful = + (MMSYSERR_NOERROR == + timeGetDevCaps(&timeCapabilities, sizeof(timeCapabilities))); + if (!timeCapsFetchSuccessful) { + return defaultValue; + } + + return timeCapabilities.wPeriodMin > 0 ? timeCapabilities.wPeriodMin + : defaultValue; +} + +/* + * MSIX implementation for SetDefaultExtensionHandlersUserChoice. + * + * Due to the fact that MSIX builds run in a virtual, walled off environment, + * calling into the Win32 registry APIs doesn't work to set registry keys. + * MSIX builds access a virtual registry. + * + * Sends a "script" via command line to Powershell to modify the registry + * in an executable that is outside of the walled garden MSIX FX package + * environment, which is the only way to do that so far. Only works + * on slightly older versions of Windows 11 (older than 23H2). + * + * This was originally done using calls to Reg.exe, but permissions can't + * be changed that way, requiring using Powershell to call into .Net functions + * directly. Launching Powershell is slow (on the order of a second on some + * systems) so this method jams the calls to do everything into one launch of + * Powershell, to make it as quick as possible. + * + */ +static nsresult SetDefaultExtensionHandlersUserChoiceImplMsix( + const wchar_t* aAumi, const wchar_t* const aSid, + const nsTArray<nsString>& aFileExtensions) { + mozilla::UniquePtr<wchar_t[]> exePath; + if (!FindPowershell(exePath)) { + LOG_ERROR_MESSAGE(L"Could not locate Powershell"); + return NS_ERROR_FAILURE; + } + + // The following is the start of the script that will be sent to Powershell. + // In includes a function; calls to the function get appended per file + // extension, done below. + nsString startScript( + uR"( +# Force exceptions to stop execution +$ErrorActionPreference = 'Stop' + +function Set-DefaultHandlerRegistry($Path, $ProgID, $Hash) { + $Path = "$Path\UserChoice" + $CurrentUser = [Microsoft.Win32.Registry]::CurrentUser + + # DeleteSubKey throws if we don't have sufficient permissions to delete key, + # signaling failure to launching process. + # + # Note: DeleteSubKeyTree fails when DENY permissions are set on key, whereas + # DeleteSubKey succeeds. + $CurrentUser.DeleteSubKey($Path, $false) + $key = $CurrentUser.CreateSubKey($Path) + + $StringType = [Microsoft.Win32.RegistryValueKind]::String + $key.SetValue('ProgID', $ProgID, $StringType) + $key.SetValue('Hash', $Hash, $StringType) +} + +)"); // Newlines in the above powershell script at the end are important!!! + + // NOTE!!!! CreateProcess / calling things on the command line has a character + // count limit. For CMD.exe, it's 8K. For CreateProcessW, it's technically + // 32K. I can't find documentation about Powershell, but think 8K is safe to + // assume as a good maximum. The above script is about 1000 characters, and we + // will append another 100 characters at most per extension, so we'd need to + // be setting about 70 handlers at once to worry about the theoretical limit. + // Which we won't do. So the length shouldn't be a problem. + if (aFileExtensions.Length() >= 70) { + LOG_ERROR_MESSAGE( + L"SetDefaultExtensionHandlersUserChoiceImplMsix can't cope with 70 or " + L"more file extensions at once. Please break it up into multiple calls " + L"with fewer extensions per call."); + return NS_ERROR_FAILURE; + } + + // NOTE!!!! + // User choice hashes have to be generated and written to the registry in the + // same minute. So we do everything we can upfront before we get to the hash + // generation, to ensure that hash generation and the call to Powershell to + // write to the registry has as much time as possible to run. + + // Program ID fetch / generation might be slow, so do that ahead of time. + nsTArray<nsString> progIDs; + nsresult rv = GenerateProgramIDs(aFileExtensions, progIDs); + NS_ENSURE_SUCCESS(rv, rv); + + nsString scriptBuffer; + + // Everyting in the loop below should succeed or fail reasonably fast (within + // 20 milliseconds or something well under a second) besides the Powershell + // call. The Powershell call itself will either fail or succeed and break out + // of the loop, so repeating 10 times should be fine and is mostly to allow + // for getting the timing right with the user choice hash generation happening + // in the same minute - meaning this should likely only happen twice through + // the loop at most. + for (int i = 0; i < 10; i++) { + // Pre-allocate the memory for the scriptBuffer upfront so that we don't + // have to keep allocating every time Append is called. + const int scriptBufferCapacity = 16 * 1024; + scriptBuffer = startScript; + scriptBuffer.SetCapacity(scriptBufferCapacity); + + SYSTEMTIME hashTimestamp; + ::GetSystemTime(&hashTimestamp); + + // Time critical stuff starts here: + + for (size_t i = 0; i + 1 < aFileExtensions.Length(); i += 2) { + const wchar_t* fileExtension = aFileExtensions[i].get(); + + nsAutoString keyPath; + AppendAssociationKeyPath(fileExtension, keyPath); + + auto hashWchar = GenerateUserChoiceHash( + fileExtension, aSid, progIDs[i / 2].get(), hashTimestamp); + if (!hashWchar) { + return NS_ERROR_FAILURE; + } + auto hash = nsDependentString(hashWchar.get()); + + // Append a line to the script buffer in the form: + // Set-DefaultHandlerRegistry $RegistryKeyPath $ProgID $UserChoiceHash + scriptBuffer += u"Set-DefaultHandlerRegistry "_ns + keyPath + u" "_ns + + progIDs[i / 2] + u" "_ns + hash + u"\n"_ns; + } + + // The hash changes at the end of each minute, so check that the hash should + // be the same by the time we're done writing. + const ULONGLONG kWriteTimingThresholdMilliseconds = 2000; + + // Generating the hash could have taken some time, so figure out what time + // we are at right now. + SYSTEMTIME writeEndTimestamp; + ::GetSystemTime(&writeEndTimestamp); + + // Check if we have enough time to launch Powershell or if we should sleep + // to the next minute and try again + auto millisecondsLeftUntilNextMinute = + GetMillisecondsToNextMinute(hashTimestamp, writeEndTimestamp); + if (millisecondsLeftUntilNextMinute >= kWriteTimingThresholdMilliseconds) { + break; + } + + LOG_ERROR_MESSAGE( + L"Hash is too close to next minute, sleeping until next minute to " + L"ensure that hash generation matches write to registry."); + + UINT sleepUntilNextMinuteBufferMilliseconds = + GetSystemSleepIntervalInMilliseconds( + 50); // Default to 50ms if we can't figure out the right interval + // to sleep for + ::Sleep(millisecondsLeftUntilNextMinute + + (sleepUntilNextMinuteBufferMilliseconds * 2)); + + // Try again, if we have any attempts left + } + + // Call Powershell to set the registry keys now - this is the really time + // consuming thing 250ms to launch on a VM in a reasonably fast case, possibly + // a lot slower on other systems + bool powershellSuccessful = + LaunchPowershell(scriptBuffer.get(), exePath.get()); + if (!powershellSuccessful) { + // If powershell failed, it likely means that something got mucked with + // the registry and that Windows is popping up notifications to the user, + // so don't try again right now, so as to not overwhelm the user and annoy + // them. + return NS_ERROR_FAILURE; + } + + // Validate now + return VerifyUserDefaults(aFileExtensions, progIDs); +} + nsresult SetDefaultExtensionHandlersUserChoiceImpl( const wchar_t* aAumi, const wchar_t* const aSid, const nsTArray<nsString>& aFileExtensions) { @@ -306,9 +688,8 @@ nsresult SetDefaultExtensionHandlersUserChoiceImpl( GetCurrentPackageFullName(&pfnLen, nullptr) != APPMODEL_ERROR_NO_PACKAGE; if (inMsix) { - // MSIX packages can not meaningfully modify the registry keys related to - // default handlers - return NS_ERROR_FAILURE; + return SetDefaultExtensionHandlersUserChoiceImplMsix(aAumi, aSid, + aFileExtensions); } for (size_t i = 0; i + 1 < aFileExtensions.Length(); i += 2) { |