chromium/chrome/browser/error_reporting/chrome_js_error_report_processor_chromeos.cc

// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
#pragma allow_unsafe_buffers
#endif

#include "chrome/browser/error_reporting/chrome_js_error_report_processor.h"

#include <errno.h>

#include <algorithm>

#include "base/files/file.h"
#include "base/files/file_util.h"
#include "base/files/scoped_file.h"
#include "base/functional/callback_helpers.h"
#include "base/logging.h"
#include "base/posix/eintr_wrapper.h"
#include "base/process/launch.h"
#include "base/process/process.h"
#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/task/bind_post_task.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "chrome/browser/error_reporting/constants.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"

// Per the memfd_create man page, we need _GNU_SOURCE
#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif
#include <sys/mman.h>

namespace {

// The format used to communicate with crash_reporter keys treats ':' as special
// (introducing the length of the value). Remove any :'s from the key names in
// place.
void ReplaceColonsWithUnderscores(std::string& key) {
  std::replace(key.begin(), key.end(), ':', '_');
}

// Gets a File pointing to some temporary location. In some cases, we have to
// do extra cleanup; pass an *unbound* ScopedClosureRunner in |cleanup| so that
// this function can add the necessary cleanup function.
// If |force_non_memfd_for_test| is true, we act as if the memfd call failed and
// go to the temp file case. Since most machines have memfd_create implemented,
// this is the only way to get some unit-test coverage on the non-memfd_create
// path.
base::File GetMemfdOrTempFile(base::ScopedClosureRunner& cleanup,
                              bool force_non_memfd_for_test) {
  DCHECK(!cleanup) << "cleanup must be unbound";
  if (!force_non_memfd_for_test) {
    int memfd = HANDLE_EINTR(memfd_create("javascript_error", 0));
    if (memfd != -1) {
      return base::File(memfd);
    }

    if (errno != ENOSYS) {
      PLOG(ERROR)
          << "Could not create memfd file for JavaScript error reporting";
      return base::File(base::File::FILE_ERROR_FAILED);
    }
  }

  // Note that some VMs and boards with old kernels don't have memfd_create
  // implemented yet. Work around by creating a temp file.
  base::FilePath output_path;
  base::ScopedFILE output_file = CreateAndOpenTemporaryStream(&output_path);
  if (!output_file) {
    PLOG(ERROR)
        << "memfd_create not implemented and cannot create temporary stream";
    return base::File(base::File::FILE_ERROR_FAILED);
  }

  DLOG(WARNING) << "JavaScript error reporting: Falling back to temp file "
                << output_path.value();

  // Need to actually delete the temp file once we're done.
  cleanup.ReplaceClosure(base::GetDeleteFileCallback(std::move(output_path)));
  return base::FILEToFile(output_file.release());
}

}  // namespace

// Called after giving crash_reporter time to finish. If crash_reporter is
// taking too long, kills it. Either way, triggers the callbacks once
// crash_reporter is done.
void ChromeJsErrorReportProcessor::WaitForCrashReporter(
    base::Process process,
    base::Time process_creation_time,
    base::ScopedClosureRunner file_cleanup,
    base::ScopedClosureRunner external_callback_runner) {
  int return_code = 0;
  bool process_done =
      process.WaitForExitWithTimeout(base::Seconds(0), &return_code);

  if (process_done) {
    if (return_code != 0) {
      LOG(WARNING) << "crash_reporter subprocess failed with return value "
                   << return_code
                   << (return_code == -1 ? " or maybe crashed" : "");
    }
    return;
  }

  // Kill the stuck process to avoid zombies.
  LOG(WARNING) << "crash_reporter failed to complete within "
               << maximium_wait_for_crash_reporter_;
  process.Terminate(0, false /*wait*/);
}

std::vector<std::string>
ChromeJsErrorReportProcessor::GetCrashReporterArgvStart() {
  return {"/sbin/crash_reporter"};
}

std::string ChromeJsErrorReportProcessor::ParamsToCrashReporterString(
    const ParameterMap& params,
    const std::optional<std::string>& stack_trace) {
  std::string result;
  for (const auto& param : params) {
    std::string key = param.first;
    const std::string& value = param.second;
    ReplaceColonsWithUnderscores(key);
    std::string value_length_string = base::NumberToString(value.length());
    base::StrAppend(&result, {key, ":", value_length_string, ":", value});
  }
  if (stack_trace) {
    const std::string& payload = stack_trace.value();

    std::string value_length_string = base::NumberToString(payload.length());
    base::StrAppend(
        &result, {kJavaScriptStackKey, ":", value_length_string, ":", payload});
  }

  return result;
}

void ChromeJsErrorReportProcessor::SendReportViaCrashReporter(
    ParameterMap params,
    std::optional<std::string> stack_trace,
    base::ScopedClosureRunner callback_runner) {
  base::ScopedClosureRunner cleanup;
  base::File output(GetMemfdOrTempFile(cleanup, force_non_memfd_for_test_));
  if (!output.IsValid()) {
    return;  // Already logged error message in GetMemfdOrTempFile.
  }

  std::string string_to_write =
      ParamsToCrashReporterString(params, stack_trace);
  if (output.WriteAtCurrentPos(string_to_write.data(),
                               string_to_write.length()) !=
      static_cast<int>(string_to_write.length())) {
    PLOG(ERROR) << "Failed to write to crash_reporter pipe";
    return;
  }

  base::LaunchOptions crash_reporter_options;
  crash_reporter_options.fds_to_remap.emplace_back(output.GetPlatformFile(),
                                                   output.GetPlatformFile());

  std::vector<std::string> argv(GetCrashReporterArgvStart());
  argv.insert(argv.end(),
              {base::StrCat({"--chrome_memfd=",
                             base::NumberToString(output.GetPlatformFile())}),
               base::StrCat({"--pid=", base::NumberToString(getpid())}),
               base::StrCat({"--uid=", base::NumberToString(geteuid())}),
               "--error_key=jserror"});

  base::Process process = base::LaunchProcess(argv, crash_reporter_options);
  if (!process.IsValid()) {
    PLOG(ERROR) << "Failed to launch " << base::JoinString(argv, " ");
    return;
  }

  // Wait for crash_reporter to finish. We need to wait for it to finish before
  // we delete the temporary files that may have been created in
  // GetMemfdOrTempFile(). (Also, it makes the unit tests much easier.)
  // However, we can't just
  // process.WaitForExitWithTimeout(maximium_wait_for_crash_reporter_) here
  // because it causes shutdown hangs if the user tries to exit right after the
  // crash_reporter is spawned. So call a delayed task to clean up after
  // crash_reporter is finished.
  base::Time process_creation_time = process.CreationTime();
  base::ThreadPool::PostDelayedTask(
      FROM_HERE, {base::MayBlock(), base::TaskPriority::BEST_EFFORT},
      base::BindOnce(&ChromeJsErrorReportProcessor::WaitForCrashReporter, this,
                     std::move(process), process_creation_time,
                     std::move(cleanup), std::move(callback_runner)),
      maximium_wait_for_crash_reporter_);
}

void ChromeJsErrorReportProcessor::SendReport(
    ParameterMap params,
    std::optional<std::string> stack_trace,
    bool send_to_production_servers,
    base::ScopedClosureRunner callback_runner,
    base::Time report_time,
    scoped_refptr<network::SharedURLLoaderFactory> loader_factory) {
  // On Chrome OS, send the report through the OS crash reporting system to
  // get more metadata and to keep all the consent logic in one place. We need
  // to do file I/O, so over to a blockable thread for the send and then back to
  // the UI thread for the finished callback.
  base::ThreadPool::PostTask(
      FROM_HERE, {base::MayBlock()},
      base::BindOnce(
          &ChromeJsErrorReportProcessor::SendReportViaCrashReporter, this,
          std::move(params), std::move(stack_trace),
          base::ScopedClosureRunner(base::BindPostTask(
              content::GetUIThreadTaskRunner({}), callback_runner.Release()))));
}