summaryrefslogtreecommitdiffstats
path: root/toolkit/components/maintenanceservice
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
commit36d22d82aa202bb199967e9512281e9a53db42c9 (patch)
tree105e8c98ddea1c1e4784a60a5a6410fa416be2de /toolkit/components/maintenanceservice
parentInitial commit. (diff)
downloadfirefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz
firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip
Adding upstream version 115.7.0esr.upstream/115.7.0esrupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/components/maintenanceservice')
-rw-r--r--toolkit/components/maintenanceservice/Makefile.in13
-rw-r--r--toolkit/components/maintenanceservice/bootstrapinstaller/maintenanceservice_installer.nsi269
-rw-r--r--toolkit/components/maintenanceservice/maintenanceservice.cpp370
-rw-r--r--toolkit/components/maintenanceservice/maintenanceservice.exe.manifest25
-rw-r--r--toolkit/components/maintenanceservice/maintenanceservice.h16
-rw-r--r--toolkit/components/maintenanceservice/maintenanceservice.rc79
-rw-r--r--toolkit/components/maintenanceservice/moz.build62
-rw-r--r--toolkit/components/maintenanceservice/resource.h20
-rw-r--r--toolkit/components/maintenanceservice/servicebase.cpp84
-rw-r--r--toolkit/components/maintenanceservice/servicebase.h22
-rw-r--r--toolkit/components/maintenanceservice/serviceinstall.cpp760
-rw-r--r--toolkit/components/maintenanceservice/serviceinstall.h19
-rw-r--r--toolkit/components/maintenanceservice/tests/gtest/ServiceStartInteractiveOnly.cpp52
-rw-r--r--toolkit/components/maintenanceservice/tests/gtest/moz.build20
-rw-r--r--toolkit/components/maintenanceservice/workmonitor.cpp812
-rw-r--r--toolkit/components/maintenanceservice/workmonitor.h5
16 files changed, 2628 insertions, 0 deletions
diff --git a/toolkit/components/maintenanceservice/Makefile.in b/toolkit/components/maintenanceservice/Makefile.in
new file mode 100644
index 0000000000..b07afbb0a9
--- /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 0000000000..685458db0e
--- /dev/null
+++ b/toolkit/components/maintenanceservice/bootstrapinstaller/maintenanceservice_installer.nsi
@@ -0,0 +1,269 @@
+# 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
+
+Unicode true
+ManifestSupportedOS all
+ManifestDPIAware true
+
+!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 machines 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"
+!ifdef BrandFullName
+!undef BrandFullName
+!endif
+!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
+ ${Unless} ${AtLeastWin7}
+ 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 "")'
+
+ 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}
+ ${OrIf} ${IsNativeARM64}
+ 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}
+ ${OrIf} ${IsNativeARM64}
+ 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}
+ ${OrIf} ${IsNativeARM64}
+ SetRegView 64
+ ${EndIf}
+ DeleteRegValue HKLM "Software\Mozilla\MaintenanceService" "Installed"
+ DeleteRegValue HKLM "Software\Mozilla\MaintenanceService" "FFPrefetchDisabled"
+ DeleteRegKey HKLM "${FallbackKey}\"
+ ${If} ${RunningX64}
+ ${OrIf} ${IsNativeARM64}
+ SetRegView lastused
+ ${EndIf}
+SectionEnd
+
diff --git a/toolkit/components/maintenanceservice/maintenanceservice.cpp b/toolkit/components/maintenanceservice/maintenanceservice.cpp
new file mode 100644
index 0000000000..d46feb1fc7
--- /dev/null
+++ b/toolkit/components/maintenanceservice/maintenanceservice.cpp
@@ -0,0 +1,370 @@
+/* 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 logFilePath[MAX_PATH + 1];
+ if (GetLogDirectoryPath(logFilePath) &&
+ PathAppendSafe(logFilePath, L"maintenanceservice-install.log")) {
+ LogInit(logFilePath);
+ }
+
+ 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. (%lu)", GetLastError()));
+ LogFinish();
+ return 1;
+ }
+
+ LOG(("The service was installed successfully"));
+ LogFinish();
+ return 0;
+ }
+
+ if (!lstrcmpi(argv[1], L"upgrade")) {
+ WCHAR logFilePath[MAX_PATH + 1];
+ if (GetLogDirectoryPath(logFilePath) &&
+ PathAppendSafe(logFilePath, L"maintenanceservice-install.log")) {
+ LogInit(logFilePath);
+ }
+
+ LOG(("Upgrading service if installed..."));
+ if (!SvcInstall(UpgradeSvc)) {
+ LOG_WARN(("Could not upgrade service. (%lu)", GetLastError()));
+ LogFinish();
+ return 1;
+ }
+
+ LOG(("The service was upgraded successfully"));
+ LogFinish();
+ return 0;
+ }
+
+ if (!lstrcmpi(argv[1], L"uninstall")) {
+ WCHAR logFilePath[MAX_PATH + 1];
+ if (GetLogDirectoryPath(logFilePath) &&
+ PathAppendSafe(logFilePath, L"maintenanceservice-uninstall.log")) {
+ LogInit(logFilePath);
+ }
+ LOG(("Uninstalling service..."));
+ if (!SvcUninstall()) {
+ LOG_WARN(("Could not uninstall service. (%lu)", 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[] = {
+ {const_cast<LPWSTR>(SVC_NAME),
+ (LPSERVICE_MAIN_FUNCTIONW)SvcMain}, // -Wwritable-strings
+ {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. (%lu)", 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);
+}
+
+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 logFilePath[MAX_PATH + 1];
+ if (GetLogDirectoryPath(logFilePath)) {
+ BackupOldLogs(logFilePath, LOGS_TO_KEEP);
+ if (PathAppendSafe(logFilePath, L"maintenanceservice.log")) {
+ LogInit(logFilePath);
+ }
+ }
+
+ // 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. (%lu)", 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.
+ BOOL success = 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, success ? NO_ERROR : 1, 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 0000000000..e6bfba8ca5
--- /dev/null
+++ b/toolkit/components/maintenanceservice/maintenanceservice.exe.manifest
@@ -0,0 +1,25 @@
+<?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}"/>
+ </application>
+ </compatibility>
+</assembly>
diff --git a/toolkit/components/maintenanceservice/maintenanceservice.h b/toolkit/components/maintenanceservice/maintenanceservice.h
new file mode 100644
index 0000000000..596b2b44ee
--- /dev/null
+++ b/toolkit/components/maintenanceservice/maintenanceservice.h
@@ -0,0 +1,16 @@
+/* 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 TOOLKIT_COMPONENTS_MAINTENANCESERVICE_MAINTENANCESERVICE_H_
+#define TOOLKIT_COMPONENTS_MAINTENANCESERVICE_MAINTENANCESERVICE_H_
+
+#include <windows.h>
+
+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);
+
+#endif // TOOLKIT_COMPONENTS_MAINTENANCESERVICE_MAINTENANCESERVICE_H_
diff --git a/toolkit/components/maintenanceservice/maintenanceservice.rc b/toolkit/components/maintenanceservice/maintenanceservice.rc
new file mode 100644
index 0000000000..e3731efb2a
--- /dev/null
+++ b/toolkit/components/maintenanceservice/maintenanceservice.rc
@@ -0,0 +1,79 @@
+/* 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
+
+/////////////////////////////////////////////////////////////////////////////
+//
+// 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 0000000000..ee9a106259
--- /dev/null
+++ b/toolkit/components/maintenanceservice/moz.build
@@ -0,0 +1,62 @@
+# -*- 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",
+]
+
+USE_LIBS += [
+ "updatecommon",
+]
+
+if CONFIG["DISABLE_UPDATER_AUTHENTICODE_CHECK"]:
+ 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["CC_TYPE"] == "clang-cl":
+ WIN32_EXE_LDFLAGS += ["-ENTRY:wmainCRTStartup"]
+
+if CONFIG["OS_TARGET"] == "WINNT" and CONFIG["CC_TYPE"] in ("gcc", "clang"):
+ # This allows us to use wmain as the entry point on mingw
+ LDFLAGS += [
+ "-municode",
+ ]
+
+RCINCLUDE = "maintenanceservice.rc"
+
+DisableStlWrapping()
+
+OS_LIBS += [
+ "comctl32",
+ "ws2_32",
+ "shell32",
+ "shlwapi",
+ "user32",
+ "userenv",
+ "uuid",
+]
+
+if CONFIG["ENABLE_TESTS"]:
+ DIRS += ["tests/gtest"]
+
+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 0000000000..fcc79dd9ae
--- /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 0000000000..d6004234c7
--- /dev/null
+++ b/toolkit/components/maintenanceservice/servicebase.cpp
@@ -0,0 +1,84 @@
+/* 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 0000000000..5ba3689a22
--- /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 0000000000..c59a74b94a
--- /dev/null
+++ b/toolkit/components/maintenanceservice/serviceinstall.cpp
@@ -0,0 +1,760 @@
+/* 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 "updatererrors.h"
+#include "commonupdatedir.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\\MozillaMaintenan" \
+ L"ceService"
+
+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";
+ mozilla::UniquePtr<char[]> serviceString;
+ int result = ReadStrings(path, kServiceKeys, kNumStrings, &serviceString);
+ if (result != OK) {
+ results->serviceDescription = mozilla::MakeUnique<char[]>(1);
+ results->serviceDescription.get()[0] = '\0';
+ }
+ results->serviceDescription.swap(serviceString);
+ 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. (%lu)", 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. (%lu)",
+ 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. (%lu)",
+ GetLastError()));
+ return FALSE;
+ }
+
+ if (!PathRemoveFileSpecW(updaterINIPath)) {
+ LOG_WARN(
+ ("Could not remove file spec when attempting to "
+ "modify service description. (%lu)",
+ GetLastError()));
+ return FALSE;
+ }
+
+ if (!PathAppendSafe(updaterINIPath, L"updater.ini")) {
+ LOG_WARN(
+ ("Could not append updater.ini filename when attempting to "
+ "modify service description. (%lu)",
+ GetLastError()));
+ return FALSE;
+ }
+
+ if (GetFileAttributesW(updaterINIPath) == INVALID_FILE_ATTRIBUTES) {
+ LOG_WARN(
+ ("updater.ini file does not exist, will not modify "
+ "service description. (%lu)",
+ GetLastError()));
+ return FALSE;
+ }
+
+ MaintenanceServiceStringTable serviceStrings;
+ int rv = ReadMaintenanceServiceStrings(updaterINIPath, &serviceStrings);
+ if (rv != OK || !strlen(serviceStrings.serviceDescription.get())) {
+ LOG_WARN(
+ ("updater.ini file does not contain a maintenance "
+ "service description."));
+ return FALSE;
+ }
+
+ int bufferSize = MultiByteToWideChar(
+ CP_UTF8, 0, serviceStrings.serviceDescription.get(), -1, nullptr, 0);
+ mozilla::UniquePtr<WCHAR[]> serviceDescription =
+ mozilla::MakeUnique<WCHAR[]>(bufferSize);
+ if (!MultiByteToWideChar(CP_UTF8, 0, serviceStrings.serviceDescription.get(),
+ -1, serviceDescription.get(), bufferSize)) {
+ LOG_WARN(("Could not convert description to wide string format. (%lu)",
+ GetLastError()));
+ return FALSE;
+ }
+
+ SERVICE_DESCRIPTIONW descriptionConfig;
+ descriptionConfig.lpDescription = serviceDescription.get();
+ if (!ChangeServiceConfig2W(serviceHandle, SERVICE_CONFIG_DESCRIPTION,
+ &descriptionConfig)) {
+ LOG_WARN(("Could not change service config. (%lu)", 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. (%lu)", GetLastError()));
+ return FALSE;
+ }
+ if (!PathAppendSafe(fixedPath, L"maintenanceservice.exe")) {
+ LOG_WARN(("Couldn't append file spec. (%lu)", 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. (%lu)", 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. (%lu)", 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. (%lu)",
+ 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. (%lu)", 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. (%lu)",
+ 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. (%lu)",
+ 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. (%lu)",
+ 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. (%lu)",
+ 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. (%lu)",
+ 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. (%lu)",
+ 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. (%lu)",
+ 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. (%lu)",
+ 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. (%lu)", GetLastError()));
+ return FALSE;
+ }
+
+ // Open the service
+ nsAutoServiceHandle schService(
+ OpenServiceW(schSCManager, SVC_NAME, SERVICE_ALL_ACCESS));
+ if (!schService) {
+ LOG_WARN(("Could not open service. (%lu)", 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. (%lu)", 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: %lu", 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. (%lu)", GetLastError()));
+ return FALSE;
+ }
+
+ // Open the service
+ nsAutoServiceHandle schService(
+ OpenServiceW(schSCManager, SVC_NAME, SERVICE_ALL_ACCESS));
+ if (!schService) {
+ LOG_WARN(("Could not open service. (%lu)", 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. (%lu)",
+ GetLastError()));
+ return GetLastError();
+ }
+
+ DWORD size = needed;
+ psd = (PSECURITY_DESCRIPTOR)LocalAlloc(LPTR, size);
+ if (!psd) {
+ LOG_WARN(
+ ("Could not allocate security descriptor. (%lu)", 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. (%lu)", 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. (%lu)", GetLastError()));
+ return GetLastError();
+ }
+
+ PSID sidBuiltinUsers;
+ DWORD SIDSize = SECURITY_MAX_SID_SIZE;
+ sidBuiltinUsers = LocalAlloc(LMEM_FIXED, SIDSize);
+ if (!sidBuiltinUsers) {
+ LOG_WARN(("Could not allocate SID memory. (%lu)", GetLastError()));
+ return GetLastError();
+ }
+ UniqueSidPtr uniqueSidBuiltinUsers(sidBuiltinUsers);
+
+ if (!CreateWellKnownSid(WinBuiltinUsersSid, nullptr, sidBuiltinUsers,
+ &SIDSize)) {
+ DWORD lastError = GetLastError();
+ LOG_WARN(("Could not create BI\\Users SID. (%lu)", lastError));
+ return lastError;
+ }
+
+ PSID sidInteractive;
+ SIDSize = SECURITY_MAX_SID_SIZE;
+ sidInteractive = LocalAlloc(LMEM_FIXED, SIDSize);
+ if (!sidInteractive) {
+ LOG_WARN(("Could not allocate SID memory. (%lu)", GetLastError()));
+ return GetLastError();
+ }
+ UniqueSidPtr uniqueSidInteractive(sidInteractive);
+
+ if (!CreateWellKnownSid(WinInteractiveSid, nullptr, sidInteractive,
+ &SIDSize)) {
+ DWORD lastError = GetLastError();
+ LOG_WARN(("Could not create Interactive SID. (%lu)", lastError));
+ return lastError;
+ }
+
+ const size_t eaCount = 2;
+ EXPLICIT_ACCESS ea[eaCount];
+ ZeroMemory(ea, sizeof(ea));
+ ea[0].grfAccessMode = REVOKE_ACCESS;
+ ea[0].Trustee.TrusteeForm = TRUSTEE_IS_SID;
+ ea[0].Trustee.TrusteeType = TRUSTEE_IS_GROUP;
+ ea[0].Trustee.ptstrName = static_cast<LPWSTR>(sidBuiltinUsers);
+ ea[1].grfAccessPermissions = SERVICE_START | SERVICE_STOP | GENERIC_READ;
+ ea[1].grfAccessMode = SET_ACCESS;
+ ea[1].grfInheritance = NO_INHERITANCE;
+ ea[1].Trustee.TrusteeForm = TRUSTEE_IS_SID;
+ ea[1].Trustee.TrusteeType = TRUSTEE_IS_GROUP;
+ ea[1].Trustee.ptstrName = static_cast<LPWSTR>(sidInteractive);
+
+ DWORD lastError = SetEntriesInAclW(eaCount, ea, pacl, &pNewAcl);
+ if (ERROR_SUCCESS != lastError) {
+ LOG_WARN(("Could not set entries in ACL. (%lu)", lastError));
+ return lastError;
+ }
+
+ // Initialize a new security descriptor.
+ SECURITY_DESCRIPTOR sd;
+ if (!InitializeSecurityDescriptor(&sd, SECURITY_DESCRIPTOR_REVISION)) {
+ LOG_WARN(
+ ("Could not initialize security descriptor. (%lu)", 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. (%lu)", 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. (%lu)", 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 0000000000..735bebd67f
--- /dev/null
+++ b/toolkit/components/maintenanceservice/serviceinstall.h
@@ -0,0 +1,19 @@
+/* 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 {
+ mozilla::UniquePtr<char[]> serviceDescription;
+};
diff --git a/toolkit/components/maintenanceservice/tests/gtest/ServiceStartInteractiveOnly.cpp b/toolkit/components/maintenanceservice/tests/gtest/ServiceStartInteractiveOnly.cpp
new file mode 100644
index 0000000000..beff8e8e47
--- /dev/null
+++ b/toolkit/components/maintenanceservice/tests/gtest/ServiceStartInteractiveOnly.cpp
@@ -0,0 +1,52 @@
+#include "gtest/gtest.h"
+
+#include <stdio.h>
+#include <windows.h>
+#include <sddl.h>
+
+#include "nsWindowsHelpers.h"
+
+TEST(MaintenanceServiceTest, ServiceStartInteractiveOnly)
+{
+ // First, make a restricted token that excludes the Interactive group.
+ SID_AND_ATTRIBUTES sid;
+ DWORD SIDSize = SECURITY_MAX_SID_SIZE;
+ sid.Sid = LocalAlloc(LMEM_FIXED, SIDSize);
+ // Automatically free the SID when we are done with it.
+ UniqueSidPtr uniqueSid(sid.Sid);
+ ASSERT_TRUE(sid.Sid);
+
+ BOOL success =
+ CreateWellKnownSid(WinInteractiveSid, nullptr, sid.Sid, &SIDSize);
+ ASSERT_TRUE(success);
+
+ HANDLE primaryToken;
+ success =
+ OpenProcessToken(GetCurrentProcess(), TOKEN_ALL_ACCESS, &primaryToken);
+ // Automatically close the token when we are done with it.
+ nsAutoHandle uniquePrimaryToken(primaryToken);
+ ASSERT_TRUE(success);
+
+ HANDLE restrictedToken;
+ success = CreateRestrictedToken(primaryToken, 0, 1, &sid, 0, nullptr, 0,
+ nullptr, &restrictedToken);
+ // Automatically close the token when we are done with it.
+ nsAutoHandle uniqueRestrictedToken(restrictedToken);
+ ASSERT_TRUE(success);
+
+ success = ImpersonateLoggedOnUser(restrictedToken);
+ ASSERT_TRUE(success);
+
+ SC_HANDLE scmHandle =
+ OpenSCManagerW(L"127.0.0.1", nullptr, SC_MANAGER_CONNECT);
+ // Automatically close the SCM when we are done with it.
+ nsAutoServiceHandle uniqueScmHandle(scmHandle);
+ ASSERT_TRUE(scmHandle);
+
+ SC_HANDLE serviceHandle =
+ OpenServiceW(scmHandle, L"MozillaMaintenance", SERVICE_START);
+ // Automatically close the SCM when we are done with it.
+ nsAutoServiceHandle uniqueServiceHandle(serviceHandle);
+ ASSERT_FALSE(serviceHandle);
+ ASSERT_EQ(GetLastError(), static_cast<DWORD>(ERROR_ACCESS_DENIED));
+}
diff --git a/toolkit/components/maintenanceservice/tests/gtest/moz.build b/toolkit/components/maintenanceservice/tests/gtest/moz.build
new file mode 100644
index 0000000000..201f67a69d
--- /dev/null
+++ b/toolkit/components/maintenanceservice/tests/gtest/moz.build
@@ -0,0 +1,20 @@
+# -*- Mode: python; c-basic-offset: 4; 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 https://mozilla.org/MPL/2.0/.
+
+Library("MaintenanceServiceTest")
+
+UNIFIED_SOURCES += [
+ "ServiceStartInteractiveOnly.cpp",
+]
+
+DisableStlWrapping()
+
+OS_LIBS += [
+ "advapi32",
+ "kernel32",
+]
+
+FINAL_LIBRARY = "xul-gtest"
diff --git a/toolkit/components/maintenanceservice/workmonitor.cpp b/toolkit/components/maintenanceservice/workmonitor.cpp
new file mode 100644
index 0000000000..96a098d547
--- /dev/null
+++ b/toolkit/components/maintenanceservice/workmonitor.cpp
@@ -0,0 +1,812 @@
+/* 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>
+
+#ifndef __MINGW32__
+# 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")
+#endif
+
+#include "mozilla/CmdLineAndEnvUtils.h"
+#include "nsWindowsHelpers.h"
+#include "mozilla/UniquePtr.h"
+
+using mozilla::UniquePtr;
+
+#include "workmonitor.h"
+#include "serviceinstall.h"
+#include "servicebase.h"
+#include "registrycertificates.h"
+#include "uachelper.h"
+#include "updatehelper.h"
+#include "pathhash.h"
+#include "updatererrors.h"
+#include "commonupdatedir.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;
+BOOL PathGetSiblingFilePath(LPWSTR destinationBuffer, LPCWSTR siblingFilePath,
+ LPCWSTR newFileName);
+BOOL DoesFallbackKeyExist();
+
+/*
+ * Reads the secure update status file and sets isApplying to true if the status
+ * is set to applying.
+ *
+ * @param patchDirPath
+ * The update patch directory path
+ * @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 patchDirPath, BOOL& isApplying) {
+ isApplying = FALSE;
+ WCHAR statusFilePath[MAX_PATH + 1] = {L'\0'};
+ if (!GetSecureOutputFilePath(patchDirPath, L".status", statusFilePath)) {
+ LOG_WARN(("Could not get path for the secure update status file"));
+ return FALSE;
+ }
+
+ nsAutoHandle statusFile(
+ CreateFileW(statusFilePath, 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) {
+ processStarted = FALSE;
+
+ LOG(("Starting update process as the service in session 0."));
+ STARTUPINFOW si;
+ PROCESS_INFORMATION pi;
+
+ ZeroMemory(&si, sizeof(si));
+ ZeroMemory(&pi, sizeof(pi));
+ si.cb = sizeof(si);
+ si.lpDesktop = const_cast<LPWSTR>(L"winsta0\\Default"); // -Wwritable-strings
+
+ // The updater command line is of the form:
+ // updater.exe update-dir apply [wait-pid [callback-dir callback-path args]]
+ auto cmdLine = mozilla::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 = const_cast<LPWSTR>(L""); // -Wwritable-strings
+ 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.get()));
+ processStarted =
+ CreateProcessW(argv[0], cmdLine.get(), 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 %lu.", 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. (%lu)",
+ argv[0], cmdLine.get(), lastError));
+ }
+
+ // Empty value on putenv is how you remove an env variable in Windows
+ putenv(const_cast<char*>("MOZ_USING_SERVICE="));
+
+ 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 (%lu)", updater,
+ GetLastError()));
+ if (!WriteStatusFailure(updateDir, SERVICE_UPDATER_NOT_FIXED_DRIVE)) {
+ LOG_WARN(("Could not write update.status service update failure. (%lu)",
+ 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. (%lu)",
+ GetLastError()));
+ if (!WriteStatusFailure(updateDir, SERVICE_COULD_NOT_LOCK_UPDATER)) {
+ LOG_WARN(("Could not write update.status service update failure. (%lu)",
+ 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. (%lu)", 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. (%lu)",
+ 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 (%lu)",
+ 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. (%lu)",
+ GetLastError()));
+ }
+ }
+ }
+ } else {
+ result = FALSE;
+ LOG_WARN(
+ ("Could not start process due to certificate check error on "
+ "updater.exe. Updating update.status. (%lu)",
+ 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. (%lu)",
+ 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. (%lu)",
+ GetLastError()));
+ return FALSE;
+ }
+
+ if (!PathRemoveFileSpecW(serviceUpdaterPath)) {
+ LOG_WARN(
+ ("Couldn't remove file spec when attempting to use a secure "
+ "updater path. (%lu)",
+ GetLastError()));
+ return FALSE;
+ }
+
+ if (!PathAppendSafe(serviceUpdaterPath, L"update")) {
+ LOG_WARN(
+ ("Couldn't append file spec when attempting to use a secure "
+ "updater path. (%lu)",
+ 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. (%lu)",
+ 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] is automatically
+ * included by Windows, argv[1] is unused but hardcoded to
+ * "MozillaMaintenance", and 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.
+ WCHAR uuidString[MAX_PATH + 1] = {L'\0'};
+ if (GetUUIDString(uuidString)) {
+ LOG(("Executing service command %ls, ID: %ls", argv[2], uuidString));
+ } else {
+ // The ID is only used by tests, so failure to allocate it isn't fatal.
+ LOG(("Executing service command %ls", argv[2]));
+ }
+
+ 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;
+ }
+
+ // The patch directory path must end with updates\0 to use the maintenance
+ // service.
+ size_t fullPathLen = NS_tstrlen(argv[4]);
+ size_t relPathLen = NS_tstrlen(PATCH_DIR_PATH);
+ if (relPathLen > fullPathLen) {
+ LOG_WARN(
+ ("The patch directory path length is not valid for this "
+ "application."));
+ return FALSE;
+ }
+
+ if (_wcsnicmp(argv[4] + fullPathLen - relPathLen, PATCH_DIR_PATH,
+ relPathLen) != 0) {
+ LOG_WARN(
+ ("The patch directory path subdirectory is not valid for this "
+ "application."));
+ return FALSE;
+ }
+
+ // Remove the secure output files so it is easier to determine when new
+ // files are created in the unelevated updater.
+ RemoveSecureOutputFiles(argv[4]);
+
+ // Create a new secure ID for this update.
+ if (!WriteSecureIDFile(argv[4])) {
+ LOG_WARN(("Unable to write to secure ID file."));
+ 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])
+ // This build flag is used as a handy proxy to tell when we're a build made
+ // for local testing, because there isn't much other reason to set it.
+#ifndef DISABLE_UPDATER_AUTHENTICODE_CHECK
+ || !IsProgramFilesPath(argv[5])
+#endif
+ ) {
+ 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 (argc <= 7 ||
+ (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. (%lu)",
+ 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 that 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' (ignored); "
+ "Install dir has: '%ls'; "
+ "Using this path for updating: '%ls'.",
+ argv[3], installDirUpdater, secureUpdaterPath));
+ DeleteSecureUpdater(secureUpdaterPath);
+ result = CopyFileW(installDirUpdater, secureUpdaterPath, FALSE);
+ }
+
+ if (!result) {
+ LOG_WARN(
+ ("Could not copy path to secure location. (%lu)", 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'. (%lu)",
+ 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 result;
+}
diff --git a/toolkit/components/maintenanceservice/workmonitor.h b/toolkit/components/maintenanceservice/workmonitor.h
new file mode 100644
index 0000000000..8fc2e51a7d
--- /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);