| // Copyright 2025 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/windows_services/service_program/crash_reporting.h" |
| |
| #include <objidl.h> |
| #include <wrl/client.h> |
| |
| #include <string> |
| #include <utility> |
| |
| #include "base/base_paths.h" |
| #include "base/check.h" |
| #include "base/debug/leak_annotations.h" |
| #include "base/files/file_path.h" |
| #include "base/files/file_util.h" |
| #include "base/functional/bind.h" |
| #include "base/path_service.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/synchronization/waitable_event.h" |
| #include "base/task/sequenced_task_runner.h" |
| #include "base/version_info/channel.h" |
| #include "chrome/common/chrome_version.h" |
| #include "chrome/install_static/install_util.h" |
| #include "chrome/installer/util/google_update_constants.h" |
| #include "chrome/windows_services/service_program/is_running_unattended.h" |
| #include "chrome/windows_services/service_program/user_crash_state.h" |
| #include "components/crash/core/app/crash_reporter_client.h" |
| #include "components/crash/core/app/crashpad.h" |
| #include "components/crash/core/common/crash_key.h" |
| |
| namespace { |
| |
| // Returns the directory holding per-user subdirectories for the crashpad |
| // databases, or an empty path in case of error. `directory_name` is the name of |
| // the directory within C:\Windows\SystemTemp that will be created to house a |
| // "Crashpad" directory. Each process should provide a distinct value (e.g., |
| // "ChromiumTracing" for the elevated tracing service in a Chromium build). |
| base::FilePath GetCrashpadDir(base::FilePath::StringViewType directory_name) { |
| base::FilePath system_temp; |
| if (!base::PathService::Get(base::DIR_SYSTEM_TEMP, &system_temp)) { |
| return base::FilePath(); |
| } |
| return system_temp.Append(directory_name) |
| .Append(FILE_PATH_LITERAL("Crashpad")); |
| } |
| |
| class CrashClient : public crash_reporter::CrashReporterClient { |
| public: |
| CrashClient(base::FilePath crashpad_dir, |
| std::unique_ptr<UserCrashState> user_crash_state); |
| ~CrashClient() override; |
| |
| const UserCrashState& user_crash_state() const { return *user_crash_state_; } |
| |
| // crash_reporter::CrashReporterClient: |
| void GetProductNameAndVersion(const std::wstring& exe_path, |
| std::wstring* product_name, |
| std::wstring* version, |
| std::wstring* special_build, |
| std::wstring* channel_name) override; |
| bool GetShouldDumpLargerDumps() override; |
| bool GetCrashDumpLocation(std::wstring* crash_dir) override; |
| bool IsRunningUnattended() override; |
| bool GetCollectStatsConsent() override; |
| bool GetCollectStatsInSample() override; |
| bool ReportingIsEnforcedByPolicy(bool* reporting_enabled) override; |
| |
| private: |
| // Returns the current value of the collect stats consent from the Windows |
| // registry. |
| bool GetCurrentCollectStatsConsent(); |
| bool GetCurrentCollectStatsInSample(); |
| void OnClientStateMediumChanged(); |
| void OnClientStateChanged(); |
| void OnProductKeyChanged(); |
| void UpdateUploadConsent(); |
| |
| const base::FilePath crashpad_dir_; |
| const std::unique_ptr<UserCrashState> user_crash_state_; |
| bool last_collect_stats_consent_ = false; |
| bool last_collect_stats_in_sample_ = false; |
| }; |
| |
| CrashClient::CrashClient(base::FilePath crashpad_dir, |
| std::unique_ptr<UserCrashState> user_crash_state) |
| : crashpad_dir_(std::move(crashpad_dir)), |
| user_crash_state_(std::move(user_crash_state)) { |
| if (user_crash_state_->client_state_medium_key().Valid()) { |
| // Unretained is safe because this owns the registry key by way of |
| // UserCrashState. |
| user_crash_state_->client_state_medium_key().StartWatching(base::BindOnce( |
| &CrashClient::OnClientStateMediumChanged, base::Unretained(this))); |
| } |
| if (user_crash_state_->client_state_key().Valid()) { |
| // Unretained is safe because this owns the registry key by way of |
| // UserCrashState. |
| user_crash_state_->client_state_key().StartWatching(base::BindOnce( |
| &CrashClient::OnClientStateChanged, base::Unretained(this))); |
| } |
| if (user_crash_state_->product_key().Valid()) { |
| // Unretained is safe because this owns the registry key by way of |
| // UserCrashState. |
| user_crash_state_->product_key().StartWatching(base::BindOnce( |
| &CrashClient::OnProductKeyChanged, base::Unretained(this))); |
| } |
| } |
| |
| CrashClient::~CrashClient() = default; |
| |
| void CrashClient::GetProductNameAndVersion(const std::wstring& exe_path, |
| std::wstring* product_name, |
| std::wstring* version, |
| std::wstring* special_build, |
| std::wstring* channel_name) { |
| // Report crashes under the same product name as the browser. This string |
| // MUST match server-side configuration. |
| *product_name = base::ASCIIToWide(PRODUCT_SHORTNAME_STRING); |
| *version = base::ASCIIToWide(CHROME_VERSION_STRING); |
| special_build->clear(); |
| *channel_name = |
| install_static::GetChromeChannelName(/*with_extended_stable=*/true); |
| } |
| |
| bool CrashClient::GetShouldDumpLargerDumps() { |
| // Capture larger dumps for Google Chrome beta, dev, and canary channels, and |
| // Chromium builds. The Google Chrome stable channel uses smaller dumps. |
| return install_static::GetChromeChannel() != version_info::Channel::STABLE; |
| } |
| |
| bool CrashClient::GetCrashDumpLocation(std::wstring* crash_dir) { |
| *crash_dir = crashpad_dir_.Append(user_crash_state_->user_sid()).value(); |
| return true; |
| } |
| |
| bool CrashClient::IsRunningUnattended() { |
| return internal::IsRunningUnattended(); |
| } |
| |
| bool CrashClient::GetCollectStatsConsent() { |
| // Cache the last returned value for use when handling a change to one of the |
| // registry keys. |
| last_collect_stats_consent_ = GetCurrentCollectStatsConsent(); |
| return last_collect_stats_consent_; |
| } |
| |
| bool CrashClient::GetCollectStatsInSample() { |
| // Cache the last returned value for use when handling a change to one of the |
| // registry keys. |
| last_collect_stats_in_sample_ = GetCurrentCollectStatsInSample(); |
| return last_collect_stats_in_sample_; |
| } |
| |
| bool CrashClient::ReportingIsEnforcedByPolicy(bool* reporting_enabled) { |
| return install_static::ReportingIsEnforcedByPolicy(reporting_enabled); |
| } |
| |
| bool CrashClient::GetCurrentCollectStatsConsent() { |
| DWORD value = 0; |
| |
| // TODO(crbug.com/40837274): The logic here is based on Chrome's behavior of |
| // conveying consent to Omaha via the value in HKLM\...\ClientStateMedium. |
| // This must be updated if Chrome and Omaha switch to using a value in HKCU. |
| |
| // First check for a value in ClientStateMedium. A value here for a |
| // per-machine install takes precedent over one in ClientState. |
| if (user_crash_state_->client_state_medium_key().ReadValueDW( |
| google_update::kRegUsageStatsField, &value) == ERROR_SUCCESS) { |
| return value == google_update::TRISTATE_TRUE; |
| } |
| |
| // Failing that, check for one in ClientState. |
| if (user_crash_state_->client_state_key().ReadValueDW( |
| google_update::kRegUsageStatsField, &value) == ERROR_SUCCESS) { |
| return value == google_update::TRISTATE_TRUE; |
| } |
| |
| // No value anywhere means no consent. |
| return false; |
| } |
| |
| bool CrashClient::GetCurrentCollectStatsInSample() { |
| DWORD value = 0; |
| |
| // Failure to read a value (e.g., because the value is not present) is |
| // considered "in the sample". |
| return user_crash_state_->product_key().ReadValueDW( |
| install_static::kRegValueChromeStatsSample, &value) != |
| ERROR_SUCCESS || |
| value == 1; |
| } |
| |
| void CrashClient::OnClientStateMediumChanged() { |
| // Unretained is safe because this owns the registry key by way of |
| // UserCrashState. |
| user_crash_state_->client_state_medium_key().StartWatching(base::BindOnce( |
| &CrashClient::OnClientStateMediumChanged, base::Unretained(this))); |
| |
| UpdateUploadConsent(); |
| } |
| |
| void CrashClient::OnClientStateChanged() { |
| // Unretained is safe because this owns the registry key by way of |
| // UserCrashState. |
| user_crash_state_->client_state_key().StartWatching(base::BindOnce( |
| &CrashClient::OnClientStateChanged, base::Unretained(this))); |
| |
| UpdateUploadConsent(); |
| } |
| |
| void CrashClient::OnProductKeyChanged() { |
| // Unretained is safe because this owns the registry key by way of |
| // UserCrashState. |
| user_crash_state_->product_key().StartWatching(base::BindOnce( |
| &CrashClient::OnProductKeyChanged, base::Unretained(this))); |
| |
| UpdateUploadConsent(); |
| } |
| |
| void CrashClient::UpdateUploadConsent() { |
| bool collect_stats_consent = GetCurrentCollectStatsConsent(); |
| bool collect_stats_in_sample = GetCurrentCollectStatsInSample(); |
| if (collect_stats_consent != last_collect_stats_consent_ || |
| collect_stats_in_sample != last_collect_stats_in_sample_) { |
| last_collect_stats_consent_ = collect_stats_consent; |
| crash_reporter::SetUploadConsent(collect_stats_consent); |
| } |
| } |
| |
| void StartCrashHandlerImpl(std::unique_ptr<UserCrashState> user_crash_state, |
| base::FilePath::StringViewType directory_name, |
| std::string_view process_type) { |
| // The child process requires that the directory holding the client's "crash |
| // dump location" already exists, so create it now before launching the child. |
| base::FilePath crashpad_dir = GetCrashpadDir(directory_name); |
| if (crashpad_dir.empty() || !base::CreateDirectory(crashpad_dir)) { |
| return; |
| } |
| |
| // Only one crash handler may be created for the lifetime of a process. |
| // Allow multiple calls to `StartCrashHandlerImpl`, but ensure that they are |
| // all on behalf of the same user. |
| static auto* const client = |
| new CrashClient(std::move(crashpad_dir), std::move(user_crash_state)); |
| ANNOTATE_LEAKING_OBJECT_PTR(client); |
| |
| // On the first call, ownership of `user_crash_state` will have been passed to |
| // the client. |
| if (user_crash_state) { |
| // This is not the first call. Crash if an attempt is being made to start a |
| // handler for a different user. |
| CHECK(client->user_crash_state().user_sid() == |
| user_crash_state->user_sid()); |
| // This is for the same user as the previous launch, so there is nothing |
| // more to be done. |
| return; |
| } |
| |
| // Disable COM exception handling as per |
| // https://learn.microsoft.com/en-us/windows/win32/api/objidl/nn-objidl-iglobaloptions. |
| if (Microsoft::WRL::ComPtr<IGlobalOptions> options; SUCCEEDED( |
| ::CoCreateInstance(CLSID_GlobalOptions, nullptr, CLSCTX_INPROC_SERVER, |
| IID_PPV_ARGS(&options)))) { |
| options->Set(COMGLB_EXCEPTION_HANDLING, COMGLB_EXCEPTION_DONOT_HANDLE); |
| } |
| |
| crash_reporter::SetCrashReporterClient(client); |
| crash_reporter::InitializeCrashKeys(); |
| |
| crash_reporter::InitializeCrashpadWithEmbeddedHandler( |
| /*initial_client=*/true, std::string(process_type), |
| /*user_data_dir=*/std::string(), |
| /*exe_path=*/base::FilePath()); |
| } |
| |
| } // namespace |
| |
| namespace windows_services { |
| |
| void StartCrashHandler(std::unique_ptr<UserCrashState> user_crash_state, |
| base::FilePath::StringViewType directory_name, |
| std::string_view process_type, |
| scoped_refptr<base::SequencedTaskRunner> task_runner) { |
| if (task_runner) { |
| base::WaitableEvent event; |
| task_runner->PostTask( |
| FROM_HERE, |
| base::BindOnce( |
| [](std::unique_ptr<UserCrashState> user_crash_state, |
| base::FilePath::StringViewType directory_name, |
| std::string_view process_type, base::WaitableEvent& event) { |
| StartCrashHandlerImpl(std::move(user_crash_state), directory_name, |
| process_type); |
| event.Signal(); |
| }, |
| std::move(user_crash_state), directory_name, process_type, |
| std::ref(event))); |
| event.Wait(); |
| return; |
| } |
| StartCrashHandlerImpl(std::move(user_crash_state), directory_name, |
| process_type); |
| } |
| |
| } // namespace windows_services |