summaryrefslogtreecommitdiffstats
path: root/toolkit/mozapps/update/common
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/mozapps/update/common')
-rw-r--r--toolkit/mozapps/update/common/certificatecheck.cpp241
-rw-r--r--toolkit/mozapps/update/common/certificatecheck.h21
-rw-r--r--toolkit/mozapps/update/common/commonupdatedir.cpp1899
-rw-r--r--toolkit/mozapps/update/common/commonupdatedir.h37
-rw-r--r--toolkit/mozapps/update/common/moz.build76
-rw-r--r--toolkit/mozapps/update/common/pathhash.cpp128
-rw-r--r--toolkit/mozapps/update/common/pathhash.h19
-rw-r--r--toolkit/mozapps/update/common/readstrings.cpp397
-rw-r--r--toolkit/mozapps/update/common/readstrings.h91
-rw-r--r--toolkit/mozapps/update/common/registrycertificates.cpp148
-rw-r--r--toolkit/mozapps/update/common/registrycertificates.h14
-rw-r--r--toolkit/mozapps/update/common/uachelper.cpp186
-rw-r--r--toolkit/mozapps/update/common/uachelper.h22
-rw-r--r--toolkit/mozapps/update/common/updatecommon.cpp464
-rw-r--r--toolkit/mozapps/update/common/updatecommon.h42
-rw-r--r--toolkit/mozapps/update/common/updatedefines.h164
-rw-r--r--toolkit/mozapps/update/common/updatehelper.cpp763
-rw-r--r--toolkit/mozapps/update/common/updatehelper.h39
-rw-r--r--toolkit/mozapps/update/common/updatererrors.h113
-rw-r--r--toolkit/mozapps/update/common/updateutils_win.cpp166
-rw-r--r--toolkit/mozapps/update/common/updateutils_win.h47
21 files changed, 5077 insertions, 0 deletions
diff --git a/toolkit/mozapps/update/common/certificatecheck.cpp b/toolkit/mozapps/update/common/certificatecheck.cpp
new file mode 100644
index 0000000000..fcaa79b825
--- /dev/null
+++ b/toolkit/mozapps/update/common/certificatecheck.cpp
@@ -0,0 +1,241 @@
+/* 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 <stdio.h>
+#include <stdlib.h>
+#include <windows.h>
+#include <softpub.h>
+#include <wintrust.h>
+
+#include "certificatecheck.h"
+#include "updatecommon.h"
+
+static const int ENCODING = X509_ASN_ENCODING | PKCS_7_ASN_ENCODING;
+
+/**
+ * Checks to see if a file stored at filePath matches the specified info.
+ *
+ * @param filePath The PE file path to check
+ * @param infoToMatch The acceptable information to match
+ * @return ERROR_SUCCESS if successful, ERROR_NOT_FOUND if the info
+ * does not match, or the last error otherwise.
+ */
+DWORD
+CheckCertificateForPEFile(LPCWSTR filePath, CertificateCheckInfo& infoToMatch) {
+ HCERTSTORE certStore = nullptr;
+ HCRYPTMSG cryptMsg = nullptr;
+ PCCERT_CONTEXT certContext = nullptr;
+ PCMSG_SIGNER_INFO signerInfo = nullptr;
+ DWORD lastError = ERROR_SUCCESS;
+
+ // Get the HCERTSTORE and HCRYPTMSG from the signed file.
+ DWORD encoding, contentType, formatType;
+ BOOL result = CryptQueryObject(
+ CERT_QUERY_OBJECT_FILE, filePath,
+ CERT_QUERY_CONTENT_FLAG_PKCS7_SIGNED_EMBED, CERT_QUERY_CONTENT_FLAG_ALL,
+ 0, &encoding, &contentType, &formatType, &certStore, &cryptMsg, nullptr);
+ if (!result) {
+ lastError = GetLastError();
+ LOG_WARN(("CryptQueryObject failed. (%d)", lastError));
+ goto cleanup;
+ }
+
+ // Pass in nullptr to get the needed signer information size.
+ DWORD signerInfoSize;
+ result = CryptMsgGetParam(cryptMsg, CMSG_SIGNER_INFO_PARAM, 0, nullptr,
+ &signerInfoSize);
+ if (!result) {
+ lastError = GetLastError();
+ LOG_WARN(("CryptMsgGetParam failed. (%d)", lastError));
+ goto cleanup;
+ }
+
+ // Allocate the needed size for the signer information.
+ signerInfo = (PCMSG_SIGNER_INFO)LocalAlloc(LPTR, signerInfoSize);
+ if (!signerInfo) {
+ lastError = GetLastError();
+ LOG_WARN(("Unable to allocate memory for Signer Info. (%d)", lastError));
+ goto cleanup;
+ }
+
+ // Get the signer information (PCMSG_SIGNER_INFO).
+ // In particular we want the issuer and serial number.
+ result = CryptMsgGetParam(cryptMsg, CMSG_SIGNER_INFO_PARAM, 0,
+ (PVOID)signerInfo, &signerInfoSize);
+ if (!result) {
+ lastError = GetLastError();
+ LOG_WARN(("CryptMsgGetParam failed. (%d)", lastError));
+ goto cleanup;
+ }
+
+ // Search for the signer certificate in the certificate store.
+ CERT_INFO certInfo;
+ certInfo.Issuer = signerInfo->Issuer;
+ certInfo.SerialNumber = signerInfo->SerialNumber;
+ certContext =
+ CertFindCertificateInStore(certStore, ENCODING, 0, CERT_FIND_SUBJECT_CERT,
+ (PVOID)&certInfo, nullptr);
+ if (!certContext) {
+ lastError = GetLastError();
+ LOG_WARN(("CertFindCertificateInStore failed. (%d)", lastError));
+ goto cleanup;
+ }
+
+ if (!DoCertificateAttributesMatch(certContext, infoToMatch)) {
+ lastError = ERROR_NOT_FOUND;
+ LOG_WARN(("Certificate did not match issuer or name. (%d)", lastError));
+ goto cleanup;
+ }
+
+cleanup:
+ if (signerInfo) {
+ LocalFree(signerInfo);
+ }
+ if (certContext) {
+ CertFreeCertificateContext(certContext);
+ }
+ if (certStore) {
+ CertCloseStore(certStore, 0);
+ }
+ if (cryptMsg) {
+ CryptMsgClose(cryptMsg);
+ }
+ return lastError;
+}
+
+/**
+ * Checks to see if a file stored at filePath matches the specified info.
+ *
+ * @param certContext The certificate context of the file
+ * @param infoToMatch The acceptable information to match
+ * @return FALSE if the info does not match or if any error occurs in the check
+ */
+BOOL DoCertificateAttributesMatch(PCCERT_CONTEXT certContext,
+ CertificateCheckInfo& infoToMatch) {
+ DWORD dwData;
+ LPWSTR szName = nullptr;
+
+ if (infoToMatch.issuer) {
+ // Pass in nullptr to get the needed size of the issuer buffer.
+ dwData = CertGetNameString(certContext, CERT_NAME_SIMPLE_DISPLAY_TYPE,
+ CERT_NAME_ISSUER_FLAG, nullptr, nullptr, 0);
+
+ if (!dwData) {
+ LOG_WARN(("CertGetNameString failed. (%d)", GetLastError()));
+ return FALSE;
+ }
+
+ // Allocate memory for Issuer name buffer.
+ szName = (LPWSTR)LocalAlloc(LPTR, dwData * sizeof(WCHAR));
+ if (!szName) {
+ LOG_WARN(
+ ("Unable to allocate memory for issuer name. (%d)", GetLastError()));
+ return FALSE;
+ }
+
+ // Get Issuer name.
+ if (!CertGetNameStringW(certContext, CERT_NAME_SIMPLE_DISPLAY_TYPE,
+ CERT_NAME_ISSUER_FLAG, nullptr, szName, dwData)) {
+ LOG_WARN(("CertGetNameString failed. (%d)", GetLastError()));
+ LocalFree(szName);
+ return FALSE;
+ }
+
+ // If the issuer does not match, return a failure.
+ if (!infoToMatch.issuer || wcscmp(szName, infoToMatch.issuer)) {
+ LocalFree(szName);
+ return FALSE;
+ }
+
+ LocalFree(szName);
+ szName = nullptr;
+ }
+
+ if (infoToMatch.name) {
+ // Pass in nullptr to get the needed size of the name buffer.
+ dwData = CertGetNameString(certContext, CERT_NAME_SIMPLE_DISPLAY_TYPE, 0,
+ nullptr, nullptr, 0);
+ if (!dwData) {
+ LOG_WARN(("CertGetNameString failed. (%d)", GetLastError()));
+ return FALSE;
+ }
+
+ // Allocate memory for the name buffer.
+ szName = (LPWSTR)LocalAlloc(LPTR, dwData * sizeof(WCHAR));
+ if (!szName) {
+ LOG_WARN(("Unable to allocate memory for subject name. (%d)",
+ GetLastError()));
+ return FALSE;
+ }
+
+ // Obtain the name.
+ if (!(CertGetNameStringW(certContext, CERT_NAME_SIMPLE_DISPLAY_TYPE, 0,
+ nullptr, szName, dwData))) {
+ LOG_WARN(("CertGetNameString failed. (%d)", GetLastError()));
+ LocalFree(szName);
+ return FALSE;
+ }
+
+ // If the issuer does not match, return a failure.
+ if (!infoToMatch.name || wcscmp(szName, infoToMatch.name)) {
+ LocalFree(szName);
+ return FALSE;
+ }
+
+ // We have a match!
+ LocalFree(szName);
+ }
+
+ // If there were any errors we would have aborted by now.
+ return TRUE;
+}
+
+/**
+ * Verifies the trust of the specified file path.
+ *
+ * @param filePath The file path to check.
+ * @return ERROR_SUCCESS if successful, or the last error code otherwise.
+ */
+DWORD
+VerifyCertificateTrustForFile(LPCWSTR filePath) {
+ // Setup the file to check.
+ WINTRUST_FILE_INFO fileToCheck;
+ ZeroMemory(&fileToCheck, sizeof(fileToCheck));
+ fileToCheck.cbStruct = sizeof(WINTRUST_FILE_INFO);
+ fileToCheck.pcwszFilePath = filePath;
+
+ // Setup what to check, we want to check it is signed and trusted.
+ WINTRUST_DATA trustData;
+ ZeroMemory(&trustData, sizeof(trustData));
+ trustData.cbStruct = sizeof(trustData);
+ trustData.pPolicyCallbackData = nullptr;
+ trustData.pSIPClientData = nullptr;
+ trustData.dwUIChoice = WTD_UI_NONE;
+ trustData.fdwRevocationChecks = WTD_REVOKE_NONE;
+ trustData.dwUnionChoice = WTD_CHOICE_FILE;
+ trustData.dwStateAction = 0;
+ trustData.hWVTStateData = nullptr;
+ trustData.pwszURLReference = nullptr;
+ // no UI
+ trustData.dwUIContext = 0;
+ trustData.pFile = &fileToCheck;
+
+ GUID policyGUID = WINTRUST_ACTION_GENERIC_VERIFY_V2;
+ // Check if the file is signed by something that is trusted.
+ LONG ret = WinVerifyTrust(nullptr, &policyGUID, &trustData);
+ if (ERROR_SUCCESS == ret) {
+ // The hash that represents the subject is trusted and there were no
+ // verification errors. No publisher nor time stamp chain errors.
+ LOG(("The file \"%ls\" is signed and the signature was verified.",
+ filePath));
+ return ERROR_SUCCESS;
+ }
+
+ DWORD lastError = GetLastError();
+ LOG_WARN(
+ ("There was an error validating trust of the certificate for file"
+ " \"%ls\". Returned: %d. (%d)",
+ filePath, ret, lastError));
+ return ret;
+}
diff --git a/toolkit/mozapps/update/common/certificatecheck.h b/toolkit/mozapps/update/common/certificatecheck.h
new file mode 100644
index 0000000000..edb8ddb095
--- /dev/null
+++ b/toolkit/mozapps/update/common/certificatecheck.h
@@ -0,0 +1,21 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef _CERTIFICATECHECK_H_
+#define _CERTIFICATECHECK_H_
+
+#include <wincrypt.h>
+
+struct CertificateCheckInfo {
+ LPCWSTR name;
+ LPCWSTR issuer;
+};
+
+BOOL DoCertificateAttributesMatch(PCCERT_CONTEXT pCertContext,
+ CertificateCheckInfo& infoToMatch);
+DWORD VerifyCertificateTrustForFile(LPCWSTR filePath);
+DWORD CheckCertificateForPEFile(LPCWSTR filePath,
+ CertificateCheckInfo& infoToMatch);
+
+#endif
diff --git a/toolkit/mozapps/update/common/commonupdatedir.cpp b/toolkit/mozapps/update/common/commonupdatedir.cpp
new file mode 100644
index 0000000000..826ec48b51
--- /dev/null
+++ b/toolkit/mozapps/update/common/commonupdatedir.cpp
@@ -0,0 +1,1899 @@
+/* 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/. */
+
+/*
+ * This file does not use many of the features Firefox provides such as
+ * nsAString and nsIFile because code in this file will be included not only
+ * in Firefox, but also in the Mozilla Maintenance Service, the Mozilla
+ * Maintenance Service installer, and TestAUSHelper.
+ */
+
+#include <cinttypes>
+#include <cwchar>
+#include <string>
+#include "city.h"
+#include "commonupdatedir.h"
+#include "updatedefines.h"
+
+#ifdef XP_WIN
+# include <accctrl.h>
+# include <aclapi.h>
+# include <cstdarg>
+# include <errno.h>
+# include <objbase.h>
+# include <shellapi.h>
+# include <shlobj.h>
+# include <strsafe.h>
+# include <winerror.h>
+# include "nsWindowsHelpers.h"
+# include "updateutils_win.h"
+#endif
+
+#ifdef XP_WIN
+// This is the name of the directory to be put in the application data directory
+// if no vendor or application name is specified.
+// (i.e. C:\ProgramData\<FALLBACK_VENDOR_NAME>)
+# define FALLBACK_VENDOR_NAME "Mozilla"
+// This describes the directory between the "Mozilla" directory and the install
+// path hash (i.e. C:\ProgramData\Mozilla\<UPDATE_PATH_MID_DIR_NAME>\<hash>)
+# define UPDATE_PATH_MID_DIR_NAME "updates"
+// This describes the directory between the update directory and the patch
+// directory.
+// (i.e. C:\ProgramData\Mozilla\updates\<hash>\<UPDATE_SUBDIRECTORY>\0)
+# define UPDATE_SUBDIRECTORY "updates"
+// This defines the leaf update directory, where the MAR file is downloaded to
+// (i.e. C:\ProgramData\Mozilla\updates\<hash>\updates\<PATCH_DIRECTORY>)
+# define PATCH_DIRECTORY "0"
+// This defines the prefix of files created to lock a directory
+# define LOCK_FILE_PREFIX "mozlock."
+
+enum class WhichUpdateDir {
+ CommonAppData,
+ UserAppData,
+};
+
+/**
+ * This is a very simple string class.
+ *
+ * This class has some substantial limitations for the sake of simplicity. It
+ * has no support whatsoever for modifying a string that already has data. There
+ * is, therefore, no append function and no support for automatically resizing
+ * strings.
+ *
+ * Error handling is also done in a slightly unusual manner. If there is ever
+ * a failure allocating or assigning to a string, it will do the simplest
+ * possible recovery: truncate itself to 0-length.
+ * This coupled with the fact that the length is cached means that an effective
+ * method of error checking is to attempt assignment and then check the length
+ * of the result.
+ */
+class SimpleAutoString {
+ private:
+ size_t mLength;
+ // Unique pointer frees the buffer when the class is deleted or goes out of
+ // scope.
+ mozilla::UniquePtr<wchar_t[]> mString;
+
+ /**
+ * Allocates enough space to store a string of the specified length.
+ */
+ bool AllocLen(size_t len) {
+ mString = mozilla::MakeUnique<wchar_t[]>(len + 1);
+ return mString.get() != nullptr;
+ }
+
+ /**
+ * Allocates a buffer of the size given.
+ */
+ bool AllocSize(size_t size) {
+ mString = mozilla::MakeUnique<wchar_t[]>(size);
+ return mString.get() != nullptr;
+ }
+
+ public:
+ SimpleAutoString() : mLength(0) {}
+
+ /*
+ * Allocates enough space for a string of the given length and formats it as
+ * an empty string.
+ */
+ bool AllocEmpty(size_t len) {
+ bool success = AllocLen(len);
+ Truncate();
+ return success;
+ }
+
+ /**
+ * These functions can potentially return null if no buffer has yet been
+ * allocated. After changing a string retrieved with MutableString, the Check
+ * method should be called to synchronize other members (ex: mLength) with the
+ * new buffer.
+ */
+ wchar_t* MutableString() { return mString.get(); }
+ const wchar_t* String() const { return mString.get(); }
+
+ size_t Length() const { return mLength; }
+
+ /**
+ * This method should be called after manually changing the string's buffer
+ * via MutableString to synchronize other members (ex: mLength) with the
+ * new buffer.
+ * Returns true if the string is now in a valid state.
+ */
+ bool Check() {
+ mLength = wcslen(mString.get());
+ return true;
+ }
+
+ void SwapBufferWith(mozilla::UniquePtr<wchar_t[]>& other) {
+ mString.swap(other);
+ if (mString) {
+ mLength = wcslen(mString.get());
+ } else {
+ mLength = 0;
+ }
+ }
+
+ void Swap(SimpleAutoString& other) {
+ mString.swap(other.mString);
+ size_t newLength = other.mLength;
+ other.mLength = mLength;
+ mLength = newLength;
+ }
+
+ /**
+ * Truncates the string to the length specified. This must not be greater than
+ * or equal to the size of the string's buffer.
+ */
+ void Truncate(size_t len = 0) {
+ if (len > mLength) {
+ return;
+ }
+ mLength = len;
+ if (mString) {
+ mString.get()[len] = L'\0';
+ }
+ }
+
+ /**
+ * Assigns a string and ensures that the resulting string is valid and has its
+ * length set properly.
+ *
+ * Note that although other similar functions in this class take length, this
+ * function takes buffer size instead because it is intended to be follow the
+ * input convention of sprintf.
+ *
+ * Returns the new length, which will be 0 if there was any failure.
+ *
+ * This function does no allocation or reallocation. If the buffer is not
+ * large enough to hold the new string, the call will fail.
+ */
+ size_t AssignSprintf(size_t bufferSize, const wchar_t* format, ...) {
+ va_list ap;
+ va_start(ap, format);
+ size_t returnValue = AssignVsprintf(bufferSize, format, ap);
+ va_end(ap);
+ return returnValue;
+ }
+ /**
+ * Same as the above, but takes a va_list like vsprintf does.
+ */
+ size_t AssignVsprintf(size_t bufferSize, const wchar_t* format, va_list ap) {
+ if (!mString) {
+ Truncate();
+ return 0;
+ }
+
+ int charsWritten = vswprintf(mString.get(), bufferSize, format, ap);
+ if (charsWritten < 0 || static_cast<size_t>(charsWritten) >= bufferSize) {
+ // charsWritten does not include the null terminator. If charsWritten is
+ // equal to the buffer size, we do not have a null terminator nor do we
+ // have room for one.
+ Truncate();
+ return 0;
+ }
+ mString.get()[charsWritten] = L'\0';
+
+ mLength = charsWritten;
+ return mLength;
+ }
+
+ /**
+ * Allocates enough space for the string and assigns a value to it with
+ * sprintf. Takes, as an argument, the maximum length that the string is
+ * expected to use (which, after adding 1 for the null byte, is the amount of
+ * space that will be allocated).
+ *
+ * Returns the new length, which will be 0 on any failure.
+ */
+ size_t AllocAndAssignSprintf(size_t maxLength, const wchar_t* format, ...) {
+ if (!AllocLen(maxLength)) {
+ Truncate();
+ return 0;
+ }
+ va_list ap;
+ va_start(ap, format);
+ size_t charsWritten = AssignVsprintf(maxLength + 1, format, ap);
+ va_end(ap);
+ return charsWritten;
+ }
+
+ /*
+ * Allocates enough for the formatted text desired. Returns maximum storable
+ * length of a string in the allocated buffer on success, or 0 on failure.
+ */
+ size_t AllocFromScprintf(const wchar_t* format, ...) {
+ va_list ap;
+ va_start(ap, format);
+ size_t returnValue = AllocFromVscprintf(format, ap);
+ va_end(ap);
+ return returnValue;
+ }
+ /**
+ * Same as the above, but takes a va_list like vscprintf does.
+ */
+ size_t AllocFromVscprintf(const wchar_t* format, va_list ap) {
+ int len = _vscwprintf(format, ap);
+ if (len < 0) {
+ Truncate();
+ return 0;
+ }
+ if (!AllocEmpty(len)) {
+ // AllocEmpty will Truncate, no need to call it here.
+ return 0;
+ }
+ return len;
+ }
+
+ /**
+ * Automatically determines how much space is necessary, allocates that much
+ * for the string, and assigns the data using swprintf. Returns the resulting
+ * length of the string, which will be 0 if the function fails.
+ */
+ size_t AutoAllocAndAssignSprintf(const wchar_t* format, ...) {
+ va_list ap;
+ va_start(ap, format);
+ size_t len = AllocFromVscprintf(format, ap);
+ va_end(ap);
+ if (len == 0) {
+ // AllocFromVscprintf will Truncate, no need to call it here.
+ return 0;
+ }
+
+ va_start(ap, format);
+ size_t charsWritten = AssignVsprintf(len + 1, format, ap);
+ va_end(ap);
+
+ if (len != charsWritten) {
+ Truncate();
+ return 0;
+ }
+ return charsWritten;
+ }
+
+ /**
+ * The following CopyFrom functions take various types of strings, allocate
+ * enough space to hold them, and then copy them into that space.
+ *
+ * They return an HRESULT that should be interpreted with the SUCCEEDED or
+ * FAILED macro.
+ */
+ HRESULT CopyFrom(const wchar_t* src) {
+ mLength = wcslen(src);
+ if (!AllocLen(mLength)) {
+ Truncate();
+ return E_OUTOFMEMORY;
+ }
+ HRESULT hrv = StringCchCopyW(mString.get(), mLength + 1, src);
+ if (FAILED(hrv)) {
+ Truncate();
+ }
+ return hrv;
+ }
+ HRESULT CopyFrom(const SimpleAutoString& src) {
+ if (!src.mString) {
+ Truncate();
+ return S_OK;
+ }
+ mLength = src.mLength;
+ if (!AllocLen(mLength)) {
+ Truncate();
+ return E_OUTOFMEMORY;
+ }
+ HRESULT hrv = StringCchCopyW(mString.get(), mLength + 1, src.mString.get());
+ if (FAILED(hrv)) {
+ Truncate();
+ }
+ return hrv;
+ }
+ HRESULT CopyFrom(const char* src) {
+ int bufferSize =
+ MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, src, -1, nullptr, 0);
+ if (bufferSize == 0) {
+ Truncate();
+ return HRESULT_FROM_WIN32(GetLastError());
+ }
+ if (!AllocSize(bufferSize)) {
+ Truncate();
+ return E_OUTOFMEMORY;
+ }
+ int charsWritten = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, src,
+ -1, mString.get(), bufferSize);
+ if (charsWritten == 0) {
+ Truncate();
+ return HRESULT_FROM_WIN32(GetLastError());
+ } else if (charsWritten != bufferSize) {
+ Truncate();
+ return E_FAIL;
+ }
+ mLength = charsWritten - 1;
+ return S_OK;
+ }
+
+ bool StartsWith(const SimpleAutoString& prefix) const {
+ if (!mString) {
+ return (prefix.mLength == 0);
+ }
+ if (!prefix.mString) {
+ return true;
+ }
+ if (prefix.mLength > mLength) {
+ return false;
+ }
+ return (wcsncmp(mString.get(), prefix.mString.get(), prefix.mLength) == 0);
+ }
+};
+
+// Deleter for use with UniquePtr
+struct CoTaskMemFreeDeleter {
+ void operator()(void* aPtr) { ::CoTaskMemFree(aPtr); }
+};
+
+/**
+ * A lot of data goes into constructing an ACL and security attributes, and the
+ * Windows documentation does not make it very clear what can be safely freed
+ * after these objects are constructed. This struct holds all of the
+ * construction data in one place so that it can be passed around and freed
+ * properly.
+ */
+struct AutoPerms {
+ SID_IDENTIFIER_AUTHORITY sidIdentifierAuthority;
+ UniqueSidPtr usersSID;
+ UniqueSidPtr adminsSID;
+ UniqueSidPtr systemSID;
+ EXPLICIT_ACCESS_W ea[3];
+ mozilla::UniquePtr<ACL, LocalFreeDeleter> acl;
+ mozilla::UniquePtr<uint8_t[]> securityDescriptorBuffer;
+ PSECURITY_DESCRIPTOR securityDescriptor;
+ SECURITY_ATTRIBUTES securityAttributes;
+};
+
+static HRESULT GetFilename(SimpleAutoString& path, SimpleAutoString& filename);
+
+enum class Tristate { False, True, Unknown };
+
+enum class Lockstate { Locked, Unlocked };
+
+/**
+ * This class will look up and store some data about the file or directory at
+ * the path given.
+ * The path can additionally be locked. For files, this is done by holding a
+ * handle to that file. For directories, this is done by holding a handle to a
+ * file within the directory.
+ */
+class FileOrDirectory {
+ private:
+ Tristate mIsHardLink;
+ DWORD mAttributes;
+ nsAutoHandle mLockHandle;
+ // This stores the name of the lock file. We need to keep track of this for
+ // directories, which are locked via a randomly named lock file inside. But
+ // we do not store a value here for files, as they do not have a separate lock
+ // file.
+ SimpleAutoString mDirLockFilename;
+
+ /**
+ * Locks the path. For directories, this is done by opening a file in the
+ * directory and storing its handle in mLockHandle. For files, we just open
+ * the file itself and store the handle.
+ * Returns true on success and false on failure.
+ *
+ * Calling this function will result in mAttributes being updated.
+ *
+ * This function is private to prevent callers from locking the directory
+ * after its attributes have been read. Part of the purpose of locking a
+ * directory is to ensure that its attributes are what we think they are and
+ * that they don't change while we hold the lock. If we get the lock after
+ * attributes are looked up, we can no longer provide that guarantee.
+ * If you think you want to call Lock(), you probably actually want to call
+ * Reset().
+ */
+ bool Lock(const wchar_t* path) {
+ mAttributes = GetFileAttributesW(path);
+ Tristate isDir = IsDirectory();
+ if (isDir == Tristate::Unknown) {
+ return false;
+ }
+
+ if (isDir == Tristate::True) {
+ SimpleAutoString lockPath;
+ if (!lockPath.AllocEmpty(MAX_PATH)) {
+ return false;
+ }
+ BOOL success = GetUUIDTempFilePath(path, NS_T(LOCK_FILE_PREFIX),
+ lockPath.MutableString());
+ if (!success || !lockPath.Check()) {
+ return false;
+ }
+
+ HRESULT hrv = GetFilename(lockPath, mDirLockFilename);
+ if (FAILED(hrv) || mDirLockFilename.Length() == 0) {
+ return false;
+ }
+
+ mLockHandle.own(CreateFileW(lockPath.String(), 0, 0, nullptr, OPEN_ALWAYS,
+ FILE_FLAG_DELETE_ON_CLOSE, nullptr));
+ } else { // If path is not a directory
+ // The usual reason for us to lock a file is to read and change the
+ // permissions so, unlike the directory lock file, make sure we request
+ // the access necessary to read and write permissions.
+ mLockHandle.own(CreateFileW(path, WRITE_DAC | READ_CONTROL, 0, nullptr,
+ OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL,
+ nullptr));
+ }
+ if (!IsLocked()) {
+ return false;
+ }
+ mAttributes = GetFileAttributesW(path);
+ // Directories and files are locked in different ways. If we think that we
+ // just locked one but we actually locked the other, our lock will be
+ // ineffective and we should not tell callers that this is locked.
+ // (This should fail earlier, since files cannot have children and
+ // directories cannot be opened with FILE_ATTRIBUTE_NORMAL. But just to be
+ // safe...)
+ if (isDir != IsDirectory()) {
+ Unlock();
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Helper function to normalize the access mask by converting generic access
+ * flags to specific ones to make it easier to check if permissions match.
+ */
+ void NormalizeAccessMask(ACCESS_MASK& mask) {
+ if ((mask & GENERIC_ALL) == GENERIC_ALL) {
+ mask &= ~GENERIC_ALL;
+ mask |= FILE_ALL_ACCESS;
+ }
+ if ((mask & GENERIC_READ) == GENERIC_READ) {
+ mask &= ~GENERIC_READ;
+ mask |= FILE_GENERIC_READ;
+ }
+ if ((mask & GENERIC_WRITE) == GENERIC_WRITE) {
+ mask &= ~GENERIC_WRITE;
+ mask |= FILE_GENERIC_WRITE;
+ }
+ if ((mask & GENERIC_EXECUTE) == GENERIC_EXECUTE) {
+ mask &= ~GENERIC_EXECUTE;
+ mask |= FILE_GENERIC_EXECUTE;
+ }
+ }
+
+ public:
+ FileOrDirectory()
+ : mIsHardLink(Tristate::Unknown),
+ mAttributes(INVALID_FILE_ATTRIBUTES),
+ mLockHandle(INVALID_HANDLE_VALUE) {}
+
+ /**
+ * If shouldLock is Locked:Locked, the file or directory will be locked.
+ * Note that locking is fallible and success should be checked via the
+ * IsLocked method.
+ */
+ FileOrDirectory(const SimpleAutoString& path, Lockstate shouldLock)
+ : FileOrDirectory() {
+ Reset(path, shouldLock);
+ }
+
+ /**
+ * Initializes the FileOrDirectory to the file with the path given.
+ *
+ * If shouldLock is Locked:Locked, the file or directory will be locked.
+ * Note that locking is fallible and success should be checked via the
+ * IsLocked method.
+ */
+ void Reset(const SimpleAutoString& path, Lockstate shouldLock) {
+ Unlock();
+ mDirLockFilename.Truncate();
+ if (shouldLock == Lockstate::Locked) {
+ // This will also update mAttributes.
+ Lock(path.String());
+ } else {
+ mAttributes = GetFileAttributesW(path.String());
+ }
+
+ mIsHardLink = Tristate::Unknown;
+ nsAutoHandle autoHandle;
+ HANDLE handle;
+ if (IsLocked() && IsDirectory() == Tristate::False) {
+ // If the path is a file and we locked it, we already have a handle to it.
+ // No need to open it again.
+ handle = mLockHandle.get();
+ } else {
+ handle = CreateFileW(path.String(), 0, FILE_SHARE_READ, nullptr,
+ OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr);
+ // Make sure this handle gets freed automatically.
+ autoHandle.own(handle);
+ }
+
+ Tristate isLink = Tristate::Unknown;
+ if (handle != INVALID_HANDLE_VALUE) {
+ BY_HANDLE_FILE_INFORMATION info;
+ BOOL success = GetFileInformationByHandle(handle, &info);
+ if (success) {
+ if (info.nNumberOfLinks > 1) {
+ isLink = Tristate::True;
+ } else {
+ isLink = Tristate::False;
+ }
+ }
+ }
+
+ mIsHardLink = Tristate::Unknown;
+ Tristate isSymLink = IsSymLink();
+ if (isLink == Tristate::False || isSymLink == Tristate::True) {
+ mIsHardLink = Tristate::False;
+ } else if (isLink == Tristate::True && isSymLink == Tristate::False) {
+ mIsHardLink = Tristate::True;
+ }
+ }
+
+ void Unlock() { mLockHandle.own(INVALID_HANDLE_VALUE); }
+
+ bool IsLocked() const { return mLockHandle.get() != INVALID_HANDLE_VALUE; }
+
+ Tristate IsSymLink() const {
+ if (mAttributes == INVALID_FILE_ATTRIBUTES) {
+ return Tristate::Unknown;
+ }
+ if (mAttributes & FILE_ATTRIBUTE_REPARSE_POINT) {
+ return Tristate::True;
+ }
+ return Tristate::False;
+ }
+
+ Tristate IsHardLink() const { return mIsHardLink; }
+
+ Tristate IsLink() const {
+ Tristate isSymLink = IsSymLink();
+ if (mIsHardLink == Tristate::True || isSymLink == Tristate::True) {
+ return Tristate::True;
+ }
+ if (mIsHardLink == Tristate::Unknown || isSymLink == Tristate::Unknown) {
+ return Tristate::Unknown;
+ }
+ return Tristate::False;
+ }
+
+ Tristate IsDirectory() const {
+ if (mAttributes == INVALID_FILE_ATTRIBUTES) {
+ return Tristate::Unknown;
+ }
+ if (mAttributes & FILE_ATTRIBUTE_DIRECTORY) {
+ return Tristate::True;
+ }
+ return Tristate::False;
+ }
+
+ Tristate IsReadonly() const {
+ if (mAttributes == INVALID_FILE_ATTRIBUTES) {
+ return Tristate::Unknown;
+ }
+ if (mAttributes & FILE_ATTRIBUTE_READONLY) {
+ return Tristate::True;
+ }
+ return Tristate::False;
+ }
+
+ DWORD Attributes() const { return mAttributes; }
+
+ /**
+ * Sets the permissions to those passed. For this to be done safely, the file
+ * must be locked and must not be a directory or a link. If these conditions
+ * are not met, the function will fail.
+ * Without locking, we can't guarantee that the file is the one we think it
+ * is. Someone might have replaced a component of the path with a symlink.
+ * With directories, setting the permissions can have the effect of setting
+ * the permissions of a malicious hardlink within.
+ */
+ HRESULT SetPerms(const AutoPerms& perms) {
+ if (IsDirectory() != Tristate::False || !IsLocked() ||
+ IsHardLink() != Tristate::False) {
+ return E_FAIL;
+ }
+
+ DWORD drv = SetSecurityInfo(mLockHandle.get(), SE_FILE_OBJECT,
+ DACL_SECURITY_INFORMATION, nullptr, nullptr,
+ perms.acl.get(), nullptr);
+ return HRESULT_FROM_WIN32(drv);
+ }
+
+ /**
+ * Checks the permissions of a file to make sure that they match the expected
+ * permissions.
+ */
+ Tristate PermsOk(const SimpleAutoString& path, const AutoPerms& perms) {
+ nsAutoHandle autoHandle;
+ HANDLE handle;
+ if (IsDirectory() == Tristate::False && IsLocked()) {
+ handle = mLockHandle.get();
+ } else {
+ handle =
+ CreateFileW(path.String(), READ_CONTROL, FILE_SHARE_READ, nullptr,
+ OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr);
+ // Make sure this handle gets freed automatically.
+ autoHandle.own(handle);
+ }
+
+ PACL dacl = nullptr;
+ SECURITY_DESCRIPTOR* securityDescriptor = nullptr;
+ DWORD drv = GetSecurityInfo(
+ handle, SE_FILE_OBJECT, DACL_SECURITY_INFORMATION, nullptr, nullptr,
+ &dacl, nullptr,
+ reinterpret_cast<PSECURITY_DESCRIPTOR*>(&securityDescriptor));
+ // Store the security descriptor in a UniquePtr so that it automatically
+ // gets freed properly. We don't need to worry about dacl, since it will
+ // point within the security descriptor.
+ mozilla::UniquePtr<SECURITY_DESCRIPTOR, LocalFreeDeleter>
+ autoSecurityDescriptor(securityDescriptor);
+ if (drv == ERROR_ACCESS_DENIED) {
+ // If access was denied reading the permissions, it seems pretty safe to
+ // say that the permissions are wrong.
+ return Tristate::False;
+ }
+ if (drv != ERROR_SUCCESS || dacl == nullptr) {
+ return Tristate::Unknown;
+ }
+
+ size_t eaLen = sizeof(perms.ea) / sizeof(perms.ea[0]);
+ for (size_t eaIndex = 0; eaIndex < eaLen; ++eaIndex) {
+ PTRUSTEE_W trustee = const_cast<PTRUSTEE_W>(&perms.ea[eaIndex].Trustee);
+ ACCESS_MASK expectedMask = perms.ea[eaIndex].grfAccessPermissions;
+ ACCESS_MASK actualMask;
+ drv = GetEffectiveRightsFromAclW(dacl, trustee, &actualMask);
+ if (drv == ERROR_ACCESS_DENIED) {
+ return Tristate::False;
+ }
+ if (drv != ERROR_SUCCESS) {
+ return Tristate::Unknown;
+ }
+ NormalizeAccessMask(expectedMask);
+ NormalizeAccessMask(actualMask);
+ if ((actualMask & expectedMask) != expectedMask) {
+ return Tristate::False;
+ }
+ }
+
+ return Tristate::True;
+ }
+
+ /**
+ * Valid only if IsDirectory() == True.
+ * Checks to see if the string given matches the filename of the lock file.
+ */
+ bool LockFilenameMatches(const wchar_t* filename) {
+ if (mDirLockFilename.Length() == 0) {
+ return false;
+ }
+ return wcscmp(filename, mDirLockFilename.String()) == 0;
+ }
+};
+
+static bool GetCachedHash(const char16_t* installPath, HKEY rootKey,
+ const SimpleAutoString& regPath,
+ mozilla::UniquePtr<NS_tchar[]>& result);
+static HRESULT GetUpdateDirectory(const wchar_t* installPath,
+ const char* vendor, const char* appName,
+ WhichUpdateDir whichDir,
+ SetPermissionsOf permsToSet,
+ mozilla::UniquePtr<wchar_t[]>& result);
+static HRESULT EnsureUpdateDirectoryPermissions(
+ const SimpleAutoString& basePath, const SimpleAutoString& updatePath,
+ bool fullUpdatePath, SetPermissionsOf permsToSet);
+static HRESULT GeneratePermissions(AutoPerms& result);
+static HRESULT MakeDir(const SimpleAutoString& path, const AutoPerms& perms);
+static HRESULT RemoveRecursive(const SimpleAutoString& path,
+ FileOrDirectory& file);
+static HRESULT MoveConflicting(const SimpleAutoString& path,
+ FileOrDirectory& file,
+ SimpleAutoString* outPath);
+static HRESULT EnsureCorrectPermissions(SimpleAutoString& path,
+ FileOrDirectory& file,
+ const SimpleAutoString& leafUpdateDir,
+ const AutoPerms& perms,
+ SetPermissionsOf permsToSet);
+static HRESULT FixDirectoryPermissions(const SimpleAutoString& path,
+ FileOrDirectory& directory,
+ const AutoPerms& perms,
+ bool& permissionsFixed);
+static HRESULT MoveFileOrDir(const SimpleAutoString& moveFrom,
+ const SimpleAutoString& moveTo,
+ const AutoPerms& perms);
+static HRESULT SplitPath(const SimpleAutoString& path,
+ SimpleAutoString& parentPath,
+ SimpleAutoString& filename);
+static bool PathConflictsWithLeaf(const SimpleAutoString& path,
+ const SimpleAutoString& leafPath);
+#endif // XP_WIN
+
+/**
+ * Returns a hash of the install path, suitable for uniquely identifying the
+ * particular Firefox installation that is running.
+ *
+ * This function includes a compatibility mode that should NOT be used except by
+ * GetUserUpdateDirectory. Previous implementations of this function could
+ * return a value inconsistent with what our installer would generate. When the
+ * update directory was migrated, this function was re-implemented to return
+ * values consistent with those generated by the installer. The compatibility
+ * mode is retained only so that we can properly get the old update directory
+ * when migrating it.
+ *
+ * @param installPath
+ * The null-terminated path to the installation directory (i.e. the
+ * directory that contains the binary). Must not be null. The path must
+ * not include a trailing slash.
+ * @param vendor
+ * A pointer to a null-terminated string containing the vendor name, or
+ * null. This is only used to look up a registry key on Windows. On
+ * other platforms, the value has no effect. If null is passed on
+ * Windows, "Mozilla" will be used.
+ * @param result
+ * The out parameter that will be set to contain the resulting hash.
+ * The value is wrapped in a UniquePtr to make cleanup easier on the
+ * caller.
+ * @param useCompatibilityMode
+ * Enables compatibility mode. Defaults to false.
+ * @return true if successful and false otherwise.
+ */
+bool GetInstallHash(const char16_t* installPath, const char* vendor,
+ mozilla::UniquePtr<NS_tchar[]>& result,
+ bool useCompatibilityMode /* = false */) {
+ MOZ_ASSERT(installPath != nullptr,
+ "Install path must not be null in GetInstallHash");
+
+ // Unable to get the cached hash, so compute it.
+ size_t pathSize =
+ std::char_traits<char16_t>::length(installPath) * sizeof(*installPath);
+ uint64_t hash =
+ CityHash64(reinterpret_cast<const char*>(installPath), pathSize);
+
+ size_t hashStrSize = sizeof(hash) * 2 + 1; // 2 hex digits per byte + null
+ result = mozilla::MakeUnique<NS_tchar[]>(hashStrSize);
+ int charsWritten;
+ if (useCompatibilityMode) {
+ // This behavior differs slightly from the default behavior.
+ // When the default output would be "1234567800000009", this instead
+ // produces "123456789".
+ charsWritten = NS_tsnprintf(result.get(), hashStrSize,
+ NS_T("%") NS_T(PRIX32) NS_T("%") NS_T(PRIX32),
+ static_cast<uint32_t>(hash >> 32),
+ static_cast<uint32_t>(hash));
+ } else {
+ charsWritten =
+ NS_tsnprintf(result.get(), hashStrSize, NS_T("%") NS_T(PRIX64), hash);
+ }
+ return !(charsWritten < 1 ||
+ static_cast<size_t>(charsWritten) > hashStrSize - 1);
+}
+
+#ifdef XP_WIN
+/**
+ * Returns true if the registry key was successfully found and read into result.
+ */
+static bool GetCachedHash(const char16_t* installPath, HKEY rootKey,
+ const SimpleAutoString& regPath,
+ mozilla::UniquePtr<NS_tchar[]>& result) {
+ // Find the size of the string we are reading before we read it so we can
+ // allocate space.
+ unsigned long bufferSize;
+ LSTATUS lrv = RegGetValueW(rootKey, regPath.String(),
+ reinterpret_cast<const wchar_t*>(installPath),
+ RRF_RT_REG_SZ, nullptr, nullptr, &bufferSize);
+ if (lrv != ERROR_SUCCESS) {
+ return false;
+ }
+ result = mozilla::MakeUnique<NS_tchar[]>(bufferSize);
+ if (!result) {
+ return false;
+ }
+ // Now read the actual value from the registry.
+ lrv = RegGetValueW(rootKey, regPath.String(),
+ reinterpret_cast<const wchar_t*>(installPath),
+ RRF_RT_REG_SZ, nullptr, result.get(), &bufferSize);
+ return (lrv == ERROR_SUCCESS);
+}
+
+/**
+ * Returns the update directory path. The update directory needs to have
+ * different permissions from the default, so we don't really want anyone using
+ * the path without the directory already being created with the correct
+ * permissions. Therefore, this function also ensures that the base directory
+ * that needs permissions set already exists. If it does not exist, it is
+ * created with the needed permissions.
+ * The desired permissions give Full Control to SYSTEM, Administrators, and
+ * Users.
+ *
+ * vendor and appName are passed as char*, not because we want that (we don't,
+ * we want wchar_t), but because of the expected origin of the data. If this
+ * data is available, it is probably available via XREAppData::vendor and
+ * XREAppData::name.
+ *
+ * @param installPath
+ * The null-terminated path to the installation directory (i.e. the
+ * directory that contains the binary). The path must not include a
+ * trailing slash. If null is passed for this value, the entire update
+ * directory path cannot be retrieved, so the function will return the
+ * update directory without the installation-specific leaf directory.
+ * This feature exists for when the caller wants to use this function
+ * to set directory permissions and does not need the full update
+ * directory path.
+ * @param vendor
+ * A pointer to a null-terminated string containing the vendor name.
+ * Will default to "Mozilla" if null is passed.
+ * @param appName
+ * A pointer to a null-terminated string containing the application
+ * name, or null.
+ * @param permsToSet
+ * Determines how aggressive to be when setting permissions.
+ * This is the behavior by value:
+ * BaseDirIfNotExists - Sets the permissions on the base
+ * directory, but only if it does not
+ * already exist.
+ * AllFilesAndDirs - Recurses through the base directory,
+ * setting the permissions on all files
+ * and directories contained. Symlinks
+ * are removed. Files with names
+ * conflicting with the creation of the
+ * update directory are moved or removed.
+ * FilesAndDirsWithBadPerms - Same as AllFilesAndDirs, but does not
+ * attempt to fix permissions if they
+ * cannot be determined.
+ * @param result
+ * The out parameter that will be set to contain the resulting path.
+ * The value is wrapped in a UniquePtr to make cleanup easier on the
+ * caller.
+ *
+ * @return An HRESULT that should be tested with SUCCEEDED or FAILED.
+ */
+HRESULT
+GetCommonUpdateDirectory(const wchar_t* installPath,
+ SetPermissionsOf permsToSet,
+ mozilla::UniquePtr<wchar_t[]>& result) {
+ return GetUpdateDirectory(installPath, nullptr, nullptr,
+ WhichUpdateDir::CommonAppData, permsToSet, result);
+}
+
+/**
+ * This function is identical to the function above except that it gets the
+ * "old" (pre-migration) update directory that is located in the user's app data
+ * directory, rather than the new one in the common app data directory.
+ *
+ * The other difference is that this function does not create or change the
+ * permissions of the update directory since the default permissions on this
+ * directory are acceptable as they are.
+ */
+HRESULT
+GetUserUpdateDirectory(const wchar_t* installPath, const char* vendor,
+ const char* appName,
+ mozilla::UniquePtr<wchar_t[]>& result) {
+ return GetUpdateDirectory(
+ installPath, vendor, appName, WhichUpdateDir::UserAppData,
+ SetPermissionsOf::BaseDirIfNotExists, // Arbitrary value
+ result);
+}
+
+/**
+ * This is a much more limited version of the GetCommonUpdateDirectory that can
+ * be called from Rust.
+ * The result parameter must be a valid pointer to a buffer of length
+ * MAX_PATH + 1
+ */
+extern "C" HRESULT get_common_update_directory(const wchar_t* installPath,
+ wchar_t* result) {
+ mozilla::UniquePtr<wchar_t[]> uniqueResult;
+ HRESULT hr = GetCommonUpdateDirectory(
+ installPath, SetPermissionsOf::BaseDirIfNotExists, uniqueResult);
+ if (FAILED(hr)) {
+ return hr;
+ }
+ return StringCchCopyW(result, MAX_PATH + 1, uniqueResult.get());
+}
+
+/**
+ * This is a helper function that does all of the work for
+ * GetCommonUpdateDirectory and GetUserUpdateDirectory. It partially exists to
+ * prevent callers of GetUserUpdateDirectory from having to pass a useless
+ * SetPermissionsOf argument, which will be ignored if whichDir is UserAppData.
+ *
+ * For information on the parameters and return value, see
+ * GetCommonUpdateDirectory.
+ */
+static HRESULT GetUpdateDirectory(const wchar_t* installPath,
+ const char* vendor, const char* appName,
+ WhichUpdateDir whichDir,
+ SetPermissionsOf permsToSet,
+ mozilla::UniquePtr<wchar_t[]>& result) {
+ PWSTR baseDirParentPath;
+ REFKNOWNFOLDERID folderID = (whichDir == WhichUpdateDir::CommonAppData)
+ ? FOLDERID_ProgramData
+ : FOLDERID_LocalAppData;
+ HRESULT hrv = SHGetKnownFolderPath(folderID, KF_FLAG_CREATE, nullptr,
+ &baseDirParentPath);
+ // Free baseDirParentPath when it goes out of scope.
+ mozilla::UniquePtr<wchar_t, CoTaskMemFreeDeleter> baseDirParentPathUnique(
+ baseDirParentPath);
+ if (FAILED(hrv)) {
+ return hrv;
+ }
+
+ SimpleAutoString baseDir;
+ if (whichDir == WhichUpdateDir::UserAppData && (vendor || appName)) {
+ const char* rawBaseDir = vendor ? vendor : appName;
+ hrv = baseDir.CopyFrom(rawBaseDir);
+ } else {
+ const wchar_t baseDirLiteral[] = NS_T(FALLBACK_VENDOR_NAME);
+ hrv = baseDir.CopyFrom(baseDirLiteral);
+ }
+ if (FAILED(hrv)) {
+ return hrv;
+ }
+
+ // Generate the base path (C:\ProgramData\Mozilla)
+ SimpleAutoString basePath;
+ size_t basePathLen =
+ wcslen(baseDirParentPath) + 1 /* path separator */ + baseDir.Length();
+ basePath.AllocAndAssignSprintf(basePathLen, L"%s\\%s", baseDirParentPath,
+ baseDir.String());
+ if (basePath.Length() != basePathLen) {
+ return E_FAIL;
+ }
+
+ // Generate the update directory path. This is the value to be returned by
+ // this function.
+ SimpleAutoString updatePath;
+ if (installPath) {
+ mozilla::UniquePtr<NS_tchar[]> hash;
+
+ // The Windows installer caches this hash value in the registry
+ bool gotHash = false;
+ SimpleAutoString regPath;
+ regPath.AutoAllocAndAssignSprintf(L"SOFTWARE\\%S\\%S\\TaskBarIDs",
+ vendor ? vendor : "Mozilla",
+ MOZ_APP_BASENAME);
+ if (regPath.Length() != 0) {
+ gotHash = GetCachedHash(reinterpret_cast<const char16_t*>(installPath),
+ HKEY_LOCAL_MACHINE, regPath, hash);
+ if (!gotHash) {
+ gotHash = GetCachedHash(reinterpret_cast<const char16_t*>(installPath),
+ HKEY_CURRENT_USER, regPath, hash);
+ }
+ }
+ bool success = true;
+ if (!gotHash) {
+ bool useCompatibilityMode = (whichDir == WhichUpdateDir::UserAppData);
+ success = GetInstallHash(reinterpret_cast<const char16_t*>(installPath),
+ vendor, hash, useCompatibilityMode);
+ }
+ if (success) {
+ const wchar_t midPathDirName[] = NS_T(UPDATE_PATH_MID_DIR_NAME);
+ size_t updatePathLen = basePath.Length() + 1 /* path separator */ +
+ wcslen(midPathDirName) + 1 /* path separator */ +
+ wcslen(hash.get());
+ updatePath.AllocAndAssignSprintf(updatePathLen, L"%s\\%s\\%s",
+ basePath.String(), midPathDirName,
+ hash.get());
+ // Permissions can still be set without this string, so wait until after
+ // setting permissions to return failure if the string assignment failed.
+ }
+ }
+
+ if (whichDir == WhichUpdateDir::CommonAppData) {
+ if (updatePath.Length() > 0) {
+ hrv = EnsureUpdateDirectoryPermissions(basePath, updatePath, true,
+ permsToSet);
+ } else {
+ hrv = EnsureUpdateDirectoryPermissions(basePath, basePath, false,
+ permsToSet);
+ }
+ if (FAILED(hrv)) {
+ return hrv;
+ }
+ }
+
+ if (!installPath) {
+ basePath.SwapBufferWith(result);
+ return S_OK;
+ }
+
+ if (updatePath.Length() == 0) {
+ return E_FAIL;
+ }
+ updatePath.SwapBufferWith(result);
+ return S_OK;
+}
+
+/**
+ * If the basePath does not exist, it is created with the expected permissions.
+ *
+ * It used to be that if basePath exists and SetPermissionsOf::AllFilesAndDirs
+ * was passed in, this function would aggressively set the permissions of
+ * the directory and everything in it. But that caused a problem: There does not
+ * seem to be a good way to ensure that, when setting permissions on a
+ * directory, a malicious process does not sneak a hard link into that directory
+ * (causing it to inherit the permissions set on the directory).
+ *
+ * To address that issue, this function now takes a different approach.
+ * To prevent abuse, permissions of directories will not be changed.
+ * Instead, directories with bad permissions are deleted and re-created with the
+ * correct permissions.
+ *
+ * @param basePath
+ * The top directory within the application data directory.
+ * Typically "C:\ProgramData\Mozilla".
+ * @param updatePath
+ * The update directory to be checked for conflicts. If files
+ * conflicting with this directory structure exist, they may be moved
+ * or deleted depending on the value of permsToSet.
+ * @param fullUpdatePath
+ * Set to true if updatePath is the full update path. If set to false,
+ * it means that we don't have the installation-specific path
+ * component.
+ * @param permsToSet
+ * See the documentation for GetCommonUpdateDirectory for the
+ * descriptions of the effects of each SetPermissionsOf value.
+ */
+static HRESULT EnsureUpdateDirectoryPermissions(
+ const SimpleAutoString& basePath, const SimpleAutoString& updatePath,
+ bool fullUpdatePath, SetPermissionsOf permsToSet) {
+ HRESULT returnValue = S_OK; // Stores the value that will eventually be
+ // returned. If errors occur, this is set to the
+ // first error encountered.
+
+ Lockstate shouldLock = permsToSet == SetPermissionsOf::BaseDirIfNotExists
+ ? Lockstate::Unlocked
+ : Lockstate::Locked;
+ FileOrDirectory baseDir(basePath, shouldLock);
+ // validBaseDir will be true if the basePath exists, and is a non-symlinked
+ // directory.
+ bool validBaseDir = baseDir.IsDirectory() == Tristate::True &&
+ baseDir.IsLink() == Tristate::False;
+
+ // The most common case when calling this function is when the caller of
+ // GetCommonUpdateDirectory just wants the update directory path, and passes
+ // in the least aggressive option for setting permissions.
+ // The most common environment is that the update directory already exists.
+ // Optimize for this case.
+ if (permsToSet == SetPermissionsOf::BaseDirIfNotExists && validBaseDir) {
+ return S_OK;
+ }
+
+ AutoPerms perms;
+ HRESULT hrv = GeneratePermissions(perms);
+ if (FAILED(hrv)) {
+ // Fatal error. There is no real way to recover from this.
+ return hrv;
+ }
+
+ if (permsToSet == SetPermissionsOf::BaseDirIfNotExists) {
+ // We know that the base directory is invalid, because otherwise we would
+ // have exited already.
+ // Ignore errors here. It could be that the directory doesn't exist at all.
+ // And ultimately, we are only interested in whether or not we successfully
+ // create the new directory.
+ MoveConflicting(basePath, baseDir, nullptr);
+
+ hrv = MakeDir(basePath, perms);
+ returnValue = FAILED(returnValue) ? returnValue : hrv;
+ return returnValue;
+ }
+
+ // We need to pass a mutable basePath to EnsureCorrectPermissions, so copy it.
+ SimpleAutoString mutBasePath;
+ hrv = mutBasePath.CopyFrom(basePath);
+ if (FAILED(hrv) || mutBasePath.Length() == 0) {
+ returnValue = FAILED(returnValue) ? returnValue : hrv;
+ return returnValue;
+ }
+
+ if (fullUpdatePath) {
+ // When we are doing a full permissions reset, we are also ensuring that no
+ // files are in the way of our required directory structure. Generate the
+ // path of the furthest leaf in our directory structure so that we can check
+ // for conflicting files.
+ SimpleAutoString leafDirPath;
+ wchar_t updateSubdirectoryName[] = NS_T(UPDATE_SUBDIRECTORY);
+ wchar_t patchDirectoryName[] = NS_T(PATCH_DIRECTORY);
+ size_t leafDirLen = updatePath.Length() + wcslen(updateSubdirectoryName) +
+ wcslen(patchDirectoryName) + 2; /* 2 path separators */
+ leafDirPath.AllocAndAssignSprintf(
+ leafDirLen, L"%s\\%s\\%s", updatePath.String(), updateSubdirectoryName,
+ patchDirectoryName);
+ if (leafDirPath.Length() == leafDirLen) {
+ hrv = EnsureCorrectPermissions(mutBasePath, baseDir, leafDirPath, perms,
+ permsToSet);
+ } else {
+ // If we cannot generate the leaf path, just do the best we can by using
+ // the updatePath.
+ returnValue = FAILED(returnValue) ? returnValue : E_FAIL;
+ hrv = EnsureCorrectPermissions(mutBasePath, baseDir, updatePath, perms,
+ permsToSet);
+ }
+ } else {
+ hrv = EnsureCorrectPermissions(mutBasePath, baseDir, updatePath, perms,
+ permsToSet);
+ }
+ returnValue = FAILED(returnValue) ? returnValue : hrv;
+
+ // EnsureCorrectPermissions does its best to remove links and conflicting
+ // files but, in doing so, it may leave us without a base update directory.
+ // Rather than checking whether it exists first, just try to create it. If
+ // successful, the directory now exists with the right permissions and no
+ // contents, which this function considers a success. If unsuccessful,
+ // most likely the directory just already exists. But we need to verify that
+ // before we can return success.
+ BOOL success = CreateDirectoryW(
+ basePath.String(),
+ const_cast<LPSECURITY_ATTRIBUTES>(&perms.securityAttributes));
+ if (success) {
+ return S_OK;
+ }
+ if (SUCCEEDED(returnValue)) {
+ baseDir.Reset(basePath, Lockstate::Unlocked);
+ if (baseDir.IsDirectory() != Tristate::True ||
+ baseDir.IsLink() != Tristate::False ||
+ baseDir.PermsOk(basePath, perms) != Tristate::True) {
+ return E_FAIL;
+ }
+ }
+
+ return returnValue;
+}
+
+/**
+ * Generates the permission set that we want to be applied to the update
+ * directory and its contents. Returns the permissions data via the result
+ * outparam.
+ *
+ * These are also the permissions that will be used to check that file
+ * permissions are correct.
+ */
+static HRESULT GeneratePermissions(AutoPerms& result) {
+ result.sidIdentifierAuthority = SECURITY_NT_AUTHORITY;
+ ZeroMemory(&result.ea, sizeof(result.ea));
+
+ // Make Users group SID and add it to the Explicit Access List.
+ PSID usersSID = nullptr;
+ BOOL success = AllocateAndInitializeSid(
+ &result.sidIdentifierAuthority, 2, SECURITY_BUILTIN_DOMAIN_RID,
+ DOMAIN_ALIAS_RID_USERS, 0, 0, 0, 0, 0, 0, &usersSID);
+ result.usersSID.reset(usersSID);
+ if (!success) {
+ return HRESULT_FROM_WIN32(GetLastError());
+ }
+ result.ea[0].grfAccessPermissions = FILE_ALL_ACCESS;
+ result.ea[0].grfAccessMode = SET_ACCESS;
+ result.ea[0].grfInheritance = SUB_CONTAINERS_AND_OBJECTS_INHERIT;
+ result.ea[0].Trustee.TrusteeForm = TRUSTEE_IS_SID;
+ result.ea[0].Trustee.TrusteeType = TRUSTEE_IS_GROUP;
+ result.ea[0].Trustee.ptstrName = static_cast<LPWSTR>(usersSID);
+
+ // Make Administrators group SID and add it to the Explicit Access List.
+ PSID adminsSID = nullptr;
+ success = AllocateAndInitializeSid(
+ &result.sidIdentifierAuthority, 2, SECURITY_BUILTIN_DOMAIN_RID,
+ DOMAIN_ALIAS_RID_ADMINS, 0, 0, 0, 0, 0, 0, &adminsSID);
+ result.adminsSID.reset(adminsSID);
+ if (!success) {
+ return HRESULT_FROM_WIN32(GetLastError());
+ }
+ result.ea[1].grfAccessPermissions = FILE_ALL_ACCESS;
+ result.ea[1].grfAccessMode = SET_ACCESS;
+ result.ea[1].grfInheritance = SUB_CONTAINERS_AND_OBJECTS_INHERIT;
+ result.ea[1].Trustee.TrusteeForm = TRUSTEE_IS_SID;
+ result.ea[1].Trustee.TrusteeType = TRUSTEE_IS_GROUP;
+ result.ea[1].Trustee.ptstrName = static_cast<LPWSTR>(adminsSID);
+
+ // Make SYSTEM user SID and add it to the Explicit Access List.
+ PSID systemSID = nullptr;
+ success = AllocateAndInitializeSid(&result.sidIdentifierAuthority, 1,
+ SECURITY_LOCAL_SYSTEM_RID, 0, 0, 0, 0, 0,
+ 0, 0, &systemSID);
+ result.systemSID.reset(systemSID);
+ if (!success) {
+ return HRESULT_FROM_WIN32(GetLastError());
+ }
+ result.ea[2].grfAccessPermissions = FILE_ALL_ACCESS;
+ result.ea[2].grfAccessMode = SET_ACCESS;
+ result.ea[2].grfInheritance = SUB_CONTAINERS_AND_OBJECTS_INHERIT;
+ result.ea[2].Trustee.TrusteeForm = TRUSTEE_IS_SID;
+ result.ea[2].Trustee.TrusteeType = TRUSTEE_IS_USER;
+ result.ea[2].Trustee.ptstrName = static_cast<LPWSTR>(systemSID);
+
+ PACL acl = nullptr;
+ DWORD drv = SetEntriesInAclW(3, result.ea, nullptr, &acl);
+ // Put the ACL in a unique pointer so that LocalFree is called when it goes
+ // out of scope
+ result.acl.reset(acl);
+ if (drv != ERROR_SUCCESS) {
+ return HRESULT_FROM_WIN32(drv);
+ }
+
+ result.securityDescriptorBuffer =
+ mozilla::MakeUnique<uint8_t[]>(SECURITY_DESCRIPTOR_MIN_LENGTH);
+ if (!result.securityDescriptorBuffer) {
+ return E_OUTOFMEMORY;
+ }
+ result.securityDescriptor = reinterpret_cast<PSECURITY_DESCRIPTOR>(
+ result.securityDescriptorBuffer.get());
+ success = InitializeSecurityDescriptor(result.securityDescriptor,
+ SECURITY_DESCRIPTOR_REVISION);
+ if (!success) {
+ return HRESULT_FROM_WIN32(GetLastError());
+ }
+
+ success =
+ SetSecurityDescriptorDacl(result.securityDescriptor, TRUE, acl, FALSE);
+ if (!success) {
+ return HRESULT_FROM_WIN32(GetLastError());
+ }
+
+ result.securityAttributes.nLength = sizeof(SECURITY_ATTRIBUTES);
+ result.securityAttributes.lpSecurityDescriptor = result.securityDescriptor;
+ result.securityAttributes.bInheritHandle = FALSE;
+ return S_OK;
+}
+
+/**
+ * Creates a directory with the permissions specified. If the directory already
+ * exists, this function will return success as long as it is a non-link
+ * directory.
+ */
+static HRESULT MakeDir(const SimpleAutoString& path, const AutoPerms& perms) {
+ BOOL success = CreateDirectoryW(
+ path.String(),
+ const_cast<LPSECURITY_ATTRIBUTES>(&perms.securityAttributes));
+ if (success) {
+ return S_OK;
+ }
+ DWORD error = GetLastError();
+ if (error != ERROR_ALREADY_EXISTS) {
+ return HRESULT_FROM_WIN32(error);
+ }
+ FileOrDirectory dir(path, Lockstate::Unlocked);
+ if (dir.IsDirectory() == Tristate::True && dir.IsLink() == Tristate::False) {
+ return S_OK;
+ }
+ return HRESULT_FROM_WIN32(error);
+}
+
+/**
+ * Attempts to move the file or directory to the Windows Recycle Bin.
+ * If removal fails with an ERROR_FILE_NOT_FOUND, the file must not exist, so
+ * this will return success in that case.
+ *
+ * The file will be unlocked in order to remove it.
+ *
+ * Whether this function succeeds or fails, the file parameter should no longer
+ * be considered accurate. If it succeeds, it will be inaccurate because the
+ * file no longer exists. If it fails, it may be inaccurate due to this function
+ * potentially setting file attributes.
+ */
+static HRESULT RemoveRecursive(const SimpleAutoString& path,
+ FileOrDirectory& file) {
+ file.Unlock();
+ if (file.IsReadonly() != Tristate::False) {
+ // Ignore errors setting attributes. We only care if it was successfully
+ // deleted.
+ DWORD attributes = file.Attributes();
+ if (attributes == INVALID_FILE_ATTRIBUTES) {
+ SetFileAttributesW(path.String(), FILE_ATTRIBUTE_NORMAL);
+ } else {
+ SetFileAttributesW(path.String(), attributes & ~FILE_ATTRIBUTE_READONLY);
+ }
+ }
+
+ // The SHFILEOPSTRUCTW expects a list of paths. The list is simply one long
+ // string separated by null characters. The end of the list is designated by
+ // two null characters.
+ SimpleAutoString pathList;
+ pathList.AllocAndAssignSprintf(path.Length() + 1, L"%s\0", path.String());
+
+ SHFILEOPSTRUCTW fileOperation;
+ fileOperation.hwnd = nullptr;
+ fileOperation.wFunc = FO_DELETE;
+ fileOperation.pFrom = pathList.String();
+ fileOperation.pTo = nullptr;
+ fileOperation.fFlags = FOF_ALLOWUNDO | FOF_NO_UI;
+ fileOperation.lpszProgressTitle = nullptr;
+
+ int rv = SHFileOperationW(&fileOperation);
+ if (rv == 0 || rv == ERROR_FILE_NOT_FOUND) {
+ return S_OK;
+ }
+
+ // Some files such as hard links can't be deleted properly with
+ // SHFileOperation, so additionally try DeleteFile.
+ BOOL success = DeleteFileW(path.String());
+ return success ? S_OK : HRESULT_FROM_WIN32(GetLastError());
+}
+
+/**
+ * Attempts to move the file or directory to a path that will not conflict with
+ * our directory structure. If this fails, the path will instead be deleted.
+ *
+ * If an attempt results in the error ERROR_FILE_NOT_FOUND, this function
+ * considers the file to no longer be a conflict and returns success.
+ *
+ * The file will be unlocked in order to move it. Strictly speaking, it may be
+ * possible to move non-directories without unlocking them, but this function
+ * will unconditionally unlock the file.
+ *
+ * If a non-null pointer is passed for outPath, the path that the file was moved
+ * to will be stored there. If the file was removed, an empty string will be
+ * stored. Note that if outPath is set to an empty string, it may not have a
+ * buffer allocated, so outPath.Length() should be checked before using
+ * outPath.String().
+ * It is ok for outPath to point to the path parameter.
+ * This function guarantees that if failure is returned, outPath will not be
+ * modified.
+ */
+static HRESULT MoveConflicting(const SimpleAutoString& path,
+ FileOrDirectory& file,
+ SimpleAutoString* outPath) {
+ file.Unlock();
+ // Try to move the file to a backup location
+ SimpleAutoString newPath;
+ unsigned int maxTries = 3;
+ const wchar_t newPathFormat[] = L"%s.bak%u";
+ size_t newPathMaxLength =
+ newPath.AllocFromScprintf(newPathFormat, path.String(), maxTries);
+ if (newPathMaxLength > 0) {
+ for (unsigned int suffix = 0; suffix <= maxTries; ++suffix) {
+ newPath.AssignSprintf(newPathMaxLength + 1, newPathFormat, path.String(),
+ suffix);
+ if (newPath.Length() == 0) {
+ // If we failed to make this string, we probably aren't going to
+ // succeed on the next one.
+ break;
+ }
+ BOOL success;
+ if (suffix < maxTries) {
+ success = MoveFileW(path.String(), newPath.String());
+ } else {
+ // Moving a file can sometimes work when deleting a file does not. If
+ // there are already the maximum number of backed up files, try
+ // overwriting the last backup before we fall back to deleting the
+ // original.
+ success = MoveFileExW(path.String(), newPath.String(),
+ MOVEFILE_REPLACE_EXISTING);
+ }
+ if (success) {
+ if (outPath) {
+ outPath->Swap(newPath);
+ }
+ return S_OK;
+ }
+ DWORD drv = GetLastError();
+ if (drv == ERROR_FILE_NOT_FOUND) {
+ if (outPath) {
+ outPath->Truncate();
+ }
+ return S_OK;
+ }
+ // If the move failed because newPath already exists, loop to try a new
+ // suffix. If the move failed for any other reason, a new suffix will
+ // probably not help.
+ // Sometimes, however, if we cannot read the existing file due to lack of
+ // permissions, we may get an "Access Denied" error. So retry in that case
+ // too.
+ if (drv != ERROR_ALREADY_EXISTS && drv != ERROR_ACCESS_DENIED) {
+ break;
+ }
+ }
+ }
+
+ // Moving failed. Try to delete.
+ HRESULT hrv = RemoveRecursive(path, file);
+ if (SUCCEEDED(hrv)) {
+ if (outPath) {
+ outPath->Truncate();
+ }
+ }
+ return hrv;
+}
+
+/**
+ * This function will ensure that the specified path and all contained files and
+ * subdirectories have the correct permissions.
+ * Files will have their permissions set to match those specified.
+ * Unfortunately, setting the permissions on directories is prone to abuse,
+ * since it can potentially result in a hard link within the directory
+ * inheriting those permissions. To get around this issue, directories will not
+ * have their permissions changed. Instead, the directory will be moved
+ * elsewhere so that it can be recreated with the correct permissions and its
+ * contents moved back in.
+ *
+ * Symlinks and hard links are removed from the checked directories.
+ *
+ * This function also ensures that nothing is in the way of leafUpdateDir.
+ * Non-directory files that conflict with this are moved or deleted.
+ *
+ * This function's second argument must receive a locked FileOrDirectory to
+ * ensure that it is not tampered with while fixing the permissions of the
+ * file/directory and any contents.
+ *
+ * If we cannot successfully determine if the path is a file or directory, we
+ * simply attempt to delete it.
+ *
+ * Note that the path parameter is not constant. Its contents may be changed by
+ * this function.
+ */
+static HRESULT EnsureCorrectPermissions(SimpleAutoString& path,
+ FileOrDirectory& file,
+ const SimpleAutoString& leafUpdateDir,
+ const AutoPerms& perms,
+ SetPermissionsOf permsToSet) {
+ HRESULT returnValue = S_OK; // Stores the value that will eventually be
+ // returned. If errors occur, this is set to the
+ // first error encountered.
+ HRESULT hrv;
+ bool conflictsWithLeaf = PathConflictsWithLeaf(path, leafUpdateDir);
+ if (file.IsDirectory() != Tristate::True ||
+ file.IsLink() != Tristate::False) {
+ // We want to keep track of the result of trying to set the permissions
+ // separately from returnValue. If we later remove the file, we should not
+ // report an error to set permissions.
+ // SetPerms will automatically abort and return failure if it is unsafe to
+ // set the permissions on the file (for example, if it is a hard link).
+ HRESULT permSetResult = file.SetPerms(perms);
+
+ bool removed = false;
+ if (file.IsLink() != Tristate::False) {
+ hrv = RemoveRecursive(path, file);
+ returnValue = FAILED(returnValue) ? returnValue : hrv;
+ if (SUCCEEDED(hrv)) {
+ removed = true;
+ }
+ }
+
+ if (FAILED(permSetResult) && !removed) {
+ returnValue = FAILED(returnValue) ? returnValue : permSetResult;
+ }
+
+ if (conflictsWithLeaf && !removed) {
+ hrv = MoveConflicting(path, file, nullptr);
+ returnValue = FAILED(returnValue) ? returnValue : hrv;
+ }
+ return returnValue;
+ }
+
+ // If the permissions cannot be read, only try to fix them if the most
+ // aggressive permission-setting option was passed. If Firefox is experiencing
+ // problems updating, it makes sense to try to force the permissions back to
+ // being correct. But there are other times when this is run more proactively,
+ // and we don't really want to move everything around unnecessarily in those
+ // cases.
+ Tristate permissionsOk = file.PermsOk(path, perms);
+ if (permissionsOk == Tristate::False ||
+ (permissionsOk == Tristate::Unknown &&
+ permsToSet == SetPermissionsOf::AllFilesAndDirs)) {
+ bool permissionsFixed;
+ hrv = FixDirectoryPermissions(path, file, perms, permissionsFixed);
+ returnValue = FAILED(returnValue) ? returnValue : hrv;
+ // We only need to move conflicting directories if they have bad permissions
+ // that we are unable to fix. If its permissions are correct, it isn't
+ // conflicting with the leaf path, it is a component of the leaf path.
+ if (!permissionsFixed && conflictsWithLeaf) {
+ // No need to check for error here. returnValue is already a failure code
+ // because FixDirectoryPermissions failed. MoveConflicting will ensure
+ // that path is correct (or empty, on deletion) whether it succeeds or
+ // fails.
+ MoveConflicting(path, file, &path);
+ if (path.Length() == 0) {
+ // Path has been deleted.
+ return returnValue;
+ }
+ }
+ if (!file.IsLocked()) {
+ // FixDirectoryPermissions or MoveConflicting may have left the directory
+ // unlocked, but we still want to recurse into it, so re-lock it.
+ file.Reset(path, Lockstate::Locked);
+ }
+ } else if (permissionsOk != Tristate::True) {
+ // If we are skipping permission setting, we want to report failure since
+ // this function did not do its job.
+ returnValue = FAILED(returnValue) ? returnValue : E_FAIL;
+ }
+
+ // We MUST not recurse into unlocked directories or links.
+ if (!file.IsLocked() || file.IsLink() != Tristate::False ||
+ file.IsDirectory() != Tristate::True) {
+ returnValue = FAILED(returnValue) ? returnValue : E_FAIL;
+ return returnValue;
+ }
+
+ // Recurse into the directory.
+ DIR directoryHandle(path.String());
+ errno = 0;
+ for (dirent* entry = readdir(&directoryHandle); entry;
+ entry = readdir(&directoryHandle)) {
+ if (wcscmp(entry->d_name, L".") == 0 || wcscmp(entry->d_name, L"..") == 0 ||
+ file.LockFilenameMatches(entry->d_name)) {
+ continue;
+ }
+
+ SimpleAutoString childBuffer;
+ if (!childBuffer.AllocEmpty(MAX_PATH)) {
+ // Just return on this failure rather than continuing. It is unlikely that
+ // this error will go away for the next path we try.
+ return FAILED(returnValue) ? returnValue : E_OUTOFMEMORY;
+ }
+
+ childBuffer.AssignSprintf(MAX_PATH + 1, L"%s\\%s", path.String(),
+ entry->d_name);
+ if (childBuffer.Length() == 0) {
+ returnValue = FAILED(returnValue)
+ ? returnValue
+ : HRESULT_FROM_WIN32(ERROR_BUFFER_OVERFLOW);
+ continue;
+ }
+
+ FileOrDirectory child(childBuffer, Lockstate::Locked);
+ hrv = EnsureCorrectPermissions(childBuffer, child, leafUpdateDir, perms,
+ permsToSet);
+ returnValue = FAILED(returnValue) ? returnValue : hrv;
+
+ // Before looping, clear any errors that might have been encountered so we
+ // can correctly get errors from readdir.
+ errno = 0;
+ }
+ if (errno != 0) {
+ returnValue = FAILED(returnValue) ? returnValue : E_FAIL;
+ }
+
+ return returnValue;
+}
+
+/**
+ * This function fixes directory permissions without setting them directly.
+ * The reasoning behind this is that if someone puts a hardlink in the
+ * directory before we set the permissions, the permissions of the linked file
+ * will be changed too. To prevent this, we will instead move the directory,
+ * recreate it with the correct permissions, and move the contents back in.
+ *
+ * The new directory will be locked with the directory parameter so that the
+ * caller can safely use the new directory. If the function fails, the directory
+ * parameter may be left locked or unlocked. However, the function will never
+ * leave the directory parameter locking something invalid. In other words, if
+ * the directory parameter is locked after this function exits, it is safe to
+ * assume that it is a locked non-link directory at the same location as the
+ * original path.
+ *
+ * The permissionsFixed outparam serves as sort of a supplement to the return
+ * value. The return value will be an error code if any part of this function
+ * fails. But the function can fail at some parts while still completing its
+ * main goal of fixing the directory permissions. To distinguish between these,
+ * this value will be set to true if the directory permissions were successfully
+ * fixed.
+ */
+static HRESULT FixDirectoryPermissions(const SimpleAutoString& path,
+ FileOrDirectory& directory,
+ const AutoPerms& perms,
+ bool& permissionsFixed) {
+ permissionsFixed = false;
+
+ SimpleAutoString parent;
+ SimpleAutoString dirName;
+ HRESULT hrv = SplitPath(path, parent, dirName);
+ if (FAILED(hrv)) {
+ return E_FAIL;
+ }
+
+ SimpleAutoString tempPath;
+ if (!tempPath.AllocEmpty(MAX_PATH)) {
+ return E_FAIL;
+ }
+ BOOL success = GetUUIDTempFilePath(parent.String(), dirName.String(),
+ tempPath.MutableString());
+ if (!success || !tempPath.Check() || tempPath.Length() == 0) {
+ return E_FAIL;
+ }
+
+ directory.Unlock();
+ success = MoveFileW(path.String(), tempPath.String());
+ if (!success) {
+ return HRESULT_FROM_WIN32(GetLastError());
+ }
+
+ success = CreateDirectoryW(path.String(), const_cast<LPSECURITY_ATTRIBUTES>(
+ &perms.securityAttributes));
+ if (!success) {
+ return E_FAIL;
+ }
+ directory.Reset(path, Lockstate::Locked);
+ if (!directory.IsLocked() || directory.IsLink() != Tristate::False ||
+ directory.IsDirectory() != Tristate::True ||
+ directory.PermsOk(path, perms) != Tristate::True) {
+ // Don't leave an invalid file locked when we return.
+ directory.Unlock();
+ return E_FAIL;
+ }
+ permissionsFixed = true;
+
+ FileOrDirectory tempDir(tempPath, Lockstate::Locked);
+ if (!tempDir.IsLocked() || tempDir.IsLink() != Tristate::False ||
+ tempDir.IsDirectory() != Tristate::True) {
+ return E_FAIL;
+ }
+
+ SimpleAutoString moveFrom;
+ SimpleAutoString moveTo;
+ if (!moveFrom.AllocEmpty(MAX_PATH) || !moveTo.AllocEmpty(MAX_PATH)) {
+ return E_OUTOFMEMORY;
+ }
+
+ // If we fail to move one file, we still want to try for the others. This will
+ // store the first error we encounter so it can be returned.
+ HRESULT returnValue = S_OK;
+
+ // Copy the contents of tempDir back to the original directory.
+ DIR directoryHandle(tempPath.String());
+ errno = 0;
+ for (dirent* entry = readdir(&directoryHandle); entry;
+ entry = readdir(&directoryHandle)) {
+ if (wcscmp(entry->d_name, L".") == 0 || wcscmp(entry->d_name, L"..") == 0 ||
+ tempDir.LockFilenameMatches(entry->d_name)) {
+ continue;
+ }
+
+ moveFrom.AssignSprintf(MAX_PATH + 1, L"%s\\%s", tempPath.String(),
+ entry->d_name);
+ if (moveFrom.Length() == 0) {
+ returnValue = FAILED(returnValue)
+ ? returnValue
+ : HRESULT_FROM_WIN32(ERROR_BUFFER_OVERFLOW);
+ continue;
+ }
+
+ moveTo.AssignSprintf(MAX_PATH + 1, L"%s\\%s", path.String(), entry->d_name);
+ if (moveTo.Length() == 0) {
+ returnValue = FAILED(returnValue)
+ ? returnValue
+ : HRESULT_FROM_WIN32(ERROR_BUFFER_OVERFLOW);
+ continue;
+ }
+
+ hrv = MoveFileOrDir(moveFrom, moveTo, perms);
+ if (FAILED(hrv)) {
+ returnValue = FAILED(returnValue) ? returnValue : hrv;
+ }
+
+ // Before looping, clear any errors that might have been encountered so we
+ // can correctly get errors from readdir.
+ errno = 0;
+ }
+ if (errno != 0) {
+ returnValue = FAILED(returnValue) ? returnValue : E_FAIL;
+ }
+
+ hrv = RemoveRecursive(tempPath, tempDir);
+ returnValue = FAILED(returnValue) ? returnValue : hrv;
+
+ return returnValue;
+}
+
+/**
+ * This function moves a file or directory from one location to another.
+ * Sometimes it cannot be moved because something (probably anti-virus) has
+ * opened it. In that case, we copy the file and attempt to remove the original.
+ *
+ * If the file cannot be copied, this function will try to remove the original
+ * anyway.
+ */
+static HRESULT MoveFileOrDir(const SimpleAutoString& moveFrom,
+ const SimpleAutoString& moveTo,
+ const AutoPerms& perms) {
+ BOOL success = MoveFileW(moveFrom.String(), moveTo.String());
+ if (success) {
+ return S_OK;
+ }
+
+ FileOrDirectory fileToMove(moveFrom, Lockstate::Locked);
+
+ // If we fail to move one file, we still want to try for the others. This will
+ // store the first error we encounter so it can be returned.
+ HRESULT returnValue = S_OK;
+
+ if (fileToMove.IsDirectory() != Tristate::True) {
+ fileToMove.Unlock();
+ if (fileToMove.IsLink() == Tristate::False) {
+ success = CopyFileW(moveFrom.String(), moveTo.String(), TRUE);
+ if (!success) {
+ returnValue = FAILED(returnValue) ? returnValue
+ : HRESULT_FROM_WIN32(GetLastError());
+ }
+ }
+ success = DeleteFileW(moveFrom.String());
+ if (!success) {
+ // If we failed to delete it, try having it removed at reboot.
+ success =
+ MoveFileExW(moveFrom.String(), nullptr, MOVEFILE_DELAY_UNTIL_REBOOT);
+ if (!success) {
+ returnValue = FAILED(returnValue) ? returnValue
+ : HRESULT_FROM_WIN32(GetLastError());
+ }
+ }
+ return returnValue;
+ } // Done handling files. The rest of this function is for moving a
+ // directory.
+
+ success = CreateDirectoryW(moveTo.String(), const_cast<LPSECURITY_ATTRIBUTES>(
+ &perms.securityAttributes));
+ if (!success) {
+ return HRESULT_FROM_WIN32(GetLastError());
+ }
+ FileOrDirectory destDir(moveTo, Lockstate::Locked);
+
+ SimpleAutoString childPath;
+ SimpleAutoString childDestPath;
+ if (!childPath.AllocEmpty(MAX_PATH) || !childDestPath.AllocEmpty(MAX_PATH)) {
+ return E_OUTOFMEMORY;
+ }
+
+ if (!fileToMove.IsLocked() || !destDir.IsLocked() ||
+ destDir.IsDirectory() != Tristate::True ||
+ destDir.IsLink() != Tristate::False) {
+ returnValue = FAILED(returnValue) ? returnValue : E_FAIL;
+ } else if (fileToMove.IsLink() == Tristate::False) {
+ DIR directoryHandle(moveFrom.String());
+ errno = 0;
+ for (dirent* entry = readdir(&directoryHandle); entry;
+ entry = readdir(&directoryHandle)) {
+ if (wcscmp(entry->d_name, L".") == 0 ||
+ wcscmp(entry->d_name, L"..") == 0 ||
+ fileToMove.LockFilenameMatches(entry->d_name)) {
+ continue;
+ }
+
+ childPath.AssignSprintf(MAX_PATH + 1, L"%s\\%s", moveFrom.String(),
+ entry->d_name);
+ if (childPath.Length() == 0) {
+ returnValue = FAILED(returnValue)
+ ? returnValue
+ : HRESULT_FROM_WIN32(ERROR_BUFFER_OVERFLOW);
+ continue;
+ }
+
+ childDestPath.AssignSprintf(MAX_PATH + 1, L"%s\\%s", moveTo.String(),
+ entry->d_name);
+ if (childDestPath.Length() == 0) {
+ returnValue = FAILED(returnValue)
+ ? returnValue
+ : HRESULT_FROM_WIN32(ERROR_BUFFER_OVERFLOW);
+ continue;
+ }
+
+ HRESULT hrv = MoveFileOrDir(childPath, childDestPath, perms);
+ if (FAILED(hrv)) {
+ returnValue = FAILED(returnValue) ? returnValue : hrv;
+ }
+
+ // Before looping, clear any errors that might have been encountered so we
+ // can correctly get errors from readdir.
+ errno = 0;
+ }
+ if (errno != 0) {
+ returnValue = FAILED(returnValue) ? returnValue : E_FAIL;
+ }
+ }
+
+ // Everything has been copied out of the directory. Now remove it.
+ HRESULT hrv = RemoveRecursive(moveFrom, fileToMove);
+ if (FAILED(hrv)) {
+ // If we failed to remove it, try having it removed on reboot.
+ success =
+ MoveFileExW(moveFrom.String(), nullptr, MOVEFILE_DELAY_UNTIL_REBOOT);
+ if (!success) {
+ returnValue = FAILED(returnValue) ? returnValue
+ : HRESULT_FROM_WIN32(GetLastError());
+ }
+ }
+
+ return returnValue;
+}
+
+/**
+ * Splits an absolute path into its parent directory and filename.
+ * For example, splits path="C:\foo\bar" into parentPath="C:\foo" and
+ * filename="bar".
+ */
+static HRESULT SplitPath(const SimpleAutoString& path,
+ SimpleAutoString& parentPath,
+ SimpleAutoString& filename) {
+ HRESULT hrv = parentPath.CopyFrom(path);
+ if (FAILED(hrv) || parentPath.Length() == 0) {
+ return hrv;
+ }
+
+ hrv = GetFilename(parentPath, filename);
+ if (FAILED(hrv)) {
+ return hrv;
+ }
+
+ size_t parentPathLen = parentPath.Length();
+ if (parentPathLen < filename.Length() + 1) {
+ return E_FAIL;
+ }
+ parentPathLen -= filename.Length() + 1;
+ parentPath.Truncate(parentPathLen);
+ if (parentPath.Length() == 0) {
+ return E_FAIL;
+ }
+
+ return S_OK;
+}
+
+/**
+ * Gets the filename of the given path. Also removes trailing path separators
+ * from the input path.
+ * Ex: If path="C:\foo\bar", filename="bar"
+ */
+static HRESULT GetFilename(SimpleAutoString& path, SimpleAutoString& filename) {
+ // Remove trailing path separators.
+ size_t pathLen = path.Length();
+ if (pathLen == 0) {
+ return E_FAIL;
+ }
+ wchar_t lastChar = path.String()[pathLen - 1];
+ while (lastChar == '/' || lastChar == '\\') {
+ --pathLen;
+ path.Truncate(pathLen);
+ if (pathLen == 0) {
+ return E_FAIL;
+ }
+ lastChar = path.String()[pathLen - 1];
+ }
+
+ const wchar_t* separator1 = wcsrchr(path.String(), '/');
+ const wchar_t* separator2 = wcsrchr(path.String(), '\\');
+ const wchar_t* separator =
+ (separator1 > separator2) ? separator1 : separator2;
+ if (separator == nullptr) {
+ return E_FAIL;
+ }
+
+ HRESULT hrv = filename.CopyFrom(separator + 1);
+ if (FAILED(hrv) || filename.Length() == 0) {
+ return E_FAIL;
+ }
+ return S_OK;
+}
+
+/**
+ * Returns true if the path conflicts with the leaf path.
+ */
+static bool PathConflictsWithLeaf(const SimpleAutoString& path,
+ const SimpleAutoString& leafPath) {
+ if (!leafPath.StartsWith(path)) {
+ return false;
+ }
+ // Make sure that the next character after the path ends is a path separator
+ // or the end of the string. We don't want to say that "C:\f" conflicts with
+ // "C:\foo\bar".
+ wchar_t charAfterPath = leafPath.String()[path.Length()];
+ return (charAfterPath == L'\\' || charAfterPath == L'\0');
+}
+#endif // XP_WIN
diff --git a/toolkit/mozapps/update/common/commonupdatedir.h b/toolkit/mozapps/update/common/commonupdatedir.h
new file mode 100644
index 0000000000..1a95b491d9
--- /dev/null
+++ b/toolkit/mozapps/update/common/commonupdatedir.h
@@ -0,0 +1,37 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#ifndef COMMONUPDATEDIR_H
+#define COMMONUPDATEDIR_H
+
+#include "mozilla/UniquePtr.h"
+#include "nsError.h"
+
+#ifdef XP_WIN
+# include <windows.h>
+typedef WCHAR NS_tchar;
+#else
+typedef char NS_tchar;
+#endif
+
+bool GetInstallHash(const char16_t* installPath, const char* vendor,
+ mozilla::UniquePtr<NS_tchar[]>& result,
+ bool useCompatibilityMode = false);
+
+#ifdef XP_WIN
+enum class SetPermissionsOf {
+ BaseDirIfNotExists,
+ AllFilesAndDirs,
+ FilesAndDirsWithBadPerms,
+};
+// This function does two things. It retrieves the update directory and it sets
+// the permissions of the directory and, optionally, its contents.
+HRESULT GetCommonUpdateDirectory(const wchar_t* installPath,
+ SetPermissionsOf dirPermsToSet,
+ mozilla::UniquePtr<wchar_t[]>& result);
+HRESULT GetUserUpdateDirectory(const wchar_t* installPath, const char* vendor,
+ const char* appName,
+ mozilla::UniquePtr<wchar_t[]>& result);
+#endif
+
+#endif
diff --git a/toolkit/mozapps/update/common/moz.build b/toolkit/mozapps/update/common/moz.build
new file mode 100644
index 0000000000..2c79661c1f
--- /dev/null
+++ b/toolkit/mozapps/update/common/moz.build
@@ -0,0 +1,76 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+EXPORTS += [
+ "commonupdatedir.h",
+ "readstrings.h",
+ "updatecommon.h",
+ "updatedefines.h",
+ "updatererrors.h",
+]
+
+if CONFIG["OS_ARCH"] == "WINNT":
+ EXPORTS += [
+ "pathhash.h",
+ "uachelper.h",
+ "updatehelper.cpp",
+ "updatehelper.h",
+ "updateutils_win.h",
+ ]
+
+ if CONFIG["MOZ_MAINTENANCE_SERVICE"]:
+ EXPORTS += [
+ "certificatecheck.h",
+ "registrycertificates.h",
+ ]
+
+Library("updatecommon")
+
+DEFINES["NS_NO_XPCOM"] = True
+USE_STATIC_LIBS = True
+
+if CONFIG["OS_ARCH"] == "WINNT":
+ # This forces the creation of updatecommon.lib, which the update agent needs
+ # in order to link to updatecommon library functions.
+ NO_EXPAND_LIBS = True
+
+DisableStlWrapping()
+
+if CONFIG["OS_ARCH"] == "WINNT":
+ SOURCES += [
+ "pathhash.cpp",
+ "uachelper.cpp",
+ "updatehelper.cpp",
+ "updateutils_win.cpp",
+ ]
+ OS_LIBS += [
+ "advapi32",
+ "ole32",
+ "rpcrt4",
+ "shell32",
+ ]
+ if CONFIG["MOZ_MAINTENANCE_SERVICE"]:
+ SOURCES += [
+ "certificatecheck.cpp",
+ "registrycertificates.cpp",
+ ]
+ OS_LIBS += [
+ "crypt32",
+ "wintrust",
+ ]
+
+SOURCES += [
+ "/other-licenses/nsis/Contrib/CityHash/cityhash/city.cpp",
+ "commonupdatedir.cpp",
+ "readstrings.cpp",
+ "updatecommon.cpp",
+]
+
+LOCAL_INCLUDES += [
+ "/other-licenses/nsis/Contrib/CityHash/cityhash",
+]
+
+DEFINES["MOZ_APP_BASENAME"] = '"%s"' % CONFIG["MOZ_APP_BASENAME"]
diff --git a/toolkit/mozapps/update/common/pathhash.cpp b/toolkit/mozapps/update/common/pathhash.cpp
new file mode 100644
index 0000000000..e70f69a755
--- /dev/null
+++ b/toolkit/mozapps/update/common/pathhash.cpp
@@ -0,0 +1,128 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include <windows.h>
+#include <wincrypt.h>
+#include "pathhash.h"
+
+/**
+ * Converts a binary sequence into a hex string
+ *
+ * @param hash The binary data sequence
+ * @param hashSize The size of the binary data sequence
+ * @param hexString A buffer to store the hex string, must be of
+ * size 2 * @hashSize
+ */
+static void BinaryDataToHexString(const BYTE* hash, DWORD& hashSize,
+ LPWSTR hexString) {
+ WCHAR* p = hexString;
+ for (DWORD i = 0; i < hashSize; ++i) {
+ wsprintfW(p, L"%.2x", hash[i]);
+ p += 2;
+ }
+}
+
+/**
+ * Calculates an MD5 hash for the given input binary data
+ *
+ * @param data Any sequence of bytes
+ * @param dataSize The number of bytes inside @data
+ * @param hash Output buffer to store hash, must be freed by the caller
+ * @param hashSize The number of bytes in the output buffer
+ * @return TRUE on success
+ */
+static BOOL CalculateMD5(const char* data, DWORD dataSize, BYTE** hash,
+ DWORD& hashSize) {
+ HCRYPTPROV hProv = 0;
+ HCRYPTHASH hHash = 0;
+
+ if (!CryptAcquireContext(&hProv, nullptr, nullptr, PROV_RSA_FULL,
+ CRYPT_VERIFYCONTEXT)) {
+ if ((DWORD)NTE_BAD_KEYSET != GetLastError()) {
+ return FALSE;
+ }
+
+ // Maybe it doesn't exist, try to create it.
+ if (!CryptAcquireContext(&hProv, nullptr, nullptr, PROV_RSA_FULL,
+ CRYPT_VERIFYCONTEXT | CRYPT_NEWKEYSET)) {
+ return FALSE;
+ }
+ }
+
+ if (!CryptCreateHash(hProv, CALG_MD5, 0, 0, &hHash)) {
+ return FALSE;
+ }
+
+ if (!CryptHashData(hHash, reinterpret_cast<const BYTE*>(data), dataSize, 0)) {
+ return FALSE;
+ }
+
+ DWORD dwCount = sizeof(DWORD);
+ if (!CryptGetHashParam(hHash, HP_HASHSIZE, (BYTE*)&hashSize, &dwCount, 0)) {
+ return FALSE;
+ }
+
+ *hash = new BYTE[hashSize];
+ ZeroMemory(*hash, hashSize);
+ if (!CryptGetHashParam(hHash, HP_HASHVAL, *hash, &hashSize, 0)) {
+ return FALSE;
+ }
+
+ if (hHash) {
+ CryptDestroyHash(hHash);
+ }
+
+ if (hProv) {
+ CryptReleaseContext(hProv, 0);
+ }
+
+ return TRUE;
+}
+
+/**
+ * Converts a file path into a unique registry location for cert storage
+ *
+ * @param filePath The input file path to get a registry path from
+ * @param registryPath A buffer to write the registry path to, must
+ * be of size in WCHARs MAX_PATH + 1
+ * @return TRUE if successful
+ */
+BOOL CalculateRegistryPathFromFilePath(const LPCWSTR filePath,
+ LPWSTR registryPath) {
+ size_t filePathLen = wcslen(filePath);
+ if (!filePathLen) {
+ return FALSE;
+ }
+
+ // If the file path ends in a slash, ignore that character
+ if (filePath[filePathLen - 1] == L'\\' || filePath[filePathLen - 1] == L'/') {
+ filePathLen--;
+ }
+
+ // Copy in the full path into our own buffer.
+ // Copying in the extra slash is OK because we calculate the hash
+ // based on the filePathLen which excludes the slash.
+ // +2 to account for the possibly trailing slash and the null terminator.
+ WCHAR* lowercasePath = new WCHAR[filePathLen + 2];
+ memset(lowercasePath, 0, (filePathLen + 2) * sizeof(WCHAR));
+ wcsncpy(lowercasePath, filePath, filePathLen + 1);
+ _wcslwr(lowercasePath);
+
+ BYTE* hash;
+ DWORD hashSize = 0;
+ if (!CalculateMD5(reinterpret_cast<const char*>(lowercasePath),
+ filePathLen * 2, &hash, hashSize)) {
+ delete[] lowercasePath;
+ return FALSE;
+ }
+ delete[] lowercasePath;
+
+ LPCWSTR baseRegPath =
+ L"SOFTWARE\\Mozilla\\"
+ L"MaintenanceService\\";
+ wcsncpy(registryPath, baseRegPath, MAX_PATH);
+ BinaryDataToHexString(hash, hashSize, registryPath + wcslen(baseRegPath));
+ delete[] hash;
+ return TRUE;
+}
diff --git a/toolkit/mozapps/update/common/pathhash.h b/toolkit/mozapps/update/common/pathhash.h
new file mode 100644
index 0000000000..26a182a411
--- /dev/null
+++ b/toolkit/mozapps/update/common/pathhash.h
@@ -0,0 +1,19 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef _PATHHASH_H_
+#define _PATHHASH_H_
+
+/**
+ * Converts a file path into a unique registry location for cert storage
+ *
+ * @param filePath The input file path to get a registry path from
+ * @param registryPath A buffer to write the registry path to, must
+ * be of size in WCHARs MAX_PATH + 1
+ * @return TRUE if successful
+ */
+BOOL CalculateRegistryPathFromFilePath(const LPCWSTR filePath,
+ LPWSTR registryPath);
+
+#endif
diff --git a/toolkit/mozapps/update/common/readstrings.cpp b/toolkit/mozapps/update/common/readstrings.cpp
new file mode 100644
index 0000000000..9868ef89be
--- /dev/null
+++ b/toolkit/mozapps/update/common/readstrings.cpp
@@ -0,0 +1,397 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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 <algorithm>
+#include <iterator>
+#include <limits.h>
+#include <string.h>
+#include <stdio.h>
+#include "readstrings.h"
+#include "updatererrors.h"
+
+#ifdef XP_WIN
+# define NS_tfopen _wfopen
+# define OPEN_MODE L"rb"
+# define NS_tstrlen wcslen
+# define NS_tstrcpy wcscpy
+#else
+# define NS_tfopen fopen
+# define OPEN_MODE "r"
+# define NS_tstrlen strlen
+# define NS_tstrcpy strcpy
+#endif
+
+// stack based FILE wrapper to ensure that fclose is called.
+class AutoFILE {
+ public:
+ explicit AutoFILE(FILE* fp) : fp_(fp) {}
+ ~AutoFILE() {
+ if (fp_) {
+ fclose(fp_);
+ }
+ }
+ operator FILE*() { return fp_; }
+
+ private:
+ FILE* fp_;
+};
+
+class AutoCharArray {
+ public:
+ explicit AutoCharArray(size_t len) { ptr_ = new char[len]; }
+ ~AutoCharArray() { delete[] ptr_; }
+ operator char*() { return ptr_; }
+
+ private:
+ char* ptr_;
+};
+
+static const char kNL[] = "\r\n";
+static const char kEquals[] = "=";
+static const char kWhitespace[] = " \t";
+static const char kRBracket[] = "]";
+
+static const char* NS_strspnp(const char* delims, const char* str) {
+ const char* d;
+ do {
+ for (d = delims; *d != '\0'; ++d) {
+ if (*str == *d) {
+ ++str;
+ break;
+ }
+ }
+ } while (*d);
+
+ return str;
+}
+
+static char* NS_strtok(const char* delims, char** str) {
+ if (!*str) {
+ return nullptr;
+ }
+
+ char* ret = (char*)NS_strspnp(delims, *str);
+
+ if (!*ret) {
+ *str = ret;
+ return nullptr;
+ }
+
+ char* i = ret;
+ do {
+ for (const char* d = delims; *d != '\0'; ++d) {
+ if (*i == *d) {
+ *i = '\0';
+ *str = ++i;
+ return ret;
+ }
+ }
+ ++i;
+ } while (*i);
+
+ *str = nullptr;
+ return ret;
+}
+
+/**
+ * Find a key in a keyList containing zero-delimited keys ending with "\0\0".
+ * Returns a zero-based index of the key in the list, or -1 if the key is not
+ * found.
+ */
+static int find_key(const char* keyList, char* key) {
+ if (!keyList) {
+ return -1;
+ }
+
+ int index = 0;
+ const char* p = keyList;
+ while (*p) {
+ if (strcmp(key, p) == 0) {
+ return index;
+ }
+
+ p += strlen(p) + 1;
+ index++;
+ }
+
+ // The key was not found if we came here
+ return -1;
+}
+
+/**
+ * A very basic parser for updater.ini taken mostly from nsINIParser.cpp
+ * that can be used by standalone apps.
+ *
+ * @param path Path to the .ini file to read
+ * @param keyList List of zero-delimited keys ending with two zero characters
+ * @param numStrings Number of strings to read into results buffer - must be
+ * equal to the number of keys
+ * @param results Array of strings. Array's length must be equal to
+ * numStrings. Each string will be populated with the value
+ * corresponding to the key with the same index in keyList.
+ * @param section Optional name of the section to read; defaults to "Strings"
+ */
+int ReadStrings(const NS_tchar* path, const char* keyList,
+ unsigned int numStrings, mozilla::UniquePtr<char[]>* results,
+ const char* section) {
+ AutoFILE fp(NS_tfopen(path, OPEN_MODE));
+
+ if (!fp) {
+ return READ_ERROR;
+ }
+
+ /* get file size */
+ if (fseek(fp, 0, SEEK_END) != 0) {
+ return READ_ERROR;
+ }
+
+ long len = ftell(fp);
+ if (len <= 0) {
+ return READ_ERROR;
+ }
+
+ size_t flen = size_t(len);
+ AutoCharArray fileContents(flen + 1);
+ if (!fileContents) {
+ return READ_STRINGS_MEM_ERROR;
+ }
+
+ /* read the file in one swoop */
+ if (fseek(fp, 0, SEEK_SET) != 0) {
+ return READ_ERROR;
+ }
+
+ size_t rd = fread(fileContents, sizeof(char), flen, fp);
+ if (rd != flen) {
+ return READ_ERROR;
+ }
+
+ fileContents[flen] = '\0';
+
+ char* buffer = fileContents;
+ bool inStringsSection = false;
+
+ unsigned int read = 0;
+
+ while (char* token = NS_strtok(kNL, &buffer)) {
+ if (token[0] == '#' || token[0] == ';') { // it's a comment
+ continue;
+ }
+
+ token = (char*)NS_strspnp(kWhitespace, token);
+ if (!*token) { // empty line
+ continue;
+ }
+
+ if (token[0] == '[') { // section header!
+ ++token;
+ char const* currSection = token;
+
+ char* rb = NS_strtok(kRBracket, &token);
+ if (!rb || NS_strtok(kWhitespace, &token)) {
+ // there's either an unclosed [Section or a [Section]Moretext!
+ // we could frankly decide that this INI file is malformed right
+ // here and stop, but we won't... keep going, looking for
+ // a well-formed [section] to continue working with
+ inStringsSection = false;
+ } else {
+ if (section) {
+ inStringsSection = strcmp(currSection, section) == 0;
+ } else {
+ inStringsSection = strcmp(currSection, "Strings") == 0;
+ }
+ }
+
+ continue;
+ }
+
+ if (!inStringsSection) {
+ // If we haven't found a section header (or we found a malformed
+ // section header), or this isn't the [Strings] section don't bother
+ // parsing this line.
+ continue;
+ }
+
+ char* key = token;
+ char* e = NS_strtok(kEquals, &token);
+ if (!e) {
+ continue;
+ }
+
+ int keyIndex = find_key(keyList, key);
+ if (keyIndex >= 0 && (unsigned int)keyIndex < numStrings) {
+ size_t valueSize = strlen(token) + 1;
+ results[keyIndex] = mozilla::MakeUnique<char[]>(valueSize);
+
+ strcpy(results[keyIndex].get(), token);
+ read++;
+ }
+ }
+
+ return (read == numStrings) ? OK : PARSE_ERROR;
+}
+
+// A wrapper function to read strings for the updater.
+// Added for compatibility with the original code.
+int ReadStrings(const NS_tchar* path, StringTable* results) {
+ const unsigned int kNumStrings = 2;
+ const char* kUpdaterKeys = "Title\0Info\0";
+ mozilla::UniquePtr<char[]> updater_strings[kNumStrings];
+
+ int result = ReadStrings(path, kUpdaterKeys, kNumStrings, updater_strings);
+
+ if (result == OK) {
+ results->title.swap(updater_strings[0]);
+ results->info.swap(updater_strings[1]);
+ }
+
+ return result;
+}
+
+IniReader::IniReader(const NS_tchar* iniPath,
+ const char* section /* = nullptr */) {
+ if (iniPath) {
+ mPath = mozilla::MakeUnique<NS_tchar[]>(NS_tstrlen(iniPath) + 1);
+ NS_tstrcpy(mPath.get(), iniPath);
+ mMaybeStatusCode = mozilla::Nothing();
+ } else {
+ mMaybeStatusCode = mozilla::Some(READ_STRINGS_MEM_ERROR);
+ }
+ if (section) {
+ mSection = mozilla::MakeUnique<char[]>(strlen(section) + 1);
+ strcpy(mSection.get(), section);
+ } else {
+ mSection.reset(nullptr);
+ }
+}
+
+bool IniReader::MaybeAddKey(const char* key, size_t& insertionIndex) {
+ if (!key || strlen(key) == 0 || mMaybeStatusCode.isSome()) {
+ return false;
+ }
+ auto existingKey = std::find_if(mKeys.begin(), mKeys.end(),
+ [=](mozilla::UniquePtr<char[]>& searchKey) {
+ return strcmp(key, searchKey.get()) == 0;
+ });
+ if (existingKey != mKeys.end()) {
+ // Key already in list
+ insertionIndex = std::distance(mKeys.begin(), existingKey);
+ return true;
+ }
+
+ // Key not already in list
+ insertionIndex = mKeys.size();
+ mKeys.emplace_back(mozilla::MakeUnique<char[]>(strlen(key) + 1));
+ strcpy(mKeys.back().get(), key);
+ return true;
+}
+
+void IniReader::AddKey(const char* key, mozilla::UniquePtr<char[]>* outputPtr) {
+ size_t insertionIndex;
+ if (!MaybeAddKey(key, insertionIndex)) {
+ return;
+ }
+
+ if (!outputPtr) {
+ return;
+ }
+
+ mNarrowOutputs.emplace_back();
+ mNarrowOutputs.back().keyIndex = insertionIndex;
+ mNarrowOutputs.back().outputPtr = outputPtr;
+}
+
+#ifdef XP_WIN
+void IniReader::AddKey(const char* key,
+ mozilla::UniquePtr<wchar_t[]>* outputPtr) {
+ size_t insertionIndex;
+ if (!MaybeAddKey(key, insertionIndex)) {
+ return;
+ }
+
+ if (!outputPtr) {
+ return;
+ }
+
+ mWideOutputs.emplace_back();
+ mWideOutputs.back().keyIndex = insertionIndex;
+ mWideOutputs.back().outputPtr = outputPtr;
+}
+
+// Returns true on success, false on failure.
+static bool ConvertToWide(const char* toConvert,
+ mozilla::UniquePtr<wchar_t[]>* result) {
+ int bufferSize = MultiByteToWideChar(CP_UTF8, 0, toConvert, -1, nullptr, 0);
+ *result = mozilla::MakeUnique<wchar_t[]>(bufferSize);
+ int charsWritten =
+ MultiByteToWideChar(CP_UTF8, 0, toConvert, -1, result->get(), bufferSize);
+ return charsWritten > 0;
+}
+#endif
+
+int IniReader::Read() {
+ if (mMaybeStatusCode.isSome()) {
+ return mMaybeStatusCode.value();
+ }
+
+ if (mKeys.size() < 1) {
+ // If there's nothing to read, just report success and return.
+ mMaybeStatusCode = mozilla::Some(OK);
+ return OK;
+ }
+
+ // First assemble the key list, which will be a character array of
+ // back-to-back null-terminated strings ending with a double null termination.
+ size_t keyListSize = 1; // For the final null
+ for (const auto& key : mKeys) {
+ keyListSize += strlen(key.get());
+ keyListSize += 1; // For the terminating null
+ }
+ mozilla::UniquePtr<char[]> keyList = mozilla::MakeUnique<char[]>(keyListSize);
+ char* keyListPtr = keyList.get();
+ for (const auto& key : mKeys) {
+ strcpy(keyListPtr, key.get());
+ // Point keyListPtr directly after the trailing null that strcpy wrote.
+ keyListPtr += strlen(key.get()) + 1;
+ }
+ *keyListPtr = '\0';
+
+ // Now make the array for the resulting data to be stored in
+ mozilla::UniquePtr<mozilla::UniquePtr<char[]>[]> results =
+ mozilla::MakeUnique<mozilla::UniquePtr<char[]>[]>(mKeys.size());
+
+ // Invoke ReadStrings to read the file and store the data for us
+ int statusCode = ReadStrings(mPath.get(), keyList.get(), mKeys.size(),
+ results.get(), mSection.get());
+ mMaybeStatusCode = mozilla::Some(statusCode);
+
+ if (statusCode != OK) {
+ return statusCode;
+ }
+
+ // Now populate the requested locations with the requested data.
+ for (const auto output : mNarrowOutputs) {
+ char* valueBuffer = results[output.keyIndex].get();
+ if (valueBuffer) {
+ *(output.outputPtr) =
+ mozilla::MakeUnique<char[]>(strlen(valueBuffer) + 1);
+ strcpy(output.outputPtr->get(), valueBuffer);
+ }
+ }
+
+#ifdef XP_WIN
+ for (const auto output : mWideOutputs) {
+ char* valueBuffer = results[output.keyIndex].get();
+ if (valueBuffer) {
+ if (!ConvertToWide(valueBuffer, output.outputPtr)) {
+ statusCode = STRING_CONVERSION_ERROR;
+ }
+ }
+ }
+#endif
+
+ return statusCode;
+}
diff --git a/toolkit/mozapps/update/common/readstrings.h b/toolkit/mozapps/update/common/readstrings.h
new file mode 100644
index 0000000000..9e0ebbefb5
--- /dev/null
+++ b/toolkit/mozapps/update/common/readstrings.h
@@ -0,0 +1,91 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef READSTRINGS_H__
+#define READSTRINGS_H__
+
+#include "mozilla/Maybe.h"
+#include "mozilla/UniquePtr.h"
+
+#include <vector>
+
+#ifdef XP_WIN
+# include <windows.h>
+typedef WCHAR NS_tchar;
+#else
+typedef char NS_tchar;
+#endif
+
+struct StringTable {
+ mozilla::UniquePtr<char[]> title;
+ mozilla::UniquePtr<char[]> info;
+};
+
+/**
+ * This function reads in localized strings from updater.ini
+ */
+int ReadStrings(const NS_tchar* path, StringTable* results);
+
+/**
+ * This function reads in localized strings corresponding to the keys from a
+ * given .ini
+ */
+int ReadStrings(const NS_tchar* path, const char* keyList,
+ unsigned int numStrings, mozilla::UniquePtr<char[]>* results,
+ const char* section = nullptr);
+
+/**
+ * This class is meant to be a slightly cleaner interface into the ReadStrings
+ * function.
+ */
+class IniReader {
+ public:
+ // IniReader must be initialized with the path of the INI file and a
+ // section to read from. If the section is null or not specified, the
+ // default section name ("Strings") will be used.
+ explicit IniReader(const NS_tchar* iniPath, const char* section = nullptr);
+
+ // Records a key that ought to be read from the INI file. When
+ // IniReader::Read() is invoked it will, if successful, store the value
+ // corresponding to the given key in the UniquePtr given.
+ // If IniReader::Read() has already been invoked, these functions do nothing.
+ // The given key must not be the empty string.
+ void AddKey(const char* key, mozilla::UniquePtr<char[]>* outputPtr);
+#ifdef XP_WIN
+ void AddKey(const char* key, mozilla::UniquePtr<wchar_t[]>* outputPtr);
+#endif
+ bool HasRead() { return mMaybeStatusCode.isSome(); }
+ // Performs the actual reading and assigns values to the requested locations.
+ // Returns the same possible values that `ReadStrings` returns.
+ // If this is called more than once, no action will be taken on subsequent
+ // calls, and the stored status code will be returned instead.
+ int Read();
+
+ private:
+ bool MaybeAddKey(const char* key, size_t& insertionIndex);
+
+ mozilla::UniquePtr<NS_tchar[]> mPath;
+ mozilla::UniquePtr<char[]> mSection;
+ std::vector<mozilla::UniquePtr<char[]>> mKeys;
+
+ template <class T>
+ struct ValueOutput {
+ size_t keyIndex;
+ T* outputPtr;
+ };
+
+ // Stores associations between keys and the buffers where their values will
+ // be stored.
+ std::vector<ValueOutput<mozilla::UniquePtr<char[]>>> mNarrowOutputs;
+#ifdef XP_WIN
+ std::vector<ValueOutput<mozilla::UniquePtr<wchar_t[]>>> mWideOutputs;
+#endif
+ // If we have attempted to read the INI, this will store the resulting
+ // status code.
+ mozilla::Maybe<int> mMaybeStatusCode;
+};
+
+#endif // READSTRINGS_H__
diff --git a/toolkit/mozapps/update/common/registrycertificates.cpp b/toolkit/mozapps/update/common/registrycertificates.cpp
new file mode 100644
index 0000000000..c5e6f9e973
--- /dev/null
+++ b/toolkit/mozapps/update/common/registrycertificates.cpp
@@ -0,0 +1,148 @@
+/* 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 <stdio.h>
+#include <stdlib.h>
+#include <windows.h>
+
+#include "registrycertificates.h"
+#include "pathhash.h"
+#include "updatecommon.h"
+#include "updatehelper.h"
+#define MAX_KEY_LENGTH 255
+
+/**
+ * Verifies if the file path matches any certificate stored in the registry.
+ *
+ * @param filePath The file path of the application to check if allowed.
+ * @param allowFallbackKeySkip when this is TRUE the fallback registry key will
+ * be used to skip the certificate check. This is the default since the
+ * fallback registry key is located under HKEY_LOCAL_MACHINE which can't be
+ * written to by a low integrity process.
+ * Note: the maintenance service binary can be used to perform this check for
+ * testing or troubleshooting.
+ * @return TRUE if the binary matches any of the allowed certificates.
+ */
+BOOL DoesBinaryMatchAllowedCertificates(LPCWSTR basePathForUpdate,
+ LPCWSTR filePath,
+ BOOL allowFallbackKeySkip) {
+ WCHAR maintenanceServiceKey[MAX_PATH + 1];
+ if (!CalculateRegistryPathFromFilePath(basePathForUpdate,
+ maintenanceServiceKey)) {
+ return FALSE;
+ }
+
+ // We use KEY_WOW64_64KEY to always force 64-bit view.
+ // The user may have both x86 and x64 applications installed
+ // which each register information. We need a consistent place
+ // to put those certificate attributes in and hence why we always
+ // force the non redirected registry under Wow6432Node.
+ // This flag is ignored on 32bit systems.
+ HKEY baseKey;
+ LONG retCode = RegOpenKeyExW(HKEY_LOCAL_MACHINE, maintenanceServiceKey, 0,
+ KEY_READ | KEY_WOW64_64KEY, &baseKey);
+ if (retCode != ERROR_SUCCESS) {
+ LOG_WARN(("Could not open key. (%d)", retCode));
+ // Our tests run with a different apply directory for each test.
+ // We use this registry key on our test machines to store the
+ // allowed name/issuers.
+ retCode = RegOpenKeyExW(HKEY_LOCAL_MACHINE, TEST_ONLY_FALLBACK_KEY_PATH, 0,
+ KEY_READ | KEY_WOW64_64KEY, &baseKey);
+ if (retCode != ERROR_SUCCESS) {
+ LOG_WARN(("Could not open fallback key. (%d)", retCode));
+ return FALSE;
+ } else if (allowFallbackKeySkip) {
+ LOG_WARN(
+ ("Fallback key present, skipping VerifyCertificateTrustForFile "
+ "check and the certificate attribute registry matching "
+ "check."));
+ RegCloseKey(baseKey);
+ return TRUE;
+ }
+ }
+
+ // Get the number of subkeys.
+ DWORD subkeyCount = 0;
+ retCode = RegQueryInfoKeyW(baseKey, nullptr, nullptr, nullptr, &subkeyCount,
+ nullptr, nullptr, nullptr, nullptr, nullptr,
+ nullptr, nullptr);
+ if (retCode != ERROR_SUCCESS) {
+ LOG_WARN(("Could not query info key. (%d)", retCode));
+ RegCloseKey(baseKey);
+ return FALSE;
+ }
+
+ // Enumerate the subkeys, each subkey represents an allowed certificate.
+ for (DWORD i = 0; i < subkeyCount; i++) {
+ WCHAR subkeyBuffer[MAX_KEY_LENGTH];
+ DWORD subkeyBufferCount = MAX_KEY_LENGTH;
+ retCode = RegEnumKeyExW(baseKey, i, subkeyBuffer, &subkeyBufferCount,
+ nullptr, nullptr, nullptr, nullptr);
+ if (retCode != ERROR_SUCCESS) {
+ LOG_WARN(("Could not enum certs. (%d)", retCode));
+ RegCloseKey(baseKey);
+ return FALSE;
+ }
+
+ // Open the subkey for the current certificate
+ HKEY subKey;
+ retCode = RegOpenKeyExW(baseKey, subkeyBuffer, 0,
+ KEY_READ | KEY_WOW64_64KEY, &subKey);
+ if (retCode != ERROR_SUCCESS) {
+ LOG_WARN(("Could not open subkey. (%d)", retCode));
+ continue; // Try the next subkey
+ }
+
+ const int MAX_CHAR_COUNT = 256;
+ DWORD valueBufSize = MAX_CHAR_COUNT * sizeof(WCHAR);
+ WCHAR name[MAX_CHAR_COUNT] = {L'\0'};
+ WCHAR issuer[MAX_CHAR_COUNT] = {L'\0'};
+
+ // Get the name from the registry
+ retCode = RegQueryValueExW(subKey, L"name", 0, nullptr, (LPBYTE)name,
+ &valueBufSize);
+ if (retCode != ERROR_SUCCESS) {
+ LOG_WARN(("Could not obtain name from registry. (%d)", retCode));
+ RegCloseKey(subKey);
+ continue; // Try the next subkey
+ }
+
+ // Get the issuer from the registry
+ valueBufSize = MAX_CHAR_COUNT * sizeof(WCHAR);
+ retCode = RegQueryValueExW(subKey, L"issuer", 0, nullptr, (LPBYTE)issuer,
+ &valueBufSize);
+ if (retCode != ERROR_SUCCESS) {
+ LOG_WARN(("Could not obtain issuer from registry. (%d)", retCode));
+ RegCloseKey(subKey);
+ continue; // Try the next subkey
+ }
+
+ CertificateCheckInfo allowedCertificate = {
+ name,
+ issuer,
+ };
+
+ retCode = CheckCertificateForPEFile(filePath, allowedCertificate);
+ if (retCode != ERROR_SUCCESS) {
+ LOG_WARN(("Error on certificate check. (%d)", retCode));
+ RegCloseKey(subKey);
+ continue; // Try the next subkey
+ }
+
+ retCode = VerifyCertificateTrustForFile(filePath);
+ if (retCode != ERROR_SUCCESS) {
+ LOG_WARN(("Error on certificate trust check. (%d)", retCode));
+ RegCloseKey(subKey);
+ continue; // Try the next subkey
+ }
+
+ RegCloseKey(baseKey);
+ // Raise the roof, we found a match!
+ return TRUE;
+ }
+
+ RegCloseKey(baseKey);
+ // No certificates match, :'(
+ return FALSE;
+}
diff --git a/toolkit/mozapps/update/common/registrycertificates.h b/toolkit/mozapps/update/common/registrycertificates.h
new file mode 100644
index 0000000000..9f68d1a8d9
--- /dev/null
+++ b/toolkit/mozapps/update/common/registrycertificates.h
@@ -0,0 +1,14 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef _REGISTRYCERTIFICATES_H_
+#define _REGISTRYCERTIFICATES_H_
+
+#include "certificatecheck.h"
+
+BOOL DoesBinaryMatchAllowedCertificates(LPCWSTR basePathForUpdate,
+ LPCWSTR filePath,
+ BOOL allowFallbackKeySkip = TRUE);
+
+#endif
diff --git a/toolkit/mozapps/update/common/uachelper.cpp b/toolkit/mozapps/update/common/uachelper.cpp
new file mode 100644
index 0000000000..d7a280034e
--- /dev/null
+++ b/toolkit/mozapps/update/common/uachelper.cpp
@@ -0,0 +1,186 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include <windows.h>
+#include <wtsapi32.h>
+#include "uachelper.h"
+#include "updatecommon.h"
+
+// See the MSDN documentation with title: Privilege Constants
+// At the time of this writing, this documentation is located at:
+// http://msdn.microsoft.com/en-us/library/windows/desktop/bb530716%28v=vs.85%29.aspx
+LPCTSTR UACHelper::PrivsToDisable[] = {
+ SE_ASSIGNPRIMARYTOKEN_NAME, SE_AUDIT_NAME, SE_BACKUP_NAME,
+ // CreateProcess will succeed but the app will fail to launch on some WinXP
+ // machines if SE_CHANGE_NOTIFY_NAME is disabled. In particular this
+ // happens for limited user accounts on those machines. The define is kept
+ // here as a reminder that it should never be re-added. This permission is
+ // for directory watching but also from MSDN: "This privilege also causes
+ // the system to skip all traversal access checks." SE_CHANGE_NOTIFY_NAME,
+ SE_CREATE_GLOBAL_NAME, SE_CREATE_PAGEFILE_NAME, SE_CREATE_PERMANENT_NAME,
+ SE_CREATE_SYMBOLIC_LINK_NAME, SE_CREATE_TOKEN_NAME, SE_DEBUG_NAME,
+ SE_ENABLE_DELEGATION_NAME, SE_IMPERSONATE_NAME, SE_INC_BASE_PRIORITY_NAME,
+ SE_INCREASE_QUOTA_NAME, SE_INC_WORKING_SET_NAME, SE_LOAD_DRIVER_NAME,
+ SE_LOCK_MEMORY_NAME, SE_MACHINE_ACCOUNT_NAME, SE_MANAGE_VOLUME_NAME,
+ SE_PROF_SINGLE_PROCESS_NAME, SE_RELABEL_NAME, SE_REMOTE_SHUTDOWN_NAME,
+ SE_RESTORE_NAME, SE_SECURITY_NAME, SE_SHUTDOWN_NAME, SE_SYNC_AGENT_NAME,
+ SE_SYSTEM_ENVIRONMENT_NAME, SE_SYSTEM_PROFILE_NAME, SE_SYSTEMTIME_NAME,
+ SE_TAKE_OWNERSHIP_NAME, SE_TCB_NAME, SE_TIME_ZONE_NAME,
+ SE_TRUSTED_CREDMAN_ACCESS_NAME, SE_UNDOCK_NAME, SE_UNSOLICITED_INPUT_NAME};
+
+/**
+ * Opens a user token for the given session ID
+ *
+ * @param sessionID The session ID for the token to obtain
+ * @return A handle to the token to obtain which will be primary if enough
+ * permissions exist. Caller should close the handle.
+ */
+HANDLE
+UACHelper::OpenUserToken(DWORD sessionID) {
+ HMODULE module = LoadLibraryW(L"wtsapi32.dll");
+ HANDLE token = nullptr;
+ decltype(WTSQueryUserToken)* wtsQueryUserToken =
+ (decltype(WTSQueryUserToken)*)GetProcAddress(module, "WTSQueryUserToken");
+ if (wtsQueryUserToken) {
+ wtsQueryUserToken(sessionID, &token);
+ }
+ FreeLibrary(module);
+ return token;
+}
+
+/**
+ * Opens a linked token for the specified token.
+ *
+ * @param token The token to get the linked token from
+ * @return A linked token or nullptr if one does not exist.
+ * Caller should close the handle.
+ */
+HANDLE
+UACHelper::OpenLinkedToken(HANDLE token) {
+ // Magic below...
+ // UAC creates 2 tokens. One is the restricted token which we have.
+ // the other is the UAC elevated one. Since we are running as a service
+ // as the system account we have access to both.
+ TOKEN_LINKED_TOKEN tlt;
+ HANDLE hNewLinkedToken = nullptr;
+ DWORD len;
+ if (GetTokenInformation(token, (TOKEN_INFORMATION_CLASS)TokenLinkedToken,
+ &tlt, sizeof(TOKEN_LINKED_TOKEN), &len)) {
+ token = tlt.LinkedToken;
+ hNewLinkedToken = token;
+ }
+ return hNewLinkedToken;
+}
+
+/**
+ * Enables or disables a privilege for the specified token.
+ *
+ * @param token The token to adjust the privilege on.
+ * @param priv The privilege to adjust.
+ * @param enable Whether to enable or disable it
+ * @return TRUE if the token was adjusted to the specified value.
+ */
+BOOL UACHelper::SetPrivilege(HANDLE token, LPCTSTR priv, BOOL enable) {
+ LUID luidOfPriv;
+ if (!LookupPrivilegeValue(nullptr, priv, &luidOfPriv)) {
+ return FALSE;
+ }
+
+ TOKEN_PRIVILEGES tokenPriv;
+ tokenPriv.PrivilegeCount = 1;
+ tokenPriv.Privileges[0].Luid = luidOfPriv;
+ tokenPriv.Privileges[0].Attributes = enable ? SE_PRIVILEGE_ENABLED : 0;
+
+ SetLastError(ERROR_SUCCESS);
+ if (!AdjustTokenPrivileges(token, false, &tokenPriv, sizeof(tokenPriv),
+ nullptr, nullptr)) {
+ return FALSE;
+ }
+
+ return GetLastError() == ERROR_SUCCESS;
+}
+
+/**
+ * For each privilege that is specified, an attempt will be made to
+ * drop the privilege.
+ *
+ * @param token The token to adjust the privilege on.
+ * Pass nullptr for current token.
+ * @param unneededPrivs An array of unneeded privileges.
+ * @param count The size of the array
+ * @return TRUE if there were no errors
+ */
+BOOL UACHelper::DisableUnneededPrivileges(HANDLE token, LPCTSTR* unneededPrivs,
+ size_t count) {
+ HANDLE obtainedToken = nullptr;
+ if (!token) {
+ // Note: This handle is a pseudo-handle and need not be closed
+ HANDLE process = GetCurrentProcess();
+ if (!OpenProcessToken(process, TOKEN_ALL_ACCESS_P, &obtainedToken)) {
+ LOG_WARN(
+ ("Could not obtain token for current process, no "
+ "privileges changed. (%d)",
+ GetLastError()));
+ return FALSE;
+ }
+ token = obtainedToken;
+ }
+
+ BOOL result = TRUE;
+ for (size_t i = 0; i < count; i++) {
+ if (SetPrivilege(token, unneededPrivs[i], FALSE)) {
+ LOG(("Disabled unneeded token privilege: %s.", unneededPrivs[i]));
+ } else {
+ LOG(("Could not disable token privilege value: %s. (%d)",
+ unneededPrivs[i], GetLastError()));
+ result = FALSE;
+ }
+ }
+
+ if (obtainedToken) {
+ CloseHandle(obtainedToken);
+ }
+ return result;
+}
+
+/**
+ * Disables privileges for the specified token.
+ * The privileges to disable are in PrivsToDisable.
+ * In the future there could be new privs and we are not sure if we should
+ * explicitly disable these or not.
+ *
+ * @param token The token to drop the privilege on.
+ * Pass nullptr for current token.
+ * @return TRUE if there were no errors
+ */
+BOOL UACHelper::DisablePrivileges(HANDLE token) {
+ static const size_t PrivsToDisableSize =
+ sizeof(UACHelper::PrivsToDisable) / sizeof(UACHelper::PrivsToDisable[0]);
+
+ return DisableUnneededPrivileges(token, UACHelper::PrivsToDisable,
+ PrivsToDisableSize);
+}
+
+/**
+ * Check if the current user can elevate.
+ *
+ * @return true if the user can elevate.
+ * false otherwise.
+ */
+bool UACHelper::CanUserElevate() {
+ HANDLE token;
+ if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &token)) {
+ return false;
+ }
+
+ TOKEN_ELEVATION_TYPE elevationType;
+ DWORD len;
+ bool canElevate =
+ GetTokenInformation(token, TokenElevationType, &elevationType,
+ sizeof(elevationType), &len) &&
+ (elevationType == TokenElevationTypeLimited);
+ CloseHandle(token);
+
+ return canElevate;
+}
diff --git a/toolkit/mozapps/update/common/uachelper.h b/toolkit/mozapps/update/common/uachelper.h
new file mode 100644
index 0000000000..e9f5215787
--- /dev/null
+++ b/toolkit/mozapps/update/common/uachelper.h
@@ -0,0 +1,22 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef _UACHELPER_H_
+#define _UACHELPER_H_
+
+class UACHelper {
+ public:
+ static HANDLE OpenUserToken(DWORD sessionID);
+ static HANDLE OpenLinkedToken(HANDLE token);
+ static BOOL DisablePrivileges(HANDLE token);
+ static bool CanUserElevate();
+
+ private:
+ static BOOL SetPrivilege(HANDLE token, LPCTSTR privs, BOOL enable);
+ static BOOL DisableUnneededPrivileges(HANDLE token, LPCTSTR* unneededPrivs,
+ size_t count);
+ static LPCTSTR PrivsToDisable[];
+};
+
+#endif
diff --git a/toolkit/mozapps/update/common/updatecommon.cpp b/toolkit/mozapps/update/common/updatecommon.cpp
new file mode 100644
index 0000000000..e270d71b7e
--- /dev/null
+++ b/toolkit/mozapps/update/common/updatecommon.cpp
@@ -0,0 +1,464 @@
+/* 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/. */
+
+#if defined(XP_WIN)
+# include <windows.h>
+# include <winioctl.h> // for FSCTL_GET_REPARSE_POINT
+# include <shlobj.h>
+# ifndef RRF_SUBKEY_WOW6464KEY
+# define RRF_SUBKEY_WOW6464KEY 0x00010000
+# endif
+#endif
+
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+#include <stdarg.h>
+
+#include "updatecommon.h"
+#ifdef XP_WIN
+# include "updatehelper.h"
+# include "nsWindowsHelpers.h"
+# include "mozilla/UniquePtr.h"
+# include "mozilla/WinHeaderOnlyUtils.h"
+
+// This struct isn't in any SDK header, so this definition was copied from:
+// https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/content/ntifs/ns-ntifs-_reparse_data_buffer
+typedef struct _REPARSE_DATA_BUFFER {
+ ULONG ReparseTag;
+ USHORT ReparseDataLength;
+ USHORT Reserved;
+ union {
+ struct {
+ USHORT SubstituteNameOffset;
+ USHORT SubstituteNameLength;
+ USHORT PrintNameOffset;
+ USHORT PrintNameLength;
+ ULONG Flags;
+ WCHAR PathBuffer[1];
+ } SymbolicLinkReparseBuffer;
+ struct {
+ USHORT SubstituteNameOffset;
+ USHORT SubstituteNameLength;
+ USHORT PrintNameOffset;
+ USHORT PrintNameLength;
+ WCHAR PathBuffer[1];
+ } MountPointReparseBuffer;
+ struct {
+ UCHAR DataBuffer[1];
+ } GenericReparseBuffer;
+ } DUMMYUNIONNAME;
+} REPARSE_DATA_BUFFER, *PREPARSE_DATA_BUFFER;
+#endif
+
+UpdateLog::UpdateLog() : logFP(nullptr) {}
+
+void UpdateLog::Init(NS_tchar* logFilePath) {
+ if (logFP) {
+ return;
+ }
+
+ // When the path is over the length limit disable logging by not opening the
+ // file and not setting logFP.
+ int dstFilePathLen = NS_tstrlen(logFilePath);
+ if (dstFilePathLen > 0 && dstFilePathLen < MAXPATHLEN - 1) {
+ NS_tstrncpy(mDstFilePath, logFilePath, MAXPATHLEN);
+#if defined(XP_WIN) || defined(XP_MACOSX)
+ logFP = NS_tfopen(mDstFilePath, NS_T("w"));
+#else
+ // On platforms that have an updates directory in the installation directory
+ // (e.g. platforms other than Windows and Mac) the update log is written to
+ // a temporary file and then to the update log file. This is needed since
+ // the installation directory is moved during a replace request. This can be
+ // removed when the platform's updates directory is located outside of the
+ // installation directory.
+ logFP = tmpfile();
+#endif
+ }
+}
+
+void UpdateLog::Finish() {
+ if (!logFP) {
+ return;
+ }
+
+#if !defined(XP_WIN) && !defined(XP_MACOSX)
+ const int blockSize = 1024;
+ char buffer[blockSize];
+ fflush(logFP);
+ rewind(logFP);
+
+ FILE* updateLogFP = NS_tfopen(mDstFilePath, NS_T("wb+"));
+ while (!feof(logFP)) {
+ size_t read = fread(buffer, 1, blockSize, logFP);
+ if (ferror(logFP)) {
+ fclose(logFP);
+ logFP = nullptr;
+ fclose(updateLogFP);
+ updateLogFP = nullptr;
+ return;
+ }
+
+ size_t written = 0;
+
+ while (written < read) {
+ size_t chunkWritten = fwrite(buffer, 1, read - written, updateLogFP);
+ if (chunkWritten <= 0) {
+ fclose(logFP);
+ logFP = nullptr;
+ fclose(updateLogFP);
+ updateLogFP = nullptr;
+ return;
+ }
+
+ written += chunkWritten;
+ }
+ }
+ fclose(updateLogFP);
+ updateLogFP = nullptr;
+#endif
+
+ fclose(logFP);
+ logFP = nullptr;
+}
+
+void UpdateLog::Flush() {
+ if (!logFP) {
+ return;
+ }
+
+ fflush(logFP);
+}
+
+void UpdateLog::Printf(const char* fmt, ...) {
+ if (!logFP) {
+ return;
+ }
+
+ va_list ap;
+ va_start(ap, fmt);
+ vfprintf(logFP, fmt, ap);
+ fprintf(logFP, "\n");
+ va_end(ap);
+#if defined(XP_WIN) && defined(MOZ_DEBUG)
+ // When the updater crashes on Windows the log file won't be flushed and this
+ // can make it easier to debug what is going on.
+ fflush(logFP);
+#endif
+}
+
+void UpdateLog::WarnPrintf(const char* fmt, ...) {
+ if (!logFP) {
+ return;
+ }
+
+ va_list ap;
+ va_start(ap, fmt);
+ fprintf(logFP, "*** Warning: ");
+ vfprintf(logFP, fmt, ap);
+ fprintf(logFP, "***\n");
+ va_end(ap);
+#if defined(XP_WIN) && defined(MOZ_DEBUG)
+ // When the updater crashes on Windows the log file won't be flushed and this
+ // can make it easier to debug what is going on.
+ fflush(logFP);
+#endif
+}
+
+#ifdef XP_WIN
+/**
+ * Determine if a path contains symlinks or junctions to disallowed locations
+ *
+ * @param fullPath The full path to check.
+ * @return true if the path contains invalid links or on errors,
+ * false if the check passes and the path can be used
+ */
+bool PathContainsInvalidLinks(wchar_t* const fullPath) {
+ wchar_t pathCopy[MAXPATHLEN + 1] = L"";
+ wcsncpy(pathCopy, fullPath, MAXPATHLEN);
+ wchar_t* remainingPath = nullptr;
+ wchar_t* nextToken = wcstok_s(pathCopy, L"\\", &remainingPath);
+ wchar_t* partialPath = nextToken;
+
+ while (nextToken) {
+ if ((GetFileAttributesW(partialPath) & FILE_ATTRIBUTE_REPARSE_POINT) != 0) {
+ nsAutoHandle h(CreateFileW(
+ partialPath, 0,
+ FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr,
+ OPEN_EXISTING,
+ FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT, nullptr));
+ if (h == INVALID_HANDLE_VALUE) {
+ if (GetLastError() == ERROR_FILE_NOT_FOUND) {
+ // The path can't be an invalid link if it doesn't exist.
+ return false;
+ } else {
+ return true;
+ }
+ }
+
+ mozilla::UniquePtr<UINT8[]> byteBuffer =
+ mozilla::MakeUnique<UINT8[]>(MAXIMUM_REPARSE_DATA_BUFFER_SIZE);
+ if (!byteBuffer) {
+ return true;
+ }
+ ZeroMemory(byteBuffer.get(), MAXIMUM_REPARSE_DATA_BUFFER_SIZE);
+ REPARSE_DATA_BUFFER* buffer = (REPARSE_DATA_BUFFER*)byteBuffer.get();
+ DWORD bytes = 0;
+ if (!DeviceIoControl(h, FSCTL_GET_REPARSE_POINT, nullptr, 0, buffer,
+ MAXIMUM_REPARSE_DATA_BUFFER_SIZE, &bytes, nullptr)) {
+ return true;
+ }
+
+ wchar_t* reparseTarget = nullptr;
+ switch (buffer->ReparseTag) {
+ case IO_REPARSE_TAG_MOUNT_POINT:
+ reparseTarget =
+ buffer->MountPointReparseBuffer.PathBuffer +
+ (buffer->MountPointReparseBuffer.SubstituteNameOffset /
+ sizeof(wchar_t));
+ if (buffer->MountPointReparseBuffer.SubstituteNameLength <
+ ARRAYSIZE(L"\\??\\")) {
+ return false;
+ }
+ break;
+ case IO_REPARSE_TAG_SYMLINK:
+ reparseTarget =
+ buffer->SymbolicLinkReparseBuffer.PathBuffer +
+ (buffer->SymbolicLinkReparseBuffer.SubstituteNameOffset /
+ sizeof(wchar_t));
+ if (buffer->SymbolicLinkReparseBuffer.SubstituteNameLength <
+ ARRAYSIZE(L"\\??\\")) {
+ return false;
+ }
+ break;
+ default:
+ return true;
+ break;
+ }
+
+ if (!reparseTarget) {
+ return false;
+ }
+ if (wcsncmp(reparseTarget, L"\\??\\", ARRAYSIZE(L"\\??\\") - 1) != 0) {
+ return true;
+ }
+ }
+
+ nextToken = wcstok_s(nullptr, L"\\", &remainingPath);
+ PathAppendW(partialPath, nextToken);
+ }
+
+ return false;
+}
+
+/**
+ * Determine if a path is located within Program Files, either native or x86
+ *
+ * @param fullPath The full path to check.
+ * @return true if fullPath begins with either Program Files directory,
+ * false if it does not or if an error is encountered
+ */
+bool IsProgramFilesPath(NS_tchar* fullPath) {
+ // Make sure we don't try to compare against a short path.
+ DWORD longInstallPathChars = GetLongPathNameW(fullPath, nullptr, 0);
+ if (longInstallPathChars == 0) {
+ return false;
+ }
+ mozilla::UniquePtr<wchar_t[]> longInstallPath =
+ mozilla::MakeUnique<wchar_t[]>(longInstallPathChars);
+ if (!longInstallPath || !GetLongPathNameW(fullPath, longInstallPath.get(),
+ longInstallPathChars)) {
+ return false;
+ }
+
+ // First check for Program Files (x86).
+ {
+ PWSTR programFiles32PathRaw = nullptr;
+ // FOLDERID_ProgramFilesX86 gets native Program Files directory on a 32-bit
+ // OS or the (x86) directory on a 64-bit OS regardless of this binary's
+ // bitness.
+ if (FAILED(SHGetKnownFolderPath(FOLDERID_ProgramFilesX86, 0, nullptr,
+ &programFiles32PathRaw))) {
+ // That call should never fail on any supported OS version.
+ return false;
+ }
+ mozilla::UniquePtr<wchar_t, mozilla::CoTaskMemFreeDeleter>
+ programFiles32Path(programFiles32PathRaw);
+ // We need this path to have a trailing slash so our prefix test doesn't
+ // match on a different folder which happens to have a name beginning with
+ // the prefix we're looking for but then also more characters after that.
+ size_t length = wcslen(programFiles32Path.get());
+ if (length == 0) {
+ return false;
+ }
+ if (programFiles32Path.get()[length - 1] == L'\\') {
+ if (wcsnicmp(longInstallPath.get(), programFiles32Path.get(), length) ==
+ 0) {
+ return true;
+ }
+ } else {
+ // Allocate space for a copy of the string along with a terminator and one
+ // extra character for the trailing backslash.
+ length += 1;
+ mozilla::UniquePtr<wchar_t[]> programFiles32PathWithSlash =
+ mozilla::MakeUnique<wchar_t[]>(length + 1);
+ if (!programFiles32PathWithSlash) {
+ return false;
+ }
+
+ NS_tsnprintf(programFiles32PathWithSlash.get(), length + 1, NS_T("%s\\"),
+ programFiles32Path.get());
+
+ if (wcsnicmp(longInstallPath.get(), programFiles32PathWithSlash.get(),
+ length) == 0) {
+ return true;
+ }
+ }
+ }
+
+ // If we didn't find (x86), check for the native Program Files.
+ {
+ // In case we're a 32-bit binary on 64-bit Windows, we now have a problem
+ // getting the right "native" Program Files path, which is that there is no
+ // FOLDERID_* value that returns that path. So we always read that one out
+ // of its canonical registry location instead. If we're on a 32-bit OS, this
+ // will be the same path that we just checked. First get the buffer size to
+ // allocate for the path.
+ DWORD length = 0;
+ if (RegGetValueW(HKEY_LOCAL_MACHINE,
+ L"Software\\Microsoft\\Windows\\CurrentVersion",
+ L"ProgramFilesDir", RRF_RT_REG_SZ | RRF_SUBKEY_WOW6464KEY,
+ nullptr, nullptr, &length) != ERROR_SUCCESS) {
+ return false;
+ }
+ // RegGetValue returns the length including the terminator, but it's in
+ // bytes, so convert that to characters.
+ DWORD lengthChars = (length / sizeof(wchar_t));
+ if (lengthChars <= 1) {
+ return false;
+ }
+ mozilla::UniquePtr<wchar_t[]> programFilesNativePath =
+ mozilla::MakeUnique<wchar_t[]>(lengthChars);
+ if (!programFilesNativePath) {
+ return false;
+ }
+
+ // Now actually read the value.
+ if (RegGetValueW(
+ HKEY_LOCAL_MACHINE, L"Software\\Microsoft\\Windows\\CurrentVersion",
+ L"ProgramFilesDir", RRF_RT_REG_SZ | RRF_SUBKEY_WOW6464KEY, nullptr,
+ programFilesNativePath.get(), &length) != ERROR_SUCCESS) {
+ return false;
+ }
+ size_t nativePathStrLen =
+ wcsnlen_s(programFilesNativePath.get(), lengthChars);
+ if (nativePathStrLen == 0) {
+ return false;
+ }
+
+ // As before, append a backslash if there isn't one already.
+ if (programFilesNativePath.get()[nativePathStrLen - 1] == L'\\') {
+ if (wcsnicmp(longInstallPath.get(), programFilesNativePath.get(),
+ nativePathStrLen) == 0) {
+ return true;
+ }
+ } else {
+ // Allocate space for a copy of the string along with a terminator and one
+ // extra character for the trailing backslash.
+ nativePathStrLen += 1;
+ mozilla::UniquePtr<wchar_t[]> programFilesNativePathWithSlash =
+ mozilla::MakeUnique<wchar_t[]>(nativePathStrLen + 1);
+ if (!programFilesNativePathWithSlash) {
+ return false;
+ }
+
+ NS_tsnprintf(programFilesNativePathWithSlash.get(), nativePathStrLen + 1,
+ NS_T("%s\\"), programFilesNativePath.get());
+
+ if (wcsnicmp(longInstallPath.get(), programFilesNativePathWithSlash.get(),
+ nativePathStrLen) == 0) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
+#endif
+
+/**
+ * Performs checks of a full path for validity for this application.
+ *
+ * @param origFullPath
+ * The full path to check.
+ * @return true if the path is valid for this application and false otherwise.
+ */
+bool IsValidFullPath(NS_tchar* origFullPath) {
+ // Subtract 1 from MAXPATHLEN for null termination.
+ if (NS_tstrlen(origFullPath) > MAXPATHLEN - 1) {
+ // The path is longer than acceptable for this application.
+ return false;
+ }
+
+#ifdef XP_WIN
+ NS_tchar testPath[MAXPATHLEN] = {NS_T('\0')};
+ // GetFullPathNameW will replace / with \ which PathCanonicalizeW requires.
+ if (GetFullPathNameW(origFullPath, MAXPATHLEN, testPath, nullptr) == 0) {
+ // Unable to get the full name for the path (e.g. invalid path).
+ return false;
+ }
+
+ NS_tchar canonicalPath[MAXPATHLEN] = {NS_T('\0')};
+ if (!PathCanonicalizeW(canonicalPath, testPath)) {
+ // Path could not be canonicalized (e.g. invalid path).
+ return false;
+ }
+
+ // Check if the path passed in resolves to a differerent path.
+ if (NS_tstricmp(origFullPath, canonicalPath) != 0) {
+ // Case insensitive string comparison between the supplied path and the
+ // canonical path are not equal. This will prevent directory traversal and
+ // the use of / in paths since they are converted to \.
+ return false;
+ }
+
+ NS_tstrncpy(testPath, origFullPath, MAXPATHLEN);
+ if (!PathStripToRootW(testPath)) {
+ // It should always be possible to strip a valid path to its root.
+ return false;
+ }
+
+ if (origFullPath[0] == NS_T('\\')) {
+ // Only allow UNC server share paths.
+ if (!PathIsUNCServerShareW(testPath)) {
+ return false;
+ }
+ }
+
+ if (PathContainsInvalidLinks(canonicalPath)) {
+ return false;
+ }
+#else
+ // Only allow full paths.
+ if (origFullPath[0] != NS_T('/')) {
+ return false;
+ }
+
+ // The path must not traverse directories
+ if (NS_tstrstr(origFullPath, NS_T("/../")) != nullptr) {
+ return false;
+ }
+
+ // The path shall not have a path traversal suffix
+ const NS_tchar invalidSuffix[] = NS_T("/..");
+ size_t pathLen = NS_tstrlen(origFullPath);
+ size_t invalidSuffixLen = NS_tstrlen(invalidSuffix);
+ if (invalidSuffixLen <= pathLen &&
+ NS_tstrncmp(origFullPath + pathLen - invalidSuffixLen, invalidSuffix,
+ invalidSuffixLen) == 0) {
+ return false;
+ }
+#endif
+ return true;
+}
diff --git a/toolkit/mozapps/update/common/updatecommon.h b/toolkit/mozapps/update/common/updatecommon.h
new file mode 100644
index 0000000000..73fdc3a559
--- /dev/null
+++ b/toolkit/mozapps/update/common/updatecommon.h
@@ -0,0 +1,42 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef UPDATECOMMON_H
+#define UPDATECOMMON_H
+
+#include "updatedefines.h"
+#include <stdio.h>
+#include "mozilla/Attributes.h"
+
+class UpdateLog {
+ public:
+ static UpdateLog& GetPrimaryLog() {
+ static UpdateLog primaryLog;
+ return primaryLog;
+ }
+
+ void Init(NS_tchar* logFilePath);
+ void Finish();
+ void Flush();
+ void Printf(const char* fmt, ...) MOZ_FORMAT_PRINTF(2, 3);
+ void WarnPrintf(const char* fmt, ...) MOZ_FORMAT_PRINTF(2, 3);
+
+ ~UpdateLog() { Finish(); }
+
+ protected:
+ UpdateLog();
+ FILE* logFP;
+ NS_tchar mDstFilePath[MAXPATHLEN];
+};
+
+bool IsValidFullPath(NS_tchar* fullPath);
+bool IsProgramFilesPath(NS_tchar* fullPath);
+
+#define LOG_WARN(args) UpdateLog::GetPrimaryLog().WarnPrintf args
+#define LOG(args) UpdateLog::GetPrimaryLog().Printf args
+#define LogInit(FILEPATH_) UpdateLog::GetPrimaryLog().Init(FILEPATH_)
+#define LogFinish() UpdateLog::GetPrimaryLog().Finish()
+#define LogFlush() UpdateLog::GetPrimaryLog().Flush()
+
+#endif
diff --git a/toolkit/mozapps/update/common/updatedefines.h b/toolkit/mozapps/update/common/updatedefines.h
new file mode 100644
index 0000000000..f716e10f66
--- /dev/null
+++ b/toolkit/mozapps/update/common/updatedefines.h
@@ -0,0 +1,164 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef UPDATEDEFINES_H
+#define UPDATEDEFINES_H
+
+#include <stdio.h>
+#include <stdarg.h>
+#include "readstrings.h"
+
+#if defined(XP_WIN)
+# include <windows.h>
+# include <shlwapi.h>
+# include <direct.h>
+# include <io.h>
+
+# ifndef F_OK
+# define F_OK 00
+# endif
+# ifndef W_OK
+# define W_OK 02
+# endif
+# ifndef R_OK
+# define R_OK 04
+# endif
+# define S_ISDIR(s) (((s)&_S_IFMT) == _S_IFDIR)
+# define S_ISREG(s) (((s)&_S_IFMT) == _S_IFREG)
+
+# define access _access
+
+# define putenv _putenv
+# if defined(_MSC_VER) && _MSC_VER < 1900
+# define stat _stat
+# endif
+# define DELETE_DIR L"tobedeleted"
+# define CALLBACK_BACKUP_EXT L".moz-callback"
+
+# define LOG_S "%S"
+# define NS_CONCAT(x, y) x##y
+// The extra layer of indirection here allows this macro to be passed macros
+# define NS_T(str) NS_CONCAT(L, str)
+# define NS_SLASH NS_T('\\')
+static inline int mywcsprintf(WCHAR* dest, size_t count, const WCHAR* fmt,
+ ...) {
+ size_t _count = count - 1;
+ va_list varargs;
+ va_start(varargs, fmt);
+ int result = _vsnwprintf(dest, count - 1, fmt, varargs);
+ va_end(varargs);
+ dest[_count] = L'\0';
+ return result;
+}
+# define NS_tsnprintf mywcsprintf
+# define NS_taccess _waccess
+# define NS_tatoi _wtoi64
+# define NS_tchdir _wchdir
+# define NS_tchmod _wchmod
+# define NS_tfopen _wfopen
+# define NS_tmkdir(path, perms) _wmkdir(path)
+# define NS_tpid __int64
+# define NS_tremove _wremove
+// _wrename is used to avoid the link tracking service.
+# define NS_trename _wrename
+# define NS_trmdir _wrmdir
+# define NS_tstat _wstat
+# define NS_tlstat _wstat // No symlinks on Windows
+# define NS_tstat_t _stat
+# define NS_tstrcat wcscat
+# define NS_tstrcmp wcscmp
+# define NS_tstricmp wcsicmp
+# define NS_tstrncmp wcsncmp
+# define NS_tstrcpy wcscpy
+# define NS_tstrncpy wcsncpy
+# define NS_tstrlen wcslen
+# define NS_tstrchr wcschr
+# define NS_tstrrchr wcsrchr
+# define NS_tstrstr wcsstr
+# include "updateutils_win.h"
+# define NS_tDIR DIR
+# define NS_tdirent dirent
+# define NS_topendir opendir
+# define NS_tclosedir closedir
+# define NS_treaddir readdir
+#else
+# include <sys/wait.h>
+# include <unistd.h>
+
+# ifdef HAVE_FTS_H
+# include <fts.h>
+# else
+# include <sys/stat.h>
+# endif
+# include <dirent.h>
+
+# ifdef XP_MACOSX
+# include <sys/time.h>
+# endif
+
+# define LOG_S "%s"
+# define NS_T(str) str
+# define NS_SLASH NS_T('/')
+# define NS_tsnprintf snprintf
+# define NS_taccess access
+# define NS_tatoi atoi
+# define NS_tchdir chdir
+# define NS_tchmod chmod
+# define NS_tfopen fopen
+# define NS_tmkdir mkdir
+# define NS_tpid int
+# define NS_tremove remove
+# define NS_trename rename
+# define NS_trmdir rmdir
+# define NS_tstat stat
+# define NS_tstat_t stat
+# define NS_tlstat lstat
+# define NS_tstrcat strcat
+# define NS_tstrcmp strcmp
+# define NS_tstricmp strcasecmp
+# define NS_tstrncmp strncmp
+# define NS_tstrcpy strcpy
+# define NS_tstrncpy strncpy
+# define NS_tstrlen strlen
+# define NS_tstrrchr strrchr
+# define NS_tstrstr strstr
+# define NS_tDIR DIR
+# define NS_tdirent dirent
+# define NS_topendir opendir
+# define NS_tclosedir closedir
+# define NS_treaddir readdir
+#endif
+
+#define BACKUP_EXT NS_T(".moz-backup")
+
+#ifndef MAXPATHLEN
+# ifdef PATH_MAX
+# define MAXPATHLEN PATH_MAX
+# elif defined(MAX_PATH)
+# define MAXPATHLEN MAX_PATH
+# elif defined(_MAX_PATH)
+# define MAXPATHLEN _MAX_PATH
+# elif defined(CCHMAXPATH)
+# define MAXPATHLEN CCHMAXPATH
+# else
+# define MAXPATHLEN 1024
+# endif
+#endif
+
+static inline bool NS_tvsnprintf(NS_tchar* dest, size_t count,
+ const NS_tchar* fmt, ...) {
+ va_list varargs;
+ va_start(varargs, fmt);
+#if defined(XP_WIN)
+ int result = _vsnwprintf(dest, count, fmt, varargs);
+#else
+ int result = vsnprintf(dest, count, fmt, varargs);
+#endif
+ va_end(varargs);
+ // The size_t cast of result is safe because result can only be positive after
+ // the first check.
+ return result >= 0 && (size_t)result < count;
+}
+
+#endif
diff --git a/toolkit/mozapps/update/common/updatehelper.cpp b/toolkit/mozapps/update/common/updatehelper.cpp
new file mode 100644
index 0000000000..b094d9eb75
--- /dev/null
+++ b/toolkit/mozapps/update/common/updatehelper.cpp
@@ -0,0 +1,763 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include <windows.h>
+
+// Needed for CreateToolhelp32Snapshot
+#include <tlhelp32.h>
+
+#include <stdio.h>
+#include <direct.h>
+#include "shlobj.h"
+
+// Needed for PathAppendW
+#include <shlwapi.h>
+
+#include "updatehelper.h"
+#include "updateutils_win.h"
+
+#ifdef MOZ_MAINTENANCE_SERVICE
+# include "mozilla/UniquePtr.h"
+# include "pathhash.h"
+# include "registrycertificates.h"
+# include "uachelper.h"
+
+using mozilla::MakeUnique;
+using mozilla::UniquePtr;
+#endif
+
+BOOL PathGetSiblingFilePath(LPWSTR destinationBuffer, LPCWSTR siblingFilePath,
+ LPCWSTR newFileName);
+
+/**
+ * Obtains the path of a file in the same directory as the specified file.
+ *
+ * @param destinationBuffer A buffer of size MAX_PATH + 1 to store the result.
+ * @param siblingFilePath The path of another file in the same directory
+ * @param newFileName The filename of another file in the same directory
+ * @return TRUE if successful
+ */
+BOOL PathGetSiblingFilePath(LPWSTR destinationBuffer, LPCWSTR siblingFilePath,
+ LPCWSTR newFileName) {
+ if (wcslen(siblingFilePath) > MAX_PATH) {
+ return FALSE;
+ }
+
+ wcsncpy(destinationBuffer, siblingFilePath, MAX_PATH + 1);
+ if (!PathRemoveFileSpecW(destinationBuffer)) {
+ return FALSE;
+ }
+
+ return PathAppendSafe(destinationBuffer, newFileName);
+}
+
+/**
+ * Obtains the path of the secure directory used to write the status and log
+ * files for updates applied with an elevated updater or an updater that is
+ * launched using the maintenance service.
+ *
+ * Example
+ * Destination buffer value:
+ * C:\Program Files (x86)\Mozilla Maintenance Service\UpdateLogs
+ *
+ * @param outBuf
+ * A buffer of size MAX_PATH + 1 to store the result.
+ * @return TRUE if successful
+ */
+BOOL GetSecureOutputDirectoryPath(LPWSTR outBuf) {
+ PWSTR progFilesX86;
+ if (FAILED(SHGetKnownFolderPath(FOLDERID_ProgramFilesX86, KF_FLAG_CREATE,
+ nullptr, &progFilesX86))) {
+ return FALSE;
+ }
+ if (wcslen(progFilesX86) > MAX_PATH) {
+ CoTaskMemFree(progFilesX86);
+ return FALSE;
+ }
+ wcsncpy(outBuf, progFilesX86, MAX_PATH + 1);
+ CoTaskMemFree(progFilesX86);
+
+ if (!PathAppendSafe(outBuf, L"Mozilla Maintenance Service")) {
+ return FALSE;
+ }
+
+ // Create the Maintenance Service directory in case it doesn't exist.
+ if (!CreateDirectoryW(outBuf, nullptr) &&
+ GetLastError() != ERROR_ALREADY_EXISTS) {
+ return FALSE;
+ }
+
+ if (!PathAppendSafe(outBuf, L"UpdateLogs")) {
+ return FALSE;
+ }
+
+ // Create the secure update output directory in case it doesn't exist.
+ if (!CreateDirectoryW(outBuf, nullptr) &&
+ GetLastError() != ERROR_ALREADY_EXISTS) {
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+/**
+ * Obtains the name of the update output file using the update patch directory
+ * path and file extension (must include the '.' separator) passed to this
+ * function.
+ *
+ * Example
+ * Patch directory path parameter:
+ * C:\ProgramData\Mozilla\updates\0123456789ABCDEF\updates\0
+ * File extension parameter:
+ * .status
+ * Destination buffer value:
+ * 0123456789ABCDEF.status
+ *
+ * @param patchDirPath
+ * The path to the update patch directory.
+ * @param fileExt
+ * The file extension for the file including the '.' separator.
+ * @param outBuf
+ * A buffer of size MAX_PATH + 1 to store the result.
+ * @return TRUE if successful
+ */
+BOOL GetSecureOutputFileName(LPCWSTR patchDirPath, LPCWSTR fileExt,
+ LPWSTR outBuf) {
+ size_t fullPathLen = wcslen(patchDirPath);
+ if (fullPathLen > MAX_PATH) {
+ return FALSE;
+ }
+
+ size_t relPathLen = wcslen(PATCH_DIR_PATH);
+ if (relPathLen > fullPathLen) {
+ return FALSE;
+ }
+
+ // The patch directory path must end with updates\0 for updates applied with
+ // an elevated updater or an updater that is launched using the maintenance
+ // service.
+ if (_wcsnicmp(patchDirPath + fullPathLen - relPathLen, PATCH_DIR_PATH,
+ relPathLen) != 0) {
+ return FALSE;
+ }
+
+ wcsncpy(outBuf, patchDirPath, MAX_PATH + 1);
+ if (!PathRemoveFileSpecW(outBuf)) {
+ return FALSE;
+ }
+
+ if (!PathRemoveFileSpecW(outBuf)) {
+ return FALSE;
+ }
+
+ PathStripPathW(outBuf);
+
+ size_t outBufLen = wcslen(outBuf);
+ size_t fileExtLen = wcslen(fileExt);
+ if (outBufLen + fileExtLen > MAX_PATH) {
+ return FALSE;
+ }
+
+ wcsncat(outBuf, fileExt, fileExtLen);
+
+ return TRUE;
+}
+
+/**
+ * Obtains the full path of the secure update output file using the update patch
+ * directory path and file extension (must include the '.' separator) passed to
+ * this function.
+ *
+ * Example
+ * Patch directory path parameter:
+ * C:\ProgramData\Mozilla\updates\0123456789ABCDEF\updates\0
+ * File extension parameter:
+ * .status
+ * Destination buffer value:
+ * C:\Program Files (x86)\Mozilla Maintenance
+ * Service\UpdateLogs\0123456789ABCDEF.status
+ *
+ * @param patchDirPath
+ * The path to the update patch directory.
+ * @param fileExt
+ * The file extension for the file including the '.' separator.
+ * @param outBuf
+ * A buffer of size MAX_PATH + 1 to store the result.
+ * @return TRUE if successful
+ */
+BOOL GetSecureOutputFilePath(LPCWSTR patchDirPath, LPCWSTR fileExt,
+ LPWSTR outBuf) {
+ if (!GetSecureOutputDirectoryPath(outBuf)) {
+ return FALSE;
+ }
+
+ WCHAR statusFileName[MAX_PATH + 1] = {L'\0'};
+ if (!GetSecureOutputFileName(patchDirPath, fileExt, statusFileName)) {
+ return FALSE;
+ }
+
+ return PathAppendSafe(outBuf, statusFileName);
+}
+
+/**
+ * Writes a UUID to the ID file in the secure output directory. This is used by
+ * the unelevated updater to determine whether an existing update status file in
+ * the secure output directory has been updated.
+ *
+ * @param patchDirPath
+ * The path to the update patch directory.
+ * @return TRUE if successful
+ */
+BOOL WriteSecureIDFile(LPCWSTR patchDirPath) {
+ WCHAR uuidString[MAX_PATH + 1] = {L'\0'};
+ if (!GetUUIDString(uuidString)) {
+ return FALSE;
+ }
+
+ WCHAR idFilePath[MAX_PATH + 1] = {L'\0'};
+ if (!GetSecureOutputFilePath(patchDirPath, L".id", idFilePath)) {
+ return FALSE;
+ }
+
+ FILE* idFile = _wfopen(idFilePath, L"wb+");
+ if (idFile == nullptr) {
+ return FALSE;
+ }
+
+ if (fprintf(idFile, "%ls\n", uuidString) == -1) {
+ fclose(idFile);
+ return FALSE;
+ }
+
+ fclose(idFile);
+
+ return TRUE;
+}
+
+/**
+ * Removes the update status and log files from the secure output directory.
+ *
+ * @param patchDirPath
+ * The path to the update patch directory.
+ */
+void RemoveSecureOutputFiles(LPCWSTR patchDirPath) {
+ WCHAR filePath[MAX_PATH + 1] = {L'\0'};
+ if (GetSecureOutputFilePath(patchDirPath, L".id", filePath)) {
+ (void)_wremove(filePath);
+ }
+ if (GetSecureOutputFilePath(patchDirPath, L".status", filePath)) {
+ (void)_wremove(filePath);
+ }
+ if (GetSecureOutputFilePath(patchDirPath, L".log", filePath)) {
+ (void)_wremove(filePath);
+ }
+}
+
+#ifdef MOZ_MAINTENANCE_SERVICE
+/**
+ * Starts the upgrade process for update of the service if it is
+ * already installed.
+ *
+ * @param installDir the installation directory where
+ * maintenanceservice_installer.exe is located.
+ * @return TRUE if successful
+ */
+BOOL StartServiceUpdate(LPCWSTR installDir) {
+ // Get a handle to the local computer SCM database
+ SC_HANDLE manager = OpenSCManager(nullptr, nullptr, SC_MANAGER_ALL_ACCESS);
+ if (!manager) {
+ return FALSE;
+ }
+
+ // Open the service
+ SC_HANDLE svc = OpenServiceW(manager, SVC_NAME, SERVICE_ALL_ACCESS);
+ if (!svc) {
+ CloseServiceHandle(manager);
+ return FALSE;
+ }
+
+ // If we reach here, then the service is installed, so
+ // proceed with upgrading it.
+
+ CloseServiceHandle(manager);
+
+ // The service exists and we opened it, get the config bytes needed
+ DWORD bytesNeeded;
+ if (!QueryServiceConfigW(svc, nullptr, 0, &bytesNeeded) &&
+ GetLastError() != ERROR_INSUFFICIENT_BUFFER) {
+ CloseServiceHandle(svc);
+ return FALSE;
+ }
+
+ // Get the service config information, in particular we want the binary
+ // path of the service.
+ UniquePtr<char[]> serviceConfigBuffer = MakeUnique<char[]>(bytesNeeded);
+ if (!QueryServiceConfigW(
+ svc,
+ reinterpret_cast<QUERY_SERVICE_CONFIGW*>(serviceConfigBuffer.get()),
+ bytesNeeded, &bytesNeeded)) {
+ CloseServiceHandle(svc);
+ return FALSE;
+ }
+
+ CloseServiceHandle(svc);
+
+ QUERY_SERVICE_CONFIGW& serviceConfig =
+ *reinterpret_cast<QUERY_SERVICE_CONFIGW*>(serviceConfigBuffer.get());
+
+ PathUnquoteSpacesW(serviceConfig.lpBinaryPathName);
+
+ // Obtain the temp path of the maintenance service binary
+ WCHAR tmpService[MAX_PATH + 1] = {L'\0'};
+ if (!PathGetSiblingFilePath(tmpService, serviceConfig.lpBinaryPathName,
+ L"maintenanceservice_tmp.exe")) {
+ return FALSE;
+ }
+
+ if (wcslen(installDir) > MAX_PATH) {
+ return FALSE;
+ }
+
+ // Get the new maintenance service path from the install dir
+ WCHAR newMaintServicePath[MAX_PATH + 1] = {L'\0'};
+ wcsncpy(newMaintServicePath, installDir, MAX_PATH);
+ PathAppendSafe(newMaintServicePath, L"maintenanceservice.exe");
+
+ // Copy the temp file in alongside the maintenace service.
+ // This is a requirement for maintenance service upgrades.
+ if (!CopyFileW(newMaintServicePath, tmpService, FALSE)) {
+ return FALSE;
+ }
+
+ // Check that the copied file's certificate matches the expected name and
+ // issuer stored in the registry for this installation and that the
+ // certificate is trusted by the system's certificate store.
+ if (!DoesBinaryMatchAllowedCertificates(installDir, tmpService)) {
+ DeleteFileW(tmpService);
+ return FALSE;
+ }
+
+ // Start the upgrade comparison process
+ STARTUPINFOW si = {0};
+ si.cb = sizeof(STARTUPINFOW);
+ // No particular desktop because no UI
+ si.lpDesktop = const_cast<LPWSTR>(L""); // -Wwritable-strings
+ PROCESS_INFORMATION pi = {0};
+ WCHAR cmdLine[64] = {'\0'};
+ wcsncpy(cmdLine, L"dummyparam.exe upgrade",
+ sizeof(cmdLine) / sizeof(cmdLine[0]) - 1);
+ BOOL svcUpdateProcessStarted =
+ CreateProcessW(tmpService, cmdLine, nullptr, nullptr, FALSE, 0, nullptr,
+ installDir, &si, &pi);
+ if (svcUpdateProcessStarted) {
+ CloseHandle(pi.hProcess);
+ CloseHandle(pi.hThread);
+ }
+ return svcUpdateProcessStarted;
+}
+
+/**
+ * Executes a maintenance service command
+ *
+ * @param argc The total number of arguments in argv
+ * @param argv An array of null terminated strings to pass to the service,
+ * @return ERROR_SUCCESS if the service command was started.
+ * Less than 16000, a windows system error code from StartServiceW
+ * More than 20000, 20000 + the last state of the service constant if
+ * the last state is something other than stopped.
+ * 17001 if the SCM could not be opened
+ * 17002 if the service could not be opened
+ */
+DWORD
+StartServiceCommand(int argc, LPCWSTR* argv) {
+ DWORD lastState = WaitForServiceStop(SVC_NAME, 5);
+ if (lastState != SERVICE_STOPPED) {
+ return 20000 + lastState;
+ }
+
+ // Get a handle to the SCM database.
+ SC_HANDLE serviceManager = OpenSCManager(
+ nullptr, nullptr, SC_MANAGER_CONNECT | SC_MANAGER_ENUMERATE_SERVICE);
+ if (!serviceManager) {
+ return 17001;
+ }
+
+ // Get a handle to the service.
+ SC_HANDLE service = OpenServiceW(serviceManager, SVC_NAME, SERVICE_START);
+ if (!service) {
+ CloseServiceHandle(serviceManager);
+ return 17002;
+ }
+
+ // Wait at most 5 seconds trying to start the service in case of errors
+ // like ERROR_SERVICE_DATABASE_LOCKED or ERROR_SERVICE_REQUEST_TIMEOUT.
+ const DWORD maxWaitMS = 5000;
+ DWORD currentWaitMS = 0;
+ DWORD lastError = ERROR_SUCCESS;
+ while (currentWaitMS < maxWaitMS) {
+ BOOL result = StartServiceW(service, argc, argv);
+ if (result) {
+ lastError = ERROR_SUCCESS;
+ break;
+ } else {
+ lastError = GetLastError();
+ }
+ Sleep(100);
+ currentWaitMS += 100;
+ }
+ CloseServiceHandle(service);
+ CloseServiceHandle(serviceManager);
+ return lastError;
+}
+
+/**
+ * Launch a service initiated action for a software update with the
+ * specified arguments.
+ *
+ * @param argc The total number of arguments in argv
+ * @param argv An array of null terminated strings to pass to the exePath,
+ * argv[0] must be the path to the updater.exe
+ * @return ERROR_SUCCESS if successful
+ */
+DWORD
+LaunchServiceSoftwareUpdateCommand(int argc, LPCWSTR* argv) {
+ // The service command is the same as the updater.exe command line except
+ // it has 4 extra args:
+ // 0) The name of the service, automatically added by Windows
+ // 1) "MozillaMaintenance" (I think this is redundant with 0)
+ // 2) The command being executed, which is "software-update"
+ // 3) The path to updater.exe (from argv[0])
+ LPCWSTR* updaterServiceArgv = new LPCWSTR[argc + 2];
+ updaterServiceArgv[0] = L"MozillaMaintenance";
+ updaterServiceArgv[1] = L"software-update";
+
+ for (int i = 0; i < argc; ++i) {
+ updaterServiceArgv[i + 2] = argv[i];
+ }
+
+ // Execute the service command by starting the service with
+ // the passed in arguments.
+ DWORD ret = StartServiceCommand(argc + 2, updaterServiceArgv);
+ delete[] updaterServiceArgv;
+ return ret;
+}
+
+/**
+ * Writes a specific failure code for the update status to a file in the secure
+ * output directory. The status file's name without the '.' separator and
+ * extension is the same as the update directory name.
+ *
+ * @param patchDirPath
+ * The path of the update patch directory.
+ * @param errorCode
+ * Error code to set
+ * @return TRUE if successful
+ */
+BOOL WriteStatusFailure(LPCWSTR patchDirPath, int errorCode) {
+ WCHAR statusFilePath[MAX_PATH + 1] = {L'\0'};
+ if (!GetSecureOutputFilePath(patchDirPath, L".status", statusFilePath)) {
+ return FALSE;
+ }
+
+ HANDLE hStatusFile = CreateFileW(statusFilePath, GENERIC_WRITE, 0, nullptr,
+ CREATE_ALWAYS, 0, nullptr);
+ if (hStatusFile == INVALID_HANDLE_VALUE) {
+ return FALSE;
+ }
+
+ char failure[32];
+ sprintf(failure, "failed: %d", errorCode);
+ DWORD toWrite = strlen(failure);
+ DWORD wrote;
+ BOOL ok = WriteFile(hStatusFile, failure, toWrite, &wrote, nullptr);
+ CloseHandle(hStatusFile);
+
+ if (!ok || wrote != toWrite) {
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+/**
+ * Waits for a service to enter a stopped state.
+ * This function does not stop the service, it just blocks until the service
+ * is stopped.
+ *
+ * @param serviceName The service to wait for.
+ * @param maxWaitSeconds The maximum number of seconds to wait
+ * @return state of the service after a timeout or when stopped.
+ * A value of 255 is returned for an error. Typical values are:
+ * SERVICE_STOPPED 0x00000001
+ * SERVICE_START_PENDING 0x00000002
+ * SERVICE_STOP_PENDING 0x00000003
+ * SERVICE_RUNNING 0x00000004
+ * SERVICE_CONTINUE_PENDING 0x00000005
+ * SERVICE_PAUSE_PENDING 0x00000006
+ * SERVICE_PAUSED 0x00000007
+ * last status not set 0x000000CF
+ * Could no query status 0x000000DF
+ * Could not open service, access denied 0x000000EB
+ * Could not open service, invalid handle 0x000000EC
+ * Could not open service, invalid name 0x000000ED
+ * Could not open service, does not exist 0x000000EE
+ * Could not open service, other error 0x000000EF
+ * Could not open SCM, access denied 0x000000FD
+ * Could not open SCM, database does not exist 0x000000FE;
+ * Could not open SCM, other error 0x000000FF;
+ * Note: The strange choice of error codes above SERVICE_PAUSED are chosen
+ * in case Windows comes out with other service stats higher than 7, they
+ * would likely call it 8 and above. JS code that uses this in TestAUSHelper
+ * only handles values up to 255 so that's why we don't use GetLastError
+ * directly.
+ */
+DWORD
+WaitForServiceStop(LPCWSTR serviceName, DWORD maxWaitSeconds) {
+ // 0x000000CF is defined above to be not set
+ DWORD lastServiceState = 0x000000CF;
+
+ // Get a handle to the SCM database.
+ SC_HANDLE serviceManager = OpenSCManager(
+ nullptr, nullptr, SC_MANAGER_CONNECT | SC_MANAGER_ENUMERATE_SERVICE);
+ if (!serviceManager) {
+ DWORD lastError = GetLastError();
+ switch (lastError) {
+ case ERROR_ACCESS_DENIED:
+ return 0x000000FD;
+ case ERROR_DATABASE_DOES_NOT_EXIST:
+ return 0x000000FE;
+ default:
+ return 0x000000FF;
+ }
+ }
+
+ // Get a handle to the service.
+ SC_HANDLE service =
+ OpenServiceW(serviceManager, serviceName, SERVICE_QUERY_STATUS);
+ if (!service) {
+ DWORD lastError = GetLastError();
+ CloseServiceHandle(serviceManager);
+ switch (lastError) {
+ case ERROR_ACCESS_DENIED:
+ return 0x000000EB;
+ case ERROR_INVALID_HANDLE:
+ return 0x000000EC;
+ case ERROR_INVALID_NAME:
+ return 0x000000ED;
+ case ERROR_SERVICE_DOES_NOT_EXIST:
+ return 0x000000EE;
+ default:
+ return 0x000000EF;
+ }
+ }
+
+ DWORD currentWaitMS = 0;
+ SERVICE_STATUS_PROCESS ssp;
+ ssp.dwCurrentState = lastServiceState;
+ while (currentWaitMS < maxWaitSeconds * 1000) {
+ DWORD bytesNeeded;
+ if (!QueryServiceStatusEx(service, SC_STATUS_PROCESS_INFO, (LPBYTE)&ssp,
+ sizeof(SERVICE_STATUS_PROCESS), &bytesNeeded)) {
+ DWORD lastError = GetLastError();
+ switch (lastError) {
+ case ERROR_INVALID_HANDLE:
+ ssp.dwCurrentState = 0x000000D9;
+ break;
+ case ERROR_ACCESS_DENIED:
+ ssp.dwCurrentState = 0x000000DA;
+ break;
+ case ERROR_INSUFFICIENT_BUFFER:
+ ssp.dwCurrentState = 0x000000DB;
+ break;
+ case ERROR_INVALID_PARAMETER:
+ ssp.dwCurrentState = 0x000000DC;
+ break;
+ case ERROR_INVALID_LEVEL:
+ ssp.dwCurrentState = 0x000000DD;
+ break;
+ case ERROR_SHUTDOWN_IN_PROGRESS:
+ ssp.dwCurrentState = 0x000000DE;
+ break;
+ // These 3 errors can occur when the service is not yet stopped but
+ // it is stopping.
+ case ERROR_INVALID_SERVICE_CONTROL:
+ case ERROR_SERVICE_CANNOT_ACCEPT_CTRL:
+ case ERROR_SERVICE_NOT_ACTIVE:
+ currentWaitMS += 50;
+ Sleep(50);
+ continue;
+ default:
+ ssp.dwCurrentState = 0x000000DF;
+ }
+
+ // We couldn't query the status so just break out
+ break;
+ }
+
+ // The service is already in use.
+ if (ssp.dwCurrentState == SERVICE_STOPPED) {
+ break;
+ }
+ currentWaitMS += 50;
+ Sleep(50);
+ }
+
+ lastServiceState = ssp.dwCurrentState;
+ CloseServiceHandle(service);
+ CloseServiceHandle(serviceManager);
+ return lastServiceState;
+}
+#endif
+
+/**
+ * Determines if there is at least one process running for the specified
+ * application. A match will be found across any session for any user.
+ *
+ * @param process The process to check for existance
+ * @return ERROR_NOT_FOUND if the process was not found
+ * ERROR_SUCCESS if the process was found and there were no errors
+ * Other Win32 system error code for other errors
+ **/
+DWORD
+IsProcessRunning(LPCWSTR filename) {
+ // Take a snapshot of all processes in the system.
+ HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
+ if (INVALID_HANDLE_VALUE == snapshot) {
+ return GetLastError();
+ }
+
+ PROCESSENTRY32W processEntry;
+ processEntry.dwSize = sizeof(PROCESSENTRY32W);
+ if (!Process32FirstW(snapshot, &processEntry)) {
+ DWORD lastError = GetLastError();
+ CloseHandle(snapshot);
+ return lastError;
+ }
+
+ do {
+ if (wcsicmp(filename, processEntry.szExeFile) == 0) {
+ CloseHandle(snapshot);
+ return ERROR_SUCCESS;
+ }
+ } while (Process32NextW(snapshot, &processEntry));
+ CloseHandle(snapshot);
+ return ERROR_NOT_FOUND;
+}
+
+/**
+ * Waits for the specified application to exit.
+ *
+ * @param filename The application to wait for.
+ * @param maxSeconds The maximum amount of seconds to wait for all
+ * instances of the application to exit.
+ * @return ERROR_SUCCESS if no instances of the application exist
+ * WAIT_TIMEOUT if the process is still running after maxSeconds.
+ * Any other Win32 system error code.
+ */
+DWORD
+WaitForProcessExit(LPCWSTR filename, DWORD maxSeconds) {
+ DWORD applicationRunningError = WAIT_TIMEOUT;
+ for (DWORD i = 0; i < maxSeconds; i++) {
+ DWORD applicationRunningError = IsProcessRunning(filename);
+ if (ERROR_NOT_FOUND == applicationRunningError) {
+ return ERROR_SUCCESS;
+ }
+ Sleep(1000);
+ }
+
+ if (ERROR_SUCCESS == applicationRunningError) {
+ return WAIT_TIMEOUT;
+ }
+
+ return applicationRunningError;
+}
+
+#ifdef MOZ_MAINTENANCE_SERVICE
+/**
+ * Determines if the fallback key exists or not
+ *
+ * @return TRUE if the fallback key exists and there was no error checking
+ */
+BOOL DoesFallbackKeyExist() {
+ HKEY testOnlyFallbackKey;
+ if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, TEST_ONLY_FALLBACK_KEY_PATH, 0,
+ KEY_READ | KEY_WOW64_64KEY,
+ &testOnlyFallbackKey) != ERROR_SUCCESS) {
+ return FALSE;
+ }
+
+ RegCloseKey(testOnlyFallbackKey);
+ return TRUE;
+}
+
+/**
+ * Determines if the file system for the specified file handle is local
+ * @param file path to check the filesystem type for, must be at most MAX_PATH
+ * @param isLocal out parameter which will hold TRUE if the drive is local
+ * @return TRUE if the call succeeded
+ */
+BOOL IsLocalFile(LPCWSTR file, BOOL& isLocal) {
+ WCHAR rootPath[MAX_PATH + 1] = {L'\0'};
+ if (wcslen(file) > MAX_PATH) {
+ return FALSE;
+ }
+
+ wcsncpy(rootPath, file, MAX_PATH);
+ PathStripToRootW(rootPath);
+ isLocal = GetDriveTypeW(rootPath) == DRIVE_FIXED;
+ return TRUE;
+}
+
+/**
+ * Determines the DWORD value of a registry key value
+ *
+ * @param key The base key to where the value name exists
+ * @param valueName The name of the value
+ * @param retValue Out parameter which will hold the value
+ * @return TRUE on success
+ */
+static BOOL GetDWORDValue(HKEY key, LPCWSTR valueName, DWORD& retValue) {
+ DWORD regDWORDValueSize = sizeof(DWORD);
+ LONG retCode =
+ RegQueryValueExW(key, valueName, 0, nullptr,
+ reinterpret_cast<LPBYTE>(&retValue), &regDWORDValueSize);
+ return ERROR_SUCCESS == retCode;
+}
+
+/**
+ * Determines if the the system's elevation type allows
+ * unprmopted elevation.
+ *
+ * @param isUnpromptedElevation Out parameter which specifies if unprompted
+ * elevation is allowed.
+ * @return TRUE if the user can actually elevate and the value was obtained
+ * successfully.
+ */
+BOOL IsUnpromptedElevation(BOOL& isUnpromptedElevation) {
+ if (!UACHelper::CanUserElevate()) {
+ return FALSE;
+ }
+
+ LPCWSTR UACBaseRegKey =
+ L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Policies\\System";
+ HKEY baseKey;
+ LONG retCode =
+ RegOpenKeyExW(HKEY_LOCAL_MACHINE, UACBaseRegKey, 0, KEY_READ, &baseKey);
+ if (retCode != ERROR_SUCCESS) {
+ return FALSE;
+ }
+
+ DWORD consent, secureDesktop;
+ BOOL success = GetDWORDValue(baseKey, L"ConsentPromptBehaviorAdmin", consent);
+ success = success &&
+ GetDWORDValue(baseKey, L"PromptOnSecureDesktop", secureDesktop);
+
+ RegCloseKey(baseKey);
+ if (success) {
+ isUnpromptedElevation = !consent && !secureDesktop;
+ }
+
+ return success;
+}
+#endif
diff --git a/toolkit/mozapps/update/common/updatehelper.h b/toolkit/mozapps/update/common/updatehelper.h
new file mode 100644
index 0000000000..b346893835
--- /dev/null
+++ b/toolkit/mozapps/update/common/updatehelper.h
@@ -0,0 +1,39 @@
+/* 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/. */
+
+#ifdef MOZ_MAINTENANCE_SERVICE
+BOOL StartServiceUpdate(LPCWSTR installDir);
+DWORD LaunchServiceSoftwareUpdateCommand(int argc, LPCWSTR* argv);
+BOOL WriteStatusFailure(LPCWSTR updateDirPath, int errorCode);
+DWORD WaitForServiceStop(LPCWSTR serviceName, DWORD maxWaitSeconds);
+BOOL DoesFallbackKeyExist();
+BOOL IsLocalFile(LPCWSTR file, BOOL& isLocal);
+DWORD StartServiceCommand(int argc, LPCWSTR* argv);
+BOOL IsUnpromptedElevation(BOOL& isUnpromptedElevation);
+#endif
+
+DWORD WaitForProcessExit(LPCWSTR filename, DWORD maxSeconds);
+DWORD IsProcessRunning(LPCWSTR filename);
+BOOL GetSecureOutputDirectoryPath(LPWSTR outBuf);
+BOOL GetSecureOutputFilePath(LPCWSTR patchDirPath, LPCWSTR fileExt,
+ LPWSTR outBuf);
+BOOL WriteSecureIDFile(LPCWSTR patchDirPath);
+void RemoveSecureOutputFiles(LPCWSTR patchDirPath);
+
+#define PATCH_DIR_PATH L"\\updates\\0"
+
+#ifdef MOZ_MAINTENANCE_SERVICE
+# define SVC_NAME L"MozillaMaintenance"
+
+# define BASE_SERVICE_REG_KEY L"SOFTWARE\\Mozilla\\MaintenanceService"
+
+// The test only fallback key, as its name implies, is only present on machines
+// that will use automated tests. Since automated tests always run from a
+// different directory for each test, the presence of this key bypasses the
+// "This is a valid installation directory" check. This key also stores
+// the allowed name and issuer for cert checks so that the cert check
+// code can still be run unchanged.
+# define TEST_ONLY_FALLBACK_KEY_PATH \
+ BASE_SERVICE_REG_KEY L"\\3932ecacee736d366d6436db0f55bce4"
+#endif
diff --git a/toolkit/mozapps/update/common/updatererrors.h b/toolkit/mozapps/update/common/updatererrors.h
new file mode 100644
index 0000000000..97a1a0bda9
--- /dev/null
+++ b/toolkit/mozapps/update/common/updatererrors.h
@@ -0,0 +1,113 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef UPDATEERRORS_H
+#define UPDATEERRORS_H
+
+#define OK 0
+
+// Error codes that are no longer used should not be used again unless they
+// aren't used in client code (e.g. nsUpdateService.js, updates.js, etc.).
+
+#define MAR_ERROR_EMPTY_ACTION_LIST 1
+#define LOADSOURCE_ERROR_WRONG_SIZE 2
+
+// Error codes 3-16 are for general update problems.
+#define USAGE_ERROR 3
+#define CRC_ERROR 4
+#define PARSE_ERROR 5
+#define READ_ERROR 6
+#define WRITE_ERROR 7
+// #define UNEXPECTED_ERROR 8 // Replaced with errors 38-42
+#define ELEVATION_CANCELED 9
+#define READ_STRINGS_MEM_ERROR 10
+#define ARCHIVE_READER_MEM_ERROR 11
+#define BSPATCH_MEM_ERROR 12
+#define UPDATER_MEM_ERROR 13
+#define UPDATER_QUOTED_PATH_MEM_ERROR 14
+#define BAD_ACTION_ERROR 15
+#define STRING_CONVERSION_ERROR 16
+
+// Error codes 17-23 are related to security tasks for MAR
+// signing and MAR protection.
+#define CERT_LOAD_ERROR 17
+#define CERT_HANDLING_ERROR 18
+#define CERT_VERIFY_ERROR 19
+#define ARCHIVE_NOT_OPEN 20
+#define COULD_NOT_READ_PRODUCT_INFO_BLOCK_ERROR 21
+#define MAR_CHANNEL_MISMATCH_ERROR 22
+#define VERSION_DOWNGRADE_ERROR 23
+
+// Error codes 24-33 and 49-58 are for the Windows maintenance service.
+#define SERVICE_UPDATER_COULD_NOT_BE_STARTED 24
+#define SERVICE_NOT_ENOUGH_COMMAND_LINE_ARGS 25
+#define SERVICE_UPDATER_SIGN_ERROR 26
+#define SERVICE_UPDATER_COMPARE_ERROR 27
+#define SERVICE_UPDATER_IDENTITY_ERROR 28
+#define SERVICE_STILL_APPLYING_ON_SUCCESS 29
+#define SERVICE_STILL_APPLYING_ON_FAILURE 30
+#define SERVICE_UPDATER_NOT_FIXED_DRIVE 31
+#define SERVICE_COULD_NOT_LOCK_UPDATER 32
+#define SERVICE_INSTALLDIR_ERROR 33
+
+#define NO_INSTALLDIR_ERROR 34
+#define WRITE_ERROR_ACCESS_DENIED 35
+// #define WRITE_ERROR_SHARING_VIOLATION 36 // Replaced with errors 46-48
+#define WRITE_ERROR_CALLBACK_APP 37
+#define UPDATE_SETTINGS_FILE_CHANNEL 38
+#define UNEXPECTED_XZ_ERROR 39
+#define UNEXPECTED_MAR_ERROR 40
+#define UNEXPECTED_BSPATCH_ERROR 41
+#define UNEXPECTED_FILE_OPERATION_ERROR 42
+#define UNEXPECTED_STAGING_ERROR 43
+#define DELETE_ERROR_STAGING_LOCK_FILE 44
+#define DELETE_ERROR_EXPECTED_DIR 46
+#define DELETE_ERROR_EXPECTED_FILE 47
+#define RENAME_ERROR_EXPECTED_FILE 48
+
+// Error codes 24-33 and 49-58 are for the Windows maintenance service.
+#define SERVICE_COULD_NOT_COPY_UPDATER 49
+#define SERVICE_STILL_APPLYING_TERMINATED 50
+#define SERVICE_STILL_APPLYING_NO_EXIT_CODE 51
+#define SERVICE_INVALID_APPLYTO_DIR_STAGED_ERROR 52
+#define SERVICE_CALC_REG_PATH_ERROR 53
+#define SERVICE_INVALID_APPLYTO_DIR_ERROR 54
+#define SERVICE_INVALID_INSTALL_DIR_PATH_ERROR 55
+#define SERVICE_INVALID_WORKING_DIR_PATH_ERROR 56
+#define SERVICE_INSTALL_DIR_REG_ERROR 57
+#define SERVICE_UPDATE_STATUS_UNCHANGED 58
+
+#define WRITE_ERROR_FILE_COPY 61
+#define WRITE_ERROR_DELETE_FILE 62
+#define WRITE_ERROR_OPEN_PATCH_FILE 63
+#define WRITE_ERROR_PATCH_FILE 64
+#define WRITE_ERROR_APPLY_DIR_PATH 65
+#define WRITE_ERROR_CALLBACK_PATH 66
+#define WRITE_ERROR_FILE_ACCESS_DENIED 67
+#define WRITE_ERROR_DIR_ACCESS_DENIED 68
+#define WRITE_ERROR_DELETE_BACKUP 69
+#define WRITE_ERROR_EXTRACT 70
+#define REMOVE_FILE_SPEC_ERROR 71
+#define INVALID_APPLYTO_DIR_STAGED_ERROR 72
+#define LOCK_ERROR_PATCH_FILE 73
+#define INVALID_APPLYTO_DIR_ERROR 74
+#define INVALID_INSTALL_DIR_PATH_ERROR 75
+#define INVALID_WORKING_DIR_PATH_ERROR 76
+#define INVALID_CALLBACK_PATH_ERROR 77
+#define INVALID_CALLBACK_DIR_ERROR 78
+#define UPDATE_STATUS_UNCHANGED 79
+
+// Error codes 80 through 99 are reserved for nsUpdateService.js
+
+// The following error codes are only used by updater.exe
+// when a fallback key exists for tests.
+#define FALLBACKKEY_UNKNOWN_ERROR 100
+#define FALLBACKKEY_REGPATH_ERROR 101
+#define FALLBACKKEY_NOKEY_ERROR 102
+#define FALLBACKKEY_SERVICE_NO_STOP_ERROR 103
+#define FALLBACKKEY_LAUNCH_ERROR 104
+
+#endif // UPDATEERRORS_H
diff --git a/toolkit/mozapps/update/common/updateutils_win.cpp b/toolkit/mozapps/update/common/updateutils_win.cpp
new file mode 100644
index 0000000000..fc2554e569
--- /dev/null
+++ b/toolkit/mozapps/update/common/updateutils_win.cpp
@@ -0,0 +1,166 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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 "updateutils_win.h"
+#include <errno.h>
+#include <shlwapi.h>
+#include <string.h>
+
+/**
+ * Note: The reason that these functions are separated from those in
+ * updatehelper.h/updatehelper.cpp is that those functions are strictly
+ * used within the updater, whereas changing functions in updateutils_win
+ * will have effects reaching beyond application update.
+ */
+
+// This section implements the minimum set of dirent APIs used by updater.cpp on
+// Windows. If updater.cpp is modified to use more of this API, we need to
+// implement those parts here too.
+static dirent gDirEnt;
+
+DIR::DIR(const WCHAR* path) : findHandle(INVALID_HANDLE_VALUE) {
+ memset(name, 0, sizeof(name));
+ wcsncpy(name, path, sizeof(name) / sizeof(name[0]));
+ wcsncat(name, L"\\*", sizeof(name) / sizeof(name[0]) - wcslen(name) - 1);
+}
+
+DIR::~DIR() {
+ if (findHandle != INVALID_HANDLE_VALUE) {
+ FindClose(findHandle);
+ }
+}
+
+dirent::dirent() { d_name[0] = L'\0'; }
+
+DIR* opendir(const WCHAR* path) { return new DIR(path); }
+
+int closedir(DIR* dir) {
+ delete dir;
+ return 0;
+}
+
+dirent* readdir(DIR* dir) {
+ WIN32_FIND_DATAW data;
+ if (dir->findHandle != INVALID_HANDLE_VALUE) {
+ BOOL result = FindNextFileW(dir->findHandle, &data);
+ if (!result) {
+ if (GetLastError() != ERROR_NO_MORE_FILES) {
+ errno = ENOENT;
+ }
+ return 0;
+ }
+ } else {
+ // Reading the first directory entry
+ dir->findHandle = FindFirstFileW(dir->name, &data);
+ if (dir->findHandle == INVALID_HANDLE_VALUE) {
+ if (GetLastError() == ERROR_FILE_NOT_FOUND) {
+ errno = ENOENT;
+ } else {
+ errno = EBADF;
+ }
+ return 0;
+ }
+ }
+ size_t direntBufferLength =
+ sizeof(gDirEnt.d_name) / sizeof(gDirEnt.d_name[0]);
+ wcsncpy(gDirEnt.d_name, data.cFileName, direntBufferLength);
+ // wcsncpy does not guarantee a null-terminated string if the source string is
+ // too long.
+ gDirEnt.d_name[direntBufferLength - 1] = '\0';
+ return &gDirEnt;
+}
+
+/**
+ * Joins a base directory path with a filename.
+ *
+ * @param base The base directory path of size MAX_PATH + 1
+ * @param extra The filename to append
+ * @return TRUE if the file name was successful appended to base
+ */
+BOOL PathAppendSafe(LPWSTR base, LPCWSTR extra) {
+ if (wcslen(base) + wcslen(extra) >= MAX_PATH) {
+ return FALSE;
+ }
+
+ return PathAppendW(base, extra);
+}
+
+/**
+ * Obtains a uuid as a wide string.
+ *
+ * @param outBuf
+ * A buffer of size MAX_PATH + 1 to store the result.
+ * @return TRUE if successful
+ */
+BOOL GetUUIDString(LPWSTR outBuf) {
+ UUID uuid;
+ RPC_WSTR uuidString = nullptr;
+
+ // Note: the return value of UuidCreate should always be RPC_S_OK on systems
+ // after Win2K / Win2003 due to the network hardware address no longer being
+ // used to create the UUID.
+ if (UuidCreate(&uuid) != RPC_S_OK) {
+ return FALSE;
+ }
+ if (UuidToStringW(&uuid, &uuidString) != RPC_S_OK) {
+ return FALSE;
+ }
+ if (!uuidString) {
+ return FALSE;
+ }
+
+ if (wcslen(reinterpret_cast<LPCWSTR>(uuidString)) > MAX_PATH) {
+ return FALSE;
+ }
+ wcsncpy(outBuf, reinterpret_cast<LPCWSTR>(uuidString), MAX_PATH + 1);
+ RpcStringFreeW(&uuidString);
+
+ return TRUE;
+}
+
+/**
+ * Build a temporary file path whose name component is a UUID.
+ *
+ * @param basePath The base directory path for the temp file
+ * @param prefix Optional prefix for the beginning of the file name
+ * @param tmpPath Output full path, with the base directory and the file
+ * name. Must already have been allocated with size >= MAX_PATH.
+ * @return TRUE if tmpPath was successfully filled in, FALSE on errors
+ */
+BOOL GetUUIDTempFilePath(LPCWSTR basePath, LPCWSTR prefix, LPWSTR tmpPath) {
+ WCHAR filename[MAX_PATH + 1] = {L"\0"};
+ if (prefix) {
+ if (wcslen(prefix) > MAX_PATH) {
+ return FALSE;
+ }
+ wcsncpy(filename, prefix, MAX_PATH + 1);
+ }
+
+ WCHAR tmpFileNameString[MAX_PATH + 1] = {L"\0"};
+ if (!GetUUIDString(tmpFileNameString)) {
+ return FALSE;
+ }
+
+ size_t tmpFileNameStringLen = wcslen(tmpFileNameString);
+ if (wcslen(filename) + tmpFileNameStringLen > MAX_PATH) {
+ return FALSE;
+ }
+ wcsncat(filename, tmpFileNameString, tmpFileNameStringLen);
+
+ size_t basePathLen = wcslen(basePath);
+ if (basePathLen > MAX_PATH) {
+ return FALSE;
+ }
+ // Use basePathLen + 1 so wcsncpy will add null termination and if a caller
+ // doesn't allocate MAX_PATH + 1 for tmpPath this won't fail when there is
+ // actually enough space allocated.
+ wcsncpy(tmpPath, basePath, basePathLen + 1);
+ if (!PathAppendSafe(tmpPath, filename)) {
+ return FALSE;
+ }
+
+ return TRUE;
+}
diff --git a/toolkit/mozapps/update/common/updateutils_win.h b/toolkit/mozapps/update/common/updateutils_win.h
new file mode 100644
index 0000000000..9de5914741
--- /dev/null
+++ b/toolkit/mozapps/update/common/updateutils_win.h
@@ -0,0 +1,47 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef WINDIRENT_H__
+#define WINDIRENT_H__
+
+/**
+ * Note: The reason that these functions are separated from those in
+ * updatehelper.h/updatehelper.cpp is that those functions are strictly
+ * used within the updater, whereas changing functions in updateutils_win
+ * will have effects reaching beyond application update.
+ */
+
+#ifndef XP_WIN
+# error This library should only be used on Windows
+#endif
+
+#include <windows.h>
+
+struct DIR {
+ explicit DIR(const WCHAR* path);
+ ~DIR();
+ HANDLE findHandle;
+ WCHAR name[MAX_PATH + 1];
+};
+
+struct dirent {
+ dirent();
+ WCHAR d_name[MAX_PATH + 1];
+};
+
+DIR* opendir(const WCHAR* path);
+int closedir(DIR* dir);
+dirent* readdir(DIR* dir);
+
+// This is the length of the UUID string including null termination returned by
+// GetUUIDString.
+#define UUID_LEN 37
+
+BOOL PathAppendSafe(LPWSTR base, LPCWSTR extra);
+BOOL GetUUIDString(LPWSTR outBuf);
+BOOL GetUUIDTempFilePath(LPCWSTR basePath, LPCWSTR prefix, LPWSTR tmpPath);
+
+#endif // WINDIRENT_H__