crashpad/client/ios_handler/in_process_handler.cc
Justin Cohen 243dffb045 ios: Stop prune and upload thread when app is inactive and may suspend.
Stop the prune thread and the upload thread when moving to the
inactive/background state. This will reduce the number of 0xdead10cc
system kills from having a file lock during iOS suspend.

Wait to start the prune thread when the application is active.
Otherwise, for iOS prewarmed applications, the prune thread will
regularly start when the application is foregrounded for the first
time when the user intentionally runs the app.

It's still possible for either the prune thread or the upload thread to
have a file lock during iOS suspend, such as when a task started in the
foreground and does not complete in time for suspension. Future work
should include considering BackgroundTasks and/or NSURLSessions, which
can more safely run in the background.

Bug: crashpad: 400
Change-Id: Ic7d4687eb795fe585327f128aa84a5928141f4a9
Reviewed-on: https://chromium-review.googlesource.com/c/crashpad/crashpad/+/3517967
Reviewed-by: Robert Sesek <rsesek@chromium.org>
Commit-Queue: Justin Cohen <justincohen@chromium.org>
2022-03-23 02:53:15 +00:00

498 lines
17 KiB
C++

// Copyright 2021 The Crashpad Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "client/ios_handler/in_process_handler.h"
#include <stdio.h>
#include <sys/stat.h>
#include <algorithm>
#include "base/cxx17_backports.h"
#include "base/logging.h"
#include "client/ios_handler/in_process_intermediate_dump_handler.h"
#include "client/prune_crash_reports.h"
#include "client/settings.h"
#include "minidump/minidump_file_writer.h"
#include "util/file/directory_reader.h"
#include "util/file/filesystem.h"
#include "util/ios/raw_logging.h"
namespace {
// Creates directory at |path|.
bool CreateDirectory(const base::FilePath& path) {
if (mkdir(path.value().c_str(), 0755) == 0) {
return true;
}
if (errno != EEXIST) {
PLOG(ERROR) << "mkdir " << path.value();
return false;
}
return true;
}
// The file extension used to indicate a file is locked.
constexpr char kLockedExtension[] = ".locked";
// The seperator used to break the bundle id (e.g. com.chromium.ios) from the
// uuid in the intermediate dump file name.
constexpr char kBundleSeperator[] = "@";
// Zero-ed codes used by kMachExceptionFromNSException and
// kMachExceptionSimulated.
constexpr mach_exception_data_type_t kEmulatedMachExceptionCodes[2] = {};
} // namespace
namespace crashpad {
namespace internal {
InProcessHandler::InProcessHandler() = default;
InProcessHandler::~InProcessHandler() {
UpdatePruneAndUploadThreads(false);
}
bool InProcessHandler::Initialize(
const base::FilePath& database,
const std::string& url,
const std::map<std::string, std::string>& annotations) {
INITIALIZATION_STATE_SET_INITIALIZING(initialized_);
annotations_ = annotations;
database_ = CrashReportDatabase::Initialize(database);
if (!database_) {
return false;
}
bundle_identifier_and_seperator_ =
system_data_.BundleIdentifier() + kBundleSeperator;
if (!url.empty()) {
// TODO(scottmg): options.rate_limit should be removed when we have a
// configurable database setting to control upload limiting.
// See https://crashpad.chromium.org/bug/23.
CrashReportUploadThread::Options upload_thread_options;
upload_thread_options.rate_limit = false;
upload_thread_options.upload_gzip = true;
upload_thread_options.watch_pending_reports = true;
upload_thread_options.identify_client_via_url = true;
upload_thread_.reset(new CrashReportUploadThread(
database_.get(), url, upload_thread_options));
}
if (!CreateDirectory(database))
return false;
static constexpr char kPendingSerializediOSDump[] =
"pending-serialized-ios-dump";
base_dir_ = database.Append(kPendingSerializediOSDump);
if (!CreateDirectory(base_dir_))
return false;
bool is_app_extension = system_data_.IsExtension();
prune_thread_.reset(new PruneIntermediateDumpsAndCrashReportsThread(
database_.get(),
PruneCondition::GetDefault(),
base_dir_,
bundle_identifier_and_seperator_,
is_app_extension));
if (is_app_extension || system_data_.IsApplicationActive())
prune_thread_->Start();
if (!is_app_extension) {
system_data_.SetActiveApplicationCallback(
[this](bool active) { UpdatePruneAndUploadThreads(active); });
}
base::FilePath cached_writer_path = NewLockedFilePath();
cached_writer_ = CreateWriterWithPath(cached_writer_path);
if (!cached_writer_.get())
return false;
// Cache the locked and unlocked path here so no allocations are needed during
// any exceptions.
cached_writer_path_ = cached_writer_path.value();
cached_writer_unlocked_path_ =
cached_writer_path.RemoveFinalExtension().value();
INITIALIZATION_STATE_SET_VALID(initialized_);
return true;
}
void InProcessHandler::DumpExceptionFromSignal(siginfo_t* siginfo,
ucontext_t* context) {
INITIALIZATION_STATE_DCHECK_VALID(initialized_);
ScopedLockedWriter writer(GetCachedWriter(),
cached_writer_path_.c_str(),
cached_writer_unlocked_path_.c_str());
if (!writer.GetWriter()) {
CRASHPAD_RAW_LOG("Cannot DumpExceptionFromSignal without writer");
return;
}
ScopedReport report(writer.GetWriter(), system_data_, annotations_);
InProcessIntermediateDumpHandler::WriteExceptionFromSignal(
writer.GetWriter(), system_data_, siginfo, context);
}
void InProcessHandler::DumpExceptionFromMachException(
exception_behavior_t behavior,
thread_t thread,
exception_type_t exception,
const mach_exception_data_type_t* code,
mach_msg_type_number_t code_count,
thread_state_flavor_t flavor,
ConstThreadState old_state,
mach_msg_type_number_t old_state_count) {
INITIALIZATION_STATE_DCHECK_VALID(initialized_);
ScopedLockedWriter writer(GetCachedWriter(),
cached_writer_path_.c_str(),
cached_writer_unlocked_path_.c_str());
if (!writer.GetWriter()) {
CRASHPAD_RAW_LOG("Cannot DumpExceptionFromMachException without writer");
return;
}
if (mach_exception_callback_for_testing_) {
mach_exception_callback_for_testing_();
}
ScopedReport report(writer.GetWriter(), system_data_, annotations_);
InProcessIntermediateDumpHandler::WriteExceptionFromMachException(
writer.GetWriter(),
behavior,
thread,
exception,
code,
code_count,
flavor,
old_state,
old_state_count);
}
void InProcessHandler::DumpExceptionFromNSExceptionWithContext(
NativeCPUContext* context) {
INITIALIZATION_STATE_DCHECK_VALID(initialized_);
// This does not use the cached writer. NSExceptionWithContext comes from
// the objective-c preprocessor and uses a best-guess approach to detecting
// uncaught exceptions, and may be called multiple times.
base::FilePath writer_path = NewLockedFilePath();
base::FilePath writer_path_unlocked = writer_path.RemoveFinalExtension();
std::unique_ptr<IOSIntermediateDumpWriter> unsafe_writer =
CreateWriterWithPath(writer_path);
ScopedLockedWriter writer(unsafe_writer.get(),
writer_path.value().c_str(),
writer_path_unlocked.value().c_str());
if (!writer.GetWriter()) {
CRASHPAD_RAW_LOG("Cannot DumpExceptionFromNSException without writer");
return;
}
ScopedReport report(writer.GetWriter(), system_data_, annotations_);
InProcessIntermediateDumpHandler::WriteExceptionFromMachException(
writer.GetWriter(),
MACH_EXCEPTION_CODES,
mach_thread_self(),
kMachExceptionFromNSException,
kEmulatedMachExceptionCodes,
std::size(kEmulatedMachExceptionCodes),
MACHINE_THREAD_STATE,
reinterpret_cast<ConstThreadState>(context),
MACHINE_THREAD_STATE_COUNT);
}
void InProcessHandler::DumpExceptionFromNSExceptionWithFrames(
const uint64_t* frames,
const size_t num_frames) {
INITIALIZATION_STATE_DCHECK_VALID(initialized_);
ScopedLockedWriter writer(GetCachedWriter(),
cached_writer_path_.c_str(),
cached_writer_unlocked_path_.c_str());
if (!writer.GetWriter()) {
CRASHPAD_RAW_LOG(
"Cannot DumpExceptionFromNSExceptionWithFrames without writer");
return;
}
ScopedReport report(
writer.GetWriter(), system_data_, annotations_, frames, num_frames);
InProcessIntermediateDumpHandler::WriteExceptionFromNSException(
writer.GetWriter());
}
bool InProcessHandler::DumpExceptionFromSimulatedMachException(
const NativeCPUContext* context,
base::FilePath* path) {
base::FilePath locked_path = NewLockedFilePath();
*path = locked_path.RemoveFinalExtension();
return DumpExceptionFromSimulatedMachExceptionAtPath(context, locked_path);
}
bool InProcessHandler::DumpExceptionFromSimulatedMachExceptionAtPath(
const NativeCPUContext* context,
const base::FilePath& path) {
// This does not use the cached writer. It's expected that simulated
// exceptions can be called multiple times and there is no expectation that
// the application is in an unsafe state, or will be terminated after this
// call.
std::unique_ptr<IOSIntermediateDumpWriter> unsafe_writer =
CreateWriterWithPath(path);
base::FilePath writer_path_unlocked = path.RemoveFinalExtension();
ScopedLockedWriter writer(unsafe_writer.get(),
path.value().c_str(),
writer_path_unlocked.value().c_str());
if (!writer.GetWriter()) {
CRASHPAD_RAW_LOG(
"Cannot DumpExceptionFromSimulatedMachExceptionAtPath without writer");
return false;
}
ScopedReport report(writer.GetWriter(), system_data_, annotations_);
InProcessIntermediateDumpHandler::WriteExceptionFromMachException(
writer.GetWriter(),
MACH_EXCEPTION_CODES,
mach_thread_self(),
kMachExceptionSimulated,
kEmulatedMachExceptionCodes,
std::size(kEmulatedMachExceptionCodes),
MACHINE_THREAD_STATE,
reinterpret_cast<ConstThreadState>(context),
MACHINE_THREAD_STATE_COUNT);
return true;
}
void InProcessHandler::ProcessIntermediateDumps(
const std::map<std::string, std::string>& annotations) {
INITIALIZATION_STATE_DCHECK_VALID(initialized_);
for (auto& file : PendingFiles())
ProcessIntermediateDump(file, annotations);
}
void InProcessHandler::ProcessIntermediateDump(
const base::FilePath& file,
const std::map<std::string, std::string>& annotations) {
INITIALIZATION_STATE_DCHECK_VALID(initialized_);
ProcessSnapshotIOSIntermediateDump process_snapshot;
if (process_snapshot.InitializeWithFilePath(file, annotations)) {
SaveSnapshot(process_snapshot);
}
}
void InProcessHandler::StartProcessingPendingReports() {
if (!upload_thread_)
return;
upload_thread_enabled_ = true;
UpdatePruneAndUploadThreads(true);
}
void InProcessHandler::UpdatePruneAndUploadThreads(bool active) {
base::AutoLock lock_owner(prune_and_upload_lock_);
// TODO(crbug.com/crashpad/400): Consider moving prune and upload thread to
// BackgroundTasks and/or NSURLSession. This might allow uploads to continue
// in the background.
if (active) {
if (!prune_thread_->is_running())
prune_thread_->Start();
if (upload_thread_enabled_ && !upload_thread_->is_running()) {
upload_thread_->Start();
}
} else {
if (prune_thread_->is_running())
prune_thread_->Stop();
if (upload_thread_enabled_ && upload_thread_->is_running())
upload_thread_->Stop();
}
}
void InProcessHandler::SaveSnapshot(
ProcessSnapshotIOSIntermediateDump& process_snapshot) {
std::unique_ptr<CrashReportDatabase::NewReport> new_report;
CrashReportDatabase::OperationStatus database_status =
database_->PrepareNewCrashReport(&new_report);
if (database_status != CrashReportDatabase::kNoError) {
Metrics::ExceptionCaptureResult(
Metrics::CaptureResult::kPrepareNewCrashReportFailed);
}
process_snapshot.SetReportID(new_report->ReportID());
UUID client_id;
Settings* const settings = database_->GetSettings();
if (settings && settings->GetClientID(&client_id)) {
process_snapshot.SetClientID(client_id);
}
MinidumpFileWriter minidump;
minidump.InitializeFromSnapshot(&process_snapshot);
if (!minidump.WriteEverything(new_report->Writer())) {
Metrics::ExceptionCaptureResult(
Metrics::CaptureResult::kMinidumpWriteFailed);
}
UUID uuid;
database_status =
database_->FinishedWritingCrashReport(std::move(new_report), &uuid);
if (database_status != CrashReportDatabase::kNoError) {
Metrics::ExceptionCaptureResult(
Metrics::CaptureResult::kFinishedWritingCrashReportFailed);
}
if (upload_thread_) {
upload_thread_->ReportPending(uuid);
}
}
std::vector<base::FilePath> InProcessHandler::PendingFiles() {
DirectoryReader reader;
std::vector<base::FilePath> files;
if (!reader.Open(base_dir_)) {
return files;
}
base::FilePath file;
DirectoryReader::Result result;
// Because the intermediate dump directory is expected to be shared,
// mitigate any spamming by limiting this to |kMaxPendingFiles|.
constexpr size_t kMaxPendingFiles = 20;
// Track other application bundles separately, so they don't spam our
// intermediate dumps into never getting processed.
std::vector<base::FilePath> other_files;
base::FilePath cached_writer_path(cached_writer_path_);
while ((result = reader.NextFile(&file)) ==
DirectoryReader::Result::kSuccess) {
// Don't try to process files marked as 'locked' from a different bundle id.
bool bundle_match =
file.value().compare(0,
bundle_identifier_and_seperator_.size(),
bundle_identifier_and_seperator_) == 0;
if (!bundle_match && file.FinalExtension() == kLockedExtension) {
continue;
}
// Never process the current cached writer path.
file = base_dir_.Append(file);
if (file == cached_writer_path)
continue;
// Otherwise, include any other unlocked, or locked files matching
// |bundle_identifier_and_seperator_|.
if (bundle_match) {
files.push_back(file);
if (files.size() >= kMaxPendingFiles)
return files;
} else {
other_files.push_back(file);
}
}
auto end_iterator =
other_files.begin() +
std::min(kMaxPendingFiles - files.size(), other_files.size());
files.insert(files.end(), other_files.begin(), end_iterator);
return files;
}
IOSIntermediateDumpWriter* InProcessHandler::GetCachedWriter() {
static_assert(
std::atomic<uint64_t>::is_always_lock_free,
"std::atomic_compare_exchange_strong uint64_t may not be signal-safe");
uint64_t thread_self;
// This is only safe when passing pthread_self(), otherwise this can lock.
pthread_threadid_np(pthread_self(), &thread_self);
uint64_t expected = 0;
if (!std::atomic_compare_exchange_strong(
&exception_thread_id_, &expected, thread_self)) {
if (expected == thread_self) {
// Another exception came in from this thread, which means it's likely
// that our own handler crashed. We could open up a new intermediate dump
// and try to save this dump, but we could end up endlessly writing dumps.
// Instead, give up.
} else {
// Another thread is handling a crash. Sleep forever.
while (1) {
sleep(std::numeric_limits<unsigned int>::max());
}
}
return nullptr;
}
return cached_writer_.get();
}
std::unique_ptr<IOSIntermediateDumpWriter>
InProcessHandler::CreateWriterWithPath(const base::FilePath& writer_path) {
std::unique_ptr<IOSIntermediateDumpWriter> writer =
std::make_unique<IOSIntermediateDumpWriter>();
if (!writer->Open(writer_path)) {
DLOG(ERROR) << "Unable to open intermediate dump file: "
<< writer_path.value();
return nullptr;
}
return writer;
}
const base::FilePath InProcessHandler::NewLockedFilePath() {
UUID uuid;
uuid.InitializeWithNew();
const std::string file_string =
bundle_identifier_and_seperator_ + uuid.ToString() + kLockedExtension;
return base_dir_.Append(file_string);
}
InProcessHandler::ScopedReport::ScopedReport(
IOSIntermediateDumpWriter* writer,
const IOSSystemDataCollector& system_data,
const std::map<std::string, std::string>& annotations,
const uint64_t* frames,
const size_t num_frames)
: writer_(writer),
frames_(frames),
num_frames_(num_frames),
rootMap_(writer) {
DCHECK(writer);
InProcessIntermediateDumpHandler::WriteHeader(writer);
InProcessIntermediateDumpHandler::WriteProcessInfo(writer, annotations);
InProcessIntermediateDumpHandler::WriteSystemInfo(writer, system_data);
}
InProcessHandler::ScopedReport::~ScopedReport() {
// Write threads and modules last (after the exception itself is written by
// DumpExceptionFrom*.)
InProcessIntermediateDumpHandler::WriteThreadInfo(
writer_, frames_, num_frames_);
InProcessIntermediateDumpHandler::WriteModuleInfo(writer_);
}
InProcessHandler::ScopedLockedWriter::ScopedLockedWriter(
IOSIntermediateDumpWriter* writer,
const char* writer_path,
const char* writer_unlocked_path)
: writer_path_(writer_path),
writer_unlocked_path_(writer_unlocked_path),
writer_(writer) {}
InProcessHandler::ScopedLockedWriter::~ScopedLockedWriter() {
if (!writer_)
return;
writer_->Close();
if (rename(writer_path_, writer_unlocked_path_) != 0) {
CRASHPAD_RAW_LOG("Could not remove locked extension.");
CRASHPAD_RAW_LOG(writer_path_);
CRASHPAD_RAW_LOG(writer_unlocked_path_);
}
}
} // namespace internal
} // namespace crashpad