diff options
Diffstat (limited to 'toolkit/mozapps/update/common')
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), ®DWORDValueSize); + 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__ |