/* 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/. */
/**
* Manifest Format
* ---------------
*
* contents = 1*( line )
* line = method LWS *( param LWS ) CRLF
* CRLF = "\r\n"
* LWS = 1*( " " | "\t" )
*
* Available methods for the manifest file:
*
* updatev3.manifest
* -----------------
* method = "add" | "add-if" | "add-if-not" | "patch" | "patch-if" |
* "remove" | "rmdir" | "rmrfdir" | type
*
* 'add-if-not' adds a file if it doesn't exist.
*
* 'type' is the update type (e.g. complete or partial) and when present MUST
* be the first entry in the update manifest. The type is used to support
* removing files that no longer exist when when applying a complete update by
* causing the actions defined in the precomplete file to be performed.
*
* precomplete
* -----------
* method = "remove" | "rmdir"
*/
#include "bspatch.h"
#include "progressui.h"
#include "archivereader.h"
#include "readstrings.h"
#include "updatererrors.h"
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <limits.h>
#include <errno.h>
#include "updatecommon.h"
#ifdef XP_MACOSX
# include
"UpdateSettingsUtil.h"
# include
"updaterfileutils_osx.h"
#endif // XP_MACOSX
#include "mozilla/CmdLineAndEnvUtils.h"
#include "mozilla/UniquePtr.h"
#ifdef XP_WIN
# include
"mozilla/Maybe.h"
# include
"mozilla/WinHeaderOnlyUtils.h"
# include <climits>
#endif // XP_WIN
// Amount of the progress bar to use in each of the 3 update stages,
// should total 100.0.
#define PROGRESS_PREPARE_SIZE 20.0f
#define PROGRESS_EXECUTE_SIZE 75.0f
#define PROGRESS_FINISH_SIZE 5.0f
// Maximum amount of time in ms to wait for the parent process to close. The 30
// seconds is rather long but there have been bug reports where the parent
// process has exited after 10 seconds and it is better to give it a chance.
#define PARENT_WAIT 30000
#if defined(XP_MACOSX)
// These functions are defined in launchchild_osx.mm
void CleanupElevatedMacUpdate(
bool aFailureOccurred);
bool IsOwnedByGroupAdmin(
const char* aAppBundle);
bool IsRecursivelyWritable(
const char* aPath);
void LaunchMacApp(
int argc,
const char** argv);
void LaunchMacPostProcess(
const char* aAppBundle);
bool ObtainUpdaterArguments(
int* aArgc,
char*** aArgv,
MARChannelStringTable* aMARStrings);
bool ServeElevatedUpdate(
int aArgc,
const char** aArgv,
const char* aMARChannelID);
void SetGroupOwnershipAndPermissions(
const char* aAppBundle);
bool PerformInstallationFromDMG(
int argc,
char** argv);
struct UpdateServerThreadArgs {
int argc;
const NS_tchar** argv;
const char* marChannelID;
};
#endif
#ifndef _O_BINARY
# define _O_BINARY 0
#endif
#ifndef NULL
# define NULL (0)
#endif
#ifndef SSIZE_MAX
# define SSIZE_MAX LONG_MAX
#endif
// We want to use execv to invoke the callback executable on platforms where
// we were launched using execv. See nsUpdateDriver.cpp.
#if defined(XP_UNIX) && !
defined(XP_MACOSX)
# define USE_EXECV
#endif
#if defined(XP_OPENBSD)
# define stat64 stat
#endif
#if defined(MOZ_VERIFY_MAR_SIGNATURE) &&
defined(MAR_NSS)
# include
"nss.h"
# include
"prerror.h"
#endif
#include "crctable.h"
#ifdef XP_WIN
# ifdef MOZ_MAINTENANCE_SERVICE
# include
"registrycertificates.h"
# endif
BOOL PathAppendSafe(LPWSTR base, LPCWSTR extra);
BOOL PathGetSiblingFilePath(LPWSTR destinationBuffer, LPCWSTR siblingFilePath,
LPCWSTR newFileName);
# include
"updatehelper.h"
// Closes the handle if valid and if the updater is elevated returns with the
// return code specified. This prevents multiple launches of the callback
// application by preventing the elevated process from launching the callback.
# define EXIT_WHEN_ELEVATED(path, handle, retCode) \
{ \
if (handle != INVALID_HANDLE_VALUE) { \
CloseHandle(handle); \
} \
if (NS_tremove(path) && errno != ENOENT) { \
return retCode; \
} \
}
#endif
//-----------------------------------------------------------------------------
// This BZ2_crc32Table variable lives in libbz2. We just took the
// data structure from bz2 and created crctables.h
static unsigned int crc32(
const unsigned char* buf,
unsigned int len) {
unsigned int crc = 0xffffffffL;
const unsigned char* end = buf + len;
for (; buf != end; ++buf)
crc = (crc << 8) ^ BZ2_crc32Table[(crc >> 24) ^ *buf];
crc = ~crc;
return crc;
}
//-----------------------------------------------------------------------------
// A simple stack based container for a FILE struct that closes the
// file descriptor from its destructor.
class AutoFile {
public:
explicit AutoFile(FILE* file = nullptr) : mFile(file) {}
~AutoFile() { close(); }
AutoFile&
operator=(FILE* file) {
close();
mFile = file;
return *
this;
}
operator FILE*() {
return mFile; }
FILE* get() {
return mFile; }
private:
FILE* mFile;
void close() {
if (mFile != nullptr) {
int rv = fclose(mFile);
if (rv != 0) {
LOG((
"File close did not execute successfully"));
}
mFile = nullptr;
}
}
};
//-----------------------------------------------------------------------------
#ifdef XP_MACOSX
// Just a simple class that sets a umask value in its constructor and resets
// it in its destructor.
class UmaskContext {
public:
explicit UmaskContext(mode_t umaskToSet);
~UmaskContext();
private:
mode_t mPreviousUmask;
};
UmaskContext::UmaskContext(mode_t umaskToSet) {
mPreviousUmask = umask(umaskToSet);
}
UmaskContext::~UmaskContext() { umask(mPreviousUmask); }
#endif
//-----------------------------------------------------------------------------
typedef void (*ThreadFunc)(
void* param);
#ifdef XP_WIN
# include <process.h>
class Thread {
public:
int Run(ThreadFunc func,
void* param) {
mThreadFunc = func;
mThreadParam = param;
unsigned int threadID;
mThread =
(HANDLE)_beginthreadex(nullptr, 0, ThreadMain,
this, 0, &threadID);
return mThread ? 0 : -1;
}
int Join() {
WaitForSingleObject(mThread, INFINITE);
CloseHandle(mThread);
return 0;
}
private:
static unsigned __stdcall ThreadMain(
void* p) {
Thread* self = (Thread*)p;
self->mThreadFunc(self->mThreadParam);
return 0;
}
HANDLE mThread;
ThreadFunc mThreadFunc;
void* mThreadParam;
};
#elif defined(XP_UNIX)
# include <pthread.h>
class Thread {
public:
int Run(ThreadFunc func,
void* param) {
return pthread_create(&thr, nullptr, (
void* (*)(
void*))func, param);
}
int Join() {
void* result;
return pthread_join(thr, &result);
}
private:
pthread_t thr;
};
#else
# error
"Unsupported platform"
#endif
//-----------------------------------------------------------------------------
static NS_tchar gPatchDirPath[MAXPATHLEN];
static NS_tchar gInstallDirPath[MAXPATHLEN];
static NS_tchar gWorkingDirPath[MAXPATHLEN];
MOZ_RUNINIT
static ArchiveReader gArchiveReader;
static bool gSucceeded =
false;
static bool sStagedUpdate =
false;
static bool sReplaceRequest =
false;
static bool sUsingService =
false;
// The updater binary can potentially run twice. It will always initially run
// with `gIsElevated == false`. If it is run an additional time with elevation,
// that iteration will run with `gIsElevated == true`.
static bool gIsElevated =
false;
// This string contains the MAR channel IDs that are later extracted by one of
// the `ReadMARChannelIDsFrom` variants.
MOZ_RUNINIT
static MARChannelStringTable gMARStrings;
// Normally, we run updates as a result of user action (the user started Firefox
// or clicked a "Restart to Update" button). But there are some cases when
// we are not:
// a) The callback app is a background task. If true then the updater is
// likely being run as part of a background task.
// The updater could be run with no callback, but this only happens
// when performing a staged update (see calls to ProcessUpdates), and there
// are already checks for sStagedUpdate when showing UI or elevating.
// b) The environment variable MOZ_APP_SILENT_START is set and not empty. This
// is set, for instance, on macOS when Firefox had no windows open for a
// while and restarted to apply updates.
//
// In these cases, the update should be installed silently, so we shouldn't:
// a) show progress UI
// b) prompt for elevation
static bool sUpdateSilently =
false;
#ifdef XP_WIN
static NS_tchar gCallbackRelPath[MAXPATHLEN];
static NS_tchar gCallbackBackupPath[MAXPATHLEN];
static NS_tchar gDeleteDirPath[MAXPATHLEN];
// Whether to copy the update-elevated.log and update.status file to the update
// patch directory from a secure directory.
static bool gCopyOutputFiles =
false;
// Whether to write the update-elevated.log and update.status file to a secure
// directory.
static bool gUseSecureOutputPath =
false;
#endif
static const NS_tchar kWhitespace[] = NS_T(
" \t");
static const NS_tchar kNL[] = NS_T(
"\r\n");
static const NS_tchar kQuote[] = NS_T(
"\"");
static inline size_t mmin(size_t a, size_t b) {
return (a > b) ? b : a; }
static NS_tchar* mstrtok(
const NS_tchar* delims, NS_tchar** str) {
if (!*str || !**str) {
*str = nullptr;
return nullptr;
}
// skip leading "whitespace"
NS_tchar* ret = *str;
const NS_tchar* d;
do {
for (d = delims; *d != NS_T(
'\0'); ++d) {
if (*ret == *d) {
++ret;
break;
}
}
}
while (*d);
if (!*ret) {
*str = ret;
return nullptr;
}
NS_tchar* i = ret;
do {
for (d = delims; *d != NS_T(
'\0'); ++d) {
if (*i == *d) {
*i = NS_T(
'\0');
*str = ++i;
return ret;
}
}
++i;
}
while (*i);
*str = nullptr;
return ret;
}
#if defined(TEST_UPDATER) ||
defined(XP_WIN) ||
defined(XP_MACOSX)
static bool EnvHasValue(
const char* name) {
const char* val = getenv(name);
return (val && *val);
}
#endif
static const NS_tchar* UpdateLogFilename() {
if (gIsElevated) {
return NS_T(
"update-elevated.log");
}
return NS_T(
"update.log");
}
#ifdef XP_WIN
/**
* Obtains the update ID from the secure id file located in secure output
* directory.
*
* @param outBuf
* A buffer of size UUID_LEN (e.g. 37) to store the result. The uuid is
* 36 characters in length and 1 more for null termination.
* @return true if successful
*/
bool GetSecureID(
char* outBuf) {
NS_tchar idFilePath[MAX_PATH + 1] = {L
'\0'};
if (!GetSecureOutputFilePath(gPatchDirPath, L
".id", idFilePath)) {
return false;
}
AutoFile idFile(NS_tfopen(idFilePath, NS_T(
"rb")));
if (idFile == nullptr) {
return false;
}
size_t read = fread(outBuf, UUID_LEN - 1, 1, idFile);
if (read != 1) {
return false;
}
outBuf[UUID_LEN - 1] =
'\0';
return true;
}
#endif
/**
* Calls LogFinish for the update log. On Windows, the unelevated updater copies
* the update status file and the update log file that were written by the
* elevated updater from the secure directory to the update patch directory.
*
* NOTE: All calls to WriteStatusFile MUST happen before calling output_finish
* because this function copies the update status file for the elevated
* updater and writing the status file after calling output_finish will
* overwrite it.
*/
static void output_finish() {
LogFinish();
#ifdef XP_WIN
if (gCopyOutputFiles) {
NS_tchar srcStatusPath[MAXPATHLEN + 1] = {NS_T(
'\0')};
if (GetSecureOutputFilePath(gPatchDirPath, L
".status", srcStatusPath)) {
NS_tchar dstStatusPath[MAXPATHLEN + 1] = {NS_T(
'\0')};
NS_tsnprintf(dstStatusPath,
sizeof(dstStatusPath) /
sizeof(dstStatusPath[0]),
NS_T(
"%s\\update.status"), gPatchDirPath);
CopyFileW(srcStatusPath, dstStatusPath,
false);
}
NS_tchar srcLogPath[MAXPATHLEN + 1] = {NS_T(
'\0')};
if (GetSecureOutputFilePath(gPatchDirPath, L
".log", srcLogPath)) {
NS_tchar dstLogPath[MAXPATHLEN + 1] = {NS_T(
'\0')};
// Unconditionally use "update-elevated.log" here rather than
// `UpdateLogFilename` since (a) secure output files are only created by
// elevated instances and (b) the copying of the secure output file is
// done by the unelevated instance, so `UpdateLogFilename` will return
// the wrong thing for this.
NS_tsnprintf(dstLogPath,
sizeof(dstLogPath) /
sizeof(dstLogPath[0]),
NS_T(
"%s\\update-elevated.log"), gPatchDirPath);
CopyFileW(srcLogPath, dstLogPath,
false);
}
}
#endif
}
/**
* Coverts a relative update path to a full path.
*
* @param relpath
* The relative path to convert to a full path.
* @return valid filesystem full path or nullptr if memory allocation fails.
*/
static NS_tchar* get_full_path(
const NS_tchar* relpath) {
NS_tchar* destpath = sStagedUpdate ? gWorkingDirPath : gInstallDirPath;
size_t lendestpath = NS_tstrlen(destpath);
size_t lenrelpath = NS_tstrlen(relpath);
NS_tchar* s =
new NS_tchar[lendestpath + lenrelpath + 2];
NS_tchar* c = s;
NS_tstrcpy(c, destpath);
c += lendestpath;
NS_tstrcat(c, NS_T(
"/"));
c++;
NS_tstrcat(c, relpath);
c += lenrelpath;
*c = NS_T(
'\0');
return s;
}
/**
* Converts a full update path into a relative path; reverses get_full_path.
*
* @param fullpath
* The absolute path to convert into a relative path.
* return pointer to the location within fullpath where the relative path starts
* or fullpath itself if it already looks relative.
*/
#ifndef XP_WIN
static const NS_tchar* get_relative_path(
const NS_tchar* fullpath) {
if (fullpath[0] !=
'/') {
return fullpath;
}
NS_tchar* prefix = sStagedUpdate ? gWorkingDirPath : gInstallDirPath;
// If the path isn't long enough to be absolute, return it as-is.
if (NS_tstrlen(fullpath) <= NS_tstrlen(prefix)) {
return fullpath;
}
return fullpath + NS_tstrlen(prefix) + 1;
}
#endif
/**
* Gets the platform specific path and performs simple checks to the path. If
* the path checks don't pass nullptr will be returned.
*
* @param line
* The line from the manifest that contains the path.
* @param isdir
* Whether the path is a directory path. Defaults to false.
* @return valid filesystem path or nullptr if the path checks fail.
*/
static NS_tchar* get_valid_path(NS_tchar** line,
bool isdir =
false) {
NS_tchar* path = mstrtok(kQuote, line);
if (!path) {
LOG((
"get_valid_path: unable to determine path: " LOG_S, *line));
return nullptr;
}
// All paths must be relative from the current working directory
if (path[0] == NS_T(
'/')) {
LOG((
"get_valid_path: path must be relative: " LOG_S, path));
return nullptr;
}
#ifdef XP_WIN
// All paths must be relative from the current working directory
if (path[0] == NS_T(
'\\') || path[1] == NS_T(
':')) {
LOG((
"get_valid_path: path must be relative: " LOG_S, path));
return nullptr;
}
#endif
if (isdir) {
// Directory paths must have a trailing forward slash.
if (path[NS_tstrlen(path) - 1] != NS_T(
'/')) {
LOG(
(
"get_valid_path: directory paths must have a trailing forward "
"slash: " LOG_S,
path));
return nullptr;
}
// Remove the trailing forward slash because stat on Windows will return
// ENOENT if the path has a trailing slash.
path[NS_tstrlen(path) - 1] = NS_T(
'\0');
}
// Don't allow relative paths that resolve to a parent directory.
if (NS_tstrstr(path, NS_T(
"..")) != nullptr) {
LOG((
"get_valid_path: paths must not contain '..': " LOG_S, path));
return nullptr;
}
return path;
}
/*
* Gets a quoted path. The return value is malloc'd and it is the responsibility
* of the caller to free it.
*
* @param path
* The path to quote.
* @return On success the quoted path and nullptr otherwise.
*/
static NS_tchar* get_quoted_path(
const NS_tchar* path) {
size_t lenQuote = NS_tstrlen(kQuote);
size_t lenPath = NS_tstrlen(path);
size_t len = lenQuote + lenPath + lenQuote + 1;
NS_tchar* s = (NS_tchar*)malloc(len *
sizeof(NS_tchar));
if (!s) {
return nullptr;
}
NS_tchar* c = s;
NS_tstrcpy(c, kQuote);
c += lenQuote;
NS_tstrcat(c, path);
c += lenPath;
NS_tstrcat(c, kQuote);
c += lenQuote;
*c = NS_T(
'\0');
return s;
}
static void ensure_write_permissions(
const NS_tchar* path) {
#ifdef XP_WIN
(
void)_wchmod(path, _S_IREAD | _S_IWRITE);
#else
struct stat fs;
if (!stat(path, &fs) && !(fs.st_mode & S_IWUSR)) {
(
void)chmod(path, fs.st_mode | S_IWUSR);
}
#endif
}
static int ensure_remove(
const NS_tchar* path) {
ensure_write_permissions(path);
int rv = NS_tremove(path);
if (rv) {
LOG((
"ensure_remove: failed to remove file: " LOG_S
", rv: %d, err: %d",
path, rv, errno));
}
return rv;
}
// Remove the directory pointed to by path and all of its files and
// sub-directories.
static int ensure_remove_recursive(
const NS_tchar* path,
bool continueEnumOnFailure =
false) {
// We use lstat rather than stat here so that we can successfully remove
// symlinks.
struct NS_tstat_t sInfo;
int rv = NS_tlstat(path, &sInfo);
if (rv) {
// This error is benign
return rv;
}
if (!S_ISDIR(sInfo.st_mode)) {
return ensure_remove(path);
}
NS_tDIR* dir;
NS_tdirent* entry;
dir = NS_topendir(path);
if (!dir) {
LOG((
"ensure_remove_recursive: unable to open directory: " LOG_S
", rv: %d, err: %d",
path, rv, errno));
return rv;
}
while ((entry = NS_treaddir(dir)) != 0) {
if (NS_tstrcmp(entry->d_name, NS_T(
".")) &&
NS_tstrcmp(entry->d_name, NS_T(
".."))) {
NS_tchar childPath[MAXPATHLEN];
NS_tsnprintf(childPath,
sizeof(childPath) /
sizeof(childPath[0]),
NS_T(
"%s/%s"), path, entry->d_name);
rv = ensure_remove_recursive(childPath);
if (rv && !continueEnumOnFailure) {
break;
}
}
}
NS_tclosedir(dir);
if (rv == OK) {
ensure_write_permissions(path);
rv = NS_trmdir(path);
if (rv) {
LOG((
"ensure_remove_recursive: unable to remove directory: " LOG_S
", rv: %d, err: %d",
path, rv, errno));
}
}
return rv;
}
static bool is_read_only(
const NS_tchar* flags) {
size_t length = NS_tstrlen(flags);
if (length == 0) {
return false;
}
// Make sure the string begins with "r"
if (flags[0] != NS_T(
'r')) {
return false;
}
// Look for "r+" or "r+b"
if (length > 1 && flags[1] == NS_T(
'+')) {
return false;
}
// Look for "rb+"
if (NS_tstrcmp(flags, NS_T(
"rb+")) == 0) {
return false;
}
return true;
}
static FILE* ensure_open(
const NS_tchar* path,
const NS_tchar* flags,
unsigned int options) {
ensure_write_permissions(path);
FILE* f = NS_tfopen(path, flags);
if (is_read_only(flags)) {
// Don't attempt to modify the file permissions if the file is being opened
// in read-only mode.
return f;
}
if (NS_tchmod(path, options) != 0) {
if (f != nullptr) {
fclose(f);
}
return nullptr;
}
struct NS_tstat_t ss;
if (NS_tstat(path, &ss) != 0 || ss.st_mode != options) {
if (f != nullptr) {
fclose(f);
}
return nullptr;
}
return f;
}
// Ensure that the directory containing this file exists.
static int ensure_parent_dir(
const NS_tchar* path) {
int rv = OK;
NS_tchar* slash = (NS_tchar*)NS_tstrrchr(path, NS_T(
'/'));
if (slash) {
*slash = NS_T(
'\0');
rv = ensure_parent_dir(path);
// Only attempt to create the directory if we're not at the root
if (rv == OK && *path) {
rv = NS_tmkdir(path, 0755);
// If the directory already exists, then ignore the error.
if (rv < 0 && errno != EEXIST) {
LOG((
"ensure_parent_dir: failed to create directory: " LOG_S
", "
"err: %d",
path, errno));
rv = WRITE_ERROR;
}
else {
rv = OK;
}
}
*slash = NS_T(
'/');
}
return rv;
}
#ifdef XP_UNIX
static int ensure_copy_symlink(
const NS_tchar* path,
const NS_tchar* dest) {
// Copy symlinks by creating a new symlink to the same target
NS_tchar target[MAXPATHLEN + 1] = {NS_T(
'\0')};
int rv = readlink(path, target, MAXPATHLEN);
if (rv == -1) {
LOG((
"ensure_copy_symlink: failed to read the link: " LOG_S
", err: %d",
path, errno));
return READ_ERROR;
}
rv = symlink(target, dest);
if (rv == -1) {
LOG((
"ensure_copy_symlink: failed to create the new link: " LOG_S
", target: " LOG_S
" err: %d",
dest, target, errno));
return READ_ERROR;
}
return 0;
}
#endif
// Copy the file named path onto a new file named dest.
static int ensure_copy(
const NS_tchar* path,
const NS_tchar* dest) {
#ifdef XP_WIN
// Fast path for Windows
bool result = CopyFileW(path, dest,
false);
if (!result) {
LOG((
"ensure_copy: failed to copy the file " LOG_S
" over to " LOG_S
", lasterr: %lx",
path, dest, GetLastError()));
return WRITE_ERROR_FILE_COPY;
}
return OK;
#else
struct NS_tstat_t ss;
int rv = NS_tlstat(path, &ss);
if (rv) {
LOG((
"ensure_copy: failed to read file status info: " LOG_S
", err: %d",
path, errno));
return READ_ERROR;
}
# ifdef XP_UNIX
if (S_ISLNK(ss.st_mode)) {
return ensure_copy_symlink(path, dest);
}
# endif
AutoFile infile(ensure_open(path, NS_T(
"rb"), ss.st_mode));
if (!infile) {
LOG((
"ensure_copy: failed to open the file for reading: " LOG_S
", err: %d",
path, errno));
return READ_ERROR;
}
AutoFile outfile(ensure_open(dest, NS_T(
"wb"), ss.st_mode));
if (!outfile) {
LOG((
"ensure_copy: failed to open the file for writing: " LOG_S
", err: %d",
dest, errno));
return WRITE_ERROR;
}
// This block size was chosen pretty arbitrarily but seems like a reasonable
// compromise. For example, the optimal block size on a modern OS X machine
// is 100k */
const int blockSize = 32 * 1024;
void* buffer = malloc(blockSize);
if (!buffer) {
return UPDATER_MEM_ERROR;
}
while (!feof(infile.get())) {
size_t read = fread(buffer, 1, blockSize, infile);
if (ferror(infile.get())) {
LOG((
"ensure_copy: failed to read the file: " LOG_S
", err: %d", path,
errno));
free(buffer);
return READ_ERROR;
}
size_t written = 0;
while (written < read) {
size_t chunkWritten = fwrite(buffer, 1, read - written, outfile);
if (chunkWritten <= 0) {
LOG((
"ensure_copy: failed to write the file: " LOG_S
", err: %d", dest,
errno));
free(buffer);
return WRITE_ERROR_FILE_COPY;
}
written += chunkWritten;
}
}
rv = NS_tchmod(dest, ss.st_mode);
free(buffer);
return rv;
#endif
}
template <
unsigned N>
struct copy_recursive_skiplist {
NS_tchar paths[N][MAXPATHLEN];
void append(
unsigned index,
const NS_tchar* path,
const NS_tchar* suffix) {
NS_tsnprintf(paths[index], MAXPATHLEN, NS_T(
"%s/%s"), path, suffix);
}
bool find(
const NS_tchar* path) {
for (
int i = 0; i <
static_cast<
int>(N); ++i) {
if (!NS_tstricmp(paths[i], path)) {
return true;
}
}
return false;
}
};
// Copy all of the files and subdirectories under path to a new directory named
// dest. The path names in the skiplist will be skipped and will not be copied.
template <
unsigned N>
static int ensure_copy_recursive(
const NS_tchar* path,
const NS_tchar* dest,
copy_recursive_skiplist<N>& skiplist) {
struct NS_tstat_t sInfo;
int rv = NS_tlstat(path, &sInfo);
if (rv) {
LOG((
"ensure_copy_recursive: path doesn't exist: " LOG_S
", rv: %d, err: %d",
path, rv, errno));
return READ_ERROR;
}
#ifdef XP_UNIX
if (S_ISLNK(sInfo.st_mode)) {
return ensure_copy_symlink(path, dest);
}
#endif
if (!S_ISDIR(sInfo.st_mode)) {
return ensure_copy(path, dest);
}
rv = NS_tmkdir(dest, sInfo.st_mode);
if (rv < 0 && errno != EEXIST) {
LOG((
"ensure_copy_recursive: could not create destination directory: " LOG_S
", rv: %d, err: %d",
path, rv, errno));
return WRITE_ERROR;
}
NS_tDIR* dir;
NS_tdirent* entry;
dir = NS_topendir(path);
if (!dir) {
LOG((
"ensure_copy_recursive: path is not a directory: " LOG_S
", rv: %d, err: %d",
path, rv, errno));
return READ_ERROR;
}
while ((entry = NS_treaddir(dir)) != 0) {
if (NS_tstrcmp(entry->d_name, NS_T(
".")) &&
NS_tstrcmp(entry->d_name, NS_T(
".."))) {
NS_tchar childPath[MAXPATHLEN];
NS_tsnprintf(childPath,
sizeof(childPath) /
sizeof(childPath[0]),
NS_T(
"%s/%s"), path, entry->d_name);
if (skiplist.find(childPath)) {
continue;
}
NS_tchar childPathDest[MAXPATHLEN];
NS_tsnprintf(childPathDest,
sizeof(childPathDest) /
sizeof(childPathDest[0]),
NS_T(
"%s/%s"), dest, entry->d_name);
rv = ensure_copy_recursive(childPath, childPathDest, skiplist);
if (rv) {
break;
}
}
}
NS_tclosedir(dir);
return rv;
}
// Renames the specified file to the new file specified. If the destination file
// exists it is removed.
static int rename_file(
const NS_tchar* spath,
const NS_tchar* dpath,
bool allowDirs =
false) {
int rv = ensure_parent_dir(dpath);
if (rv) {
return rv;
}
struct NS_tstat_t spathInfo;
rv = NS_tstat(spath, &spathInfo);
if (rv) {
LOG((
"rename_file: failed to read file status info: " LOG_S
", "
"err: %d",
spath, errno));
return READ_ERROR;
}
if (!S_ISREG(spathInfo.st_mode)) {
if (allowDirs && !S_ISDIR(spathInfo.st_mode)) {
LOG((
"rename_file: path present, but not a file: " LOG_S
", err: %d",
spath, errno));
return RENAME_ERROR_EXPECTED_FILE;
}
LOG((
"rename_file: proceeding to rename the directory"));
}
if (!NS_taccess(dpath, F_OK)) {
if (ensure_remove(dpath)) {
LOG(
(
"rename_file: destination file exists and could not be "
"removed: " LOG_S,
dpath));
return WRITE_ERROR_DELETE_FILE;
}
}
if (NS_trename(spath, dpath) != 0) {
LOG((
"rename_file: failed to rename file - src: " LOG_S
", "
"dst:" LOG_S
", err: %d",
spath, dpath, errno));
return WRITE_ERROR;
}
return OK;
}
#ifdef XP_WIN
// Remove the directory pointed to by path and all of its files and
// sub-directories. If a file is in use move it to the tobedeleted directory
// and attempt to schedule removal of the file on reboot
static int remove_recursive_on_reboot(
const NS_tchar* path,
const NS_tchar* deleteDir) {
struct NS_tstat_t sInfo;
int rv = NS_tlstat(path, &sInfo);
if (rv) {
// This error is benign
return rv;
}
if (!S_ISDIR(sInfo.st_mode)) {
NS_tchar tmpDeleteFile[MAXPATHLEN + 1];
GetUUIDTempFilePath(deleteDir, L
"rep", tmpDeleteFile);
if (NS_tremove(tmpDeleteFile) && errno != ENOENT) {
LOG((
"remove_recursive_on_reboot: failed to remove temporary file: " LOG_S
", err: %d",
tmpDeleteFile, errno));
}
rv = rename_file(path, tmpDeleteFile,
false);
if (MoveFileEx(rv ? path : tmpDeleteFile, nullptr,
MOVEFILE_DELAY_UNTIL_REBOOT)) {
LOG(
(
"remove_recursive_on_reboot: file will be removed on OS "
"reboot: " LOG_S,
rv ? path : tmpDeleteFile));
}
else {
LOG((
"remove_recursive_on_reboot: failed to schedule OS reboot removal of "
"file: " LOG_S,
rv ? path : tmpDeleteFile));
}
return rv;
}
NS_tDIR* dir;
NS_tdirent* entry;
dir = NS_topendir(path);
if (!dir) {
LOG((
"remove_recursive_on_reboot: unable to open directory: " LOG_S
", rv: %d, err: %d",
path, rv, errno));
return rv;
}
while ((entry = NS_treaddir(dir)) != 0) {
if (NS_tstrcmp(entry->d_name, NS_T(
".")) &&
NS_tstrcmp(entry->d_name, NS_T(
".."))) {
NS_tchar childPath[MAXPATHLEN];
NS_tsnprintf(childPath,
sizeof(childPath) /
sizeof(childPath[0]),
NS_T(
"%s/%s"), path, entry->d_name);
// There is no need to check the return value of this call since this
// function is only called after an update is successful and there is not
// much that can be done to recover if it isn't successful. There is also
// no need to log the value since it will have already been logged.
remove_recursive_on_reboot(childPath, deleteDir);
}
}
NS_tclosedir(dir);
if (rv == OK) {
ensure_write_permissions(path);
rv = NS_trmdir(path);
if (rv) {
LOG((
"remove_recursive_on_reboot: unable to remove directory: " LOG_S
", rv: %d, err: %d",
path, rv, errno));
}
}
return rv;
}
#endif
//-----------------------------------------------------------------------------
// Create a backup of the specified file by renaming it.
static int backup_create(
const NS_tchar* path) {
NS_tchar backup[MAXPATHLEN];
NS_tsnprintf(backup,
sizeof(backup) /
sizeof(backup[0]),
NS_T(
"%s") BACKUP_EXT, path);
return rename_file(path, backup);
}
// Rename the backup of the specified file that was created by renaming it back
// to the original file.
static int backup_restore(
const NS_tchar* path,
const NS_tchar* relPath) {
NS_tchar backup[MAXPATHLEN];
NS_tsnprintf(backup,
sizeof(backup) /
sizeof(backup[0]),
NS_T(
"%s") BACKUP_EXT, path);
NS_tchar relBackup[MAXPATHLEN];
NS_tsnprintf(relBackup,
sizeof(relBackup) /
sizeof(relBackup[0]),
NS_T(
"%s") BACKUP_EXT, relPath);
if (NS_taccess(backup, F_OK)) {
LOG((
"backup_restore: backup file doesn't exist: " LOG_S, relBackup));
return OK;
}
return rename_file(backup, path);
}
// Discard the backup of the specified file that was created by renaming it.
static int backup_discard(
const NS_tchar* path,
const NS_tchar* relPath) {
NS_tchar backup[MAXPATHLEN];
NS_tsnprintf(backup,
sizeof(backup) /
sizeof(backup[0]),
NS_T(
"%s") BACKUP_EXT, path);
NS_tchar relBackup[MAXPATHLEN];
NS_tsnprintf(relBackup,
sizeof(relBackup) /
sizeof(relBackup[0]),
NS_T(
"%s") BACKUP_EXT, relPath);
// Nothing to discard
if (NS_taccess(backup, F_OK)) {
return OK;
}
int rv = ensure_remove(backup);
#if defined(XP_WIN)
if (rv && !sStagedUpdate && !sReplaceRequest) {
LOG((
"backup_discard: unable to remove: " LOG_S, relBackup));
NS_tchar path[MAXPATHLEN + 1];
GetUUIDTempFilePath(gDeleteDirPath, L
"moz", path);
if (rename_file(backup, path)) {
LOG((
"backup_discard: failed to rename file:" LOG_S
", dst:" LOG_S,
relBackup, relPath));
return WRITE_ERROR_DELETE_BACKUP;
}
// The MoveFileEx call to remove the file on OS reboot will fail if the
// process doesn't have write access to the HKEY_LOCAL_MACHINE registry key
// but this is ok since the installer / uninstaller will delete the
// directory containing the file along with its contents after an update is
// applied, on reinstall, and on uninstall.
if (MoveFileEx(path, nullptr, MOVEFILE_DELAY_UNTIL_REBOOT)) {
LOG(
(
"backup_discard: file renamed and will be removed on OS "
"reboot: " LOG_S,
relPath));
}
else {
LOG(
(
"backup_discard: failed to schedule OS reboot removal of "
"file: " LOG_S,
relPath));
}
}
#else
if (rv) {
return WRITE_ERROR_DELETE_BACKUP;
}
#endif
return OK;
}
// Helper function for post-processing a temporary backup.
static void backup_finish(
const NS_tchar* path,
const NS_tchar* relPath,
int status) {
if (status == OK) {
backup_discard(path, relPath);
}
else {
backup_restore(path, relPath);
}
}
//-----------------------------------------------------------------------------
static int DoUpdate();
class Action {
public:
Action() : mProgressCost(1), mNext(nullptr) {}
virtual ~Action() =
default;
virtual int Parse(NS_tchar* line) = 0;
// Do any preprocessing to ensure that the action can be performed. Execute
// will be called if this Action and all others return OK from this method.
virtual int Prepare() = 0;
// Perform the operation. Return OK to indicate success. After all actions
// have been executed, Finish will be called. A requirement of Execute is
// that its operation be reversable from Finish.
virtual int Execute() = 0;
// Finish is called after execution of all actions. If status is OK, then
// all actions were successfully executed. Otherwise, some action failed.
virtual void Finish(
int status) = 0;
int mProgressCost;
private:
Action* mNext;
friend class ActionList;
};
class RemoveFile :
public Action {
public:
RemoveFile() : mSkip(0) {}
int Parse(NS_tchar* line) override;
int Prepare() override;
int Execute() override;
void Finish(
int status) override;
private:
mozilla::UniquePtr<NS_tchar[]> mFile;
mozilla::UniquePtr<NS_tchar[]> mRelPath;
int mSkip;
};
int RemoveFile::Parse(NS_tchar* line) {
// format "<deadfile>"
NS_tchar* validPath = get_valid_path(&line);
if (!validPath) {
return PARSE_ERROR;
}
mRelPath = mozilla::MakeUnique<NS_tchar[]>(MAXPATHLEN);
NS_tstrcpy(mRelPath.get(), validPath);
mFile.reset(get_full_path(validPath));
if (!mFile) {
return PARSE_ERROR;
}
return OK;
}
int RemoveFile::Prepare() {
// Skip the file if it already doesn't exist.
int rv = NS_taccess(mFile.get(), F_OK);
if (rv) {
mSkip = 1;
mProgressCost = 0;
return OK;
}
LOG((
"PREPARE REMOVEFILE " LOG_S, mRelPath.get()));
// Make sure that we're actually a file...
struct NS_tstat_t fileInfo;
rv = NS_tstat(mFile.get(), &fileInfo);
if (rv) {
LOG((
"failed to read file status info: " LOG_S
", err: %d", mFile.get(),
errno));
return READ_ERROR;
}
if (!S_ISREG(fileInfo.st_mode)) {
LOG((
"path present, but not a file: " LOG_S, mFile.get()));
return DELETE_ERROR_EXPECTED_FILE;
}
NS_tchar* slash = (NS_tchar*)NS_tstrrchr(mFile.get(), NS_T(
'/'));
if (slash) {
*slash = NS_T(
'\0');
rv = NS_taccess(mFile.get(), W_OK);
*slash = NS_T(
'/');
}
else {
rv = NS_taccess(NS_T(
"."), W_OK);
}
if (rv) {
LOG((
"access failed: %d", errno));
return WRITE_ERROR_FILE_ACCESS_DENIED;
}
return OK;
}
int RemoveFile::Execute() {
if (mSkip) {
return OK;
}
LOG((
"EXECUTE REMOVEFILE " LOG_S, mRelPath.get()));
// The file is checked for existence here and in Prepare since it might have
// been removed by a separate instruction: bug 311099.
int rv = NS_taccess(mFile.get(), F_OK);
if (rv) {
LOG((
"file cannot be removed because it does not exist; skipping"));
mSkip = 1;
return OK;
}
if (sStagedUpdate) {
// Staged updates don't need backup files so just remove it.
rv = ensure_remove(mFile.get());
if (rv) {
return rv;
}
}
else {
// Rename the old file. It will be removed in Finish.
rv = backup_create(mFile.get());
if (rv) {
LOG((
"backup_create failed: %d", rv));
return rv;
}
}
return OK;
}
void RemoveFile::Finish(
int status) {
if (mSkip) {
return;
}
LOG((
"FINISH REMOVEFILE " LOG_S, mRelPath.get()));
// Staged updates don't create backup files.
if (!sStagedUpdate) {
backup_finish(mFile.get(), mRelPath.get(), status);
}
}
class RemoveDir :
public Action {
public:
RemoveDir() : mSkip(0) {}
int Parse(NS_tchar* line) override;
int Prepare() override;
// check that the source dir exists
int Execute() override;
void Finish(
int status) override;
private:
mozilla::UniquePtr<NS_tchar[]> mDir;
mozilla::UniquePtr<NS_tchar[]> mRelPath;
int mSkip;
};
int RemoveDir::Parse(NS_tchar* line) {
// format "<deaddir>/"
NS_tchar* validPath = get_valid_path(&line,
true);
if (!validPath) {
return PARSE_ERROR;
}
mRelPath = mozilla::MakeUnique<NS_tchar[]>(MAXPATHLEN);
NS_tstrcpy(mRelPath.get(), validPath);
mDir.reset(get_full_path(validPath));
if (!mDir) {
return PARSE_ERROR;
}
return OK;
}
int RemoveDir::Prepare() {
// We expect the directory to exist if we are to remove it.
int rv = NS_taccess(mDir.get(), F_OK);
if (rv) {
mSkip = 1;
mProgressCost = 0;
return OK;
}
LOG((
"PREPARE REMOVEDIR " LOG_S
"/", mRelPath.get()));
// Make sure that we're actually a dir.
struct NS_tstat_t dirInfo;
rv = NS_tstat(mDir.get(), &dirInfo);
if (rv) {
LOG((
"failed to read directory status info: " LOG_S
", err: %d",
mRelPath.get(), errno));
return READ_ERROR;
}
if (!S_ISDIR(dirInfo.st_mode)) {
LOG((
"path present, but not a directory: " LOG_S, mRelPath.get()));
return DELETE_ERROR_EXPECTED_DIR;
}
rv = NS_taccess(mDir.get(), W_OK);
if (rv) {
LOG((
"access failed: %d, %d", rv, errno));
return WRITE_ERROR_DIR_ACCESS_DENIED;
}
return OK;
}
int RemoveDir::Execute() {
if (mSkip) {
return OK;
}
LOG((
"EXECUTE REMOVEDIR " LOG_S
"/", mRelPath.get()));
// The directory is checked for existence at every step since it might have
// been removed by a separate instruction: bug 311099.
int rv = NS_taccess(mDir.get(), F_OK);
if (rv) {
LOG((
"directory no longer exists; skipping"));
mSkip = 1;
}
return OK;
}
void RemoveDir::Finish(
int status) {
if (mSkip || status != OK) {
return;
}
LOG((
"FINISH REMOVEDIR " LOG_S
"/", mRelPath.get()));
// The directory is checked for existence at every step since it might have
// been removed by a separate instruction: bug 311099.
int rv = NS_taccess(mDir.get(), F_OK);
if (rv) {
LOG((
"directory no longer exists; skipping"));
return;
}
if (status == OK) {
if (NS_trmdir(mDir.get())) {
LOG((
"non-fatal error removing directory: " LOG_S
"/, rv: %d, err: %d",
mRelPath.get(), rv, errno));
}
}
}
class AddFile :
public Action {
public:
AddFile() : mAdded(
false) {}
int Parse(NS_tchar* line) override;
int Prepare() override;
int Execute() override;
void Finish(
int status) override;
private:
mozilla::UniquePtr<NS_tchar[]> mFile;
mozilla::UniquePtr<NS_tchar[]> mRelPath;
bool mAdded;
};
int AddFile::Parse(NS_tchar* line) {
// format "<newfile>"
NS_tchar* validPath = get_valid_path(&line);
if (!validPath) {
return PARSE_ERROR;
}
mRelPath = mozilla::MakeUnique<NS_tchar[]>(MAXPATHLEN);
NS_tstrcpy(mRelPath.get(), validPath);
mFile.reset(get_full_path(validPath));
if (!mFile) {
return PARSE_ERROR;
}
return OK;
}
int AddFile::Prepare() {
LOG((
"PREPARE ADD " LOG_S, mRelPath.get()));
return OK;
}
int AddFile::Execute() {
LOG((
"EXECUTE ADD " LOG_S, mRelPath.get()));
int rv;
// First make sure that we can actually get rid of any existing file.
rv = NS_taccess(mFile.get(), F_OK);
if (rv == 0) {
if (sStagedUpdate) {
// Staged updates don't need backup files so just remove it.
rv = ensure_remove(mFile.get());
}
else {
rv = backup_create(mFile.get());
}
if (rv) {
return rv;
}
}
else {
rv = ensure_parent_dir(mFile.get());
if (rv) {
return rv;
}
}
#ifdef XP_WIN
char sourcefile[MAXPATHLEN];
if (!WideCharToMultiByte(CP_UTF8, 0, mRelPath.get(), -1, sourcefile,
MAXPATHLEN, nullptr, nullptr)) {
LOG((
"error converting wchar to utf8: %lu", GetLastError()));
return STRING_CONVERSION_ERROR;
}
rv = gArchiveReader.ExtractFile(sourcefile, mFile.get());
#else
rv = gArchiveReader.ExtractFile(mRelPath.get(), mFile.get());
#endif
if (!rv) {
mAdded =
true;
}
return rv;
}
void AddFile::Finish(
int status) {
LOG((
"FINISH ADD " LOG_S, mRelPath.get()));
// Staged updates don't create backup files.
if (!sStagedUpdate) {
// When there is an update failure and a file has been added it is removed
// here since there might not be a backup to replace it.
if (status && mAdded) {
if (NS_tremove(mFile.get()) && errno != ENOENT) {
LOG((
"non-fatal error after update failure removing added file: " LOG_S
", err: %d",
mFile.get(), errno));
}
}
backup_finish(mFile.get(), mRelPath.get(), status);
}
}
class PatchFile :
public Action {
public:
PatchFile() : mPatchFile(nullptr), mPatchIndex(-1), buf(nullptr) {}
~PatchFile() override;
int Parse(NS_tchar* line) override;
int Prepare() override;
// should check for patch file and for checksum here
int Execute() override;
void Finish(
int status) override;
private:
int LoadSourceFile(FILE* ofile);
static int sPatchIndex;
const NS_tchar* mPatchFile;
mozilla::UniquePtr<NS_tchar[]> mFile;
mozilla::UniquePtr<NS_tchar[]> mFileRelPath;
int mPatchIndex;
MBSPatchHeader header;
unsigned char* buf;
NS_tchar spath[MAXPATHLEN];
AutoFile mPatchStream;
};
int PatchFile::sPatchIndex = 0;
PatchFile::~PatchFile() {
// Make sure mPatchStream gets unlocked on Windows; the system will do that,
// but not until some indeterminate future time, and we want determinism.
// Normally this happens at the end of Execute, when we close the stream;
// this call is here in case Execute errors out.
#ifdef XP_WIN
if (mPatchStream) {
UnlockFile((HANDLE)_get_osfhandle(fileno(mPatchStream)), 0, 0, -1, -1);
}
#endif
// Patch files are written to the <working_dir>/updating directory which is
// removed after the update has finished so don't delete patch files here.
if (buf) {
free(buf);
}
}
int PatchFile::LoadSourceFile(FILE* ofile) {
struct stat os;
int rv = fstat(fileno((FILE*)ofile), &os);
if (rv) {
LOG((
"LoadSourceFile: unable to stat destination file: " LOG_S
", "
"err: %d",
mFileRelPath.get(), errno));
return READ_ERROR;
}
if (uint32_t(os.st_size) != header.slen) {
LOG(
(
"LoadSourceFile: destination file size %d does not match expected "
"size %d",
uint32_t(os.st_size), header.slen));
return LOADSOURCE_ERROR_WRONG_SIZE;
}
buf = (
unsigned char*)malloc(header.slen);
if (!buf) {
return UPDATER_MEM_ERROR;
}
size_t r = header.slen;
unsigned char* rb = buf;
while (r) {
const size_t count = mmin(SSIZE_MAX, r);
size_t c = fread(rb, 1, count, ofile);
if (c != count) {
LOG((
"LoadSourceFile: error reading destination file: " LOG_S,
mFileRelPath.get()));
return READ_ERROR;
}
r -= c;
rb += c;
}
// Verify that the contents of the source file correspond to what we expect.
unsigned int crc = crc32(buf, header.slen);
if (crc != header.scrc32) {
LOG(
(
"LoadSourceFile: destination file crc %d does not match expected "
"crc %d",
crc, header.scrc32));
return CRC_ERROR;
}
return OK;
}
int PatchFile::Parse(NS_tchar* line) {
// format "<patchfile>" "<filetopatch>"
// Get the path to the patch file inside of the mar
mPatchFile = mstrtok(kQuote, &line);
if (!mPatchFile) {
return PARSE_ERROR;
}
// consume whitespace between args
NS_tchar* q = mstrtok(kQuote, &line);
if (!q) {
return PARSE_ERROR;
}
NS_tchar* validPath = get_valid_path(&line);
if (!validPath) {
return PARSE_ERROR;
}
mFileRelPath = mozilla::MakeUnique<NS_tchar[]>(MAXPATHLEN);
NS_tstrcpy(mFileRelPath.get(), validPath);
mFile.reset(get_full_path(validPath));
if (!mFile) {
return PARSE_ERROR;
}
return OK;
}
int PatchFile::Prepare() {
LOG((
"PREPARE PATCH " LOG_S, mFileRelPath.get()));
// extract the patch to a temporary file
mPatchIndex = sPatchIndex++;
NS_tsnprintf(spath,
sizeof(spath) /
sizeof(spath[0]),
NS_T(
"%s/updating/%d.patch"), gWorkingDirPath, mPatchIndex);
// The removal of pre-existing patch files here is in case a previous update
// crashed and left these files behind.
if (NS_tremove(spath) && errno != ENOENT) {
LOG((
"failure removing pre-existing patch file: " LOG_S
", err: %d", spath,
errno));
return WRITE_ERROR;
}
mPatchStream = NS_tfopen(spath, NS_T(
"wb+"));
if (!mPatchStream) {
return WRITE_ERROR;
}
#ifdef XP_WIN
// Lock the patch file, so it can't be messed with between
// when we're done creating it and when we go to apply it.
if (!LockFile((HANDLE)_get_osfhandle(fileno(mPatchStream)), 0, 0, -1, -1)) {
LOG((
"Couldn't lock patch file: %lu", GetLastError()));
return LOCK_ERROR_PATCH_FILE;
}
char sourcefile[MAXPATHLEN];
if (!WideCharToMultiByte(CP_UTF8, 0, mPatchFile, -1, sourcefile, MAXPATHLEN,
nullptr, nullptr)) {
LOG((
"error converting wchar to utf8: %lu", GetLastError()));
return STRING_CONVERSION_ERROR;
}
int rv = gArchiveReader.ExtractFileToStream(sourcefile, mPatchStream);
#else
int rv = gArchiveReader.ExtractFileToStream(mPatchFile, mPatchStream);
#endif
return rv;
}
int PatchFile::Execute() {
LOG((
"EXECUTE PATCH " LOG_S, mFileRelPath.get()));
fseek(mPatchStream, 0, SEEK_SET);
int rv = MBS_ReadHeader(mPatchStream, &header);
if (rv) {
return rv;
}
FILE* origfile = nullptr;
#ifdef XP_WIN
if (NS_tstrcmp(mFileRelPath.get(), gCallbackRelPath) == 0) {
// Read from the copy of the callback when patching since the callback can't
// be opened for reading to prevent the application from being launched.
origfile = NS_tfopen(gCallbackBackupPath, NS_T(
"rb"));
}
else {
origfile = NS_tfopen(mFile.get(), NS_T(
"rb"));
}
#else
origfile = NS_tfopen(mFile.get(), NS_T(
"rb"));
#endif
if (!origfile) {
LOG((
"unable to open destination file: " LOG_S
", err: %d",
mFileRelPath.get(), errno));
return READ_ERROR;
}
rv = LoadSourceFile(origfile);
fclose(origfile);
if (rv) {
LOG((
"LoadSourceFile failed"));
return rv;
}
// Rename the destination file if it exists before proceeding so it can be
// used to restore the file to its original state if there is an error.
struct NS_tstat_t ss;
rv = NS_tstat(mFile.get(), &ss);
if (rv) {
LOG((
"failed to read file status info: " LOG_S
", err: %d",
mFileRelPath.get(), errno));
return READ_ERROR;
}
// Staged updates don't need backup files.
if (!sStagedUpdate) {
rv = backup_create(mFile.get());
if (rv) {
return rv;
}
}
#if defined(HAVE_POSIX_FALLOCATE)
AutoFile ofile(ensure_open(mFile.get(), NS_T(
"wb+"), ss.st_mode));
posix_fallocate(fileno((FILE*)ofile), 0, header.dlen);
#elif defined(XP_WIN)
bool shouldTruncate =
true;
// Creating the file, setting the size, and then closing the file handle
// lessens fragmentation more than any other method tested. Other methods that
// have been tested are:
// 1. _chsize / _chsize_s reduced fragmentation though not completely.
// 2. _get_osfhandle and then setting the size reduced fragmentation though
// not completely. There are also reports of _get_osfhandle failing on
// mingw.
HANDLE hfile = CreateFileW(mFile.get(), GENERIC_WRITE, 0, nullptr,
CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
if (hfile != INVALID_HANDLE_VALUE) {
if (SetFilePointer(hfile, header.dlen, nullptr, FILE_BEGIN) !=
INVALID_SET_FILE_POINTER &&
SetEndOfFile(hfile) != 0) {
shouldTruncate =
false;
}
CloseHandle(hfile);
}
AutoFile ofile(ensure_open(
mFile.get(), shouldTruncate ? NS_T(
"wb+") : NS_T(
"rb+"), ss.st_mode));
#elif defined(XP_MACOSX)
AutoFile ofile(ensure_open(mFile.get(), NS_T(
"wb+"), ss.st_mode));
// Modified code from FileUtils.cpp
fstore_t store = {F_ALLOCATECONTIG, F_PEOFPOSMODE, 0, header.dlen};
// Try to get a continous chunk of disk space
rv = fcntl(fileno((FILE*)ofile), F_PREALLOCATE, &store);
if (rv == -1) {
// OK, perhaps we are too fragmented, allocate non-continuous
store.fst_flags = F_ALLOCATEALL;
rv = fcntl(fileno((FILE*)ofile), F_PREALLOCATE, &store);
}
if (rv != -1) {
ftruncate(fileno((FILE*)ofile), header.dlen);
}
#else
AutoFile ofile(ensure_open(mFile.get(), NS_T(
"wb+"), ss.st_mode));
#endif
if (ofile == nullptr) {
LOG((
"unable to create new file: " LOG_S
", err: %d", mFileRelPath.get(),
errno));
return WRITE_ERROR_OPEN_PATCH_FILE;
}
#ifdef XP_WIN
if (!shouldTruncate) {
fseek(ofile, 0, SEEK_SET);
}
#endif
rv = MBS_ApplyPatch(&header, mPatchStream, buf, ofile);
// Go ahead and do a bit of cleanup now to minimize runtime overhead.
// Make sure mPatchStream gets unlocked on Windows; the system will do that,
// but not until some indeterminate future time, and we want determinism.
#ifdef XP_WIN
UnlockFile((HANDLE)_get_osfhandle(fileno(mPatchStream)), 0, 0, -1, -1);
#endif
// Set mPatchStream to nullptr to make AutoFile close the file,
// so it can be deleted on Windows.
mPatchStream = nullptr;
// Patch files are written to the <working_dir>/updating directory which is
// removed after the update has finished so don't delete patch files here.
spath[0] = NS_T(
'\0');
free(buf);
buf = nullptr;
return rv;
}
void PatchFile::Finish(
int status) {
LOG((
"FINISH PATCH " LOG_S, mFileRelPath.get()));
// Staged updates don't create backup files.
if (!sStagedUpdate) {
backup_finish(mFile.get(), mFileRelPath.get(), status);
}
}
class AddIfFile :
public AddFile {
public:
int Parse(NS_tchar* line) override;
int Prepare() override;
int Execute() override;
void Finish(
int status) override;
protected:
mozilla::UniquePtr<NS_tchar[]> mTestFile;
};
int AddIfFile::Parse(NS_tchar* line) {
// format "<testfile>" "<newfile>"
mTestFile.reset(get_full_path(get_valid_path(&line)));
if (!mTestFile) {
return PARSE_ERROR;
}
// consume whitespace between args
NS_tchar* q = mstrtok(kQuote, &line);
if (!q) {
return PARSE_ERROR;
}
return AddFile::Parse(line);
}
int AddIfFile::Prepare() {
// If the test file does not exist, then skip this action.
if (NS_taccess(mTestFile.get(), F_OK)) {
mTestFile = nullptr;
return OK;
}
return AddFile::Prepare();
}
int AddIfFile::Execute() {
if (!mTestFile) {
return OK;
}
return AddFile::Execute();
}
void AddIfFile::Finish(
int status) {
if (!mTestFile) {
return;
}
AddFile::Finish(status);
}
class AddIfNotFile :
public AddFile {
public:
int Parse(NS_tchar* line) override;
int Prepare() override;
int Execute() override;
void Finish(
int status) override;
protected:
mozilla::UniquePtr<NS_tchar[]> mTestFile;
};
int AddIfNotFile::Parse(NS_tchar* line) {
// format "<testfile>" "<newfile>"
mTestFile.reset(get_full_path(get_valid_path(&line)));
if (!mTestFile) {
return PARSE_ERROR;
}
// consume whitespace between args
NS_tchar* q = mstrtok(kQuote, &line);
if (!q) {
return PARSE_ERROR;
}
return AddFile::Parse(line);
}
int AddIfNotFile::Prepare() {
// If the test file exists, then skip this action.
if (!NS_taccess(mTestFile.get(), F_OK)) {
mTestFile = NULL;
return OK;
}
return AddFile::Prepare();
}
int AddIfNotFile::Execute() {
if (!mTestFile) {
return OK;
}
return AddFile::Execute();
}
void AddIfNotFile::Finish(
int status) {
if (!mTestFile) {
return;
}
AddFile::Finish(status);
}
class PatchIfFile :
public PatchFile {
public:
int Parse(NS_tchar* line) override;
int Prepare() override;
// should check for patch file and for checksum here
int Execute() override;
void Finish(
int status) override;
private:
mozilla::UniquePtr<NS_tchar[]> mTestFile;
};
int PatchIfFile::Parse(NS_tchar* line) {
// format "<testfile>" "<patchfile>" "<filetopatch>"
mTestFile.reset(get_full_path(get_valid_path(&line)));
if (!mTestFile) {
return PARSE_ERROR;
}
// consume whitespace between args
NS_tchar* q = mstrtok(kQuote, &line);
if (!q) {
return PARSE_ERROR;
}
return PatchFile::Parse(line);
}
int PatchIfFile::Prepare() {
// If the test file does not exist, then skip this action.
if (NS_taccess(mTestFile.get(), F_OK)) {
mTestFile = nullptr;
return OK;
}
return PatchFile::Prepare();
}
int PatchIfFile::Execute() {
if (!mTestFile) {
return OK;
}
return PatchFile::Execute();
}
void PatchIfFile::Finish(
int status) {
if (!mTestFile) {
return;
}
PatchFile::Finish(status);
}
//-----------------------------------------------------------------------------
#ifdef XP_WIN
# include
"nsWindowsRestart.cpp"
# include
"nsWindowsHelpers.h"
# include
"uachelper.h"
# ifdef MOZ_MAINTENANCE_SERVICE
# include
"pathhash.h"
# endif
/**
* Launch the post update application (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.
* @return true if there was no error starting the process.
*/
bool LaunchWinPostProcess(
const WCHAR* installationDir,
const WCHAR* updateInfoDir) {
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")) {
LOG(
(
"LaunchWinPostProcess failed because PathAppendSafe failed when "
"getting INI path"));
return false;
}
WCHAR exefile[MAX_PATH + 1];
WCHAR exearg[MAX_PATH + 1];
if (!GetPrivateProfileStringW(L
"PostUpdateWin", L
"ExeRelPath", nullptr,
exefile, MAX_PATH + 1, inifile)) {
LOG((
"LaunchWinPostProcess failed due to failure to retrieve ExeRelPath"));
return false;
}
if (!GetPrivateProfileStringW(L
"PostUpdateWin", L
"ExeArg", nullptr, exearg,
MAX_PATH + 1, inifile)) {
LOG((
"LaunchWinPostProcess failed due to failure to retrieve ExeArg"));
return false;
}
// The relative path must not contain directory traversals, current directory,
// or colons.
if (wcsstr(exefile, L
"..") != nullptr || wcsstr(exefile, L
"./") != nullptr ||
wcsstr(exefile, L
".\\") != nullptr || wcsstr(exefile, L
":") != nullptr) {
LOG(
(
"LaunchWinPostProcess failed because executable path contains "
"disallowed characters"));
return false;
}
// The relative path must not start with a decimal point, backslash, or
// forward slash.
if (exefile[0] == L
'.' || exefile[0] == L
'\\' || exefile[0] == L
'/') {
LOG((
"LaunchWinPostProcess failed because first character is invalid"));
return false;
}
WCHAR exefullpath[MAX_PATH + 1] = {L
'\0'};
wcsncpy(exefullpath, installationDir, MAX_PATH);
if (!PathAppendSafe(exefullpath, exefile)) {
LOG(
(
"LaunchWinPostProcess failed because PathAppendSafe failed when "
"getting full executable path"));
return false;
}
if (!IsValidFullPath(exefullpath)) {
LOG(
(
"LaunchWinPostProcess failed because executable path is not a valid, "
"full path"));
return false;
}
# if !
defined(TEST_UPDATER) &&
defined(MOZ_MAINTENANCE_SERVICE)
if (sUsingService &&
!DoesBinaryMatchAllowedCertificates(installationDir, exefullpath)) {
LOG(
(
"LaunchWinPostProcess failed because the binary doesn't match the "
"allowed certificates"));
return false;
}
# endif
WCHAR dlogFile[MAX_PATH + 1];
if (!PathGetSiblingFilePath(dlogFile, exefullpath, L
"uninstall.update")) {
LOG((
"LaunchWinPostProcess failed because dlogFile path is unavailable"));
return false;
}
WCHAR slogFile[MAX_PATH + 1] = {L
'\0'};
if (gCopyOutputFiles) {
if (!GetSecureOutputFilePath(gPatchDirPath, L
".log", slogFile)) {
LOG(
(
"LaunchWinPostProcess failed because a secure slogFile path is "
"unavailable"));
return false;
}
}
else {
wcsncpy(slogFile, updateInfoDir, MAX_PATH);
if (!PathAppendSafe(slogFile, UpdateLogFilename())) {
LOG((
"LaunchWinPostProcess failed because slogFile path is unavailable"));
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) {
LOG(
(
"LaunchWinPostProcess failed due to failure to allocate %zu wchars "
"for cmdline",
len + 1));
return false;
}
wcsncpy(cmdline, dummyArg, len);
wcscat(cmdline, exearg);
// 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 =
const_cast<LPWSTR>(L
"");
// -Wwritable-strings
PROCESS_INFORMATION pi = {0};
// Invoke post-update with a minimal environment to avoid environment
// variables intended to relaunch Firefox impacting post-update operations, in
// particular background tasks. The updater will invoke the callback
// application with the current (non-minimal) environment.
//
// N.b.: two null terminating characters! The first terminates a non-existent
// key-value pair, the second (automatically added) terminates the block of
// key-value pairs.
const WCHAR* emptyEnvironment = L
"\0";
bool 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
(LPVOID)emptyEnvironment, workingDirectory, &si, &pi);
free(cmdline);
if (ok) {
LOG((
"LaunchWinPostProcess - Waiting for process to complete"));
WaitForSingleObject(pi.hProcess, INFINITE);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
LOG((
"LaunchWinPostProcess - Process completed"));
}
else {
LOG((
"LaunchWinPostProcess - CreateProcessW failed: %lu", GetLastError()));
}
return ok;
}
#endif
static void LaunchCallbackApp(
const NS_tchar* workingDir,
int argc,
NS_tchar** argv,
bool usingService) {
putenv(
const_cast<
char*>(
"MOZ_LAUNCHED_CHILD=1"));
// Run from the specified working directory (see bug 312360).
if (NS_tchdir(workingDir) != 0) {
LOG((
"Warning: chdir failed"));
}
#if defined(USE_EXECV)
execv(argv[0], argv);
#elif defined(XP_MACOSX)
LaunchMacApp(argc, (
const char**)argv);
#elif defined(XP_WIN)
// Do not allow the callback to run when running an update through the
// service as session 0. The unelevated updater.exe will do the launching.
if (!usingService) {
HANDLE hProcess;
if (WinLaunchChild(argv[0], argc, argv, nullptr, &hProcess)) {
// Keep the current process around until the callback process has created
// its message queue, to avoid the launched process's windows being forced
// into the background.
mozilla::WaitForInputIdle(hProcess);
CloseHandle(hProcess);
}
}
#else
# warning
"Need implementaton of LaunchCallbackApp"
#endif
}
static bool WriteToFile(
const NS_tchar* aFilename,
const char* aStatus) {
LOG((
"Writing status to file: %s", aStatus));
NS_tchar statusFilePath[MAXPATHLEN + 1] = {NS_T(
'\0')};
#if defined(XP_WIN)
if (gUseSecureOutputPath) {
if (!GetSecureOutputFilePath(gPatchDirPath, L
".status", statusFilePath)) {
LOG((
"WriteToFile failed to get secure output path"));
return false;
}
}
else {
NS_tsnprintf(statusFilePath,
sizeof(statusFilePath) /
sizeof(statusFilePath[0]),
NS_T(
"%s\\%s"), gPatchDirPath, aFilename);
}
#else
NS_tsnprintf(statusFilePath,
sizeof(statusFilePath) /
sizeof(statusFilePath[0]),
NS_T(
"%s/%s"), gPatchDirPath, aFilename);
// Make sure that the directory for the update status file exists
if (ensure_parent_dir(statusFilePath)) {
LOG((
"WriteToFile failed to ensure parent directory's existence"));
return false;
}
#endif
AutoFile statusFile(NS_tfopen(statusFilePath, NS_T(
"wb+")));
if (statusFile == nullptr) {
LOG((
"WriteToFile failed to open status file: %d", errno));
return false;
}
if (fwrite(aStatus, strlen(aStatus), 1, statusFile) != 1) {
LOG((
"WriteToFile failed to write to status file: %d", errno));
return false;
}
#if defined(XP_WIN)
if (gUseSecureOutputPath) {
// This is done after the update status file has been written so if the
// write to the update status file fails an existing update status file
// won't be used.
if (!WriteSecureIDFile(gPatchDirPath)) {
LOG((
"WriteToFile failed to write secure ID file"));
return false;
}
}
#endif
return true;
}
/**
* Writes a string to the update.status file.
*
* NOTE: All calls to WriteStatusFile MUST happen before calling output_finish
* because the output_finish function copies the update status file for
* the elevated updater and writing the status file after calling
* output_finish will overwrite it.
*
* @param aStatus
* The string to write to the update.status file.
* @return true on success.
*/
static bool WriteStatusFile(
const char* aStatus) {
return WriteToFile(NS_T(
"update.status"), aStatus);
}
/**
* Writes a string to the update.status file based on the status param.
*
* NOTE: All calls to WriteStatusFile MUST happen before calling output_finish
* because the output_finish function copies the update status file for
* the elevated updater and writing the status file after calling
* output_finish will overwrite it.
*
* @param status
* A status code used to determine what string to write to the
* update.status file (see code).
*/
static void WriteStatusFile(
int status) {
const char* text;
char buf[32];
if (status == OK) {
if (sStagedUpdate) {
text =
"applied\n";
}
else {
text =
"succeeded\n";
}
}
else {
snprintf(buf,
sizeof(buf) /
sizeof(buf[0]),
"failed: %d\n", status);
text = buf;
}
WriteStatusFile(text);
}
#if defined(XP_WIN)
/*
* Parses the passed contents of an update status file and checks if the
* contained status matches the expected status.
*
* @param statusString The status file contents.
* @param expectedStatus The status to compare the update status file's
* contents against.
* @param errorCode Optional out parameter. If a pointer is passed and the
* update status file contains an error code, the code
* will be returned via the out parameter. If a pointer is
* passed and the update status file does not contain an error
* code, or any error code after the status could not be
* parsed, mozilla::Nothing will be returned via this
* parameter.
* @return true if the status is set to the value indicated by expectedStatus.
*/
static bool UpdateStatusIs(
const char* statusString,
const char* expectedStatus,
mozilla::Maybe<
int>* errorCode = nullptr) {
if (errorCode) {
*errorCode = mozilla::Nothing();
}
// Parse the update status file. Expected format is:
// Update status string
// Optionally followed by:
// Colon character (':')
// Space character (' ')
// Integer error code
// Newline character
const char* statusEnd = strchr(statusString,
':');
if (statusEnd == nullptr) {
statusEnd = strchr(statusString,
'\n');
}
if (statusEnd == nullptr) {
statusEnd = strchr(statusString,
'\0');
}
size_t statusLen = statusEnd - statusString;
size_t expectedStatusLen = strlen(expectedStatus);
bool statusMatch =
statusLen == expectedStatusLen &&
strncmp(statusString, expectedStatus, expectedStatusLen) == 0;
// We only need to continue parsing if (a) there is a place to store the error
// code if we parse it, and (b) there is a status code to parse. If the status
// string didn't end with a ':', there won't be an error code after it.
if (!errorCode || *statusEnd !=
':') {
return statusMatch;
}
const char* errorCodeStart = statusEnd + 1;
char* errorCodeEnd = nullptr;
--> --------------------
--> maximum size reached
--> --------------------