| // Copyright 2021 The Crashpad Authors |
| // |
| // 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/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() { |
| if (cached_writer_) { |
| cached_writer_->Close(); |
| } |
| UpdatePruneAndUploadThreads(false, UploadBehavior::kUploadWhenAppIsActive); |
| } |
| |
| bool InProcessHandler::Initialize( |
| const base::FilePath& database, |
| const std::string& url, |
| const std::map<std::string, std::string>& annotations, |
| ProcessPendingReportsObservationCallback callback) { |
| 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, callback)); |
| } |
| |
| 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) { |
| dispatch_async( |
| dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ |
| UpdatePruneAndUploadThreads(active, |
| UploadBehavior::kUploadWhenAppIsActive); |
| }); |
| }); |
| } |
| |
| 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::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, |
| exception_type_t exception, |
| base::FilePath* path) { |
| base::FilePath locked_path = NewLockedFilePath(); |
| *path = locked_path.RemoveFinalExtension(); |
| return DumpExceptionFromSimulatedMachExceptionAtPath( |
| context, exception, locked_path); |
| } |
| |
| bool InProcessHandler::DumpExceptionFromSimulatedMachExceptionAtPath( |
| const NativeCPUContext* context, |
| exception_type_t exception, |
| 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(), |
| exception, |
| kEmulatedMachExceptionCodes, |
| std::size(kEmulatedMachExceptionCodes), |
| MACHINE_THREAD_STATE, |
| reinterpret_cast<ConstThreadState>(context), |
| MACHINE_THREAD_STATE_COUNT); |
| return true; |
| } |
| |
| bool InProcessHandler::MoveIntermediateDumpAtPathToPending( |
| const base::FilePath& path) { |
| base::FilePath new_path_unlocked = NewLockedFilePath().RemoveFinalExtension(); |
| return MoveFileOrDirectory(path, new_path_unlocked); |
| } |
| 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( |
| UploadBehavior upload_behavior) { |
| if (!upload_thread_) |
| return; |
| |
| upload_thread_enabled_ = true; |
| |
| // This may be a no-op if IsApplicationActive is false, as it is not safe to |
| // start the upload thread when in the background (due to the potential for |
| // flocked files in shared containers). |
| // 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. |
| UpdatePruneAndUploadThreads(system_data_.IsApplicationActive(), |
| upload_behavior); |
| } |
| |
| void InProcessHandler::UpdatePruneAndUploadThreads( |
| bool active, |
| UploadBehavior upload_behavior) { |
| 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. |
| bool threads_should_run; |
| switch (upload_behavior) { |
| case UploadBehavior::kUploadWhenAppIsActive: |
| threads_should_run = active; |
| break; |
| case UploadBehavior::kUploadImmediately: |
| threads_should_run = true; |
| break; |
| } |
| if (threads_should_run) { |
| 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); |
| return; |
| } |
| 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); |
| return; |
| } |
| UUID uuid; |
| database_status = |
| database_->FinishedWritingCrashReport(std::move(new_report), &uuid); |
| if (database_status != CrashReportDatabase::kNoError) { |
| Metrics::ExceptionCaptureResult( |
| Metrics::CaptureResult::kFinishedWritingCrashReportFailed); |
| return; |
| } |
| |
| 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); |
| // Grab the report creation time before writing the report. |
| uint64_t report_time_nanos = ClockMonotonicNanoseconds(); |
| InProcessIntermediateDumpHandler::WriteHeader(writer); |
| InProcessIntermediateDumpHandler::WriteProcessInfo(writer, annotations); |
| InProcessIntermediateDumpHandler::WriteSystemInfo( |
| writer, system_data, report_time_nanos); |
| } |
| |
| 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 |