999 lines
36 KiB
C++
999 lines
36 KiB
C++
/* 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 "nsWindowsHelpers.h"
|
|
|
|
#include "mozilla/CmdLineAndEnvUtils.h"
|
|
#include "mozilla/NotNull.h"
|
|
#include "mozilla/UniquePtr.h"
|
|
#include "mozilla/Unused.h"
|
|
|
|
using namespace mozilla;
|
|
|
|
#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();
|
|
|
|
/**
|
|
* The updater is always the same version as the application, so there is no
|
|
* need for it to keep track of argument versioning. But the Maintenance
|
|
* Service may be called upon to update old versions of the application that are
|
|
* also installed. So it has to be able to handle any past argument format
|
|
* version.
|
|
*/
|
|
enum UpdaterArgVersion {
|
|
// The version 1 format looks like
|
|
// updater patch-dir apply-to-dir wait-pid [callback-working-dir callback-path
|
|
// args...]
|
|
Version1,
|
|
// The version 2 format looks like
|
|
// updater patch-dir install-dir apply-to-dir [wait-pid [callback-working-dir
|
|
// callback-path args...]]
|
|
Version2,
|
|
// The version 3 format looks like
|
|
// updater 3 patch-dir install-dir apply-to-dir which-invocation [wait-pid
|
|
// [callback-working-dir callback-path args...]]
|
|
Version3,
|
|
};
|
|
|
|
/**
|
|
* Represents the arguments passed to the MMS symbolically rather than
|
|
* numerically so that we don't have to do a bunch of version checking and
|
|
* index juggling every time we want a value.
|
|
*
|
|
* Should only be instantiated via `parseUpdaterArgs`.
|
|
*
|
|
* Raw character pointers will be references to within `argv` and are guaranteed
|
|
* not to be `null`.
|
|
*
|
|
* It's very intentional that the only non-optional raw argument pointers are
|
|
* the updater and the patch directory. It is important that `parseUpdaterArgs`
|
|
* be as permissive as possible by always making a best effort attempt to return
|
|
* at least the patch directory so that we can write a failure status there,
|
|
* even if none of the other arguments are valid.
|
|
*/
|
|
struct UpdaterArgs {
|
|
UpdaterArgVersion version;
|
|
UniquePtr<wchar_t[]> fullCommandLine;
|
|
NotNull<wchar_t*> updaterBin;
|
|
NotNull<wchar_t*> patchDirPath;
|
|
Maybe<NotNull<wchar_t*>> installDirPath;
|
|
Maybe<NotNull<wchar_t*>> applyToDirPath;
|
|
Maybe<NotNull<wchar_t*>> whichInvocation;
|
|
Maybe<NotNull<wchar_t*>> waitPid;
|
|
Maybe<NotNull<wchar_t*>> callbackWorkingDir;
|
|
Maybe<NotNull<wchar_t*>> callbackBinPath;
|
|
// The callback arguments are currently not included here (other than in
|
|
// `fullCommandLine`) simply because we do not need them in the Maintenance
|
|
// Service (other than to pass unmodified to the updater).
|
|
};
|
|
|
|
Maybe<NotNull<wchar_t*>> optionalArg(int argc, wchar_t** argv, int index) {
|
|
if (argc > index) {
|
|
return Some(WrapNotNull(argv[index]));
|
|
}
|
|
return Nothing();
|
|
}
|
|
|
|
/**
|
|
* 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_t* str) {
|
|
while (*str) {
|
|
if (!iswdigit(*str++)) {
|
|
return FALSE;
|
|
}
|
|
}
|
|
return TRUE;
|
|
}
|
|
|
|
void logParam(const char* name, Maybe<NotNull<wchar_t*>>& maybeValue) {
|
|
if (maybeValue) {
|
|
LOG(("Loaded param %s as \"%S\"", name, maybeValue.value().get()));
|
|
} else {
|
|
LOG(("Loaded param %s as Nothing", name));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* See `UpdaterArgs`.
|
|
* Returns `Nothing` if the arguments can't be parsed at all.
|
|
*/
|
|
Maybe<UpdaterArgs> parseUpdaterArgs(int argc, wchar_t** argv) {
|
|
if (argc < 1) {
|
|
LOG_WARN(("Argument parsing failed: No arguments!"));
|
|
return Nothing();
|
|
}
|
|
Maybe<NotNull<wchar_t*>> updaterBin = Some(WrapNotNull(argv[0]));
|
|
|
|
UniquePtr<wchar_t[]> fullCommandLine = mozilla::MakeCommandLine(argc, argv);
|
|
LOG(("Command Line: %S", fullCommandLine.get()));
|
|
|
|
UpdaterArgVersion version;
|
|
Maybe<NotNull<wchar_t*>> patchDirPath = Nothing();
|
|
Maybe<NotNull<wchar_t*>> installDirPath = Nothing();
|
|
Maybe<NotNull<wchar_t*>> applyToDirPath = Nothing();
|
|
Maybe<NotNull<wchar_t*>> whichInvocation = Nothing();
|
|
Maybe<NotNull<wchar_t*>> waitPid = Nothing();
|
|
Maybe<NotNull<wchar_t*>> callbackWorkingDir = Nothing();
|
|
Maybe<NotNull<wchar_t*>> callbackBinPath = Nothing();
|
|
if (argc > 1 && wcscmp(argv[1], L"3") == 0) {
|
|
LOG(("Identified argument format version 3"));
|
|
version = UpdaterArgVersion::Version3;
|
|
|
|
// The version 3 format looks like
|
|
// index 0 1 2 3 4 5
|
|
// updater 3 patch-dir install-dir apply-to-dir which-invocation
|
|
// index 6 7 8 9+
|
|
// [wait-pid [callback-working-dir callback-path args...]]
|
|
if (argc < 3) {
|
|
LOG_WARN(("No arguments for version 3"));
|
|
return Nothing();
|
|
}
|
|
patchDirPath = Some(WrapNotNull(argv[2]));
|
|
installDirPath = optionalArg(argc, argv, 3);
|
|
applyToDirPath = optionalArg(argc, argv, 4);
|
|
whichInvocation = optionalArg(argc, argv, 5);
|
|
waitPid = optionalArg(argc, argv, 6);
|
|
callbackWorkingDir = optionalArg(argc, argv, 7);
|
|
callbackBinPath = optionalArg(argc, argv, 8);
|
|
} else if ((argc == 4 && wcscmp(argv[3], L"-1") == 0) ||
|
|
(argc >= 4 &&
|
|
(wcsstr(argv[3], L"/replace") != nullptr || isDigits(argv[3])))) {
|
|
LOG(("Identified argument format version 1"));
|
|
version = UpdaterArgVersion::Version1;
|
|
|
|
// The version 1 format looks like
|
|
// index 0 1 2 3 4
|
|
// updater patch-dir apply-to-dir wait-pid [callback-working-dir
|
|
// index 5 6+
|
|
// callback-path args...]
|
|
patchDirPath = Some(WrapNotNull(argv[1]));
|
|
applyToDirPath = Some(WrapNotNull(argv[2]));
|
|
waitPid = Some(WrapNotNull(argv[3]));
|
|
callbackWorkingDir = optionalArg(argc, argv, 4);
|
|
callbackBinPath = optionalArg(argc, argv, 5);
|
|
} else {
|
|
LOG(("Identified argument format version 2"));
|
|
version = UpdaterArgVersion::Version2;
|
|
|
|
// The version 2 format looks like
|
|
// index 0 1 2 3 4
|
|
// updater patch-dir install-dir apply-to-dir [wait-pid
|
|
// index 5 6 7+
|
|
// [callback-working-dir callback-path args...]]
|
|
if (argc < 2) {
|
|
LOG_WARN(("No arguments for version 2"));
|
|
return Nothing();
|
|
}
|
|
patchDirPath = Some(WrapNotNull(argv[1]));
|
|
installDirPath = optionalArg(argc, argv, 2);
|
|
applyToDirPath = optionalArg(argc, argv, 3);
|
|
waitPid = optionalArg(argc, argv, 4);
|
|
callbackWorkingDir = optionalArg(argc, argv, 5);
|
|
callbackBinPath = optionalArg(argc, argv, 6);
|
|
}
|
|
|
|
logParam("updaterBin", updaterBin);
|
|
logParam("patchDirPath", patchDirPath);
|
|
logParam("installDirPath", installDirPath);
|
|
logParam("applyToDirPath", applyToDirPath);
|
|
logParam("whichInvocation", whichInvocation);
|
|
logParam("waitPid", waitPid);
|
|
logParam("callbackWorkingDir", callbackWorkingDir);
|
|
logParam("callbackBinPath", callbackBinPath);
|
|
|
|
return Some(UpdaterArgs{
|
|
.version = version,
|
|
.fullCommandLine = std::move(fullCommandLine),
|
|
.updaterBin = updaterBin.value(),
|
|
.patchDirPath = patchDirPath.value(),
|
|
.installDirPath = installDirPath,
|
|
.applyToDirPath = applyToDirPath,
|
|
.whichInvocation = whichInvocation,
|
|
.waitPid = waitPid,
|
|
.callbackWorkingDir = callbackWorkingDir,
|
|
.callbackBinPath = callbackBinPath,
|
|
});
|
|
}
|
|
|
|
/*
|
|
* 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 args The updater arguments.
|
|
* @return boolean True if we're staging an update
|
|
*/
|
|
static bool IsUpdateBeingStaged(const UpdaterArgs& args) {
|
|
// PID will be set to -1 if we're supposed to stage an update.
|
|
return args.waitPid && wcscmp(args.waitPid.value(), L"-1") == 0;
|
|
}
|
|
|
|
/**
|
|
* Determines whether the update request we are servicing is a replace request.
|
|
*
|
|
* @param args The updater arguments.
|
|
* @return boolean True if this is a replace request
|
|
*/
|
|
static bool IsUpdateAReplaceRequest(const UpdaterArgs& args) {
|
|
return args.waitPid && wcsstr(args.waitPid.value(), L"/replace");
|
|
}
|
|
|
|
/**
|
|
* Gets the installation directory from the arguments passed to updater.exe.
|
|
*
|
|
* @param args The updater arguments.
|
|
* @param aResultDir Buffer to hold the installation directory.
|
|
*/
|
|
static BOOL GetInstallationDir(const UpdaterArgs& args,
|
|
WCHAR aResultDir[MAX_PATH + 1]) {
|
|
if (args.installDirPath) {
|
|
wcsncpy(aResultDir, args.installDirPath.value(), MAX_PATH);
|
|
} else if (args.applyToDirPath) {
|
|
if (args.version != UpdaterArgVersion::Version1) {
|
|
// In version 1, we infer the install directory from the "apply to"
|
|
// directory (i.e. using it directly or converting "dir\Firefox\updated"
|
|
// to "dir\Firefox"). But this is only an appropriate conversion to make
|
|
// in version 1, when (a) the arguments were guaranteed to have
|
|
// a format that would work like this, and (b) it was valid to not specify
|
|
// the install directory as an argument.
|
|
return FALSE;
|
|
}
|
|
wcsncpy(aResultDir, args.applyToDirPath.value(), MAX_PATH);
|
|
} else {
|
|
return FALSE;
|
|
}
|
|
|
|
WCHAR* backSlash = wcsrchr(aResultDir, L'\\');
|
|
// Make sure that the path does not include trailing backslashes
|
|
if (backSlash && (backSlash[1] == L'\0')) {
|
|
*backSlash = L'\0';
|
|
}
|
|
|
|
// Handle the version 1 "dir\Firefox\updated" to "dir\Firefox" conversion.
|
|
if (!args.installDirPath &&
|
|
(IsUpdateBeingStaged(args) || IsUpdateAReplaceRequest(args))) {
|
|
return PathRemoveFileSpecW(aResultDir);
|
|
}
|
|
return TRUE;
|
|
}
|
|
|
|
/**
|
|
* Runs an update process as the service using the SYSTEM account.
|
|
*
|
|
* @param args The updater arguments.
|
|
* @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(const UpdaterArgs& args, 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""); // -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", args.fullCommandLine.get()));
|
|
processStarted = CreateProcessW(
|
|
args.updaterBin, args.fullCommandLine.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(args.patchDirPath, isApplying) && isApplying) {
|
|
if (updateWasSuccessful) {
|
|
LOG(
|
|
("update.status is still applying even though update was "
|
|
"successful."));
|
|
if (!WriteStatusFailure(args.patchDirPath,
|
|
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(args.patchDirPath, 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)",
|
|
args.updaterBin.get(), args.fullCommandLine.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 Patch directory, where the update status file is.
|
|
*
|
|
* @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;
|
|
}
|
|
|
|
return DoesBinaryMatchAllowedCertificates(installDir, updater);
|
|
}
|
|
|
|
/**
|
|
* Processes a software update command
|
|
*
|
|
* @param args The updater arguments.
|
|
*
|
|
* @return TRUE if the update was successful.
|
|
*/
|
|
BOOL ProcessSoftwareUpdateCommand(const UpdaterArgs& args) {
|
|
BOOL result = TRUE;
|
|
if (!args.installDirPath && !args.applyToDirPath) {
|
|
LOG_WARN(
|
|
("Not enough command line parameters specified. "
|
|
"Updating update.status."));
|
|
|
|
if (!WriteStatusFailure(args.patchDirPath,
|
|
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(args, installDir)) {
|
|
LOG_WARN(("Could not get the installation directory"));
|
|
if (!WriteStatusFailure(args.patchDirPath, SERVICE_INSTALLDIR_ERROR)) {
|
|
LOG_WARN(
|
|
("Could not write update.status for GetInstallationDir failure."));
|
|
}
|
|
return FALSE;
|
|
}
|
|
|
|
if (UpdaterIsValid(args.updaterBin, installDir, args.patchDirPath)) {
|
|
BOOL updateProcessWasStarted = FALSE;
|
|
if (StartUpdateProcess(args, 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(args)) {
|
|
// We might not execute code after StartServiceUpdate because
|
|
// the service installer will stop the service if it is running.
|
|
LOG(("Starting service update"));
|
|
StartServiceUpdate(installDir);
|
|
} else {
|
|
LOG(("Skipping update of the service because we are staging"));
|
|
}
|
|
} 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(args.patchDirPath,
|
|
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(args.patchDirPath, 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) {
|
|
const int serviceArgCount = 3;
|
|
if (argc < serviceArgCount) {
|
|
LOG_WARN(
|
|
("Not enough command line arguments to execute a service command"));
|
|
return FALSE;
|
|
}
|
|
|
|
const wchar_t* serviceName = argv[1];
|
|
const wchar_t* serviceCommand = argv[2];
|
|
|
|
// 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", serviceCommand, uuidString));
|
|
} else {
|
|
// The ID is only used by tests, so failure to allocate it isn't fatal.
|
|
LOG(("Executing service command %ls", serviceCommand));
|
|
}
|
|
|
|
BOOL result = FALSE;
|
|
if (!lstrcmpi(serviceCommand, L"software-update")) {
|
|
Maybe<UpdaterArgs> maybeArgs =
|
|
parseUpdaterArgs(argc - serviceArgCount, argv + serviceArgCount);
|
|
if (!maybeArgs) {
|
|
// Not really much we can do here. `parseUpdaterArgs` is extremely
|
|
// permissive. If it failed, we don't even have a patch directory to write
|
|
// an error to.
|
|
LOG_WARN(("Unable to parse updater arguments!"));
|
|
return FALSE;
|
|
}
|
|
UpdaterArgs args = maybeArgs.extract();
|
|
|
|
// This check is also performed in updater.cpp and is performed here
|
|
// as well since the maintenance service can be called directly.
|
|
if (!IsValidFullPath(args.patchDirPath)) {
|
|
// 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(args.patchDirPath);
|
|
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(args.patchDirPath + 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(args.patchDirPath);
|
|
|
|
// Create a new secure ID for this update.
|
|
if (!WriteSecureIDFile(args.patchDirPath)) {
|
|
LOG_WARN(("Unable to write to secure ID file."));
|
|
return FALSE;
|
|
}
|
|
|
|
if (args.version == UpdaterArgVersion::Version1) {
|
|
// This check is also performed in updater.cpp and is performed here
|
|
// as well since the maintenance service can be called directly.
|
|
if (!args.applyToDirPath || !IsValidFullPath(args.applyToDirPath.value())
|
|
// 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(args.applyToDirPath.value())
|
|
#endif
|
|
) {
|
|
LOG_WARN(
|
|
("The apply-to directory path is not valid for this application."));
|
|
if (!WriteStatusFailure(args.patchDirPath,
|
|
SERVICE_INVALID_INSTALL_DIR_PATH_ERROR)) {
|
|
LOG_WARN(("Could not write update.status for previous failure."));
|
|
}
|
|
return FALSE;
|
|
}
|
|
} else {
|
|
// This check is also performed in updater.cpp and is performed here
|
|
// as well since the maintenance service can be called directly.
|
|
if (!args.installDirPath || !IsValidFullPath(args.installDirPath.value())
|
|
// 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(args.installDirPath.value())
|
|
#endif
|
|
) {
|
|
LOG_WARN(
|
|
("The install directory path is not valid for this application."));
|
|
if (!WriteStatusFailure(args.patchDirPath,
|
|
SERVICE_INVALID_INSTALL_DIR_PATH_ERROR)) {
|
|
LOG_WARN(("Could not write update.status for previous failure."));
|
|
}
|
|
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 (!args.applyToDirPath ||
|
|
!IsValidFullPath(args.applyToDirPath.value())) {
|
|
LOG_WARN(
|
|
("The working directory path is not valid for this application."));
|
|
if (!WriteStatusFailure(args.patchDirPath,
|
|
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(args.applyToDirPath.value(), args.installDirPath.value(),
|
|
MAX_PATH) != 0) {
|
|
if (!IsUpdateBeingStaged(args) && !IsUpdateAReplaceRequest(args)) {
|
|
LOG_WARN(
|
|
("Installation directory and working directory must be the "
|
|
"same for non-staged updates. Exiting."));
|
|
if (!WriteStatusFailure(args.patchDirPath,
|
|
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"), args.applyToDirPath.value().get());
|
|
if (!PathRemoveFileSpecW(workingDirParent)) {
|
|
LOG_WARN(
|
|
("Couldn't remove file spec when attempting to verify the "
|
|
"working directory path. (%lu)",
|
|
GetLastError()));
|
|
if (!WriteStatusFailure(args.patchDirPath, REMOVE_FILE_SPEC_ERROR)) {
|
|
LOG_WARN(("Could not write update.status for previous failure."));
|
|
}
|
|
return FALSE;
|
|
}
|
|
|
|
if (_wcsnicmp(workingDirParent, args.installDirPath.value(),
|
|
MAX_PATH) != 0) {
|
|
LOG_WARN(
|
|
("The apply-to directory must be the same as or "
|
|
"the direct child of the installation directory! Exiting."));
|
|
if (!WriteStatusFailure(args.patchDirPath,
|
|
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(args, installDir)) {
|
|
LOG_WARN(("Could not get the installation directory"));
|
|
if (!WriteStatusFailure(args.patchDirPath, 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(args.patchDirPath,
|
|
SERVICE_INSTALL_DIR_REG_ERROR)) {
|
|
LOG_WARN(("Could not write update.status for previous failure."));
|
|
}
|
|
return FALSE;
|
|
}
|
|
RegCloseKey(baseKey);
|
|
} else {
|
|
if (!WriteStatusFailure(args.patchDirPath,
|
|
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);
|
|
result = PathAppendSafe(installDirUpdater, L"updater.exe");
|
|
if (!result) {
|
|
LOG_WARN(("Install directory updater could not be determined."));
|
|
}
|
|
|
|
if (result) {
|
|
result = UpdaterIsValid(installDirUpdater, installDir, args.patchDirPath);
|
|
}
|
|
|
|
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'.",
|
|
args.updaterBin.get(), installDirUpdater, secureUpdaterPath));
|
|
DeleteSecureUpdater(secureUpdaterPath);
|
|
result = CopyFileW(installDirUpdater, secureUpdaterPath, FALSE);
|
|
}
|
|
|
|
if (!result) {
|
|
LOG_WARN(
|
|
("Could not copy path to secure location. (%lu)", GetLastError()));
|
|
if (!WriteStatusFailure(args.patchDirPath,
|
|
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.
|
|
args.updaterBin = WrapNotNull(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(args);
|
|
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.
|
|
} else {
|
|
LOG_WARN(("Service command not recognized: %ls.", serviceCommand));
|
|
// result is already set to FALSE
|
|
}
|
|
|
|
LOG(("%ls service command %ls complete with result: %ls.", serviceName,
|
|
serviceCommand, result ? L"Success" : L"Failure"));
|
|
return result;
|
|
}
|