diff options
Diffstat (limited to 'toolkit/mozapps/update/common')
21 files changed, 3931 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..8c53fa5fd6 --- /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. (%lu)", 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. (%lu)", 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. (%lu)", 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. (%lu)", 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. (%lu)", lastError)); + goto cleanup; + } + + if (!DoCertificateAttributesMatch(certContext, infoToMatch)) { + lastError = ERROR_NOT_FOUND; + LOG_WARN(("Certificate did not match issuer or name. (%lu)", 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. (%lu)", 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. (%lu)", + 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. (%lu)", 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. (%lu)", 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. (%lu)", + GetLastError())); + return FALSE; + } + + // Obtain the name. + if (!(CertGetNameStringW(certContext, CERT_NAME_SIMPLE_DISPLAY_TYPE, 0, + nullptr, szName, dwData))) { + LOG_WARN(("CertGetNameString failed. (%lu)", 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: %ld. (%lu)", + 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..af9f8456df --- /dev/null +++ b/toolkit/mozapps/update/common/certificatecheck.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 _CERTIFICATECHECK_H_ +#define _CERTIFICATECHECK_H_ + +#include <windows.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..0ba9fcef94 --- /dev/null +++ b/toolkit/mozapps/update/common/commonupdatedir.cpp @@ -0,0 +1,723 @@ +/* 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 old update directory +// (i.e. C:\ProgramData\<OLD_ROOT_UPDATE_DIR_NAME>) +# define OLD_ROOT_UPDATE_DIR_NAME "Mozilla" +// This is the name of the current update directory +// (i.e. C:\ProgramData\<ROOT_UPDATE_DIR_NAME>) +// It is really important that we properly set the permissions on this +// directory at creation time. Thus, it is really important that this code be +// the creator of this directory. We had many problems with the old update +// directory having been previously created by old versions of Firefox. To avoid +// this problem in the future, we are including a UUID in the root update +// directory name to attempt to ensure that it will be created by this code and +// won't already exist with the wrong permissions. +# define ROOT_UPDATE_DIR_NAME "Mozilla-1de4eec8-1241-4177-a864-e594e8d1fb38" +// 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" + +enum class WhichUpdateDir { + CurrentUpdateDir, + UnmigratedUpdateDir, +}; + +/** + * 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 bool GetCachedHash(const char16_t* installPath, HKEY rootKey, + const SimpleAutoString& regPath, + mozilla::UniquePtr<NS_tchar[]>& result); +static HRESULT GetUpdateDirectory(const wchar_t* installPath, + WhichUpdateDir whichDir, + mozilla::UniquePtr<wchar_t[]>& result); +static HRESULT GeneratePermissions(AutoPerms& result); +static HRESULT MakeDir(const SimpleAutoString& path, const AutoPerms& perms); +#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 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. + * @return true if successful and false otherwise. + */ +bool GetInstallHash(const char16_t* installPath, + mozilla::UniquePtr<NS_tchar[]>& result) { + MOZ_ASSERT(installPath != nullptr, + "Install path must not be null in GetInstallHash"); + + 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 = + 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); + // 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. + * + * @param installPath + * Must be the null-terminated path to the installation directory (i.e. + * the directory that contains the binary). The path must not include a + * trailing slash. + * @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, + mozilla::UniquePtr<wchar_t[]>& result) { + return GetUpdateDirectory(installPath, WhichUpdateDir::CurrentUpdateDir, + result); +} + +/** + * This function is identical to the function above except that it gets the + * "old" (pre-migration) update directory. + * + * The other difference is that this function does not create the directory. + */ +HRESULT +GetOldUpdateDirectory(const wchar_t* installPath, + mozilla::UniquePtr<wchar_t[]>& result) { + return GetUpdateDirectory(installPath, WhichUpdateDir::UnmigratedUpdateDir, + result); +} + +/** + * This is a 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, 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. + * + * For information on the parameters and return value, see + * GetCommonUpdateDirectory. + */ +static HRESULT GetUpdateDirectory(const wchar_t* installPath, + WhichUpdateDir whichDir, + mozilla::UniquePtr<wchar_t[]>& result) { + MOZ_ASSERT(installPath != nullptr, + "Install path must not be null in GetUpdateDirectory"); + + AutoPerms perms; + HRESULT hrv = GeneratePermissions(perms); + if (FAILED(hrv)) { + return hrv; + } + + PWSTR baseDirParentPath; + hrv = SHGetKnownFolderPath(FOLDERID_ProgramData, 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::UnmigratedUpdateDir) { + const wchar_t baseDirLiteral[] = NS_T(OLD_ROOT_UPDATE_DIR_NAME); + hrv = baseDir.CopyFrom(baseDirLiteral); + } else { + const wchar_t baseDirLiteral[] = NS_T(ROOT_UPDATE_DIR_NAME); + hrv = baseDir.CopyFrom(baseDirLiteral); + } + if (FAILED(hrv)) { + return hrv; + } + + // Generate the base path + // (C:\ProgramData\Mozilla-1de4eec8-1241-4177-a864-e594e8d1fb38) + 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; + } + + if (whichDir == WhichUpdateDir::CurrentUpdateDir) { + hrv = MakeDir(basePath, perms); + if (FAILED(hrv)) { + return hrv; + } + } + + // Generate what we are going to call the mid-path + // (C:\ProgramData\Mozilla-1de4eec8-1241-4177-a864-e594e8d1fb38\updates) + const wchar_t midPathDirName[] = NS_T(UPDATE_PATH_MID_DIR_NAME); + size_t midPathLen = + basePath.Length() + 1 /* path separator */ + wcslen(midPathDirName); + SimpleAutoString midPath; + midPath.AllocAndAssignSprintf(midPathLen, L"%s\\%s", basePath.String(), + midPathDirName); + if (midPath.Length() != midPathLen) { + return E_FAIL; + } + + mozilla::UniquePtr<NS_tchar[]> hash; + + // The Windows installer caches this hash value in the registry + bool gotHash = false; + SimpleAutoString regPath; + regPath.AutoAllocAndAssignSprintf(L"SOFTWARE\\Mozilla\\%S\\TaskBarIDs", + 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); + } + } + // If we couldn't get it out of the registry, we'll just have to regenerate + // it. + if (!gotHash) { + bool success = + GetInstallHash(reinterpret_cast<const char16_t*>(installPath), hash); + if (!success) { + return E_FAIL; + } + } + + size_t updatePathLen = + midPath.Length() + 1 /* path separator */ + wcslen(hash.get()); + SimpleAutoString updatePath; + updatePath.AllocAndAssignSprintf(updatePathLen, L"%s\\%s", midPath.String(), + hash.get()); + if (updatePath.Length() != updatePathLen) { + return E_FAIL; + } + + updatePath.SwapBufferWith(result); + return S_OK; +} + +/** + * 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. + */ +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 S_OK; + } + return HRESULT_FROM_WIN32(error); +} +#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..5d7f88b15e --- /dev/null +++ b/toolkit/mozapps/update/common/commonupdatedir.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/. */ +#ifndef COMMONUPDATEDIR_H +#define COMMONUPDATEDIR_H + +#include "mozilla/UniquePtr.h" + +#ifdef XP_WIN +# include <windows.h> +typedef WCHAR NS_tchar; +#else +typedef char NS_tchar; +#endif + +bool GetInstallHash(const char16_t* installPath, + mozilla::UniquePtr<NS_tchar[]>& result); + +#ifdef XP_WIN +// In addition to getting the update directory, this function also creates it. +// This is to ensure that, when it is created, it is created with the correct +// permissions. The default permissions on the containing directory can cause +// problems, so it is very, very important that we make sure that the +// permissions are set properly. Thus, we won't even give out the path of the +// update directory without ensuring that it was created with the correct +// permissions. +HRESULT GetCommonUpdateDirectory(const wchar_t* installPath, + mozilla::UniquePtr<wchar_t[]>& result); +// Returns the old common update directory. Since this directory was used before +// we made sure to always set the correct permissions, it is possible that the +// permissions on this directory are set such that files can only be modified +// or deleted by the user that created them. This function exists entirely to +// allow us to migrate files out of the old update directory and into the new +// one. +HRESULT GetOldUpdateDirectory(const wchar_t* installPath, + 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..17f08ae95e --- /dev/null +++ b/toolkit/mozapps/update/common/pathhash.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 _PATHHASH_H_ +#define _PATHHASH_H_ + +#include <windows.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..17c2d002a1 --- /dev/null +++ b/toolkit/mozapps/update/common/readstrings.cpp @@ -0,0 +1,396 @@ +/* -*- 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 <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.empty()) { + // 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..786218130a --- /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. (%ld)", 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. (%ld)", 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. (%ld)", 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. (%ld)", 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. (%ld)", 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. (%ld)", 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. (%ld)", 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. (%ld)", retCode)); + RegCloseKey(subKey); + continue; // Try the next subkey + } + + retCode = VerifyCertificateTrustForFile(filePath); + if (retCode != ERROR_SUCCESS) { + LOG_WARN(("Error on certificate trust check. (%ld)", 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..07e9bd53f9 --- /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. (%lu)", + 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. (%lu)", + 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..c9915981a0 --- /dev/null +++ b/toolkit/mozapps/update/common/uachelper.h @@ -0,0 +1,24 @@ +/* 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_ + +#include <windows.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..9e00ac5716 --- /dev/null +++ b/toolkit/mozapps/update/common/updatecommon.cpp @@ -0,0 +1,470 @@ +/* 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 <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; + } + + time_t rawtime = time(nullptr); + struct tm* timeinfo = localtime(&rawtime); + + if (nullptr != timeinfo) { + // attempt to format the time similar to rfc-3339 so that it works with + // sort(1). xxxx-xx-xx xx:xx:xx+xxxx -> 24 chars + 1 NUL + const size_t buffer_size = 25; + char buffer[buffer_size] = {0}; + + if (0 == strftime(buffer, buffer_size, "%Y-%m-%d %H:%M:%S%z", timeinfo)) { + buffer[0] = '\0'; // reset buffer into a defined state and try posix ts + if (0 > snprintf(buffer, buffer_size, "%d", (int)mktime(timeinfo))) { + buffer[0] = '\0'; // reset and give up + } + } + + fprintf(logFP, "%s: ", buffer); + } + + va_list ap; + va_start(ap, fmt); + vfprintf(logFP, fmt, ap); + va_end(ap); + + fprintf(logFP, "\n"); +#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); + 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 (!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); + + 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); + + // 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); + + 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..e317bdcb1f --- /dev/null +++ b/toolkit/mozapps/update/common/updatecommon.h @@ -0,0 +1,43 @@ +/* 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 <time.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..f2663d5b57 --- /dev/null +++ b/toolkit/mozapps/update/common/updatererrors.h @@ -0,0 +1,130 @@ +/* -*- 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. UpdateService.jsm, 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 + +// Error codes 10-14 are related to memory allocation failures. +// Note: If more memory allocation error codes are added, the implementation of +// isMemoryAllocationErrorCode in UpdateService.jsm should be updated to account +// for them. +#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. +// Note: If more maintenance service error codes are added, the implementations +// of IsServiceSpecificErrorCode in updater.cpp and UpdateService.jsm should be +// updated to account for them. +#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. +// Note: If more maintenance service error codes are added, the implementations +// of IsServiceSpecificErrorCode in updater.cpp and UpdateService.jsm should be +// updated to account for them. +#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 UpdateService.jsm + +// 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 + +#define SILENT_UPDATE_NEEDED_ELEVATION_ERROR 105 +#define WRITE_ERROR_BACKGROUND_TASK_SHARING_VIOLATION 106 + +// Error codes 110 and 111 are reserved for UpdateService.jsm + +#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__ |