diff options
Diffstat (limited to 'toolkit/mozapps/update/updater')
45 files changed, 7483 insertions, 0 deletions
diff --git a/toolkit/mozapps/update/updater/Launchd.plist b/toolkit/mozapps/update/updater/Launchd.plist new file mode 100644 index 0000000000..f0b5cef085 --- /dev/null +++ b/toolkit/mozapps/update/updater/Launchd.plist @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>Label</key> + <string>org.mozilla.updater</string> + <key>RunAtLoad</key> + <true/> +</dict> +</plist> diff --git a/toolkit/mozapps/update/updater/Makefile.in b/toolkit/mozapps/update/updater/Makefile.in new file mode 100644 index 0000000000..502c09d21f --- /dev/null +++ b/toolkit/mozapps/update/updater/Makefile.in @@ -0,0 +1,28 @@ +# vim:set ts=8 sw=8 sts=8 noet: +# 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/. + +# For changes here, also consider ./updater-xpcshell/Makefile.in + +ifndef MOZ_WINCONSOLE +ifdef MOZ_DEBUG +MOZ_WINCONSOLE = 1 +else +MOZ_WINCONSOLE = 0 +endif +endif + +include $(topsrcdir)/config/rules.mk + +ifeq (cocoa,$(MOZ_WIDGET_TOOLKIT)) +export:: + $(call py_action,preprocessor,-Fsubstitution -DMOZ_MACBUNDLE_ID='$(MOZ_MACBUNDLE_ID)' $(srcdir)/macbuild/Contents/Info.plist.in -o $(DIST)/bin/Info.plist) +libs:: + $(NSINSTALL) -D $(DIST)/bin/updater.app + rsync -a -C --exclude '*.in' $(srcdir)/macbuild/Contents $(DIST)/bin/updater.app + rsync -a -C $(DIST)/bin/Info.plist $(DIST)/bin/updater.app/Contents + $(call py_action,preprocessor,-Fsubstitution --output-encoding utf-16 -DAPP_NAME='$(MOZ_APP_DISPLAYNAME)' $(srcdir)/macbuild/Contents/Resources/English.lproj/InfoPlist.strings.in -o $(DIST)/bin/updater.app/Contents/Resources/English.lproj/InfoPlist.strings) + $(NSINSTALL) -D $(DIST)/bin/updater.app/Contents/MacOS + $(NSINSTALL) $(DIST)/bin/org.mozilla.updater $(DIST)/bin/updater.app/Contents/MacOS +endif diff --git a/toolkit/mozapps/update/updater/TsanOptions.cpp b/toolkit/mozapps/update/updater/TsanOptions.cpp new file mode 100644 index 0000000000..44a0b53afc --- /dev/null +++ b/toolkit/mozapps/update/updater/TsanOptions.cpp @@ -0,0 +1,23 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 "mozilla/Attributes.h" +#include "mozilla/TsanOptions.h" + +#ifndef _MSC_VER // Not supported by clang-cl yet + +// See also mozglue/build/TsanOptions.cpp before modifying this +extern "C" const char* __tsan_default_suppressions() { + // clang-format off + return "# Add your suppressions below\n" + + // External uninstrumented libraries + MOZ_TSAN_DEFAULT_EXTLIB_SUPPRESSIONS + + // End of suppressions. + ; // Please keep this semicolon. + // clang-format on +} +#endif // _MSC_VER diff --git a/toolkit/mozapps/update/updater/archivereader.cpp b/toolkit/mozapps/update/updater/archivereader.cpp new file mode 100644 index 0000000000..534ed2d176 --- /dev/null +++ b/toolkit/mozapps/update/updater/archivereader.cpp @@ -0,0 +1,344 @@ +/* -*- 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 <string.h> +#include <stdlib.h> +#include <fcntl.h> +#ifdef XP_WIN +# include <windows.h> +#endif +#include "archivereader.h" +#include "updatererrors.h" +#ifdef XP_WIN +# include "nsAlgorithm.h" // Needed by nsVersionComparator.cpp +# include "updatehelper.h" +#endif +#define XZ_USE_CRC64 +#include "xz.h" + +// These are generated at compile time based on the DER file for the channel +// being used +#ifdef MOZ_VERIFY_MAR_SIGNATURE +# ifdef TEST_UPDATER +# include "../xpcshellCert.h" +# elif DEP_UPDATER +# include "../dep1Cert.h" +# include "../dep2Cert.h" +# else +# include "primaryCert.h" +# include "secondaryCert.h" +# endif +#endif + +#define UPDATER_NO_STRING_GLUE_STL +#include "nsVersionComparator.cpp" +#undef UPDATER_NO_STRING_GLUE_STL + +#if defined(XP_WIN) +# include <io.h> +#endif + +/** + * Performs a verification on the opened MAR file with the passed in + * certificate name ID and type ID. + * + * @param archive The MAR file to verify the signature on. + * @param certData The certificate data. + * @return OK on success, CERT_VERIFY_ERROR on failure. + */ +template <uint32_t SIZE> +int VerifyLoadedCert(MarFile* archive, const uint8_t (&certData)[SIZE]) { + (void)archive; + (void)certData; + +#ifdef MOZ_VERIFY_MAR_SIGNATURE + const uint32_t size = SIZE; + const uint8_t* const data = &certData[0]; + if (mar_verify_signatures(archive, &data, &size, 1)) { + return CERT_VERIFY_ERROR; + } +#endif + + return OK; +} + +/** + * Performs a verification on the opened MAR file. Both the primary and backup + * keys stored are stored in the current process and at least the primary key + * will be tried. Success will be returned as long as one of the two + * signatures verify. + * + * @return OK on success + */ +int ArchiveReader::VerifySignature() { + if (!mArchive) { + return ARCHIVE_NOT_OPEN; + } + +#ifndef MOZ_VERIFY_MAR_SIGNATURE + return OK; +#else +# ifdef TEST_UPDATER + int rv = VerifyLoadedCert(mArchive, xpcshellCertData); +# elif DEP_UPDATER + int rv = VerifyLoadedCert(mArchive, dep1CertData); + if (rv != OK) { + rv = VerifyLoadedCert(mArchive, dep2CertData); + } +# else + int rv = VerifyLoadedCert(mArchive, primaryCertData); + if (rv != OK) { + rv = VerifyLoadedCert(mArchive, secondaryCertData); + } +# endif + return rv; +#endif +} + +/** + * Verifies that the MAR file matches the current product, channel, and version + * + * @param MARChannelID The MAR channel name to use, only updates from MARs + * with a matching MAR channel name will succeed. + * If an empty string is passed, no check will be done + * for the channel name in the product information block. + * If a comma separated list of values is passed then + * one value must match. + * @param appVersion The application version to use, only MARs with an + * application version >= to appVersion will be applied. + * @return OK on success + * COULD_NOT_READ_PRODUCT_INFO_BLOCK if the product info block + * could not be read. + * MARCHANNEL_MISMATCH_ERROR if update-settings.ini's MAR + * channel ID doesn't match the MAR + * file's MAR channel ID. + * VERSION_DOWNGRADE_ERROR if the application version for + * this updater is newer than the + * one in the MAR. + */ +int ArchiveReader::VerifyProductInformation(const char* MARChannelID, + const char* appVersion) { + if (!mArchive) { + return ARCHIVE_NOT_OPEN; + } + + ProductInformationBlock productInfoBlock; + int rv = mar_read_product_info_block(mArchive, &productInfoBlock); + if (rv != OK) { + return COULD_NOT_READ_PRODUCT_INFO_BLOCK_ERROR; + } + + // Only check the MAR channel name if specified, it should be passed in from + // the update-settings.ini file. + if (MARChannelID && strlen(MARChannelID)) { + // Check for at least one match in the comma separated list of values. + const char* delimiter = " ,\t"; + // Make a copy of the string in case a read only memory buffer + // was specified. strtok modifies the input buffer. + char channelCopy[512] = {0}; + strncpy(channelCopy, MARChannelID, sizeof(channelCopy) - 1); + char* channel = strtok(channelCopy, delimiter); + rv = MAR_CHANNEL_MISMATCH_ERROR; + while (channel) { + if (!strcmp(channel, productInfoBlock.MARChannelID)) { + rv = OK; + break; + } + channel = strtok(nullptr, delimiter); + } + } + + if (rv == OK) { + /* Compare both versions to ensure we don't have a downgrade + -1 if appVersion is older than productInfoBlock.productVersion + 1 if appVersion is newer than productInfoBlock.productVersion + 0 if appVersion is the same as productInfoBlock.productVersion + This even works with strings like: + - 12.0a1 being older than 12.0a2 + - 12.0a2 being older than 12.0b1 + - 12.0a1 being older than 12.0 + - 12.0 being older than 12.1a1 */ + int versionCompareResult = + mozilla::CompareVersions(appVersion, productInfoBlock.productVersion); + if (1 == versionCompareResult) { + rv = VERSION_DOWNGRADE_ERROR; + } + } + + free((void*)productInfoBlock.MARChannelID); + free((void*)productInfoBlock.productVersion); + return rv; +} + +int ArchiveReader::Open(const NS_tchar* path) { + if (mArchive) { + Close(); + } + + if (!mInBuf) { + mInBuf = (uint8_t*)malloc(mInBufSize); + if (!mInBuf) { + // Try again with a smaller buffer. + mInBufSize = 1024; + mInBuf = (uint8_t*)malloc(mInBufSize); + if (!mInBuf) { + return ARCHIVE_READER_MEM_ERROR; + } + } + } + + if (!mOutBuf) { + mOutBuf = (uint8_t*)malloc(mOutBufSize); + if (!mOutBuf) { + // Try again with a smaller buffer. + mOutBufSize = 1024; + mOutBuf = (uint8_t*)malloc(mOutBufSize); + if (!mOutBuf) { + return ARCHIVE_READER_MEM_ERROR; + } + } + } + +#ifdef XP_WIN + mArchive = mar_wopen(path); +#else + mArchive = mar_open(path); +#endif + if (!mArchive) { + return READ_ERROR; + } + + xz_crc32_init(); + xz_crc64_init(); + + return OK; +} + +void ArchiveReader::Close() { + if (mArchive) { + mar_close(mArchive); + mArchive = nullptr; + } + + if (mInBuf) { + free(mInBuf); + mInBuf = nullptr; + } + + if (mOutBuf) { + free(mOutBuf); + mOutBuf = nullptr; + } +} + +int ArchiveReader::ExtractFile(const char* name, const NS_tchar* dest) { + const MarItem* item = mar_find_item(mArchive, name); + if (!item) { + return READ_ERROR; + } + +#ifdef XP_WIN + FILE* fp = _wfopen(dest, L"wb+"); +#else + int fd = creat(dest, item->flags); + if (fd == -1) { + return WRITE_ERROR; + } + + FILE* fp = fdopen(fd, "wb"); +#endif + if (!fp) { + return WRITE_ERROR; + } + + int rv = ExtractItemToStream(item, fp); + + fclose(fp); + return rv; +} + +int ArchiveReader::ExtractFileToStream(const char* name, FILE* fp) { + const MarItem* item = mar_find_item(mArchive, name); + if (!item) { + return READ_ERROR; + } + + return ExtractItemToStream(item, fp); +} + +int ArchiveReader::ExtractItemToStream(const MarItem* item, FILE* fp) { + /* decompress the data chunk by chunk */ + + int offset, inlen, ret = OK; + struct xz_buf strm = {0}; + enum xz_ret xz_rv = XZ_OK; + + struct xz_dec* dec = xz_dec_init(XZ_DYNALLOC, 64 * 1024 * 1024); + if (!dec) { + return UNEXPECTED_XZ_ERROR; + } + + strm.in = mInBuf; + strm.in_pos = 0; + strm.in_size = 0; + strm.out = mOutBuf; + strm.out_pos = 0; + strm.out_size = mOutBufSize; + + offset = 0; + for (;;) { + if (!item->length) { + ret = UNEXPECTED_MAR_ERROR; + break; + } + + if (offset < (int)item->length && strm.in_pos == strm.in_size) { + inlen = mar_read(mArchive, item, offset, mInBuf, mInBufSize); + if (inlen <= 0) { + ret = READ_ERROR; + break; + } + offset += inlen; + strm.in_size = inlen; + strm.in_pos = 0; + } + + xz_rv = xz_dec_run(dec, &strm); + + if (strm.out_pos == mOutBufSize) { + if (fwrite(mOutBuf, 1, strm.out_pos, fp) != strm.out_pos) { + ret = WRITE_ERROR_EXTRACT; + break; + } + + strm.out_pos = 0; + } + + if (xz_rv == XZ_OK) { + // There is still more data to decompress. + continue; + } + + // The return value of xz_dec_run is not XZ_OK and if it isn't XZ_STREAM_END + // an error has occured. + if (xz_rv != XZ_STREAM_END) { + ret = UNEXPECTED_XZ_ERROR; + break; + } + + // Write out the remainder of the decompressed data. In the case of + // strm.out_pos == 0 this is needed to create empty files included in the + // mar file. + if (fwrite(mOutBuf, 1, strm.out_pos, fp) != strm.out_pos) { + ret = WRITE_ERROR_EXTRACT; + } + break; + } + + xz_dec_end(dec); + return ret; +} diff --git a/toolkit/mozapps/update/updater/archivereader.h b/toolkit/mozapps/update/updater/archivereader.h new file mode 100644 index 0000000000..1f18327f1d --- /dev/null +++ b/toolkit/mozapps/update/updater/archivereader.h @@ -0,0 +1,46 @@ +/* -*- 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 ArchiveReader_h__ +#define ArchiveReader_h__ + +#include <stdio.h> +#include "mar.h" + +#ifdef XP_WIN +# include <windows.h> + +typedef WCHAR NS_tchar; +#else +typedef char NS_tchar; +#endif + +// This class provides an API to extract files from an update archive. +class ArchiveReader { + public: + ArchiveReader() = default; + ~ArchiveReader() { Close(); } + + int Open(const NS_tchar* path); + int VerifySignature(); + int VerifyProductInformation(const char* MARChannelID, + const char* appVersion); + void Close(); + + int ExtractFile(const char* item, const NS_tchar* destination); + int ExtractFileToStream(const char* item, FILE* fp); + + private: + int ExtractItemToStream(const MarItem* item, FILE* fp); + + MarFile* mArchive = nullptr; + uint8_t* mInBuf = nullptr; + uint8_t* mOutBuf = nullptr; + size_t mInBufSize = 262144; + size_t mOutBufSize = 262144; +}; + +#endif // ArchiveReader_h__ diff --git a/toolkit/mozapps/update/updater/autograph_stage.pem b/toolkit/mozapps/update/updater/autograph_stage.pem new file mode 100644 index 0000000000..bd8513a04d --- /dev/null +++ b/toolkit/mozapps/update/updater/autograph_stage.pem @@ -0,0 +1,14 @@ +-----BEGIN PUBLIC KEY----- +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAvN249lqptaK9L7VltsDt +6X/Hik/iqHSdJMwAWOoB8exyUvura0VMZhYNGCl046zKnE9aX5aMk4s4MJX0Kw9Q +KofWUZ+hni18gyXFjecg6AyuEiMAJpSDknWnkZ1hucXTLNpwXwRHPW5YHIinKidz +kTCREsZl0IU+gieEYXziQ4eBvc9eSNnprKhN/00AxHlmwCtY+3HLso9PYptcOspf +yuQC/PKLwBb6hqcwEoHsT0w1roRRSACZCHfJYtzXteW7uY3NcUOrSlWFMtZguXuO +K0U/OJaVnfcJ6REB9HTAzgmL54QlXlGTge8Vj+XMx4GqZD1fuM7rctIFclSL/wWi +tq8MOedINL2lj2YKB8ArU2kWmi+v7HLcS94WHHcGsBh7SrNRZQEfiMBKrHHW+mqO +xRRbyR3zAn6M78UOFqMboEQWzWHKFNhw8VI1CA8maylNuArAZhJzdLvUUo2IuQQo +floKjdeooezDYBeeeJXOcGUv3VrulIuL3MA5k1l+c6uBX7NFWX8ukBTG09b3sNP+ +iH4P2AIcKoccxFpjswlUZCnSKF0jRu1Ue+IulHDNzora8WDOqK0IsfNfZMNyykGf +8WsELSO3m4CxXuCbY8hmm67dTQ5DKYf874GUm7yOCe2u4piRSJ20eA4WguwxmEIj +96Kk7NgCLtRU3G754oOTksUCAwEAAQ== +-----END PUBLIC KEY----- diff --git a/toolkit/mozapps/update/updater/bspatch/LICENSE b/toolkit/mozapps/update/updater/bspatch/LICENSE new file mode 100644 index 0000000000..f2521d71cd --- /dev/null +++ b/toolkit/mozapps/update/updater/bspatch/LICENSE @@ -0,0 +1,23 @@ +Copyright 2003,2004 Colin Percival +All rights reserved + +Redistribution and use in source and binary forms, with or without +modification, are permitted providing that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING +IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/toolkit/mozapps/update/updater/bspatch/bspatch.cpp b/toolkit/mozapps/update/updater/bspatch/bspatch.cpp new file mode 100644 index 0000000000..8eabd0e427 --- /dev/null +++ b/toolkit/mozapps/update/updater/bspatch/bspatch.cpp @@ -0,0 +1,216 @@ +/*- + * Copyright 2003,2004 Colin Percival + * All rights reserved + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted providing that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING + * IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * Changelog: + * 2005-04-26 - Define the header as a C structure, add a CRC32 checksum to + * the header, and make all the types 32-bit. + * --Benjamin Smedberg <benjamin@smedbergs.us> + */ + +#include "bspatch.h" +#include "updatererrors.h" + +#include <sys/stat.h> +#include <stdlib.h> +#include <stdio.h> +#include <string.h> +#include <limits.h> + +#if defined(XP_WIN) +# include <io.h> +#endif + +#ifdef XP_WIN +# include <winsock2.h> +#else +# include <arpa/inet.h> +#endif + +#ifndef SSIZE_MAX +# define SSIZE_MAX LONG_MAX +#endif + +int MBS_ReadHeader(FILE* file, MBSPatchHeader* header) { + size_t s = fread(header, 1, sizeof(MBSPatchHeader), file); + if (s != sizeof(MBSPatchHeader)) { + return READ_ERROR; + } + + header->slen = ntohl(header->slen); + header->scrc32 = ntohl(header->scrc32); + header->dlen = ntohl(header->dlen); + header->cblen = ntohl(header->cblen); + header->difflen = ntohl(header->difflen); + header->extralen = ntohl(header->extralen); + + struct stat hs; + s = fstat(fileno(file), &hs); + if (s != 0) { + return READ_ERROR; + } + + if (memcmp(header->tag, "MBDIFF10", 8) != 0) { + return UNEXPECTED_BSPATCH_ERROR; + } + + if (hs.st_size > INT_MAX) { + return UNEXPECTED_BSPATCH_ERROR; + } + + size_t size = static_cast<size_t>(hs.st_size); + if (size < sizeof(MBSPatchHeader)) { + return UNEXPECTED_BSPATCH_ERROR; + } + size -= sizeof(MBSPatchHeader); + + if (size < header->cblen) { + return UNEXPECTED_BSPATCH_ERROR; + } + size -= header->cblen; + + if (size < header->difflen) { + return UNEXPECTED_BSPATCH_ERROR; + } + size -= header->difflen; + + if (size < header->extralen) { + return UNEXPECTED_BSPATCH_ERROR; + } + size -= header->extralen; + + if (size != 0) { + return UNEXPECTED_BSPATCH_ERROR; + } + + return OK; +} + +int MBS_ApplyPatch(const MBSPatchHeader* header, FILE* patchFile, + unsigned char* fbuffer, FILE* file) { + unsigned char* fbufstart = fbuffer; + unsigned char* fbufend = fbuffer + header->slen; + + unsigned char* buf = (unsigned char*)malloc(header->cblen + header->difflen + + header->extralen); + if (!buf) { + return BSPATCH_MEM_ERROR; + } + + int rv = OK; + + size_t r = header->cblen + header->difflen + header->extralen; + unsigned char* wb = buf; + while (r) { + const size_t count = (r > SSIZE_MAX) ? SSIZE_MAX : r; + size_t c = fread(wb, 1, count, patchFile); + if (c != count) { + rv = READ_ERROR; + goto end; + } + + r -= c; + wb += c; + + if (c == 0 && r) { + rv = UNEXPECTED_BSPATCH_ERROR; + goto end; + } + } + + { + MBSPatchTriple* ctrlsrc = (MBSPatchTriple*)buf; + if (header->cblen % sizeof(MBSPatchTriple) != 0) { + rv = UNEXPECTED_BSPATCH_ERROR; + goto end; + } + + unsigned char* diffsrc = buf + header->cblen; + unsigned char* extrasrc = diffsrc + header->difflen; + + MBSPatchTriple* ctrlend = (MBSPatchTriple*)diffsrc; + unsigned char* diffend = extrasrc; + unsigned char* extraend = extrasrc + header->extralen; + + while (ctrlsrc < ctrlend) { + ctrlsrc->x = ntohl(ctrlsrc->x); + ctrlsrc->y = ntohl(ctrlsrc->y); + ctrlsrc->z = ntohl(ctrlsrc->z); + +#ifdef DEBUG_bsmedberg + printf( + "Applying block:\n" + " x: %u\n" + " y: %u\n" + " z: %i\n", + ctrlsrc->x, ctrlsrc->y, ctrlsrc->z); +#endif + + /* Add x bytes from oldfile to x bytes from the diff block */ + + if (ctrlsrc->x > static_cast<size_t>(fbufend - fbuffer) || + ctrlsrc->x > static_cast<size_t>(diffend - diffsrc)) { + rv = UNEXPECTED_BSPATCH_ERROR; + goto end; + } + for (uint32_t i = 0; i < ctrlsrc->x; ++i) { + diffsrc[i] += fbuffer[i]; + } + if ((uint32_t)fwrite(diffsrc, 1, ctrlsrc->x, file) != ctrlsrc->x) { + rv = WRITE_ERROR_PATCH_FILE; + goto end; + } + fbuffer += ctrlsrc->x; + diffsrc += ctrlsrc->x; + + /* Copy y bytes from the extra block */ + + if (ctrlsrc->y > static_cast<size_t>(extraend - extrasrc)) { + rv = UNEXPECTED_BSPATCH_ERROR; + goto end; + } + if ((uint32_t)fwrite(extrasrc, 1, ctrlsrc->y, file) != ctrlsrc->y) { + rv = WRITE_ERROR_PATCH_FILE; + goto end; + } + extrasrc += ctrlsrc->y; + + /* "seek" forwards in oldfile by z bytes */ + + if (ctrlsrc->z < fbufstart - fbuffer || ctrlsrc->z > fbufend - fbuffer) { + rv = UNEXPECTED_BSPATCH_ERROR; + goto end; + } + fbuffer += ctrlsrc->z; + + /* and on to the next control block */ + + ++ctrlsrc; + } + } + +end: + free(buf); + return rv; +} diff --git a/toolkit/mozapps/update/updater/bspatch/bspatch.h b/toolkit/mozapps/update/updater/bspatch/bspatch.h new file mode 100644 index 0000000000..189e618557 --- /dev/null +++ b/toolkit/mozapps/update/updater/bspatch/bspatch.h @@ -0,0 +1,93 @@ +/*- + * Copyright 2003,2004 Colin Percival + * All rights reserved + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted providing that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING + * IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * Changelog: + * 2005-04-26 - Define the header as a C structure, add a CRC32 checksum to + * the header, and make all the types 32-bit. + * --Benjamin Smedberg <benjamin@smedbergs.us> + */ + +#ifndef bspatch_h__ +#define bspatch_h__ + +#include <stdint.h> +#include <stdio.h> + +typedef struct MBSPatchHeader_ { + /* "MBDIFF10" */ + char tag[8]; + + /* Length of the file to be patched */ + uint32_t slen; + + /* CRC32 of the file to be patched */ + uint32_t scrc32; + + /* Length of the result file */ + uint32_t dlen; + + /* Length of the control block in bytes */ + uint32_t cblen; + + /* Length of the diff block in bytes */ + uint32_t difflen; + + /* Length of the extra block in bytes */ + uint32_t extralen; + + /* Control block (MBSPatchTriple[]) */ + /* Diff block (binary data) */ + /* Extra block (binary data) */ +} MBSPatchHeader; + +/** + * Read the header of a patch file into the MBSPatchHeader structure. + * + * @param fd Must have been opened for reading, and be at the beginning + * of the file. + */ +int MBS_ReadHeader(FILE* file, MBSPatchHeader* header); + +/** + * Apply a patch. This method does not validate the checksum of the original + * file: client code should validate the checksum before calling this method. + * + * @param patchfd Must have been processed by MBS_ReadHeader + * @param fbuffer The original file read into a memory buffer of length + * header->slen. + * @param filefd Must have been opened for writing. Should be truncated + * to header->dlen if it is an existing file. The offset + * should be at the beginning of the file. + */ +int MBS_ApplyPatch(const MBSPatchHeader* header, FILE* patchFile, + unsigned char* fbuffer, FILE* file); + +typedef struct MBSPatchTriple_ { + uint32_t x; /* add x bytes from oldfile to x bytes from the diff block */ + uint32_t y; /* copy y bytes from the extra block */ + int32_t z; /* seek forwards in oldfile by z bytes */ +} MBSPatchTriple; + +#endif // bspatch_h__ diff --git a/toolkit/mozapps/update/updater/bspatch/moz.build b/toolkit/mozapps/update/updater/bspatch/moz.build new file mode 100644 index 0000000000..46c6d11fad --- /dev/null +++ b/toolkit/mozapps/update/updater/bspatch/moz.build @@ -0,0 +1,22 @@ +# -*- 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/. + +if CONFIG["OS_ARCH"] == "WINNT": + USE_STATIC_LIBS = True + +EXPORTS += [ + "bspatch.h", +] + +SOURCES += [ + "bspatch.cpp", +] + +USE_LIBS += [ + "updatecommon", +] + +Library("bspatch") diff --git a/toolkit/mozapps/update/updater/bspatch/moz.yaml b/toolkit/mozapps/update/updater/bspatch/moz.yaml new file mode 100644 index 0000000000..ce611f97a1 --- /dev/null +++ b/toolkit/mozapps/update/updater/bspatch/moz.yaml @@ -0,0 +1,30 @@ +# Version of this schema +schema: 1 + +bugzilla: + # Bugzilla product and component for this directory and subdirectories + product: "Toolkit" + component: "Application Update" + +# The source from this directory was adapted from Colin Percival's bspatch +# tool in mid 2005 and was obtained from bsdiff version 4.2. Edits were +# later added by the Chromium dev team and were copied to here as well + +# Document the source of externally hosted code +origin: + name: "bsdiff/bspatch" + description: "Builds and applies patches to binary files" + + # Full URL for the package's homepage/etc + # Usually different from repository url + url: "https://www.daemonology.net/bsdiff/bsdiff-4.2.tar.gz" + + # Human-readable identifier for this version/release + # Generally "version NNN", "tag SSS", "bookmark SSS" + release: "version 4.2" + + # The package's license, where possible using the mnemonic from + # https://spdx.org/licenses/ + # Multiple licenses can be specified (as a YAML list) + # A "LICENSE" file must exist containing the full license text + license: "BSD-2-Clause" diff --git a/toolkit/mozapps/update/updater/crctable.h b/toolkit/mozapps/update/updater/crctable.h new file mode 100644 index 0000000000..dadabcc9a1 --- /dev/null +++ b/toolkit/mozapps/update/updater/crctable.h @@ -0,0 +1,71 @@ +/** + * This file was part of bzip2/libbzip2. + * We extracted only this table for bzip2 crc comptability + */ + +/** + I think this is an implementation of the AUTODIN-II, + Ethernet & FDDI 32-bit CRC standard. Vaguely derived + from code by Rob Warnock, in Section 51 of the + comp.compression FAQ. +*/ +unsigned int BZ2_crc32Table[256] = { + + /*-- Ugly, innit? --*/ + + 0x00000000L, 0x04c11db7L, 0x09823b6eL, 0x0d4326d9L, 0x130476dcL, + 0x17c56b6bL, 0x1a864db2L, 0x1e475005L, 0x2608edb8L, 0x22c9f00fL, + 0x2f8ad6d6L, 0x2b4bcb61L, 0x350c9b64L, 0x31cd86d3L, 0x3c8ea00aL, + 0x384fbdbdL, 0x4c11db70L, 0x48d0c6c7L, 0x4593e01eL, 0x4152fda9L, + 0x5f15adacL, 0x5bd4b01bL, 0x569796c2L, 0x52568b75L, 0x6a1936c8L, + 0x6ed82b7fL, 0x639b0da6L, 0x675a1011L, 0x791d4014L, 0x7ddc5da3L, + 0x709f7b7aL, 0x745e66cdL, 0x9823b6e0L, 0x9ce2ab57L, 0x91a18d8eL, + 0x95609039L, 0x8b27c03cL, 0x8fe6dd8bL, 0x82a5fb52L, 0x8664e6e5L, + 0xbe2b5b58L, 0xbaea46efL, 0xb7a96036L, 0xb3687d81L, 0xad2f2d84L, + 0xa9ee3033L, 0xa4ad16eaL, 0xa06c0b5dL, 0xd4326d90L, 0xd0f37027L, + 0xddb056feL, 0xd9714b49L, 0xc7361b4cL, 0xc3f706fbL, 0xceb42022L, + 0xca753d95L, 0xf23a8028L, 0xf6fb9d9fL, 0xfbb8bb46L, 0xff79a6f1L, + 0xe13ef6f4L, 0xe5ffeb43L, 0xe8bccd9aL, 0xec7dd02dL, 0x34867077L, + 0x30476dc0L, 0x3d044b19L, 0x39c556aeL, 0x278206abL, 0x23431b1cL, + 0x2e003dc5L, 0x2ac12072L, 0x128e9dcfL, 0x164f8078L, 0x1b0ca6a1L, + 0x1fcdbb16L, 0x018aeb13L, 0x054bf6a4L, 0x0808d07dL, 0x0cc9cdcaL, + 0x7897ab07L, 0x7c56b6b0L, 0x71159069L, 0x75d48ddeL, 0x6b93dddbL, + 0x6f52c06cL, 0x6211e6b5L, 0x66d0fb02L, 0x5e9f46bfL, 0x5a5e5b08L, + 0x571d7dd1L, 0x53dc6066L, 0x4d9b3063L, 0x495a2dd4L, 0x44190b0dL, + 0x40d816baL, 0xaca5c697L, 0xa864db20L, 0xa527fdf9L, 0xa1e6e04eL, + 0xbfa1b04bL, 0xbb60adfcL, 0xb6238b25L, 0xb2e29692L, 0x8aad2b2fL, + 0x8e6c3698L, 0x832f1041L, 0x87ee0df6L, 0x99a95df3L, 0x9d684044L, + 0x902b669dL, 0x94ea7b2aL, 0xe0b41de7L, 0xe4750050L, 0xe9362689L, + 0xedf73b3eL, 0xf3b06b3bL, 0xf771768cL, 0xfa325055L, 0xfef34de2L, + 0xc6bcf05fL, 0xc27dede8L, 0xcf3ecb31L, 0xcbffd686L, 0xd5b88683L, + 0xd1799b34L, 0xdc3abdedL, 0xd8fba05aL, 0x690ce0eeL, 0x6dcdfd59L, + 0x608edb80L, 0x644fc637L, 0x7a089632L, 0x7ec98b85L, 0x738aad5cL, + 0x774bb0ebL, 0x4f040d56L, 0x4bc510e1L, 0x46863638L, 0x42472b8fL, + 0x5c007b8aL, 0x58c1663dL, 0x558240e4L, 0x51435d53L, 0x251d3b9eL, + 0x21dc2629L, 0x2c9f00f0L, 0x285e1d47L, 0x36194d42L, 0x32d850f5L, + 0x3f9b762cL, 0x3b5a6b9bL, 0x0315d626L, 0x07d4cb91L, 0x0a97ed48L, + 0x0e56f0ffL, 0x1011a0faL, 0x14d0bd4dL, 0x19939b94L, 0x1d528623L, + 0xf12f560eL, 0xf5ee4bb9L, 0xf8ad6d60L, 0xfc6c70d7L, 0xe22b20d2L, + 0xe6ea3d65L, 0xeba91bbcL, 0xef68060bL, 0xd727bbb6L, 0xd3e6a601L, + 0xdea580d8L, 0xda649d6fL, 0xc423cd6aL, 0xc0e2d0ddL, 0xcda1f604L, + 0xc960ebb3L, 0xbd3e8d7eL, 0xb9ff90c9L, 0xb4bcb610L, 0xb07daba7L, + 0xae3afba2L, 0xaafbe615L, 0xa7b8c0ccL, 0xa379dd7bL, 0x9b3660c6L, + 0x9ff77d71L, 0x92b45ba8L, 0x9675461fL, 0x8832161aL, 0x8cf30badL, + 0x81b02d74L, 0x857130c3L, 0x5d8a9099L, 0x594b8d2eL, 0x5408abf7L, + 0x50c9b640L, 0x4e8ee645L, 0x4a4ffbf2L, 0x470cdd2bL, 0x43cdc09cL, + 0x7b827d21L, 0x7f436096L, 0x7200464fL, 0x76c15bf8L, 0x68860bfdL, + 0x6c47164aL, 0x61043093L, 0x65c52d24L, 0x119b4be9L, 0x155a565eL, + 0x18197087L, 0x1cd86d30L, 0x029f3d35L, 0x065e2082L, 0x0b1d065bL, + 0x0fdc1becL, 0x3793a651L, 0x3352bbe6L, 0x3e119d3fL, 0x3ad08088L, + 0x2497d08dL, 0x2056cd3aL, 0x2d15ebe3L, 0x29d4f654L, 0xc5a92679L, + 0xc1683bceL, 0xcc2b1d17L, 0xc8ea00a0L, 0xd6ad50a5L, 0xd26c4d12L, + 0xdf2f6bcbL, 0xdbee767cL, 0xe3a1cbc1L, 0xe760d676L, 0xea23f0afL, + 0xeee2ed18L, 0xf0a5bd1dL, 0xf464a0aaL, 0xf9278673L, 0xfde69bc4L, + 0x89b8fd09L, 0x8d79e0beL, 0x803ac667L, 0x84fbdbd0L, 0x9abc8bd5L, + 0x9e7d9662L, 0x933eb0bbL, 0x97ffad0cL, 0xafb010b1L, 0xab710d06L, + 0xa6322bdfL, 0xa2f33668L, 0xbcb4666dL, 0xb8757bdaL, 0xb5365d03L, + 0xb1f740b4L}; + +/*-------------------------------------------------------------*/ +/*--- end crctable.h ---*/ +/*-------------------------------------------------------------*/ diff --git a/toolkit/mozapps/update/updater/dep1.der b/toolkit/mozapps/update/updater/dep1.der Binary files differnew file mode 100644 index 0000000000..655c2d10d4 --- /dev/null +++ b/toolkit/mozapps/update/updater/dep1.der diff --git a/toolkit/mozapps/update/updater/dep2.der b/toolkit/mozapps/update/updater/dep2.der Binary files differnew file mode 100644 index 0000000000..c59ac7f790 --- /dev/null +++ b/toolkit/mozapps/update/updater/dep2.der diff --git a/toolkit/mozapps/update/updater/gen_cert_header.py b/toolkit/mozapps/update/updater/gen_cert_header.py new file mode 100644 index 0000000000..da78cad674 --- /dev/null +++ b/toolkit/mozapps/update/updater/gen_cert_header.py @@ -0,0 +1,27 @@ +# 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/. + +import os + + +def file_byte_generator(filename, block_size=512): + with open(filename, "rb") as f: + while True: + block = f.read(block_size) + if block: + for byte in block: + yield byte + else: + break + + +def create_header(out_fh, in_filename): + assert out_fh.name.endswith(".h") + array_name = os.path.basename(out_fh.name)[:-2] + "Data" + hexified = ["0x%02x" % byte for byte in file_byte_generator(in_filename)] + + print("const uint8_t " + array_name + "[] = {", file=out_fh) + print(", ".join(hexified), file=out_fh) + print("};", file=out_fh) + return 0 diff --git a/toolkit/mozapps/update/updater/launchchild_osx.mm b/toolkit/mozapps/update/updater/launchchild_osx.mm new file mode 100644 index 0000000000..3d28a064c7 --- /dev/null +++ b/toolkit/mozapps/update/updater/launchchild_osx.mm @@ -0,0 +1,500 @@ +/* -*- 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 <Cocoa/Cocoa.h> +#include <CoreServices/CoreServices.h> +#include <crt_externs.h> +#include <stdlib.h> +#include <stdio.h> +#include <spawn.h> +#include <SystemConfiguration/SystemConfiguration.h> +#include <sys/types.h> +#include <sys/sysctl.h> +#include "readstrings.h" + +#define ARCH_PATH "/usr/bin/arch" +#if defined(__x86_64__) +// Work around the fact that this constant is not available in the macOS SDK +# define kCFBundleExecutableArchitectureARM64 0x0100000c +#endif + +class MacAutoreleasePool { + public: + MacAutoreleasePool() { mPool = [[NSAutoreleasePool alloc] init]; } + ~MacAutoreleasePool() { [mPool release]; } + + private: + NSAutoreleasePool* mPool; +}; + +#if defined(__x86_64__) +/* + * Returns true if the process is running under Rosetta translation. Returns + * false if running natively or if an error was encountered. We use the + * `sysctl.proc_translated` sysctl which is documented by Apple to be used + * for this purpose. + */ +bool IsProcessRosettaTranslated() { + int ret = 0; + size_t size = sizeof(ret); + if (sysctlbyname("sysctl.proc_translated", &ret, &size, NULL, 0) == -1) { + if (errno != ENOENT) { + fprintf(stderr, "Failed to check for translation environment\n"); + } + return false; + } + return (ret == 1); +} + +// Returns true if the binary at |executablePath| can be executed natively +// on an arm64 Mac. Returns false otherwise or if an error occurred. +bool IsBinaryArmExecutable(const char* executablePath) { + bool isArmExecutable = false; + + CFURLRef url = ::CFURLCreateFromFileSystemRepresentation( + kCFAllocatorDefault, (const UInt8*)executablePath, strlen(executablePath), false); + if (!url) { + return false; + } + + CFArrayRef archs = ::CFBundleCopyExecutableArchitecturesForURL(url); + if (!archs) { + CFRelease(url); + return false; + } + + CFIndex archCount = ::CFArrayGetCount(archs); + for (CFIndex i = 0; i < archCount; i++) { + CFNumberRef currentArch = static_cast<CFNumberRef>(::CFArrayGetValueAtIndex(archs, i)); + int currentArchInt = 0; + if (!::CFNumberGetValue(currentArch, kCFNumberIntType, ¤tArchInt)) { + continue; + } + if (currentArchInt == kCFBundleExecutableArchitectureARM64) { + isArmExecutable = true; + break; + } + } + + CFRelease(url); + CFRelease(archs); + + return isArmExecutable; +} + +// Returns true if the executable provided in |executablePath| should be +// launched with a preference for arm64. After updating from an x64 version +// running under Rosetta, if the update is to a universal binary with arm64 +// support we want to switch to arm64 execution mode. Returns true if those +// conditions are met and the arch(1) utility at |archPath| is executable. +// It should be safe to always launch with arch and fallback to x64, but we +// limit its use to the only scenario it is necessary to minimize risk. +bool ShouldPreferArmLaunch(const char* archPath, const char* executablePath) { + // If not running under Rosetta, we are not on arm64 hardware. + if (!IsProcessRosettaTranslated()) { + return false; + } + + // Ensure the arch(1) utility is present and executable. + NSFileManager* fileMgr = [NSFileManager defaultManager]; + NSString* archPathString = [NSString stringWithUTF8String:archPath]; + if (![fileMgr isExecutableFileAtPath:archPathString]) { + return false; + } + + // Ensure the binary can be run natively on arm64. + return IsBinaryArmExecutable(executablePath); +} +#endif // __x86_64__ + +void LaunchChild(int argc, const char** argv) { + MacAutoreleasePool pool; + + @try { + bool preferArmLaunch = false; + +#if defined(__x86_64__) + // When running under Rosetta, child processes inherit the architecture + // preference of their parent and therefore universal binaries launched + // by an emulated x64 process will launch in x64 mode. If we are running + // under Rosetta, launch the child process with a preference for arm64 so + // that we will switch to arm64 execution if we have just updated from + // x64 to a universal build. This includes if we were already a universal + // build and the user is intentionally running under Rosetta. + preferArmLaunch = ShouldPreferArmLaunch(ARCH_PATH, argv[0]); +#endif // __x86_64__ + + NSString* launchPath; + NSMutableArray* arguments; + + if (preferArmLaunch) { + launchPath = [NSString stringWithUTF8String:ARCH_PATH]; + + // Size the arguments array to include all the arguments + // in |argv| plus two arguments to pass to the arch(1) utility. + arguments = [NSMutableArray arrayWithCapacity:argc + 2]; + [arguments addObject:[NSString stringWithUTF8String:"-arm64"]]; + [arguments addObject:[NSString stringWithUTF8String:"-x86_64"]]; + + // Add the first argument from |argv|. The rest are added below. + [arguments addObject:[NSString stringWithUTF8String:argv[0]]]; + } else { + launchPath = [NSString stringWithUTF8String:argv[0]]; + arguments = [NSMutableArray arrayWithCapacity:argc - 1]; + } + + for (int i = 1; i < argc; i++) { + [arguments addObject:[NSString stringWithUTF8String:argv[i]]]; + } + [NSTask launchedTaskWithLaunchPath:launchPath arguments:arguments]; + } @catch (NSException* e) { + NSLog(@"%@: %@", e.name, e.reason); + } +} + +void LaunchMacPostProcess(const char* aAppBundle) { + MacAutoreleasePool pool; + + // Launch helper to perform post processing for the update; this is the Mac + // analogue of LaunchWinPostProcess (PostUpdateWin). + NSString* iniPath = [NSString stringWithUTF8String:aAppBundle]; + iniPath = [iniPath stringByAppendingPathComponent:@"Contents/Resources/updater.ini"]; + + NSFileManager* fileManager = [NSFileManager defaultManager]; + if (![fileManager fileExistsAtPath:iniPath]) { + // the file does not exist; there is nothing to run + return; + } + + int readResult; + mozilla::UniquePtr<char[]> values[2]; + readResult = + ReadStrings([iniPath UTF8String], "ExeRelPath\0ExeArg\0", 2, values, "PostUpdateMac"); + if (readResult) { + return; + } + + NSString* exeRelPath = [NSString stringWithUTF8String:values[0].get()]; + NSString* exeArg = [NSString stringWithUTF8String:values[1].get()]; + if (!exeArg || !exeRelPath) { + return; + } + + // The path must not traverse directories and it must be a relative path. + if ([exeRelPath isEqualToString:@".."] || [exeRelPath hasPrefix:@"/"] || + [exeRelPath hasPrefix:@"../"] || [exeRelPath hasSuffix:@"/.."] || + [exeRelPath containsString:@"/../"]) { + return; + } + + NSString* exeFullPath = [NSString stringWithUTF8String:aAppBundle]; + exeFullPath = [exeFullPath stringByAppendingPathComponent:exeRelPath]; + + mozilla::UniquePtr<char[]> optVal; + readResult = ReadStrings([iniPath UTF8String], "ExeAsync\0", 1, &optVal, "PostUpdateMac"); + + NSTask* task = [[NSTask alloc] init]; + [task setLaunchPath:exeFullPath]; + [task setArguments:[NSArray arrayWithObject:exeArg]]; + [task launch]; + if (!readResult) { + NSString* exeAsync = [NSString stringWithUTF8String:optVal.get()]; + if ([exeAsync isEqualToString:@"false"]) { + [task waitUntilExit]; + } + } + // ignore the return value of the task, there's nothing we can do with it + [task release]; +} + +id ConnectToUpdateServer() { + MacAutoreleasePool pool; + + id updateServer = nil; + BOOL isConnected = NO; + int currTry = 0; + const int numRetries = 10; // Number of IPC connection retries before + // giving up. + while (!isConnected && currTry < numRetries) { + @try { + updateServer = (id)[NSConnection + rootProxyForConnectionWithRegisteredName:@"org.mozilla.updater.server" + host:nil + usingNameServer:[NSSocketPortNameServer sharedInstance]]; + if (!updateServer || ![updateServer respondsToSelector:@selector(abort)] || + ![updateServer respondsToSelector:@selector(getArguments)] || + ![updateServer respondsToSelector:@selector(shutdown)]) { + NSLog(@"Server doesn't exist or doesn't provide correct selectors."); + sleep(1); // Wait 1 second. + currTry++; + } else { + isConnected = YES; + } + } @catch (NSException* e) { + NSLog(@"Encountered exception, retrying: %@: %@", e.name, e.reason); + sleep(1); // Wait 1 second. + currTry++; + } + } + if (!isConnected) { + NSLog(@"Failed to connect to update server after several retries."); + return nil; + } + return updateServer; +} + +void CleanupElevatedMacUpdate(bool aFailureOccurred) { + MacAutoreleasePool pool; + + id updateServer = ConnectToUpdateServer(); + if (updateServer) { + @try { + if (aFailureOccurred) { + [updateServer performSelector:@selector(abort)]; + } else { + [updateServer performSelector:@selector(shutdown)]; + } + } @catch (NSException* e) { + } + } + + NSFileManager* manager = [NSFileManager defaultManager]; + [manager removeItemAtPath:@"/Library/PrivilegedHelperTools/org.mozilla.updater" error:nil]; + [manager removeItemAtPath:@"/Library/LaunchDaemons/org.mozilla.updater.plist" error:nil]; + const char* launchctlArgs[] = {"/bin/launchctl", "remove", "org.mozilla.updater"}; + // The following call will terminate the current process due to the "remove" + // argument in launchctlArgs. + LaunchChild(3, launchctlArgs); +} + +// Note: Caller is responsible for freeing argv. +bool ObtainUpdaterArguments(int* argc, char*** argv) { + MacAutoreleasePool pool; + + id updateServer = ConnectToUpdateServer(); + if (!updateServer) { + // Let's try our best and clean up. + CleanupElevatedMacUpdate(true); + return false; // Won't actually get here due to CleanupElevatedMacUpdate. + } + + @try { + NSArray* updaterArguments = [updateServer performSelector:@selector(getArguments)]; + *argc = [updaterArguments count]; + char** tempArgv = (char**)malloc(sizeof(char*) * (*argc)); + for (int i = 0; i < *argc; i++) { + int argLen = [[updaterArguments objectAtIndex:i] length] + 1; + tempArgv[i] = (char*)malloc(argLen); + strncpy(tempArgv[i], [[updaterArguments objectAtIndex:i] UTF8String], argLen); + } + *argv = tempArgv; + } @catch (NSException* e) { + // Let's try our best and clean up. + CleanupElevatedMacUpdate(true); + return false; // Won't actually get here due to CleanupElevatedMacUpdate. + } + return true; +} + +/** + * The ElevatedUpdateServer is launched from a non-elevated updater process. + * It allows an elevated updater process (usually a privileged helper tool) to + * connect to it and receive all the necessary arguments to complete a + * successful update. + */ +@interface ElevatedUpdateServer : NSObject { + NSArray* mUpdaterArguments; + BOOL mShouldKeepRunning; + BOOL mAborted; +} +- (id)initWithArgs:(NSArray*)args; +- (BOOL)runServer; +- (NSArray*)getArguments; +- (void)abort; +- (BOOL)wasAborted; +- (void)shutdown; +- (BOOL)shouldKeepRunning; +@end + +@implementation ElevatedUpdateServer + +- (id)initWithArgs:(NSArray*)args { + self = [super init]; + if (!self) { + return nil; + } + mUpdaterArguments = args; + mShouldKeepRunning = YES; + mAborted = NO; + return self; +} + +- (BOOL)runServer { + NSPort* serverPort = [NSSocketPort port]; + NSConnection* server = [NSConnection connectionWithReceivePort:serverPort sendPort:serverPort]; + [server setRootObject:self]; + if ([server registerName:@"org.mozilla.updater.server" + withNameServer:[NSSocketPortNameServer sharedInstance]] == NO) { + NSLog(@"Unable to register as DirectoryServer."); + NSLog(@"Is another copy running?"); + return NO; + } + + while ([self shouldKeepRunning] && [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode + beforeDate:[NSDate distantFuture]]) + ; + return ![self wasAborted]; +} + +- (NSArray*)getArguments { + return mUpdaterArguments; +} + +- (void)abort { + mAborted = YES; + [self shutdown]; +} + +- (BOOL)wasAborted { + return mAborted; +} + +- (void)shutdown { + mShouldKeepRunning = NO; +} + +- (BOOL)shouldKeepRunning { + return mShouldKeepRunning; +} + +@end + +bool ServeElevatedUpdate(int argc, const char** argv) { + MacAutoreleasePool pool; + + NSMutableArray* updaterArguments = [NSMutableArray arrayWithCapacity:argc]; + for (int i = 0; i < argc; i++) { + [updaterArguments addObject:[NSString stringWithUTF8String:argv[i]]]; + } + + ElevatedUpdateServer* updater = + [[ElevatedUpdateServer alloc] initWithArgs:[updaterArguments copy]]; + bool didSucceed = [updater runServer]; + + [updater release]; + return didSucceed; +} + +bool IsOwnedByGroupAdmin(const char* aAppBundle) { + MacAutoreleasePool pool; + + NSString* appDir = [NSString stringWithUTF8String:aAppBundle]; + NSFileManager* fileManager = [NSFileManager defaultManager]; + + NSDictionary* attributes = [fileManager attributesOfItemAtPath:appDir error:nil]; + bool isOwnedByAdmin = false; + if (attributes && [[attributes valueForKey:NSFileGroupOwnerAccountID] intValue] == 80) { + isOwnedByAdmin = true; + } + return isOwnedByAdmin; +} + +void SetGroupOwnershipAndPermissions(const char* aAppBundle) { + MacAutoreleasePool pool; + + NSString* appDir = [NSString stringWithUTF8String:aAppBundle]; + NSFileManager* fileManager = [NSFileManager defaultManager]; + NSError* error = nil; + NSArray* paths = [fileManager subpathsOfDirectoryAtPath:appDir error:&error]; + if (error) { + return; + } + + // Set group ownership of Firefox.app to 80 ("admin") and permissions to + // 0775. + if (![fileManager setAttributes:@{ + NSFileGroupOwnerAccountID : @(80), + NSFilePosixPermissions : @(0775) + } + ofItemAtPath:appDir + error:&error] || + error) { + return; + } + + NSArray* permKeys = + [NSArray arrayWithObjects:NSFileGroupOwnerAccountID, NSFilePosixPermissions, nil]; + // For all descendants of Firefox.app, set group ownership to 80 ("admin") and + // ensure write permission for the group. + for (NSString* currPath in paths) { + NSString* child = [appDir stringByAppendingPathComponent:currPath]; + NSDictionary* oldAttributes = [fileManager attributesOfItemAtPath:child error:&error]; + if (error) { + return; + } + // Skip symlinks, since they could be pointing to files outside of the .app + // bundle. + if ([oldAttributes fileType] == NSFileTypeSymbolicLink) { + continue; + } + NSNumber* oldPerms = (NSNumber*)[oldAttributes valueForKey:NSFilePosixPermissions]; + NSArray* permObjects = [NSArray + arrayWithObjects:[NSNumber numberWithUnsignedLong:80], + [NSNumber numberWithUnsignedLong:[oldPerms shortValue] | 020], nil]; + NSDictionary* attributes = [NSDictionary dictionaryWithObjects:permObjects forKeys:permKeys]; + if (![fileManager setAttributes:attributes ofItemAtPath:child error:&error] || error) { + return; + } + } +} + +/** + * Helper to launch macOS tasks via NSTask. + */ +static void LaunchTask(NSString* aPath, NSArray* aArguments) { + if (@available(macOS 10.13, *)) { + NSTask* task = [[NSTask alloc] init]; + [task setExecutableURL:[NSURL fileURLWithPath:aPath]]; + if (aArguments) { + [task setArguments:aArguments]; + } + [task launchAndReturnError:nil]; + [task release]; + } else { + NSArray* arguments = aArguments; + if (!arguments) { + arguments = @[]; + } + [NSTask launchedTaskWithLaunchPath:aPath arguments:arguments]; + } +} + +static void RegisterAppWithLaunchServices(NSString* aBundlePath) { + NSArray* arguments = @[ @"-f", aBundlePath ]; + LaunchTask(@"/System/Library/Frameworks/CoreServices.framework/Frameworks/" + @"LaunchServices.framework/Support/lsregister", + arguments); +} + +static void StripQuarantineBit(NSString* aBundlePath) { + NSArray* arguments = @[ @"-d", @"com.apple.quarantine", aBundlePath ]; + LaunchTask(@"/usr/bin/xattr", arguments); +} + +bool PerformInstallationFromDMG(int argc, char** argv) { + MacAutoreleasePool pool; + if (argc < 4) { + return false; + } + NSString* bundlePath = [NSString stringWithUTF8String:argv[2]]; + NSString* destPath = [NSString stringWithUTF8String:argv[3]]; + if ([[NSFileManager defaultManager] copyItemAtPath:bundlePath toPath:destPath error:nil]) { + RegisterAppWithLaunchServices(destPath); + StripQuarantineBit(destPath); + return true; + } + return false; +} diff --git a/toolkit/mozapps/update/updater/loaddlls.cpp b/toolkit/mozapps/update/updater/loaddlls.cpp new file mode 100644 index 0000000000..462bd0bc18 --- /dev/null +++ b/toolkit/mozapps/update/updater/loaddlls.cpp @@ -0,0 +1,84 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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> + +// Delayed load libraries are loaded when the first symbol is used. +// The following ensures that we load the delayed loaded libraries from the +// system directory. +struct AutoLoadSystemDependencies { + AutoLoadSystemDependencies() { + // Remove the current directory from the search path for dynamically loaded + // DLLs as a precaution. This call has no effect for delay load DLLs. + SetDllDirectory(L""); + + HMODULE module = ::GetModuleHandleW(L"kernel32.dll"); + if (module) { + // SetDefaultDllDirectories is always available on Windows 8 and above. It + // is also available on Windows Vista, Windows Server 2008, and + // Windows 7 when MS KB2533623 has been applied. + decltype(SetDefaultDllDirectories)* setDefaultDllDirectories = + (decltype(SetDefaultDllDirectories)*)GetProcAddress( + module, "SetDefaultDllDirectories"); + if (setDefaultDllDirectories) { + setDefaultDllDirectories(LOAD_LIBRARY_SEARCH_SYSTEM32); + return; + } + } + + // When SetDefaultDllDirectories is not available, fallback to preloading + // dlls. The order that these are loaded does not matter since they are + // loaded using the LOAD_WITH_ALTERED_SEARCH_PATH flag. +#ifdef HAVE_64BIT_BUILD + // DLLs for Firefox x64 on Windows 7 (x64). + // Note: dwmapi.dll is preloaded since a crash will try to load it from the + // application's directory. + static LPCWSTR delayDLLs[] = { + L"apphelp.dll", L"cryptbase.dll", L"cryptsp.dll", L"dwmapi.dll", + L"mpr.dll", L"ntmarta.dll", L"profapi.dll", L"propsys.dll", + L"sspicli.dll", L"wsock32.dll"}; + +#else + // DLLs for Firefox x86 on Windows XP through Windows 7 (x86 and x64). + // Note: dwmapi.dll is preloaded since a crash will try to load it from the + // application's directory. + static LPCWSTR delayDLLs[] = { + L"apphelp.dll", L"crypt32.dll", L"cryptbase.dll", L"cryptsp.dll", + L"dwmapi.dll", L"mpr.dll", L"msasn1.dll", L"ntmarta.dll", + L"profapi.dll", L"propsys.dll", L"psapi.dll", L"secur32.dll", + L"sspicli.dll", L"userenv.dll", L"uxtheme.dll", L"ws2_32.dll", + L"ws2help.dll", L"wsock32.dll"}; +#endif + + WCHAR systemDirectory[MAX_PATH + 1] = {L'\0'}; + // If GetSystemDirectory fails we accept that we'll load the DLLs from the + // normal search path. + GetSystemDirectoryW(systemDirectory, MAX_PATH + 1); + size_t systemDirLen = wcslen(systemDirectory); + + // Make the system directory path terminate with a slash + if (systemDirectory[systemDirLen - 1] != L'\\' && systemDirLen) { + systemDirectory[systemDirLen] = L'\\'; + ++systemDirLen; + // No need to re-null terminate + } + + // For each known DLL ensure it is loaded from the system32 directory + for (size_t i = 0; i < sizeof(delayDLLs) / sizeof(delayDLLs[0]); ++i) { + size_t fileLen = wcslen(delayDLLs[i]); + wcsncpy(systemDirectory + systemDirLen, delayDLLs[i], + MAX_PATH - systemDirLen); + if (systemDirLen + fileLen <= MAX_PATH) { + systemDirectory[systemDirLen + fileLen] = L'\0'; + } else { + systemDirectory[MAX_PATH] = L'\0'; + } + LPCWSTR fullModulePath = systemDirectory; // just for code readability + // LOAD_WITH_ALTERED_SEARCH_PATH makes a dll look in its own directory for + // dependencies and is only available on Win 7 and below. + LoadLibraryExW(fullModulePath, nullptr, LOAD_WITH_ALTERED_SEARCH_PATH); + } + } +} loadDLLs; diff --git a/toolkit/mozapps/update/updater/macbuild/Contents/Info.plist.in b/toolkit/mozapps/update/updater/macbuild/Contents/Info.plist.in new file mode 100644 index 0000000000..b6d5f60153 --- /dev/null +++ b/toolkit/mozapps/update/updater/macbuild/Contents/Info.plist.in @@ -0,0 +1,40 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>CFBundleDevelopmentRegion</key> + <string>English</string> + <key>CFBundleDisplayName</key> + <string>updater</string> + <key>CFBundleExecutable</key> + <string>org.mozilla.updater</string> + <key>CFBundleIconFile</key> + <string>updater.icns</string> + <key>CFBundleIdentifier</key> + <string>org.mozilla.updater</string> + <key>CFBundleInfoDictionaryVersion</key> + <string>6.0</string> + <key>CFBundleName</key> + <string>updater</string> + <key>CFBundlePackageType</key> + <string>APPL</string> + <key>CFBundleSignature</key> + <string>????</string> + <key>CFBundleVersion</key> + <string>1.0</string> + <key>LSHasLocalizedDisplayName</key> + <true/> + <key>NSMainNibFile</key> + <string>MainMenu</string> + <key>NSRequiresAquaSystemAppearance</key> + <false/> + <key>NSPrincipalClass</key> + <string>NSApplication</string> + <key>LSUIElement</key> + <true/> + <key>SMAuthorizedClients</key> + <array> + <string>identifier "@MOZ_MACBUNDLE_ID@" and ((anchor apple generic and certificate leaf[field.1.2.840.113635.100.6.1.9]) or (anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] and certificate leaf[field.1.2.840.113635.100.6.1.13] and certificate leaf[subject.OU] = "43AQ936H96"))</string> + </array> +</dict> +</plist> diff --git a/toolkit/mozapps/update/updater/macbuild/Contents/Resources/English.lproj/InfoPlist.strings.in b/toolkit/mozapps/update/updater/macbuild/Contents/Resources/English.lproj/InfoPlist.strings.in new file mode 100644 index 0000000000..e8036ec8cc --- /dev/null +++ b/toolkit/mozapps/update/updater/macbuild/Contents/Resources/English.lproj/InfoPlist.strings.in @@ -0,0 +1,8 @@ +/* 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/. */ + +/* Localized versions of Info.plist keys */ + +CFBundleName = "Software Update"; +CFBundleDisplayName = "@APP_NAME@ Software Update"; diff --git a/toolkit/mozapps/update/updater/macbuild/Contents/Resources/English.lproj/MainMenu.nib/classes.nib b/toolkit/mozapps/update/updater/macbuild/Contents/Resources/English.lproj/MainMenu.nib/classes.nib new file mode 100644 index 0000000000..6cfb50406b --- /dev/null +++ b/toolkit/mozapps/update/updater/macbuild/Contents/Resources/English.lproj/MainMenu.nib/classes.nib @@ -0,0 +1,19 @@ +{ + IBClasses = ( + { + CLASS = FirstResponder; + LANGUAGE = ObjC; + SUPERCLASS = NSObject; +}, + { + CLASS = UpdaterUI; + LANGUAGE = ObjC; + OUTLETS = { + progressBar = NSProgressIndicator; + progressTextField = NSTextField; + }; + SUPERCLASS = NSObject; +} + ); + IBVersion = 1; +}
\ No newline at end of file diff --git a/toolkit/mozapps/update/updater/macbuild/Contents/Resources/English.lproj/MainMenu.nib/info.nib b/toolkit/mozapps/update/updater/macbuild/Contents/Resources/English.lproj/MainMenu.nib/info.nib new file mode 100644 index 0000000000..1509178370 --- /dev/null +++ b/toolkit/mozapps/update/updater/macbuild/Contents/Resources/English.lproj/MainMenu.nib/info.nib @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>IBDocumentLocation</key> + <string>111 162 356 240 0 0 1440 878 </string> + <key>IBEditorPositions</key> + <dict> + <key>29</key> + <string>106 299 84 44 0 0 1440 878 </string> + </dict> + <key>IBFramework Version</key> + <string>489.0</string> + <key>IBOpenObjects</key> + <array> + <integer>21</integer> + <integer>29</integer> + </array> + <key>IBSystem Version</key> + <string>10J567</string> +</dict> +</plist> diff --git a/toolkit/mozapps/update/updater/macbuild/Contents/Resources/updater.icns b/toolkit/mozapps/update/updater/macbuild/Contents/Resources/updater.icns Binary files differnew file mode 100644 index 0000000000..d7499c6692 --- /dev/null +++ b/toolkit/mozapps/update/updater/macbuild/Contents/Resources/updater.icns diff --git a/toolkit/mozapps/update/updater/module.ver b/toolkit/mozapps/update/updater/module.ver new file mode 100644 index 0000000000..771416bb11 --- /dev/null +++ b/toolkit/mozapps/update/updater/module.ver @@ -0,0 +1 @@ +WIN32_MODULE_DESCRIPTION=@MOZ_APP_DISPLAYNAME@ Software Updater diff --git a/toolkit/mozapps/update/updater/moz.build b/toolkit/mozapps/update/updater/moz.build new file mode 100644 index 0000000000..40d7a77a6b --- /dev/null +++ b/toolkit/mozapps/update/updater/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/. + +if CONFIG["MOZ_WIDGET_TOOLKIT"] == "cocoa": + Program("org.mozilla.updater") +else: + Program("updater") + +updater_rel_path = "" +include("updater-common.build") +DIRS += ["updater-dep"] +if CONFIG["ENABLE_TESTS"]: + DIRS += ["updater-xpcshell"] + +if CONFIG["MOZ_WIDGET_TOOLKIT"] == "cocoa": + LDFLAGS += [ + "-sectcreate", + "__TEXT", + "__info_plist", + TOPOBJDIR + "/dist/bin/Info.plist", + "-sectcreate", + "__TEXT", + "__launchd_plist", + SRCDIR + "/Launchd.plist", + ] + +GENERATED_FILES = [ + "dep1Cert.h", + "dep2Cert.h", + "primaryCert.h", + "secondaryCert.h", + "xpcshellCert.h", +] + +primary_cert = GENERATED_FILES["primaryCert.h"] +secondary_cert = GENERATED_FILES["secondaryCert.h"] + +# This is how the xpcshellCertificate.der file is generated, in case we ever +# have to regenerate it. +# ./certutil -L -d modules/libmar/tests/unit/data -n mycert -r > xpcshellCertificate.der +xpcshell_cert = GENERATED_FILES["xpcshellCert.h"] +dep1_cert = GENERATED_FILES["dep1Cert.h"] +dep2_cert = GENERATED_FILES["dep2Cert.h"] + +primary_cert.script = "gen_cert_header.py:create_header" +secondary_cert.script = "gen_cert_header.py:create_header" +xpcshell_cert.script = "gen_cert_header.py:create_header" +dep1_cert.script = "gen_cert_header.py:create_header" +dep2_cert.script = "gen_cert_header.py:create_header" + +if CONFIG["MOZ_UPDATE_CHANNEL"] in ("beta", "release", "esr"): + primary_cert.inputs += ["release_primary.der"] + secondary_cert.inputs += ["release_secondary.der"] +elif CONFIG["MOZ_UPDATE_CHANNEL"] in ( + "nightly", + "aurora", + "nightly-elm", + "nightly-profiling", + "nightly-oak", + "nightly-ux", +): + primary_cert.inputs += ["nightly_aurora_level3_primary.der"] + secondary_cert.inputs += ["nightly_aurora_level3_secondary.der"] +else: + primary_cert.inputs += ["dep1.der"] + secondary_cert.inputs += ["dep2.der"] + +dep1_cert.inputs += ["dep1.der"] +dep2_cert.inputs += ["dep2.der"] +xpcshell_cert.inputs += ["xpcshellCertificate.der"] + +if CONFIG["MOZ_WIDGET_TOOLKIT"] == "gtk": + FINAL_TARGET_FILES.icons += ["updater.png"] diff --git a/toolkit/mozapps/update/updater/nightly_aurora_level3_primary.der b/toolkit/mozapps/update/updater/nightly_aurora_level3_primary.der Binary files differnew file mode 100644 index 0000000000..44fd95dcff --- /dev/null +++ b/toolkit/mozapps/update/updater/nightly_aurora_level3_primary.der diff --git a/toolkit/mozapps/update/updater/nightly_aurora_level3_secondary.der b/toolkit/mozapps/update/updater/nightly_aurora_level3_secondary.der Binary files differnew file mode 100644 index 0000000000..90f8e6e82c --- /dev/null +++ b/toolkit/mozapps/update/updater/nightly_aurora_level3_secondary.der diff --git a/toolkit/mozapps/update/updater/progressui.h b/toolkit/mozapps/update/updater/progressui.h new file mode 100644 index 0000000000..e283c4d1cd --- /dev/null +++ b/toolkit/mozapps/update/updater/progressui.h @@ -0,0 +1,40 @@ +/* -*- 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 PROGRESSUI_H__ +#define PROGRESSUI_H__ + +#include "updatedefines.h" + +#if defined(XP_WIN) +typedef WCHAR NS_tchar; +# define NS_main wmain +#else +typedef char NS_tchar; +# define NS_main main +#endif + +// Called to perform any initialization of the widget toolkit +int InitProgressUI(int* argc, NS_tchar*** argv); + +#if defined(XP_WIN) +// Called on the main thread at startup +int ShowProgressUI(bool indeterminate = false, bool initUIStrings = true); +int InitProgressUIStrings(); +#elif defined(XP_MACOSX) +// Called on the main thread at startup +int ShowProgressUI(bool indeterminate = false); +#else +// Called on the main thread at startup +int ShowProgressUI(); +#endif +// May be called from any thread +void QuitProgressUI(); + +// May be called from any thread: progress is a number between 0 and 100 +void UpdateProgressUI(float progress); + +#endif // PROGRESSUI_H__ diff --git a/toolkit/mozapps/update/updater/progressui_gtk.cpp b/toolkit/mozapps/update/updater/progressui_gtk.cpp new file mode 100644 index 0000000000..cfdcd5587c --- /dev/null +++ b/toolkit/mozapps/update/updater/progressui_gtk.cpp @@ -0,0 +1,121 @@ +/* -*- 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 <gtk/gtk.h> +#include <unistd.h> +#include "mozilla/Sprintf.h" +#include "mozilla/Atomics.h" +#include "progressui.h" +#include "readstrings.h" +#include "updatererrors.h" + +#define TIMER_INTERVAL 100 + +static float sProgressVal; // between 0 and 100 +static mozilla::Atomic<gboolean> sQuit(FALSE); +static gboolean sEnableUI; +static guint sTimerID; + +static GtkWidget* sWin; +static GtkWidget* sLabel; +static GtkWidget* sProgressBar; +static GdkPixbuf* sPixbuf; + +StringTable sStrings; + +static gboolean UpdateDialog(gpointer data) { + if (sQuit) { + gtk_widget_hide(sWin); + gtk_main_quit(); + } + + float progress = sProgressVal; + + gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(sProgressBar), + progress / 100.0); + + return TRUE; +} + +static gboolean OnDeleteEvent(GtkWidget* widget, GdkEvent* event, + gpointer user_data) { + return TRUE; +} + +int InitProgressUI(int* pargc, char*** pargv) { + sEnableUI = gtk_init_check(pargc, pargv); + if (sEnableUI) { + // Prepare to show the UI here in case the files are modified by the update. + char ini_path[PATH_MAX]; + SprintfLiteral(ini_path, "%s.ini", (*pargv)[0]); + if (ReadStrings(ini_path, &sStrings) != OK) { + sEnableUI = false; + return -1; + } + + char icon_path[PATH_MAX]; + SprintfLiteral(icon_path, "%s/icons/updater.png", (*pargv)[2]); + sPixbuf = gdk_pixbuf_new_from_file(icon_path, nullptr); + } + return 0; +} + +int ShowProgressUI() { + if (!sEnableUI) { + return -1; + } + + // Only show the Progress UI if the process is taking a significant amount of + // time where a significant amount of time is defined as .5 seconds after + // ShowProgressUI is called sProgress is less than 70. + usleep(500000); + + if (sQuit || sProgressVal > 70.0f) { + return 0; + } + + sWin = gtk_window_new(GTK_WINDOW_TOPLEVEL); + if (!sWin) { + return -1; + } + + g_signal_connect(G_OBJECT(sWin), "delete_event", G_CALLBACK(OnDeleteEvent), + nullptr); + + gtk_window_set_title(GTK_WINDOW(sWin), sStrings.title.get()); + gtk_window_set_type_hint(GTK_WINDOW(sWin), GDK_WINDOW_TYPE_HINT_DIALOG); + gtk_window_set_position(GTK_WINDOW(sWin), GTK_WIN_POS_CENTER_ALWAYS); + gtk_window_set_resizable(GTK_WINDOW(sWin), FALSE); + gtk_window_set_decorated(GTK_WINDOW(sWin), TRUE); + gtk_window_set_deletable(GTK_WINDOW(sWin), FALSE); + gtk_window_set_icon(GTK_WINDOW(sWin), sPixbuf); + g_object_unref(sPixbuf); + + GtkWidget* vbox = gtk_vbox_new(TRUE, 6); + sLabel = gtk_label_new(sStrings.info.get()); + gtk_misc_set_alignment(GTK_MISC(sLabel), 0.0f, 0.0f); + sProgressBar = gtk_progress_bar_new(); + + gtk_box_pack_start(GTK_BOX(vbox), sLabel, FALSE, FALSE, 0); + gtk_box_pack_start(GTK_BOX(vbox), sProgressBar, TRUE, TRUE, 0); + + sTimerID = g_timeout_add(TIMER_INTERVAL, UpdateDialog, nullptr); + + gtk_container_set_border_width(GTK_CONTAINER(sWin), 10); + gtk_container_add(GTK_CONTAINER(sWin), vbox); + gtk_widget_show_all(sWin); + + gtk_main(); + return 0; +} + +// Called on a background thread +void QuitProgressUI() { sQuit = TRUE; } + +// Called on a background thread +void UpdateProgressUI(float progress) { + sProgressVal = progress; // 32-bit writes are atomic +} diff --git a/toolkit/mozapps/update/updater/progressui_null.cpp b/toolkit/mozapps/update/updater/progressui_null.cpp new file mode 100644 index 0000000000..49877b2faf --- /dev/null +++ b/toolkit/mozapps/update/updater/progressui_null.cpp @@ -0,0 +1,15 @@ +/* -*- 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 "progressui.h" + +int InitProgressUI(int* argc, char*** argv) { return 0; } + +int ShowProgressUI() { return 0; } + +void QuitProgressUI() {} + +void UpdateProgressUI(float progress) {} diff --git a/toolkit/mozapps/update/updater/progressui_osx.mm b/toolkit/mozapps/update/updater/progressui_osx.mm new file mode 100644 index 0000000000..b6e01ea71e --- /dev/null +++ b/toolkit/mozapps/update/updater/progressui_osx.mm @@ -0,0 +1,133 @@ +/* -*- 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/. */ + +#import <Cocoa/Cocoa.h> +#include <stdio.h> +#include <unistd.h> +#include "mozilla/Sprintf.h" +#include "progressui.h" +#include "readstrings.h" +#include "updatererrors.h" + +#define TIMER_INTERVAL 0.2 + +static float sProgressVal; // between 0 and 100 +static BOOL sQuit = NO; +static BOOL sIndeterminate = NO; +static StringTable sLabels; +static const char* sUpdatePath; + +@interface UpdaterUI : NSObject { + IBOutlet NSProgressIndicator* progressBar; + IBOutlet NSTextField* progressTextField; +} +@end + +@implementation UpdaterUI + +- (void)awakeFromNib { + NSWindow* w = [progressBar window]; + + [w setTitle:[NSString stringWithUTF8String:sLabels.title.get()]]; + [progressTextField setStringValue:[NSString stringWithUTF8String:sLabels.info.get()]]; + + NSRect origTextFrame = [progressTextField frame]; + [progressTextField sizeToFit]; + + int widthAdjust = progressTextField.frame.size.width - origTextFrame.size.width; + + if (widthAdjust > 0) { + NSRect f; + f.size.width = w.frame.size.width + widthAdjust; + f.size.height = w.frame.size.height; + [w setFrame:f display:YES]; + } + + [w center]; + + [progressBar setIndeterminate:sIndeterminate]; + [progressBar setDoubleValue:0.0]; + + [[NSTimer scheduledTimerWithTimeInterval:TIMER_INTERVAL + target:self + selector:@selector(updateProgressUI:) + userInfo:nil + repeats:YES] retain]; + + // Make sure we are on top initially + [NSApp activateIgnoringOtherApps:YES]; +} + +// called when the timer goes off +- (void)updateProgressUI:(NSTimer*)aTimer { + if (sQuit) { + [aTimer invalidate]; + [aTimer release]; + + // It seems to be necessary to activate and hide ourselves before we stop, + // otherwise the "run" method will not return until the user focuses some + // other app. The activate step is necessary if we are not the active app. + // This is a big hack, but it seems to do the trick. + [NSApp activateIgnoringOtherApps:YES]; + [NSApp hide:self]; + [NSApp stop:self]; + } + + float progress = sProgressVal; + + [progressBar setDoubleValue:(double)progress]; +} + +// leave this as returning a BOOL instead of NSApplicationTerminateReply +// for backward compatibility +- (BOOL)applicationShouldTerminate:(NSApplication*)sender { + return sQuit; +} + +@end + +int InitProgressUI(int* pargc, char*** pargv) { + sUpdatePath = (*pargv)[1]; + + return 0; +} + +int ShowProgressUI(bool indeterminate) { + if (!sUpdatePath) { + // InitProgressUI was never called. + return -1; + } + + // Only show the Progress UI if the process is taking a significant amount of + // time where a significant amount of time is defined as .5 seconds after + // ShowProgressUI is called sProgress is less than 70. + usleep(500000); + + if (sQuit || sProgressVal > 70.0f) { + return 0; + } + + char path[PATH_MAX]; + SprintfLiteral(path, "%s/updater.ini", sUpdatePath); + if (ReadStrings(path, &sLabels) != OK) { + return -1; + } + + sIndeterminate = indeterminate; + [NSApplication sharedApplication]; + [[NSBundle mainBundle] loadNibNamed:@"MainMenu" owner:NSApp topLevelObjects:nil]; + [NSApp run]; + + return 0; +} + +// Called on a background thread +void QuitProgressUI() { sQuit = YES; } + +// Called on a background thread +void UpdateProgressUI(float progress) { + sProgressVal = progress; // 32-bit writes are atomic +} diff --git a/toolkit/mozapps/update/updater/progressui_win.cpp b/toolkit/mozapps/update/updater/progressui_win.cpp new file mode 100644 index 0000000000..51bd2d8cce --- /dev/null +++ b/toolkit/mozapps/update/updater/progressui_win.cpp @@ -0,0 +1,302 @@ +/* -*- 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 <stdio.h> +#include <windows.h> +#include <commctrl.h> +#include <process.h> +#include <io.h> + +#include "resource.h" +#include "progressui.h" +#include "readstrings.h" +#include "updatererrors.h" + +#define TIMER_ID 1 +#define TIMER_INTERVAL 100 + +#define RESIZE_WINDOW(hwnd, extrax, extray) \ + { \ + RECT windowSize; \ + GetWindowRect(hwnd, &windowSize); \ + SetWindowPos(hwnd, 0, 0, 0, windowSize.right - windowSize.left + extrax, \ + windowSize.bottom - windowSize.top + extray, \ + SWP_NOMOVE | SWP_NOZORDER); \ + } + +#define MOVE_WINDOW(hwnd, dx, dy) \ + { \ + RECT rc; \ + POINT pt; \ + GetWindowRect(hwnd, &rc); \ + pt.x = rc.left; \ + pt.y = rc.top; \ + ScreenToClient(GetParent(hwnd), &pt); \ + SetWindowPos(hwnd, 0, pt.x + dx, pt.y + dy, 0, 0, \ + SWP_NOSIZE | SWP_NOZORDER); \ + } + +static float sProgress; // between 0 and 100 +static BOOL sQuit = FALSE; +static BOOL sIndeterminate = FALSE; +static StringTable sUIStrings; + +static BOOL GetStringsFile(WCHAR filename[MAX_PATH]) { + if (!GetModuleFileNameW(nullptr, filename, MAX_PATH)) { + return FALSE; + } + + WCHAR* dot = wcsrchr(filename, '.'); + if (!dot || wcsicmp(dot + 1, L"exe")) { + return FALSE; + } + + wcscpy(dot + 1, L"ini"); + return TRUE; +} + +static void UpdateDialog(HWND hDlg) { + int pos = int(sProgress + 0.5f); + HWND hWndPro = GetDlgItem(hDlg, IDC_PROGRESS); + SendMessage(hWndPro, PBM_SETPOS, pos, 0L); +} + +// The code in this function is from MSDN: +// http://msdn.microsoft.com/library/default.asp?url=/library/en-us/winui/winui/windowsuserinterface/windowing/dialogboxes/usingdialogboxes.asp +static void CenterDialog(HWND hDlg) { + RECT rc, rcOwner, rcDlg; + + // Get the owner window and dialog box rectangles. + HWND desktop = GetDesktopWindow(); + + GetWindowRect(desktop, &rcOwner); + GetWindowRect(hDlg, &rcDlg); + CopyRect(&rc, &rcOwner); + + // Offset the owner and dialog box rectangles so that + // right and bottom values represent the width and + // height, and then offset the owner again to discard + // space taken up by the dialog box. + + OffsetRect(&rcDlg, -rcDlg.left, -rcDlg.top); + OffsetRect(&rc, -rc.left, -rc.top); + OffsetRect(&rc, -rcDlg.right, -rcDlg.bottom); + + // The new position is the sum of half the remaining + // space and the owner's original position. + + SetWindowPos(hDlg, HWND_TOP, rcOwner.left + (rc.right / 2), + rcOwner.top + (rc.bottom / 2), 0, 0, // ignores size arguments + SWP_NOSIZE); +} + +static void InitDialog(HWND hDlg) { + mozilla::UniquePtr<WCHAR[]> szwTitle; + mozilla::UniquePtr<WCHAR[]> szwInfo; + + int bufferSize = + MultiByteToWideChar(CP_UTF8, 0, sUIStrings.title.get(), -1, nullptr, 0); + szwTitle = mozilla::MakeUnique<WCHAR[]>(bufferSize); + MultiByteToWideChar(CP_UTF8, 0, sUIStrings.title.get(), -1, szwTitle.get(), + bufferSize); + bufferSize = + MultiByteToWideChar(CP_UTF8, 0, sUIStrings.info.get(), -1, nullptr, 0); + szwInfo = mozilla::MakeUnique<WCHAR[]>(bufferSize); + MultiByteToWideChar(CP_UTF8, 0, sUIStrings.info.get(), -1, szwInfo.get(), + bufferSize); + + SetWindowTextW(hDlg, szwTitle.get()); + SetWindowTextW(GetDlgItem(hDlg, IDC_INFO), szwInfo.get()); + + // Set dialog icon + HICON hIcon = LoadIcon(GetModuleHandle(nullptr), MAKEINTRESOURCE(IDI_DIALOG)); + if (hIcon) { + SendMessage(hDlg, WM_SETICON, ICON_BIG, (LPARAM)hIcon); + } + + HWND hWndPro = GetDlgItem(hDlg, IDC_PROGRESS); + SendMessage(hWndPro, PBM_SETRANGE, 0, MAKELPARAM(0, 100)); + if (sIndeterminate) { + LONG_PTR val = GetWindowLongPtr(hWndPro, GWL_STYLE); + SetWindowLongPtr(hWndPro, GWL_STYLE, val | PBS_MARQUEE); + SendMessage(hWndPro, (UINT)PBM_SETMARQUEE, (WPARAM)TRUE, (LPARAM)50); + } + + // Resize the dialog to fit all of the text if necessary. + RECT infoSize, textSize; + HWND hWndInfo = GetDlgItem(hDlg, IDC_INFO); + + // Get the control's font for calculating the new size for the control + HDC hDCInfo = GetDC(hWndInfo); + HFONT hInfoFont, hOldFont = NULL; + hInfoFont = (HFONT)SendMessage(hWndInfo, WM_GETFONT, 0, 0); + + if (hInfoFont) { + hOldFont = (HFONT)SelectObject(hDCInfo, hInfoFont); + } + + // Measure the space needed for the text on a single line. DT_CALCRECT means + // nothing is drawn. + if (DrawText(hDCInfo, szwInfo.get(), -1, &textSize, + DT_CALCRECT | DT_NOCLIP | DT_SINGLELINE)) { + GetClientRect(hWndInfo, &infoSize); + SIZE extra; + // Calculate the additional space needed for the text by subtracting from + // the rectangle returned by DrawText the existing client rectangle's width + // and height. + extra.cx = + (textSize.right - textSize.left) - (infoSize.right - infoSize.left); + extra.cy = + (textSize.bottom - textSize.top) - (infoSize.bottom - infoSize.top); + if (extra.cx < 0) { + extra.cx = 0; + } + if (extra.cy < 0) { + extra.cy = 0; + } + if ((extra.cx > 0) || (extra.cy > 0)) { + RESIZE_WINDOW(hDlg, extra.cx, extra.cy); + RESIZE_WINDOW(hWndInfo, extra.cx, extra.cy); + RESIZE_WINDOW(hWndPro, extra.cx, 0); + MOVE_WINDOW(hWndPro, 0, extra.cy); + } + } + + if (hOldFont) { + SelectObject(hDCInfo, hOldFont); + } + + ReleaseDC(hWndInfo, hDCInfo); + + CenterDialog(hDlg); // make dialog appear in the center of the screen + + SetTimer(hDlg, TIMER_ID, TIMER_INTERVAL, nullptr); +} + +// Message handler for update dialog. +static LRESULT CALLBACK DialogProc(HWND hDlg, UINT message, WPARAM wParam, + LPARAM lParam) { + switch (message) { + case WM_INITDIALOG: + InitDialog(hDlg); + return TRUE; + + case WM_TIMER: + if (sQuit) { + EndDialog(hDlg, 0); + } else { + UpdateDialog(hDlg); + } + return TRUE; + + case WM_COMMAND: + return TRUE; + } + return FALSE; +} + +int InitProgressUI(int* argc, WCHAR*** argv) { return 0; } + +/** + * Initializes the progress UI strings + * + * @return 0 on success, -1 on error + */ +int InitProgressUIStrings() { + // If we do not have updater.ini, then we should not bother showing UI. + WCHAR filename[MAX_PATH]; + if (!GetStringsFile(filename)) { + return -1; + } + + if (_waccess(filename, 04)) { + return -1; + } + + // If the updater.ini doesn't have the required strings, then we should not + // bother showing UI. + if (ReadStrings(filename, &sUIStrings) != OK) { + return -1; + } + + return 0; +} + +int ShowProgressUI(bool indeterminate, bool initUIStrings) { + sIndeterminate = indeterminate; + if (!indeterminate) { + // Only show the Progress UI if the process is taking a significant amount + // of time where a significant amount of time is defined as .5 seconds after + // ShowProgressUI is called sProgress is less than 70. + Sleep(500); + + if (sQuit || sProgress > 70.0f) { + return 0; + } + } + + // Don't load the UI if there's an <exe_name>.Local directory for redirection. + WCHAR appPath[MAX_PATH + 1] = {L'\0'}; + if (!GetModuleFileNameW(nullptr, appPath, MAX_PATH)) { + return -1; + } + + if (wcslen(appPath) + wcslen(L".Local") >= MAX_PATH) { + return -1; + } + + wcscat(appPath, L".Local"); + + if (!_waccess(appPath, 04)) { + return -1; + } + + // Don't load the UI if the strings for the UI are not provided. + if (initUIStrings && InitProgressUIStrings() == -1) { + return -1; + } + + if (!GetModuleFileNameW(nullptr, appPath, MAX_PATH)) { + return -1; + } + + // Use an activation context that supports visual styles for the controls. + ACTCTXW actx = {0}; + actx.cbSize = sizeof(ACTCTXW); + actx.dwFlags = ACTCTX_FLAG_RESOURCE_NAME_VALID | ACTCTX_FLAG_HMODULE_VALID; + actx.hModule = GetModuleHandle(NULL); // Use the embedded manifest + // This is needed only for Win XP but doesn't cause a problem with other + // versions of Windows. + actx.lpSource = appPath; + actx.lpResourceName = MAKEINTRESOURCE(IDR_COMCTL32_MANIFEST); + + HANDLE hactx = INVALID_HANDLE_VALUE; + hactx = CreateActCtxW(&actx); + ULONG_PTR actxCookie = NULL; + if (hactx != INVALID_HANDLE_VALUE) { + // Push the specified activation context to the top of the activation stack. + ActivateActCtx(hactx, &actxCookie); + } + + INITCOMMONCONTROLSEX icc = {sizeof(INITCOMMONCONTROLSEX), ICC_PROGRESS_CLASS}; + InitCommonControlsEx(&icc); + + DialogBox(GetModuleHandle(nullptr), MAKEINTRESOURCE(IDD_DIALOG), nullptr, + (DLGPROC)DialogProc); + + if (hactx != INVALID_HANDLE_VALUE) { + // Deactivate the context now that the comctl32.dll is loaded. + DeactivateActCtx(0, actxCookie); + } + + return 0; +} + +void QuitProgressUI() { sQuit = TRUE; } + +void UpdateProgressUI(float progress) { + sProgress = progress; // 32-bit writes are atomic +} diff --git a/toolkit/mozapps/update/updater/release_primary.der b/toolkit/mozapps/update/updater/release_primary.der Binary files differnew file mode 100644 index 0000000000..1d94f88ad7 --- /dev/null +++ b/toolkit/mozapps/update/updater/release_primary.der diff --git a/toolkit/mozapps/update/updater/release_secondary.der b/toolkit/mozapps/update/updater/release_secondary.der Binary files differnew file mode 100644 index 0000000000..474706c4b7 --- /dev/null +++ b/toolkit/mozapps/update/updater/release_secondary.der diff --git a/toolkit/mozapps/update/updater/resource.h b/toolkit/mozapps/update/updater/resource.h new file mode 100644 index 0000000000..1dcb47fca1 --- /dev/null +++ b/toolkit/mozapps/update/updater/resource.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/. */ + +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by updater.rc +// +#define IDD_DIALOG 101 +#define IDC_PROGRESS 1000 +#define IDC_INFO 1002 +#define IDI_DIALOG 1003 +#define TYPE_CERT 512 +#define IDR_PRIMARY_CERT 1004 +#define IDR_BACKUP_CERT 1005 +#define IDS_UPDATER_IDENTITY 1006 +#define IDR_XPCSHELL_CERT 1007 +#define IDR_COMCTL32_MANIFEST 17 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +# ifndef APSTUDIO_READONLY_SYMBOLS +# define _APS_NEXT_RESOURCE_VALUE 102 +# define _APS_NEXT_COMMAND_VALUE 40001 +# define _APS_NEXT_CONTROL_VALUE 1008 +# define _APS_NEXT_SYMED_VALUE 101 +# endif +#endif diff --git a/toolkit/mozapps/update/updater/updater-common.build b/toolkit/mozapps/update/updater/updater-common.build new file mode 100644 index 0000000000..16c2d15003 --- /dev/null +++ b/toolkit/mozapps/update/updater/updater-common.build @@ -0,0 +1,120 @@ +# -*- 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/. + +srcs = [ + "archivereader.cpp", + "updater.cpp", +] + +have_progressui = 0 + +if CONFIG["MOZ_VERIFY_MAR_SIGNATURE"]: + USE_LIBS += [ + "verifymar", + ] + +if CONFIG["OS_ARCH"] == "WINNT": + have_progressui = 1 + srcs += [ + "loaddlls.cpp", + "progressui_win.cpp", + ] + RCINCLUDE = "%supdater.rc" % updater_rel_path + DEFINES["UNICODE"] = True + DEFINES["_UNICODE"] = True + USE_STATIC_LIBS = True + + # Pick up nsWindowsRestart.cpp + LOCAL_INCLUDES += [ + "/toolkit/xre", + ] + OS_LIBS += [ + "comctl32", + "ws2_32", + "shell32", + "shlwapi", + "crypt32", + "advapi32", + "gdi32", + "user32", + "userenv", + "uuid", + ] + +USE_LIBS += [ + "bspatch", + "mar", + "updatecommon", + "xz-embedded", +] + +if CONFIG["MOZ_WIDGET_TOOLKIT"] == "gtk": + have_progressui = 1 + srcs += [ + "progressui_gtk.cpp", + ] + +if CONFIG["MOZ_WIDGET_TOOLKIT"] == "cocoa": + have_progressui = 1 + srcs += [ + "launchchild_osx.mm", + "progressui_osx.mm", + ] + OS_LIBS += [ + "-framework Cocoa", + "-framework Security", + "-framework SystemConfiguration", + ] + UNIFIED_SOURCES += [ + "/toolkit/xre/updaterfileutils_osx.mm", + ] + LOCAL_INCLUDES += [ + "/toolkit/xre", + ] + +if have_progressui == 0: + srcs += [ + "progressui_null.cpp", + ] + +SOURCES += sorted(srcs) + +if CONFIG["MOZ_TSAN"]: + # Since mozglue is not linked to the updater, + # we need to include our own TSan suppression list. + SOURCES += [ + "TsanOptions.cpp", + ] + +DEFINES["SPRINTF_H_USES_VSNPRINTF"] = True +DEFINES["NS_NO_XPCOM"] = True +DisableStlWrapping() +for var in ("MAR_CHANNEL_ID", "MOZ_APP_VERSION"): + DEFINES[var] = '"%s"' % CONFIG[var] + +LOCAL_INCLUDES += [ + "/toolkit/mozapps/update/common", + "/xpcom/base", # for nsVersionComparator.cpp +] + +DELAYLOAD_DLLS += [ + "crypt32.dll", + "comctl32.dll", + "userenv.dll", + "wsock32.dll", +] + +if CONFIG["CC_TYPE"] == "clang-cl": + WIN32_EXE_LDFLAGS += ["-ENTRY:wmainCRTStartup"] +elif CONFIG["OS_ARCH"] == "WINNT": + WIN32_EXE_LDFLAGS += ["-municode"] + +if CONFIG["MOZ_WIDGET_TOOLKIT"] == "gtk": + CXXFLAGS += CONFIG["MOZ_GTK3_CFLAGS"] + OS_LIBS += CONFIG["MOZ_GTK3_LIBS"] + +if CONFIG["CC_TYPE"] == "gcc": + CXXFLAGS += ["-Wno-format-truncation"] diff --git a/toolkit/mozapps/update/updater/updater-dep/moz.build b/toolkit/mozapps/update/updater/updater-dep/moz.build new file mode 100644 index 0000000000..89c148987c --- /dev/null +++ b/toolkit/mozapps/update/updater/updater-dep/moz.build @@ -0,0 +1,13 @@ +# -*- 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/. + +FINAL_TARGET = "_tests/updater-dep" + +Program("updater-dep") + +updater_rel_path = "../" +DEFINES["DEP_UPDATER"] = True +include("../updater-common.build") diff --git a/toolkit/mozapps/update/updater/updater-xpcshell/Makefile.in b/toolkit/mozapps/update/updater/updater-xpcshell/Makefile.in new file mode 100644 index 0000000000..00a43f12d7 --- /dev/null +++ b/toolkit/mozapps/update/updater/updater-xpcshell/Makefile.in @@ -0,0 +1,48 @@ +# vim:set ts=8 sw=8 sts=8 noet: +# 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/. + +# For changes here, also consider ../Makefile.in + +XPCSHELLTESTDIR = $(topobjdir)/_tests/xpcshell/toolkit/mozapps/update/tests + +ifeq (,$(MOZ_SUITE)$(MOZ_THUNDERBIRD)) +MOCHITESTBROWSERDIR = $(topobjdir)/_tests/testing/mochitest/browser/toolkit/mozapps/update/tests/browser +endif + +ifndef MOZ_WINCONSOLE +ifdef MOZ_DEBUG +MOZ_WINCONSOLE = 1 +else +MOZ_WINCONSOLE = 0 +endif +endif + +include $(topsrcdir)/config/rules.mk + +ifneq (,$(COMPILE_ENVIRONMENT)$(MOZ_ARTIFACT_BUILDS)) +tools:: +ifeq (cocoa,$(MOZ_WIDGET_TOOLKIT)) + # Copy for xpcshell tests + $(NSINSTALL) -D $(XPCSHELLTESTDIR)/data/updater-xpcshell.app + rsync -a -C --exclude '*.in' $(srcdir)/../macbuild/Contents $(XPCSHELLTESTDIR)/data/updater-xpcshell.app + $(call py_action,preprocessor,-Fsubstitution --output-encoding utf-16 -DAPP_NAME='$(MOZ_APP_DISPLAYNAME)' $(srcdir)/../macbuild/Contents/Resources/English.lproj/InfoPlist.strings.in -o $(XPCSHELLTESTDIR)/data/updater-xpcshell.app/Contents/Resources/English.lproj/InfoPlist.strings) + $(NSINSTALL) -D $(XPCSHELLTESTDIR)/data/updater-xpcshell.app/Contents/MacOS + $(NSINSTALL) $(FINAL_TARGET)/updater-xpcshell $(XPCSHELLTESTDIR)/data/updater-xpcshell.app/Contents/MacOS + rm -Rf $(XPCSHELLTESTDIR)/data/updater.app + mv $(XPCSHELLTESTDIR)/data/updater-xpcshell.app $(XPCSHELLTESTDIR)/data/updater.app + mv $(XPCSHELLTESTDIR)/data/updater.app/Contents/MacOS/updater-xpcshell $(XPCSHELLTESTDIR)/data/updater.app/Contents/MacOS/org.mozilla.updater + +ifdef MOCHITESTBROWSERDIR + rsync -a -C $(XPCSHELLTESTDIR)/data/updater.app $(MOCHITESTBROWSERDIR)/ +endif +else + $(MKDIR) -p $(XPCSHELLTESTDIR)/data + cp $(FINAL_TARGET)/updater-xpcshell$(BIN_SUFFIX) $(XPCSHELLTESTDIR)/data/updater$(BIN_SUFFIX) +ifdef MOCHITESTBROWSERDIR + $(MKDIR) -p $(MOCHITESTBROWSERDIR) + cp $(FINAL_TARGET)/updater-xpcshell$(BIN_SUFFIX) $(MOCHITESTBROWSERDIR)/updater$(BIN_SUFFIX) +endif +endif +endif diff --git a/toolkit/mozapps/update/updater/updater-xpcshell/moz.build b/toolkit/mozapps/update/updater/updater-xpcshell/moz.build new file mode 100644 index 0000000000..33558a2c59 --- /dev/null +++ b/toolkit/mozapps/update/updater/updater-xpcshell/moz.build @@ -0,0 +1,13 @@ +# -*- 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/. + +FINAL_TARGET = "_tests/xpcshell/toolkit/mozapps/update/tests" + +Program("updater-xpcshell") + +updater_rel_path = "../" +DEFINES["TEST_UPDATER"] = True +include("../updater-common.build") diff --git a/toolkit/mozapps/update/updater/updater.cpp b/toolkit/mozapps/update/updater/updater.cpp new file mode 100644 index 0000000000..844230fa19 --- /dev/null +++ b/toolkit/mozapps/update/updater/updater.cpp @@ -0,0 +1,4741 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Manifest Format + * --------------- + * + * contents = 1*( line ) + * line = method LWS *( param LWS ) CRLF + * CRLF = "\r\n" + * LWS = 1*( " " | "\t" ) + * + * Available methods for the manifest file: + * + * updatev3.manifest + * ----------------- + * method = "add" | "add-if" | "add-if-not" | "patch" | "patch-if" | + * "remove" | "rmdir" | "rmrfdir" | type + * + * 'add-if-not' adds a file if it doesn't exist. + * + * 'type' is the update type (e.g. complete or partial) and when present MUST + * be the first entry in the update manifest. The type is used to support + * removing files that no longer exist when when applying a complete update by + * causing the actions defined in the precomplete file to be performed. + * + * precomplete + * ----------- + * method = "remove" | "rmdir" + */ +#include "bspatch.h" +#include "progressui.h" +#include "archivereader.h" +#include "readstrings.h" +#include "updatererrors.h" + +#include <stdio.h> +#include <string.h> +#include <stdlib.h> + +#include <sys/stat.h> +#include <fcntl.h> +#include <limits.h> +#include <errno.h> + +#include "updatecommon.h" +#ifdef XP_MACOSX +# include "updaterfileutils_osx.h" +#endif // XP_MACOSX + +#include "mozilla/CmdLineAndEnvUtils.h" +#include "mozilla/UniquePtr.h" +#ifdef XP_WIN +# include "mozilla/Maybe.h" +# include "mozilla/WinHeaderOnlyUtils.h" +# include <climits> +#endif // XP_WIN + +// Amount of the progress bar to use in each of the 3 update stages, +// should total 100.0. +#define PROGRESS_PREPARE_SIZE 20.0f +#define PROGRESS_EXECUTE_SIZE 75.0f +#define PROGRESS_FINISH_SIZE 5.0f + +// Maximum amount of time in ms to wait for the parent process to close. The 30 +// seconds is rather long but there have been bug reports where the parent +// process has exited after 10 seconds and it is better to give it a chance. +#define PARENT_WAIT 30000 + +#if defined(XP_MACOSX) +// These functions are defined in launchchild_osx.mm +void CleanupElevatedMacUpdate(bool aFailureOccurred); +bool IsOwnedByGroupAdmin(const char* aAppBundle); +bool IsRecursivelyWritable(const char* aPath); +void LaunchChild(int argc, const char** argv); +void LaunchMacPostProcess(const char* aAppBundle); +bool ObtainUpdaterArguments(int* argc, char*** argv); +bool ServeElevatedUpdate(int argc, const char** argv); +void SetGroupOwnershipAndPermissions(const char* aAppBundle); +bool PerformInstallationFromDMG(int argc, char** argv); +struct UpdateServerThreadArgs { + int argc; + const NS_tchar** argv; +}; +#endif + +#ifndef _O_BINARY +# define _O_BINARY 0 +#endif + +#ifndef NULL +# define NULL (0) +#endif + +#ifndef SSIZE_MAX +# define SSIZE_MAX LONG_MAX +#endif + +// We want to use execv to invoke the callback executable on platforms where +// we were launched using execv. See nsUpdateDriver.cpp. +#if defined(XP_UNIX) && !defined(XP_MACOSX) +# define USE_EXECV +#endif + +#if defined(XP_OPENBSD) +# define stat64 stat +#endif + +#if defined(MOZ_VERIFY_MAR_SIGNATURE) && !defined(XP_WIN) && !defined(XP_MACOSX) +# include "nss.h" +# include "prerror.h" +#endif + +#include "crctable.h" + +#ifdef XP_WIN +# ifdef MOZ_MAINTENANCE_SERVICE +# include "registrycertificates.h" +# endif +BOOL PathAppendSafe(LPWSTR base, LPCWSTR extra); +BOOL PathGetSiblingFilePath(LPWSTR destinationBuffer, LPCWSTR siblingFilePath, + LPCWSTR newFileName); +# include "updatehelper.h" + +// Closes the handle if valid and if the updater is elevated returns with the +// return code specified. This prevents multiple launches of the callback +// application by preventing the elevated process from launching the callback. +# define EXIT_WHEN_ELEVATED(path, handle, retCode) \ + { \ + if (handle != INVALID_HANDLE_VALUE) { \ + CloseHandle(handle); \ + } \ + if (NS_tremove(path) && errno != ENOENT) { \ + return retCode; \ + } \ + } +#endif + +//----------------------------------------------------------------------------- + +// This BZ2_crc32Table variable lives in libbz2. We just took the +// data structure from bz2 and created crctables.h + +static unsigned int crc32(const unsigned char* buf, unsigned int len) { + unsigned int crc = 0xffffffffL; + + const unsigned char* end = buf + len; + for (; buf != end; ++buf) + crc = (crc << 8) ^ BZ2_crc32Table[(crc >> 24) ^ *buf]; + + crc = ~crc; + return crc; +} + +//----------------------------------------------------------------------------- + +// A simple stack based container for a FILE struct that closes the +// file descriptor from its destructor. +class AutoFile { + public: + explicit AutoFile(FILE* file = nullptr) : mFile(file) {} + + ~AutoFile() { + if (mFile != nullptr) { + fclose(mFile); + } + } + + AutoFile& operator=(FILE* file) { + if (mFile != 0) { + fclose(mFile); + } + mFile = file; + return *this; + } + + operator FILE*() { return mFile; } + + FILE* get() { return mFile; } + + private: + FILE* mFile; +}; + +struct MARChannelStringTable { + MARChannelStringTable() { + MARChannelID = mozilla::MakeUnique<char[]>(1); + MARChannelID[0] = '\0'; + } + + mozilla::UniquePtr<char[]> MARChannelID; +}; + +//----------------------------------------------------------------------------- + +#ifdef XP_MACOSX + +// Just a simple class that sets a umask value in its constructor and resets +// it in its destructor. +class UmaskContext { + public: + explicit UmaskContext(mode_t umaskToSet); + ~UmaskContext(); + + private: + mode_t mPreviousUmask; +}; + +UmaskContext::UmaskContext(mode_t umaskToSet) { + mPreviousUmask = umask(umaskToSet); +} + +UmaskContext::~UmaskContext() { umask(mPreviousUmask); } + +#endif + +//----------------------------------------------------------------------------- + +typedef void (*ThreadFunc)(void* param); + +#ifdef XP_WIN +# include <process.h> + +class Thread { + public: + int Run(ThreadFunc func, void* param) { + mThreadFunc = func; + mThreadParam = param; + + unsigned int threadID; + + mThread = + (HANDLE)_beginthreadex(nullptr, 0, ThreadMain, this, 0, &threadID); + + return mThread ? 0 : -1; + } + int Join() { + WaitForSingleObject(mThread, INFINITE); + CloseHandle(mThread); + return 0; + } + + private: + static unsigned __stdcall ThreadMain(void* p) { + Thread* self = (Thread*)p; + self->mThreadFunc(self->mThreadParam); + return 0; + } + HANDLE mThread; + ThreadFunc mThreadFunc; + void* mThreadParam; +}; + +#elif defined(XP_UNIX) +# include <pthread.h> + +class Thread { + public: + int Run(ThreadFunc func, void* param) { + return pthread_create(&thr, nullptr, (void* (*)(void*))func, param); + } + int Join() { + void* result; + return pthread_join(thr, &result); + } + + private: + pthread_t thr; +}; + +#else +# error "Unsupported platform" +#endif + +//----------------------------------------------------------------------------- + +static NS_tchar gPatchDirPath[MAXPATHLEN]; +static NS_tchar gInstallDirPath[MAXPATHLEN]; +static NS_tchar gWorkingDirPath[MAXPATHLEN]; +static ArchiveReader gArchiveReader; +static bool gSucceeded = false; +static bool sStagedUpdate = false; +static bool sReplaceRequest = false; +static bool sUsingService = false; + +// Normally, we run updates as a result of user action (the user started Firefox +// or clicked a "Restart to Update" button). But there are some cases when +// we are not: +// a) The callback app is a background task. If true then the updater is +// likely being run as part of a background task. +// The updater could be run with no callback, but this only happens +// when performing a staged update (see calls to ProcessUpdates), and there +// are already checks for sStagedUpdate when showing UI or elevating. +// b) The environment variable MOZ_APP_SILENT_START is set and not empty. This +// is set, for instance, on macOS when Firefox had no windows open for a +// while and restarted to apply updates. +// +// In these cases, the update should be installed silently, so we shouldn't: +// a) show progress UI +// b) prompt for elevation +static bool sUpdateSilently = false; + +#ifdef XP_WIN +static NS_tchar gCallbackRelPath[MAXPATHLEN]; +static NS_tchar gCallbackBackupPath[MAXPATHLEN]; +static NS_tchar gDeleteDirPath[MAXPATHLEN]; + +// Whether to copy the update.log and update.status file to the update patch +// directory from a secure directory. +static bool gCopyOutputFiles = false; +// Whether to write the update.log and update.status file to a secure directory. +static bool gUseSecureOutputPath = false; +#endif + +static const NS_tchar kWhitespace[] = NS_T(" \t"); +static const NS_tchar kNL[] = NS_T("\r\n"); +static const NS_tchar kQuote[] = NS_T("\""); + +static inline size_t mmin(size_t a, size_t b) { return (a > b) ? b : a; } + +static NS_tchar* mstrtok(const NS_tchar* delims, NS_tchar** str) { + if (!*str || !**str) { + *str = nullptr; + return nullptr; + } + + // skip leading "whitespace" + NS_tchar* ret = *str; + const NS_tchar* d; + do { + for (d = delims; *d != NS_T('\0'); ++d) { + if (*ret == *d) { + ++ret; + break; + } + } + } while (*d); + + if (!*ret) { + *str = ret; + return nullptr; + } + + NS_tchar* i = ret; + do { + for (d = delims; *d != NS_T('\0'); ++d) { + if (*i == *d) { + *i = NS_T('\0'); + *str = ++i; + return ret; + } + } + ++i; + } while (*i); + + *str = nullptr; + return ret; +} + +#if defined(TEST_UPDATER) || defined(XP_WIN) || defined(XP_MACOSX) +static bool EnvHasValue(const char* name) { + const char* val = getenv(name); + return (val && *val); +} +#endif + +#ifdef XP_WIN +/** + * Obtains the update ID from the secure id file located in secure output + * directory. + * + * @param outBuf + * A buffer of size UUID_LEN (e.g. 37) to store the result. The uuid is + * 36 characters in length and 1 more for null termination. + * @return true if successful + */ +bool GetSecureID(char* outBuf) { + NS_tchar idFilePath[MAX_PATH + 1] = {L'\0'}; + if (!GetSecureOutputFilePath(gPatchDirPath, L".id", idFilePath)) { + return false; + } + + AutoFile idFile(NS_tfopen(idFilePath, NS_T("rb"))); + if (idFile == nullptr) { + return false; + } + + size_t read = fread(outBuf, UUID_LEN - 1, 1, idFile); + if (read != 1) { + return false; + } + + outBuf[UUID_LEN - 1] = '\0'; + return true; +} +#endif + +/** + * Calls LogFinish for the update log. On Windows, the unelevated updater copies + * the update status file and the update log file that were written by the + * elevated updater from the secure directory to the update patch directory. + * + * NOTE: All calls to WriteStatusFile MUST happen before calling output_finish + * because this function copies the update status file for the elevated + * updater and writing the status file after calling output_finish will + * overwrite it. + */ +static void output_finish() { + LogFinish(); +#ifdef XP_WIN + if (gCopyOutputFiles) { + NS_tchar srcStatusPath[MAXPATHLEN + 1] = {NS_T('\0')}; + if (GetSecureOutputFilePath(gPatchDirPath, L".status", srcStatusPath)) { + NS_tchar dstStatusPath[MAXPATHLEN + 1] = {NS_T('\0')}; + NS_tsnprintf(dstStatusPath, + sizeof(dstStatusPath) / sizeof(dstStatusPath[0]), + NS_T("%s\\update.status"), gPatchDirPath); + CopyFileW(srcStatusPath, dstStatusPath, false); + } + + NS_tchar srcLogPath[MAXPATHLEN + 1] = {NS_T('\0')}; + if (GetSecureOutputFilePath(gPatchDirPath, L".log", srcLogPath)) { + NS_tchar dstLogPath[MAXPATHLEN + 1] = {NS_T('\0')}; + NS_tsnprintf(dstLogPath, sizeof(dstLogPath) / sizeof(dstLogPath[0]), + NS_T("%s\\update.log"), gPatchDirPath); + CopyFileW(srcLogPath, dstLogPath, false); + } + } +#endif +} + +/** + * Coverts a relative update path to a full path. + * + * @param relpath + * The relative path to convert to a full path. + * @return valid filesystem full path or nullptr if memory allocation fails. + */ +static NS_tchar* get_full_path(const NS_tchar* relpath) { + NS_tchar* destpath = sStagedUpdate ? gWorkingDirPath : gInstallDirPath; + size_t lendestpath = NS_tstrlen(destpath); + size_t lenrelpath = NS_tstrlen(relpath); + NS_tchar* s = new NS_tchar[lendestpath + lenrelpath + 2]; + + NS_tchar* c = s; + + NS_tstrcpy(c, destpath); + c += lendestpath; + NS_tstrcat(c, NS_T("/")); + c++; + + NS_tstrcat(c, relpath); + c += lenrelpath; + *c = NS_T('\0'); + return s; +} + +/** + * Converts a full update path into a relative path; reverses get_full_path. + * + * @param fullpath + * The absolute path to convert into a relative path. + * return pointer to the location within fullpath where the relative path starts + * or fullpath itself if it already looks relative. + */ +#ifndef XP_WIN +static const NS_tchar* get_relative_path(const NS_tchar* fullpath) { + if (fullpath[0] != '/') { + return fullpath; + } + + NS_tchar* prefix = sStagedUpdate ? gWorkingDirPath : gInstallDirPath; + + // If the path isn't long enough to be absolute, return it as-is. + if (NS_tstrlen(fullpath) <= NS_tstrlen(prefix)) { + return fullpath; + } + + return fullpath + NS_tstrlen(prefix) + 1; +} +#endif + +/** + * Gets the platform specific path and performs simple checks to the path. If + * the path checks don't pass nullptr will be returned. + * + * @param line + * The line from the manifest that contains the path. + * @param isdir + * Whether the path is a directory path. Defaults to false. + * @return valid filesystem path or nullptr if the path checks fail. + */ +static NS_tchar* get_valid_path(NS_tchar** line, bool isdir = false) { + NS_tchar* path = mstrtok(kQuote, line); + if (!path) { + LOG(("get_valid_path: unable to determine path: " LOG_S, *line)); + return nullptr; + } + + // All paths must be relative from the current working directory + if (path[0] == NS_T('/')) { + LOG(("get_valid_path: path must be relative: " LOG_S, path)); + return nullptr; + } + +#ifdef XP_WIN + // All paths must be relative from the current working directory + if (path[0] == NS_T('\\') || path[1] == NS_T(':')) { + LOG(("get_valid_path: path must be relative: " LOG_S, path)); + return nullptr; + } +#endif + + if (isdir) { + // Directory paths must have a trailing forward slash. + if (path[NS_tstrlen(path) - 1] != NS_T('/')) { + LOG( + ("get_valid_path: directory paths must have a trailing forward " + "slash: " LOG_S, + path)); + return nullptr; + } + + // Remove the trailing forward slash because stat on Windows will return + // ENOENT if the path has a trailing slash. + path[NS_tstrlen(path) - 1] = NS_T('\0'); + } + + // Don't allow relative paths that resolve to a parent directory. + if (NS_tstrstr(path, NS_T("..")) != nullptr) { + LOG(("get_valid_path: paths must not contain '..': " LOG_S, path)); + return nullptr; + } + + return path; +} + +/* + * Gets a quoted path. The return value is malloc'd and it is the responsibility + * of the caller to free it. + * + * @param path + * The path to quote. + * @return On success the quoted path and nullptr otherwise. + */ +static NS_tchar* get_quoted_path(const NS_tchar* path) { + size_t lenQuote = NS_tstrlen(kQuote); + size_t lenPath = NS_tstrlen(path); + size_t len = lenQuote + lenPath + lenQuote + 1; + + NS_tchar* s = (NS_tchar*)malloc(len * sizeof(NS_tchar)); + if (!s) { + return nullptr; + } + + NS_tchar* c = s; + NS_tstrcpy(c, kQuote); + c += lenQuote; + NS_tstrcat(c, path); + c += lenPath; + NS_tstrcat(c, kQuote); + c += lenQuote; + *c = NS_T('\0'); + return s; +} + +static void ensure_write_permissions(const NS_tchar* path) { +#ifdef XP_WIN + (void)_wchmod(path, _S_IREAD | _S_IWRITE); +#else + struct stat fs; + if (!stat(path, &fs) && !(fs.st_mode & S_IWUSR)) { + (void)chmod(path, fs.st_mode | S_IWUSR); + } +#endif +} + +static int ensure_remove(const NS_tchar* path) { + ensure_write_permissions(path); + int rv = NS_tremove(path); + if (rv) { + LOG(("ensure_remove: failed to remove file: " LOG_S ", rv: %d, err: %d", + path, rv, errno)); + } + return rv; +} + +// Remove the directory pointed to by path and all of its files and +// sub-directories. +static int ensure_remove_recursive(const NS_tchar* path, + bool continueEnumOnFailure = false) { + // We use lstat rather than stat here so that we can successfully remove + // symlinks. + struct NS_tstat_t sInfo; + int rv = NS_tlstat(path, &sInfo); + if (rv) { + // This error is benign + return rv; + } + if (!S_ISDIR(sInfo.st_mode)) { + return ensure_remove(path); + } + + NS_tDIR* dir; + NS_tdirent* entry; + + dir = NS_topendir(path); + if (!dir) { + LOG(("ensure_remove_recursive: unable to open directory: " LOG_S + ", rv: %d, err: %d", + path, rv, errno)); + return rv; + } + + while ((entry = NS_treaddir(dir)) != 0) { + if (NS_tstrcmp(entry->d_name, NS_T(".")) && + NS_tstrcmp(entry->d_name, NS_T(".."))) { + NS_tchar childPath[MAXPATHLEN]; + NS_tsnprintf(childPath, sizeof(childPath) / sizeof(childPath[0]), + NS_T("%s/%s"), path, entry->d_name); + rv = ensure_remove_recursive(childPath); + if (rv && !continueEnumOnFailure) { + break; + } + } + } + + NS_tclosedir(dir); + + if (rv == OK) { + ensure_write_permissions(path); + rv = NS_trmdir(path); + if (rv) { + LOG(("ensure_remove_recursive: unable to remove directory: " LOG_S + ", rv: %d, err: %d", + path, rv, errno)); + } + } + return rv; +} + +static bool is_read_only(const NS_tchar* flags) { + size_t length = NS_tstrlen(flags); + if (length == 0) { + return false; + } + + // Make sure the string begins with "r" + if (flags[0] != NS_T('r')) { + return false; + } + + // Look for "r+" or "r+b" + if (length > 1 && flags[1] == NS_T('+')) { + return false; + } + + // Look for "rb+" + if (NS_tstrcmp(flags, NS_T("rb+")) == 0) { + return false; + } + + return true; +} + +static FILE* ensure_open(const NS_tchar* path, const NS_tchar* flags, + unsigned int options) { + ensure_write_permissions(path); + FILE* f = NS_tfopen(path, flags); + if (is_read_only(flags)) { + // Don't attempt to modify the file permissions if the file is being opened + // in read-only mode. + return f; + } + if (NS_tchmod(path, options) != 0) { + if (f != nullptr) { + fclose(f); + } + return nullptr; + } + struct NS_tstat_t ss; + if (NS_tstat(path, &ss) != 0 || ss.st_mode != options) { + if (f != nullptr) { + fclose(f); + } + return nullptr; + } + return f; +} + +// Ensure that the directory containing this file exists. +static int ensure_parent_dir(const NS_tchar* path) { + int rv = OK; + + NS_tchar* slash = (NS_tchar*)NS_tstrrchr(path, NS_T('/')); + if (slash) { + *slash = NS_T('\0'); + rv = ensure_parent_dir(path); + // Only attempt to create the directory if we're not at the root + if (rv == OK && *path) { + rv = NS_tmkdir(path, 0755); + // If the directory already exists, then ignore the error. + if (rv < 0 && errno != EEXIST) { + LOG(("ensure_parent_dir: failed to create directory: " LOG_S ", " + "err: %d", + path, errno)); + rv = WRITE_ERROR; + } else { + rv = OK; + } + } + *slash = NS_T('/'); + } + return rv; +} + +#ifdef XP_UNIX +static int ensure_copy_symlink(const NS_tchar* path, const NS_tchar* dest) { + // Copy symlinks by creating a new symlink to the same target + NS_tchar target[MAXPATHLEN + 1] = {NS_T('\0')}; + int rv = readlink(path, target, MAXPATHLEN); + if (rv == -1) { + LOG(("ensure_copy_symlink: failed to read the link: " LOG_S ", err: %d", + path, errno)); + return READ_ERROR; + } + rv = symlink(target, dest); + if (rv == -1) { + LOG(("ensure_copy_symlink: failed to create the new link: " LOG_S + ", target: " LOG_S " err: %d", + dest, target, errno)); + return READ_ERROR; + } + return 0; +} +#endif + +// Copy the file named path onto a new file named dest. +static int ensure_copy(const NS_tchar* path, const NS_tchar* dest) { +#ifdef XP_WIN + // Fast path for Windows + bool result = CopyFileW(path, dest, false); + if (!result) { + LOG(("ensure_copy: failed to copy the file " LOG_S " over to " LOG_S + ", lasterr: %lx", + path, dest, GetLastError())); + return WRITE_ERROR_FILE_COPY; + } + return OK; +#else + struct NS_tstat_t ss; + int rv = NS_tlstat(path, &ss); + if (rv) { + LOG(("ensure_copy: failed to read file status info: " LOG_S ", err: %d", + path, errno)); + return READ_ERROR; + } + +# ifdef XP_UNIX + if (S_ISLNK(ss.st_mode)) { + return ensure_copy_symlink(path, dest); + } +# endif + + AutoFile infile(ensure_open(path, NS_T("rb"), ss.st_mode)); + if (!infile) { + LOG(("ensure_copy: failed to open the file for reading: " LOG_S ", err: %d", + path, errno)); + return READ_ERROR; + } + AutoFile outfile(ensure_open(dest, NS_T("wb"), ss.st_mode)); + if (!outfile) { + LOG(("ensure_copy: failed to open the file for writing: " LOG_S ", err: %d", + dest, errno)); + return WRITE_ERROR; + } + + // This block size was chosen pretty arbitrarily but seems like a reasonable + // compromise. For example, the optimal block size on a modern OS X machine + // is 100k */ + const int blockSize = 32 * 1024; + void* buffer = malloc(blockSize); + if (!buffer) { + return UPDATER_MEM_ERROR; + } + + while (!feof(infile.get())) { + size_t read = fread(buffer, 1, blockSize, infile); + if (ferror(infile.get())) { + LOG(("ensure_copy: failed to read the file: " LOG_S ", err: %d", path, + errno)); + free(buffer); + return READ_ERROR; + } + + size_t written = 0; + + while (written < read) { + size_t chunkWritten = fwrite(buffer, 1, read - written, outfile); + if (chunkWritten <= 0) { + LOG(("ensure_copy: failed to write the file: " LOG_S ", err: %d", dest, + errno)); + free(buffer); + return WRITE_ERROR_FILE_COPY; + } + + written += chunkWritten; + } + } + + rv = NS_tchmod(dest, ss.st_mode); + + free(buffer); + return rv; +#endif +} + +template <unsigned N> +struct copy_recursive_skiplist { + NS_tchar paths[N][MAXPATHLEN]; + + void append(unsigned index, const NS_tchar* path, const NS_tchar* suffix) { + NS_tsnprintf(paths[index], MAXPATHLEN, NS_T("%s/%s"), path, suffix); + } + + bool find(const NS_tchar* path) { + for (int i = 0; i < static_cast<int>(N); ++i) { + if (!NS_tstricmp(paths[i], path)) { + return true; + } + } + return false; + } +}; + +// Copy all of the files and subdirectories under path to a new directory named +// dest. The path names in the skiplist will be skipped and will not be copied. +template <unsigned N> +static int ensure_copy_recursive(const NS_tchar* path, const NS_tchar* dest, + copy_recursive_skiplist<N>& skiplist) { + struct NS_tstat_t sInfo; + int rv = NS_tlstat(path, &sInfo); + if (rv) { + LOG(("ensure_copy_recursive: path doesn't exist: " LOG_S + ", rv: %d, err: %d", + path, rv, errno)); + return READ_ERROR; + } + +#ifdef XP_UNIX + if (S_ISLNK(sInfo.st_mode)) { + return ensure_copy_symlink(path, dest); + } +#endif + + if (!S_ISDIR(sInfo.st_mode)) { + return ensure_copy(path, dest); + } + + rv = NS_tmkdir(dest, sInfo.st_mode); + if (rv < 0 && errno != EEXIST) { + LOG(("ensure_copy_recursive: could not create destination directory: " LOG_S + ", rv: %d, err: %d", + path, rv, errno)); + return WRITE_ERROR; + } + + NS_tDIR* dir; + NS_tdirent* entry; + + dir = NS_topendir(path); + if (!dir) { + LOG(("ensure_copy_recursive: path is not a directory: " LOG_S + ", rv: %d, err: %d", + path, rv, errno)); + return READ_ERROR; + } + + while ((entry = NS_treaddir(dir)) != 0) { + if (NS_tstrcmp(entry->d_name, NS_T(".")) && + NS_tstrcmp(entry->d_name, NS_T(".."))) { + NS_tchar childPath[MAXPATHLEN]; + NS_tsnprintf(childPath, sizeof(childPath) / sizeof(childPath[0]), + NS_T("%s/%s"), path, entry->d_name); + if (skiplist.find(childPath)) { + continue; + } + NS_tchar childPathDest[MAXPATHLEN]; + NS_tsnprintf(childPathDest, + sizeof(childPathDest) / sizeof(childPathDest[0]), + NS_T("%s/%s"), dest, entry->d_name); + rv = ensure_copy_recursive(childPath, childPathDest, skiplist); + if (rv) { + break; + } + } + } + NS_tclosedir(dir); + return rv; +} + +// Renames the specified file to the new file specified. If the destination file +// exists it is removed. +static int rename_file(const NS_tchar* spath, const NS_tchar* dpath, + bool allowDirs = false) { + int rv = ensure_parent_dir(dpath); + if (rv) { + return rv; + } + + struct NS_tstat_t spathInfo; + rv = NS_tstat(spath, &spathInfo); + if (rv) { + LOG(("rename_file: failed to read file status info: " LOG_S ", " + "err: %d", + spath, errno)); + return READ_ERROR; + } + + if (!S_ISREG(spathInfo.st_mode)) { + if (allowDirs && !S_ISDIR(spathInfo.st_mode)) { + LOG(("rename_file: path present, but not a file: " LOG_S ", err: %d", + spath, errno)); + return RENAME_ERROR_EXPECTED_FILE; + } + LOG(("rename_file: proceeding to rename the directory")); + } + + if (!NS_taccess(dpath, F_OK)) { + if (ensure_remove(dpath)) { + LOG( + ("rename_file: destination file exists and could not be " + "removed: " LOG_S, + dpath)); + return WRITE_ERROR_DELETE_FILE; + } + } + + if (NS_trename(spath, dpath) != 0) { + LOG(("rename_file: failed to rename file - src: " LOG_S ", " + "dst:" LOG_S ", err: %d", + spath, dpath, errno)); + return WRITE_ERROR; + } + + return OK; +} + +#ifdef XP_WIN +// Remove the directory pointed to by path and all of its files and +// sub-directories. If a file is in use move it to the tobedeleted directory +// and attempt to schedule removal of the file on reboot +static int remove_recursive_on_reboot(const NS_tchar* path, + const NS_tchar* deleteDir) { + struct NS_tstat_t sInfo; + int rv = NS_tlstat(path, &sInfo); + if (rv) { + // This error is benign + return rv; + } + + if (!S_ISDIR(sInfo.st_mode)) { + NS_tchar tmpDeleteFile[MAXPATHLEN + 1]; + GetUUIDTempFilePath(deleteDir, L"rep", tmpDeleteFile); + if (NS_tremove(tmpDeleteFile) && errno != ENOENT) { + LOG(("remove_recursive_on_reboot: failed to remove temporary file: " LOG_S + ", err: %d", + tmpDeleteFile, errno)); + } + rv = rename_file(path, tmpDeleteFile, false); + if (MoveFileEx(rv ? path : tmpDeleteFile, nullptr, + MOVEFILE_DELAY_UNTIL_REBOOT)) { + LOG( + ("remove_recursive_on_reboot: file will be removed on OS " + "reboot: " LOG_S, + rv ? path : tmpDeleteFile)); + } else { + LOG(( + "remove_recursive_on_reboot: failed to schedule OS reboot removal of " + "file: " LOG_S, + rv ? path : tmpDeleteFile)); + } + return rv; + } + + NS_tDIR* dir; + NS_tdirent* entry; + + dir = NS_topendir(path); + if (!dir) { + LOG(("remove_recursive_on_reboot: unable to open directory: " LOG_S + ", rv: %d, err: %d", + path, rv, errno)); + return rv; + } + + while ((entry = NS_treaddir(dir)) != 0) { + if (NS_tstrcmp(entry->d_name, NS_T(".")) && + NS_tstrcmp(entry->d_name, NS_T(".."))) { + NS_tchar childPath[MAXPATHLEN]; + NS_tsnprintf(childPath, sizeof(childPath) / sizeof(childPath[0]), + NS_T("%s/%s"), path, entry->d_name); + // There is no need to check the return value of this call since this + // function is only called after an update is successful and there is not + // much that can be done to recover if it isn't successful. There is also + // no need to log the value since it will have already been logged. + remove_recursive_on_reboot(childPath, deleteDir); + } + } + + NS_tclosedir(dir); + + if (rv == OK) { + ensure_write_permissions(path); + rv = NS_trmdir(path); + if (rv) { + LOG(("remove_recursive_on_reboot: unable to remove directory: " LOG_S + ", rv: %d, err: %d", + path, rv, errno)); + } + } + return rv; +} +#endif + +//----------------------------------------------------------------------------- + +// Create a backup of the specified file by renaming it. +static int backup_create(const NS_tchar* path) { + NS_tchar backup[MAXPATHLEN]; + NS_tsnprintf(backup, sizeof(backup) / sizeof(backup[0]), + NS_T("%s") BACKUP_EXT, path); + + return rename_file(path, backup); +} + +// Rename the backup of the specified file that was created by renaming it back +// to the original file. +static int backup_restore(const NS_tchar* path, const NS_tchar* relPath) { + NS_tchar backup[MAXPATHLEN]; + NS_tsnprintf(backup, sizeof(backup) / sizeof(backup[0]), + NS_T("%s") BACKUP_EXT, path); + + NS_tchar relBackup[MAXPATHLEN]; + NS_tsnprintf(relBackup, sizeof(relBackup) / sizeof(relBackup[0]), + NS_T("%s") BACKUP_EXT, relPath); + + if (NS_taccess(backup, F_OK)) { + LOG(("backup_restore: backup file doesn't exist: " LOG_S, relBackup)); + return OK; + } + + return rename_file(backup, path); +} + +// Discard the backup of the specified file that was created by renaming it. +static int backup_discard(const NS_tchar* path, const NS_tchar* relPath) { + NS_tchar backup[MAXPATHLEN]; + NS_tsnprintf(backup, sizeof(backup) / sizeof(backup[0]), + NS_T("%s") BACKUP_EXT, path); + + NS_tchar relBackup[MAXPATHLEN]; + NS_tsnprintf(relBackup, sizeof(relBackup) / sizeof(relBackup[0]), + NS_T("%s") BACKUP_EXT, relPath); + + // Nothing to discard + if (NS_taccess(backup, F_OK)) { + return OK; + } + + int rv = ensure_remove(backup); +#if defined(XP_WIN) + if (rv && !sStagedUpdate && !sReplaceRequest) { + LOG(("backup_discard: unable to remove: " LOG_S, relBackup)); + NS_tchar path[MAXPATHLEN + 1]; + GetUUIDTempFilePath(gDeleteDirPath, L"moz", path); + if (rename_file(backup, path)) { + LOG(("backup_discard: failed to rename file:" LOG_S ", dst:" LOG_S, + relBackup, relPath)); + return WRITE_ERROR_DELETE_BACKUP; + } + // The MoveFileEx call to remove the file on OS reboot will fail if the + // process doesn't have write access to the HKEY_LOCAL_MACHINE registry key + // but this is ok since the installer / uninstaller will delete the + // directory containing the file along with its contents after an update is + // applied, on reinstall, and on uninstall. + if (MoveFileEx(path, nullptr, MOVEFILE_DELAY_UNTIL_REBOOT)) { + LOG( + ("backup_discard: file renamed and will be removed on OS " + "reboot: " LOG_S, + relPath)); + } else { + LOG( + ("backup_discard: failed to schedule OS reboot removal of " + "file: " LOG_S, + relPath)); + } + } +#else + if (rv) { + return WRITE_ERROR_DELETE_BACKUP; + } +#endif + + return OK; +} + +// Helper function for post-processing a temporary backup. +static void backup_finish(const NS_tchar* path, const NS_tchar* relPath, + int status) { + if (status == OK) { + backup_discard(path, relPath); + } else { + backup_restore(path, relPath); + } +} + +//----------------------------------------------------------------------------- + +static int DoUpdate(); + +class Action { + public: + Action() : mProgressCost(1), mNext(nullptr) {} + virtual ~Action() = default; + + virtual int Parse(NS_tchar* line) = 0; + + // Do any preprocessing to ensure that the action can be performed. Execute + // will be called if this Action and all others return OK from this method. + virtual int Prepare() = 0; + + // Perform the operation. Return OK to indicate success. After all actions + // have been executed, Finish will be called. A requirement of Execute is + // that its operation be reversable from Finish. + virtual int Execute() = 0; + + // Finish is called after execution of all actions. If status is OK, then + // all actions were successfully executed. Otherwise, some action failed. + virtual void Finish(int status) = 0; + + int mProgressCost; + + private: + Action* mNext; + + friend class ActionList; +}; + +class RemoveFile : public Action { + public: + RemoveFile() : mSkip(0) {} + + int Parse(NS_tchar* line) override; + int Prepare() override; + int Execute() override; + void Finish(int status) override; + + private: + mozilla::UniquePtr<NS_tchar[]> mFile; + mozilla::UniquePtr<NS_tchar[]> mRelPath; + int mSkip; +}; + +int RemoveFile::Parse(NS_tchar* line) { + // format "<deadfile>" + + NS_tchar* validPath = get_valid_path(&line); + if (!validPath) { + return PARSE_ERROR; + } + + mRelPath = mozilla::MakeUnique<NS_tchar[]>(MAXPATHLEN); + NS_tstrcpy(mRelPath.get(), validPath); + + mFile.reset(get_full_path(validPath)); + if (!mFile) { + return PARSE_ERROR; + } + + return OK; +} + +int RemoveFile::Prepare() { + // Skip the file if it already doesn't exist. + int rv = NS_taccess(mFile.get(), F_OK); + if (rv) { + mSkip = 1; + mProgressCost = 0; + return OK; + } + + LOG(("PREPARE REMOVEFILE " LOG_S, mRelPath.get())); + + // Make sure that we're actually a file... + struct NS_tstat_t fileInfo; + rv = NS_tstat(mFile.get(), &fileInfo); + if (rv) { + LOG(("failed to read file status info: " LOG_S ", err: %d", mFile.get(), + errno)); + return READ_ERROR; + } + + if (!S_ISREG(fileInfo.st_mode)) { + LOG(("path present, but not a file: " LOG_S, mFile.get())); + return DELETE_ERROR_EXPECTED_FILE; + } + + NS_tchar* slash = (NS_tchar*)NS_tstrrchr(mFile.get(), NS_T('/')); + if (slash) { + *slash = NS_T('\0'); + rv = NS_taccess(mFile.get(), W_OK); + *slash = NS_T('/'); + } else { + rv = NS_taccess(NS_T("."), W_OK); + } + + if (rv) { + LOG(("access failed: %d", errno)); + return WRITE_ERROR_FILE_ACCESS_DENIED; + } + + return OK; +} + +int RemoveFile::Execute() { + if (mSkip) { + return OK; + } + + LOG(("EXECUTE REMOVEFILE " LOG_S, mRelPath.get())); + + // The file is checked for existence here and in Prepare since it might have + // been removed by a separate instruction: bug 311099. + int rv = NS_taccess(mFile.get(), F_OK); + if (rv) { + LOG(("file cannot be removed because it does not exist; skipping")); + mSkip = 1; + return OK; + } + + if (sStagedUpdate) { + // Staged updates don't need backup files so just remove it. + rv = ensure_remove(mFile.get()); + if (rv) { + return rv; + } + } else { + // Rename the old file. It will be removed in Finish. + rv = backup_create(mFile.get()); + if (rv) { + LOG(("backup_create failed: %d", rv)); + return rv; + } + } + + return OK; +} + +void RemoveFile::Finish(int status) { + if (mSkip) { + return; + } + + LOG(("FINISH REMOVEFILE " LOG_S, mRelPath.get())); + + // Staged updates don't create backup files. + if (!sStagedUpdate) { + backup_finish(mFile.get(), mRelPath.get(), status); + } +} + +class RemoveDir : public Action { + public: + RemoveDir() : mSkip(0) {} + + int Parse(NS_tchar* line) override; + int Prepare() override; // check that the source dir exists + int Execute() override; + void Finish(int status) override; + + private: + mozilla::UniquePtr<NS_tchar[]> mDir; + mozilla::UniquePtr<NS_tchar[]> mRelPath; + int mSkip; +}; + +int RemoveDir::Parse(NS_tchar* line) { + // format "<deaddir>/" + + NS_tchar* validPath = get_valid_path(&line, true); + if (!validPath) { + return PARSE_ERROR; + } + + mRelPath = mozilla::MakeUnique<NS_tchar[]>(MAXPATHLEN); + NS_tstrcpy(mRelPath.get(), validPath); + + mDir.reset(get_full_path(validPath)); + if (!mDir) { + return PARSE_ERROR; + } + + return OK; +} + +int RemoveDir::Prepare() { + // We expect the directory to exist if we are to remove it. + int rv = NS_taccess(mDir.get(), F_OK); + if (rv) { + mSkip = 1; + mProgressCost = 0; + return OK; + } + + LOG(("PREPARE REMOVEDIR " LOG_S "/", mRelPath.get())); + + // Make sure that we're actually a dir. + struct NS_tstat_t dirInfo; + rv = NS_tstat(mDir.get(), &dirInfo); + if (rv) { + LOG(("failed to read directory status info: " LOG_S ", err: %d", + mRelPath.get(), errno)); + return READ_ERROR; + } + + if (!S_ISDIR(dirInfo.st_mode)) { + LOG(("path present, but not a directory: " LOG_S, mRelPath.get())); + return DELETE_ERROR_EXPECTED_DIR; + } + + rv = NS_taccess(mDir.get(), W_OK); + if (rv) { + LOG(("access failed: %d, %d", rv, errno)); + return WRITE_ERROR_DIR_ACCESS_DENIED; + } + + return OK; +} + +int RemoveDir::Execute() { + if (mSkip) { + return OK; + } + + LOG(("EXECUTE REMOVEDIR " LOG_S "/", mRelPath.get())); + + // The directory is checked for existence at every step since it might have + // been removed by a separate instruction: bug 311099. + int rv = NS_taccess(mDir.get(), F_OK); + if (rv) { + LOG(("directory no longer exists; skipping")); + mSkip = 1; + } + + return OK; +} + +void RemoveDir::Finish(int status) { + if (mSkip || status != OK) { + return; + } + + LOG(("FINISH REMOVEDIR " LOG_S "/", mRelPath.get())); + + // The directory is checked for existence at every step since it might have + // been removed by a separate instruction: bug 311099. + int rv = NS_taccess(mDir.get(), F_OK); + if (rv) { + LOG(("directory no longer exists; skipping")); + return; + } + + if (status == OK) { + if (NS_trmdir(mDir.get())) { + LOG(("non-fatal error removing directory: " LOG_S "/, rv: %d, err: %d", + mRelPath.get(), rv, errno)); + } + } +} + +class AddFile : public Action { + public: + AddFile() : mAdded(false) {} + + int Parse(NS_tchar* line) override; + int Prepare() override; + int Execute() override; + void Finish(int status) override; + + private: + mozilla::UniquePtr<NS_tchar[]> mFile; + mozilla::UniquePtr<NS_tchar[]> mRelPath; + bool mAdded; +}; + +int AddFile::Parse(NS_tchar* line) { + // format "<newfile>" + + NS_tchar* validPath = get_valid_path(&line); + if (!validPath) { + return PARSE_ERROR; + } + + mRelPath = mozilla::MakeUnique<NS_tchar[]>(MAXPATHLEN); + NS_tstrcpy(mRelPath.get(), validPath); + + mFile.reset(get_full_path(validPath)); + if (!mFile) { + return PARSE_ERROR; + } + + return OK; +} + +int AddFile::Prepare() { + LOG(("PREPARE ADD " LOG_S, mRelPath.get())); + + return OK; +} + +int AddFile::Execute() { + LOG(("EXECUTE ADD " LOG_S, mRelPath.get())); + + int rv; + + // First make sure that we can actually get rid of any existing file. + rv = NS_taccess(mFile.get(), F_OK); + if (rv == 0) { + if (sStagedUpdate) { + // Staged updates don't need backup files so just remove it. + rv = ensure_remove(mFile.get()); + } else { + rv = backup_create(mFile.get()); + } + if (rv) { + return rv; + } + } else { + rv = ensure_parent_dir(mFile.get()); + if (rv) { + return rv; + } + } + +#ifdef XP_WIN + char sourcefile[MAXPATHLEN]; + if (!WideCharToMultiByte(CP_UTF8, 0, mRelPath.get(), -1, sourcefile, + MAXPATHLEN, nullptr, nullptr)) { + LOG(("error converting wchar to utf8: %lu", GetLastError())); + return STRING_CONVERSION_ERROR; + } + + rv = gArchiveReader.ExtractFile(sourcefile, mFile.get()); +#else + rv = gArchiveReader.ExtractFile(mRelPath.get(), mFile.get()); +#endif + if (!rv) { + mAdded = true; + } + return rv; +} + +void AddFile::Finish(int status) { + LOG(("FINISH ADD " LOG_S, mRelPath.get())); + // Staged updates don't create backup files. + if (!sStagedUpdate) { + // When there is an update failure and a file has been added it is removed + // here since there might not be a backup to replace it. + if (status && mAdded) { + if (NS_tremove(mFile.get()) && errno != ENOENT) { + LOG(("non-fatal error after update failure removing added file: " LOG_S + ", err: %d", + mFile.get(), errno)); + } + } + backup_finish(mFile.get(), mRelPath.get(), status); + } +} + +class PatchFile : public Action { + public: + PatchFile() : mPatchFile(nullptr), mPatchIndex(-1), buf(nullptr) {} + + ~PatchFile() override; + + int Parse(NS_tchar* line) override; + int Prepare() override; // should check for patch file and for checksum here + int Execute() override; + void Finish(int status) override; + + private: + int LoadSourceFile(FILE* ofile); + + static int sPatchIndex; + + const NS_tchar* mPatchFile; + mozilla::UniquePtr<NS_tchar[]> mFile; + mozilla::UniquePtr<NS_tchar[]> mFileRelPath; + int mPatchIndex; + MBSPatchHeader header; + unsigned char* buf; + NS_tchar spath[MAXPATHLEN]; + AutoFile mPatchStream; +}; + +int PatchFile::sPatchIndex = 0; + +PatchFile::~PatchFile() { + // Make sure mPatchStream gets unlocked on Windows; the system will do that, + // but not until some indeterminate future time, and we want determinism. + // Normally this happens at the end of Execute, when we close the stream; + // this call is here in case Execute errors out. +#ifdef XP_WIN + if (mPatchStream) { + UnlockFile((HANDLE)_get_osfhandle(fileno(mPatchStream)), 0, 0, -1, -1); + } +#endif + // Patch files are written to the <working_dir>/updating directory which is + // removed after the update has finished so don't delete patch files here. + + if (buf) { + free(buf); + } +} + +int PatchFile::LoadSourceFile(FILE* ofile) { + struct stat os; + int rv = fstat(fileno((FILE*)ofile), &os); + if (rv) { + LOG(("LoadSourceFile: unable to stat destination file: " LOG_S ", " + "err: %d", + mFileRelPath.get(), errno)); + return READ_ERROR; + } + + if (uint32_t(os.st_size) != header.slen) { + LOG( + ("LoadSourceFile: destination file size %d does not match expected " + "size %d", + uint32_t(os.st_size), header.slen)); + return LOADSOURCE_ERROR_WRONG_SIZE; + } + + buf = (unsigned char*)malloc(header.slen); + if (!buf) { + return UPDATER_MEM_ERROR; + } + + size_t r = header.slen; + unsigned char* rb = buf; + while (r) { + const size_t count = mmin(SSIZE_MAX, r); + size_t c = fread(rb, 1, count, ofile); + if (c != count) { + LOG(("LoadSourceFile: error reading destination file: " LOG_S, + mFileRelPath.get())); + return READ_ERROR; + } + + r -= c; + rb += c; + } + + // Verify that the contents of the source file correspond to what we expect. + + unsigned int crc = crc32(buf, header.slen); + + if (crc != header.scrc32) { + LOG( + ("LoadSourceFile: destination file crc %d does not match expected " + "crc %d", + crc, header.scrc32)); + return CRC_ERROR; + } + + return OK; +} + +int PatchFile::Parse(NS_tchar* line) { + // format "<patchfile>" "<filetopatch>" + + // Get the path to the patch file inside of the mar + mPatchFile = mstrtok(kQuote, &line); + if (!mPatchFile) { + return PARSE_ERROR; + } + + // consume whitespace between args + NS_tchar* q = mstrtok(kQuote, &line); + if (!q) { + return PARSE_ERROR; + } + + NS_tchar* validPath = get_valid_path(&line); + if (!validPath) { + return PARSE_ERROR; + } + + mFileRelPath = mozilla::MakeUnique<NS_tchar[]>(MAXPATHLEN); + NS_tstrcpy(mFileRelPath.get(), validPath); + + mFile.reset(get_full_path(validPath)); + if (!mFile) { + return PARSE_ERROR; + } + + return OK; +} + +int PatchFile::Prepare() { + LOG(("PREPARE PATCH " LOG_S, mFileRelPath.get())); + + // extract the patch to a temporary file + mPatchIndex = sPatchIndex++; + + NS_tsnprintf(spath, sizeof(spath) / sizeof(spath[0]), + NS_T("%s/updating/%d.patch"), gWorkingDirPath, mPatchIndex); + + // The removal of pre-existing patch files here is in case a previous update + // crashed and left these files behind. + if (NS_tremove(spath) && errno != ENOENT) { + LOG(("failure removing pre-existing patch file: " LOG_S ", err: %d", spath, + errno)); + return WRITE_ERROR; + } + + mPatchStream = NS_tfopen(spath, NS_T("wb+")); + if (!mPatchStream) { + return WRITE_ERROR; + } + +#ifdef XP_WIN + // Lock the patch file, so it can't be messed with between + // when we're done creating it and when we go to apply it. + if (!LockFile((HANDLE)_get_osfhandle(fileno(mPatchStream)), 0, 0, -1, -1)) { + LOG(("Couldn't lock patch file: %lu", GetLastError())); + return LOCK_ERROR_PATCH_FILE; + } + + char sourcefile[MAXPATHLEN]; + if (!WideCharToMultiByte(CP_UTF8, 0, mPatchFile, -1, sourcefile, MAXPATHLEN, + nullptr, nullptr)) { + LOG(("error converting wchar to utf8: %lu", GetLastError())); + return STRING_CONVERSION_ERROR; + } + + int rv = gArchiveReader.ExtractFileToStream(sourcefile, mPatchStream); +#else + int rv = gArchiveReader.ExtractFileToStream(mPatchFile, mPatchStream); +#endif + + return rv; +} + +int PatchFile::Execute() { + LOG(("EXECUTE PATCH " LOG_S, mFileRelPath.get())); + + fseek(mPatchStream, 0, SEEK_SET); + + int rv = MBS_ReadHeader(mPatchStream, &header); + if (rv) { + return rv; + } + + FILE* origfile = nullptr; +#ifdef XP_WIN + if (NS_tstrcmp(mFileRelPath.get(), gCallbackRelPath) == 0) { + // Read from the copy of the callback when patching since the callback can't + // be opened for reading to prevent the application from being launched. + origfile = NS_tfopen(gCallbackBackupPath, NS_T("rb")); + } else { + origfile = NS_tfopen(mFile.get(), NS_T("rb")); + } +#else + origfile = NS_tfopen(mFile.get(), NS_T("rb")); +#endif + + if (!origfile) { + LOG(("unable to open destination file: " LOG_S ", err: %d", + mFileRelPath.get(), errno)); + return READ_ERROR; + } + + rv = LoadSourceFile(origfile); + fclose(origfile); + if (rv) { + LOG(("LoadSourceFile failed")); + return rv; + } + + // Rename the destination file if it exists before proceeding so it can be + // used to restore the file to its original state if there is an error. + struct NS_tstat_t ss; + rv = NS_tstat(mFile.get(), &ss); + if (rv) { + LOG(("failed to read file status info: " LOG_S ", err: %d", + mFileRelPath.get(), errno)); + return READ_ERROR; + } + + // Staged updates don't need backup files. + if (!sStagedUpdate) { + rv = backup_create(mFile.get()); + if (rv) { + return rv; + } + } + +#if defined(HAVE_POSIX_FALLOCATE) + AutoFile ofile(ensure_open(mFile.get(), NS_T("wb+"), ss.st_mode)); + posix_fallocate(fileno((FILE*)ofile), 0, header.dlen); +#elif defined(XP_WIN) + bool shouldTruncate = true; + // Creating the file, setting the size, and then closing the file handle + // lessens fragmentation more than any other method tested. Other methods that + // have been tested are: + // 1. _chsize / _chsize_s reduced fragmentation though not completely. + // 2. _get_osfhandle and then setting the size reduced fragmentation though + // not completely. There are also reports of _get_osfhandle failing on + // mingw. + HANDLE hfile = CreateFileW(mFile.get(), GENERIC_WRITE, 0, nullptr, + CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); + + if (hfile != INVALID_HANDLE_VALUE) { + if (SetFilePointer(hfile, header.dlen, nullptr, FILE_BEGIN) != + INVALID_SET_FILE_POINTER && + SetEndOfFile(hfile) != 0) { + shouldTruncate = false; + } + CloseHandle(hfile); + } + + AutoFile ofile(ensure_open( + mFile.get(), shouldTruncate ? NS_T("wb+") : NS_T("rb+"), ss.st_mode)); +#elif defined(XP_MACOSX) + AutoFile ofile(ensure_open(mFile.get(), NS_T("wb+"), ss.st_mode)); + // Modified code from FileUtils.cpp + fstore_t store = {F_ALLOCATECONTIG, F_PEOFPOSMODE, 0, header.dlen}; + // Try to get a continous chunk of disk space + rv = fcntl(fileno((FILE*)ofile), F_PREALLOCATE, &store); + if (rv == -1) { + // OK, perhaps we are too fragmented, allocate non-continuous + store.fst_flags = F_ALLOCATEALL; + rv = fcntl(fileno((FILE*)ofile), F_PREALLOCATE, &store); + } + + if (rv != -1) { + ftruncate(fileno((FILE*)ofile), header.dlen); + } +#else + AutoFile ofile(ensure_open(mFile.get(), NS_T("wb+"), ss.st_mode)); +#endif + + if (ofile == nullptr) { + LOG(("unable to create new file: " LOG_S ", err: %d", mFileRelPath.get(), + errno)); + return WRITE_ERROR_OPEN_PATCH_FILE; + } + +#ifdef XP_WIN + if (!shouldTruncate) { + fseek(ofile, 0, SEEK_SET); + } +#endif + + rv = MBS_ApplyPatch(&header, mPatchStream, buf, ofile); + + // Go ahead and do a bit of cleanup now to minimize runtime overhead. + // Make sure mPatchStream gets unlocked on Windows; the system will do that, + // but not until some indeterminate future time, and we want determinism. +#ifdef XP_WIN + UnlockFile((HANDLE)_get_osfhandle(fileno(mPatchStream)), 0, 0, -1, -1); +#endif + // Set mPatchStream to nullptr to make AutoFile close the file, + // so it can be deleted on Windows. + mPatchStream = nullptr; + // Patch files are written to the <working_dir>/updating directory which is + // removed after the update has finished so don't delete patch files here. + spath[0] = NS_T('\0'); + free(buf); + buf = nullptr; + + return rv; +} + +void PatchFile::Finish(int status) { + LOG(("FINISH PATCH " LOG_S, mFileRelPath.get())); + + // Staged updates don't create backup files. + if (!sStagedUpdate) { + backup_finish(mFile.get(), mFileRelPath.get(), status); + } +} + +class AddIfFile : public AddFile { + public: + int Parse(NS_tchar* line) override; + int Prepare() override; + int Execute() override; + void Finish(int status) override; + + protected: + mozilla::UniquePtr<NS_tchar[]> mTestFile; +}; + +int AddIfFile::Parse(NS_tchar* line) { + // format "<testfile>" "<newfile>" + + mTestFile.reset(get_full_path(get_valid_path(&line))); + if (!mTestFile) { + return PARSE_ERROR; + } + + // consume whitespace between args + NS_tchar* q = mstrtok(kQuote, &line); + if (!q) { + return PARSE_ERROR; + } + + return AddFile::Parse(line); +} + +int AddIfFile::Prepare() { + // If the test file does not exist, then skip this action. + if (NS_taccess(mTestFile.get(), F_OK)) { + mTestFile = nullptr; + return OK; + } + + return AddFile::Prepare(); +} + +int AddIfFile::Execute() { + if (!mTestFile) { + return OK; + } + + return AddFile::Execute(); +} + +void AddIfFile::Finish(int status) { + if (!mTestFile) { + return; + } + + AddFile::Finish(status); +} + +class AddIfNotFile : public AddFile { + public: + int Parse(NS_tchar* line) override; + int Prepare() override; + int Execute() override; + void Finish(int status) override; + + protected: + mozilla::UniquePtr<NS_tchar[]> mTestFile; +}; + +int AddIfNotFile::Parse(NS_tchar* line) { + // format "<testfile>" "<newfile>" + + mTestFile.reset(get_full_path(get_valid_path(&line))); + if (!mTestFile) { + return PARSE_ERROR; + } + + // consume whitespace between args + NS_tchar* q = mstrtok(kQuote, &line); + if (!q) { + return PARSE_ERROR; + } + + return AddFile::Parse(line); +} + +int AddIfNotFile::Prepare() { + // If the test file exists, then skip this action. + if (!NS_taccess(mTestFile.get(), F_OK)) { + mTestFile = NULL; + return OK; + } + + return AddFile::Prepare(); +} + +int AddIfNotFile::Execute() { + if (!mTestFile) { + return OK; + } + + return AddFile::Execute(); +} + +void AddIfNotFile::Finish(int status) { + if (!mTestFile) { + return; + } + + AddFile::Finish(status); +} + +class PatchIfFile : public PatchFile { + public: + int Parse(NS_tchar* line) override; + int Prepare() override; // should check for patch file and for checksum here + int Execute() override; + void Finish(int status) override; + + private: + mozilla::UniquePtr<NS_tchar[]> mTestFile; +}; + +int PatchIfFile::Parse(NS_tchar* line) { + // format "<testfile>" "<patchfile>" "<filetopatch>" + + mTestFile.reset(get_full_path(get_valid_path(&line))); + if (!mTestFile) { + return PARSE_ERROR; + } + + // consume whitespace between args + NS_tchar* q = mstrtok(kQuote, &line); + if (!q) { + return PARSE_ERROR; + } + + return PatchFile::Parse(line); +} + +int PatchIfFile::Prepare() { + // If the test file does not exist, then skip this action. + if (NS_taccess(mTestFile.get(), F_OK)) { + mTestFile = nullptr; + return OK; + } + + return PatchFile::Prepare(); +} + +int PatchIfFile::Execute() { + if (!mTestFile) { + return OK; + } + + return PatchFile::Execute(); +} + +void PatchIfFile::Finish(int status) { + if (!mTestFile) { + return; + } + + PatchFile::Finish(status); +} + +//----------------------------------------------------------------------------- + +#ifdef XP_WIN +# include "nsWindowsRestart.cpp" +# include "nsWindowsHelpers.h" +# include "uachelper.h" +# ifdef MOZ_MAINTENANCE_SERVICE +# include "pathhash.h" +# endif + +/** + * Launch the post update application (helper.exe). It takes in the path of the + * callback application to calculate the path of helper.exe. For service updates + * this is called from both the system account and the current user account. + * + * @param installationDir The path to the callback application binary. + * @param updateInfoDir The directory where update info is stored. + * @return true if there was no error starting the process. + */ +bool LaunchWinPostProcess(const WCHAR* installationDir, + const WCHAR* updateInfoDir) { + WCHAR workingDirectory[MAX_PATH + 1] = {L'\0'}; + wcsncpy(workingDirectory, installationDir, MAX_PATH); + + // Launch helper.exe to perform post processing (e.g. registry and log file + // modifications) for the update. + WCHAR inifile[MAX_PATH + 1] = {L'\0'}; + wcsncpy(inifile, installationDir, MAX_PATH); + if (!PathAppendSafe(inifile, L"updater.ini")) { + return false; + } + + WCHAR exefile[MAX_PATH + 1]; + WCHAR exearg[MAX_PATH + 1]; + if (!GetPrivateProfileStringW(L"PostUpdateWin", L"ExeRelPath", nullptr, + exefile, MAX_PATH + 1, inifile)) { + return false; + } + + if (!GetPrivateProfileStringW(L"PostUpdateWin", L"ExeArg", nullptr, exearg, + MAX_PATH + 1, inifile)) { + return false; + } + + // The relative path must not contain directory traversals, current directory, + // or colons. + if (wcsstr(exefile, L"..") != nullptr || wcsstr(exefile, L"./") != nullptr || + wcsstr(exefile, L".\\") != nullptr || wcsstr(exefile, L":") != nullptr) { + return false; + } + + // The relative path must not start with a decimal point, backslash, or + // forward slash. + if (exefile[0] == L'.' || exefile[0] == L'\\' || exefile[0] == L'/') { + return false; + } + + WCHAR exefullpath[MAX_PATH + 1] = {L'\0'}; + wcsncpy(exefullpath, installationDir, MAX_PATH); + if (!PathAppendSafe(exefullpath, exefile)) { + return false; + } + + if (!IsValidFullPath(exefullpath)) { + return false; + } + +# if !defined(TEST_UPDATER) && defined(MOZ_MAINTENANCE_SERVICE) + if (sUsingService && + !DoesBinaryMatchAllowedCertificates(installationDir, exefullpath)) { + return false; + } +# endif + + WCHAR dlogFile[MAX_PATH + 1]; + if (!PathGetSiblingFilePath(dlogFile, exefullpath, L"uninstall.update")) { + return false; + } + + WCHAR slogFile[MAX_PATH + 1] = {L'\0'}; + if (gCopyOutputFiles) { + if (!GetSecureOutputFilePath(gPatchDirPath, L".log", slogFile)) { + return false; + } + } else { + wcsncpy(slogFile, updateInfoDir, MAX_PATH); + if (!PathAppendSafe(slogFile, L"update.log")) { + return false; + } + } + + WCHAR dummyArg[14] = {L'\0'}; + wcsncpy(dummyArg, L"argv0ignored ", + sizeof(dummyArg) / sizeof(dummyArg[0]) - 1); + + size_t len = wcslen(exearg) + wcslen(dummyArg); + WCHAR* cmdline = (WCHAR*)malloc((len + 1) * sizeof(WCHAR)); + if (!cmdline) { + return false; + } + + wcsncpy(cmdline, dummyArg, len); + wcscat(cmdline, exearg); + + // We want to launch the post update helper app to update the Windows + // registry even if there is a failure with removing the uninstall.update + // file or copying the update.log file. + CopyFileW(slogFile, dlogFile, false); + + STARTUPINFOW si = {sizeof(si), 0}; + si.lpDesktop = const_cast<LPWSTR>(L""); // -Wwritable-strings + PROCESS_INFORMATION pi = {0}; + + bool ok = CreateProcessW(exefullpath, cmdline, + nullptr, // no special security attributes + nullptr, // no special thread attributes + false, // don't inherit filehandles + 0, // No special process creation flags + nullptr, // inherit my environment + workingDirectory, &si, &pi); + free(cmdline); + if (ok) { + WaitForSingleObject(pi.hProcess, INFINITE); + CloseHandle(pi.hProcess); + CloseHandle(pi.hThread); + } + return ok; +} + +#endif + +static void LaunchCallbackApp(const NS_tchar* workingDir, int argc, + NS_tchar** argv, bool usingService) { + putenv(const_cast<char*>("MOZ_LAUNCHED_CHILD=1")); + + // Run from the specified working directory (see bug 312360). + if (NS_tchdir(workingDir) != 0) { + LOG(("Warning: chdir failed")); + } + +#if defined(USE_EXECV) + execv(argv[0], argv); +#elif defined(XP_MACOSX) + LaunchChild(argc, (const char**)argv); +#elif defined(XP_WIN) + // Do not allow the callback to run when running an update through the + // service as session 0. The unelevated updater.exe will do the launching. + if (!usingService) { + HANDLE hProcess; + if (WinLaunchChild(argv[0], argc, argv, nullptr, &hProcess)) { + // Keep the current process around until the callback process has created + // its message queue, to avoid the launched process's windows being forced + // into the background. + mozilla::WaitForInputIdle(hProcess); + CloseHandle(hProcess); + } + } +#else +# warning "Need implementaton of LaunchCallbackApp" +#endif +} + +static bool WriteToFile(const NS_tchar* aFilename, const char* aStatus) { + NS_tchar statusFilePath[MAXPATHLEN + 1] = {NS_T('\0')}; +#if defined(XP_WIN) + if (gUseSecureOutputPath) { + if (!GetSecureOutputFilePath(gPatchDirPath, L".status", statusFilePath)) { + return false; + } + } else { + NS_tsnprintf(statusFilePath, + sizeof(statusFilePath) / sizeof(statusFilePath[0]), + NS_T("%s\\%s"), gPatchDirPath, aFilename); + } +#else + NS_tsnprintf(statusFilePath, + sizeof(statusFilePath) / sizeof(statusFilePath[0]), + NS_T("%s/%s"), gPatchDirPath, aFilename); + // Make sure that the directory for the update status file exists + if (ensure_parent_dir(statusFilePath)) { + return false; + } +#endif + + AutoFile statusFile(NS_tfopen(statusFilePath, NS_T("wb+"))); + if (statusFile == nullptr) { + return false; + } + + if (fwrite(aStatus, strlen(aStatus), 1, statusFile) != 1) { + return false; + } + +#if defined(XP_WIN) + if (gUseSecureOutputPath) { + // This is done after the update status file has been written so if the + // write to the update status file fails an existing update status file + // won't be used. + if (!WriteSecureIDFile(gPatchDirPath)) { + return false; + } + } +#endif + + return true; +} + +/** + * Writes a string to the update.status file. + * + * NOTE: All calls to WriteStatusFile MUST happen before calling output_finish + * because the output_finish function copies the update status file for + * the elevated updater and writing the status file after calling + * output_finish will overwrite it. + * + * @param aStatus + * The string to write to the update.status file. + * @return true on success. + */ +static bool WriteStatusFile(const char* aStatus) { + return WriteToFile(NS_T("update.status"), aStatus); +} + +/** + * Writes a string to the update.status file based on the status param. + * + * NOTE: All calls to WriteStatusFile MUST happen before calling output_finish + * because the output_finish function copies the update status file for + * the elevated updater and writing the status file after calling + * output_finish will overwrite it. + * + * @param status + * A status code used to determine what string to write to the + * update.status file (see code). + */ +static void WriteStatusFile(int status) { + const char* text; + + char buf[32]; + if (status == OK) { + if (sStagedUpdate) { + text = "applied\n"; + } else { + text = "succeeded\n"; + } + } else { + snprintf(buf, sizeof(buf) / sizeof(buf[0]), "failed: %d\n", status); + text = buf; + } + + WriteStatusFile(text); +} + +#if defined(XP_WIN) +/* + * Parses the passed contents of an update status file and checks if the + * contained status matches the expected status. + * + * @param statusString The status file contents. + * @param expectedStatus The status to compare the update status file's + * contents against. + * @param errorCode Optional out parameter. If a pointer is passed and the + * update status file contains an error code, the code + * will be returned via the out parameter. If a pointer is + * passed and the update status file does not contain an error + * code, or any error code after the status could not be + * parsed, mozilla::Nothing will be returned via this + * parameter. + * @return true if the status is set to the value indicated by expectedStatus. + */ +static bool UpdateStatusIs(const char* statusString, const char* expectedStatus, + mozilla::Maybe<int>* errorCode = nullptr) { + if (errorCode) { + *errorCode = mozilla::Nothing(); + } + + // Parse the update status file. Expected format is: + // Update status string + // Optionally followed by: + // Colon character (':') + // Space character (' ') + // Integer error code + // Newline character + const char* statusEnd = strchr(statusString, ':'); + if (statusEnd == nullptr) { + statusEnd = strchr(statusString, '\n'); + } + if (statusEnd == nullptr) { + statusEnd = strchr(statusString, '\0'); + } + size_t statusLen = statusEnd - statusString; + size_t expectedStatusLen = strlen(expectedStatus); + + bool statusMatch = + statusLen == expectedStatusLen && + strncmp(statusString, expectedStatus, expectedStatusLen) == 0; + + // We only need to continue parsing if (a) there is a place to store the error + // code if we parse it, and (b) there is a status code to parse. If the status + // string didn't end with a ':', there won't be an error code after it. + if (!errorCode || *statusEnd != ':') { + return statusMatch; + } + + const char* errorCodeStart = statusEnd + 1; + char* errorCodeEnd = nullptr; + // strtol skips an arbitrary number of leading whitespace characters. This + // technically allows us to successfully consume slightly misformatted status + // files, since the expected format is for there to be a single space only. + long longErrorCode = strtol(errorCodeStart, &errorCodeEnd, 10); + if (errorCodeEnd != errorCodeStart && longErrorCode < INT_MAX && + longErrorCode > INT_MIN) { + // We don't allow equality with INT_MAX/INT_MIN for two reasons. It could + // be that, on this platform, INT_MAX/INT_MIN equal LONG_MAX/LONG_MIN, which + // is what strtol gives us if the parsed value was out of bounds. And those + // values are already way, way outside the set of valid update error codes + // anyways. + errorCode->emplace(static_cast<int>(longErrorCode)); + } + return statusMatch; +} + +/* + * Reads the secure update status file and sets statusMatch to true if the + * status matches the expected status that was passed. + * + * @param expectedStatus The status to compare the update status file's + * contents against. + * @param statusMatch Out parameter for specifying if the status is set to + * the value indicated by expectedStatus + * @param errorCode Optional out parameter. If a pointer is passed and the + * update status file contains an error code, the code + * will be returned via the out parameter. If a pointer is + * passed and the update status file does not contain an error + * code, or any error code after the status could not be + * parsed, mozilla::Nothing will be returned via this + * parameter. + * @return true if the information was retrieved successfully. + */ +static bool CompareSecureUpdateStatus( + const char* expectedStatus, bool& statusMatch, + mozilla::Maybe<int>* errorCode = nullptr) { + NS_tchar statusFilePath[MAX_PATH + 1] = {L'\0'}; + if (!GetSecureOutputFilePath(gPatchDirPath, L".status", statusFilePath)) { + return false; + } + + AutoFile file(NS_tfopen(statusFilePath, NS_T("rb"))); + if (file == nullptr) { + return false; + } + + const size_t bufferLength = 32; + char buf[bufferLength] = {0}; + size_t charsRead = fread(buf, sizeof(buf[0]), bufferLength - 1, file); + if (ferror(file)) { + return false; + } + buf[charsRead] = '\0'; + + statusMatch = UpdateStatusIs(buf, expectedStatus, errorCode); + return true; +} + +/* + * Reads the secure update status file and sets isSucceeded to true if the + * status is set to succeeded. + * + * @param isSucceeded Out parameter for specifying if the status + * is set to succeeded or not. + * @return true if the information was retrieved successfully. + */ +static bool IsSecureUpdateStatusSucceeded(bool& isSucceeded) { + return CompareSecureUpdateStatus("succeeded", isSucceeded); +} +#endif + +#ifdef MOZ_MAINTENANCE_SERVICE +/* + * Read the update.status file and sets isPendingService to true if + * the status is set to pending-service. + * + * @param isPendingService Out parameter for specifying if the status + * is set to pending-service or not. + * @return true if the information was retrieved and it is pending + * or pending-service. + */ +static bool IsUpdateStatusPendingService() { + NS_tchar filename[MAXPATHLEN]; + NS_tsnprintf(filename, sizeof(filename) / sizeof(filename[0]), + NS_T("%s/update.status"), gPatchDirPath); + + AutoFile file(NS_tfopen(filename, NS_T("rb"))); + if (file == nullptr) { + return false; + } + + const size_t bufferLength = 32; + char buf[bufferLength] = {0}; + size_t charsRead = fread(buf, sizeof(buf[0]), bufferLength - 1, file); + if (ferror(file)) { + return false; + } + buf[charsRead] = '\0'; + + return UpdateStatusIs(buf, "pending-service") || + UpdateStatusIs(buf, "applied-service"); +} + +/* + * Reads the secure update status file and sets isFailed to true if the + * status is set to failed. + * + * @param isFailed Out parameter for specifying if the status + * is set to failed or not. + * @param errorCode Optional out parameter. If a pointer is passed and the + * update status file contains an error code, the code + * will be returned via the out parameter. If a pointer is + * passed and the update status file does not contain an error + * code, or any error code after the status could not be + * parsed, mozilla::Nothing will be returned via this + * parameter. + * @return true if the information was retrieved successfully. + */ +static bool IsSecureUpdateStatusFailed( + bool& isFailed, mozilla::Maybe<int>* errorCode = nullptr) { + return CompareSecureUpdateStatus("failed", isFailed, errorCode); +} + +/** + * This function determines whether the error represented by the passed error + * code could potentially be recovered from or bypassed by updating without + * using the Maintenance Service (i.e. by showing a UAC prompt). + * We don't really want to show a UAC prompt, but it's preferable over the + * manual update doorhanger + * + * @param errorCode An integer error code from the update.status file. Should + * be one of the codes enumerated in updatererrors.h. + * @returns true if the code represents a Maintenance Service specific error. + * Otherwise, false. + */ +static bool IsServiceSpecificErrorCode(int errorCode) { + return ((errorCode >= 24 && errorCode <= 33) || + (errorCode >= 49 && errorCode <= 58)); +} +#endif + +/* + * Copy the entire contents of the application installation directory to the + * destination directory for the update process. + * + * @return 0 if successful, an error code otherwise. + */ +static int CopyInstallDirToDestDir() { + // These files should not be copied over to the updated app +#ifdef XP_WIN +# define SKIPLIST_COUNT 3 +#elif XP_MACOSX +# define SKIPLIST_COUNT 0 +#else +# define SKIPLIST_COUNT 2 +#endif + copy_recursive_skiplist<SKIPLIST_COUNT> skiplist; +#ifndef XP_MACOSX + skiplist.append(0, gInstallDirPath, NS_T("updated")); + skiplist.append(1, gInstallDirPath, NS_T("updates/0")); +# ifdef XP_WIN + skiplist.append(2, gInstallDirPath, NS_T("updated.update_in_progress.lock")); +# endif +#endif + + return ensure_copy_recursive(gInstallDirPath, gWorkingDirPath, skiplist); +} + +/* + * Replace the application installation directory with the destination + * directory in order to finish a staged update task + * + * @return 0 if successful, an error code otherwise. + */ +static int ProcessReplaceRequest() { + // The replacement algorithm is like this: + // 1. Move destDir to tmpDir. In case of failure, abort. + // 2. Move newDir to destDir. In case of failure, revert step 1 and abort. + // 3. Delete tmpDir (or defer it to the next reboot). + +#ifdef XP_MACOSX + NS_tchar destDir[MAXPATHLEN]; + NS_tsnprintf(destDir, sizeof(destDir) / sizeof(destDir[0]), + NS_T("%s/Contents"), gInstallDirPath); +#elif XP_WIN + // Windows preserves the case of the file/directory names. We use the + // GetLongPathName API in order to get the correct case for the directory + // name, so that if the user has used a different case when launching the + // application, the installation directory's name does not change. + NS_tchar destDir[MAXPATHLEN]; + if (!GetLongPathNameW(gInstallDirPath, destDir, + sizeof(destDir) / sizeof(destDir[0]))) { + return NO_INSTALLDIR_ERROR; + } +#else + NS_tchar* destDir = gInstallDirPath; +#endif + + NS_tchar tmpDir[MAXPATHLEN]; + NS_tsnprintf(tmpDir, sizeof(tmpDir) / sizeof(tmpDir[0]), NS_T("%s.bak"), + destDir); + + NS_tchar newDir[MAXPATHLEN]; + NS_tsnprintf(newDir, sizeof(newDir) / sizeof(newDir[0]), +#ifdef XP_MACOSX + NS_T("%s/Contents"), gWorkingDirPath); +#else + NS_T("%s.bak/updated"), gInstallDirPath); +#endif + + // First try to remove the possibly existing temp directory, because if this + // directory exists, we will fail to rename destDir. + // No need to error check here because if this fails, we will fail in the + // next step anyways. + ensure_remove_recursive(tmpDir); + + LOG(("Begin moving destDir (" LOG_S ") to tmpDir (" LOG_S ")", destDir, + tmpDir)); + int rv = rename_file(destDir, tmpDir, true); +#ifdef XP_WIN + // On Windows, if Firefox is launched using the shortcut, it will hold a + // handle to its installation directory open, which might not get released in + // time. Therefore we wait a little bit here to see if the handle is released. + // If it's not released, we just fail to perform the replace request. + const int max_retries = 10; + int retries = 0; + while (rv == WRITE_ERROR && (retries++ < max_retries)) { + LOG( + ("PerformReplaceRequest: destDir rename attempt %d failed. " + "File: " LOG_S ". Last error: %lu, err: %d", + retries, destDir, GetLastError(), rv)); + + Sleep(100); + + rv = rename_file(destDir, tmpDir, true); + } +#endif + if (rv) { + // The status file will have 'pending' written to it so there is no value in + // returning an error specific for this failure. + LOG(("Moving destDir to tmpDir failed, err: %d", rv)); + return rv; + } + + LOG(("Begin moving newDir (" LOG_S ") to destDir (" LOG_S ")", newDir, + destDir)); + rv = rename_file(newDir, destDir, true); +#ifdef XP_MACOSX + if (rv) { + LOG(("Moving failed. Begin copying newDir (" LOG_S ") to destDir (" LOG_S + ")", + newDir, destDir)); + copy_recursive_skiplist<0> skiplist; + rv = ensure_copy_recursive(newDir, destDir, skiplist); + } +#endif + if (rv) { + LOG(("Moving newDir to destDir failed, err: %d", rv)); + LOG(("Now, try to move tmpDir back to destDir")); + ensure_remove_recursive(destDir); + int rv2 = rename_file(tmpDir, destDir, true); + if (rv2) { + LOG(("Moving tmpDir back to destDir failed, err: %d", rv2)); + } + // The status file will be have 'pending' written to it so there is no value + // in returning an error specific for this failure. + return rv; + } + +#if !defined(XP_WIN) && !defined(XP_MACOSX) + // Platforms that have their updates directory in the installation directory + // need to have the last-update.log and backup-update.log files moved from the + // old installation directory to the new installation directory. + NS_tchar tmpLog[MAXPATHLEN]; + NS_tsnprintf(tmpLog, sizeof(tmpLog) / sizeof(tmpLog[0]), + NS_T("%s/updates/last-update.log"), tmpDir); + if (!NS_taccess(tmpLog, F_OK)) { + NS_tchar destLog[MAXPATHLEN]; + NS_tsnprintf(destLog, sizeof(destLog) / sizeof(destLog[0]), + NS_T("%s/updates/last-update.log"), destDir); + if (NS_tremove(destLog) && errno != ENOENT) { + LOG(("non-fatal error removing log file: " LOG_S ", err: %d", destLog, + errno)); + } + NS_trename(tmpLog, destLog); + } +#endif + + LOG(("Now, remove the tmpDir")); + rv = ensure_remove_recursive(tmpDir, true); + if (rv) { + LOG(("Removing tmpDir failed, err: %d", rv)); +#ifdef XP_WIN + NS_tchar deleteDir[MAXPATHLEN]; + NS_tsnprintf(deleteDir, sizeof(deleteDir) / sizeof(deleteDir[0]), + NS_T("%s\\%s"), destDir, DELETE_DIR); + // Attempt to remove the tobedeleted directory and then recreate it if it + // was successfully removed. + _wrmdir(deleteDir); + if (NS_taccess(deleteDir, F_OK)) { + NS_tmkdir(deleteDir, 0755); + } + remove_recursive_on_reboot(tmpDir, deleteDir); +#endif + } + +#ifdef XP_MACOSX + // On OS X, we we need to remove the staging directory after its Contents + // directory has been moved. + NS_tchar updatedAppDir[MAXPATHLEN]; + NS_tsnprintf(updatedAppDir, sizeof(updatedAppDir) / sizeof(updatedAppDir[0]), + NS_T("%s/Updated.app"), gPatchDirPath); + ensure_remove_recursive(updatedAppDir); +#endif + + gSucceeded = true; + + return 0; +} + +#if defined(XP_WIN) && defined(MOZ_MAINTENANCE_SERVICE) +static void WaitForServiceFinishThread(void* param) { + // We wait at most 10 minutes, we already waited 5 seconds previously + // before deciding to show this UI. + WaitForServiceStop(SVC_NAME, 595); + QuitProgressUI(); +} +#endif + +#ifdef MOZ_VERIFY_MAR_SIGNATURE +/** + * This function reads in the ACCEPTED_MAR_CHANNEL_IDS from update-settings.ini + * + * @param path The path to the ini file that is to be read + * @param results A pointer to the location to store the read strings + * @return OK on success + */ +static int ReadMARChannelIDs(const NS_tchar* path, + MARChannelStringTable* results) { + const unsigned int kNumStrings = 1; + const char* kUpdaterKeys = "ACCEPTED_MAR_CHANNEL_IDS\0"; + int result = ReadStrings(path, kUpdaterKeys, kNumStrings, + &results->MARChannelID, "Settings"); + + return result; +} +#endif + +static int GetUpdateFileName(NS_tchar* fileName, int maxChars) { + NS_tsnprintf(fileName, maxChars, NS_T("%s/update.mar"), gPatchDirPath); + return OK; +} + +static void UpdateThreadFunc(void* param) { + // open ZIP archive and process... + int rv; + if (sReplaceRequest) { + rv = ProcessReplaceRequest(); + } else { + NS_tchar dataFile[MAXPATHLEN]; + rv = GetUpdateFileName(dataFile, sizeof(dataFile) / sizeof(dataFile[0])); + if (rv == OK) { + rv = gArchiveReader.Open(dataFile); + } + +#ifdef MOZ_VERIFY_MAR_SIGNATURE + if (rv == OK) { + rv = gArchiveReader.VerifySignature(); + } + + if (rv == OK) { + if (rv == OK) { + NS_tchar updateSettingsPath[MAXPATHLEN]; + NS_tsnprintf(updateSettingsPath, + sizeof(updateSettingsPath) / sizeof(updateSettingsPath[0]), +# ifdef XP_MACOSX + NS_T("%s/Contents/Resources/update-settings.ini"), +# else + NS_T("%s/update-settings.ini"), +# endif + gInstallDirPath); + MARChannelStringTable MARStrings; + if (ReadMARChannelIDs(updateSettingsPath, &MARStrings) != OK) { + rv = UPDATE_SETTINGS_FILE_CHANNEL; + } else { + rv = gArchiveReader.VerifyProductInformation( + MARStrings.MARChannelID.get(), MOZ_APP_VERSION); + } + } + } +#endif + + if (rv == OK && sStagedUpdate) { +#ifdef TEST_UPDATER + // The MOZ_TEST_SKIP_UPDATE_STAGE environment variable prevents copying + // the files in dist/bin in the test updater when staging an update since + // this can cause tests to timeout. + if (EnvHasValue("MOZ_TEST_SKIP_UPDATE_STAGE")) { + rv = OK; + } else if (EnvHasValue("MOZ_TEST_SLOW_SKIP_UPDATE_STAGE")) { + // The following is to simulate staging so the UI tests have time to + // show that the update is being staged. + NS_tchar continueFilePath[MAXPATHLEN] = {NS_T('\0')}; + NS_tsnprintf(continueFilePath, + sizeof(continueFilePath) / sizeof(continueFilePath[0]), + NS_T("%s/continueStaging"), gInstallDirPath); + // Use 300 retries for staging requests to lessen the likelihood of + // tests intermittently failing on verify tasks due to launching the + // updater. The total time to wait with the default interval of 100 ms + // is approximately 30 seconds. The total time for tests is longer to + // account for the extra time it takes for the updater to launch. + const int max_retries = 300; + int retries = 0; + while (retries++ < max_retries) { +# ifdef XP_WIN + Sleep(100); +# else + usleep(100000); +# endif + // Continue after the continue file exists and is removed. + if (!NS_tremove(continueFilePath)) { + break; + } + } + rv = OK; + } else { + rv = CopyInstallDirToDestDir(); + } +#else + rv = CopyInstallDirToDestDir(); +#endif + } + + if (rv == OK) { + rv = DoUpdate(); + gArchiveReader.Close(); + NS_tchar updatingDir[MAXPATHLEN]; + NS_tsnprintf(updatingDir, sizeof(updatingDir) / sizeof(updatingDir[0]), + NS_T("%s/updating"), gWorkingDirPath); + ensure_remove_recursive(updatingDir); + } + } + + if (rv && (sReplaceRequest || sStagedUpdate)) { + ensure_remove_recursive(gWorkingDirPath); + // When attempting to replace the application, we should fall back + // to non-staged updates in case of a failure. We do this by + // setting the status to pending, exiting the updater, and + // launching the callback application. The callback application's + // startup path will see the pending status, and will start the + // updater application again in order to apply the update without + // staging. + if (sReplaceRequest) { + WriteStatusFile(sUsingService ? "pending-service" : "pending"); + } else { + WriteStatusFile(rv); + } + LOG(("failed: %d", rv)); +#ifdef TEST_UPDATER + // Some tests need to use --test-process-updates again. + putenv(const_cast<char*>("MOZ_TEST_PROCESS_UPDATES=")); +#endif + } else { + if (rv) { + LOG(("failed: %d", rv)); + } else { +#ifdef XP_MACOSX + // If the update was successful we need to update the timestamp on the + // top-level Mac OS X bundle directory so that Mac OS X's Launch Services + // picks up any major changes when the bundle is updated. + if (!sStagedUpdate && utimes(gInstallDirPath, nullptr) != 0) { + LOG(("Couldn't set access/modification time on application bundle.")); + } +#endif + LOG(("succeeded")); + } + WriteStatusFile(rv); + } + + LOG(("calling QuitProgressUI")); + QuitProgressUI(); +} + +#ifdef XP_MACOSX +static void ServeElevatedUpdateThreadFunc(void* param) { + UpdateServerThreadArgs* threadArgs = (UpdateServerThreadArgs*)param; + gSucceeded = ServeElevatedUpdate(threadArgs->argc, threadArgs->argv); + if (!gSucceeded) { + WriteStatusFile(ELEVATION_CANCELED); + } + QuitProgressUI(); +} + +void freeArguments(int argc, char** argv) { + for (int i = 0; i < argc; i++) { + free(argv[i]); + } + free(argv); +} +#endif + +int LaunchCallbackAndPostProcessApps(int argc, NS_tchar** argv, + int callbackIndex +#ifdef XP_WIN + , + const WCHAR* elevatedLockFilePath, + HANDLE updateLockFileHandle +#elif XP_MACOSX + , + bool isElevated, + mozilla::UniquePtr<UmaskContext> + umaskContext +#endif +) { +#ifdef XP_MACOSX + umaskContext.reset(); +#endif + + if (argc > callbackIndex) { +#if defined(XP_WIN) + if (gSucceeded) { + if (!LaunchWinPostProcess(gInstallDirPath, gPatchDirPath)) { + fprintf(stderr, "The post update process was not launched"); + } + +# ifdef MOZ_MAINTENANCE_SERVICE + // The service update will only be executed if it is already installed. + // For first time installs of the service, the install will happen from + // the PostUpdate process. We do the service update process here + // because it's possible we are updating with updater.exe without the + // service if the service failed to apply the update. We want to update + // the service to a newer version in that case. If we are not running + // through the service, then MOZ_USING_SERVICE will not exist. + if (!sUsingService) { + StartServiceUpdate(gInstallDirPath); + } +# endif + } + EXIT_WHEN_ELEVATED(elevatedLockFilePath, updateLockFileHandle, 0); +#elif XP_MACOSX + if (!isElevated) { + if (gSucceeded) { + LaunchMacPostProcess(gInstallDirPath); + } +#endif + + LaunchCallbackApp(argv[5], argc - callbackIndex, argv + callbackIndex, + sUsingService); +#ifdef XP_MACOSX + } // if (!isElevated) +#endif /* XP_MACOSX */ +} +return 0; +} + +bool ShouldRunSilently(int argc, NS_tchar** argv) { +#ifdef MOZ_BACKGROUNDTASKS + // If the callback has a --backgroundtask switch, consider it a background + // task. The CheckArg semantics aren't reproduced in full here, + // there's e.g. no check for a parameter and no case-insensitive comparison. + for (int i = 1; i < argc; ++i) { + if (const auto option = mozilla::internal::ReadAsOption(argv[i])) { + const NS_tchar* arg = option.value(); + if (NS_tstrcmp(arg, NS_T("backgroundtask")) == 0) { + return true; + } + } + } +#endif // MOZ_BACKGROUNDTASKS + +#if defined(XP_WIN) || defined(XP_MACOSX) + if (EnvHasValue("MOZ_APP_SILENT_START")) { + return true; + } +#endif + + return false; +} + +int NS_main(int argc, NS_tchar** argv) { +#ifdef MOZ_MAINTENANCE_SERVICE + sUsingService = EnvHasValue("MOZ_USING_SERVICE"); + putenv(const_cast<char*>("MOZ_USING_SERVICE=")); +#endif + + // The callback is the remaining arguments starting at callbackIndex. + // The argument specified by callbackIndex is the callback executable and the + // argument prior to callbackIndex is the working directory. + const int callbackIndex = 6; + + // `isDMGInstall` is only ever true for macOS, but we are declaring it here + // to avoid a ton of extra #ifdef's. + bool isDMGInstall = false; + +#ifdef XP_MACOSX + // We want to control file permissions explicitly, or else we could end up + // corrupting installs for other users on the system. Accordingly, set the + // umask to 0 for all file creations below and reset it on exit. See Bug + // 1337007 + mozilla::UniquePtr<UmaskContext> umaskContext(new UmaskContext(0)); + + bool isElevated = + strstr(argv[0], "/Library/PrivilegedHelperTools/org.mozilla.updater") != + 0; + if (isElevated) { + if (!ObtainUpdaterArguments(&argc, &argv)) { + // Won't actually get here because ObtainUpdaterArguments will terminate + // the current process on failure. + return 1; + } + } + + if (argc == 4 && (strstr(argv[1], "-dmgInstall") != 0)) { + isDMGInstall = true; + if (isElevated) { + PerformInstallationFromDMG(argc, argv); + freeArguments(argc, argv); + CleanupElevatedMacUpdate(true); + return 0; + } + } +#endif + + if (!isDMGInstall) { + // Skip update-related code path for DMG installs. + +#if defined(MOZ_VERIFY_MAR_SIGNATURE) && !defined(XP_WIN) && !defined(XP_MACOSX) + // On Windows and Mac we rely on native APIs to do verifications so we don't + // need to initialize NSS at all there. + // Otherwise, minimize the amount of NSS we depend on by avoiding all the + // NSS databases. + if (NSS_NoDB_Init(nullptr) != SECSuccess) { + PRErrorCode error = PR_GetError(); + fprintf(stderr, "Could not initialize NSS: %s (%d)", + PR_ErrorToName(error), (int)error); + _exit(1); + } +#endif + + // To process an update the updater command line must at a minimum have the + // directory path containing the updater.mar file to process as the first + // argument, the install directory as the second argument, and the directory + // to apply the update to as the third argument. When the updater is + // launched by another process the PID of the parent process should be + // provided in the optional fourth argument and the updater will wait on the + // parent process to exit if the value is non-zero and the process is + // present. This is necessary due to not being able to update files that are + // in use on Windows. The optional fifth argument is the callback's working + // directory and the optional sixth argument is the callback path. The + // callback is the application to launch after updating and it will be + // launched when these arguments are provided whether the update was + // successful or not. All remaining arguments are optional and are passed to + // the callback when it is launched. + if (argc < 4) { + fprintf(stderr, + "Usage: updater patch-dir install-dir apply-to-dir [wait-pid " + "[callback-working-dir callback-path args...]]\n"); +#ifdef XP_MACOSX + if (isElevated) { + freeArguments(argc, argv); + CleanupElevatedMacUpdate(true); + } +#endif + return 1; + } + +#if defined(TEST_UPDATER) && defined(XP_WIN) + // The tests use nsIProcess to launch the updater and it is simpler for the + // tests to just set an environment variable and have the test updater set + // the current working directory than it is to set the current working + // directory in the test itself. + if (EnvHasValue("CURWORKDIRPATH")) { + const WCHAR* val = _wgetenv(L"CURWORKDIRPATH"); + NS_tchdir(val); + } +#endif + + } // if (!isDMGInstall) + + // The directory containing the update information. + NS_tstrncpy(gPatchDirPath, argv[1], MAXPATHLEN); + gPatchDirPath[MAXPATHLEN - 1] = NS_T('\0'); + +#ifdef XP_WIN + NS_tchar elevatedLockFilePath[MAXPATHLEN] = {NS_T('\0')}; + NS_tsnprintf(elevatedLockFilePath, + sizeof(elevatedLockFilePath) / sizeof(elevatedLockFilePath[0]), + NS_T("%s\\update_elevated.lock"), gPatchDirPath); + gUseSecureOutputPath = + sUsingService || (NS_tremove(elevatedLockFilePath) && errno != ENOENT); +#endif + + if (!isDMGInstall) { + // This check is also performed in workmonitor.cpp since the maintenance + // service can be called directly. + if (!IsValidFullPath(argv[1])) { + // Since the status file is written to the patch directory and the patch + // directory is invalid don't write the status file. + fprintf(stderr, + "The patch directory path is not valid for this " + "application (" LOG_S ")\n", + argv[1]); +#ifdef XP_MACOSX + if (isElevated) { + freeArguments(argc, argv); + CleanupElevatedMacUpdate(true); + } +#endif + return 1; + } + + // This check is also performed in workmonitor.cpp since the maintenance + // service can be called directly. + if (!IsValidFullPath(argv[2])) { + WriteStatusFile(INVALID_INSTALL_DIR_PATH_ERROR); + fprintf(stderr, + "The install directory path is not valid for this " + "application (" LOG_S ")\n", + argv[2]); +#ifdef XP_MACOSX + if (isElevated) { + freeArguments(argc, argv); + CleanupElevatedMacUpdate(true); + } +#endif + return 1; + } + + } // if (!isDMGInstall) + + // The directory we're going to update to. + // We copy this string because we need to remove trailing slashes. The C++ + // standard says that it's always safe to write to strings pointed to by argv + // elements, but I don't necessarily believe it. + NS_tstrncpy(gInstallDirPath, argv[2], MAXPATHLEN); + gInstallDirPath[MAXPATHLEN - 1] = NS_T('\0'); + NS_tchar* slash = NS_tstrrchr(gInstallDirPath, NS_SLASH); + if (slash && !slash[1]) { + *slash = NS_T('\0'); + } + +#ifdef XP_WIN + bool useService = false; + bool testOnlyFallbackKeyExists = false; + // Prevent the updater from falling back from updating with the Maintenance + // Service to updating without the Service. Used for Service tests. + // This is set below via the MOZ_NO_SERVICE_FALLBACK environment variable. + bool noServiceFallback = false; + // Force the updater to use the Maintenance Service incorrectly, causing it + // to fail. Used to test the mechanism that allows the updater to fall back + // from using the Maintenance Service to updating without it. + // This is set below via the MOZ_FORCE_SERVICE_FALLBACK environment variable. + bool forceServiceFallback = false; +#endif + + if (!isDMGInstall) { +#ifdef XP_WIN + // We never want the service to be used unless we build with + // the maintenance service. +# ifdef MOZ_MAINTENANCE_SERVICE + useService = IsUpdateStatusPendingService(); +# ifdef TEST_UPDATER + noServiceFallback = EnvHasValue("MOZ_NO_SERVICE_FALLBACK"); + putenv(const_cast<char*>("MOZ_NO_SERVICE_FALLBACK=")); + forceServiceFallback = EnvHasValue("MOZ_FORCE_SERVICE_FALLBACK"); + putenv(const_cast<char*>("MOZ_FORCE_SERVICE_FALLBACK=")); + // 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. + testOnlyFallbackKeyExists = DoesFallbackKeyExist(); +# endif +# endif + + // Remove everything except close window from the context menu + { + HKEY hkApp = nullptr; + RegCreateKeyExW(HKEY_CURRENT_USER, L"Software\\Classes\\Applications", 0, + nullptr, REG_OPTION_NON_VOLATILE, KEY_SET_VALUE, nullptr, + &hkApp, nullptr); + RegCloseKey(hkApp); + if (RegCreateKeyExW(HKEY_CURRENT_USER, + L"Software\\Classes\\Applications\\updater.exe", 0, + nullptr, REG_OPTION_VOLATILE, KEY_SET_VALUE, nullptr, + &hkApp, nullptr) == ERROR_SUCCESS) { + RegSetValueExW(hkApp, L"IsHostApp", 0, REG_NONE, 0, 0); + RegSetValueExW(hkApp, L"NoOpenWith", 0, REG_NONE, 0, 0); + RegSetValueExW(hkApp, L"NoStartPage", 0, REG_NONE, 0, 0); + RegCloseKey(hkApp); + } + } +#endif + + } // if (!isDMGInstall) + + // If there is a PID specified and it is not '0' then wait for the process to + // exit. + NS_tpid pid = 0; + if (argc > 4) { + pid = NS_tatoi(argv[4]); + if (pid == -1) { + // This is a signal from the parent process that the updater should stage + // the update. + sStagedUpdate = true; + } else if (NS_tstrstr(argv[4], NS_T("/replace"))) { + // We're processing a request to replace the application with a staged + // update. + sReplaceRequest = true; + } + } + + if (!isDMGInstall) { + // This check is also performed in workmonitor.cpp since the maintenance + // service can be called directly. + if (!IsValidFullPath(argv[3])) { + WriteStatusFile(INVALID_WORKING_DIR_PATH_ERROR); + fprintf(stderr, + "The working directory path is not valid for this " + "application (" LOG_S ")\n", + argv[3]); +#ifdef XP_MACOSX + if (isElevated) { + freeArguments(argc, argv); + CleanupElevatedMacUpdate(true); + } +#endif + return 1; + } + // The directory we're going to update to. + // We copy this string because we need to remove trailing slashes. The C++ + // standard says that it's always safe to write to strings pointed to by + // argv elements, but I don't necessarily believe it. + NS_tstrncpy(gWorkingDirPath, argv[3], MAXPATHLEN); + gWorkingDirPath[MAXPATHLEN - 1] = NS_T('\0'); + slash = NS_tstrrchr(gWorkingDirPath, NS_SLASH); + if (slash && !slash[1]) { + *slash = NS_T('\0'); + } + + if (argc > callbackIndex) { + if (!IsValidFullPath(argv[callbackIndex])) { + WriteStatusFile(INVALID_CALLBACK_PATH_ERROR); + fprintf(stderr, + "The callback file path is not valid for this " + "application (" LOG_S ")\n", + argv[callbackIndex]); +#ifdef XP_MACOSX + if (isElevated) { + freeArguments(argc, argv); + CleanupElevatedMacUpdate(true); + } +#endif + return 1; + } + + size_t len = NS_tstrlen(gInstallDirPath); + NS_tchar callbackInstallDir[MAXPATHLEN] = {NS_T('\0')}; + NS_tstrncpy(callbackInstallDir, argv[callbackIndex], len); + if (NS_tstrcmp(gInstallDirPath, callbackInstallDir) != 0) { + WriteStatusFile(INVALID_CALLBACK_DIR_ERROR); + fprintf(stderr, + "The callback file must be located in the " + "installation directory (" LOG_S ")\n", + argv[callbackIndex]); +#ifdef XP_MACOSX + if (isElevated) { + freeArguments(argc, argv); + CleanupElevatedMacUpdate(true); + } +#endif + return 1; + } + + sUpdateSilently = + ShouldRunSilently(argc - callbackIndex, argv + callbackIndex); + } + + } // if (!isDMGInstall) + + if (!sUpdateSilently && !isDMGInstall +#ifdef XP_MACOSX + && !isElevated +#endif + ) { + InitProgressUI(&argc, &argv); + } + +#ifdef XP_MACOSX + if (!isElevated && (!IsRecursivelyWritable(argv[2]) || isDMGInstall)) { + // If the app directory isn't recursively writeable or if this is a DMG + // install, an elevated helper process is required. + if (sUpdateSilently) { + // An elevated update always requires an elevation dialog, so if we are + // updating silently, don't do an elevated update. + // This means that we cannot successfully perform silent updates from + // non-admin accounts on a Mac. + // It also means that we cannot silently perform the first update by an + // admin who was not the installing user. Once the first update has been + // installed, the permissions of the installation directory should be + // changed such that we don't need to elevate in the future. + // Firefox shouldn't actually launch the updater at all in this case. This + // is defense in depth. + WriteStatusFile(SILENT_UPDATE_NEEDED_ELEVATION_ERROR); + fprintf(stderr, + "Skipping update to avoid elevation prompt from silent update."); + } else { + UpdateServerThreadArgs threadArgs; + threadArgs.argc = argc; + threadArgs.argv = const_cast<const NS_tchar**>(argv); + + Thread t1; + if (t1.Run(ServeElevatedUpdateThreadFunc, &threadArgs) == 0) { + // Show an indeterminate progress bar while an elevated update is in + // progress. + if (!isDMGInstall) { + ShowProgressUI(true); + } + } + t1.Join(); + } + + LaunchCallbackAndPostProcessApps(argc, argv, callbackIndex, false, + std::move(umaskContext)); + return gSucceeded ? 0 : 1; + } +#endif + +#ifdef XP_WIN + HANDLE updateLockFileHandle = INVALID_HANDLE_VALUE; +#endif + + if (!isDMGInstall) { + NS_tchar logFilePath[MAXPATHLEN + 1] = {L'\0'}; +#ifdef XP_WIN + if (gUseSecureOutputPath) { + // Remove the secure output files so it is easier to determine when new + // files are created in the unelevated updater. + RemoveSecureOutputFiles(gPatchDirPath); + + (void)GetSecureOutputFilePath(gPatchDirPath, L".log", logFilePath); + } else { + NS_tsnprintf(logFilePath, sizeof(logFilePath) / sizeof(logFilePath[0]), + NS_T("%s\\update.log"), gPatchDirPath); + } +#else + NS_tsnprintf(logFilePath, sizeof(logFilePath) / sizeof(logFilePath[0]), + NS_T("%s/update.log"), gPatchDirPath); +#endif + LogInit(logFilePath); + + if (!WriteStatusFile("applying")) { + LOG(("failed setting status to 'applying'")); +#ifdef XP_MACOSX + if (isElevated) { + freeArguments(argc, argv); + CleanupElevatedMacUpdate(true); + } +#endif + output_finish(); + return 1; + } + + if (sStagedUpdate) { + LOG(("Performing a staged update")); + } else if (sReplaceRequest) { + LOG(("Performing a replace request")); + } + + LOG(("PATCH DIRECTORY " LOG_S, gPatchDirPath)); + LOG(("INSTALLATION DIRECTORY " LOG_S, gInstallDirPath)); + LOG(("WORKING DIRECTORY " LOG_S, gWorkingDirPath)); + +#if defined(XP_WIN) + // These checks are also performed in workmonitor.cpp since the maintenance + // service can be called directly. + if (_wcsnicmp(gWorkingDirPath, gInstallDirPath, MAX_PATH) != 0) { + if (!sStagedUpdate && !sReplaceRequest) { + WriteStatusFile(INVALID_APPLYTO_DIR_ERROR); + LOG( + ("Installation directory and working directory must be the same " + "for non-staged updates. Exiting.")); + output_finish(); + return 1; + } + + NS_tchar workingDirParent[MAX_PATH]; + NS_tsnprintf(workingDirParent, + sizeof(workingDirParent) / sizeof(workingDirParent[0]), + NS_T("%s"), gWorkingDirPath); + if (!PathRemoveFileSpecW(workingDirParent)) { + WriteStatusFile(REMOVE_FILE_SPEC_ERROR); + LOG(("Error calling PathRemoveFileSpecW: %lu", GetLastError())); + output_finish(); + return 1; + } + + if (_wcsnicmp(workingDirParent, gInstallDirPath, MAX_PATH) != 0) { + WriteStatusFile(INVALID_APPLYTO_DIR_STAGED_ERROR); + LOG( + ("The apply-to directory must be the same as or " + "a child of the installation directory! Exiting.")); + output_finish(); + return 1; + } + } +#endif + +#ifdef XP_WIN + if (pid > 0) { + HANDLE parent = OpenProcess(SYNCHRONIZE, false, (DWORD)pid); + // May return nullptr if the parent process has already gone away. + // Otherwise, wait for the parent process to exit before starting the + // update. + if (parent) { + DWORD waitTime = PARENT_WAIT; +# ifdef TEST_UPDATER + if (EnvHasValue("MOZ_TEST_SHORTER_WAIT_PID")) { + // Use a shorter time to wait for the PID to exit for the test. + waitTime = 100; + } +# endif + DWORD result = WaitForSingleObject(parent, waitTime); + CloseHandle(parent); + if (result != WAIT_OBJECT_0) { + // Continue to update since the parent application sometimes doesn't + // exit (see bug 1375242) so any fixes to the parent application will + // be applied instead of leaving the client in a broken state. + LOG(("The parent process didn't exit! Continuing with update.")); + } + } + } +#endif + +#ifdef XP_WIN + if (sReplaceRequest || sStagedUpdate) { + // On Windows, when performing a stage or replace request the current + // working directory for the process must be changed so it isn't locked. + NS_tchar sysDir[MAX_PATH + 1] = {L'\0'}; + if (GetSystemDirectoryW(sysDir, MAX_PATH + 1)) { + NS_tchdir(sysDir); + } + } + + // lastFallbackError keeps track of the last error for the service not being + // used, in case of an error when fallback is not enabled we write the + // error to the update.status file. + // When fallback is disabled (MOZ_NO_SERVICE_FALLBACK does not exist) then + // we will instead fallback to not using the service and display a UAC + // prompt. + int lastFallbackError = FALLBACKKEY_UNKNOWN_ERROR; + + // Check whether a second instance of the updater should be launched by the + // maintenance service or with the 'runas' verb when write access is denied + // to the installation directory. + if (!sUsingService && + (argc > callbackIndex || sStagedUpdate || sReplaceRequest)) { + NS_tchar updateLockFilePath[MAXPATHLEN]; + if (sStagedUpdate) { + // When staging an update, the lock file is: + // <install_dir>\updated.update_in_progress.lock + NS_tsnprintf(updateLockFilePath, + sizeof(updateLockFilePath) / sizeof(updateLockFilePath[0]), + NS_T("%s/updated.update_in_progress.lock"), + gInstallDirPath); + } else if (sReplaceRequest) { + // When processing a replace request, the lock file is: + // <install_dir>\..\moz_update_in_progress.lock + NS_tchar installDir[MAXPATHLEN]; + NS_tstrcpy(installDir, gInstallDirPath); + NS_tchar* slash = (NS_tchar*)NS_tstrrchr(installDir, NS_SLASH); + *slash = NS_T('\0'); + NS_tsnprintf(updateLockFilePath, + sizeof(updateLockFilePath) / sizeof(updateLockFilePath[0]), + NS_T("%s\\moz_update_in_progress.lock"), installDir); + } else { + // In the non-staging update case, the lock file is: + // <install_dir>\<app_name>.exe.update_in_progress.lock + NS_tsnprintf(updateLockFilePath, + sizeof(updateLockFilePath) / sizeof(updateLockFilePath[0]), + NS_T("%s.update_in_progress.lock"), argv[callbackIndex]); + } + + // The update_in_progress.lock file should only exist during an update. In + // case it exists attempt to remove it and exit if that fails to prevent + // simultaneous updates occurring. + if (NS_tremove(updateLockFilePath) && errno != ENOENT) { + // Try to fall back to the old way of doing updates if a staged + // update fails. + if (sReplaceRequest) { + // Note that this could fail, but if it does, there isn't too much we + // can do in order to recover anyways. + WriteStatusFile("pending"); + } else if (sStagedUpdate) { + WriteStatusFile(DELETE_ERROR_STAGING_LOCK_FILE); + } + LOG(("Update already in progress! Exiting")); + output_finish(); + return 1; + } + + updateLockFileHandle = + CreateFileW(updateLockFilePath, GENERIC_READ | GENERIC_WRITE, 0, + nullptr, OPEN_ALWAYS, FILE_FLAG_DELETE_ON_CLOSE, nullptr); + + // Even if a file has no sharing access, you can still get its attributes + bool startedFromUnelevatedUpdater = + GetFileAttributesW(elevatedLockFilePath) != INVALID_FILE_ATTRIBUTES; + + // If we're running from the service, then we were started with the same + // token as the service so the permissions are already dropped. If we're + // running from an elevated updater that was started from an unelevated + // updater, then we drop the permissions here. We do not drop the + // permissions on the originally called updater because we use its token + // to start the callback application. + if (startedFromUnelevatedUpdater) { + // Disable every privilege we don't need. Processes started using + // CreateProcess will use the same token as this process. + UACHelper::DisablePrivileges(nullptr); + } + + if (updateLockFileHandle == INVALID_HANDLE_VALUE || + (useService && testOnlyFallbackKeyExists && + (noServiceFallback || forceServiceFallback))) { + HANDLE elevatedFileHandle; + if (NS_tremove(elevatedLockFilePath) && errno != ENOENT) { + LOG(("Unable to create elevated lock file! Exiting")); + output_finish(); + return 1; + } + + elevatedFileHandle = CreateFileW( + elevatedLockFilePath, GENERIC_READ | GENERIC_WRITE, 0, nullptr, + OPEN_ALWAYS, FILE_FLAG_DELETE_ON_CLOSE, nullptr); + if (elevatedFileHandle == INVALID_HANDLE_VALUE) { + LOG(("Unable to create elevated lock file! Exiting")); + output_finish(); + return 1; + } + + auto cmdLine = mozilla::MakeCommandLine(argc - 1, argv + 1); + if (!cmdLine) { + CloseHandle(elevatedFileHandle); + output_finish(); + return 1; + } + +# ifdef MOZ_MAINTENANCE_SERVICE +// Only invoke the service for installations in Program Files. +// This check is duplicated in workmonitor.cpp because the service can +// be invoked directly without going through the updater. +# ifndef TEST_UPDATER + if (useService) { + useService = IsProgramFilesPath(gInstallDirPath); + } +# endif + + // Make sure the path to the updater to use for the update is on local. + // We do this check to make sure that file locking is available for + // race condition security checks. + if (useService) { + BOOL isLocal = FALSE; + useService = IsLocalFile(argv[0], isLocal) && isLocal; + } + + // If we have unprompted elevation we should NOT use the service + // for the update. Service updates happen with the SYSTEM account + // which has more privs than we need to update with. + // Windows 8 provides a user interface so users can configure this + // behavior and it can be configured in the registry in all Windows + // versions that support UAC. + if (useService) { + BOOL unpromptedElevation; + if (IsUnpromptedElevation(unpromptedElevation)) { + useService = !unpromptedElevation; + } + } + + // Make sure the service registry entries for the instsallation path + // are available. If not don't use the service. + if (useService) { + WCHAR maintenanceServiceKey[MAX_PATH + 1]; + if (CalculateRegistryPathFromFilePath(gInstallDirPath, + maintenanceServiceKey)) { + HKEY baseKey = nullptr; + if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, maintenanceServiceKey, 0, + KEY_READ | KEY_WOW64_64KEY, + &baseKey) == ERROR_SUCCESS) { + RegCloseKey(baseKey); + } else { +# ifdef TEST_UPDATER + useService = testOnlyFallbackKeyExists; +# endif + if (!useService) { + lastFallbackError = FALLBACKKEY_NOKEY_ERROR; + } + } + } else { + useService = false; + lastFallbackError = FALLBACKKEY_REGPATH_ERROR; + } + } + + // Originally we used to write "pending" to update.status before + // launching the service command. This is no longer needed now + // since the service command is launched from updater.exe. If anything + // fails in between, we can fall back to using the normal update process + // on our own. + + // If we still want to use the service try to launch the service + // comamnd for the update. + if (useService) { + // Get the secure ID before trying to update so it is possible to + // determine if the updater or the maintenance service has created a + // new one. + char uuidStringBefore[UUID_LEN] = {'\0'}; + bool checkID = GetSecureID(uuidStringBefore); + // Write a catchall service failure status in case it fails without + // changing the status. + WriteStatusFile(SERVICE_UPDATE_STATUS_UNCHANGED); + + int serviceArgc = argc; + if (forceServiceFallback && serviceArgc > 2) { + // To force the service to fail, we can just pass it too few + // arguments. However, we don't want to pass it no arguments, + // because then it won't have enough information to write out the + // update status file telling us that it failed. + serviceArgc = 2; + } + + // If the update couldn't be started, then set useService to false so + // we do the update the old way. + DWORD ret = + LaunchServiceSoftwareUpdateCommand(serviceArgc, (LPCWSTR*)argv); + useService = (ret == ERROR_SUCCESS); + // If the command was launched then wait for the service to be done. + if (useService) { + bool showProgressUI = false; + // Never show the progress UI when staging updates or in a + // background task. + if (!sStagedUpdate && !sUpdateSilently) { + // We need to call this separately instead of allowing + // ShowProgressUI to initialize the strings because the service + // will move the ini file out of the way when running updater. + showProgressUI = !InitProgressUIStrings(); + } + + // Wait for the service to stop for 5 seconds. If the service + // has still not stopped then show an indeterminate progress bar. + DWORD lastState = WaitForServiceStop(SVC_NAME, 5); + if (lastState != SERVICE_STOPPED) { + Thread t1; + if (t1.Run(WaitForServiceFinishThread, nullptr) == 0 && + showProgressUI) { + ShowProgressUI(true, false); + } + t1.Join(); + } + + lastState = WaitForServiceStop(SVC_NAME, 1); + if (lastState != SERVICE_STOPPED) { + // If the service doesn't stop after 10 minutes there is + // something seriously wrong. + lastFallbackError = FALLBACKKEY_SERVICE_NO_STOP_ERROR; + useService = false; + } else { + // Copy the secure output files if the secure ID has changed. + gCopyOutputFiles = true; + char uuidStringAfter[UUID_LEN] = {'\0'}; + if (checkID && GetSecureID(uuidStringAfter) && + strncmp(uuidStringBefore, uuidStringAfter, + sizeof(uuidStringBefore)) == 0) { + LOG( + ("The secure ID hasn't changed after launching the updater " + "using the service")); + gCopyOutputFiles = false; + } + if (gCopyOutputFiles && !sStagedUpdate && !noServiceFallback) { + // If the Maintenance Service fails for a Service-specific + // reason, we ought to fall back to attempting to update + // without the Service. + // However, we need the secure output files to be able to be + // check the error code, and we can't fall back when we are + // staging, because we will need to show a UAC. + bool updateFailed; + mozilla::Maybe<int> maybeErrorCode; + bool success = + IsSecureUpdateStatusFailed(updateFailed, &maybeErrorCode); + if (success && updateFailed && maybeErrorCode.isSome() && + IsServiceSpecificErrorCode(maybeErrorCode.value())) { + useService = false; + } + } + } + } else { + lastFallbackError = FALLBACKKEY_LAUNCH_ERROR; + } + } +# endif + + // If the service can't be used when staging an update, make sure that + // the UAC prompt is not shown! + if (!useService && sStagedUpdate) { + if (updateLockFileHandle != INVALID_HANDLE_VALUE) { + CloseHandle(updateLockFileHandle); + } + // Set an error so the failure is reported. This will be reset + // to pending so the update can be applied during the next startup, + // see bug 1552853. + WriteStatusFile(UNEXPECTED_STAGING_ERROR); + LOG( + ("Non-critical update staging error! Falling back to non-staged " + "updates and exiting")); + output_finish(); + // We don't have a callback when staging so we can just exit. + return 0; + } + + // If the service can't be used when in a background task, make sure + // that the UAC prompt is not shown! + if (!useService && sUpdateSilently) { + if (updateLockFileHandle != INVALID_HANDLE_VALUE) { + CloseHandle(updateLockFileHandle); + } + // Set an error so we don't get into an update loop when the callback + // runs. This will be reset to pending by handleUpdateFailure in + // UpdateService.jsm. + WriteStatusFile(SILENT_UPDATE_NEEDED_ELEVATION_ERROR); + LOG(("Skipping update to avoid UAC prompt from background task.")); + output_finish(); + + LaunchCallbackApp(argv[5], argc - callbackIndex, argv + callbackIndex, + sUsingService); + return 0; + } + + // If we didn't want to use the service at all, or if an update was + // already happening, or launching the service command failed, then + // launch the elevated updater.exe as we do without the service. + // We don't launch the elevated updater in the case that we did have + // write access all along because in that case the only reason we're + // using the service is because we are testing. + if (!useService && !noServiceFallback && + (updateLockFileHandle == INVALID_HANDLE_VALUE || + forceServiceFallback)) { + // Get the secure ID before trying to update so it is possible to + // determine if the updater has created a new one. + char uuidStringBefore[UUID_LEN] = {'\0'}; + bool checkID = GetSecureID(uuidStringBefore); + // Write a catchall failure status in case it fails without changing + // the status. + WriteStatusFile(UPDATE_STATUS_UNCHANGED); + + SHELLEXECUTEINFO sinfo; + memset(&sinfo, 0, sizeof(SHELLEXECUTEINFO)); + sinfo.cbSize = sizeof(SHELLEXECUTEINFO); + sinfo.fMask = SEE_MASK_FLAG_NO_UI | SEE_MASK_FLAG_DDEWAIT | + SEE_MASK_NOCLOSEPROCESS; + sinfo.hwnd = nullptr; + sinfo.lpFile = argv[0]; + sinfo.lpParameters = cmdLine.get(); + if (forceServiceFallback) { + // In testing, we don't actually want a UAC prompt. We should + // already have the permissions such that we shouldn't need it. + // And we don't have a good way of accepting the prompt in + // automation. + sinfo.lpVerb = L"open"; + // This handle is what lets the updater that we spawn below know + // that it's the elevated updater. We are going to close it so that + // it doesn't know that and will run un-elevated. Doing this make + // this makes for an imperfect test of the service fallback + // functionality because it changes how the (usually) elevated + // updater runs. One of the effects of this is that the secure + // output files will not be used. So that functionality won't really + // be covered by testing. But we can't really have the updater run + // elevated, because that would require a UAC, which we have no way + // to deal with in automation. + CloseHandle(elevatedFileHandle); + // We need to let go of the update lock to let the un-elevated + // updater we are about to spawn update. + if (updateLockFileHandle != INVALID_HANDLE_VALUE) { + CloseHandle(updateLockFileHandle); + } + } else { + sinfo.lpVerb = L"runas"; + } + sinfo.nShow = SW_SHOWNORMAL; + + bool result = ShellExecuteEx(&sinfo); + + if (result) { + WaitForSingleObject(sinfo.hProcess, INFINITE); + CloseHandle(sinfo.hProcess); + + // Copy the secure output files if the secure ID has changed. + gCopyOutputFiles = true; + char uuidStringAfter[UUID_LEN] = {'\0'}; + if (checkID && GetSecureID(uuidStringAfter) && + strncmp(uuidStringBefore, uuidStringAfter, + sizeof(uuidStringBefore)) == 0) { + LOG( + ("The secure ID hasn't changed after launching the updater " + "using runas")); + gCopyOutputFiles = false; + } + } else { + // Don't copy the secure output files if the elevation request was + // canceled since the status file written below is in the patch + // directory. At this point it should already be set to false and + // this is set here to make it clear that it should be false at this + // point and to prevent future changes from regressing this code. + gCopyOutputFiles = false; + WriteStatusFile(ELEVATION_CANCELED); + } + } + + // If we started the elevated updater, and it finished, check the secure + // update status file to make sure that it succeeded, and if it did we + // need to launch the PostUpdate process in the unelevated updater which + // is running in the current user's session. Note that we don't need to + // do this when staging an update since the PostUpdate step runs during + // the replace request. + if (!sStagedUpdate) { + bool updateStatusSucceeded = false; + if (IsSecureUpdateStatusSucceeded(updateStatusSucceeded) && + updateStatusSucceeded) { + if (!LaunchWinPostProcess(gInstallDirPath, gPatchDirPath)) { + fprintf(stderr, + "The post update process which runs as the user" + " for service update could not be launched."); + } + } + } + + CloseHandle(elevatedFileHandle); + + if (updateLockFileHandle != INVALID_HANDLE_VALUE) { + CloseHandle(updateLockFileHandle); + } + + if (!useService && noServiceFallback) { + // When the service command was not launched at all. + // We should only reach this code path because we had write access + // all along to the directory and a fallback key existed, and we + // have fallback disabled (MOZ_NO_SERVICE_FALLBACK env var exists). + // We only currently use this env var from XPCShell tests. + gCopyOutputFiles = false; + WriteStatusFile(lastFallbackError); + } + + // The logging output needs to be finished before launching the callback + // application so the update status file contains the value from the + // secure directory used by the maintenance service and the elevated + // updater. + output_finish(); + if (argc > callbackIndex) { + LaunchCallbackApp(argv[5], argc - callbackIndex, argv + callbackIndex, + sUsingService); + } + return 0; + + // This is the end of the code block for launching another instance of + // the updater using either the maintenance service or with the 'runas' + // verb when the updater doesn't have write access to the installation + // directory. + } + // This is the end of the code block when the updater was not launched by + // the service that checks whether the updater has write access to the + // installation directory. + } + // If we made it this far this is the updater instance that will perform the + // actual update and gCopyOutputFiles will be false (e.g. the default + // value). +#endif + + if (sStagedUpdate) { +#ifdef TEST_UPDATER + // This allows testing that the correct UI after an update staging failure + // that falls back to applying the update on startup. It is simulated due + // to the difficulty of creating the conditions for this type of staging + // failure. + if (EnvHasValue("MOZ_TEST_STAGING_ERROR")) { +# ifdef XP_WIN + if (updateLockFileHandle != INVALID_HANDLE_VALUE) { + CloseHandle(updateLockFileHandle); + } +# endif + // WRITE_ERROR is one of the cases where the staging failure falls back + // to applying the update on startup. + WriteStatusFile(WRITE_ERROR); + output_finish(); + return 0; + } +#endif + // When staging updates, blow away the old installation directory and + // create it from scratch. + ensure_remove_recursive(gWorkingDirPath); + } + if (!sReplaceRequest) { + // Try to create the destination directory if it doesn't exist + int rv = NS_tmkdir(gWorkingDirPath, 0755); + if (rv != OK && errno != EEXIST) { +#ifdef XP_MACOSX + if (isElevated) { + freeArguments(argc, argv); + CleanupElevatedMacUpdate(true); + } +#endif + output_finish(); + return 1; + } + } + +#ifdef XP_WIN + NS_tchar applyDirLongPath[MAXPATHLEN]; + if (!GetLongPathNameW( + gWorkingDirPath, applyDirLongPath, + sizeof(applyDirLongPath) / sizeof(applyDirLongPath[0]))) { + WriteStatusFile(WRITE_ERROR_APPLY_DIR_PATH); + LOG(("NS_main: unable to find apply to dir: " LOG_S, gWorkingDirPath)); + output_finish(); + EXIT_WHEN_ELEVATED(elevatedLockFilePath, updateLockFileHandle, 1); + if (argc > callbackIndex) { + LaunchCallbackApp(argv[5], argc - callbackIndex, argv + callbackIndex, + sUsingService); + } + return 1; + } + + HANDLE callbackFile = INVALID_HANDLE_VALUE; + if (argc > callbackIndex) { + // If the callback executable is specified it must exist for a successful + // update. It is important we null out the whole buffer here because + // later we make the assumption that the callback application is inside + // the apply-to dir. If we don't have a fully null'ed out buffer it can + // lead to stack corruption which causes crashes and other problems. + NS_tchar callbackLongPath[MAXPATHLEN]; + ZeroMemory(callbackLongPath, sizeof(callbackLongPath)); + NS_tchar* targetPath = argv[callbackIndex]; + NS_tchar buffer[MAXPATHLEN * 2] = {NS_T('\0')}; + size_t bufferLeft = MAXPATHLEN * 2; + if (sReplaceRequest) { + // In case of replace requests, we should look for the callback file in + // the destination directory. + size_t commonPrefixLength = + PathCommonPrefixW(argv[callbackIndex], gInstallDirPath, nullptr); + NS_tchar* p = buffer; + NS_tstrncpy(p, argv[callbackIndex], commonPrefixLength); + p += commonPrefixLength; + bufferLeft -= commonPrefixLength; + NS_tstrncpy(p, gInstallDirPath + commonPrefixLength, bufferLeft); + + size_t len = NS_tstrlen(gInstallDirPath + commonPrefixLength); + p += len; + bufferLeft -= len; + *p = NS_T('\\'); + ++p; + bufferLeft--; + *p = NS_T('\0'); + NS_tchar installDir[MAXPATHLEN]; + NS_tstrcpy(installDir, gInstallDirPath); + size_t callbackPrefixLength = + PathCommonPrefixW(argv[callbackIndex], installDir, nullptr); + NS_tstrncpy(p, + argv[callbackIndex] + + std::max(callbackPrefixLength, commonPrefixLength), + bufferLeft); + targetPath = buffer; + } + if (!GetLongPathNameW( + targetPath, callbackLongPath, + sizeof(callbackLongPath) / sizeof(callbackLongPath[0]))) { + WriteStatusFile(WRITE_ERROR_CALLBACK_PATH); + LOG(("NS_main: unable to find callback file: " LOG_S, targetPath)); + output_finish(); + EXIT_WHEN_ELEVATED(elevatedLockFilePath, updateLockFileHandle, 1); + if (argc > callbackIndex) { + LaunchCallbackApp(argv[5], argc - callbackIndex, argv + callbackIndex, + sUsingService); + } + return 1; + } + + // Doing this is only necessary when we're actually applying a patch. + if (!sReplaceRequest) { + int len = NS_tstrlen(applyDirLongPath); + NS_tchar* s = callbackLongPath; + NS_tchar* d = gCallbackRelPath; + // advance to the apply to directory and advance past the trailing + // backslash if present. + s += len; + if (*s == NS_T('\\')) { + ++s; + } + + // Copy the string and replace backslashes with forward slashes along + // the way. + do { + if (*s == NS_T('\\')) { + *d = NS_T('/'); + } else { + *d = *s; + } + ++s; + ++d; + } while (*s); + *d = NS_T('\0'); + ++d; + + const size_t callbackBackupPathBufSize = + sizeof(gCallbackBackupPath) / sizeof(gCallbackBackupPath[0]); + const int callbackBackupPathLen = + NS_tsnprintf(gCallbackBackupPath, callbackBackupPathBufSize, + NS_T("%s" CALLBACK_BACKUP_EXT), argv[callbackIndex]); + + if (callbackBackupPathLen < 0 || + callbackBackupPathLen >= + static_cast<int>(callbackBackupPathBufSize)) { + WriteStatusFile(USAGE_ERROR); + LOG(("NS_main: callback backup path truncated")); + output_finish(); + + // Don't attempt to launch the callback when the callback path is + // longer than expected. + EXIT_WHEN_ELEVATED(elevatedLockFilePath, updateLockFileHandle, 1); + return 1; + } + + // Make a copy of the callback executable so it can be read when + // patching. + if (!CopyFileW(argv[callbackIndex], gCallbackBackupPath, false)) { + DWORD copyFileError = GetLastError(); + if (copyFileError == ERROR_ACCESS_DENIED) { + WriteStatusFile(WRITE_ERROR_ACCESS_DENIED); + } else { + WriteStatusFile(WRITE_ERROR_CALLBACK_APP); + } + LOG(("NS_main: failed to copy callback file " LOG_S + " into place at " LOG_S, + argv[callbackIndex], gCallbackBackupPath)); + output_finish(); + EXIT_WHEN_ELEVATED(elevatedLockFilePath, updateLockFileHandle, 1); + LaunchCallbackApp(argv[callbackIndex], argc - callbackIndex, + argv + callbackIndex, sUsingService); + return 1; + } + + // Since the process may be signaled as exited by WaitForSingleObject + // before the release of the executable image try to lock the main + // executable file multiple times before giving up. If we end up giving + // up, we won't fail the update. + const int max_retries = 10; + int retries = 1; + DWORD lastWriteError = 0; + do { + // By opening a file handle wihout FILE_SHARE_READ to the callback + // executable, the OS will prevent launching the process while it is + // being updated. + callbackFile = CreateFileW(targetPath, DELETE | GENERIC_WRITE, + // allow delete, rename, and write + FILE_SHARE_DELETE | FILE_SHARE_WRITE, + nullptr, OPEN_EXISTING, 0, nullptr); + if (callbackFile != INVALID_HANDLE_VALUE) { + break; + } + + lastWriteError = GetLastError(); + LOG( + ("NS_main: callback app file open attempt %d failed. " + "File: " LOG_S ". Last error: %lu", + retries, targetPath, lastWriteError)); + + Sleep(100); + } while (++retries <= max_retries); + + // CreateFileW will fail if the callback executable is already in use. + if (callbackFile == INVALID_HANDLE_VALUE) { + bool proceedWithoutExclusive = true; + + // Fail the update if the last error was not a sharing violation. + if (lastWriteError != ERROR_SHARING_VIOLATION) { + LOG(( + "NS_main: callback app file in use, failed to exclusively open " + "executable file: " LOG_S, + argv[callbackIndex])); + if (lastWriteError == ERROR_ACCESS_DENIED) { + WriteStatusFile(WRITE_ERROR_ACCESS_DENIED); + } else { + WriteStatusFile(WRITE_ERROR_CALLBACK_APP); + } + + proceedWithoutExclusive = false; + } + + // Fail even on sharing violation from a background task, since a + // background task has a higher risk of interfering with a running + // app. Note that this does not apply when staging (when an exclusive + // lock isn't necessary), as there is no callback. + if (lastWriteError == ERROR_SHARING_VIOLATION && sUpdateSilently) { + LOG(( + "NS_main: callback app file in use, failed to exclusively open " + "executable file from background task: " LOG_S, + argv[callbackIndex])); + WriteStatusFile(WRITE_ERROR_BACKGROUND_TASK_SHARING_VIOLATION); + + proceedWithoutExclusive = false; + } + + if (!proceedWithoutExclusive) { + if (NS_tremove(gCallbackBackupPath) && errno != ENOENT) { + LOG( + ("NS_main: unable to remove backup of callback app file, " + "path: " LOG_S, + gCallbackBackupPath)); + } + output_finish(); + EXIT_WHEN_ELEVATED(elevatedLockFilePath, updateLockFileHandle, 1); + LaunchCallbackApp(argv[5], argc - callbackIndex, + argv + callbackIndex, sUsingService); + return 1; + } + + LOG( + ("NS_main: callback app file in use, continuing without " + "exclusive access for executable file: " LOG_S, + argv[callbackIndex])); + } + } + } + + // DELETE_DIR is not required when performing a staged update or replace + // request; it can be used during a replace request but then it doesn't + // use gDeleteDirPath. + if (!sStagedUpdate && !sReplaceRequest) { + // The directory to move files that are in use to on Windows. This + // directory will be deleted after the update is finished, on OS reboot + // using MoveFileEx if it contains files that are in use, or by the post + // update process after the update finishes. On Windows when performing a + // normal update (e.g. the update is not a staged update and is not a + // replace request) gWorkingDirPath is the same as gInstallDirPath and + // gWorkingDirPath is used because it is the destination directory. + NS_tsnprintf(gDeleteDirPath, + sizeof(gDeleteDirPath) / sizeof(gDeleteDirPath[0]), + NS_T("%s/%s"), gWorkingDirPath, DELETE_DIR); + + if (NS_taccess(gDeleteDirPath, F_OK)) { + NS_tmkdir(gDeleteDirPath, 0755); + } + } +#endif /* XP_WIN */ + + // Run update process on a background thread. ShowProgressUI may return + // before QuitProgressUI has been called, so wait for UpdateThreadFunc to + // terminate. Avoid showing the progress UI when staging an update, or if + // this is an elevated process on OSX. + Thread t; + if (t.Run(UpdateThreadFunc, nullptr) == 0) { + if (!sStagedUpdate && !sReplaceRequest && !sUpdateSilently +#ifdef XP_MACOSX + && !isElevated +#endif + ) { + ShowProgressUI(); + } + } + t.Join(); + +#ifdef XP_WIN + if (argc > callbackIndex && !sReplaceRequest) { + if (callbackFile != INVALID_HANDLE_VALUE) { + CloseHandle(callbackFile); + } + // Remove the copy of the callback executable. + if (NS_tremove(gCallbackBackupPath) && errno != ENOENT) { + LOG( + ("NS_main: non-fatal error removing backup of callback app file, " + "path: " LOG_S, + gCallbackBackupPath)); + } + } + + if (!sStagedUpdate && !sReplaceRequest && _wrmdir(gDeleteDirPath)) { + LOG(("NS_main: unable to remove directory: " LOG_S ", err: %d", + DELETE_DIR, errno)); + // The directory probably couldn't be removed due to it containing files + // that are in use and will be removed on OS reboot. The call to remove + // the directory on OS reboot is done after the calls to remove the files + // so the files are removed first on OS reboot since the directory must be + // empty for the directory removal to be successful. The MoveFileEx call + // to remove the directory on OS reboot will fail if the process doesn't + // have write access to the HKEY_LOCAL_MACHINE registry key but this is ok + // since the installer / uninstaller will delete the directory along with + // its contents after an update is applied, on reinstall, and on + // uninstall. + if (MoveFileEx(gDeleteDirPath, nullptr, MOVEFILE_DELAY_UNTIL_REBOOT)) { + LOG(("NS_main: directory will be removed on OS reboot: " LOG_S, + DELETE_DIR)); + } else { + LOG( + ("NS_main: failed to schedule OS reboot removal of " + "directory: " LOG_S, + DELETE_DIR)); + } + } +#endif /* XP_WIN */ + + } // if (!isDMGInstall) + +#ifdef XP_MACOSX + if (isElevated) { + SetGroupOwnershipAndPermissions(gInstallDirPath); + freeArguments(argc, argv); + CleanupElevatedMacUpdate(false); + } else if (IsOwnedByGroupAdmin(gInstallDirPath)) { + // If the group ownership of the Firefox .app bundle was set to the "admin" + // group during a previous elevated update, we need to ensure that all files + // in the bundle have group ownership of "admin" as well as write permission + // for the group to not break updates in the future. + SetGroupOwnershipAndPermissions(gInstallDirPath); + } +#endif /* XP_MACOSX */ + + output_finish(); + + int retVal = LaunchCallbackAndPostProcessApps(argc, argv, callbackIndex +#ifdef XP_WIN + , + elevatedLockFilePath, + updateLockFileHandle +#elif XP_MACOSX + , + isElevated, + std::move(umaskContext) +#endif + ); + + return retVal ? retVal : (gSucceeded ? 0 : 1); +} + +class ActionList { + public: + ActionList() : mFirst(nullptr), mLast(nullptr), mCount(0) {} + ~ActionList(); + + void Append(Action* action); + int Prepare(); + int Execute(); + void Finish(int status); + + private: + Action* mFirst; + Action* mLast; + int mCount; +}; + +ActionList::~ActionList() { + Action* a = mFirst; + while (a) { + Action* b = a; + a = a->mNext; + delete b; + } +} + +void ActionList::Append(Action* action) { + if (mLast) { + mLast->mNext = action; + } else { + mFirst = action; + } + + mLast = action; + mCount++; +} + +int ActionList::Prepare() { + // If the action list is empty then we should fail in order to signal that + // something has gone wrong. Otherwise we report success when nothing is + // actually done. See bug 327140. + if (mCount == 0) { + LOG(("empty action list")); + return MAR_ERROR_EMPTY_ACTION_LIST; + } + + Action* a = mFirst; + int i = 0; + while (a) { + int rv = a->Prepare(); + if (rv) { + return rv; + } + + float percent = float(++i) / float(mCount); + UpdateProgressUI(PROGRESS_PREPARE_SIZE * percent); + + a = a->mNext; + } + + return OK; +} + +int ActionList::Execute() { + int currentProgress = 0, maxProgress = 0; + Action* a = mFirst; + while (a) { + maxProgress += a->mProgressCost; + a = a->mNext; + } + + a = mFirst; + while (a) { + int rv = a->Execute(); + if (rv) { + LOG(("### execution failed")); + return rv; + } + + currentProgress += a->mProgressCost; + float percent = float(currentProgress) / float(maxProgress); + UpdateProgressUI(PROGRESS_PREPARE_SIZE + PROGRESS_EXECUTE_SIZE * percent); + + a = a->mNext; + } + + return OK; +} + +void ActionList::Finish(int status) { + Action* a = mFirst; + int i = 0; + while (a) { + a->Finish(status); + + float percent = float(++i) / float(mCount); + UpdateProgressUI(PROGRESS_PREPARE_SIZE + PROGRESS_EXECUTE_SIZE + + PROGRESS_FINISH_SIZE * percent); + + a = a->mNext; + } + + if (status == OK) { + gSucceeded = true; + } +} + +#ifdef XP_WIN +int add_dir_entries(const NS_tchar* dirpath, ActionList* list) { + int rv = OK; + WIN32_FIND_DATAW finddata; + HANDLE hFindFile; + NS_tchar searchspec[MAXPATHLEN]; + NS_tchar foundpath[MAXPATHLEN]; + + NS_tsnprintf(searchspec, sizeof(searchspec) / sizeof(searchspec[0]), + NS_T("%s*"), dirpath); + mozilla::UniquePtr<const NS_tchar> pszSpec(get_full_path(searchspec)); + + hFindFile = FindFirstFileW(pszSpec.get(), &finddata); + if (hFindFile != INVALID_HANDLE_VALUE) { + do { + // Don't process the current or parent directory. + if (NS_tstrcmp(finddata.cFileName, NS_T(".")) == 0 || + NS_tstrcmp(finddata.cFileName, NS_T("..")) == 0) { + continue; + } + + NS_tsnprintf(foundpath, sizeof(foundpath) / sizeof(foundpath[0]), + NS_T("%s%s"), dirpath, finddata.cFileName); + if (finddata.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) { + NS_tsnprintf(foundpath, sizeof(foundpath) / sizeof(foundpath[0]), + NS_T("%s/"), foundpath); + // Recurse into the directory. + rv = add_dir_entries(foundpath, list); + if (rv) { + LOG(("add_dir_entries error: " LOG_S ", err: %d", foundpath, rv)); + return rv; + } + } else { + // Add the file to be removed to the ActionList. + NS_tchar* quotedpath = get_quoted_path(foundpath); + if (!quotedpath) { + return PARSE_ERROR; + } + + mozilla::UniquePtr<Action> action(new RemoveFile()); + rv = action->Parse(quotedpath); + if (rv) { + LOG(("add_dir_entries Parse error on recurse: " LOG_S ", err: %d", + quotedpath, rv)); + free(quotedpath); + return rv; + } + free(quotedpath); + + list->Append(action.release()); + } + } while (FindNextFileW(hFindFile, &finddata) != 0); + + FindClose(hFindFile); + { + // Add the directory to be removed to the ActionList. + NS_tchar* quotedpath = get_quoted_path(dirpath); + if (!quotedpath) { + return PARSE_ERROR; + } + + mozilla::UniquePtr<Action> action(new RemoveDir()); + rv = action->Parse(quotedpath); + if (rv) { + LOG(("add_dir_entries Parse error on close: " LOG_S ", err: %d", + quotedpath, rv)); + } else { + list->Append(action.release()); + } + free(quotedpath); + } + } + + return rv; +} + +#elif defined(HAVE_FTS_H) + int add_dir_entries(const NS_tchar* dirpath, ActionList* list) { + int rv = OK; + FTS* ftsdir; + FTSENT* ftsdirEntry; + mozilla::UniquePtr<NS_tchar[]> searchpath(get_full_path(dirpath)); + + // Remove the trailing slash so the paths don't contain double slashes. The + // existence of the slash has already been checked in DoUpdate. + searchpath[NS_tstrlen(searchpath.get()) - 1] = NS_T('\0'); + char* const pathargv[] = {searchpath.get(), nullptr}; + + // FTS_NOCHDIR is used so relative paths from the destination directory are + // returned. + if (!(ftsdir = fts_open(pathargv, + FTS_PHYSICAL | FTS_NOSTAT | FTS_XDEV | FTS_NOCHDIR, + nullptr))) { + return UNEXPECTED_FILE_OPERATION_ERROR; + } + + while ((ftsdirEntry = fts_read(ftsdir)) != nullptr) { + NS_tchar foundpath[MAXPATHLEN]; + NS_tchar* quotedpath = nullptr; + mozilla::UniquePtr<Action> action; + + switch (ftsdirEntry->fts_info) { + // Filesystem objects that shouldn't be in the application's directories + case FTS_SL: + case FTS_SLNONE: + case FTS_DEFAULT: + LOG(("add_dir_entries: found a non-standard file: " LOG_S, + ftsdirEntry->fts_path)); + // Fall through and try to remove as a file + [[fallthrough]]; + + // Files + case FTS_F: + case FTS_NSOK: + // Add the file to be removed to the ActionList. + NS_tsnprintf(foundpath, sizeof(foundpath) / sizeof(foundpath[0]), + NS_T("%s"), ftsdirEntry->fts_accpath); + quotedpath = get_quoted_path(get_relative_path(foundpath)); + if (!quotedpath) { + rv = UPDATER_QUOTED_PATH_MEM_ERROR; + break; + } + action.reset(new RemoveFile()); + rv = action->Parse(quotedpath); + free(quotedpath); + if (!rv) { + list->Append(action.release()); + } + break; + + // Directories + case FTS_DP: + // Add the directory to be removed to the ActionList. + NS_tsnprintf(foundpath, sizeof(foundpath) / sizeof(foundpath[0]), + NS_T("%s/"), ftsdirEntry->fts_accpath); + quotedpath = get_quoted_path(get_relative_path(foundpath)); + if (!quotedpath) { + rv = UPDATER_QUOTED_PATH_MEM_ERROR; + break; + } + + action.reset(new RemoveDir()); + rv = action->Parse(quotedpath); + free(quotedpath); + if (!rv) { + list->Append(action.release()); + } + break; + + // Errors + case FTS_DNR: + case FTS_NS: + // ENOENT is an acceptable error for FTS_DNR and FTS_NS and means that + // we're racing with ourselves. Though strange, the entry will be + // removed anyway. + if (ENOENT == ftsdirEntry->fts_errno) { + rv = OK; + break; + } + [[fallthrough]]; + + case FTS_ERR: + rv = UNEXPECTED_FILE_OPERATION_ERROR; + LOG(("add_dir_entries: fts_read() error: " LOG_S ", err: %d", + ftsdirEntry->fts_path, ftsdirEntry->fts_errno)); + break; + + case FTS_DC: + rv = UNEXPECTED_FILE_OPERATION_ERROR; + LOG(("add_dir_entries: fts_read() returned FT_DC: " LOG_S, + ftsdirEntry->fts_path)); + break; + + default: + // FTS_D is ignored and FTS_DP is used instead (post-order). + rv = OK; + break; + } + + if (rv != OK) { + break; + } + } + + fts_close(ftsdir); + + return rv; + } + +#else + +int add_dir_entries(const NS_tchar* dirpath, ActionList* list) { + int rv = OK; + NS_tchar foundpath[PATH_MAX]; + struct { + dirent dent_buffer; + char chars[NAME_MAX]; + } ent_buf; + struct dirent* ent; + mozilla::UniquePtr<NS_tchar[]> searchpath(get_full_path(dirpath)); + + DIR* dir = opendir(searchpath.get()); + if (!dir) { + LOG(("add_dir_entries error on opendir: " LOG_S ", err: %d", + searchpath.get(), errno)); + return UNEXPECTED_FILE_OPERATION_ERROR; + } + + while (readdir_r(dir, (dirent*)&ent_buf, &ent) == 0 && ent) { + if ((strcmp(ent->d_name, ".") == 0) || (strcmp(ent->d_name, "..") == 0)) { + continue; + } + + NS_tsnprintf(foundpath, sizeof(foundpath) / sizeof(foundpath[0]), + NS_T("%s%s"), searchpath.get(), ent->d_name); + struct stat64 st_buf; + int test = stat64(foundpath, &st_buf); + if (test) { + closedir(dir); + return UNEXPECTED_FILE_OPERATION_ERROR; + } + if (S_ISDIR(st_buf.st_mode)) { + NS_tsnprintf(foundpath, sizeof(foundpath) / sizeof(foundpath[0]), + NS_T("%s%s/"), dirpath, ent->d_name); + // Recurse into the directory. + rv = add_dir_entries(foundpath, list); + if (rv) { + LOG(("add_dir_entries error: " LOG_S ", err: %d", foundpath, rv)); + closedir(dir); + return rv; + } + } else { + // Add the file to be removed to the ActionList. + NS_tchar* quotedpath = get_quoted_path(get_relative_path(foundpath)); + if (!quotedpath) { + closedir(dir); + return PARSE_ERROR; + } + + mozilla::UniquePtr<Action> action(new RemoveFile()); + rv = action->Parse(quotedpath); + if (rv) { + LOG(("add_dir_entries Parse error on recurse: " LOG_S ", err: %d", + quotedpath, rv)); + free(quotedpath); + closedir(dir); + return rv; + } + free(quotedpath); + + list->Append(action.release()); + } + } + closedir(dir); + + // Add the directory to be removed to the ActionList. + NS_tchar* quotedpath = get_quoted_path(get_relative_path(dirpath)); + if (!quotedpath) { + return PARSE_ERROR; + } + + mozilla::UniquePtr<Action> action(new RemoveDir()); + rv = action->Parse(quotedpath); + if (rv) { + LOG(("add_dir_entries Parse error on close: " LOG_S ", err: %d", quotedpath, + rv)); + } else { + list->Append(action.release()); + } + free(quotedpath); + + return rv; +} + +#endif + +/* + * Gets the contents of an update manifest file. The return value is malloc'd + * and it is the responsibility of the caller to free it. + * + * @param manifest + * The full path to the manifest file. + * @return On success the contents of the manifest and nullptr otherwise. + */ +static NS_tchar* GetManifestContents(const NS_tchar* manifest) { + AutoFile mfile(NS_tfopen(manifest, NS_T("rb"))); + if (mfile == nullptr) { + LOG(("GetManifestContents: error opening manifest file: " LOG_S, manifest)); + return nullptr; + } + + struct stat ms; + int rv = fstat(fileno((FILE*)mfile), &ms); + if (rv) { + LOG(("GetManifestContents: error stating manifest file: " LOG_S, manifest)); + return nullptr; + } + + char* mbuf = (char*)malloc(ms.st_size + 1); + if (!mbuf) { + return nullptr; + } + + size_t r = ms.st_size; + char* rb = mbuf; + while (r) { + const size_t count = mmin(SSIZE_MAX, r); + size_t c = fread(rb, 1, count, mfile); + if (c != count) { + LOG(("GetManifestContents: error reading manifest file: " LOG_S, + manifest)); + free(mbuf); + return nullptr; + } + + r -= c; + rb += c; + } + *rb = '\0'; + +#ifndef XP_WIN + return mbuf; +#else + NS_tchar* wrb = (NS_tchar*)malloc((ms.st_size + 1) * sizeof(NS_tchar)); + if (!wrb) { + free(mbuf); + return nullptr; + } + + if (!MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, mbuf, -1, wrb, + ms.st_size + 1)) { + LOG(("GetManifestContents: error converting utf8 to utf16le: %lu", + GetLastError())); + free(mbuf); + free(wrb); + return nullptr; + } + free(mbuf); + + return wrb; +#endif +} + +int AddPreCompleteActions(ActionList* list) { +#ifdef XP_MACOSX + mozilla::UniquePtr<NS_tchar[]> manifestPath( + get_full_path(NS_T("Contents/Resources/precomplete"))); +#else + mozilla::UniquePtr<NS_tchar[]> manifestPath( + get_full_path(NS_T("precomplete"))); +#endif + + NS_tchar* buf = GetManifestContents(manifestPath.get()); + if (!buf) { + LOG( + ("AddPreCompleteActions: error getting contents of precomplete " + "manifest")); + // Applications aren't required to have a precomplete manifest. The mar + // generation scripts enforce the presence of a precomplete manifest. + return OK; + } + NS_tchar* rb = buf; + + int rv; + NS_tchar* line; + while ((line = mstrtok(kNL, &rb)) != 0) { + // skip comments + if (*line == NS_T('#')) { + continue; + } + + NS_tchar* token = mstrtok(kWhitespace, &line); + if (!token) { + LOG(("AddPreCompleteActions: token not found in manifest")); + free(buf); + return PARSE_ERROR; + } + + Action* action = nullptr; + if (NS_tstrcmp(token, NS_T("remove")) == 0) { // rm file + action = new RemoveFile(); + } else if (NS_tstrcmp(token, NS_T("remove-cc")) == + 0) { // no longer supported + continue; + } else if (NS_tstrcmp(token, NS_T("rmdir")) == 0) { // rmdir if empty + action = new RemoveDir(); + } else { + LOG(("AddPreCompleteActions: unknown token: " LOG_S, token)); + free(buf); + return PARSE_ERROR; + } + + if (!action) { + free(buf); + return BAD_ACTION_ERROR; + } + + rv = action->Parse(line); + if (rv) { + delete action; + free(buf); + return rv; + } + + list->Append(action); + } + + free(buf); + return OK; +} + +int DoUpdate() { + NS_tchar manifest[MAXPATHLEN]; + NS_tsnprintf(manifest, sizeof(manifest) / sizeof(manifest[0]), + NS_T("%s/updating/update.manifest"), gWorkingDirPath); + ensure_parent_dir(manifest); + + // extract the manifest + int rv = gArchiveReader.ExtractFile("updatev3.manifest", manifest); + if (rv) { + LOG(("DoUpdate: error extracting manifest file")); + return rv; + } + + NS_tchar* buf = GetManifestContents(manifest); + // The manifest is located in the <working_dir>/updating directory which is + // removed after the update has finished so don't delete it here. + if (!buf) { + LOG(("DoUpdate: error opening manifest file: " LOG_S, manifest)); + return READ_ERROR; + } + NS_tchar* rb = buf; + + ActionList list; + NS_tchar* line; + bool isFirstAction = true; + while ((line = mstrtok(kNL, &rb)) != 0) { + // skip comments + if (*line == NS_T('#')) { + continue; + } + + NS_tchar* token = mstrtok(kWhitespace, &line); + if (!token) { + LOG(("DoUpdate: token not found in manifest")); + free(buf); + return PARSE_ERROR; + } + + if (isFirstAction) { + isFirstAction = false; + // The update manifest isn't required to have a type declaration. The mar + // generation scripts enforce the presence of the type declaration. + if (NS_tstrcmp(token, NS_T("type")) == 0) { + const NS_tchar* type = mstrtok(kQuote, &line); + LOG(("UPDATE TYPE " LOG_S, type)); + if (NS_tstrcmp(type, NS_T("complete")) == 0) { + rv = AddPreCompleteActions(&list); + if (rv) { + free(buf); + return rv; + } + } + continue; + } + } + + Action* action = nullptr; + if (NS_tstrcmp(token, NS_T("remove")) == 0) { // rm file + action = new RemoveFile(); + } else if (NS_tstrcmp(token, NS_T("rmdir")) == 0) { // rmdir if empty + action = new RemoveDir(); + } else if (NS_tstrcmp(token, NS_T("rmrfdir")) == 0) { // rmdir recursive + const NS_tchar* reldirpath = mstrtok(kQuote, &line); + if (!reldirpath) { + free(buf); + return PARSE_ERROR; + } + + if (reldirpath[NS_tstrlen(reldirpath) - 1] != NS_T('/')) { + free(buf); + return PARSE_ERROR; + } + + rv = add_dir_entries(reldirpath, &list); + if (rv) { + free(buf); + return rv; + } + + continue; + } else if (NS_tstrcmp(token, NS_T("add")) == 0) { + action = new AddFile(); + } else if (NS_tstrcmp(token, NS_T("patch")) == 0) { + action = new PatchFile(); + } else if (NS_tstrcmp(token, NS_T("add-if")) == 0) { // Add if exists + action = new AddIfFile(); + } else if (NS_tstrcmp(token, NS_T("add-if-not")) == + 0) { // Add if not exists + action = new AddIfNotFile(); + } else if (NS_tstrcmp(token, NS_T("patch-if")) == 0) { // Patch if exists + action = new PatchIfFile(); + } else { + LOG(("DoUpdate: unknown token: " LOG_S, token)); + free(buf); + return PARSE_ERROR; + } + + if (!action) { + free(buf); + return BAD_ACTION_ERROR; + } + + rv = action->Parse(line); + if (rv) { + free(buf); + return rv; + } + + list.Append(action); + } + + rv = list.Prepare(); + if (rv) { + free(buf); + return rv; + } + + rv = list.Execute(); + + list.Finish(rv); + free(buf); + return rv; +} diff --git a/toolkit/mozapps/update/updater/updater.exe.comctl32.manifest b/toolkit/mozapps/update/updater/updater.exe.comctl32.manifest new file mode 100644 index 0000000000..9df0057e64 --- /dev/null +++ b/toolkit/mozapps/update/updater/updater.exe.comctl32.manifest @@ -0,0 +1,43 @@ +<?xml version="1.0" encoding="UTF-8" standalone="yes"?> +<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0"> +<assemblyIdentity + version="1.0.0.0" + processorArchitecture="*" + name="Updater" + type="win32" +/> +<description>Updater</description> +<dependency> + <dependentAssembly> + <assemblyIdentity + type="win32" + name="Microsoft.Windows.Common-Controls" + version="6.0.0.0" + processorArchitecture="*" + publicKeyToken="6595b64144ccf1df" + language="*" + /> + </dependentAssembly> +</dependency> +<ms_asmv3:trustInfo xmlns:ms_asmv3="urn:schemas-microsoft-com:asm.v3"> + <ms_asmv3:security> + <ms_asmv3:requestedPrivileges> + <ms_asmv3:requestedExecutionLevel level="asInvoker" uiAccess="false" /> + </ms_asmv3:requestedPrivileges> + </ms_asmv3:security> +</ms_asmv3:trustInfo> +<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1"> + <application> + <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/> + <supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/> + <supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/> + <supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/> + </application> +</compatibility> +<ms_asmv3:application xmlns:ms_asmv3="urn:schemas-microsoft-com:asm.v3"> + <ms_asmv3:windowsSettings xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings"> + <dpiAware>True/PM</dpiAware> + <dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2,PerMonitor</dpiAwareness> + </ms_asmv3:windowsSettings> +</ms_asmv3:application> +</assembly> diff --git a/toolkit/mozapps/update/updater/updater.exe.manifest b/toolkit/mozapps/update/updater/updater.exe.manifest new file mode 100644 index 0000000000..6646ec6534 --- /dev/null +++ b/toolkit/mozapps/update/updater/updater.exe.manifest @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="UTF-8" standalone="yes"?> +<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0"> +<assemblyIdentity + version="1.0.0.0" + processorArchitecture="*" + name="Updater" + type="win32" +/> +<description>Updater</description> +<ms_asmv3:trustInfo xmlns:ms_asmv3="urn:schemas-microsoft-com:asm.v3"> + <ms_asmv3:security> + <ms_asmv3:requestedPrivileges> + <ms_asmv3:requestedExecutionLevel level="asInvoker" uiAccess="false" /> + </ms_asmv3:requestedPrivileges> + </ms_asmv3:security> +</ms_asmv3:trustInfo> +<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1"> + <application> + <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/> + <supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/> + <supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/> + <supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/> + </application> +</compatibility> +<ms_asmv3:application xmlns:ms_asmv3="urn:schemas-microsoft-com:asm.v3"> + <ms_asmv3:windowsSettings xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings"> + <dpiAware>True/PM</dpiAware> + <dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2,PerMonitor</dpiAwareness> + </ms_asmv3:windowsSettings> +</ms_asmv3:application> +</assembly> diff --git a/toolkit/mozapps/update/updater/updater.ico b/toolkit/mozapps/update/updater/updater.ico Binary files differnew file mode 100644 index 0000000000..48457029d6 --- /dev/null +++ b/toolkit/mozapps/update/updater/updater.ico diff --git a/toolkit/mozapps/update/updater/updater.png b/toolkit/mozapps/update/updater/updater.png Binary files differnew file mode 100644 index 0000000000..6f1251bd03 --- /dev/null +++ b/toolkit/mozapps/update/updater/updater.png diff --git a/toolkit/mozapps/update/updater/updater.rc b/toolkit/mozapps/update/updater/updater.rc new file mode 100644 index 0000000000..aff834a597 --- /dev/null +++ b/toolkit/mozapps/update/updater/updater.rc @@ -0,0 +1,137 @@ +/* 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/. */ + +// Microsoft Visual C++ generated resource script. +// +#if defined(TEST_UPDATER) || defined(DEP_UPDATER) +#include "../resource.h" +#define MANIFEST_PATH "../updater.exe.manifest" +#define COMCTL32_MANIFEST_PATH "../updater.exe.comctl32.manifest" +#define ICON_PATH "../updater.ico" +#else +#include "resource.h" +#define MANIFEST_PATH "updater.exe.manifest" +#define COMCTL32_MANIFEST_PATH "updater.exe.comctl32.manifest" +#define ICON_PATH "updater.ico" +#endif + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winresrc.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (U.S.) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +#ifdef _WIN32 +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US +#pragma code_page(1252) +#endif //_WIN32 + +///////////////////////////////////////////////////////////////////////////// +// +// RT_MANIFEST +// + +1 RT_MANIFEST MANIFEST_PATH +IDR_COMCTL32_MANIFEST RT_MANIFEST COMCTL32_MANIFEST_PATH + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +IDI_DIALOG ICON ICON_PATH + + +///////////////////////////////////////////////////////////////////////////// +// +// Embedded an identifier to uniquely identiy this as a Mozilla updater. +// + +STRINGTABLE +{ + IDS_UPDATER_IDENTITY, "moz-updater.exe-4cdccec4-5ee0-4a06-9817-4cd899a9db49" +} + + +///////////////////////////////////////////////////////////////////////////// +// +// Dialog +// + +IDD_DIALOG DIALOGEX 0, 0, 253, 41 +STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CAPTION +FONT 8, "MS Shell Dlg", 400, 0, 0x1 +BEGIN + CONTROL "",IDC_PROGRESS,"msctls_progress32",WS_BORDER,7,24,239,10 + LTEXT "",IDC_INFO,7,8,239,13,SS_NOPREFIX +END + + +///////////////////////////////////////////////////////////////////////////// +// +// DESIGNINFO +// + +#ifdef APSTUDIO_INVOKED +GUIDELINES DESIGNINFO +BEGIN + IDD_DIALOG, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 246 + TOPMARGIN, 7 + BOTTOMMARGIN, 39 + END +END +#endif // APSTUDIO_INVOKED + + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winresrc.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + +#endif // English (U.S.) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED + diff --git a/toolkit/mozapps/update/updater/xpcshellCertificate.der b/toolkit/mozapps/update/updater/xpcshellCertificate.der Binary files differnew file mode 100644 index 0000000000..ea1fd47faa --- /dev/null +++ b/toolkit/mozapps/update/updater/xpcshellCertificate.der |