diff options
Diffstat (limited to 'toolkit/components/maintenanceservice')
14 files changed, 2529 insertions, 0 deletions
diff --git a/toolkit/components/maintenanceservice/Makefile.in b/toolkit/components/maintenanceservice/Makefile.in new file mode 100644 index 000000000..b07afbb0a --- /dev/null +++ b/toolkit/components/maintenanceservice/Makefile.in @@ -0,0 +1,13 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +ifndef MOZ_WINCONSOLE +ifdef MOZ_DEBUG +MOZ_WINCONSOLE = 1 +else +MOZ_WINCONSOLE = 0 +endif +endif + +include $(topsrcdir)/config/rules.mk diff --git a/toolkit/components/maintenanceservice/bootstrapinstaller/maintenanceservice_installer.nsi b/toolkit/components/maintenanceservice/bootstrapinstaller/maintenanceservice_installer.nsi new file mode 100644 index 000000000..9e831dc9c --- /dev/null +++ b/toolkit/components/maintenanceservice/bootstrapinstaller/maintenanceservice_installer.nsi @@ -0,0 +1,278 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +; Set verbosity to 3 (e.g. no script) to lessen the noise in the build logs +!verbose 3 + +; 7-Zip provides better compression than the lzma from NSIS so we add the files +; uncompressed and use 7-Zip to create a SFX archive of it +SetDatablockOptimize on +SetCompress off +CRCCheck on + +RequestExecutionLevel admin + +; The commands inside this ifdef require NSIS 3.0a2 or greater so the ifdef can +; be removed after we require NSIS 3.0a2 or greater. +!ifdef NSIS_PACKEDVERSION + Unicode true + ManifestSupportedOS all + ManifestDPIAware true +!endif + +!addplugindir ./ + +; Variables +Var TempMaintServiceName +Var BrandFullNameDA +Var BrandFullName + +; Other included files may depend upon these includes! +; The following includes are provided by NSIS. +!include FileFunc.nsh +!include LogicLib.nsh +!include MUI.nsh +!include WinMessages.nsh +!include WinVer.nsh +!include WordFunc.nsh + +!insertmacro GetOptions +!insertmacro GetParameters +!insertmacro GetSize + +; The test slaves use this fallback key to run tests. +; And anyone that wants to run tests themselves should already have +; this installed. +!define FallbackKey \ + "SOFTWARE\Mozilla\MaintenanceService\3932ecacee736d366d6436db0f55bce4" + +!define CompanyName "Mozilla Corporation" +!define BrandFullNameInternal "" + +; The following includes are custom. +!include defines.nsi +; We keep defines.nsi defined so that we get other things like +; the version number, but we redefine BrandFullName +!define MaintFullName "Mozilla Maintenance Service" +!undef BrandFullName +!define BrandFullName "${MaintFullName}" + +!include common.nsh +!include locales.nsi + +VIAddVersionKey "FileDescription" "${MaintFullName} Installer" +VIAddVersionKey "OriginalFilename" "maintenanceservice_installer.exe" + +Name "${MaintFullName}" +OutFile "maintenanceservice_installer.exe" + +; Get installation folder from registry if available +InstallDirRegKey HKLM "Software\Mozilla\MaintenanceService" "" + +SetOverwrite on + +!define MaintUninstallKey \ + "Software\Microsoft\Windows\CurrentVersion\Uninstall\MozillaMaintenanceService" + +; Always install into the 32-bit location even if we have a 64-bit build. +; This is because we use only 1 service for all Firefox channels. +; Allow either x86 and x64 builds to exist at this location, depending on +; what is the latest build. +InstallDir "$PROGRAMFILES32\${MaintFullName}\" +ShowUnInstDetails nevershow + +################################################################################ +# Modern User Interface - MUI + +!define MUI_ICON setup.ico +!define MUI_UNICON setup.ico +!define MUI_WELCOMEPAGE_TITLE_3LINES +!define MUI_UNWELCOMEFINISHPAGE_BITMAP wizWatermark.bmp + +;Interface Settings +!define MUI_ABORTWARNING + +; Uninstaller Pages +!insertmacro MUI_UNPAGE_CONFIRM +!insertmacro MUI_UNPAGE_INSTFILES + +################################################################################ +# Language + +!insertmacro MOZ_MUI_LANGUAGE 'baseLocale' +!verbose push +!verbose 3 +!include "overrideLocale.nsh" +!include "customLocale.nsh" +!verbose pop + +; Set this after the locale files to override it if it is in the locale +; using " " for BrandingText will hide the "Nullsoft Install System..." branding +BrandingText " " + +Function .onInit + ; Remove the current exe directory from the search order. + ; This only effects LoadLibrary calls and not implicitly loaded DLLs. + System::Call 'kernel32::SetDllDirectoryW(w "")' + + SetSilent silent + ; On Windows 2000 we do not install the maintenance service. + ; We won't run this installer from the parent installer, but just in case + ; someone tries to execute it on Windows 2000... + ${Unless} ${AtLeastWinXP} + Abort + ${EndUnless} +FunctionEnd + +Function un.onInit + ; Remove the current exe directory from the search order. + ; This only effects LoadLibrary calls and not implicitly loaded DLLs. + System::Call 'kernel32::SetDllDirectoryW(w "")' + +; The commands inside this ifndef are needed prior to NSIS 3.0a2 and can be +; removed after we require NSIS 3.0a2 or greater. +!ifndef NSIS_PACKEDVERSION + ${If} ${AtLeastWinVista} + System::Call 'user32::SetProcessDPIAware()' + ${EndIf} +!endif + + StrCpy $BrandFullNameDA "${MaintFullName}" + StrCpy $BrandFullName "${MaintFullName}" +FunctionEnd + +Section "MaintenanceService" + AllowSkipFiles off + + CreateDirectory $INSTDIR + SetOutPath $INSTDIR + + ; If the service already exists, then it will be stopped when upgrading it + ; via the maintenanceservice_tmp.exe command executed below. + ; The maintenanceservice_tmp.exe command will rename the file to + ; maintenanceservice.exe if maintenanceservice_tmp.exe is newer. + ; If the service does not exist yet, we install it and drop the file on + ; disk as maintenanceservice.exe directly. + StrCpy $TempMaintServiceName "maintenanceservice.exe" + IfFileExists "$INSTDIR\maintenanceservice.exe" 0 skipAlreadyExists + StrCpy $TempMaintServiceName "maintenanceservice_tmp.exe" + skipAlreadyExists: + + ; We always write out a copy and then decide whether to install it or + ; not via calling its 'install' cmdline which works by version comparison. + CopyFiles "$EXEDIR\maintenanceservice.exe" "$INSTDIR\$TempMaintServiceName" + + ; The updater.ini file is only used when performing an install or upgrade, + ; and only if that install or upgrade is successful. If an old updater.ini + ; happened to be copied into the maintenance service installation directory + ; but the service was not newer, the updater.ini file would be unused. + ; It is used to fill the description of the service on success. + CopyFiles "$EXEDIR\updater.ini" "$INSTDIR\updater.ini" + + ; Install the application maintenance service. + ; If a service already exists, the command line parameter will stop the + ; service and only install itself if it is newer than the already installed + ; service. If successful it will remove the old maintenanceservice.exe + ; and replace it with maintenanceservice_tmp.exe. + ClearErrors + ;${GetParameters} $0 + ;${GetOptions} "$0" "/Upgrade" $0 + ;${If} ${Errors} + ExecWait '"$INSTDIR\$TempMaintServiceName" forceinstall' + ;${Else} + ; The upgrade cmdline is the same as install except + ; It will fail if the service isn't already installed. + ; ExecWait '"$INSTDIR\$TempMaintServiceName" upgrade' + ;${EndIf} + + WriteUninstaller "$INSTDIR\Uninstall.exe" + WriteRegStr HKLM "${MaintUninstallKey}" "DisplayName" "${MaintFullName}" + WriteRegStr HKLM "${MaintUninstallKey}" "UninstallString" \ + '"$INSTDIR\uninstall.exe"' + WriteRegStr HKLM "${MaintUninstallKey}" "DisplayIcon" \ + "$INSTDIR\Uninstall.exe,0" + WriteRegStr HKLM "${MaintUninstallKey}" "DisplayVersion" "${AppVersion}" + WriteRegStr HKLM "${MaintUninstallKey}" "Publisher" "Mozilla" + WriteRegStr HKLM "${MaintUninstallKey}" "Comments" "${BrandFullName}" + WriteRegDWORD HKLM "${MaintUninstallKey}" "NoModify" 1 + ${GetSize} "$INSTDIR" "/S=0K" $R2 $R3 $R4 + WriteRegDWORD HKLM "${MaintUninstallKey}" "EstimatedSize" $R2 + + ; Write out that a maintenance service was attempted. + ; We do this because on upgrades we will check this value and we only + ; want to install once on the first upgrade to maintenance service. + ; Also write out that we are currently installed, preferences will check + ; this value to determine if we should show the service update pref. + ; Since the Maintenance service can be installed either x86 or x64, + ; always use the 64-bit registry for checking if an attempt was made. + ${If} ${RunningX64} + SetRegView 64 + ${EndIf} + WriteRegDWORD HKLM "Software\Mozilla\MaintenanceService" "Attempted" 1 + WriteRegDWORD HKLM "Software\Mozilla\MaintenanceService" "Installed" 1 + DeleteRegValue HKLM "Software\Mozilla\MaintenanceService" "FFPrefetchDisabled" + + ; Included here for debug purposes only. + ; These keys are used to bypass the installation dir is a valid installation + ; check from the service so that tests can be run. + WriteRegStr HKLM "${FallbackKey}\0" "name" "Mozilla Corporation" + WriteRegStr HKLM "${FallbackKey}\0" "issuer" "DigiCert SHA2 Assured ID Code Signing CA" + WriteRegStr HKLM "${FallbackKey}\1" "name" "Mozilla Fake SPC" + WriteRegStr HKLM "${FallbackKey}\1" "issuer" "Mozilla Fake CA" + ${If} ${RunningX64} + SetRegView lastused + ${EndIf} +SectionEnd + +; By renaming before deleting we improve things slightly in case +; there is a file in use error. In this case a new install can happen. +Function un.RenameDelete + Pop $9 + ; If the .moz-delete file already exists previously, delete it + ; If it doesn't exist, the call is ignored. + ; We don't need to pass /REBOOTOK here since it was already marked that way + ; if it exists. + Delete "$9.moz-delete" + Rename "$9" "$9.moz-delete" + ${If} ${Errors} + Delete /REBOOTOK "$9" + ${Else} + Delete /REBOOTOK "$9.moz-delete" + ${EndIf} + ClearErrors +FunctionEnd + +Section "Uninstall" + ; Delete the service so that no updates will be attempted + ExecWait '"$INSTDIR\maintenanceservice.exe" uninstall' + + Push "$INSTDIR\updater.ini" + Call un.RenameDelete + Push "$INSTDIR\maintenanceservice.exe" + Call un.RenameDelete + Push "$INSTDIR\maintenanceservice_tmp.exe" + Call un.RenameDelete + Push "$INSTDIR\maintenanceservice.old" + Call un.RenameDelete + Push "$INSTDIR\Uninstall.exe" + Call un.RenameDelete + Push "$INSTDIR\update\updater.ini" + Call un.RenameDelete + Push "$INSTDIR\update\updater.exe" + Call un.RenameDelete + RMDir /REBOOTOK "$INSTDIR\update" + RMDir /REBOOTOK "$INSTDIR" + DeleteRegKey HKLM "${MaintUninstallKey}" + + ${If} ${RunningX64} + SetRegView 64 + ${EndIf} + DeleteRegValue HKLM "Software\Mozilla\MaintenanceService" "Installed" + DeleteRegValue HKLM "Software\Mozilla\MaintenanceService" "FFPrefetchDisabled" + DeleteRegKey HKLM "${FallbackKey}\" + ${If} ${RunningX64} + SetRegView lastused + ${EndIf} +SectionEnd + diff --git a/toolkit/components/maintenanceservice/maintenanceservice.cpp b/toolkit/components/maintenanceservice/maintenanceservice.cpp new file mode 100644 index 000000000..f1275b095 --- /dev/null +++ b/toolkit/components/maintenanceservice/maintenanceservice.cpp @@ -0,0 +1,391 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include <windows.h> +#include <shlwapi.h> +#include <stdio.h> +#include <wchar.h> +#include <shlobj.h> + +#include "serviceinstall.h" +#include "maintenanceservice.h" +#include "servicebase.h" +#include "workmonitor.h" +#include "uachelper.h" +#include "updatehelper.h" +#include "registrycertificates.h" + +// Link w/ subsystem window so we don't get a console when executing +// this binary through the installer. +#pragma comment(linker, "/SUBSYSTEM:windows") + +SERVICE_STATUS gSvcStatus = { 0 }; +SERVICE_STATUS_HANDLE gSvcStatusHandle = nullptr; +HANDLE gWorkDoneEvent = nullptr; +HANDLE gThread = nullptr; +bool gServiceControlStopping = false; + +// logs are pretty small, about 20 lines, so 10 seems reasonable. +#define LOGS_TO_KEEP 10 + +BOOL GetLogDirectoryPath(WCHAR *path); + +int +wmain(int argc, WCHAR **argv) +{ + // If command-line parameter is "install", install the service + // or upgrade if already installed + // If command line parameter is "forceinstall", install the service + // even if it is older than what is already installed. + // If command-line parameter is "upgrade", upgrade the service + // but do not install it if it is not already installed. + // If command line parameter is "uninstall", uninstall the service. + // Otherwise, the service is probably being started by the SCM. + bool forceInstall = !lstrcmpi(argv[1], L"forceinstall"); + if (!lstrcmpi(argv[1], L"install") || forceInstall) { + WCHAR updatePath[MAX_PATH + 1]; + if (GetLogDirectoryPath(updatePath)) { + LogInit(updatePath, L"maintenanceservice-install.log"); + } + + SvcInstallAction action = InstallSvc; + if (forceInstall) { + action = ForceInstallSvc; + LOG(("Installing service with force specified...")); + } else { + LOG(("Installing service...")); + } + + bool ret = SvcInstall(action); + if (!ret) { + LOG_WARN(("Could not install service. (%d)", GetLastError())); + LogFinish(); + return 1; + } + + LOG(("The service was installed successfully")); + LogFinish(); + return 0; + } + + if (!lstrcmpi(argv[1], L"upgrade")) { + WCHAR updatePath[MAX_PATH + 1]; + if (GetLogDirectoryPath(updatePath)) { + LogInit(updatePath, L"maintenanceservice-install.log"); + } + + LOG(("Upgrading service if installed...")); + if (!SvcInstall(UpgradeSvc)) { + LOG_WARN(("Could not upgrade service. (%d)", GetLastError())); + LogFinish(); + return 1; + } + + LOG(("The service was upgraded successfully")); + LogFinish(); + return 0; + } + + if (!lstrcmpi(argv[1], L"uninstall")) { + WCHAR updatePath[MAX_PATH + 1]; + if (GetLogDirectoryPath(updatePath)) { + LogInit(updatePath, L"maintenanceservice-uninstall.log"); + } + LOG(("Uninstalling service...")); + if (!SvcUninstall()) { + LOG_WARN(("Could not uninstall service. (%d)", GetLastError())); + LogFinish(); + return 1; + } + LOG(("The service was uninstalled successfully")); + LogFinish(); + return 0; + } + + if (!lstrcmpi(argv[1], L"check-cert") && argc > 2) { + return DoesBinaryMatchAllowedCertificates(argv[2], argv[3], FALSE) ? 0 : 1; + } + + SERVICE_TABLE_ENTRYW DispatchTable[] = { + { SVC_NAME, (LPSERVICE_MAIN_FUNCTIONW) SvcMain }, + { nullptr, nullptr } + }; + + // This call returns when the service has stopped. + // The process should simply terminate when the call returns. + if (!StartServiceCtrlDispatcherW(DispatchTable)) { + LOG_WARN(("StartServiceCtrlDispatcher failed. (%d)", GetLastError())); + } + + return 0; +} + +/** + * Obtains the base path where logs should be stored + * + * @param path The out buffer for the backup log path of size MAX_PATH + 1 + * @return TRUE if successful. + */ +BOOL +GetLogDirectoryPath(WCHAR *path) +{ + if (!GetModuleFileNameW(nullptr, path, MAX_PATH)) { + return FALSE; + } + + if (!PathRemoveFileSpecW(path)) { + return FALSE; + } + + if (!PathAppendSafe(path, L"logs")) { + return FALSE; + } + CreateDirectoryW(path, nullptr); + return TRUE; +} + +/** + * Calculated a backup path based on the log number. + * + * @param path The out buffer to store the log path of size MAX_PATH + 1 + * @param basePath The base directory where the calculated path should go + * @param logNumber The log number, 0 == updater.log + * @return TRUE if successful. + */ +BOOL +GetBackupLogPath(LPWSTR path, LPCWSTR basePath, int logNumber) +{ + WCHAR logName[64] = { L'\0' }; + wcsncpy(path, basePath, sizeof(logName) / sizeof(logName[0]) - 1); + if (logNumber <= 0) { + swprintf(logName, sizeof(logName) / sizeof(logName[0]), + L"maintenanceservice.log"); + } else { + swprintf(logName, sizeof(logName) / sizeof(logName[0]), + L"maintenanceservice-%d.log", logNumber); + } + return PathAppendSafe(path, logName); +} + +/** + * Moves the old log files out of the way before a new one is written. + * If you for example keep 3 logs, then this function will do: + * updater2.log -> updater3.log + * updater1.log -> updater2.log + * updater.log -> updater1.log + * Which clears room for a new updater.log in the basePath directory + * + * @param basePath The base directory path where log files are stored + * @param numLogsToKeep The number of logs to keep + */ +void +BackupOldLogs(LPCWSTR basePath, int numLogsToKeep) +{ + WCHAR oldPath[MAX_PATH + 1]; + WCHAR newPath[MAX_PATH + 1]; + for (int i = numLogsToKeep; i >= 1; i--) { + if (!GetBackupLogPath(oldPath, basePath, i -1)) { + continue; + } + + if (!GetBackupLogPath(newPath, basePath, i)) { + continue; + } + + if (!MoveFileExW(oldPath, newPath, MOVEFILE_REPLACE_EXISTING)) { + continue; + } + } +} + +/** + * Ensures the service is shutdown once all work is complete. + * There is an issue on XP SP2 and below where the service can hang + * in a stop pending state even though the SCM is notified of a stopped + * state. Control *should* be returned to StartServiceCtrlDispatcher from the + * call to SetServiceStatus on a stopped state in the wmain thread. + * Sometimes this is not the case though. This thread will terminate the process + * if it has been 5 seconds after all work is done and the process is still not + * terminated. This thread is only started once a stopped state was sent to the + * SCM. The stop pending hang can be reproduced intermittently even if you set + * a stopped state dirctly and never set a stop pending state. It is safe to + * forcefully terminate the process ourselves since all work is done once we + * start this thread. +*/ +DWORD WINAPI +EnsureProcessTerminatedThread(LPVOID) +{ + Sleep(5000); + exit(0); + return 0; +} + +void +StartTerminationThread() +{ + // If the process does not self terminate like it should, this thread + // will terminate the process after 5 seconds. + HANDLE thread = CreateThread(nullptr, 0, EnsureProcessTerminatedThread, + nullptr, 0, nullptr); + if (thread) { + CloseHandle(thread); + } +} + +/** + * Main entry point when running as a service. + */ +void WINAPI +SvcMain(DWORD argc, LPWSTR *argv) +{ + // Setup logging, and backup the old logs + WCHAR updatePath[MAX_PATH + 1]; + if (GetLogDirectoryPath(updatePath)) { + BackupOldLogs(updatePath, LOGS_TO_KEEP); + LogInit(updatePath, L"maintenanceservice.log"); + } + + // Disable every privilege we don't need. Processes started using + // CreateProcess will use the same token as this process. + UACHelper::DisablePrivileges(nullptr); + + // Register the handler function for the service + gSvcStatusHandle = RegisterServiceCtrlHandlerW(SVC_NAME, SvcCtrlHandler); + if (!gSvcStatusHandle) { + LOG_WARN(("RegisterServiceCtrlHandler failed. (%d)", GetLastError())); + ExecuteServiceCommand(argc, argv); + LogFinish(); + exit(1); + } + + // These values will be re-used later in calls involving gSvcStatus + gSvcStatus.dwServiceType = SERVICE_WIN32_OWN_PROCESS; + gSvcStatus.dwServiceSpecificExitCode = 0; + + // Report initial status to the SCM + ReportSvcStatus(SERVICE_START_PENDING, NO_ERROR, 3000); + + // This event will be used to tell the SvcCtrlHandler when the work is + // done for when a stop comamnd is manually issued. + gWorkDoneEvent = CreateEvent(nullptr, TRUE, FALSE, nullptr); + if (!gWorkDoneEvent) { + ReportSvcStatus(SERVICE_STOPPED, 1, 0); + StartTerminationThread(); + return; + } + + // Initialization complete and we're about to start working on + // the actual command. Report the service state as running to the SCM. + ReportSvcStatus(SERVICE_RUNNING, NO_ERROR, 0); + + // The service command was executed, stop logging and set an event + // to indicate the work is done in case someone is waiting on a + // service stop operation. + ExecuteServiceCommand(argc, argv); + LogFinish(); + + SetEvent(gWorkDoneEvent); + + // If we aren't already in a stopping state then tell the SCM we're stopped + // now. If we are already in a stopping state then the SERVICE_STOPPED state + // will be set by the SvcCtrlHandler. + if (!gServiceControlStopping) { + ReportSvcStatus(SERVICE_STOPPED, NO_ERROR, 0); + StartTerminationThread(); + } +} + +/** + * Sets the current service status and reports it to the SCM. + * + * @param currentState The current state (see SERVICE_STATUS) + * @param exitCode The system error code + * @param waitHint Estimated time for pending operation in milliseconds + */ +void +ReportSvcStatus(DWORD currentState, + DWORD exitCode, + DWORD waitHint) +{ + static DWORD dwCheckPoint = 1; + + gSvcStatus.dwCurrentState = currentState; + gSvcStatus.dwWin32ExitCode = exitCode; + gSvcStatus.dwWaitHint = waitHint; + + if (SERVICE_START_PENDING == currentState || + SERVICE_STOP_PENDING == currentState) { + gSvcStatus.dwControlsAccepted = 0; + } else { + gSvcStatus.dwControlsAccepted = SERVICE_ACCEPT_STOP | + SERVICE_ACCEPT_SHUTDOWN; + } + + if ((SERVICE_RUNNING == currentState) || + (SERVICE_STOPPED == currentState)) { + gSvcStatus.dwCheckPoint = 0; + } else { + gSvcStatus.dwCheckPoint = dwCheckPoint++; + } + + // Report the status of the service to the SCM. + SetServiceStatus(gSvcStatusHandle, &gSvcStatus); +} + +/** + * Since the SvcCtrlHandler should only spend at most 30 seconds before + * returning, this function does the service stop work for the SvcCtrlHandler. +*/ +DWORD WINAPI +StopServiceAndWaitForCommandThread(LPVOID) +{ + do { + ReportSvcStatus(SERVICE_STOP_PENDING, NO_ERROR, 1000); + } while(WaitForSingleObject(gWorkDoneEvent, 100) == WAIT_TIMEOUT); + CloseHandle(gWorkDoneEvent); + gWorkDoneEvent = nullptr; + ReportSvcStatus(SERVICE_STOPPED, NO_ERROR, 0); + StartTerminationThread(); + return 0; +} + +/** + * Called by SCM whenever a control code is sent to the service + * using the ControlService function. + */ +void WINAPI +SvcCtrlHandler(DWORD dwCtrl) +{ + // After a SERVICE_CONTROL_STOP there should be no more commands sent to + // the SvcCtrlHandler. + if (gServiceControlStopping) { + return; + } + + // Handle the requested control code. + switch(dwCtrl) { + case SERVICE_CONTROL_SHUTDOWN: + case SERVICE_CONTROL_STOP: { + gServiceControlStopping = true; + ReportSvcStatus(SERVICE_STOP_PENDING, NO_ERROR, 1000); + + // The SvcCtrlHandler thread should not spend more than 30 seconds in + // shutdown so we spawn a new thread for stopping the service + HANDLE thread = CreateThread(nullptr, 0, + StopServiceAndWaitForCommandThread, + nullptr, 0, nullptr); + if (thread) { + CloseHandle(thread); + } else { + // Couldn't start the thread so just call the stop ourselves. + // If it happens to take longer than 30 seconds the caller will + // get an error. + StopServiceAndWaitForCommandThread(nullptr); + } + } + break; + default: + break; + } +} diff --git a/toolkit/components/maintenanceservice/maintenanceservice.exe.manifest b/toolkit/components/maintenanceservice/maintenanceservice.exe.manifest new file mode 100644 index 000000000..cb317c47d --- /dev/null +++ b/toolkit/components/maintenanceservice/maintenanceservice.exe.manifest @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="UTF-8" standalone="yes"?> +<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0"> +<assemblyIdentity + version="1.0.0.0" + processorArchitecture="*" + name="MaintenanceService" + type="win32" +/> +<description>MaintenanceService</description> +<ms_asmv3:trustInfo xmlns:ms_asmv3="urn:schemas-microsoft-com:asm.v3"> + <ms_asmv3:security> + <ms_asmv3:requestedPrivileges> + <ms_asmv3:requestedExecutionLevel level="requireAdministrator" uiAccess="false" /> + </ms_asmv3:requestedPrivileges> + </ms_asmv3:security> +</ms_asmv3:trustInfo> + <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1"> + <application> + <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/> + <supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/> + <supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/> + <supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/> + <supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}"/> + </application> + </compatibility> +</assembly> diff --git a/toolkit/components/maintenanceservice/maintenanceservice.h b/toolkit/components/maintenanceservice/maintenanceservice.h new file mode 100644 index 000000000..9e02914a0 --- /dev/null +++ b/toolkit/components/maintenanceservice/maintenanceservice.h @@ -0,0 +1,10 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +void WINAPI SvcMain(DWORD dwArgc, LPWSTR *lpszArgv); +void SvcInit(DWORD dwArgc, LPWSTR *lpszArgv); +void WINAPI SvcCtrlHandler(DWORD dwCtrl); +void ReportSvcStatus(DWORD dwCurrentState, + DWORD dwWin32ExitCode, + DWORD dwWaitHint); diff --git a/toolkit/components/maintenanceservice/maintenanceservice.rc b/toolkit/components/maintenanceservice/maintenanceservice.rc new file mode 100644 index 000000000..ddd3e942b --- /dev/null +++ b/toolkit/components/maintenanceservice/maintenanceservice.rc @@ -0,0 +1,86 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Microsoft Visual C++ generated resource script. +// +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winresrc.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (U.S.) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +#ifdef _WIN32 +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US +#pragma code_page(1252) +#endif //_WIN32 + +///////////////////////////////////////////////////////////////////////////// +// +// RT_MANIFEST +// + +1 RT_MANIFEST "maintenanceservice.exe.manifest" + +///////////////////////////////////////////////////////////////////////////// +// +// DESIGNINFO +// + +#ifdef APSTUDIO_INVOKED +GUIDELINES DESIGNINFO +BEGIN +END +#endif // APSTUDIO_INVOKED + + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winresrc.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + +#endif // English (U.S.) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED + diff --git a/toolkit/components/maintenanceservice/moz.build b/toolkit/components/maintenanceservice/moz.build new file mode 100644 index 000000000..2c54c8c76 --- /dev/null +++ b/toolkit/components/maintenanceservice/moz.build @@ -0,0 +1,54 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +Program('maintenanceservice') + +SOURCES += [ + 'maintenanceservice.cpp', + 'servicebase.cpp', + 'serviceinstall.cpp', + 'workmonitor.cpp', +] + +if CONFIG['OS_ARCH'] == 'WINNT': + USE_LIBS += [ + 'updatecommon-standalone', + ] +else: + USE_LIBS += [ + 'updatecommon', + ] + +# For debugging purposes only +#DEFINES['DISABLE_UPDATER_AUTHENTICODE_CHECK'] = True + +DEFINES['UNICODE'] = True +DEFINES['_UNICODE'] = True +DEFINES['NS_NO_XPCOM'] = True + +# Pick up nsWindowsRestart.cpp +LOCAL_INCLUDES += [ + '/toolkit/mozapps/update/common', + '/toolkit/xre', +] + +USE_STATIC_LIBS = True + +if CONFIG['_MSC_VER']: + WIN32_EXE_LDFLAGS += ['-ENTRY:wmainCRTStartup'] + +RCINCLUDE = 'maintenanceservice.rc' + +DISABLE_STL_WRAPPING = True + +OS_LIBS += [ + 'comctl32', + 'ws2_32', + 'shell32', +] + +with Files('**'): + BUG_COMPONENT = ('Toolkit', 'Application Update') diff --git a/toolkit/components/maintenanceservice/resource.h b/toolkit/components/maintenanceservice/resource.h new file mode 100644 index 000000000..45619457c --- /dev/null +++ b/toolkit/components/maintenanceservice/resource.h @@ -0,0 +1,20 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by updater.rc +// +#define IDI_DIALOG 1003 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1003 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/toolkit/components/maintenanceservice/servicebase.cpp b/toolkit/components/maintenanceservice/servicebase.cpp new file mode 100644 index 000000000..a858c4537 --- /dev/null +++ b/toolkit/components/maintenanceservice/servicebase.cpp @@ -0,0 +1,86 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "servicebase.h" +#include "nsWindowsHelpers.h" + +// Shared code between applications and updater.exe +#include "nsWindowsRestart.cpp" + +/** + * Verifies if 2 files are byte for byte equivalent. + * + * @param file1Path The first file to verify. + * @param file2Path The second file to verify. + * @param sameContent Out parameter, TRUE if the files are equal + * @return TRUE If there was no error checking the files. + */ +BOOL +VerifySameFiles(LPCWSTR file1Path, LPCWSTR file2Path, BOOL &sameContent) +{ + sameContent = FALSE; + nsAutoHandle file1(CreateFileW(file1Path, GENERIC_READ, FILE_SHARE_READ, + nullptr, OPEN_EXISTING, 0, nullptr)); + if (INVALID_HANDLE_VALUE == file1) { + return FALSE; + } + nsAutoHandle file2(CreateFileW(file2Path, GENERIC_READ, FILE_SHARE_READ, + nullptr, OPEN_EXISTING, 0, nullptr)); + if (INVALID_HANDLE_VALUE == file2) { + return FALSE; + } + + DWORD fileSize1 = GetFileSize(file1, nullptr); + DWORD fileSize2 = GetFileSize(file2, nullptr); + if (INVALID_FILE_SIZE == fileSize1 || INVALID_FILE_SIZE == fileSize2) { + return FALSE; + } + + if (fileSize1 != fileSize2) { + // sameContent is already set to FALSE + return TRUE; + } + + char buf1[COMPARE_BLOCKSIZE]; + char buf2[COMPARE_BLOCKSIZE]; + DWORD numBlocks = fileSize1 / COMPARE_BLOCKSIZE; + DWORD leftOver = fileSize1 % COMPARE_BLOCKSIZE; + DWORD readAmount; + for (DWORD i = 0; i < numBlocks; i++) { + if (!ReadFile(file1, buf1, COMPARE_BLOCKSIZE, &readAmount, nullptr) || + readAmount != COMPARE_BLOCKSIZE) { + return FALSE; + } + + if (!ReadFile(file2, buf2, COMPARE_BLOCKSIZE, &readAmount, nullptr) || + readAmount != COMPARE_BLOCKSIZE) { + return FALSE; + } + + if (memcmp(buf1, buf2, COMPARE_BLOCKSIZE)) { + // sameContent is already set to FALSE + return TRUE; + } + } + + if (leftOver) { + if (!ReadFile(file1, buf1, leftOver, &readAmount, nullptr) || + readAmount != leftOver) { + return FALSE; + } + + if (!ReadFile(file2, buf2, leftOver, &readAmount, nullptr) || + readAmount != leftOver) { + return FALSE; + } + + if (memcmp(buf1, buf2, leftOver)) { + // sameContent is already set to FALSE + return TRUE; + } + } + + sameContent = TRUE; + return TRUE; +} diff --git a/toolkit/components/maintenanceservice/servicebase.h b/toolkit/components/maintenanceservice/servicebase.h new file mode 100644 index 000000000..447981489 --- /dev/null +++ b/toolkit/components/maintenanceservice/servicebase.h @@ -0,0 +1,22 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include <windows.h> +#include "updatecommon.h" + +BOOL PathAppendSafe(LPWSTR base, LPCWSTR extra); +BOOL VerifySameFiles(LPCWSTR file1Path, LPCWSTR file2Path, BOOL &sameContent); + +// 32KiB for comparing files at a time seems reasonable. +// The bigger the better for speed, but this will be used +// on the stack so I don't want it to be too big. +#define COMPARE_BLOCKSIZE 32768 + +// The following string resource value is used to uniquely identify the signed +// Mozilla application as an updater. Before the maintenance service will +// execute the updater it must have this updater identity string in its string +// table. No other signed Mozilla product will have this string table value. +#define UPDATER_IDENTITY_STRING \ + "moz-updater.exe-4cdccec4-5ee0-4a06-9817-4cd899a9db49" +#define IDS_UPDATER_IDENTITY 1006 diff --git a/toolkit/components/maintenanceservice/serviceinstall.cpp b/toolkit/components/maintenanceservice/serviceinstall.cpp new file mode 100644 index 000000000..3b9522d47 --- /dev/null +++ b/toolkit/components/maintenanceservice/serviceinstall.cpp @@ -0,0 +1,759 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include <windows.h> +#include <aclapi.h> +#include <stdlib.h> +#include <shlwapi.h> + +// Used for DNLEN and UNLEN +#include <lm.h> + +#include <nsWindowsHelpers.h> +#include "mozilla/UniquePtr.h" + +#include "serviceinstall.h" +#include "servicebase.h" +#include "updatehelper.h" +#include "shellapi.h" +#include "readstrings.h" +#include "errors.h" + +#pragma comment(lib, "version.lib") + +// This uninstall key is defined originally in maintenanceservice_installer.nsi +#define MAINT_UNINSTALL_KEY L"Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\MozillaMaintenanceService" + +static BOOL +UpdateUninstallerVersionString(LPWSTR versionString) +{ + HKEY uninstallKey; + if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, + MAINT_UNINSTALL_KEY, 0, + KEY_WRITE | KEY_WOW64_32KEY, + &uninstallKey) != ERROR_SUCCESS) { + return FALSE; + } + + LONG rv = RegSetValueExW(uninstallKey, L"DisplayVersion", 0, REG_SZ, + reinterpret_cast<const BYTE *>(versionString), + (wcslen(versionString) + 1) * sizeof(WCHAR)); + RegCloseKey(uninstallKey); + return rv == ERROR_SUCCESS; +} + +/** + * A wrapper function to read strings for the maintenance service. + * + * @param path The path of the ini file to read from + * @param results The maintenance service strings that were read + * @return OK on success +*/ +static int +ReadMaintenanceServiceStrings(LPCWSTR path, + MaintenanceServiceStringTable *results) +{ + // Read in the maintenance service description string if specified. + const unsigned int kNumStrings = 1; + const char *kServiceKeys = "MozillaMaintenanceDescription\0"; + char serviceStrings[kNumStrings][MAX_TEXT_LEN]; + int result = ReadStrings(path, kServiceKeys, + kNumStrings, serviceStrings); + if (result != OK) { + serviceStrings[0][0] = '\0'; + } + strncpy(results->serviceDescription, + serviceStrings[0], MAX_TEXT_LEN - 1); + results->serviceDescription[MAX_TEXT_LEN - 1] = '\0'; + return result; +} + +/** + * Obtains the version number from the specified PE file's version information + * Version Format: A.B.C.D (Example 10.0.0.300) + * + * @param path The path of the file to check the version on + * @param A The first part of the version number + * @param B The second part of the version number + * @param C The third part of the version number + * @param D The fourth part of the version number + * @return TRUE if successful + */ +static BOOL +GetVersionNumberFromPath(LPWSTR path, DWORD &A, DWORD &B, + DWORD &C, DWORD &D) +{ + DWORD fileVersionInfoSize = GetFileVersionInfoSizeW(path, 0); + mozilla::UniquePtr<char[]> fileVersionInfo(new char[fileVersionInfoSize]); + if (!GetFileVersionInfoW(path, 0, fileVersionInfoSize, + fileVersionInfo.get())) { + LOG_WARN(("Could not obtain file info of old service. (%d)", + GetLastError())); + return FALSE; + } + + VS_FIXEDFILEINFO *fixedFileInfo = + reinterpret_cast<VS_FIXEDFILEINFO *>(fileVersionInfo.get()); + UINT size; + if (!VerQueryValueW(fileVersionInfo.get(), L"\\", + reinterpret_cast<LPVOID*>(&fixedFileInfo), &size)) { + LOG_WARN(("Could not query file version info of old service. (%d)", + GetLastError())); + return FALSE; + } + + A = HIWORD(fixedFileInfo->dwFileVersionMS); + B = LOWORD(fixedFileInfo->dwFileVersionMS); + C = HIWORD(fixedFileInfo->dwFileVersionLS); + D = LOWORD(fixedFileInfo->dwFileVersionLS); + return TRUE; +} + +/** + * Updates the service description with what is stored in updater.ini + * at the same path as the currently executing module binary. + * + * @param serviceHandle A handle to an opened service with + * SERVICE_CHANGE_CONFIG access right + * @param TRUE on succcess. +*/ +BOOL +UpdateServiceDescription(SC_HANDLE serviceHandle) +{ + WCHAR updaterINIPath[MAX_PATH + 1]; + if (!GetModuleFileNameW(nullptr, updaterINIPath, + sizeof(updaterINIPath) / + sizeof(updaterINIPath[0]))) { + LOG_WARN(("Could not obtain module filename when attempting to " + "modify service description. (%d)", GetLastError())); + return FALSE; + } + + if (!PathRemoveFileSpecW(updaterINIPath)) { + LOG_WARN(("Could not remove file spec when attempting to " + "modify service description. (%d)", GetLastError())); + return FALSE; + } + + if (!PathAppendSafe(updaterINIPath, L"updater.ini")) { + LOG_WARN(("Could not append updater.ini filename when attempting to " + "modify service description. (%d)", GetLastError())); + return FALSE; + } + + if (GetFileAttributesW(updaterINIPath) == INVALID_FILE_ATTRIBUTES) { + LOG_WARN(("updater.ini file does not exist, will not modify " + "service description. (%d)", GetLastError())); + return FALSE; + } + + MaintenanceServiceStringTable serviceStrings; + int rv = ReadMaintenanceServiceStrings(updaterINIPath, &serviceStrings); + if (rv != OK || !strlen(serviceStrings.serviceDescription)) { + LOG_WARN(("updater.ini file does not contain a maintenance " + "service description.")); + return FALSE; + } + + WCHAR serviceDescription[MAX_TEXT_LEN]; + if (!MultiByteToWideChar(CP_UTF8, 0, + serviceStrings.serviceDescription, -1, + serviceDescription, + sizeof(serviceDescription) / + sizeof(serviceDescription[0]))) { + LOG_WARN(("Could not convert description to wide string format. (%d)", + GetLastError())); + return FALSE; + } + + SERVICE_DESCRIPTIONW descriptionConfig; + descriptionConfig.lpDescription = serviceDescription; + if (!ChangeServiceConfig2W(serviceHandle, + SERVICE_CONFIG_DESCRIPTION, + &descriptionConfig)) { + LOG_WARN(("Could not change service config. (%d)", GetLastError())); + return FALSE; + } + + LOG(("The service description was updated successfully.")); + return TRUE; +} + +/** + * Determines if the MozillaMaintenance service path needs to be updated + * and fixes it if it is wrong. + * + * @param service A handle to the service to fix. + * @param currentServicePath The current (possibly wrong) path that is used. + * @param servicePathWasWrong Out parameter set to TRUE if a fix was needed. + * @return TRUE if the service path is now correct. +*/ +BOOL +FixServicePath(SC_HANDLE service, + LPCWSTR currentServicePath, + BOOL &servicePathWasWrong) +{ + // When we originally upgraded the MozillaMaintenance service we + // would uninstall the service on each upgrade. This had an + // intermittent error which could cause the service to use the file + // maintenanceservice_tmp.exe as the install path. Only a small number + // of Nightly users would be affected by this, but we check for this + // state here and fix the user if they are affected. + // + // We also fix the path in the case of the path not being quoted. + size_t currentServicePathLen = wcslen(currentServicePath); + bool doesServiceHaveCorrectPath = + currentServicePathLen > 2 && + !wcsstr(currentServicePath, L"maintenanceservice_tmp.exe") && + currentServicePath[0] == L'\"' && + currentServicePath[currentServicePathLen - 1] == L'\"'; + + if (doesServiceHaveCorrectPath) { + LOG(("The MozillaMaintenance service path is correct.")); + servicePathWasWrong = FALSE; + return TRUE; + } + // This is a recoverable situation so not logging as a warning + LOG(("The MozillaMaintenance path is NOT correct. It was: %ls", + currentServicePath)); + + servicePathWasWrong = TRUE; + WCHAR fixedPath[MAX_PATH + 1] = { L'\0' }; + wcsncpy(fixedPath, currentServicePath, MAX_PATH); + PathUnquoteSpacesW(fixedPath); + if (!PathRemoveFileSpecW(fixedPath)) { + LOG_WARN(("Couldn't remove file spec. (%d)", GetLastError())); + return FALSE; + } + if (!PathAppendSafe(fixedPath, L"maintenanceservice.exe")) { + LOG_WARN(("Couldn't append file spec. (%d)", GetLastError())); + return FALSE; + } + PathQuoteSpacesW(fixedPath); + + + if (!ChangeServiceConfigW(service, SERVICE_NO_CHANGE, SERVICE_NO_CHANGE, + SERVICE_NO_CHANGE, fixedPath, nullptr, nullptr, + nullptr, nullptr, nullptr, nullptr)) { + LOG_WARN(("Could not fix service path. (%d)", GetLastError())); + return FALSE; + } + + LOG(("Fixed service path to: %ls.", fixedPath)); + return TRUE; +} + +/** + * Installs or upgrades the SVC_NAME service. + * If an existing service is already installed, we replace it with the + * currently running process. + * + * @param action The action to perform. + * @return TRUE if the service was installed/upgraded + */ +BOOL +SvcInstall(SvcInstallAction action) +{ + // Get a handle to the local computer SCM database with full access rights. + nsAutoServiceHandle schSCManager(OpenSCManager(nullptr, nullptr, + SC_MANAGER_ALL_ACCESS)); + if (!schSCManager) { + LOG_WARN(("Could not open service manager. (%d)", GetLastError())); + return FALSE; + } + + WCHAR newServiceBinaryPath[MAX_PATH + 1]; + if (!GetModuleFileNameW(nullptr, newServiceBinaryPath, + sizeof(newServiceBinaryPath) / + sizeof(newServiceBinaryPath[0]))) { + LOG_WARN(("Could not obtain module filename when attempting to " + "install service. (%d)", + GetLastError())); + return FALSE; + } + + // Check if we already have the service installed. + nsAutoServiceHandle schService(OpenServiceW(schSCManager, + SVC_NAME, + SERVICE_ALL_ACCESS)); + DWORD lastError = GetLastError(); + if (!schService && ERROR_SERVICE_DOES_NOT_EXIST != lastError) { + // The service exists but we couldn't open it + LOG_WARN(("Could not open service. (%d)", GetLastError())); + return FALSE; + } + + if (schService) { + // The service exists but it may not have the correct permissions. + // This could happen if the permissions were not set correctly originally + // or have been changed after the installation. This will reset the + // permissions back to allow limited user accounts. + if (!SetUserAccessServiceDACL(schService)) { + LOG_WARN(("Could not reset security ACE on service handle. It might not be " + "possible to start the service. This error should never " + "happen. (%d)", GetLastError())); + } + + // The service exists and we opened it + DWORD bytesNeeded; + if (!QueryServiceConfigW(schService, nullptr, 0, &bytesNeeded) && + GetLastError() != ERROR_INSUFFICIENT_BUFFER) { + LOG_WARN(("Could not determine buffer size for query service config. (%d)", + GetLastError())); + return FALSE; + } + + // Get the service config information, in particular we want the binary + // path of the service. + mozilla::UniquePtr<char[]> serviceConfigBuffer(new char[bytesNeeded]); + if (!QueryServiceConfigW(schService, + reinterpret_cast<QUERY_SERVICE_CONFIGW*>(serviceConfigBuffer.get()), + bytesNeeded, &bytesNeeded)) { + LOG_WARN(("Could open service but could not query service config. (%d)", + GetLastError())); + return FALSE; + } + QUERY_SERVICE_CONFIGW &serviceConfig = + *reinterpret_cast<QUERY_SERVICE_CONFIGW*>(serviceConfigBuffer.get()); + + // Check if we need to fix the service path + BOOL servicePathWasWrong; + static BOOL alreadyCheckedFixServicePath = FALSE; + if (!alreadyCheckedFixServicePath) { + if (!FixServicePath(schService, serviceConfig.lpBinaryPathName, + servicePathWasWrong)) { + LOG_WARN(("Could not fix service path. This should never happen. (%d)", + GetLastError())); + // True is returned because the service is pointing to + // maintenanceservice_tmp.exe so it actually was upgraded to the + // newest installed service. + return TRUE; + } else if (servicePathWasWrong) { + // Now that the path is fixed we should re-attempt the install. + // This current process' image path is maintenanceservice_tmp.exe. + // The service used to point to maintenanceservice_tmp.exe. + // The service was just fixed to point to maintenanceservice.exe. + // Re-attempting an install from scratch will work as normal. + alreadyCheckedFixServicePath = TRUE; + LOG(("Restarting install action: %d", action)); + return SvcInstall(action); + } + } + + // Ensure the service path is not quoted. We own this memory and know it to + // be large enough for the quoted path, so it is large enough for the + // unquoted path. This function cannot fail. + PathUnquoteSpacesW(serviceConfig.lpBinaryPathName); + + // Obtain the existing maintenanceservice file's version number and + // the new file's version number. Versions are in the format of + // A.B.C.D. + DWORD existingA, existingB, existingC, existingD; + DWORD newA, newB, newC, newD; + BOOL obtainedExistingVersionInfo = + GetVersionNumberFromPath(serviceConfig.lpBinaryPathName, + existingA, existingB, + existingC, existingD); + if (!GetVersionNumberFromPath(newServiceBinaryPath, newA, + newB, newC, newD)) { + LOG_WARN(("Could not obtain version number from new path")); + return FALSE; + } + + // Check if we need to replace the old binary with the new one + // If we couldn't get the old version info then we assume we should + // replace it. + if (ForceInstallSvc == action || + !obtainedExistingVersionInfo || + (existingA < newA) || + (existingA == newA && existingB < newB) || + (existingA == newA && existingB == newB && + existingC < newC) || + (existingA == newA && existingB == newB && + existingC == newC && existingD < newD)) { + + // We have a newer updater, so update the description from the INI file. + UpdateServiceDescription(schService); + + schService.reset(); + if (!StopService()) { + return FALSE; + } + + if (!wcscmp(newServiceBinaryPath, serviceConfig.lpBinaryPathName)) { + LOG(("File is already in the correct location, no action needed for " + "upgrade. The path is: \"%ls\"", newServiceBinaryPath)); + return TRUE; + } + + BOOL result = TRUE; + + // Attempt to copy the new binary over top the existing binary. + // If there is an error we try to move it out of the way and then + // copy it in. First try the safest / easiest way to overwrite the file. + if (!CopyFileW(newServiceBinaryPath, + serviceConfig.lpBinaryPathName, FALSE)) { + LOG_WARN(("Could not overwrite old service binary file. " + "This should never happen, but if it does the next " + "upgrade will fix it, the service is not a critical " + "component that needs to be installed for upgrades " + "to work. (%d)", GetLastError())); + + // We rename the last 3 filename chars in an unsafe way. Manually + // verify there are more than 3 chars for safe failure in MoveFileExW. + const size_t len = wcslen(serviceConfig.lpBinaryPathName); + if (len > 3) { + // Calculate the temp file path that we're moving the file to. This + // is the same as the proper service path but with a .old extension. + LPWSTR oldServiceBinaryTempPath = + new WCHAR[len + 1]; + memset(oldServiceBinaryTempPath, 0, (len + 1) * sizeof (WCHAR)); + wcsncpy(oldServiceBinaryTempPath, serviceConfig.lpBinaryPathName, len); + // Rename the last 3 chars to 'old' + wcsncpy(oldServiceBinaryTempPath + len - 3, L"old", 3); + + // Move the current (old) service file to the temp path. + if (MoveFileExW(serviceConfig.lpBinaryPathName, + oldServiceBinaryTempPath, + MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH)) { + // The old binary is moved out of the way, copy in the new one. + if (!CopyFileW(newServiceBinaryPath, + serviceConfig.lpBinaryPathName, FALSE)) { + // It is best to leave the old service binary in this condition. + LOG_WARN(("The new service binary could not be copied in." + " The service will not be upgraded.")); + result = FALSE; + } else { + LOG(("The new service binary was copied in by first moving the" + " old one out of the way.")); + } + + // Attempt to get rid of the old service temp path. + if (DeleteFileW(oldServiceBinaryTempPath)) { + LOG(("The old temp service path was deleted: %ls.", + oldServiceBinaryTempPath)); + } else { + // The old temp path could not be removed. It will be removed + // the next time the user can't copy the binary in or on uninstall. + LOG_WARN(("The old temp service path was not deleted.")); + } + } else { + // It is best to leave the old service binary in this condition. + LOG_WARN(("Could not move old service file out of the way from:" + " \"%ls\" to \"%ls\". Service will not be upgraded. (%d)", + serviceConfig.lpBinaryPathName, + oldServiceBinaryTempPath, GetLastError())); + result = FALSE; + } + delete[] oldServiceBinaryTempPath; + } else { + // It is best to leave the old service binary in this condition. + LOG_WARN(("Service binary path was less than 3, service will" + " not be updated. This should never happen.")); + result = FALSE; + } + } else { + WCHAR versionStr[128] = { L'\0' }; + swprintf(versionStr, 128, L"%d.%d.%d.%d", newA, newB, newC, newD); + if (!UpdateUninstallerVersionString(versionStr)) { + LOG(("The uninstaller version string could not be updated.")); + } + LOG(("The new service binary was copied in.")); + } + + // We made a copy of ourselves to the existing location. + // The tmp file (the process of which we are executing right now) will be + // left over. Attempt to delete the file on the next reboot. + if (MoveFileExW(newServiceBinaryPath, nullptr, + MOVEFILE_DELAY_UNTIL_REBOOT)) { + LOG(("Deleting the old file path on the next reboot: %ls.", + newServiceBinaryPath)); + } else { + LOG_WARN(("Call to delete the old file path failed: %ls.", + newServiceBinaryPath)); + } + + return result; + } + + // We don't need to copy ourselves to the existing location. + // The tmp file (the process of which we are executing right now) will be + // left over. Attempt to delete the file on the next reboot. + MoveFileExW(newServiceBinaryPath, nullptr, MOVEFILE_DELAY_UNTIL_REBOOT); + + // nothing to do, we already have a newer service installed + return TRUE; + } + + // If the service does not exist and we are upgrading, don't install it. + if (UpgradeSvc == action) { + // The service does not exist and we are upgrading, so don't install it + return TRUE; + } + + // Quote the path only if it contains spaces. + PathQuoteSpacesW(newServiceBinaryPath); + // The service does not already exist so create the service as on demand + schService.own(CreateServiceW(schSCManager, SVC_NAME, SVC_DISPLAY_NAME, + SERVICE_ALL_ACCESS, SERVICE_WIN32_OWN_PROCESS, + SERVICE_DEMAND_START, SERVICE_ERROR_NORMAL, + newServiceBinaryPath, nullptr, nullptr, + nullptr, nullptr, nullptr)); + if (!schService) { + LOG_WARN(("Could not create Windows service. " + "This error should never happen since a service install " + "should only be called when elevated. (%d)", GetLastError())); + return FALSE; + } + + if (!SetUserAccessServiceDACL(schService)) { + LOG_WARN(("Could not set security ACE on service handle, the service will not " + "be able to be started from unelevated processes. " + "This error should never happen. (%d)", + GetLastError())); + } + + UpdateServiceDescription(schService); + + return TRUE; +} + +/** + * Stops the Maintenance service. + * + * @return TRUE if successful. + */ +BOOL +StopService() +{ + // Get a handle to the local computer SCM database with full access rights. + nsAutoServiceHandle schSCManager(OpenSCManager(nullptr, nullptr, + SC_MANAGER_ALL_ACCESS)); + if (!schSCManager) { + LOG_WARN(("Could not open service manager. (%d)", GetLastError())); + return FALSE; + } + + // Open the service + nsAutoServiceHandle schService(OpenServiceW(schSCManager, SVC_NAME, + SERVICE_ALL_ACCESS)); + if (!schService) { + LOG_WARN(("Could not open service. (%d)", GetLastError())); + return FALSE; + } + + LOG(("Sending stop request...")); + SERVICE_STATUS status; + SetLastError(ERROR_SUCCESS); + if (!ControlService(schService, SERVICE_CONTROL_STOP, &status) && + GetLastError() != ERROR_SERVICE_NOT_ACTIVE) { + LOG_WARN(("Error sending stop request. (%d)", GetLastError())); + } + + schSCManager.reset(); + schService.reset(); + + LOG(("Waiting for service stop...")); + DWORD lastState = WaitForServiceStop(SVC_NAME, 30); + + // The service can be in a stopped state but the exe still in use + // so make sure the process is really gone before proceeding + WaitForProcessExit(L"maintenanceservice.exe", 30); + LOG(("Done waiting for service stop, last service state: %d", lastState)); + + return lastState == SERVICE_STOPPED; +} + +/** + * Uninstalls the Maintenance service. + * + * @return TRUE if successful. + */ +BOOL +SvcUninstall() +{ + // Get a handle to the local computer SCM database with full access rights. + nsAutoServiceHandle schSCManager(OpenSCManager(nullptr, nullptr, + SC_MANAGER_ALL_ACCESS)); + if (!schSCManager) { + LOG_WARN(("Could not open service manager. (%d)", GetLastError())); + return FALSE; + } + + // Open the service + nsAutoServiceHandle schService(OpenServiceW(schSCManager, SVC_NAME, + SERVICE_ALL_ACCESS)); + if (!schService) { + LOG_WARN(("Could not open service. (%d)", GetLastError())); + return FALSE; + } + + //Stop the service so it deletes faster and so the uninstaller + // can actually delete its EXE. + DWORD totalWaitTime = 0; + SERVICE_STATUS status; + static const int maxWaitTime = 1000 * 60; // Never wait more than a minute + if (ControlService(schService, SERVICE_CONTROL_STOP, &status)) { + do { + Sleep(status.dwWaitHint); + totalWaitTime += (status.dwWaitHint + 10); + if (status.dwCurrentState == SERVICE_STOPPED) { + break; + } else if (totalWaitTime > maxWaitTime) { + break; + } + } while (QueryServiceStatus(schService, &status)); + } + + // Delete the service or mark it for deletion + BOOL deleted = DeleteService(schService); + if (!deleted) { + deleted = (GetLastError() == ERROR_SERVICE_MARKED_FOR_DELETE); + } + + return deleted; +} + +/** + * Sets the access control list for user access for the specified service. + * + * @param hService The service to set the access control list on + * @return TRUE if successful + */ +BOOL +SetUserAccessServiceDACL(SC_HANDLE hService) +{ + PACL pNewAcl = nullptr; + PSECURITY_DESCRIPTOR psd = nullptr; + DWORD lastError = SetUserAccessServiceDACL(hService, pNewAcl, psd); + if (pNewAcl) { + LocalFree((HLOCAL)pNewAcl); + } + if (psd) { + LocalFree((LPVOID)psd); + } + return ERROR_SUCCESS == lastError; +} + +/** + * Sets the access control list for user access for the specified service. + * + * @param hService The service to set the access control list on + * @param pNewAcl The out param ACL which should be freed by caller + * @param psd out param security descriptor, should be freed by caller + * @return ERROR_SUCCESS if successful + */ +DWORD +SetUserAccessServiceDACL(SC_HANDLE hService, PACL &pNewAcl, + PSECURITY_DESCRIPTOR psd) +{ + // Get the current security descriptor needed size + DWORD needed = 0; + if (!QueryServiceObjectSecurity(hService, DACL_SECURITY_INFORMATION, + &psd, 0, &needed)) { + if (GetLastError() != ERROR_INSUFFICIENT_BUFFER) { + LOG_WARN(("Could not query service object security size. (%d)", + GetLastError())); + return GetLastError(); + } + + DWORD size = needed; + psd = (PSECURITY_DESCRIPTOR)LocalAlloc(LPTR, size); + if (!psd) { + LOG_WARN(("Could not allocate security descriptor. (%d)", + GetLastError())); + return ERROR_INSUFFICIENT_BUFFER; + } + + // Get the actual security descriptor now + if (!QueryServiceObjectSecurity(hService, DACL_SECURITY_INFORMATION, + psd, size, &needed)) { + LOG_WARN(("Could not allocate security descriptor. (%d)", + GetLastError())); + return GetLastError(); + } + } + + // Get the current DACL from the security descriptor. + PACL pacl = nullptr; + BOOL bDaclPresent = FALSE; + BOOL bDaclDefaulted = FALSE; + if ( !GetSecurityDescriptorDacl(psd, &bDaclPresent, &pacl, + &bDaclDefaulted)) { + LOG_WARN(("Could not obtain DACL. (%d)", GetLastError())); + return GetLastError(); + } + + PSID sid; + DWORD SIDSize = SECURITY_MAX_SID_SIZE; + sid = LocalAlloc(LMEM_FIXED, SIDSize); + if (!sid) { + LOG_WARN(("Could not allocate SID memory. (%d)", GetLastError())); + return GetLastError(); + } + + if (!CreateWellKnownSid(WinBuiltinUsersSid, nullptr, sid, &SIDSize)) { + DWORD lastError = GetLastError(); + LOG_WARN(("Could not create well known SID. (%d)", lastError)); + LocalFree(sid); + return lastError; + } + + // Lookup the account name, the function fails if you don't pass in + // a buffer for the domain name but it's not used since we're using + // the built in account Sid. + SID_NAME_USE accountType; + WCHAR accountName[UNLEN + 1] = { L'\0' }; + WCHAR domainName[DNLEN + 1] = { L'\0' }; + DWORD accountNameSize = UNLEN + 1; + DWORD domainNameSize = DNLEN + 1; + if (!LookupAccountSidW(nullptr, sid, accountName, + &accountNameSize, + domainName, &domainNameSize, &accountType)) { + LOG_WARN(("Could not lookup account Sid, will try Users. (%d)", + GetLastError())); + wcsncpy(accountName, L"Users", UNLEN); + } + + // We already have the group name so we can get rid of the SID + FreeSid(sid); + sid = nullptr; + + // Build the ACE, BuildExplicitAccessWithName cannot fail so it is not logged. + EXPLICIT_ACCESS ea; + BuildExplicitAccessWithNameW(&ea, accountName, + SERVICE_START | SERVICE_STOP | GENERIC_READ, + SET_ACCESS, NO_INHERITANCE); + DWORD lastError = SetEntriesInAclW(1, (PEXPLICIT_ACCESS)&ea, pacl, &pNewAcl); + if (ERROR_SUCCESS != lastError) { + LOG_WARN(("Could not set entries in ACL. (%d)", lastError)); + return lastError; + } + + // Initialize a new security descriptor. + SECURITY_DESCRIPTOR sd; + if (!InitializeSecurityDescriptor(&sd, SECURITY_DESCRIPTOR_REVISION)) { + LOG_WARN(("Could not initialize security descriptor. (%d)", + GetLastError())); + return GetLastError(); + } + + // Set the new DACL in the security descriptor. + if (!SetSecurityDescriptorDacl(&sd, TRUE, pNewAcl, FALSE)) { + LOG_WARN(("Could not set security descriptor DACL. (%d)", + GetLastError())); + return GetLastError(); + } + + // Set the new security descriptor for the service object. + if (!SetServiceObjectSecurity(hService, DACL_SECURITY_INFORMATION, &sd)) { + LOG_WARN(("Could not set object security. (%d)", + GetLastError())); + return GetLastError(); + } + + // Woohoo, raise the roof + LOG(("User access was set successfully on the service.")); + return ERROR_SUCCESS; +} diff --git a/toolkit/components/maintenanceservice/serviceinstall.h b/toolkit/components/maintenanceservice/serviceinstall.h new file mode 100644 index 000000000..d8532a968 --- /dev/null +++ b/toolkit/components/maintenanceservice/serviceinstall.h @@ -0,0 +1,21 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "readstrings.h" + +#define SVC_DISPLAY_NAME L"Mozilla Maintenance Service" + +enum SvcInstallAction { UpgradeSvc, InstallSvc, ForceInstallSvc }; +BOOL SvcInstall(SvcInstallAction action); +BOOL SvcUninstall(); +BOOL StopService(); +BOOL SetUserAccessServiceDACL(SC_HANDLE hService); +DWORD SetUserAccessServiceDACL(SC_HANDLE hService, PACL &pNewAcl, + PSECURITY_DESCRIPTOR psd); + +struct MaintenanceServiceStringTable +{ + char serviceDescription[MAX_TEXT_LEN]; +}; + diff --git a/toolkit/components/maintenanceservice/workmonitor.cpp b/toolkit/components/maintenanceservice/workmonitor.cpp new file mode 100644 index 000000000..d06db3ca2 --- /dev/null +++ b/toolkit/components/maintenanceservice/workmonitor.cpp @@ -0,0 +1,758 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include <shlobj.h> +#include <shlwapi.h> +#include <wtsapi32.h> +#include <userenv.h> +#include <shellapi.h> + +#pragma comment(lib, "wtsapi32.lib") +#pragma comment(lib, "userenv.lib") +#pragma comment(lib, "shlwapi.lib") +#pragma comment(lib, "ole32.lib") +#pragma comment(lib, "rpcrt4.lib") + +#include "nsWindowsHelpers.h" + +#include "workmonitor.h" +#include "serviceinstall.h" +#include "servicebase.h" +#include "registrycertificates.h" +#include "uachelper.h" +#include "updatehelper.h" +#include "pathhash.h" +#include "errors.h" + +// Wait 15 minutes for an update operation to run at most. +// Updates usually take less than a minute so this seems like a +// significantly large and safe amount of time to wait. +static const int TIME_TO_WAIT_ON_UPDATER = 15 * 60 * 1000; +wchar_t* MakeCommandLine(int argc, wchar_t** argv); +BOOL WriteStatusFailure(LPCWSTR updateDirPath, int errorCode); +BOOL PathGetSiblingFilePath(LPWSTR destinationBuffer, LPCWSTR siblingFilePath, + LPCWSTR newFileName); +BOOL DoesFallbackKeyExist(); + +/* + * Read the update.status file and sets isApplying to true if + * the status is set to applying. + * + * @param updateDirPath The directory where update.status is stored + * @param isApplying Out parameter for specifying if the status + * is set to applying or not. + * @return TRUE if the information was filled. +*/ +static BOOL +IsStatusApplying(LPCWSTR updateDirPath, BOOL &isApplying) +{ + isApplying = FALSE; + WCHAR updateStatusFilePath[MAX_PATH + 1] = {L'\0'}; + wcsncpy(updateStatusFilePath, updateDirPath, MAX_PATH); + if (!PathAppendSafe(updateStatusFilePath, L"update.status")) { + LOG_WARN(("Could not append path for update.status file")); + return FALSE; + } + + nsAutoHandle statusFile(CreateFileW(updateStatusFilePath, GENERIC_READ, + FILE_SHARE_READ | + FILE_SHARE_WRITE | + FILE_SHARE_DELETE, + nullptr, OPEN_EXISTING, 0, nullptr)); + + if (INVALID_HANDLE_VALUE == statusFile) { + LOG_WARN(("Could not open update.status file")); + return FALSE; + } + + char buf[32] = { 0 }; + DWORD read; + if (!ReadFile(statusFile, buf, sizeof(buf), &read, nullptr)) { + LOG_WARN(("Could not read from update.status file")); + return FALSE; + } + + const char kApplying[] = "applying"; + isApplying = strncmp(buf, kApplying, + sizeof(kApplying) - 1) == 0; + return TRUE; +} + +/** + * Determines whether we're staging an update. + * + * @param argc The argc value normally sent to updater.exe + * @param argv The argv value normally sent to updater.exe + * @return boolean True if we're staging an update + */ +static bool +IsUpdateBeingStaged(int argc, LPWSTR *argv) +{ + // PID will be set to -1 if we're supposed to stage an update. + return argc == 4 && !wcscmp(argv[3], L"-1") || + argc == 5 && !wcscmp(argv[4], L"-1"); +} + +/** + * Determines whether the param only contains digits. + * + * @param str The string to check + * @param boolean True if the param only contains digits + */ +static bool +IsDigits(WCHAR *str) +{ + while (*str) { + if (!iswdigit(*str++)) { + return FALSE; + } + } + return TRUE; +} + +/** + * Determines whether the command line contains just the directory to apply the + * update to (old command line) or if it contains the installation directory and + * the directory to apply the update to. + * + * @param argc The argc value normally sent to updater.exe + * @param argv The argv value normally sent to updater.exe + * @param boolean True if the command line contains just the directory to apply + * the update to + */ +static bool +IsOldCommandline(int argc, LPWSTR *argv) +{ + return argc == 4 && !wcscmp(argv[3], L"-1") || + argc >= 4 && (wcsstr(argv[3], L"/replace") || IsDigits(argv[3])); +} + +/** + * Gets the installation directory from the arguments passed to updater.exe. + * + * @param argcTmp The argc value normally sent to updater.exe + * @param argvTmp The argv value normally sent to updater.exe + * @param aResultDir Buffer to hold the installation directory. + */ +static BOOL +GetInstallationDir(int argcTmp, LPWSTR *argvTmp, WCHAR aResultDir[MAX_PATH + 1]) +{ + int index = 3; + if (IsOldCommandline(argcTmp, argvTmp)) { + index = 2; + } + + if (argcTmp < index) { + return FALSE; + } + + wcsncpy(aResultDir, argvTmp[2], MAX_PATH); + WCHAR* backSlash = wcsrchr(aResultDir, L'\\'); + // Make sure that the path does not include trailing backslashes + if (backSlash && (backSlash[1] == L'\0')) { + *backSlash = L'\0'; + } + + // The new command line's argv[2] is always the installation directory. + if (index == 2) { + bool backgroundUpdate = IsUpdateBeingStaged(argcTmp, argvTmp); + bool replaceRequest = (argcTmp >= 4 && wcsstr(argvTmp[3], L"/replace")); + if (backgroundUpdate || replaceRequest) { + return PathRemoveFileSpecW(aResultDir); + } + } + return TRUE; +} + +/** + * Runs an update process as the service using the SYSTEM account. + * + * @param argc The number of arguments in argv + * @param argv The arguments normally passed to updater.exe + * argv[0] must be the path to updater.exe + * @param processStarted Set to TRUE if the process was started. + * @return TRUE if the update process was run had a return code of 0. + */ +BOOL +StartUpdateProcess(int argc, + LPWSTR *argv, + LPCWSTR installDir, + BOOL &processStarted) +{ + LOG(("Starting update process as the service in session 0.")); + STARTUPINFO si = {0}; + si.cb = sizeof(STARTUPINFO); + si.lpDesktop = L"winsta0\\Default"; + PROCESS_INFORMATION pi = {0}; + + // The updater command line is of the form: + // updater.exe update-dir apply [wait-pid [callback-dir callback-path args]] + LPWSTR cmdLine = MakeCommandLine(argc, argv); + + int index = 3; + if (IsOldCommandline(argc, argv)) { + index = 2; + } + + // If we're about to start the update process from session 0, + // then we should not show a GUI. This only really needs to be done + // on Vista and higher, but it's better to keep everything consistent + // across all OS if it's of no harm. + if (argc >= index) { + // Setting the desktop to blank will ensure no GUI is displayed + si.lpDesktop = L""; + si.dwFlags |= STARTF_USESHOWWINDOW; + si.wShowWindow = SW_HIDE; + } + + // Add an env var for MOZ_USING_SERVICE so the updater.exe can + // do anything special that it needs to do for service updates. + // Search in updater.cpp for more info on MOZ_USING_SERVICE. + putenv(const_cast<char*>("MOZ_USING_SERVICE=1")); + LOG(("Starting service with cmdline: %ls", cmdLine)); + processStarted = CreateProcessW(argv[0], cmdLine, + nullptr, nullptr, FALSE, + CREATE_DEFAULT_ERROR_MODE, + nullptr, + nullptr, &si, &pi); + + BOOL updateWasSuccessful = FALSE; + if (processStarted) { + BOOL processTerminated = FALSE; + BOOL noProcessExitCode = FALSE; + // Wait for the updater process to finish + LOG(("Process was started... waiting on result.")); + DWORD waitRes = WaitForSingleObject(pi.hProcess, TIME_TO_WAIT_ON_UPDATER); + if (WAIT_TIMEOUT == waitRes) { + // We waited a long period of time for updater.exe and it never finished + // so kill it. + TerminateProcess(pi.hProcess, 1); + processTerminated = TRUE; + } else { + // Check the return code of updater.exe to make sure we get 0 + DWORD returnCode; + if (GetExitCodeProcess(pi.hProcess, &returnCode)) { + LOG(("Process finished with return code %d.", returnCode)); + // updater returns 0 if successful. + updateWasSuccessful = (returnCode == 0); + } else { + LOG_WARN(("Process finished but could not obtain return code.")); + noProcessExitCode = TRUE; + } + } + CloseHandle(pi.hProcess); + CloseHandle(pi.hThread); + + // Check just in case updater.exe didn't change the status from + // applying. If this is the case we report an error. + BOOL isApplying = FALSE; + if (IsStatusApplying(argv[1], isApplying) && isApplying) { + if (updateWasSuccessful) { + LOG(("update.status is still applying even though update was " + "successful.")); + if (!WriteStatusFailure(argv[1], + SERVICE_STILL_APPLYING_ON_SUCCESS)) { + LOG_WARN(("Could not write update.status still applying on " + "success error.")); + } + // Since we still had applying we know updater.exe didn't do its + // job correctly. + updateWasSuccessful = FALSE; + } else { + LOG_WARN(("update.status is still applying and update was not successful.")); + int failcode = SERVICE_STILL_APPLYING_ON_FAILURE; + if (noProcessExitCode) { + failcode = SERVICE_STILL_APPLYING_NO_EXIT_CODE; + } else if (processTerminated) { + failcode = SERVICE_STILL_APPLYING_TERMINATED; + } + if (!WriteStatusFailure(argv[1], failcode)) { + LOG_WARN(("Could not write update.status still applying on " + "failure error.")); + } + } + } + } else { + DWORD lastError = GetLastError(); + LOG_WARN(("Could not create process as current user, " + "updaterPath: %ls; cmdLine: %ls. (%d)", + argv[0], cmdLine, lastError)); + } + + // Empty value on putenv is how you remove an env variable in Windows + putenv(const_cast<char*>("MOZ_USING_SERVICE=")); + + free(cmdLine); + return updateWasSuccessful; +} + +/** + * Validates a file as an official updater. + * + * @param updater Path to the updater to validate + * @param installDir Path to the application installation + * being updated + * @param updateDir Update applyTo direcotry, + * where logs will be written + * + * @return true if updater is the path to a valid updater + */ +static bool +UpdaterIsValid(LPWSTR updater, LPWSTR installDir, LPWSTR updateDir) +{ + // Make sure the path to the updater to use for the update is local. + // We do this check to make sure that file locking is available for + // race condition security checks. + BOOL isLocal = FALSE; + if (!IsLocalFile(updater, isLocal) || !isLocal) { + LOG_WARN(("Filesystem in path %ls is not supported (%d)", + updater, GetLastError())); + if (!WriteStatusFailure(updateDir, SERVICE_UPDATER_NOT_FIXED_DRIVE)) { + LOG_WARN(("Could not write update.status service update failure. (%d)", + GetLastError())); + } + return false; + } + + nsAutoHandle noWriteLock(CreateFileW(updater, GENERIC_READ, FILE_SHARE_READ, + nullptr, OPEN_EXISTING, 0, nullptr)); + if (INVALID_HANDLE_VALUE == noWriteLock) { + LOG_WARN(("Could not set no write sharing access on file. (%d)", + GetLastError())); + if (!WriteStatusFailure(updateDir, SERVICE_COULD_NOT_LOCK_UPDATER)) { + LOG_WARN(("Could not write update.status service update failure. (%d)", + GetLastError())); + } + return false; + } + + // Verify that the updater.exe that we are executing is the same + // as the one in the installation directory which we are updating. + // The installation dir that we are installing to is installDir. + WCHAR installDirUpdater[MAX_PATH + 1] = { L'\0' }; + wcsncpy(installDirUpdater, installDir, MAX_PATH); + if (!PathAppendSafe(installDirUpdater, L"updater.exe")) { + LOG_WARN(("Install directory updater could not be determined.")); + return false; + } + + BOOL updaterIsCorrect; + if (!VerifySameFiles(updater, installDirUpdater, updaterIsCorrect)) { + LOG_WARN(("Error checking if the updaters are the same.\n" + "Path 1: %ls\nPath 2: %ls", updater, installDirUpdater)); + return false; + } + + if (!updaterIsCorrect) { + LOG_WARN(("The updaters do not match, updater will not run.\n" + "Path 1: %ls\nPath 2: %ls", updater, installDirUpdater)); + if (!WriteStatusFailure(updateDir, SERVICE_UPDATER_COMPARE_ERROR)) { + LOG_WARN(("Could not write update.status updater compare failure.")); + } + return false; + } + + LOG(("updater.exe was compared successfully to the installation directory" + " updater.exe.")); + + // Check to make sure the updater.exe module has the unique updater identity. + // This is a security measure to make sure that the signed executable that + // we will run is actually an updater. + bool result = true; + HMODULE updaterModule = LoadLibraryEx(updater, nullptr, + LOAD_LIBRARY_AS_DATAFILE); + if (!updaterModule) { + LOG_WARN(("updater.exe module could not be loaded. (%d)", GetLastError())); + result = false; + } else { + char updaterIdentity[64]; + if (!LoadStringA(updaterModule, IDS_UPDATER_IDENTITY, + updaterIdentity, sizeof(updaterIdentity))) { + LOG_WARN(("The updater.exe application does not contain the Mozilla" + " updater identity.")); + result = false; + } + + if (strcmp(updaterIdentity, UPDATER_IDENTITY_STRING)) { + LOG_WARN(("The updater.exe identity string is not valid.")); + result = false; + } + FreeLibrary(updaterModule); + } + + if (result) { + LOG(("The updater.exe application contains the Mozilla" + " updater identity.")); + } else { + if (!WriteStatusFailure(updateDir, SERVICE_UPDATER_IDENTITY_ERROR)) { + LOG_WARN(("Could not write update.status no updater identity.")); + } + return false; + } + +#ifndef DISABLE_UPDATER_AUTHENTICODE_CHECK + return DoesBinaryMatchAllowedCertificates(installDir, updater); +#else + return true; +#endif +} + +/** + * Processes a software update command + * + * @param argc The number of arguments in argv + * @param argv The arguments normally passed to updater.exe + * argv[0] must be the path to updater.exe + * @return TRUE if the update was successful. + */ +BOOL +ProcessSoftwareUpdateCommand(DWORD argc, LPWSTR *argv) +{ + BOOL result = TRUE; + if (argc < 3) { + LOG_WARN(("Not enough command line parameters specified. " + "Updating update.status.")); + + // We can only update update.status if argv[1] exists. argv[1] is + // the directory where the update.status file exists. + if (argc < 2 || + !WriteStatusFailure(argv[1], SERVICE_NOT_ENOUGH_COMMAND_LINE_ARGS)) { + LOG_WARN(("Could not write update.status service update failure. (%d)", + GetLastError())); + } + return FALSE; + } + + WCHAR installDir[MAX_PATH + 1] = {L'\0'}; + if (!GetInstallationDir(argc, argv, installDir)) { + LOG_WARN(("Could not get the installation directory")); + if (!WriteStatusFailure(argv[1], SERVICE_INSTALLDIR_ERROR)) { + LOG_WARN(("Could not write update.status for GetInstallationDir failure.")); + } + return FALSE; + } + + if (UpdaterIsValid(argv[0], installDir, argv[1])) { + BOOL updateProcessWasStarted = FALSE; + if (StartUpdateProcess(argc, argv, installDir, + updateProcessWasStarted)) { + LOG(("updater.exe was launched and run successfully!")); + LogFlush(); + + // Don't attempt to update the service when the update is being staged. + if (!IsUpdateBeingStaged(argc, argv)) { + // We might not execute code after StartServiceUpdate because + // the service installer will stop the service if it is running. + StartServiceUpdate(installDir); + } + } else { + result = FALSE; + LOG_WARN(("Error running update process. Updating update.status (%d)", + GetLastError())); + LogFlush(); + + // If the update process was started, then updater.exe is responsible for + // setting the failure code. If it could not be started then we do the + // work. We set an error instead of directly setting status pending + // so that the app.update.service.errors pref can be updated when + // the callback app restarts. + if (!updateProcessWasStarted) { + if (!WriteStatusFailure(argv[1], + SERVICE_UPDATER_COULD_NOT_BE_STARTED)) { + LOG_WARN(("Could not write update.status service update failure. (%d)", + GetLastError())); + } + } + } + } else { + result = FALSE; + LOG_WARN(("Could not start process due to certificate check error on " + "updater.exe. Updating update.status. (%d)", GetLastError())); + + // When there is a certificate check error on the updater.exe application, + // we want to write out the error. + if (!WriteStatusFailure(argv[1], + SERVICE_UPDATER_SIGN_ERROR)) { + LOG_WARN(("Could not write pending state to update.status. (%d)", + GetLastError())); + } + } + + return result; +} + +/** + * Obtains the updater path alongside a subdir of the service binary. + * The purpose of this function is to return a path that is likely high + * integrity and therefore more safe to execute code from. + * + * @param serviceUpdaterPath Out parameter for the path where the updater + * should be copied to. + * @return TRUE if a file path was obtained. + */ +BOOL +GetSecureUpdaterPath(WCHAR serviceUpdaterPath[MAX_PATH + 1]) +{ + if (!GetModuleFileNameW(nullptr, serviceUpdaterPath, MAX_PATH)) { + LOG_WARN(("Could not obtain module filename when attempting to " + "use a secure updater path. (%d)", GetLastError())); + return FALSE; + } + + if (!PathRemoveFileSpecW(serviceUpdaterPath)) { + LOG_WARN(("Couldn't remove file spec when attempting to use a secure " + "updater path. (%d)", GetLastError())); + return FALSE; + } + + if (!PathAppendSafe(serviceUpdaterPath, L"update")) { + LOG_WARN(("Couldn't append file spec when attempting to use a secure " + "updater path. (%d)", GetLastError())); + return FALSE; + } + + CreateDirectoryW(serviceUpdaterPath, nullptr); + + if (!PathAppendSafe(serviceUpdaterPath, L"updater.exe")) { + LOG_WARN(("Couldn't append file spec when attempting to use a secure " + "updater path. (%d)", GetLastError())); + return FALSE; + } + + return TRUE; +} + +/** + * Deletes the passed in updater path and the associated updater.ini file. + * + * @param serviceUpdaterPath The path to delete. + * @return TRUE if a file was deleted. + */ +BOOL +DeleteSecureUpdater(WCHAR serviceUpdaterPath[MAX_PATH + 1]) +{ + BOOL result = FALSE; + if (serviceUpdaterPath[0]) { + result = DeleteFileW(serviceUpdaterPath); + if (!result && GetLastError() != ERROR_PATH_NOT_FOUND && + GetLastError() != ERROR_FILE_NOT_FOUND) { + LOG_WARN(("Could not delete service updater path: '%ls'.", + serviceUpdaterPath)); + } + + WCHAR updaterINIPath[MAX_PATH + 1] = { L'\0' }; + if (PathGetSiblingFilePath(updaterINIPath, serviceUpdaterPath, + L"updater.ini")) { + result = DeleteFileW(updaterINIPath); + if (!result && GetLastError() != ERROR_PATH_NOT_FOUND && + GetLastError() != ERROR_FILE_NOT_FOUND) { + LOG_WARN(("Could not delete service updater INI path: '%ls'.", + updaterINIPath)); + } + } + } + return result; +} + +/** + * Executes a service command. + * + * @param argc The number of arguments in argv + * @param argv The service command line arguments, argv[0] and argv[1] + * and automatically included by Windows. argv[2] is the + * service command. + * + * @return FALSE if there was an error executing the service command. + */ +BOOL +ExecuteServiceCommand(int argc, LPWSTR *argv) +{ + if (argc < 3) { + LOG_WARN(("Not enough command line arguments to execute a service command")); + return FALSE; + } + + // The tests work by making sure the log has changed, so we put a + // unique ID in the log. + RPC_WSTR guidString = RPC_WSTR(L""); + GUID guid; + HRESULT hr = CoCreateGuid(&guid); + if (SUCCEEDED(hr)) { + UuidToString(&guid, &guidString); + } + LOG(("Executing service command %ls, ID: %ls", + argv[2], reinterpret_cast<LPCWSTR>(guidString))); + RpcStringFree(&guidString); + + BOOL result = FALSE; + if (!lstrcmpi(argv[2], L"software-update")) { + // This check is also performed in updater.cpp and is performed here + // as well since the maintenance service can be called directly. + if (argc < 4 || !IsValidFullPath(argv[4])) { + // Since the status file is written to the patch directory and the patch + // directory is invalid don't write the status file. + LOG_WARN(("The patch directory path is not valid for this application.")); + return FALSE; + } + + // This check is also performed in updater.cpp and is performed here + // as well since the maintenance service can be called directly. + if (argc < 5 || !IsValidFullPath(argv[5])) { + LOG_WARN(("The install directory path is not valid for this application.")); + if (!WriteStatusFailure(argv[4], SERVICE_INVALID_INSTALL_DIR_PATH_ERROR)) { + LOG_WARN(("Could not write update.status for previous failure.")); + } + return FALSE; + } + + if (!IsOldCommandline(argc - 3, argv + 3)) { + // This check is also performed in updater.cpp and is performed here + // as well since the maintenance service can be called directly. + if (argc < 6 || !IsValidFullPath(argv[6])) { + LOG_WARN(("The working directory path is not valid for this application.")); + if (!WriteStatusFailure(argv[4], SERVICE_INVALID_WORKING_DIR_PATH_ERROR)) { + LOG_WARN(("Could not write update.status for previous failure.")); + } + return FALSE; + } + + // These checks are also performed in updater.cpp and is performed here + // as well since the maintenance service can be called directly. + if (_wcsnicmp(argv[6], argv[5], MAX_PATH) != 0) { + if (wcscmp(argv[7], L"-1") != 0 && !wcsstr(argv[7], L"/replace")) { + LOG_WARN(("Installation directory and working directory must be the " + "same for non-staged updates. Exiting.")); + if (!WriteStatusFailure(argv[4], SERVICE_INVALID_APPLYTO_DIR_ERROR)) { + LOG_WARN(("Could not write update.status for previous failure.")); + } + return FALSE; + } + + NS_tchar workingDirParent[MAX_PATH]; + NS_tsnprintf(workingDirParent, + sizeof(workingDirParent) / sizeof(workingDirParent[0]), + NS_T("%s"), argv[6]); + if (!PathRemoveFileSpecW(workingDirParent)) { + LOG_WARN(("Couldn't remove file spec when attempting to verify the " + "working directory path. (%d)", GetLastError())); + if (!WriteStatusFailure(argv[4], REMOVE_FILE_SPEC_ERROR)) { + LOG_WARN(("Could not write update.status for previous failure.")); + } + return FALSE; + } + + if (_wcsnicmp(workingDirParent, argv[5], MAX_PATH) != 0) { + LOG_WARN(("The apply-to directory must be the same as or " + "a child of the installation directory! Exiting.")); + if (!WriteStatusFailure(argv[4], SERVICE_INVALID_APPLYTO_DIR_STAGED_ERROR)) { + LOG_WARN(("Could not write update.status for previous failure.")); + } + return FALSE; + } + } + + } + + // Use the passed in command line arguments for the update, except for the + // path to updater.exe. We always look for updater.exe in the installation + // directory, then we copy updater.exe to a the directory of the + // MozillaMaintenance service so that a low integrity process cannot + // replace the updater.exe at any point and use that for the update. + // It also makes DLL injection attacks harder. + WCHAR installDir[MAX_PATH + 1] = { L'\0' }; + if (!GetInstallationDir(argc - 3, argv + 3, installDir)) { + LOG_WARN(("Could not get the installation directory")); + if (!WriteStatusFailure(argv[4], SERVICE_INSTALLDIR_ERROR)) { + LOG_WARN(("Could not write update.status for previous failure.")); + } + return FALSE; + } + + if (!DoesFallbackKeyExist()) { + WCHAR maintenanceServiceKey[MAX_PATH + 1]; + if (CalculateRegistryPathFromFilePath(installDir, maintenanceServiceKey)) { + LOG(("Checking for Maintenance Service registry. key: '%ls'", + maintenanceServiceKey)); + HKEY baseKey = nullptr; + if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, + maintenanceServiceKey, 0, + KEY_READ | KEY_WOW64_64KEY, + &baseKey) != ERROR_SUCCESS) { + LOG_WARN(("The maintenance service registry key does not exist.")); + if (!WriteStatusFailure(argv[4], SERVICE_INSTALL_DIR_REG_ERROR)) { + LOG_WARN(("Could not write update.status for previous failure.")); + } + return FALSE; + } + RegCloseKey(baseKey); + } else { + if (!WriteStatusFailure(argv[4], SERVICE_CALC_REG_PATH_ERROR)) { + LOG_WARN(("Could not write update.status for previous failure.")); + } + return FALSE; + } + } + + WCHAR installDirUpdater[MAX_PATH + 1] = { L'\0' }; + wcsncpy(installDirUpdater, installDir, MAX_PATH); + if (!PathAppendSafe(installDirUpdater, L"updater.exe")) { + LOG_WARN(("Install directory updater could not be determined.")); + result = FALSE; + } + + result = UpdaterIsValid(installDirUpdater, installDir, argv[4]); + + WCHAR secureUpdaterPath[MAX_PATH + 1] = { L'\0' }; + if (result) { + result = GetSecureUpdaterPath(secureUpdaterPath); // Does its own logging + } + if (result) { + LOG(("Passed in path: '%ls'; Using this path for updating: '%ls'.", + installDirUpdater, secureUpdaterPath)); + DeleteSecureUpdater(secureUpdaterPath); + result = CopyFileW(installDirUpdater, secureUpdaterPath, FALSE); + } + + if (!result) { + LOG_WARN(("Could not copy path to secure location. (%d)", + GetLastError())); + if (!WriteStatusFailure(argv[4], SERVICE_COULD_NOT_COPY_UPDATER)) { + LOG_WARN(("Could not write update.status could not copy updater error")); + } + } else { + + // We obtained the path and copied it successfully, update the path to + // use for the service update. + argv[3] = secureUpdaterPath; + + WCHAR installDirUpdaterINIPath[MAX_PATH + 1] = { L'\0' }; + WCHAR secureUpdaterINIPath[MAX_PATH + 1] = { L'\0' }; + if (PathGetSiblingFilePath(secureUpdaterINIPath, secureUpdaterPath, + L"updater.ini") && + PathGetSiblingFilePath(installDirUpdaterINIPath, installDirUpdater, + L"updater.ini")) { + // This is non fatal if it fails there is no real harm + if (!CopyFileW(installDirUpdaterINIPath, secureUpdaterINIPath, FALSE)) { + LOG_WARN(("Could not copy updater.ini from: '%ls' to '%ls'. (%d)", + installDirUpdaterINIPath, secureUpdaterINIPath, GetLastError())); + } + } + + result = ProcessSoftwareUpdateCommand(argc - 3, argv + 3); + DeleteSecureUpdater(secureUpdaterPath); + } + + // We might not reach here if the service install succeeded + // because the service self updates itself and the service + // installer will stop the service. + LOG(("Service command %ls complete.", argv[2])); + } else { + LOG_WARN(("Service command not recognized: %ls.", argv[2])); + // result is already set to FALSE + } + + LOG(("service command %ls complete with result: %ls.", + argv[1], (result ? L"Success" : L"Failure"))); + return TRUE; +} diff --git a/toolkit/components/maintenanceservice/workmonitor.h b/toolkit/components/maintenanceservice/workmonitor.h new file mode 100644 index 000000000..ac4cd679b --- /dev/null +++ b/toolkit/components/maintenanceservice/workmonitor.h @@ -0,0 +1,5 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +BOOL ExecuteServiceCommand(int argc, LPWSTR *argv); |