/** * 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; }
/** * 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, BOOL &processStarted) { LOG(("Starting update process as the service in session 0.\n")); STARTUPINFO si = {0}; si.cb = sizeof(STARTUPINFO); si.lpDesktop = L"winsta0\\Default"; PROCESS_INFORMATION pi = {0}; // The updater command line is of the form: // updater.exe update-dir apply [wait-pid [callback-dir callback-path args]] LPWSTR cmdLine = MakeCommandLine(argc, argv); // 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 >= 2 ) { // Setting the desktop to blank will ensure no GUI is displayed si.lpDesktop = L""; si.dwFlags |= STARTF_USESHOWWINDOW; si.wShowWindow = SW_HIDE; } // We move the updater.ini file out of the way because we will handle // executing PostUpdate through the service. We handle PostUpdate from // the service because there are some per user things that happen that // can't run in session 0 which we run updater.exe in. // Once we are done running updater.exe we rename updater.ini back so // that if there were any errors the next updater.exe will run correctly. WCHAR updaterINI[MAX_PATH + 1]; WCHAR updaterINITemp[MAX_PATH + 1]; BOOL selfHandlePostUpdate = FALSE; // We use the updater.ini from the same directory as the updater.exe // because of background updates. if (PathGetSiblingFilePath(updaterINI, argv[0], L"updater.ini") && PathGetSiblingFilePath(updaterINITemp, argv[0], L"updater.tmp")) { selfHandlePostUpdate = MoveFileExW(updaterINI, updaterINITemp, MOVEFILE_REPLACE_EXISTING); } // 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\n", cmdLine)); processStarted = CreateProcessW(argv[0], cmdLine, NULL, NULL, FALSE, CREATE_DEFAULT_ERROR_MODE, NULL, NULL, &si, &pi); // Empty value on putenv is how you remove an env variable in Windows putenv(const_cast<char*>("MOZ_USING_SERVICE=")); BOOL updateWasSuccessful = FALSE; if (processStarted) { // Wait for the updater process to finish LOG(("Process was started... waiting on result.\n")); 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); } else { // Check the return code of updater.exe to make sure we get 0 DWORD returnCode; if (GetExitCodeProcess(pi.hProcess, &returnCode)) { LOG(("Process finished with return code %d.\n", returnCode)); // updater returns 0 if successful. updateWasSuccessful = (returnCode == 0); } else { LOG(("Process finished but could not obtain return code.\n")); } } 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 know update " " was successful.\n")); if (!WriteStatusFailure(argv[1], SERVICE_STILL_APPLYING_ON_SUCCESS)) { LOG(("Could not write update.status still applying on" " success error.\n")); } // Since we still had applying we know updater.exe didn't do its // job correctly. updateWasSuccessful = FALSE; } else { LOG(("update.status is still applying and update was not successful.\n")); if (!WriteStatusFailure(argv[1], SERVICE_STILL_APPLYING_ON_FAILURE)) { LOG(("Could not write update.status still applying on" " success error.\n")); } } } } else { DWORD lastError = GetLastError(); LOG(("Could not create process as current user, " "updaterPath: %ls; cmdLine: %l. (%d)\n", argv[0], cmdLine, lastError)); } // Now that we're done with the update, restore back the updater.ini file // We use it ourselves, and also we want it back in case we had any type // of error so that the normal update process can use it. if (selfHandlePostUpdate) { MoveFileExW(updaterINITemp, updaterINI, MOVEFILE_REPLACE_EXISTING); // Only run the PostUpdate if the update was successful if (updateWasSuccessful && argc > 2) { LPCWSTR installationDir = argv[2]; LPCWSTR updateInfoDir = argv[1]; // Launch the PostProcess with admin access in session 0. This is // actually launching the post update process but it takes in the // callback app path to figure out where to apply to. // The PostUpdate process with user only access will be done inside // the unelevated updater.exe after the update process is complete // from the service. We don't know here which session to start // the user PostUpdate process from. LOG(("Launching post update process as the service in session 0.\n")); if (!LaunchWinPostProcess(installationDir, updateInfoDir, true, NULL)) { LOG(("The post update process could not be launched.\n")); } } } free(cmdLine); return updateWasSuccessful; }
/** * Executes a service command. * * @param argc The number of arguments in argv * @param argv The service command line arguments, argv[0] and argv[1] * and automatically included by Windows. argv[2] is the * service command. * * @return FALSE if there was an error executing the service command. */ BOOL ExecuteServiceCommand(int argc, LPWSTR *argv) { if (argc < 3) { LOG_WARN(("Not enough command line arguments to execute a service command")); return FALSE; } // The tests work by making sure the log has changed, so we put a // unique ID in the log. RPC_WSTR guidString = RPC_WSTR(L""); GUID guid; HRESULT hr = CoCreateGuid(&guid); if (SUCCEEDED(hr)) { UuidToString(&guid, &guidString); } LOG(("Executing service command %ls, ID: %ls", argv[2], reinterpret_cast<LPCWSTR>(guidString))); RpcStringFree(&guidString); BOOL result = FALSE; if (!lstrcmpi(argv[2], L"software-update")) { // Use the passed in command line arguments for the update, except for the // path to updater.exe. We copy updater.exe to a the directory of the // MozillaMaintenance service so that a low integrity process cannot // replace the updater.exe at any point and use that for the update. // It also makes DLL injection attacks harder. LPWSTR oldUpdaterPath = argv[3]; WCHAR secureUpdaterPath[MAX_PATH + 1] = { L'\0' }; result = GetSecureUpdaterPath(secureUpdaterPath); // Does its own logging if (result) { LOG(("Passed in path: '%ls'; Using this path for updating: '%ls'.", oldUpdaterPath, secureUpdaterPath)); DeleteSecureUpdater(secureUpdaterPath); result = CopyFileW(oldUpdaterPath, secureUpdaterPath, FALSE); } if (!result) { LOG_WARN(("Could not copy path to secure location. (%d)", GetLastError())); if (argc > 4 && !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 oldUpdaterINIPath[MAX_PATH + 1] = { L'\0' }; WCHAR secureUpdaterINIPath[MAX_PATH + 1] = { L'\0' }; if (PathGetSiblingFilePath(secureUpdaterINIPath, secureUpdaterPath, L"updater.ini") && PathGetSiblingFilePath(oldUpdaterINIPath, oldUpdaterPath, L"updater.ini")) { // This is non fatal if it fails there is no real harm if (!CopyFileW(oldUpdaterINIPath, secureUpdaterINIPath, FALSE)) { LOG_WARN(("Could not copy updater.ini from: '%ls' to '%ls'. (%d)", oldUpdaterINIPath, secureUpdaterINIPath, GetLastError())); } } result = ProcessSoftwareUpdateCommand(argc - 3, argv + 3); DeleteSecureUpdater(secureUpdaterPath); } // We might not reach here if the service install succeeded // because the service self updates itself and the service // installer will stop the service. LOG(("Service command %ls complete.", argv[2])); } else { LOG_WARN(("Service command not recognized: %ls.", argv[2])); // result is already set to FALSE } LOG(("service command %ls complete with result: %ls.", argv[1], (result ? L"Success" : L"Failure"))); return TRUE; }
/** * Launch the post update application as the specified user (helper.exe). * It takes in the path of the callback application to calculate the path * of helper.exe. For service updates this is called from both the system * account and the current user account. * * @param installationDir The path to the callback application binary. * @param updateInfoDir The directory where update info is stored. * @param forceSync If true even if the ini file specifies async, the * process will wait for termination of PostUpdate. * @param userToken The user token to run as, if nullptr the current * user will be used. * @return TRUE if there was no error starting the process. */ BOOL LaunchWinPostProcess(const WCHAR *installationDir, const WCHAR *updateInfoDir, bool forceSync, HANDLE userToken) { WCHAR workingDirectory[MAX_PATH + 1] = { L'\0' }; wcsncpy(workingDirectory, installationDir, MAX_PATH); // Launch helper.exe to perform post processing (e.g. registry and log file // modifications) for the update. WCHAR inifile[MAX_PATH + 1] = { L'\0' }; wcsncpy(inifile, installationDir, MAX_PATH); if (!PathAppendSafe(inifile, L"updater.ini")) { return FALSE; } WCHAR exefile[MAX_PATH + 1]; WCHAR exearg[MAX_PATH + 1]; WCHAR exeasync[10]; bool async = true; if (!GetPrivateProfileStringW(L"PostUpdateWin", L"ExeRelPath", nullptr, exefile, MAX_PATH + 1, inifile)) { return FALSE; } if (!GetPrivateProfileStringW(L"PostUpdateWin", L"ExeArg", nullptr, exearg, MAX_PATH + 1, inifile)) { return FALSE; } if (!GetPrivateProfileStringW(L"PostUpdateWin", L"ExeAsync", L"TRUE", exeasync, sizeof(exeasync)/sizeof(exeasync[0]), inifile)) { return FALSE; } WCHAR exefullpath[MAX_PATH + 1] = { L'\0' }; wcsncpy(exefullpath, installationDir, MAX_PATH); if (!PathAppendSafe(exefullpath, exefile)) { return false; } WCHAR dlogFile[MAX_PATH + 1]; if (!PathGetSiblingFilePath(dlogFile, exefullpath, L"uninstall.update")) { return FALSE; } WCHAR slogFile[MAX_PATH + 1] = { L'\0' }; wcsncpy(slogFile, updateInfoDir, MAX_PATH); if (!PathAppendSafe(slogFile, L"update.log")) { return FALSE; } WCHAR dummyArg[14] = { L'\0' }; wcsncpy(dummyArg, L"argv0ignored ", sizeof(dummyArg) / sizeof(dummyArg[0]) - 1); size_t len = wcslen(exearg) + wcslen(dummyArg); WCHAR *cmdline = (WCHAR *) malloc((len + 1) * sizeof(WCHAR)); if (!cmdline) { return FALSE; } wcsncpy(cmdline, dummyArg, len); wcscat(cmdline, exearg); if (forceSync || !_wcsnicmp(exeasync, L"false", 6) || !_wcsnicmp(exeasync, L"0", 2)) { async = false; } // We want to launch the post update helper app to update the Windows // registry even if there is a failure with removing the uninstall.update // file or copying the update.log file. CopyFileW(slogFile, dlogFile, false); STARTUPINFOW si = {sizeof(si), 0}; si.lpDesktop = L""; PROCESS_INFORMATION pi = {0}; bool ok; if (userToken) { ok = CreateProcessAsUserW(userToken, exefullpath, cmdline, nullptr, // no special security attributes nullptr, // no special thread attributes false, // don't inherit filehandles 0, // No special process creation flags nullptr, // inherit my environment workingDirectory, &si, &pi); } else { ok = CreateProcessW(exefullpath, cmdline, nullptr, // no special security attributes nullptr, // no special thread attributes false, // don't inherit filehandles 0, // No special process creation flags nullptr, // inherit my environment workingDirectory, &si, &pi); } free(cmdline); if (ok) { if (!async) WaitForSingleObject(pi.hProcess, INFINITE); CloseHandle(pi.hProcess); CloseHandle(pi.hThread); } return ok; }
/** * Starts the upgrade process for update of the service if it is * already installed. * * @param installDir the installation directory where * maintenanceservice_installer.exe is located. * @return TRUE if successful */ BOOL StartServiceUpdate(LPCWSTR installDir) { // Get a handle to the local computer SCM database SC_HANDLE manager = OpenSCManager(nullptr, nullptr, SC_MANAGER_ALL_ACCESS); if (!manager) { return FALSE; } // Open the service SC_HANDLE svc = OpenServiceW(manager, SVC_NAME, SERVICE_ALL_ACCESS); if (!svc) { CloseServiceHandle(manager); return FALSE; } // If we reach here, then the service is installed, so // proceed with upgrading it. CloseServiceHandle(manager); // The service exists and we opened it, get the config bytes needed DWORD bytesNeeded; if (!QueryServiceConfigW(svc, nullptr, 0, &bytesNeeded) && GetLastError() != ERROR_INSUFFICIENT_BUFFER) { CloseServiceHandle(svc); return FALSE; } // Get the service config information, in particular we want the binary // path of the service. mozilla::ScopedDeleteArray<char> serviceConfigBuffer(new char[bytesNeeded]); if (!QueryServiceConfigW(svc, reinterpret_cast<QUERY_SERVICE_CONFIGW*>(serviceConfigBuffer.get()), bytesNeeded, &bytesNeeded)) { CloseServiceHandle(svc); return FALSE; } CloseServiceHandle(svc); QUERY_SERVICE_CONFIGW &serviceConfig = *reinterpret_cast<QUERY_SERVICE_CONFIGW*>(serviceConfigBuffer.get()); PathUnquoteSpacesW(serviceConfig.lpBinaryPathName); // Obtain the temp path of the maintenance service binary WCHAR tmpService[MAX_PATH + 1] = { L'\0' }; if (!PathGetSiblingFilePath(tmpService, serviceConfig.lpBinaryPathName, L"maintenanceservice_tmp.exe")) { return FALSE; } // Get the new maintenance service path from the install dir WCHAR newMaintServicePath[MAX_PATH + 1] = { L'\0' }; wcsncpy(newMaintServicePath, installDir, MAX_PATH); PathAppendSafe(newMaintServicePath, L"maintenanceservice.exe"); // Copy the temp file in alongside the maintenace service. // This is a requirement for maintenance service upgrades. if (!CopyFileW(newMaintServicePath, tmpService, FALSE)) { return FALSE; } // Start the upgrade comparison process STARTUPINFOW si = {0}; si.cb = sizeof(STARTUPINFOW); // No particular desktop because no UI si.lpDesktop = L""; PROCESS_INFORMATION pi = {0}; WCHAR cmdLine[64] = { '\0' }; wcsncpy(cmdLine, L"dummyparam.exe upgrade", sizeof(cmdLine) / sizeof(cmdLine[0]) - 1); BOOL svcUpdateProcessStarted = CreateProcessW(tmpService, cmdLine, nullptr, nullptr, FALSE, 0, nullptr, installDir, &si, &pi); if (svcUpdateProcessStarted) { CloseHandle(pi.hProcess); CloseHandle(pi.hThread); } return svcUpdateProcessStarted; }