diff --git a/cef_paths2.gypi b/cef_paths2.gypi index 83fe00690..8e5b8f89f 100644 --- a/cef_paths2.gypi +++ b/cef_paths2.gypi @@ -17,6 +17,7 @@ 'include/base/cef_cancelable_callback.h', 'include/base/cef_compiler_specific.h', 'include/base/cef_dump_without_crashing.h', + 'include/base/cef_immediate_crash.h', 'include/base/cef_lock.h', 'include/base/cef_logging.h', 'include/base/cef_macros.h', diff --git a/include/base/cef_immediate_crash.h b/include/base/cef_immediate_crash.h new file mode 100644 index 000000000..eb78eeb89 --- /dev/null +++ b/include/base/cef_immediate_crash.h @@ -0,0 +1,197 @@ +// Copyright (c) 2025 Marshall A. Greenblatt. Portions copyright (c) 2019 +// Google Inc. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * 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. +// * Neither the name of Google Inc. nor the name Chromium Embedded +// Framework nor the names of its contributors may be used to endorse +// or promote products derived from this software without specific prior +// written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "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 COPYRIGHT +// OWNER OR CONTRIBUTORS 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. + +#ifndef CEF_INCLUDE_BASE_CEF_IMMEDIATE_CRASH_H_ +#define CEF_INCLUDE_BASE_CEF_IMMEDIATE_CRASH_H_ +#pragma once + +#if defined(USING_CHROMIUM_INCLUDES) +// When building CEF include the Chromium header directly. +#include "base/immediate_crash.h" +#else // !USING_CHROMIUM_INCLUDES +// The following is substantially similar to the Chromium implementation. +// If the Chromium implementation diverges the below implementation should be +// updated to match. + +#include "include/base/cef_build.h" + +#if defined(OS_WIN) +#include +#endif + +// Crashes in the fastest possible way with no attempt at logging. +// There are several constraints; see http://crbug.com/664209 for more context. +// +// - TRAP_SEQUENCE_() must be fatal. It should not be possible to ignore the +// resulting exception or simply hit 'continue' to skip over it in a debugger. +// - Different instances of TRAP_SEQUENCE_() must not be folded together, to +// ensure crash reports are debuggable. Unlike __builtin_trap(), asm volatile +// blocks will not be folded together. +// Note: TRAP_SEQUENCE_() previously required an instruction with a unique +// nonce since unlike clang, GCC folds together identical asm volatile +// blocks. +// - TRAP_SEQUENCE_() must produce a signal that is distinct from an invalid +// memory access. +// - TRAP_SEQUENCE_() must be treated as a set of noreturn instructions. +// __builtin_unreachable() is used to provide that hint here. clang also uses +// this as a heuristic to pack the instructions in the function epilogue to +// improve code density. +// - base::ImmediateCrash() is used in allocation hooks. To prevent recursions, +// TRAP_SEQUENCE_() must not allocate. +// +// Additional properties that are nice to have: +// - TRAP_SEQUENCE_() should be as compact as possible. +// - The first instruction of TRAP_SEQUENCE_() should not change, to avoid +// shifting crash reporting clusters. As a consequence of this, explicit +// assembly is preferred over intrinsics. +// Note: this last bullet point may no longer be true, and may be removed in +// the future. + +// Note: TRAP_SEQUENCE Is currently split into two macro helpers due to the fact +// that clang emits an actual instruction for __builtin_unreachable() on certain +// platforms (see https://crbug.com/958675). In addition, the int3/bkpt/brk will +// be removed in followups, so splitting it up like this now makes it easy to +// land the followups. + +#if defined(COMPILER_GCC) + +#if defined(ARCH_CPU_X86_FAMILY) + +// TODO(crbug.com/40625592): In theory, it should be possible to use just +// int3. However, there are a number of crashes with SIGILL as the exception +// code, so it seems likely that there's a signal handler that allows execution +// to continue after SIGTRAP. +#define TRAP_SEQUENCE1_() asm volatile("int3") + +#if defined(OS_APPLE) +// Intentionally empty: __builtin_unreachable() is always part of the sequence +// (see IMMEDIATE_CRASH below) and already emits a ud2 on Mac. +#define TRAP_SEQUENCE2_() asm volatile("") +#else +#define TRAP_SEQUENCE2_() asm volatile("ud2") +#endif // defined(OS_APPLE) + +#elif defined(ARCH_CPU_ARMEL) + +// bkpt will generate a SIGBUS when running on armv7 and a SIGTRAP when running +// as a 32 bit userspace app on arm64. There doesn't seem to be any way to +// cause a SIGTRAP from userspace without using a syscall (which would be a +// problem for sandboxing). +// TODO(crbug.com/40625592): Remove bkpt from this sequence. +#define TRAP_SEQUENCE1_() asm volatile("bkpt #0") +#define TRAP_SEQUENCE2_() asm volatile("udf #0") + +#elif defined(ARCH_CPU_ARM64) + +// This will always generate a SIGTRAP on arm64. +// TODO(crbug.com/40625592): Remove brk from this sequence. +#define TRAP_SEQUENCE1_() asm volatile("brk #0") +#define TRAP_SEQUENCE2_() asm volatile("hlt #0") + +#else + +// Crash report accuracy will not be guaranteed on other architectures, but at +// least this will crash as expected. +#define TRAP_SEQUENCE1_() __builtin_trap() +#define TRAP_SEQUENCE2_() asm volatile("") + +#endif // ARCH_CPU_* + +#elif defined(COMPILER_MSVC) + +#if !defined(__clang__) + +// MSVC x64 doesn't support inline asm, so use the MSVC intrinsic. +#define TRAP_SEQUENCE1_() __debugbreak() +#define TRAP_SEQUENCE2_() + +#elif defined(ARCH_CPU_ARM64) + +// Windows ARM64 uses "BRK #F000" as its breakpoint instruction, and +// __debugbreak() generates that in both VC++ and clang. +#define TRAP_SEQUENCE1_() __debugbreak() +// Intentionally empty: __builtin_unreachable() is always part of the sequence +// (see IMMEDIATE_CRASH below) and already emits a ud2 on Win64, +// https://crbug.com/958373 +#define TRAP_SEQUENCE2_() __asm volatile("") + +#else + +#define TRAP_SEQUENCE1_() asm volatile("int3") +#define TRAP_SEQUENCE2_() asm volatile("ud2") + +#endif // __clang__ + +#else + +#error No supported trap sequence! + +#endif // COMPILER_GCC + +#define TRAP_SEQUENCE_() \ + do { \ + TRAP_SEQUENCE1_(); \ + TRAP_SEQUENCE2_(); \ + } while (false) + +// This version of ALWAYS_INLINE inlines even in is_debug=true. +// TODO(pbos): See if NDEBUG can be dropped from ALWAYS_INLINE as well, and if +// so merge. Otherwise document why it cannot inline in debug in +// base/compiler_specific.h. +#if defined(COMPILER_GCC) +#define IMMEDIATE_CRASH_ALWAYS_INLINE inline __attribute__((__always_inline__)) +#elif defined(COMPILER_MSVC) +#define IMMEDIATE_CRASH_ALWAYS_INLINE __forceinline +#else +#define IMMEDIATE_CRASH_ALWAYS_INLINE inline +#endif + +namespace base { + +[[noreturn]] IMMEDIATE_CRASH_ALWAYS_INLINE void ImmediateCrash() { +#if defined(OS_WIN) + // We can't use abort() on Windows because it results in the + // abort/retry/ignore dialog which disrupts automated tests. + // TODO(crbug.com/40948553): investigate if such dialogs can + // be suppressed + TRAP_SEQUENCE_(); +#if defined(__clang__) || defined(COMPILER_GCC) + __builtin_unreachable(); +#endif // defined(__clang__) || defined(COMPILER_GCC) +#else // !defined(OS_WIN) + abort(); +#endif // !defined(OS_WIN) +} + +} // namespace base + +#endif // !USING_CHROMIUM_INCLUDES + +#endif // CEF_INCLUDE_BASE_CEF_LOCK_H_ diff --git a/include/base/cef_logging.h b/include/base/cef_logging.h index 7414738fb..de821eb19 100644 --- a/include/base/cef_logging.h +++ b/include/base/cef_logging.h @@ -189,16 +189,50 @@ namespace cef { namespace logging { +class ScopedEarlySupport; + +namespace internal { + +// Structure defining the baseline logging implementation used by client +// and wrapper code that links libcef_dll_wrapper. +struct Implementation { + decltype(&cef_get_min_log_level) get_min_log_level; + decltype(&cef_get_vlog_level) get_vlog_level; + decltype(&cef_log) log; +}; + +// Returns the currently configured logging implementation. +const Implementation* GetImplementation(); + +// Change the logging implementation for the lifespan of this scoped object. +// See ScopedEarlySupport for usage. +class ScopedImplementation { + public: + ScopedImplementation(const ScopedImplementation&) = delete; + ScopedImplementation& operator=(const ScopedImplementation&) = delete; + + private: + friend class logging::ScopedEarlySupport; + + ScopedImplementation(); + ~ScopedImplementation(); + void Init(const Implementation* impl); + + const Implementation* previous_ = nullptr; +}; + +} // namespace internal + // Gets the current log level. inline int GetMinLogLevel() { - return cef_get_min_log_level(); + return internal::GetImplementation()->get_min_log_level(); } // Gets the current vlog level for the given file (usually taken from // __FILE__). Note that |N| is the size *with* the null terminator. template int GetVlogLevel(const char (&file)[N]) { - return cef_get_vlog_level(file, N); + return internal::GetImplementation()->get_vlog_level(file, N); } typedef int LogSeverity; @@ -218,6 +252,64 @@ const LogSeverity LOG_DFATAL = LOG_ERROR; const LogSeverity LOG_DFATAL = LOG_FATAL; #endif +/// +/// Support the use of CEF logging macros during early application startup, +/// prior to loading libcef. Not for use during or after CEF initialization. +/// Support is scoped to this object's lifespan. This implementation is not +/// thread-safe and should not be used for logging from multiple threads. +/// +class ScopedEarlySupport final : public internal::ScopedImplementation { + public: + /// + /// Logging configuration. + /// + struct Config { + /// + /// Configure logging level. + /// + int min_log_level = LOG_ERROR; + int vlog_level = 0; + + /// + /// Configure log line formatting. + /// + const char* log_prefix = nullptr; + bool log_process_id = true; + bool log_thread_id = true; + bool log_timestamp = true; + bool log_tickcount = true; + + /// + /// Optionally override the default handling of formatted log lines. For + /// example, this callback could be used to write |log_line| to a file. + /// Return false to proceed with the default behavior of writing to stderr + /// or debugger console. FATAL errors will still intentionally crash the + /// application. + /// + bool (*formatted_log_handler)(const char* /*log_line*/) = nullptr; + }; + + explicit ScopedEarlySupport(const Config& config); + + ScopedEarlySupport(const ScopedEarlySupport&) = delete; + ScopedEarlySupport& operator=(const ScopedEarlySupport&) = delete; + + private: + static const Config& GetConfig(); + + static int get_min_log_level(); + static int get_vlog_level(const char* file_start, size_t N); + static void log(const char* file, + int line, + int severity, + const char* message); + + const struct Impl { + internal::Implementation ptrs; + Config config; + } impl_; +}; + // A few definitions of macros that don't generate much code. These are used // by LOG() and LOG_IF, etc. Since these are used all over our code, it's // better to have compact code for these operations. diff --git a/libcef_dll/base/cef_logging.cc b/libcef_dll/base/cef_logging.cc index 8368386ad..034b50644 100644 --- a/libcef_dll/base/cef_logging.cc +++ b/libcef_dll/base/cef_logging.cc @@ -9,13 +9,26 @@ #include #include -#include #elif defined(OS_POSIX) #include +#include +#include #include #include +#include +#include +#include #endif +#if defined(OS_APPLE) +#include +#endif + +#include +#include +#include + +#include "include/base/cef_immediate_crash.h" #include "include/internal/cef_string_types.h" namespace cef { @@ -130,8 +143,254 @@ std::string safe_strerror(int err) { } #endif // defined(OS_POSIX) +const internal::Implementation* g_impl_override = nullptr; + +const char* const log_severity_names[] = {"INFO", "WARNING", "ERROR", "FATAL"}; +static_assert(LOG_NUM_SEVERITIES == std::size(log_severity_names), + "Incorrect number of log_severity_names"); + +const char* log_severity_name(int severity) { + if (severity >= 0 && severity < LOG_NUM_SEVERITIES) { + return log_severity_names[severity]; + } + return "UNKNOWN"; +} + +#if !defined(NDEBUG) && defined(OS_WIN) +bool IsUser32AndGdi32Available() { + static const bool is_user32_and_gdi32_available = [] { + // If win32k syscalls aren't disabled, then user32 and gdi32 are available. + PROCESS_MITIGATION_SYSTEM_CALL_DISABLE_POLICY policy = {}; + if (::GetProcessMitigationPolicy(GetCurrentProcess(), + ProcessSystemCallDisablePolicy, &policy, + sizeof(policy))) { + return policy.DisallowWin32kSystemCalls == 0; + } + + return true; + }(); + return is_user32_and_gdi32_available; +} + +std::wstring UTF8ToWide(const std::string& utf8) { + if (utf8.empty()) { + return {}; + } + int size = MultiByteToWideChar(CP_UTF8, 0, utf8.data(), + static_cast(utf8.size()), nullptr, 0); + if (size <= 0) { + return {}; + } + std::wstring utf16(size, L'\0'); + if (MultiByteToWideChar(CP_UTF8, 0, utf8.data(), + static_cast(utf8.size()), &utf16[0], + size) != size) { + return {}; + } + return utf16; +} + +// Displays a message box to the user with the error message in it. Used for +// fatal messages, where we close the app simultaneously. This is for developers +// only; we don't use this in circumstances (like release builds) where users +// could see it, since users don't understand these messages anyway. +void DisplayDebugMessageInDialog(const std::string& message) { + if (IsUser32AndGdi32Available()) { + MessageBoxW(nullptr, UTF8ToWide(message).c_str(), L"Fatal error", + MB_OK | MB_ICONHAND | MB_TOPMOST); + } else { + OutputDebugStringW(UTF8ToWide(message).c_str()); + } +} +#endif // !defined(NDEBUG) && defined(OS_WIN) + +[[noreturn]] void HandleFatal(const std::string& message) { + // Don't display assertions to the user in release mode. The enduser can't do + // anything with this information, and displaying message boxes when the + // application is hosed can cause additional problems. We intentionally don't + // implement a dialog on other platforms. You can just look at stderr. +#if !defined(NDEBUG) && defined(OS_WIN) + if (!::IsDebuggerPresent()) { + // Displaying a dialog is unnecessary when debugging and can complicate + // debugging. + DisplayDebugMessageInDialog(message); + } +#endif + + // Crash the process to generate a dump. + base::ImmediateCrash(); +} + +uint64_t TickCount() { +#if defined(OS_WIN) + return ::GetTickCount(); +#elif defined(OS_APPLE) + return mach_absolute_time(); +#elif defined(OS_POSIX) + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + + uint64_t absolute_micro = static_cast(ts.tv_sec) * 1000000 + + static_cast(ts.tv_nsec) / 1000; + + return absolute_micro; +#else +#error Unsupported platform +#endif +} + } // namespace +namespace internal { + +const Implementation* GetImplementation() { + if (g_impl_override) { + return g_impl_override; + } + static constexpr Implementation default_impl = { + &cef_get_min_log_level, &cef_get_vlog_level, &cef_log}; + return &default_impl; +} + +ScopedImplementation::~ScopedImplementation() { + g_impl_override = previous_; +} + +ScopedImplementation::ScopedImplementation() = default; + +void ScopedImplementation::Init(const Implementation* impl) { + previous_ = g_impl_override; + g_impl_override = impl; +} + +} // namespace internal + +ScopedEarlySupport::ScopedEarlySupport(const Config& config) + : impl_{{&ScopedEarlySupport::get_min_log_level, + &ScopedEarlySupport::get_vlog_level, &ScopedEarlySupport::log}, + config} { + Init(&impl_.ptrs); +} + +// static +const ScopedEarlySupport::Config& ScopedEarlySupport::GetConfig() { + return reinterpret_cast(g_impl_override) + ->config; +} + +// static +int ScopedEarlySupport::get_min_log_level() { + return GetConfig().min_log_level; +} + +// static +int ScopedEarlySupport::get_vlog_level(const char* file_start, size_t N) { + return GetConfig().vlog_level; +} + +// static +void ScopedEarlySupport::log(const char* file, + int line, + int severity, + const char* message) { + const Config& config = GetConfig(); + + // Most logging initializes `file` from __FILE__. Unfortunately, because we + // build from out/Foo we get a `../../` (or \) prefix for all of our + // __FILE__s. This isn't true for base::Location::Current() which already does + // the stripping (and is used for some logging, especially CHECKs). + // + // Here we strip the first 6 (../../ or ..\..\) characters if `file` starts + // with `.` but defensively clamp to strlen(file) just in case. + const std::string_view filename = + file[0] == '.' ? std::string_view(file).substr( + std::min(std::size_t{6}, strlen(file))) + : file; + + std::stringstream stream; + + stream << '['; + if (config.log_prefix) { + stream << config.log_prefix << ':'; + } + if (config.log_process_id) { +#if defined(OS_WIN) + stream << ::GetCurrentProcessId() << ':'; +#elif defined(OS_POSIX) + stream << getpid() << ':'; +#else +#error Unsupported platform +#endif + } + if (config.log_thread_id) { +#if defined(OS_WIN) + stream << ::GetCurrentThreadId() << ':'; +#elif defined(OS_APPLE) + uint64_t tid; + if (pthread_threadid_np(nullptr, &tid) == 0) { + stream << tid << ':'; + } +#elif defined(OS_POSIX) + stream << pthread_self() << ':'; +#else +#error Unsupported platform +#endif + } + if (config.log_timestamp) { +#if defined(OS_WIN) + SYSTEMTIME local_time; + GetLocalTime(&local_time); + stream << std::setfill('0') << std::setw(2) << local_time.wMonth + << std::setw(2) << local_time.wDay << '/' << std::setw(2) + << local_time.wHour << std::setw(2) << local_time.wMinute + << std::setw(2) << local_time.wSecond << '.' << std::setw(3) + << local_time.wMilliseconds << ':'; +#elif defined(OS_POSIX) + timeval tv; + gettimeofday(&tv, nullptr); + time_t t = tv.tv_sec; + struct tm local_time; + localtime_r(&t, &local_time); + struct tm* tm_time = &local_time; + stream << std::setfill('0') << std::setw(2) << 1 + tm_time->tm_mon + << std::setw(2) << tm_time->tm_mday << '/' << std::setw(2) + << tm_time->tm_hour << std::setw(2) << tm_time->tm_min + << std::setw(2) << tm_time->tm_sec << '.' << std::setw(6) + << tv.tv_usec << ':'; +#else +#error Unsupported platform +#endif + } + if (config.log_tickcount) { + stream << TickCount() << ':'; + } + if (severity >= 0) { + stream << log_severity_name(severity); + } else { + stream << "VERBOSE" << -severity; + } + stream << ":" << filename << ":" << line << "] " << message; + + const std::string& log_line = stream.str(); + + if (!config.formatted_log_handler || + !config.formatted_log_handler(log_line.c_str())) { + // Log to stderr. + std::cerr << log_line << std::endl; + +#if !defined(NDEBUG) && defined(OS_WIN) + if (severity < LOG_FATAL) { + // Log to the debugger console in debug builds. + OutputDebugStringW(UTF8ToWide(log_line).c_str()); + } +#endif + } + + if (severity == LOG_FATAL) { + HandleFatal(log_line); + } +} + // MSVC doesn't like complex extern templates and DLLs. #if !defined(COMPILER_MSVC) // Explicit instantiations for commonly used comparisons. @@ -184,7 +443,8 @@ LogMessage::LogMessage(const char* file, LogMessage::~LogMessage() { std::string str_newline(stream_.str()); - cef_log(file_, line_, severity_, str_newline.c_str()); + internal::GetImplementation()->log(file_, line_, severity_, + str_newline.c_str()); } #if defined(OS_WIN)