// Copyright 2015 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/crash_report_database.h" #include #include #import #include #include #include #include #include #include "base/logging.h" #include "base/posix/eintr_wrapper.h" #include "base/scoped_generic.h" #include "base/strings/string_piece.h" #include "base/strings/stringprintf.h" #include "base/strings/sys_string_conversions.h" #include "util/file/file_io.h" #include "util/mac/xattr.h" namespace crashpad { namespace { const char kDatabaseDirectoryName[] = "Crashpad"; const char kWriteDirectory[] = "new"; const char kUploadPendingDirectory[] = "pending"; const char kCompletedDirectory[] = "completed"; const char* const kReportDirectories[] = { kWriteDirectory, kUploadPendingDirectory, kCompletedDirectory, }; const char kCrashReportFileExtension[] = "dmp"; const char kXattrUUID[] = "uuid"; const char kXattrCollectorID[] = "id"; const char kXattrCreationTime[] = "creation_time"; const char kXattrIsUploaded[] = "uploaded"; const char kXattrLastUploadTime[] = "last_upload_time"; const char kXattrUploadAttemptCount[] = "upload_count"; const char kXattrDatabaseInitialized[] = "initialized"; // Ensures that the node at |path| is a directory, and creates it if it does // not exist. If the |path| points to a file, rather than a directory, or the // directory could not be created, returns false. Otherwise, returns true, // indicating that |path| already was or now is a directory. bool CreateOrEnsureDirectoryExists(const base::FilePath& path) { if (mkdir(path.value().c_str(), 0755) == 0) { return true; } else if (errno == EEXIST) { struct stat st; if (stat(path.value().c_str(), &st) != 0) { PLOG(ERROR) << "stat"; return false; } if (S_ISDIR(st.st_mode)) { return true; } else { LOG(ERROR) << "not a directory"; return false; } } else { PLOG(ERROR) << "mkdir"; return false; } } //! \brief A CrashReportDatabase that uses HFS+ extended attributes to store //! report metadata. //! //! The database maintains three directories of reports: `"new"` to hold crash //! reports that are in the process of being written, `"completed"` to hold //! reports that have been written and are awaing upload, and `"uploaded"` to //! hold reports successfully uploaded to a collection server. If the user has //! opted out of report collection, reports will still be written and moved //! to the completed directory, but they just will not be uploaded. //! //! The database stores its metadata in extended filesystem attributes. To //! ensure safe access, the report file is locked using `O_EXLOCK` during all //! extended attribute operations. The lock should be obtained using //! ObtainReportLock(). class CrashReportDatabaseMac : public CrashReportDatabase { public: explicit CrashReportDatabaseMac(const base::FilePath& path); virtual ~CrashReportDatabaseMac(); bool Initialize(); // CrashReportDatabase: OperationStatus PrepareNewCrashReport(NewReport** report) override; OperationStatus FinishedWritingCrashReport(NewReport* report, UUID* uuid) override; OperationStatus ErrorWritingCrashReport(NewReport* report) override; OperationStatus LookUpCrashReport(const UUID& uuid, Report* report) override; OperationStatus GetPendingReports( std::vector* reports) override; OperationStatus GetCompletedReports( std::vector* reports) override; OperationStatus GetReportForUploading(const UUID& uuid, const Report** report) override; OperationStatus RecordUploadAttempt(const Report* report, bool successful, const std::string& id) override; OperationStatus SkipReportUpload(const UUID& uuid) override; private: //! \brief A private extension of the Report class that maintains bookkeeping //! information of the database. struct UploadReport : public Report { //! \brief Stores the flock of the file for the duration of //! GetReportForUploading() and RecordUploadAttempt(). int lock_fd; }; //! \brief Locates a crash report in the database by UUID. //! //! \param[in] uuid The UUID of the crash report to locate. //! //! \return The full path to the report file, or an empty path if it cannot be //! found. base::FilePath LocateCrashReport(const UUID& uuid); //! \brief Obtains an exclusive advisory lock on a file. //! //! The flock is used to prevent cross-process concurrent metadata reads or //! writes. While xattrs do not observe the lock, if the lock-then-mutate //! protocol is observed by all clients of the database, it still enforces //! synchronization. //! //! This does not block, and so callers must ensure that the lock is valid //! after calling. //! //! \param[in] path The path of the file to lcok. //! //! \return A scoped lock object. If the result is not valid, an error is //! logged. static base::ScopedFD ObtainReportLock(const base::FilePath& path); //! \brief Reads all the database xattrs from a file into a Report. The file //! must be locked with ObtainReportLock. //! //! \param[in] path The path of the report. //! \param[out] report The object into which data will be read. //! //! \return `true` if all the metadata was read successfully, `false` //! otherwise. static bool ReadReportMetadataLocked(const base::FilePath& path, Report* report); //! \brief Reads the metadata from all the reports in a database subdirectory. //! Invalid reports are skipped. //! //! \param[in] path The database subdirectory path. //! \param[out] reports An empty vector of reports, which will be filled. //! //! \return The operation status code. static OperationStatus ReportsInDirectory(const base::FilePath& path, std::vector* reports); //! \brief Creates a database xattr name from the short constant name. //! //! \param[in] name The short name of the extended attribute. //! //! \return The long name of the extended attribute. static std::string XattrName(const base::StringPiece& name); base::FilePath base_dir_; DISALLOW_COPY_AND_ASSIGN(CrashReportDatabaseMac); }; CrashReportDatabaseMac::CrashReportDatabaseMac(const base::FilePath& path) : CrashReportDatabase(), base_dir_(path) { } CrashReportDatabaseMac::~CrashReportDatabaseMac() {} bool CrashReportDatabaseMac::Initialize() { // Check if the database already exists. if (!CreateOrEnsureDirectoryExists(base_dir_)) return false; // Create the three processing directories for the database. for (size_t i = 0; i < arraysize(kReportDirectories); ++i) { if (!CreateOrEnsureDirectoryExists(base_dir_.Append(kReportDirectories[i]))) return false; } // Write an xattr as the last step, to ensure the filesystem has support for // them. This attribute will never be read. return WriteXattrBool(base_dir_, XattrName(kXattrDatabaseInitialized), true); } CrashReportDatabase::OperationStatus CrashReportDatabaseMac::PrepareNewCrashReport(NewReport** out_report) { uuid_t uuid_gen; uuid_generate(uuid_gen); UUID uuid(uuid_gen); scoped_ptr report(new NewReport()); report->path = base_dir_.Append(kWriteDirectory) .Append(uuid.ToString() + "." + kCrashReportFileExtension); report->handle = HANDLE_EINTR(open(report->path.value().c_str(), O_CREAT | O_WRONLY | O_EXCL | O_EXLOCK, 0600)); if (report->handle < 0) { PLOG(ERROR) << "open " << report->path.value(); return kFileSystemError; } // TODO(rsesek): Potentially use an fsetxattr() here instead. if (!WriteXattr(report->path, XattrName(kXattrUUID), uuid.ToString())) { PLOG_IF(ERROR, IGNORE_EINTR(close(report->handle)) != 0) << "close"; return kDatabaseError; } *out_report = report.release(); return kNoError; } CrashReportDatabase::OperationStatus CrashReportDatabaseMac::FinishedWritingCrashReport(NewReport* report, UUID* uuid) { // Takes ownership of the |handle| and the O_EXLOCK. base::ScopedFD lock(report->handle); // Take ownership of the report. scoped_ptr scoped_report(report); // Get the report's UUID to return. std::string uuid_string; if (ReadXattr(report->path, XattrName(kXattrUUID), &uuid_string) != XattrStatus::kOK || !uuid->InitializeFromString(uuid_string)) { LOG(ERROR) << "Failed to read UUID for crash report " << report->path.value(); return kDatabaseError; } // Record the creation time of this report. if (!WriteXattrTimeT(report->path, XattrName(kXattrCreationTime), time(nullptr))) { return kDatabaseError; } // Move the report to its new location for uploading. base::FilePath new_path = base_dir_.Append(kUploadPendingDirectory).Append(report->path.BaseName()); if (rename(report->path.value().c_str(), new_path.value().c_str()) != 0) { PLOG(ERROR) << "rename " << report->path.value() << " to " << new_path.value(); return kFileSystemError; } return kNoError; } CrashReportDatabase::OperationStatus CrashReportDatabaseMac::ErrorWritingCrashReport(NewReport* report) { // Takes ownership of the |handle| and the O_EXLOCK. base::ScopedFD lock(report->handle); // Take ownership of the report. scoped_ptr scoped_report(report); // Remove the file that the report would have been written to had no error // occurred. if (unlink(report->path.value().c_str()) != 0) { PLOG(ERROR) << "unlink " << report->path.value(); return kFileSystemError; } return kNoError; } CrashReportDatabase::OperationStatus CrashReportDatabaseMac::LookUpCrashReport(const UUID& uuid, CrashReportDatabase::Report* report) { base::FilePath path = LocateCrashReport(uuid); if (path.empty()) return kReportNotFound; base::ScopedFD lock(ObtainReportLock(path)); if (!lock.is_valid()) return kBusyError; *report = Report(); report->file_path = path; if (!ReadReportMetadataLocked(path, report)) return kDatabaseError; return kNoError; } CrashReportDatabase::OperationStatus CrashReportDatabaseMac::GetPendingReports( std::vector* reports) { return ReportsInDirectory(base_dir_.Append(kUploadPendingDirectory), reports); } CrashReportDatabase::OperationStatus CrashReportDatabaseMac::GetCompletedReports( std::vector* reports) { return ReportsInDirectory(base_dir_.Append(kCompletedDirectory), reports); } CrashReportDatabase::OperationStatus CrashReportDatabaseMac::GetReportForUploading(const UUID& uuid, const Report** report) { base::FilePath report_path = LocateCrashReport(uuid); if (report_path.empty()) return kReportNotFound; scoped_ptr upload_report(new UploadReport()); upload_report->file_path = report_path; base::ScopedFD lock(ObtainReportLock(report_path)); if (!lock.is_valid()) return kBusyError; if (!ReadReportMetadataLocked(report_path, upload_report.get())) return kDatabaseError; upload_report->lock_fd = lock.release(); *report = upload_report.release(); return kNoError; } CrashReportDatabase::OperationStatus CrashReportDatabaseMac::RecordUploadAttempt(const Report* report, bool successful, const std::string& id) { DCHECK(report); DCHECK(successful || id.empty()); base::FilePath report_path = LocateCrashReport(report->uuid); if (report_path.empty()) return kReportNotFound; scoped_ptr upload_report( static_cast(report)); base::ScopedFD lock(upload_report->lock_fd); if (!lock.is_valid()) return kBusyError; if (successful) { base::FilePath new_path = base_dir_.Append(kCompletedDirectory).Append(report_path.BaseName()); if (rename(report_path.value().c_str(), new_path.value().c_str()) != 0) { PLOG(ERROR) << "rename " << report_path.value() << " to " << new_path.value(); return kFileSystemError; } report_path = new_path; } if (!WriteXattrBool(report_path, XattrName(kXattrIsUploaded), successful)) { return kDatabaseError; } if (!WriteXattr(report_path, XattrName(kXattrCollectorID), id)) { return kDatabaseError; } if (!WriteXattrTimeT(report_path, XattrName(kXattrLastUploadTime), time(nullptr))) { return kDatabaseError; } int upload_attempts = 0; std::string name = XattrName(kXattrUploadAttemptCount); if (ReadXattrInt(report_path, name, &upload_attempts) == XattrStatus::kOtherError) { return kDatabaseError; } if (!WriteXattrInt(report_path, name, ++upload_attempts)) { return kDatabaseError; } return kNoError; } CrashReportDatabase::OperationStatus CrashReportDatabaseMac::SkipReportUpload( const UUID& uuid) { base::FilePath report_path = LocateCrashReport(uuid); if (report_path.empty()) return kReportNotFound; base::ScopedFD lock(ObtainReportLock(report_path)); if (!lock.is_valid()) return kBusyError; base::FilePath new_path = base_dir_.Append(kCompletedDirectory).Append(report_path.BaseName()); if (rename(report_path.value().c_str(), new_path.value().c_str()) != 0) { PLOG(ERROR) << "rename " << report_path.value() << " to " << new_path.value(); return kFileSystemError; } return kNoError; } base::FilePath CrashReportDatabaseMac::LocateCrashReport(const UUID& uuid) { const std::string target_uuid = uuid.ToString(); for (size_t i = 0; i < arraysize(kReportDirectories); ++i) { base::FilePath path = base_dir_.Append(kReportDirectories[i]) .Append(target_uuid + "." + kCrashReportFileExtension); // Test if the path exists. struct stat st; if (lstat(path.value().c_str(), &st)) { continue; } // Check that the UUID of the report matches. std::string uuid_string; if (ReadXattr(path, XattrName(kXattrUUID), &uuid_string) == XattrStatus::kOK && uuid_string == target_uuid) { return path; } } return base::FilePath(); } // static base::ScopedFD CrashReportDatabaseMac::ObtainReportLock( const base::FilePath& path) { int fd = HANDLE_EINTR(open(path.value().c_str(), O_RDONLY | O_EXLOCK | O_CLOEXEC | O_NONBLOCK)); PLOG_IF(ERROR, fd < 0) << "open lock " << path.value(); return base::ScopedFD(fd); } // static bool CrashReportDatabaseMac::ReadReportMetadataLocked( const base::FilePath& path, Report* report) { std::string uuid_string; if (ReadXattr(path, XattrName(kXattrUUID), &uuid_string) != XattrStatus::kOK || !report->uuid.InitializeFromString(uuid_string)) { return false; } if (ReadXattrTimeT(path, XattrName(kXattrCreationTime), &report->creation_time) != XattrStatus::kOK) { return false; } report->id = std::string(); if (ReadXattr(path, XattrName(kXattrCollectorID), &report->id) == XattrStatus::kOtherError) { return false; } report->uploaded = false; if (ReadXattrBool(path, XattrName(kXattrIsUploaded), &report->uploaded) == XattrStatus::kOtherError) { return false; } report->last_upload_attempt_time = 0; if (ReadXattrTimeT(path, XattrName(kXattrLastUploadTime), &report->last_upload_attempt_time) == XattrStatus::kOtherError) { return false; } report->upload_attempts = 0; if (ReadXattrInt(path, XattrName(kXattrUploadAttemptCount), &report->upload_attempts) == XattrStatus::kOtherError) { return false; } return true; } // static CrashReportDatabase::OperationStatus CrashReportDatabaseMac::ReportsInDirectory( const base::FilePath& path, std::vector* reports) { DCHECK(reports->empty()); NSError* error = nil; NSArray* paths = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:base::SysUTF8ToNSString(path.value()) error:&error]; if (error) { LOG(ERROR) << "Failed to enumerate reports in directory " << path.value() << ": " << [[error description] UTF8String]; return kFileSystemError; } reports->reserve([paths count]); for (NSString* entry in paths) { base::FilePath report_path = path.Append([entry fileSystemRepresentation]); base::ScopedFD lock(ObtainReportLock(report_path)); if (!lock.is_valid()) continue; Report report; if (!ReadReportMetadataLocked(report_path, &report)) { LOG(WARNING) << "Failed to read report metadata for " << report_path.value(); continue; } reports->push_back(report); } return kNoError; } // static std::string CrashReportDatabaseMac::XattrName(const base::StringPiece& name) { return base::StringPrintf("com.googlecode.crashpad.%s", name.data()); } } // namespace // static scoped_ptr CrashReportDatabase::Initialize( const base::FilePath& path) { scoped_ptr database_mac( new CrashReportDatabaseMac(path.Append(kDatabaseDirectoryName))); if (!database_mac->Initialize()) database_mac.reset(); return scoped_ptr(database_mac.release()); } } // namespace crashpad