/* * Copyright (C) 2019 The Android Open Source Project * * 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 "Log.h" #include "incidentd_util.h" #include "proto_util.h" #include "PrivacyFilter.h" #include "WorkDirectory.h" #include #include #include #include #include #include #include #include #include #include #include namespace android { namespace os { namespace incidentd { using std::thread; using google::protobuf::MessageLite; using google::protobuf::RepeatedPtrField; using google::protobuf::io::FileInputStream; using google::protobuf::io::FileOutputStream; /** * Turn off to skip removing files for debugging. */ static const bool DO_UNLINK = true; /** * File extension for envelope files. */ static const string EXTENSION_ENVELOPE(".envelope"); /** * File extension for data files. */ static const string EXTENSION_DATA(".data"); /** * Send these reports to dropbox. */ const ComponentName DROPBOX_SENTINEL("android", "DROPBOX"); /** metadata field id in IncidentProto */ const int FIELD_ID_INCIDENT_METADATA = 2; // Args for exec gzip static const char* GZIP[] = {"/system/bin/gzip", NULL}; /** * Read a protobuf from disk into the message. */ static status_t read_proto(MessageLite* msg, const string& filename) { int fd = open(filename.c_str(), O_RDONLY | O_CLOEXEC); if (fd < 0) { return -errno; } FileInputStream stream(fd); stream.SetCloseOnDelete(fd); if (!msg->ParseFromZeroCopyStream(&stream)) { return BAD_VALUE; } return stream.GetErrno(); } /** * Write a protobuf to disk. */ static status_t write_proto(const MessageLite& msg, const string& filename) { int fd = open(filename.c_str(), O_CREAT | O_TRUNC | O_RDWR | O_CLOEXEC, 0660); if (fd < 0) { return -errno; } FileOutputStream stream(fd); stream.SetCloseOnDelete(fd); if (!msg.SerializeToZeroCopyStream(&stream)) { ALOGW("write_proto: error writing to %s", filename.c_str()); return BAD_VALUE; } return stream.GetErrno(); } static string strip_extension(const string& filename) { return filename.substr(0, filename.find('.')); } static bool ends_with(const string& str, const string& ending) { if (str.length() >= ending.length()) { return str.compare(str.length()-ending.length(), ending.length(), ending) == 0; } else { return false; } } // Returns true if it was a valid timestamp. static bool parse_timestamp_ns(const string& id, int64_t* result) { char* endptr; *result = strtoll(id.c_str(), &endptr, 10); return id.length() != 0 && *endptr == '\0'; } static bool has_section(const ReportFileProto_Report& report, int section) { const size_t sectionCount = report.section_size(); for (int i = 0; i < sectionCount; i++) { if (report.section(i) == section) { return true; } } return false; } status_t create_directory(const char* directory) { struct stat st; status_t err = NO_ERROR; char* dir = strdup(directory); // Skip first slash char* d = dir + 1; // Create directories, assigning them to the system user bool last = false; while (!last) { d = strchr(d, '/'); if (d != NULL) { *d = '\0'; } else { last = true; } if (stat(dir, &st) == 0) { if (!S_ISDIR(st.st_mode)) { err = ALREADY_EXISTS; goto done; } } else { ALOGE("No such directory %s, something wrong.", dir); err = -1; goto done; } if (!last) { *d++ = '/'; } } // Ensure that the final directory is owned by the system with 0770. If it isn't // we won't write into it. if (stat(directory, &st) != 0) { ALOGE("No incident reports today. Can't stat: %s", directory); err = -errno; goto done; } if ((st.st_mode & 0777) != 0770) { ALOGE("No incident reports today. Mode is %0o on report directory %s", st.st_mode, directory); err = BAD_VALUE; goto done; } if (st.st_uid != AID_INCIDENTD || st.st_gid != AID_INCIDENTD) { ALOGE("No incident reports today. Owner is %d and group is %d on report directory %s", st.st_uid, st.st_gid, directory); err = BAD_VALUE; goto done; } done: free(dir); return err; } void log_envelope(const ReportFileProto& envelope) { ALOGD("Envelope: {"); for (int i=0; i& workDirectory, int64_t timestampNs, const string& envelopeFileName, const string& dataFileName) :mWorkDirectory(workDirectory), mTimestampNs(timestampNs), mEnvelopeFileName(envelopeFileName), mDataFileName(dataFileName), mEnvelope(), mDataFd(-1), mError(NO_ERROR) { // might get overwritten when we read but that's ok mEnvelope.set_data_file(mDataFileName); } ReportFile::~ReportFile() { if (mDataFd >= 0) { close(mDataFd); } } int64_t ReportFile::getTimestampNs() const { return mTimestampNs; } void ReportFile::addReport(const IncidentReportArgs& args) { // There is only one report per component. Merge into an existing one if necessary. ReportFileProto_Report* report; const int reportCount = mEnvelope.report_size(); int i = 0; for (; i < reportCount; i++) { report = mEnvelope.mutable_report(i); if (report->pkg() == args.receiverPkg() && report->cls() == args.receiverCls()) { if (args.getPrivacyPolicy() < report->privacy_policy()) { // Lower privacy policy (less restrictive) wins. report->set_privacy_policy(args.getPrivacyPolicy()); } report->set_all_sections(report->all_sections() | args.all()); for (int section: args.sections()) { if (!has_section(*report, section)) { report->add_section(section); } } break; } } if (i >= reportCount) { report = mEnvelope.add_report(); report->set_pkg(args.receiverPkg()); report->set_cls(args.receiverCls()); report->set_privacy_policy(args.getPrivacyPolicy()); report->set_all_sections(args.all()); report->set_gzip(args.gzip()); for (int section: args.sections()) { report->add_section(section); } } for (const vector& header: args.headers()) { report->add_header(header.data(), header.size()); } } void ReportFile::removeReport(const string& pkg, const string& cls) { RepeatedPtrField* reports = mEnvelope.mutable_report(); const int reportCount = reports->size(); for (int i = 0; i < reportCount; i++) { const ReportFileProto_Report& r = reports->Get(i); if (r.pkg() == pkg && r.cls() == cls) { reports->DeleteSubrange(i, 1); return; } } } void ReportFile::removeReports(const string& pkg) { RepeatedPtrField* reports = mEnvelope.mutable_report(); const int reportCount = reports->size(); for (int i = reportCount-1; i >= 0; i--) { const ReportFileProto_Report& r = reports->Get(i); if (r.pkg() == pkg) { reports->DeleteSubrange(i, 1); } } } void ReportFile::setMetadata(const IncidentMetadata& metadata) { *mEnvelope.mutable_metadata() = metadata; } void ReportFile::markCompleted() { mEnvelope.set_completed(true); } status_t ReportFile::markApproved(const string& pkg, const string& cls) { size_t const reportCount = mEnvelope.report_size(); for (int reportIndex = 0; reportIndex < reportCount; reportIndex++) { ReportFileProto_Report* report = mEnvelope.mutable_report(reportIndex); if (report->pkg() == pkg && report->cls() == cls) { report->set_share_approved(true); return NO_ERROR; } } return NAME_NOT_FOUND; } void ReportFile::setMaxPersistedPrivacyPolicy(int persistedPrivacyPolicy) { mEnvelope.set_privacy_policy(persistedPrivacyPolicy); } status_t ReportFile::saveEnvelope() { return save_envelope_impl(true); } status_t ReportFile::trySaveEnvelope() { return save_envelope_impl(false); } status_t ReportFile::loadEnvelope() { return load_envelope_impl(true); } status_t ReportFile::tryLoadEnvelope() { return load_envelope_impl(false); } const ReportFileProto& ReportFile::getEnvelope() { return mEnvelope; } status_t ReportFile::startWritingDataFile() { if (mDataFd >= 0) { ALOGW("ReportFile::startWritingDataFile called with the file already open: %s", mDataFileName.c_str()); return ALREADY_EXISTS; } mDataFd = open(mDataFileName.c_str(), O_CREAT | O_TRUNC | O_RDWR | O_CLOEXEC, 0660); if (mDataFd < 0) { return -errno; } return NO_ERROR; } void ReportFile::closeDataFile() { if (mDataFd >= 0) { mEnvelope.set_data_file_size(lseek(mDataFd, 0, SEEK_END)); close(mDataFd); mDataFd = -1; } } status_t ReportFile::startFilteringData(int writeFd, const IncidentReportArgs& args) { // Open data file. int dataFd = open(mDataFileName.c_str(), O_RDONLY | O_CLOEXEC); if (dataFd < 0) { ALOGW("Error opening incident report '%s' %s", getDataFileName().c_str(), strerror(-errno)); close(writeFd); return -errno; } // Check that the size on disk is what we thought we wrote. struct stat st; if (fstat(dataFd, &st) != 0) { ALOGW("Error running fstat incident report '%s' %s", getDataFileName().c_str(), strerror(-errno)); close(writeFd); return -errno; } if (st.st_size != mEnvelope.data_file_size()) { ALOGW("File size mismatch. Envelope says %" PRIi64 " bytes but data file is %" PRIi64 " bytes: %s", (int64_t)mEnvelope.data_file_size(), st.st_size, mDataFileName.c_str()); ALOGW("Removing incident report"); mWorkDirectory->remove(this); close(writeFd); return BAD_VALUE; } pid_t zipPid = 0; if (args.gzip()) { Fpipe zipPipe; if (!zipPipe.init()) { ALOGE("[ReportFile] Failed to setup pipe for gzip"); close(writeFd); return -errno; } int status = 0; zipPid = fork_execute_cmd((char* const*)GZIP, zipPipe.readFd().release(), writeFd, &status); close(writeFd); if (zipPid < 0 || status != 0) { ALOGE("[ReportFile] Failed to fork and exec gzip"); return status; } writeFd = zipPipe.writeFd().release(); } status_t err; for (const auto& report : mEnvelope.report()) { for (const auto& header : report.header()) { write_header_section(writeFd, reinterpret_cast(header.c_str()), header.size()); } } if (mEnvelope.has_metadata()) { write_section(writeFd, FIELD_ID_INCIDENT_METADATA, mEnvelope.metadata()); } err = filter_and_write_report(writeFd, dataFd, mEnvelope.privacy_policy(), args); if (err != NO_ERROR) { ALOGW("Error writing incident report '%s' to dropbox: %s", getDataFileName().c_str(), strerror(-err)); } close(writeFd); if (zipPid > 0) { status_t err = wait_child(zipPid, /* timeout_ms= */ 10 * 1000); if (err != 0) { ALOGE("[ReportFile] abnormal child process: %s", strerror(-err)); } return err; } return NO_ERROR; } string ReportFile::getDataFileName() const { return mDataFileName; } string ReportFile::getEnvelopeFileName() const { return mEnvelopeFileName; } int ReportFile::getDataFileFd() { return mDataFd; } void ReportFile::setWriteError(status_t err) { mError = err; } status_t ReportFile::getWriteError() { return mError; } string ReportFile::getId() { return to_string(mTimestampNs); } status_t ReportFile::save_envelope_impl(bool cleanup) { status_t err; err = write_proto(mEnvelope, mEnvelopeFileName); if (err != NO_ERROR) { // If there was an error writing the envelope, then delete the whole thing. if (cleanup) { mWorkDirectory->remove(this); } return err; } return NO_ERROR; } status_t ReportFile::load_envelope_impl(bool cleanup) { status_t err; err = read_proto(&mEnvelope, mEnvelopeFileName); if (err != NO_ERROR) { // If there was an error reading the envelope, then delete the whole thing. if (cleanup) { mWorkDirectory->remove(this); } return err; } return NO_ERROR; } // ================================================================================ // WorkDirectory::WorkDirectory() :mDirectory("/data/misc/incidents"), mMaxFileCount(100), mMaxDiskUsageBytes(100 * 1024 * 1024) { // Incident reports can take up to 100MB on disk. // TODO: Should be a flag. create_directory(mDirectory.c_str()); } WorkDirectory::WorkDirectory(const string& dir, int maxFileCount, long maxDiskUsageBytes) :mDirectory(dir), mMaxFileCount(maxFileCount), mMaxDiskUsageBytes(maxDiskUsageBytes) { create_directory(mDirectory.c_str()); } sp WorkDirectory::createReportFile() { unique_lock lock(mLock); status_t err; clean_directory_locked(); int64_t timestampNs = make_timestamp_ns_locked(); string envelopeFileName = make_filename(timestampNs, EXTENSION_ENVELOPE); string dataFileName = make_filename(timestampNs, EXTENSION_DATA); sp result = new ReportFile(this, timestampNs, envelopeFileName, dataFileName); err = result->trySaveEnvelope(); if (err != NO_ERROR) { ALOGW("Can't save envelope file %s: %s", strerror(-errno), envelopeFileName.c_str()); return nullptr; } return result; } status_t WorkDirectory::getReports(vector>* result, int64_t after) { unique_lock lock(mLock); const bool DBG = true; if (DBG) { ALOGD("WorkDirectory::getReports"); } map files; get_directory_contents_locked(&files, after); for (map::iterator it = files.begin(); it != files.end(); it++) { sp reportFile = new ReportFile(this, it->second.timestampNs, it->second.envelope, it->second.data); if (DBG) { ALOGD(" %s", reportFile->getId().c_str()); } result->push_back(reportFile); } return NO_ERROR; } sp WorkDirectory::getReport(const string& pkg, const string& cls, const string& id, IncidentReportArgs* args) { unique_lock lock(mLock); status_t err; int64_t timestampNs; if (!parse_timestamp_ns(id, ×tampNs)) { return nullptr; } // Make the ReportFile object, and then see if it's valid and for pkg and cls. sp result = new ReportFile(this, timestampNs, make_filename(timestampNs, EXTENSION_ENVELOPE), make_filename(timestampNs, EXTENSION_DATA)); err = result->tryLoadEnvelope(); if (err != NO_ERROR) { ALOGW("Can't open envelope file for report %s/%s %s", pkg.c_str(), cls.c_str(), id.c_str()); return nullptr; } const ReportFileProto& envelope = result->getEnvelope(); const size_t reportCount = envelope.report_size(); for (int i = 0; i < reportCount; i++) { const ReportFileProto_Report& report = envelope.report(i); if (report.pkg() == pkg && report.cls() == cls) { if (args != nullptr) { get_args_from_report(args, report); } return result; } } return nullptr; } bool WorkDirectory::hasMore(int64_t after) { unique_lock lock(mLock); map files; get_directory_contents_locked(&files, after); return files.size() > 0; } void WorkDirectory::commit(const sp& report, const string& pkg, const string& cls) { status_t err; ALOGI("Committing report %s for %s/%s", report->getId().c_str(), pkg.c_str(), cls.c_str()); unique_lock lock(mLock); // Load the envelope here inside the lock. err = report->loadEnvelope(); report->removeReport(pkg, cls); delete_files_for_report_if_necessary(report); } void WorkDirectory::commitAll(const string& pkg) { status_t err; ALOGI("All reports for %s", pkg.c_str()); unique_lock lock(mLock); map files; get_directory_contents_locked(&files, 0); for (map::iterator it = files.begin(); it != files.end(); it++) { sp reportFile = new ReportFile(this, it->second.timestampNs, it->second.envelope, it->second.data); err = reportFile->loadEnvelope(); if (err != NO_ERROR) { continue; } reportFile->removeReports(pkg); delete_files_for_report_if_necessary(reportFile); } } void WorkDirectory::remove(const sp& report) { unique_lock lock(mLock); // Set this to false to leave files around for debugging. if (DO_UNLINK) { unlink(report->getDataFileName().c_str()); unlink(report->getEnvelopeFileName().c_str()); } } int64_t WorkDirectory::make_timestamp_ns_locked() { // Guarantee that we don't have duplicate timestamps. // This is a little bit lame, but since reports are created on the // same thread and are kinda slow we'll seldomly actually hit the // condition. The bigger risk is the clock getting reset and causing // a collision. In that case, we'll just make incident reporting a // little bit slower. Nobody will notice if we just loop until we // have a unique file name. int64_t timestampNs = 0; do { struct timespec spec; if (timestampNs > 0) { spec.tv_sec = 0; spec.tv_nsec = 1; nanosleep(&spec, nullptr); } clock_gettime(CLOCK_REALTIME, &spec); timestampNs = int64_t(spec.tv_sec) * 1000 + spec.tv_nsec; } while (file_exists_locked(timestampNs)); return (timestampNs >= 0)? timestampNs : -timestampNs; } /** * It is required to hold the lock here so in case someone else adds it * our result is still correct for the caller. */ bool WorkDirectory::file_exists_locked(int64_t timestampNs) { const string filename = make_filename(timestampNs, EXTENSION_ENVELOPE); struct stat st; return stat(filename.c_str(), &st) == 0; } string WorkDirectory::make_filename(int64_t timestampNs, const string& extension) { // Zero pad the timestamp so it can also be alpha sorted. stringstream result; result << mDirectory << '/' << setfill('0') << setw(20) << timestampNs << extension; return result.str(); } off_t WorkDirectory::get_directory_contents_locked(map* files, int64_t after) { DIR* dir; struct dirent* entry; if ((dir = opendir(mDirectory.c_str())) == NULL) { ALOGE("Couldn't open incident directory: %s", mDirectory.c_str()); return -1; } string dirbase(mDirectory); if (mDirectory[dirbase.size() - 1] != '/') dirbase += "/"; off_t totalSize = 0; // Enumerate, count and add up size while ((entry = readdir(dir)) != NULL) { if (entry->d_name[0] == '.') { continue; } string entryname = entry->d_name; // local to this dir string filename = dirbase + entryname; // fully qualified bool isEnvelope = ends_with(entryname, EXTENSION_ENVELOPE); bool isData = ends_with(entryname, EXTENSION_DATA); // If the file isn't one of our files, just ignore it. Otherwise, // sum up the sizes. if (isEnvelope || isData) { string timestamp = strip_extension(entryname); int64_t timestampNs; if (!parse_timestamp_ns(timestamp, ×tampNs)) { continue; } if (after == 0 || timestampNs > after) { struct stat st; if (stat(filename.c_str(), &st) != 0) { ALOGE("Unable to stat file %s", filename.c_str()); continue; } if (!S_ISREG(st.st_mode)) { continue; } WorkDirectoryEntry& entry = (*files)[timestamp]; if (isEnvelope) { entry.envelope = filename; } else if (isData) { entry.data = filename; } entry.timestampNs = timestampNs; entry.size += st.st_size; totalSize += st.st_size; } } } closedir(dir); // Now check if there are any data files that don't have envelope files. // If there are, then just go ahead and delete them now. Don't wait for // a cleaning. if (DO_UNLINK) { map::iterator it = files->begin(); while (it != files->end()) { if (it->second.envelope.length() == 0) { unlink(it->second.data.c_str()); it = files->erase(it); } else { it++; } } } return totalSize; } void WorkDirectory::clean_directory_locked() { DIR* dir; struct dirent* entry; struct stat st; // Map of filename without extension to the entries about it. Conveniently, // this also keeps the list sorted by filename, which is a timestamp. map files; off_t totalSize = get_directory_contents_locked(&files, 0); if (totalSize < 0) { return; } int totalCount = files.size(); // Count or size is less than max, then we're done. if (totalSize < mMaxDiskUsageBytes && totalCount < mMaxFileCount) { return; } // Remove files until we're under our limits. if (DO_UNLINK) { for (map::const_iterator it = files.begin(); it != files.end() && (totalSize >= mMaxDiskUsageBytes || totalCount >= mMaxFileCount); it++) { unlink(it->second.envelope.c_str()); unlink(it->second.data.c_str()); totalSize -= it->second.size; totalCount--; } } } void WorkDirectory::delete_files_for_report_if_necessary(const sp& report) { if (report->getEnvelope().report_size() == 0) { ALOGI("Report %s is finished. Deleting from storage.", report->getId().c_str()); if (DO_UNLINK) { unlink(report->getDataFileName().c_str()); unlink(report->getEnvelopeFileName().c_str()); } } } // ================================================================================ void get_args_from_report(IncidentReportArgs* out, const ReportFileProto_Report& report) { out->setPrivacyPolicy(report.privacy_policy()); out->setAll(report.all_sections()); out->setReceiverPkg(report.pkg()); out->setReceiverCls(report.cls()); out->setGzip(report.gzip()); const int sectionCount = report.section_size(); for (int i = 0; i < sectionCount; i++) { out->addSection(report.section(i)); } const int headerCount = report.header_size(); for (int i = 0; i < headerCount; i++) { const string& header = report.header(i); vector vec(header.begin(), header.end()); out->addHeader(vec); } } } // namespace incidentd } // namespace os } // namespace android