/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=8 sts=2 et sw=2 tw=80: */ /* 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 https://mozilla.org/MPL/2.0/. */ #include "ErrorHandler.h" #include #include "mozilla/ArrayUtils.h" #include "mozilla/CmdLineAndEnvUtils.h" #include "mozilla/DebugOnly.h" #include "mozilla/JSONWriter.h" #include "mozilla/UniquePtr.h" #include "mozilla/Unused.h" #include "mozilla/WinTokenUtils.h" #include "mozilla/XREAppData.h" #include "mozilla/glue/WindowsDllServices.h" #include "mozilla/mscom/ProcessRuntime.h" #include "nsWindowsHelpers.h" #if defined(MOZ_LAUNCHER_PROCESS) # include "mozilla/LauncherRegistryInfo.h" #endif // defined(MOZ_LAUNCHER_PROCESS) #include #include #include #include #include #include #include #include #include #if !defined(__MINGW32__) # include # include # include #endif // !defined(__MINGW32__) #include #if !defined(RRF_SUBKEY_WOW6464KEY) # define RRF_SUBKEY_WOW6464KEY 0x00010000 #endif // !defined(RRF_SUBKEY_WOW6464KEY) #define QUOTE_ME2(x) #x #define QUOTE_ME(x) QUOTE_ME2(x) #define TELEMETRY_BASE_URL L"https://incoming.telemetry.mozilla.org/submit" #define TELEMETRY_NAMESPACE L"/firefox-launcher-process" #define TELEMETRY_LAUNCHER_PING_DOCTYPE L"/launcher-process-failure" #define TELEMETRY_LAUNCHER_PING_VERSION L"/1" static const wchar_t kUrl[] = TELEMETRY_BASE_URL TELEMETRY_NAMESPACE TELEMETRY_LAUNCHER_PING_DOCTYPE TELEMETRY_LAUNCHER_PING_VERSION L"/"; static const uint32_t kGuidCharLenWithNul = 39; static const uint32_t kGuidCharLenNoBracesNoNul = 36; static const mozilla::StaticXREAppData* gAppData; // Ordinarily, errors are only reported to the Windows Event Log when they are // not reported upstream via telemetry (usually due either to telemetry being // disabled or to network failure). // // If `--log-launcher-error` is given at the command line, launcher errors will // always be reported to the Windows Event Log, regardless of whether or not // they're sent upstream. static bool gForceEventLog = false; namespace { constexpr wchar_t kEventSourceName[] = L"" MOZ_APP_DISPLAYNAME " Launcher"; struct EventSourceDeleter { using pointer = HANDLE; void operator()(pointer aEvtSrc) { ::DeregisterEventSource(aEvtSrc); } }; using EventLog = mozilla::UniquePtr; struct SerializedEventData { HRESULT mHr; uint32_t mLine; char mFile[1]; }; } // anonymous namespace static void PostErrorToLog(const mozilla::LauncherError& aError) { // This is very bare-bones; just enough to spit out an HRESULT to the // Application event log. EventLog log(::RegisterEventSourceW(nullptr, kEventSourceName)); if (!log) { return; } size_t fileLen = strlen(aError.mFile); size_t dataLen = sizeof(HRESULT) + sizeof(uint32_t) + fileLen; auto evtDataBuf = mozilla::MakeUnique(dataLen); SerializedEventData& evtData = *reinterpret_cast(evtDataBuf.get()); evtData.mHr = aError.mError.AsHResult(); evtData.mLine = aError.mLine; // Since this is binary data, we're not concerning ourselves with null // terminators. memcpy(evtData.mFile, aError.mFile, fileLen); ::ReportEventW(log.get(), EVENTLOG_ERROR_TYPE, 0, aError.mError.AsHResult(), nullptr, 0, dataLen, nullptr, evtDataBuf.get()); } #if defined(MOZ_TELEMETRY_REPORTING) namespace { // This JSONWriteFunc writes directly to a temp file. By creating this file // with the FILE_ATTRIBUTE_TEMPORARY attribute, we hint to the OS that this // file is short-lived. The OS will try to avoid flushing it to disk if at // all possible. class TempFileWriter final : public mozilla::JSONWriteFunc { public: TempFileWriter() : mFailed(false), mSuccessfulHandoff(false) { wchar_t name[MAX_PATH + 1] = {}; if (_wtmpnam_s(name)) { mFailed = true; return; } mTempFileName = name; mTempFile.own(::CreateFileW(name, GENERIC_WRITE, FILE_SHARE_READ, nullptr, CREATE_NEW, FILE_ATTRIBUTE_TEMPORARY, nullptr)); if (mTempFile.get() == INVALID_HANDLE_VALUE) { mFailed = true; } } ~TempFileWriter() { if (mSuccessfulHandoff) { // It is no longer our responsibility to delete the temp file if we have // successfully handed it off to pingsender. return; } mTempFile.reset(); ::DeleteFileW(mTempFileName.c_str()); } explicit operator bool() const { return !mFailed; } void Write(const mozilla::Span& aStr) final { if (mFailed) { return; } DWORD bytesWritten = 0; if (!::WriteFile(mTempFile, aStr.data(), aStr.size(), &bytesWritten, nullptr) || bytesWritten != aStr.size()) { mFailed = true; } } const std::wstring& GetFileName() const { return mTempFileName; } void SetSuccessfulHandoff() { mSuccessfulHandoff = true; } private: bool mFailed; bool mSuccessfulHandoff; std::wstring mTempFileName; nsAutoHandle mTempFile; }; using SigMap = mozilla::Vector; } // anonymous namespace // This is the guideline for maximum string length for telemetry intake static const size_t kMaxStrLen = 80; static mozilla::UniquePtr WideToUTF8(const wchar_t* aStr, const size_t aStrLenExclNul) { // Yes, this might not handle surrogate pairs correctly. Let's just let // WideCharToMultiByte fail in that unlikely case. size_t cvtLen = std::min(aStrLenExclNul, kMaxStrLen); int numConv = ::WideCharToMultiByte(CP_UTF8, 0, aStr, cvtLen, nullptr, 0, nullptr, nullptr); if (!numConv) { return nullptr; } // Include room for the null terminator by adding one auto buf = mozilla::MakeUnique(numConv + 1); numConv = ::WideCharToMultiByte(CP_UTF8, 0, aStr, cvtLen, buf.get(), numConv, nullptr, nullptr); if (!numConv) { return nullptr; } // Add null termination. numConv does not include the terminator, so we don't // subtract 1 when indexing into buf. buf[numConv] = 0; return buf; } static mozilla::UniquePtr WideToUTF8(const wchar_t* aStr) { return WideToUTF8(aStr, wcslen(aStr)); } static mozilla::UniquePtr WideToUTF8(const std::wstring& aStr) { return WideToUTF8(aStr.c_str(), aStr.length()); } // MinGW does not support the Windows Security Center APIs. # if !defined(__MINGW32__) static mozilla::UniquePtr WideToUTF8(const _bstr_t& aStr) { return WideToUTF8(static_cast(aStr), aStr.length()); } namespace { struct ProviderKey { WSC_SECURITY_PROVIDER mProviderType; const char* mKey; }; } // anonymous namespace static bool EnumWSCProductList(RefPtr& aProdList, mozilla::JSONWriter& aJson) { LONG count; HRESULT hr = aProdList->get_Count(&count); if (FAILED(hr)) { return false; } // Unlikely, but put a bound on the max length of the output array for the // purposes of telemetry intake. count = std::min(count, 1000L); // Record the name(s) of each active registered product in this category for (LONG index = 0; index < count; ++index) { RefPtr product; hr = aProdList->get_Item(index, getter_AddRefs(product)); if (FAILED(hr)) { return false; } WSC_SECURITY_PRODUCT_STATE state; hr = product->get_ProductState(&state); if (FAILED(hr)) { return false; } // We only care about products that are active if (state == WSC_SECURITY_PRODUCT_STATE_OFF || state == WSC_SECURITY_PRODUCT_STATE_SNOOZED || state == WSC_SECURITY_PRODUCT_STATE_EXPIRED) { continue; } _bstr_t bName; hr = product->get_ProductName(bName.GetAddress()); if (FAILED(hr)) { return false; } auto buf = WideToUTF8(bName); if (!buf) { return false; } aJson.StringElement(mozilla::MakeStringSpan(buf.get())); } return true; } static const ProviderKey gProvKeys[] = { {WSC_SECURITY_PROVIDER_ANTIVIRUS, "av"}, {WSC_SECURITY_PROVIDER_ANTISPYWARE, "antispyware"}, {WSC_SECURITY_PROVIDER_FIREWALL, "firewall"}}; static bool AddWscInfo(mozilla::JSONWriter& aJson) { // We need COM for this. Using ProcessRuntime so that process-global COM // configuration is done correctly mozilla::mscom::ProcessRuntime mscom( mozilla::mscom::ProcessRuntime::ProcessCategory::Launcher); if (!mscom) { // We haven't written anything yet, so we can return true here and continue // capturing data. return true; } aJson.StartObjectProperty("security"); const CLSID clsid = __uuidof(WSCProductList); const IID iid = __uuidof(IWSCProductList); for (uint32_t index = 0; index < mozilla::ArrayLength(gProvKeys); ++index) { // NB: A separate instance of IWSCProductList is needed for each distinct // security provider type; MSDN says that we cannot reuse the same object // and call Initialize() to pave over the previous data. RefPtr prodList; HRESULT hr = ::CoCreateInstance(clsid, nullptr, CLSCTX_INPROC_SERVER, iid, getter_AddRefs(prodList)); if (FAILED(hr)) { return false; } hr = prodList->Initialize(gProvKeys[index].mProviderType); if (FAILED(hr)) { return false; } aJson.StartArrayProperty(mozilla::MakeStringSpan(gProvKeys[index].mKey)); if (!EnumWSCProductList(prodList, aJson)) { return false; } aJson.EndArray(); } aJson.EndObject(); return true; } # endif // !defined(__MINGW32__) // Max array length for telemetry intake. static const size_t kMaxArrayLen = 1000; static bool AddModuleInfo(const nsAutoHandle& aSnapshot, mozilla::JSONWriter& aJson) { if (aSnapshot.get() == INVALID_HANDLE_VALUE) { // We haven't written anything yet, so we can return true here and continue // capturing data. return true; } SigMap signatures; size_t moduleCount = 0; MODULEENTRY32W module = {sizeof(module)}; if (!::Module32FirstW(aSnapshot, &module)) { // We haven't written anything yet, so we can return true here and continue // capturing data. return true; } mozilla::glue::BasicDllServices dllServices; aJson.StartObjectProperty("modules"); // For each module, add its version number (or empty string if not present), // followed by an optional index into the signatures array do { ++moduleCount; wchar_t leaf[_MAX_FNAME] = {}; if (::_wsplitpath_s(module.szExePath, nullptr, 0, nullptr, 0, leaf, mozilla::ArrayLength(leaf), nullptr, 0)) { return false; } if (_wcslwr_s(leaf, mozilla::ArrayLength(leaf))) { return false; } auto leafUtf8 = WideToUTF8(leaf); if (!leafUtf8) { return false; } aJson.StartArrayProperty(mozilla::MakeStringSpan(leafUtf8.get())); std::string version; DWORD verInfoSize = ::GetFileVersionInfoSizeW(module.szExePath, nullptr); if (verInfoSize) { auto verInfoBuf = mozilla::MakeUnique(verInfoSize); if (::GetFileVersionInfoW(module.szExePath, 0, verInfoSize, verInfoBuf.get())) { VS_FIXEDFILEINFO* fixedInfo = nullptr; UINT fixedInfoLen = 0; if (::VerQueryValueW(verInfoBuf.get(), L"\\", reinterpret_cast(&fixedInfo), &fixedInfoLen)) { std::ostringstream oss; oss << HIWORD(fixedInfo->dwFileVersionMS) << '.' << LOWORD(fixedInfo->dwFileVersionMS) << '.' << HIWORD(fixedInfo->dwFileVersionLS) << '.' << LOWORD(fixedInfo->dwFileVersionLS); version = oss.str(); } } } aJson.StringElement(version); mozilla::Maybe sigIndex; auto signedBy = dllServices.GetBinaryOrgName(module.szExePath); if (signedBy) { std::wstring strSignedBy(signedBy.get()); auto entry = std::find(signatures.begin(), signatures.end(), strSignedBy); if (entry == signatures.end()) { mozilla::Unused << signatures.append(std::move(strSignedBy)); entry = &signatures.back(); } sigIndex = mozilla::Some(entry - signatures.begin()); } if (sigIndex) { aJson.IntElement(sigIndex.value()); } aJson.EndArray(); } while (moduleCount < kMaxArrayLen && ::Module32NextW(aSnapshot, &module)); aJson.EndObject(); aJson.StartArrayProperty("signatures"); // Serialize each entry in the signatures array for (auto&& itr : signatures) { auto sigUtf8 = WideToUTF8(itr); if (!sigUtf8) { continue; } aJson.StringElement(mozilla::MakeStringSpan(sigUtf8.get())); } aJson.EndArray(); return true; } namespace { struct PingThreadContext { explicit PingThreadContext(const mozilla::LauncherError& aError, const char* aProcessType) : mLauncherError(aError), mModulesSnapshot(::CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, 0)), mProcessType(aProcessType ? aProcessType : "") {} mozilla::LauncherError mLauncherError; nsAutoHandle mModulesSnapshot; std::string mProcessType; }; } // anonymous namespace static bool PrepPing(const PingThreadContext& aContext, const std::wstring& aId, mozilla::JSONWriter& aJson) { # if defined(DEBUG) const mozilla::JSONWriter::CollectionStyle style = mozilla::JSONWriter::MultiLineStyle; # else const mozilla::JSONWriter::CollectionStyle style = mozilla::JSONWriter::SingleLineStyle; # endif // defined(DEBUG) aJson.Start(style); aJson.StringProperty("type", "launcher-process-failure"); aJson.IntProperty("version", 1); auto idUtf8 = WideToUTF8(aId); if (idUtf8) { aJson.StringProperty("id", mozilla::MakeStringSpan(idUtf8.get())); } time_t now; time(&now); tm gmTm; if (!gmtime_s(&gmTm, &now)) { char isoTimeBuf[32] = {}; if (strftime(isoTimeBuf, mozilla::ArrayLength(isoTimeBuf), "%FT%T.000Z", &gmTm)) { aJson.StringProperty("creationDate", isoTimeBuf); } } aJson.StringProperty("update_channel", QUOTE_ME(MOZ_UPDATE_CHANNEL)); if (gAppData) { aJson.StringProperty("build_id", mozilla::MakeStringSpan(gAppData->buildID)); aJson.StringProperty("build_version", mozilla::MakeStringSpan(gAppData->version)); } OSVERSIONINFOEXW osv = {sizeof(osv)}; if (::GetVersionExW(reinterpret_cast(&osv))) { std::ostringstream oss; oss << osv.dwMajorVersion << "." << osv.dwMinorVersion << "." << osv.dwBuildNumber; if (osv.dwMajorVersion == 10 && osv.dwMinorVersion == 0) { // Get the "Update Build Revision" (UBR) value DWORD ubrValue; DWORD ubrValueLen = sizeof(ubrValue); LSTATUS ubrOk = ::RegGetValueW(HKEY_LOCAL_MACHINE, L"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion", L"UBR", RRF_RT_DWORD | RRF_SUBKEY_WOW6464KEY, nullptr, &ubrValue, &ubrValueLen); if (ubrOk == ERROR_SUCCESS) { oss << "." << ubrValue; } } if (oss) { aJson.StringProperty("os_version", oss.str()); } bool isServer = osv.wProductType == VER_NT_DOMAIN_CONTROLLER || osv.wProductType == VER_NT_SERVER; aJson.BoolProperty("server_os", isServer); } WCHAR localeName[LOCALE_NAME_MAX_LENGTH] = {}; int localeNameLen = ::GetUserDefaultLocaleName(localeName, mozilla::ArrayLength(localeName)); if (localeNameLen) { auto localeNameUtf8 = WideToUTF8(localeName, localeNameLen - 1); if (localeNameUtf8) { aJson.StringProperty("os_locale", mozilla::MakeStringSpan(localeNameUtf8.get())); } } SYSTEM_INFO sysInfo; ::GetNativeSystemInfo(&sysInfo); aJson.IntProperty("cpu_arch", sysInfo.wProcessorArchitecture); aJson.IntProperty("num_logical_cpus", sysInfo.dwNumberOfProcessors); mozilla::LauncherResult isAdminWithoutUac = mozilla::IsAdminWithoutUac(); if (isAdminWithoutUac.isOk()) { aJson.BoolProperty("is_admin_without_uac", isAdminWithoutUac.unwrap()); } if (!aContext.mProcessType.empty()) { aJson.StringProperty("process_type", aContext.mProcessType); } MEMORYSTATUSEX memStatus = {sizeof(memStatus)}; if (::GlobalMemoryStatusEx(&memStatus)) { aJson.StartObjectProperty("memory"); aJson.IntProperty("total_phys", memStatus.ullTotalPhys); aJson.IntProperty("avail_phys", memStatus.ullAvailPhys); aJson.IntProperty("avail_page_file", memStatus.ullAvailPageFile); aJson.IntProperty("avail_virt", memStatus.ullAvailVirtual); aJson.EndObject(); } aJson.StringProperty("xpcom_abi", TARGET_XPCOM_ABI); aJson.StartObjectProperty("launcher_error", style); std::string srcFileLeaf(aContext.mLauncherError.mFile); // Obtain the leaf name of the file for privacy reasons // (In case this is somebody's local build) auto pos = srcFileLeaf.find_last_of("/\\"); if (pos != std::string::npos) { srcFileLeaf = srcFileLeaf.substr(pos + 1); } aJson.StringProperty("source_file", srcFileLeaf); aJson.IntProperty("source_line", aContext.mLauncherError.mLine); aJson.IntProperty("hresult", aContext.mLauncherError.mError.AsHResult()); # if defined(NIGHTLY_BUILD) if (aContext.mLauncherError.mDetourError.isSome()) { static const char* kHexMap = "0123456789abcdef"; char hexStr[sizeof(mozilla::DetourError::mOrigBytes) * 2 + 1]; int cnt = 0; for (uint8_t byte : aContext.mLauncherError.mDetourError->mOrigBytes) { hexStr[cnt++] = kHexMap[(byte >> 4) & 0x0f]; hexStr[cnt++] = kHexMap[byte & 0x0f]; } hexStr[cnt] = 0; aJson.StringProperty("detour_orig_bytes", hexStr); } # endif // defined(NIGHTLY_BUILD) aJson.EndObject(); # if !defined(__MINGW32__) if (!AddWscInfo(aJson)) { return false; } # endif // !defined(__MINGW32__) if (!AddModuleInfo(aContext.mModulesSnapshot, aJson)) { return false; } aJson.End(); return true; } static bool DoSendPing(const PingThreadContext& aContext) { TempFileWriter tempFile; mozilla::JSONWriter json(tempFile); UUID uuid; if (::UuidCreate(&uuid) != RPC_S_OK) { return false; } wchar_t guidBuf[kGuidCharLenWithNul] = {}; if (::StringFromGUID2(uuid, guidBuf, kGuidCharLenWithNul) != kGuidCharLenWithNul) { return false; } // Strip the curly braces off of the guid std::wstring guidNoBraces(guidBuf + 1, kGuidCharLenNoBracesNoNul); // Populate json with the ping information if (!PrepPing(aContext, guidNoBraces, json)) { return false; } // Obtain the name of the temp file that we have written const std::wstring& fileName = tempFile.GetFileName(); // Using the path to our executable binary, construct the path to // pingsender.exe mozilla::UniquePtr exePath(mozilla::GetFullBinaryPath()); wchar_t drive[_MAX_DRIVE] = {}; wchar_t dir[_MAX_DIR] = {}; if (_wsplitpath_s(exePath.get(), drive, mozilla::ArrayLength(drive), dir, mozilla::ArrayLength(dir), nullptr, 0, nullptr, 0)) { return false; } wchar_t pingSenderPath[MAX_PATH + 1] = {}; if (_wmakepath_s(pingSenderPath, mozilla::ArrayLength(pingSenderPath), drive, dir, L"pingsender", L"exe")) { return false; } // Construct the telemetry URL wchar_t urlBuf[mozilla::ArrayLength(kUrl) + kGuidCharLenNoBracesNoNul] = {}; if (wcscpy_s(urlBuf, kUrl)) { return false; } if (wcscat_s(urlBuf, guidNoBraces.c_str())) { return false; } // Now build the command line arguments to pingsender wchar_t* pingSenderArgv[] = {pingSenderPath, urlBuf, const_cast(fileName.c_str())}; mozilla::UniquePtr pingSenderCmdLine(mozilla::MakeCommandLine( mozilla::ArrayLength(pingSenderArgv), pingSenderArgv)); // Now start pingsender to handle the rest PROCESS_INFORMATION pi; STARTUPINFOW si = {sizeof(si)}; si.dwFlags = STARTF_USESHOWWINDOW; si.wShowWindow = SW_HIDE; if (!::CreateProcessW(pingSenderPath, pingSenderCmdLine.get(), nullptr, nullptr, FALSE, 0, nullptr, nullptr, &si, &pi)) { return false; } tempFile.SetSuccessfulHandoff(); nsAutoHandle proc(pi.hProcess); nsAutoHandle thread(pi.hThread); return true; } static unsigned __stdcall SendPingThread(void* aContext) { mozilla::UniquePtr context( reinterpret_cast(aContext)); if (!DoSendPing(*context) || gForceEventLog) { PostErrorToLog(context->mLauncherError); } return 0; } #endif // defined(MOZ_TELEMETRY_REPORTING) static bool SendPing(const mozilla::LauncherError& aError, const char* aProcessType) { #if defined(MOZ_TELEMETRY_REPORTING) # if defined(MOZ_LAUNCHER_PROCESS) mozilla::LauncherRegistryInfo regInfo; mozilla::LauncherResult telemetryEnabled = regInfo.IsTelemetryEnabled(); if (telemetryEnabled.isErr() || !telemetryEnabled.unwrap()) { // Do not send anything if telemetry has been opted out return false; } # endif // defined(MOZ_LAUNCHER_PROCESS) // We send this ping when the launcher process fails. After we start the // SendPingThread, this thread falls back from running as the launcher process // to running as the browser main thread. Once this happens, it will be unsafe // to set up PoisonIOInterposer (since we have already spun up a background // thread). mozilla::SaveToEnv("MOZ_DISABLE_POISON_IO_INTERPOSER=1"); // Capture aError and our module list into context for processing on another // thread. auto thdParam = mozilla::MakeUnique(aError, aProcessType); // The ping does a lot of file I/O. Since we want this thread to continue // executing browser startup, we should gather that information on a // background thread. uintptr_t thdHandle = _beginthreadex(nullptr, 0, &SendPingThread, thdParam.get(), STACK_SIZE_PARAM_IS_A_RESERVATION, nullptr); if (!thdHandle) { return false; } // We have handed off thdParam to the background thread mozilla::Unused << thdParam.release(); ::CloseHandle(reinterpret_cast(thdHandle)); return true; #else return false; #endif } namespace mozilla { void HandleLauncherError(const LauncherError& aError, const char* aProcessType) { #if defined(MOZ_LAUNCHER_PROCESS) LauncherRegistryInfo regInfo; Unused << regInfo.DisableDueToFailure(); #endif // defined(MOZ_LAUNCHER_PROCESS) if (!SendPing(aError, aProcessType)) { // couldn't (or shouldn't) send telemetry; fall back to event log PostErrorToLog(aError); } } void SetLauncherErrorAppData(const StaticXREAppData& aAppData) { gAppData = &aAppData; } void SetLauncherErrorForceEventLog() { gForceEventLog = true; } } // namespace mozilla