diff options
Diffstat (limited to 'toolkit/mozapps/update/common')
19 files changed, 2364 insertions, 0 deletions
diff --git a/toolkit/mozapps/update/common/certificatecheck.cpp b/toolkit/mozapps/update/common/certificatecheck.cpp new file mode 100644 index 000000000..afb2e7ee3 --- /dev/null +++ b/toolkit/mozapps/update/common/certificatecheck.cpp @@ -0,0 +1,250 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include <stdio.h> +#include <stdlib.h> +#include <windows.h> +#include <softpub.h> +#include <wintrust.h> + +#include "certificatecheck.h" +#include "updatecommon.h" + +static const int ENCODING = X509_ASN_ENCODING | PKCS_7_ASN_ENCODING; + +/** + * Checks to see if a file stored at filePath matches the specified info. + * + * @param filePath The PE file path to check + * @param infoToMatch The acceptable information to match + * @return ERROR_SUCCESS if successful, ERROR_NOT_FOUND if the info + * does not match, or the last error otherwise. + */ +DWORD +CheckCertificateForPEFile(LPCWSTR filePath, + CertificateCheckInfo &infoToMatch) +{ + HCERTSTORE certStore = nullptr; + HCRYPTMSG cryptMsg = nullptr; + PCCERT_CONTEXT certContext = nullptr; + PCMSG_SIGNER_INFO signerInfo = nullptr; + DWORD lastError = ERROR_SUCCESS; + + // Get the HCERTSTORE and HCRYPTMSG from the signed file. + DWORD encoding, contentType, formatType; + BOOL result = CryptQueryObject(CERT_QUERY_OBJECT_FILE, + filePath, + CERT_QUERY_CONTENT_FLAG_PKCS7_SIGNED_EMBED, + CERT_QUERY_CONTENT_FLAG_ALL, + 0, &encoding, &contentType, + &formatType, &certStore, &cryptMsg, nullptr); + if (!result) { + lastError = GetLastError(); + LOG_WARN(("CryptQueryObject failed. (%d)", lastError)); + goto cleanup; + } + + // Pass in nullptr to get the needed signer information size. + DWORD signerInfoSize; + result = CryptMsgGetParam(cryptMsg, CMSG_SIGNER_INFO_PARAM, 0, + nullptr, &signerInfoSize); + if (!result) { + lastError = GetLastError(); + LOG_WARN(("CryptMsgGetParam failed. (%d)", lastError)); + goto cleanup; + } + + // Allocate the needed size for the signer information. + signerInfo = (PCMSG_SIGNER_INFO)LocalAlloc(LPTR, signerInfoSize); + if (!signerInfo) { + lastError = GetLastError(); + LOG_WARN(("Unable to allocate memory for Signer Info. (%d)", lastError)); + goto cleanup; + } + + // Get the signer information (PCMSG_SIGNER_INFO). + // In particular we want the issuer and serial number. + result = CryptMsgGetParam(cryptMsg, CMSG_SIGNER_INFO_PARAM, 0, + (PVOID)signerInfo, &signerInfoSize); + if (!result) { + lastError = GetLastError(); + LOG_WARN(("CryptMsgGetParam failed. (%d)", lastError)); + goto cleanup; + } + + // Search for the signer certificate in the certificate store. + CERT_INFO certInfo; + certInfo.Issuer = signerInfo->Issuer; + certInfo.SerialNumber = signerInfo->SerialNumber; + certContext = CertFindCertificateInStore(certStore, ENCODING, 0, + CERT_FIND_SUBJECT_CERT, + (PVOID)&certInfo, nullptr); + if (!certContext) { + lastError = GetLastError(); + LOG_WARN(("CertFindCertificateInStore failed. (%d)", lastError)); + goto cleanup; + } + + if (!DoCertificateAttributesMatch(certContext, infoToMatch)) { + lastError = ERROR_NOT_FOUND; + LOG_WARN(("Certificate did not match issuer or name. (%d)", lastError)); + goto cleanup; + } + +cleanup: + if (signerInfo) { + LocalFree(signerInfo); + } + if (certContext) { + CertFreeCertificateContext(certContext); + } + if (certStore) { + CertCloseStore(certStore, 0); + } + if (cryptMsg) { + CryptMsgClose(cryptMsg); + } + return lastError; +} + +/** + * Checks to see if a file stored at filePath matches the specified info. + * + * @param certContext The certificate context of the file + * @param infoToMatch The acceptable information to match + * @return FALSE if the info does not match or if any error occurs in the check + */ +BOOL +DoCertificateAttributesMatch(PCCERT_CONTEXT certContext, + CertificateCheckInfo &infoToMatch) +{ + DWORD dwData; + LPWSTR szName = nullptr; + + if (infoToMatch.issuer) { + // Pass in nullptr to get the needed size of the issuer buffer. + dwData = CertGetNameString(certContext, + CERT_NAME_SIMPLE_DISPLAY_TYPE, + CERT_NAME_ISSUER_FLAG, nullptr, + nullptr, 0); + + if (!dwData) { + LOG_WARN(("CertGetNameString failed. (%d)", GetLastError())); + return FALSE; + } + + // Allocate memory for Issuer name buffer. + szName = (LPWSTR)LocalAlloc(LPTR, dwData * sizeof(WCHAR)); + if (!szName) { + LOG_WARN(("Unable to allocate memory for issuer name. (%d)", + GetLastError())); + return FALSE; + } + + // Get Issuer name. + if (!CertGetNameStringW(certContext, CERT_NAME_SIMPLE_DISPLAY_TYPE, + CERT_NAME_ISSUER_FLAG, nullptr, szName, dwData)) { + LOG_WARN(("CertGetNameString failed. (%d)", GetLastError())); + LocalFree(szName); + return FALSE; + } + + // If the issuer does not match, return a failure. + if (!infoToMatch.issuer || + wcscmp(szName, infoToMatch.issuer)) { + LocalFree(szName); + return FALSE; + } + + LocalFree(szName); + szName = nullptr; + } + + if (infoToMatch.name) { + // Pass in nullptr to get the needed size of the name buffer. + dwData = CertGetNameString(certContext, CERT_NAME_SIMPLE_DISPLAY_TYPE, + 0, nullptr, nullptr, 0); + if (!dwData) { + LOG_WARN(("CertGetNameString failed. (%d)", GetLastError())); + return FALSE; + } + + // Allocate memory for the name buffer. + szName = (LPWSTR)LocalAlloc(LPTR, dwData * sizeof(WCHAR)); + if (!szName) { + LOG_WARN(("Unable to allocate memory for subject name. (%d)", + GetLastError())); + return FALSE; + } + + // Obtain the name. + if (!(CertGetNameStringW(certContext, CERT_NAME_SIMPLE_DISPLAY_TYPE, 0, + nullptr, szName, dwData))) { + LOG_WARN(("CertGetNameString failed. (%d)", GetLastError())); + LocalFree(szName); + return FALSE; + } + + // If the issuer does not match, return a failure. + if (!infoToMatch.name || + wcscmp(szName, infoToMatch.name)) { + LocalFree(szName); + return FALSE; + } + + // We have a match! + LocalFree(szName); + } + + // If there were any errors we would have aborted by now. + return TRUE; +} + +/** + * Verifies the trust of the specified file path. + * + * @param filePath The file path to check. + * @return ERROR_SUCCESS if successful, or the last error code otherwise. + */ +DWORD +VerifyCertificateTrustForFile(LPCWSTR filePath) +{ + // Setup the file to check. + WINTRUST_FILE_INFO fileToCheck; + ZeroMemory(&fileToCheck, sizeof(fileToCheck)); + fileToCheck.cbStruct = sizeof(WINTRUST_FILE_INFO); + fileToCheck.pcwszFilePath = filePath; + + // Setup what to check, we want to check it is signed and trusted. + WINTRUST_DATA trustData; + ZeroMemory(&trustData, sizeof(trustData)); + trustData.cbStruct = sizeof(trustData); + trustData.pPolicyCallbackData = nullptr; + trustData.pSIPClientData = nullptr; + trustData.dwUIChoice = WTD_UI_NONE; + trustData.fdwRevocationChecks = WTD_REVOKE_NONE; + trustData.dwUnionChoice = WTD_CHOICE_FILE; + trustData.dwStateAction = 0; + trustData.hWVTStateData = nullptr; + trustData.pwszURLReference = nullptr; + // no UI + trustData.dwUIContext = 0; + trustData.pFile = &fileToCheck; + + GUID policyGUID = WINTRUST_ACTION_GENERIC_VERIFY_V2; + // Check if the file is signed by something that is trusted. + LONG ret = WinVerifyTrust(nullptr, &policyGUID, &trustData); + if (ERROR_SUCCESS == ret) { + // The hash that represents the subject is trusted and there were no + // verification errors. No publisher nor time stamp chain errors. + LOG(("The file \"%ls\" is signed and the signature was verified.", + filePath)); + return ERROR_SUCCESS; + } + + DWORD lastError = GetLastError(); + LOG_WARN(("There was an error validating trust of the certificate for file" + " \"%ls\". Returned: %d. (%d)", filePath, ret, lastError)); + return ret; +} diff --git a/toolkit/mozapps/update/common/certificatecheck.h b/toolkit/mozapps/update/common/certificatecheck.h new file mode 100644 index 000000000..43a7c85b6 --- /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 <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/errors.h b/toolkit/mozapps/update/common/errors.h new file mode 100644 index 000000000..aac029175 --- /dev/null +++ b/toolkit/mozapps/update/common/errors.h @@ -0,0 +1,110 @@ +/* -*- 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 Errors_h__ +#define Errors_h__ + +#define OK 0 + +// Error codes that are no longer used should not be used again unless they +// aren't used in client code (e.g. nsUpdateService.js, updates.js, +// UpdatePrompt.js, etc.). + +#define MAR_ERROR_EMPTY_ACTION_LIST 1 +#define LOADSOURCE_ERROR_WRONG_SIZE 2 + +// Error codes 3-16 are for general update problems. +#define USAGE_ERROR 3 +#define CRC_ERROR 4 +#define PARSE_ERROR 5 +#define READ_ERROR 6 +#define WRITE_ERROR 7 +// #define UNEXPECTED_ERROR 8 // Replaced with errors 38-42 +#define ELEVATION_CANCELED 9 +#define READ_STRINGS_MEM_ERROR 10 +#define ARCHIVE_READER_MEM_ERROR 11 +#define BSPATCH_MEM_ERROR 12 +#define UPDATER_MEM_ERROR 13 +#define UPDATER_QUOTED_PATH_MEM_ERROR 14 +#define BAD_ACTION_ERROR 15 +#define STRING_CONVERSION_ERROR 16 + +// Error codes 17-23 are related to security tasks for MAR +// signing and MAR protection. +#define CERT_LOAD_ERROR 17 +#define CERT_HANDLING_ERROR 18 +#define CERT_VERIFY_ERROR 19 +#define ARCHIVE_NOT_OPEN 20 +#define COULD_NOT_READ_PRODUCT_INFO_BLOCK_ERROR 21 +#define MAR_CHANNEL_MISMATCH_ERROR 22 +#define VERSION_DOWNGRADE_ERROR 23 + +// Error codes 24-33 and 49-57 are for the Windows maintenance service. +#define SERVICE_UPDATER_COULD_NOT_BE_STARTED 24 +#define SERVICE_NOT_ENOUGH_COMMAND_LINE_ARGS 25 +#define SERVICE_UPDATER_SIGN_ERROR 26 +#define SERVICE_UPDATER_COMPARE_ERROR 27 +#define SERVICE_UPDATER_IDENTITY_ERROR 28 +#define SERVICE_STILL_APPLYING_ON_SUCCESS 29 +#define SERVICE_STILL_APPLYING_ON_FAILURE 30 +#define SERVICE_UPDATER_NOT_FIXED_DRIVE 31 +#define SERVICE_COULD_NOT_LOCK_UPDATER 32 +#define SERVICE_INSTALLDIR_ERROR 33 + +#define NO_INSTALLDIR_ERROR 34 +#define WRITE_ERROR_ACCESS_DENIED 35 +// #define WRITE_ERROR_SHARING_VIOLATION 36 // Replaced with errors 46-48 +#define WRITE_ERROR_CALLBACK_APP 37 +#define UNEXPECTED_BZIP_ERROR 39 +#define UNEXPECTED_MAR_ERROR 40 +#define UNEXPECTED_BSPATCH_ERROR 41 +#define UNEXPECTED_FILE_OPERATION_ERROR 42 +#define FILESYSTEM_MOUNT_READWRITE_ERROR 43 +#define DELETE_ERROR_EXPECTED_DIR 46 +#define DELETE_ERROR_EXPECTED_FILE 47 +#define RENAME_ERROR_EXPECTED_FILE 48 + +// Error codes 24-33 and 49-57 are for the Windows maintenance service. +#define SERVICE_COULD_NOT_COPY_UPDATER 49 +#define SERVICE_STILL_APPLYING_TERMINATED 50 +#define SERVICE_STILL_APPLYING_NO_EXIT_CODE 51 +#define SERVICE_INVALID_APPLYTO_DIR_STAGED_ERROR 52 +#define SERVICE_CALC_REG_PATH_ERROR 53 +#define SERVICE_INVALID_APPLYTO_DIR_ERROR 54 +#define SERVICE_INVALID_INSTALL_DIR_PATH_ERROR 55 +#define SERVICE_INVALID_WORKING_DIR_PATH_ERROR 56 +#define SERVICE_INSTALL_DIR_REG_ERROR 57 + +#define 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 + +// Error codes 80 through 99 are reserved for nsUpdateService.js + +// The following error codes are only used by updater.exe +// when a fallback key exists for tests. +#define FALLBACKKEY_UNKNOWN_ERROR 100 +#define FALLBACKKEY_REGPATH_ERROR 101 +#define FALLBACKKEY_NOKEY_ERROR 102 +#define FALLBACKKEY_SERVICE_NO_STOP_ERROR 103 +#define FALLBACKKEY_LAUNCH_ERROR 104 + +#endif // Errors_h__ diff --git a/toolkit/mozapps/update/common/moz.build b/toolkit/mozapps/update/common/moz.build new file mode 100644 index 000000000..cacb0bad2 --- /dev/null +++ b/toolkit/mozapps/update/common/moz.build @@ -0,0 +1,32 @@ +# -*- 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 += [ + 'readstrings.h', + 'updatecommon.h', + 'updatedefines.h', +] + +if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'windows': + EXPORTS += [ + 'pathhash.h', + 'uachelper.h', + 'updatehelper.cpp', + 'updatehelper.h', + ] + if CONFIG['MOZ_MAINTENANCE_SERVICE']: + EXPORTS += [ + 'certificatecheck.h', + 'registrycertificates.h', + ] + +Library('updatecommon') + +DEFINES['NS_NO_XPCOM'] = True + +srcdir = '.' + +include('sources.mozbuild') diff --git a/toolkit/mozapps/update/common/pathhash.cpp b/toolkit/mozapps/update/common/pathhash.cpp new file mode 100644 index 000000000..89a004cde --- /dev/null +++ b/toolkit/mozapps/update/common/pathhash.cpp @@ -0,0 +1,139 @@ +/* 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 (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 000000000..a238317e1 --- /dev/null +++ b/toolkit/mozapps/update/common/pathhash.h @@ -0,0 +1,19 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef _PATHHASH_H_ +#define _PATHHASH_H_ + +/** + * Converts a file path into a unique registry location for cert storage + * + * @param filePath The input file path to get a registry path from + * @param registryPath A buffer to write the registry path to, must + * be of size in WCHARs MAX_PATH + 1 + * @return TRUE if successful +*/ +BOOL CalculateRegistryPathFromFilePath(const LPCWSTR filePath, + LPWSTR registryPath); + +#endif diff --git a/toolkit/mozapps/update/common/readstrings.cpp b/toolkit/mozapps/update/common/readstrings.cpp new file mode 100644 index 000000000..428c91c0b --- /dev/null +++ b/toolkit/mozapps/update/common/readstrings.cpp @@ -0,0 +1,236 @@ +/* -*- 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 <limits.h> +#include <string.h> +#include <stdio.h> +#include "readstrings.h" +#include "errors.h" + +#ifdef XP_WIN +# define NS_tfopen _wfopen +# define OPEN_MODE L"rb" +#else +# define NS_tfopen fopen +# define OPEN_MODE "r" +#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 Two-dimensional array of strings to be filled in the same order as the keys provided + * @param section Optional name of the section to read; defaults to "Strings" + */ +int +ReadStrings(const NS_tchar *path, + const char *keyList, + unsigned int numStrings, + char results[][MAX_TEXT_LEN], + 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) + { + strncpy(results[keyIndex], token, MAX_TEXT_LEN - 1); + results[keyIndex][MAX_TEXT_LEN - 1] = '\0'; + 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"; + char updater_strings[kNumStrings][MAX_TEXT_LEN]; + + int result = ReadStrings(path, kUpdaterKeys, kNumStrings, updater_strings); + + strncpy(results->title, updater_strings[0], MAX_TEXT_LEN - 1); + results->title[MAX_TEXT_LEN - 1] = '\0'; + strncpy(results->info, updater_strings[1], MAX_TEXT_LEN - 1); + results->info[MAX_TEXT_LEN - 1] = '\0'; + + return result; +} diff --git a/toolkit/mozapps/update/common/readstrings.h b/toolkit/mozapps/update/common/readstrings.h new file mode 100644 index 000000000..cd1909fc9 --- /dev/null +++ b/toolkit/mozapps/update/common/readstrings.h @@ -0,0 +1,43 @@ +/* -*- 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__ + +#define MAX_TEXT_LEN 600 + +#ifdef XP_WIN +# include <windows.h> + typedef WCHAR NS_tchar; +#else + typedef char NS_tchar; +#endif + +#ifndef NULL +#define NULL 0 +#endif + +struct StringTable +{ + char title[MAX_TEXT_LEN]; + char info[MAX_TEXT_LEN]; +}; + +/** + * 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, + char results[][MAX_TEXT_LEN], + const char *section = nullptr); + +#endif // READSTRINGS_H__ diff --git a/toolkit/mozapps/update/common/registrycertificates.cpp b/toolkit/mozapps/update/common/registrycertificates.cpp new file mode 100644 index 000000000..1c9c44619 --- /dev/null +++ b/toolkit/mozapps/update/common/registrycertificates.cpp @@ -0,0 +1,154 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include <stdio.h> +#include <stdlib.h> +#include <windows.h> + +#include "registrycertificates.h" +#include "pathhash.h" +#include "updatecommon.h" +#include "updatehelper.h" +#define MAX_KEY_LENGTH 255 + +/** + * Verifies if the file path matches any certificate stored in the registry. + * + * @param filePath The file path of the application to check if allowed. + * @param allowFallbackKeySkip when this is TRUE the fallback registry key will + * be used to skip the certificate check. This is the default since the + * fallback registry key is located under HKEY_LOCAL_MACHINE which can't be + * written to by a low integrity process. + * Note: the maintenance service binary can be used to perform this check for + * testing or troubleshooting. + * @return TRUE if the binary matches any of the allowed certificates. + */ +BOOL +DoesBinaryMatchAllowedCertificates(LPCWSTR basePathForUpdate, LPCWSTR filePath, + BOOL allowFallbackKeySkip) +{ + WCHAR maintenanceServiceKey[MAX_PATH + 1]; + if (!CalculateRegistryPathFromFilePath(basePathForUpdate, + maintenanceServiceKey)) { + return FALSE; + } + + // We use KEY_WOW64_64KEY to always force 64-bit view. + // The user may have both x86 and x64 applications installed + // which each register information. We need a consistent place + // to put those certificate attributes in and hence why we always + // force the non redirected registry under Wow6432Node. + // This flag is ignored on 32bit systems. + HKEY baseKey; + LONG retCode = RegOpenKeyExW(HKEY_LOCAL_MACHINE, + maintenanceServiceKey, 0, + KEY_READ | KEY_WOW64_64KEY, &baseKey); + if (retCode != ERROR_SUCCESS) { + LOG_WARN(("Could not open key. (%d)", retCode)); + // Our tests run with a different apply directory for each test. + // We use this registry key on our test slaves to store the + // allowed name/issuers. + retCode = RegOpenKeyExW(HKEY_LOCAL_MACHINE, + TEST_ONLY_FALLBACK_KEY_PATH, 0, + KEY_READ | KEY_WOW64_64KEY, &baseKey); + if (retCode != ERROR_SUCCESS) { + LOG_WARN(("Could not open fallback key. (%d)", retCode)); + return FALSE; + } else if (allowFallbackKeySkip) { + LOG_WARN(("Fallback key present, skipping VerifyCertificateTrustForFile " + "check and the certificate attribute registry matching " + "check.")); + RegCloseKey(baseKey); + return TRUE; + } + } + + // Get the number of subkeys. + DWORD subkeyCount = 0; + retCode = RegQueryInfoKeyW(baseKey, nullptr, nullptr, nullptr, &subkeyCount, + nullptr, nullptr, nullptr, nullptr, nullptr, + nullptr, nullptr); + if (retCode != ERROR_SUCCESS) { + LOG_WARN(("Could not query info key. (%d)", retCode)); + RegCloseKey(baseKey); + return FALSE; + } + + // Enumerate the subkeys, each subkey represents an allowed certificate. + for (DWORD i = 0; i < subkeyCount; i++) { + WCHAR subkeyBuffer[MAX_KEY_LENGTH]; + DWORD subkeyBufferCount = MAX_KEY_LENGTH; + retCode = RegEnumKeyExW(baseKey, i, subkeyBuffer, + &subkeyBufferCount, nullptr, + nullptr, nullptr, nullptr); + if (retCode != ERROR_SUCCESS) { + LOG_WARN(("Could not enum certs. (%d)", retCode)); + RegCloseKey(baseKey); + return FALSE; + } + + // Open the subkey for the current certificate + HKEY subKey; + retCode = RegOpenKeyExW(baseKey, + subkeyBuffer, + 0, + KEY_READ | KEY_WOW64_64KEY, + &subKey); + if (retCode != ERROR_SUCCESS) { + LOG_WARN(("Could not open subkey. (%d)", retCode)); + continue; // Try the next subkey + } + + const int MAX_CHAR_COUNT = 256; + DWORD valueBufSize = MAX_CHAR_COUNT * sizeof(WCHAR); + WCHAR name[MAX_CHAR_COUNT] = { L'\0' }; + WCHAR issuer[MAX_CHAR_COUNT] = { L'\0' }; + + // Get the name from the registry + retCode = RegQueryValueExW(subKey, L"name", 0, nullptr, + (LPBYTE)name, &valueBufSize); + if (retCode != ERROR_SUCCESS) { + LOG_WARN(("Could not obtain name from registry. (%d)", retCode)); + RegCloseKey(subKey); + continue; // Try the next subkey + } + + // Get the issuer from the registry + valueBufSize = MAX_CHAR_COUNT * sizeof(WCHAR); + retCode = RegQueryValueExW(subKey, L"issuer", 0, nullptr, + (LPBYTE)issuer, &valueBufSize); + if (retCode != ERROR_SUCCESS) { + LOG_WARN(("Could not obtain issuer from registry. (%d)", retCode)); + RegCloseKey(subKey); + continue; // Try the next subkey + } + + CertificateCheckInfo allowedCertificate = { + name, + issuer, + }; + + retCode = CheckCertificateForPEFile(filePath, allowedCertificate); + if (retCode != ERROR_SUCCESS) { + LOG_WARN(("Error on certificate check. (%d)", retCode)); + RegCloseKey(subKey); + continue; // Try the next subkey + } + + retCode = VerifyCertificateTrustForFile(filePath); + if (retCode != ERROR_SUCCESS) { + LOG_WARN(("Error on certificate trust check. (%d)", retCode)); + RegCloseKey(subKey); + continue; // Try the next subkey + } + + RegCloseKey(baseKey); + // Raise the roof, we found a match! + return TRUE; + } + + RegCloseKey(baseKey); + // No certificates match, :'( + return FALSE; +} diff --git a/toolkit/mozapps/update/common/registrycertificates.h b/toolkit/mozapps/update/common/registrycertificates.h new file mode 100644 index 000000000..9f68d1a8d --- /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/sources.mozbuild b/toolkit/mozapps/update/common/sources.mozbuild new file mode 100644 index 000000000..cde51e09b --- /dev/null +++ b/toolkit/mozapps/update/common/sources.mozbuild @@ -0,0 +1,28 @@ +# 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/. + +sources = [] + +if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'windows': + sources += [ + 'pathhash.cpp', + 'uachelper.cpp', + 'updatehelper.cpp', + ] + if CONFIG['MOZ_MAINTENANCE_SERVICE']: + sources += [ + 'certificatecheck.cpp', + 'registrycertificates.cpp', + ] + OS_LIBS += [ + 'crypt32', + 'wintrust', + ] + +sources += [ + 'readstrings.cpp', + 'updatecommon.cpp', +] + +SOURCES += sorted(['%s/%s' % (srcdir, s) for s in sources]) diff --git a/toolkit/mozapps/update/common/uachelper.cpp b/toolkit/mozapps/update/common/uachelper.cpp new file mode 100644 index 000000000..11647aa72 --- /dev/null +++ b/toolkit/mozapps/update/common/uachelper.cpp @@ -0,0 +1,222 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include <windows.h> +#include <wtsapi32.h> +#include "uachelper.h" +#include "updatecommon.h" + +// See the MSDN documentation with title: Privilege Constants +// At the time of this writing, this documentation is located at: +// http://msdn.microsoft.com/en-us/library/windows/desktop/bb530716%28v=vs.85%29.aspx +LPCTSTR UACHelper::PrivsToDisable[] = { + SE_ASSIGNPRIMARYTOKEN_NAME, + SE_AUDIT_NAME, + SE_BACKUP_NAME, + // CreateProcess will succeed but the app will fail to launch on some WinXP + // machines if SE_CHANGE_NOTIFY_NAME is disabled. In particular this happens + // for limited user accounts on those machines. The define is kept here as a + // reminder that it should never be re-added. + // This permission is for directory watching but also from MSDN: "This + // privilege also causes the system to skip all traversal access checks." + // SE_CHANGE_NOTIFY_NAME, + SE_CREATE_GLOBAL_NAME, + SE_CREATE_PAGEFILE_NAME, + SE_CREATE_PERMANENT_NAME, + SE_CREATE_SYMBOLIC_LINK_NAME, + SE_CREATE_TOKEN_NAME, + SE_DEBUG_NAME, + SE_ENABLE_DELEGATION_NAME, + SE_IMPERSONATE_NAME, + SE_INC_BASE_PRIORITY_NAME, + SE_INCREASE_QUOTA_NAME, + SE_INC_WORKING_SET_NAME, + SE_LOAD_DRIVER_NAME, + SE_LOCK_MEMORY_NAME, + SE_MACHINE_ACCOUNT_NAME, + SE_MANAGE_VOLUME_NAME, + SE_PROF_SINGLE_PROCESS_NAME, + SE_RELABEL_NAME, + SE_REMOTE_SHUTDOWN_NAME, + SE_RESTORE_NAME, + SE_SECURITY_NAME, + SE_SHUTDOWN_NAME, + SE_SYNC_AGENT_NAME, + SE_SYSTEM_ENVIRONMENT_NAME, + SE_SYSTEM_PROFILE_NAME, + SE_SYSTEMTIME_NAME, + SE_TAKE_OWNERSHIP_NAME, + SE_TCB_NAME, + SE_TIME_ZONE_NAME, + SE_TRUSTED_CREDMAN_ACCESS_NAME, + SE_UNDOCK_NAME, + SE_UNSOLICITED_INPUT_NAME +}; + +/** + * Opens a user token for the given session ID + * + * @param sessionID The session ID for the token to obtain + * @return A handle to the token to obtain which will be primary if enough + * permissions exist. Caller should close the handle. + */ +HANDLE +UACHelper::OpenUserToken(DWORD sessionID) +{ + HMODULE module = LoadLibraryW(L"wtsapi32.dll"); + HANDLE token = nullptr; + decltype(WTSQueryUserToken)* wtsQueryUserToken = + (decltype(WTSQueryUserToken)*) GetProcAddress(module, "WTSQueryUserToken"); + if (wtsQueryUserToken) { + wtsQueryUserToken(sessionID, &token); + } + FreeLibrary(module); + return token; +} + +/** + * Opens a linked token for the specified token. + * + * @param token The token to get the linked token from + * @return A linked token or nullptr if one does not exist. + * Caller should close the handle. + */ +HANDLE +UACHelper::OpenLinkedToken(HANDLE token) +{ + // Magic below... + // UAC creates 2 tokens. One is the restricted token which we have. + // the other is the UAC elevated one. Since we are running as a service + // as the system account we have access to both. + TOKEN_LINKED_TOKEN tlt; + HANDLE hNewLinkedToken = nullptr; + DWORD len; + if (GetTokenInformation(token, (TOKEN_INFORMATION_CLASS)TokenLinkedToken, + &tlt, sizeof(TOKEN_LINKED_TOKEN), &len)) { + token = tlt.LinkedToken; + hNewLinkedToken = token; + } + return hNewLinkedToken; +} + + +/** + * Enables or disables a privilege for the specified token. + * + * @param token The token to adjust the privilege on. + * @param priv The privilege to adjust. + * @param enable Whether to enable or disable it + * @return TRUE if the token was adjusted to the specified value. + */ +BOOL +UACHelper::SetPrivilege(HANDLE token, LPCTSTR priv, BOOL enable) +{ + LUID luidOfPriv; + if (!LookupPrivilegeValue(nullptr, priv, &luidOfPriv)) { + return FALSE; + } + + TOKEN_PRIVILEGES tokenPriv; + tokenPriv.PrivilegeCount = 1; + tokenPriv.Privileges[0].Luid = luidOfPriv; + tokenPriv.Privileges[0].Attributes = enable ? SE_PRIVILEGE_ENABLED : 0; + + SetLastError(ERROR_SUCCESS); + if (!AdjustTokenPrivileges(token, false, &tokenPriv, + sizeof(tokenPriv), nullptr, nullptr)) { + return FALSE; + } + + return GetLastError() == ERROR_SUCCESS; +} + +/** + * For each privilege that is specified, an attempt will be made to + * drop the privilege. + * + * @param token The token to adjust the privilege on. + * Pass nullptr for current token. + * @param unneededPrivs An array of unneeded privileges. + * @param count The size of the array + * @return TRUE if there were no errors + */ +BOOL +UACHelper::DisableUnneededPrivileges(HANDLE token, + LPCTSTR *unneededPrivs, + size_t count) +{ + HANDLE obtainedToken = nullptr; + if (!token) { + // Note: This handle is a pseudo-handle and need not be closed + HANDLE process = GetCurrentProcess(); + if (!OpenProcessToken(process, TOKEN_ALL_ACCESS_P, &obtainedToken)) { + LOG_WARN(("Could not obtain token for current process, no " + "privileges changed. (%d)", GetLastError())); + return FALSE; + } + token = obtainedToken; + } + + BOOL result = TRUE; + for (size_t i = 0; i < count; i++) { + if (SetPrivilege(token, unneededPrivs[i], FALSE)) { + LOG(("Disabled unneeded token privilege: %s.", + unneededPrivs[i])); + } else { + LOG(("Could not disable token privilege value: %s. (%d)", + unneededPrivs[i], GetLastError())); + result = FALSE; + } + } + + if (obtainedToken) { + CloseHandle(obtainedToken); + } + return result; +} + +/** + * Disables privileges for the specified token. + * The privileges to disable are in PrivsToDisable. + * In the future there could be new privs and we are not sure if we should + * explicitly disable these or not. + * + * @param token The token to drop the privilege on. + * Pass nullptr for current token. + * @return TRUE if there were no errors + */ +BOOL +UACHelper::DisablePrivileges(HANDLE token) +{ + static const size_t PrivsToDisableSize = + sizeof(UACHelper::PrivsToDisable) / sizeof(UACHelper::PrivsToDisable[0]); + + return DisableUnneededPrivileges(token, UACHelper::PrivsToDisable, + PrivsToDisableSize); +} + +/** + * Check if the current user can elevate. + * + * @return true if the user can elevate. + * false otherwise. + */ +bool +UACHelper::CanUserElevate() +{ + HANDLE token; + if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &token)) { + return false; + } + + TOKEN_ELEVATION_TYPE elevationType; + DWORD len; + bool canElevate = GetTokenInformation(token, TokenElevationType, + &elevationType, + sizeof(elevationType), &len) && + (elevationType == TokenElevationTypeLimited); + CloseHandle(token); + + return canElevate; +} diff --git a/toolkit/mozapps/update/common/uachelper.h b/toolkit/mozapps/update/common/uachelper.h new file mode 100644 index 000000000..6481ff5b5 --- /dev/null +++ b/toolkit/mozapps/update/common/uachelper.h @@ -0,0 +1,23 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef _UACHELPER_H_ +#define _UACHELPER_H_ + +class UACHelper +{ +public: + static HANDLE OpenUserToken(DWORD sessionID); + static HANDLE OpenLinkedToken(HANDLE token); + static BOOL DisablePrivileges(HANDLE token); + static bool CanUserElevate(); + +private: + static BOOL SetPrivilege(HANDLE token, LPCTSTR privs, BOOL enable); + static BOOL DisableUnneededPrivileges(HANDLE token, + LPCTSTR *unneededPrivs, size_t count); + static LPCTSTR PrivsToDisable[]; +}; + +#endif diff --git a/toolkit/mozapps/update/common/updatecommon.cpp b/toolkit/mozapps/update/common/updatecommon.cpp new file mode 100644 index 000000000..aff7d7260 --- /dev/null +++ b/toolkit/mozapps/update/common/updatecommon.cpp @@ -0,0 +1,213 @@ +/* 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> +#endif + + +#include <stdio.h> +#include <string.h> +#include <stdlib.h> +#include <stdarg.h> + +#include "updatecommon.h" + +UpdateLog::UpdateLog() : logFP(nullptr) +{ +} + +void UpdateLog::Init(NS_tchar* sourcePath, + const NS_tchar* fileName) +{ + if (logFP) { + return; + } + + int dstFilePathLen = NS_tsnprintf(mDstFilePath, + sizeof(mDstFilePath)/sizeof(mDstFilePath[0]), + NS_T("%s/%s"), sourcePath, fileName); + // If the destination path was over the length limit, + // disable logging by skipping opening the file and setting logFP. + if ((dstFilePathLen > 0) && + (dstFilePathLen < + static_cast<int>(sizeof(mDstFilePath)/sizeof(mDstFilePath[0])))) { +#ifdef XP_WIN + if (GetTempFileNameW(sourcePath, L"log", 0, mTmpFilePath) != 0) { + logFP = NS_tfopen(mTmpFilePath, NS_T("w")); + + // Delete this file now so it is possible to tell from the unelevated + // updater process if the elevated updater process has written the log. + DeleteFileW(mDstFilePath); + } +#elif 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; + +#ifdef XP_WIN + // When the log file already exists then the elevated updater has already + // written the log file and the temp file for the log should be discarded. + if (!NS_taccess(mDstFilePath, F_OK)) { + DeleteFileW(mTmpFilePath); + } else { + MoveFileW(mTmpFilePath, mDstFilePath); + } +#endif +} + +void UpdateLog::Flush() +{ + if (!logFP) { + return; + } + + fflush(logFP); +} + +void UpdateLog::Printf(const char *fmt, ... ) +{ + if (!logFP) { + return; + } + + va_list ap; + va_start(ap, fmt); + vfprintf(logFP, fmt, ap); + fprintf(logFP, "\n"); + va_end(ap); +} + +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); +} + +/** + * 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; + } + } +#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 || + NS_tstrstr(origFullPath, NS_T("./")) != nullptr) { + 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 000000000..3055851d8 --- /dev/null +++ b/toolkit/mozapps/update/common/updatecommon.h @@ -0,0 +1,47 @@ +/* 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> + +class UpdateLog +{ +public: + static UpdateLog & GetPrimaryLog() + { + static UpdateLog primaryLog; + return primaryLog; + } + + void Init(NS_tchar* sourcePath, const NS_tchar* fileName); + void Finish(); + void Flush(); + void Printf(const char *fmt, ... ); + void WarnPrintf(const char *fmt, ... ); + + ~UpdateLog() + { + Finish(); + } + +protected: + UpdateLog(); + FILE *logFP; + NS_tchar mTmpFilePath[MAXPATHLEN]; + NS_tchar mDstFilePath[MAXPATHLEN]; +}; + +bool IsValidFullPath(NS_tchar* fullPath); + +#define LOG_WARN(args) UpdateLog::GetPrimaryLog().WarnPrintf args +#define LOG(args) UpdateLog::GetPrimaryLog().Printf args +#define LogInit(PATHNAME_, FILENAME_) \ + UpdateLog::GetPrimaryLog().Init(PATHNAME_, FILENAME_) +#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 000000000..760d2c4c4 --- /dev/null +++ b/toolkit/mozapps/update/common/updatedefines.h @@ -0,0 +1,142 @@ +/* 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 "readstrings.h" + +#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 + +#if defined(XP_WIN) +# include <windows.h> +# include <shlwapi.h> +# include <direct.h> +# include <io.h> +# include <stdio.h> +# include <stdarg.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_T(str) 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_tchdir _wchdir +# define NS_tchmod _wchmod +# define NS_tfopen _wfopen +# define NS_tmkdir(path, perms) _wmkdir(path) +# 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_tstrcpy wcscpy +# define NS_tstrncpy wcsncpy +# define NS_tstrlen wcslen +# define NS_tstrchr wcschr +# define NS_tstrrchr wcsrchr +# define NS_tstrstr wcsstr +# include "win_dirent.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 SOLARIS +# include <sys/stat.h> +#else +# include <fts.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_tchdir chdir +# define NS_tchmod chmod +# define NS_tfopen fopen +# define NS_tmkdir mkdir +# 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_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") + +#endif diff --git a/toolkit/mozapps/update/common/updatehelper.cpp b/toolkit/mozapps/update/common/updatehelper.cpp new file mode 100644 index 000000000..afb89bacf --- /dev/null +++ b/toolkit/mozapps/update/common/updatehelper.cpp @@ -0,0 +1,609 @@ +/* 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> +#ifndef ONLY_SERVICE_LAUNCHING + +#include <stdio.h> +#include "shlobj.h" +#include "updatehelper.h" +#include "uachelper.h" +#include "pathhash.h" +#include "mozilla/UniquePtr.h" + +// Needed for PathAppendW +#include <shlwapi.h> + +using mozilla::MakeUnique; +using mozilla::UniquePtr; + +BOOL PathAppendSafe(LPWSTR base, LPCWSTR extra); +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); + if (!PathRemoveFileSpecW(destinationBuffer)) { + return FALSE; + } + + if (wcslen(destinationBuffer) + wcslen(newFileName) >= MAX_PATH) { + return FALSE; + } + + return PathAppendSafe(destinationBuffer, newFileName); +} + +/** + * 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; + } + + // 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; + } + + // Start the upgrade comparison process + STARTUPINFOW si = {0}; + si.cb = sizeof(STARTUPINFOW); + // No particular desktop because no UI + si.lpDesktop = L""; + 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; +} + +#endif + +/** + * 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; +} + +#ifndef ONLY_SERVICE_LAUNCHING + +/** + * Launch a service initiated action for a software update with the + * specified arguments. + * + * @param exePath The path of the executable to run + * @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 2 extra args: 1) The Path to udpater.exe, and 2) the command + // being executed which is "software-update" + 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; +} + +/** + * 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); +} + +/** + * Sets update.status to a specific failure code + * + * @param updateDirPath The path of the update directory + * @return TRUE if successful + */ +BOOL +WriteStatusFailure(LPCWSTR updateDirPath, int errorCode) +{ + // The temp file is not removed on failure since there is client code that + // will remove it. + WCHAR tmpUpdateStatusFilePath[MAX_PATH + 1] = { L'\0' }; + if (GetTempFileNameW(updateDirPath, L"svc", 0, tmpUpdateStatusFilePath) == 0) { + return FALSE; + } + + HANDLE tmpStatusFile = CreateFileW(tmpUpdateStatusFilePath, GENERIC_WRITE, 0, + nullptr, CREATE_ALWAYS, 0, nullptr); + if (tmpStatusFile == INVALID_HANDLE_VALUE) { + return FALSE; + } + + char failure[32]; + sprintf(failure, "failed: %d", errorCode); + DWORD toWrite = strlen(failure); + DWORD wrote; + BOOL ok = WriteFile(tmpStatusFile, failure, + toWrite, &wrote, nullptr); + CloseHandle(tmpStatusFile); + + if (!ok || wrote != toWrite) { + return FALSE; + } + + WCHAR updateStatusFilePath[MAX_PATH + 1] = { L'\0' }; + wcsncpy(updateStatusFilePath, updateDirPath, MAX_PATH); + if (!PathAppendSafe(updateStatusFilePath, L"update.status")) { + return FALSE; + } + + if (MoveFileExW(tmpUpdateStatusFilePath, updateStatusFilePath, + MOVEFILE_REPLACE_EXISTING) == 0) { + return FALSE; + } + + return TRUE; +} + +#endif + +/** + * 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; +} + +#ifndef ONLY_SERVICE_LAUNCHING + +/** + * 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 applicaiton 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; +} + +/** + * 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; +} + +#endif + +/** + * 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); + isUnpromptedElevation = !consent && !secureDesktop; + + RegCloseKey(baseKey); + return success; +} diff --git a/toolkit/mozapps/update/common/updatehelper.h b/toolkit/mozapps/update/common/updatehelper.h new file mode 100644 index 000000000..3f010df49 --- /dev/null +++ b/toolkit/mozapps/update/common/updatehelper.h @@ -0,0 +1,29 @@ +/* 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/. */ + +BOOL StartServiceUpdate(LPCWSTR installDir); +DWORD LaunchServiceSoftwareUpdateCommand(int argc, LPCWSTR *argv); +BOOL WriteStatusFailure(LPCWSTR updateDirPath, int errorCode); +DWORD WaitForServiceStop(LPCWSTR serviceName, DWORD maxWaitSeconds); +DWORD WaitForProcessExit(LPCWSTR filename, DWORD maxSeconds); +DWORD IsProcessRunning(LPCWSTR filename); +BOOL DoesFallbackKeyExist(); +BOOL IsLocalFile(LPCWSTR file, BOOL &isLocal); +DWORD StartServiceCommand(int argc, LPCWSTR* argv); +BOOL IsUnpromptedElevation(BOOL &isUnpromptedElevation); + +#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" + diff --git a/toolkit/mozapps/update/common/win_dirent.h b/toolkit/mozapps/update/common/win_dirent.h new file mode 100644 index 000000000..28f5317ff --- /dev/null +++ b/toolkit/mozapps/update/common/win_dirent.h @@ -0,0 +1,32 @@ +/* -*- 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__ + +#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]; +}; + +struct dirent { + dirent(); + WCHAR d_name[MAX_PATH]; +}; + +DIR* opendir(const WCHAR* path); +int closedir(DIR* dir); +dirent* readdir(DIR* dir); + +#endif // WINDIRENT_H__ |