Use the consent service to check if user has consented to taking a
bugreport
Test: verified current flows of bugreport generation still work
Test: TBD test the new flow
Change-Id: I5fc154905515db664f76d1453c9d79364dc2f01e
diff --git a/cmds/dumpstate/dumpstate.cpp b/cmds/dumpstate/dumpstate.cpp
index c005084..dfc5b49 100644
--- a/cmds/dumpstate/dumpstate.cpp
+++ b/cmds/dumpstate/dumpstate.cpp
@@ -51,6 +51,7 @@
#include <android-base/unique_fd.h>
#include <android/hardware/dumpstate/1.0/IDumpstateDevice.h>
#include <android/hidl/manager/1.0/IServiceManager.h>
+#include <android/os/IIncidentCompanion.h>
#include <cutils/native_handle.h>
#include <cutils/properties.h>
#include <dumpsys.h>
@@ -83,15 +84,19 @@
using android::UNKNOWN_ERROR;
using android::Vector;
using android::base::StringPrintf;
+using android::os::IDumpstateListener;
using android::os::dumpstate::CommandOptions;
using android::os::dumpstate::DumpFileToFd;
using android::os::dumpstate::DumpstateSectionReporter;
using android::os::dumpstate::GetPidByName;
using android::os::dumpstate::PropertiesHelper;
+typedef Dumpstate::ConsentCallback::ConsentResult UserConsentResult;
+
/* read before root is shed */
static char cmdline_buf[16384] = "(unknown)";
static const char *dump_traces_path = nullptr;
+static const uint64_t USER_CONSENT_TIMEOUT_MS = 30 * 1000;
// TODO: variables and functions below should be part of dumpstate object
@@ -165,6 +170,13 @@
return false;
}
+static bool UnlinkAndLogOnError(const std::string& file) {
+ if (unlink(file.c_str()) != -1) {
+ MYLOGE("Failed to remove file (%s): %s\n", file.c_str(), strerror(errno));
+ return false;
+ }
+ return true;
+}
} // namespace
} // namespace os
@@ -657,6 +669,32 @@
return timeout_ms > MINIMUM_LOGCAT_TIMEOUT_MS ? timeout_ms : MINIMUM_LOGCAT_TIMEOUT_MS;
}
+Dumpstate::ConsentCallback::ConsentCallback() : result_(UNAVAILABLE), start_time_(Nanotime()) {
+}
+
+android::binder::Status Dumpstate::ConsentCallback::onReportApproved() {
+ std::lock_guard<std::mutex> lock(lock_);
+ result_ = APPROVED;
+ MYLOGD("User approved consent to share bugreport\n");
+ return android::binder::Status::ok();
+}
+
+android::binder::Status Dumpstate::ConsentCallback::onReportDenied() {
+ std::lock_guard<std::mutex> lock(lock_);
+ result_ = DENIED;
+ MYLOGW("User denied consent to share bugreport\n");
+ return android::binder::Status::ok();
+}
+
+UserConsentResult Dumpstate::ConsentCallback::getResult() {
+ std::lock_guard<std::mutex> lock(lock_);
+ return result_;
+}
+
+uint64_t Dumpstate::ConsentCallback::getElapsedTimeMs() const {
+ return Nanotime() - start_time_;
+}
+
void Dumpstate::PrintHeader() const {
std::string build, fingerprint, radio, bootloader, network;
char date[80];
@@ -1886,14 +1924,6 @@
ds.path_ = new_path;
}
}
- // The zip file lives in an internal directory. Copy it over to output.
- if (ds.options_->bugreport_fd.get() != -1) {
- bool copy_succeeded =
- android::os::CopyFileToFd(ds.path_, ds.options_->bugreport_fd.get());
- if (!copy_succeeded && remove(ds.path_.c_str())) {
- MYLOGE("remove(%s): %s", ds.path_.c_str(), strerror(errno));
- }
- } // else - the file just remains in the internal directory.
}
}
if (do_text_file) {
@@ -2186,8 +2216,8 @@
options_ = std::move(options);
}
-Dumpstate::RunStatus Dumpstate::Run() {
- Dumpstate::RunStatus status = RunInternal();
+Dumpstate::RunStatus Dumpstate::Run(int32_t calling_uid, const std::string& calling_package) {
+ Dumpstate::RunStatus status = RunInternal(calling_uid, calling_package);
if (listener_ != nullptr) {
switch (status) {
case Dumpstate::RunStatus::OK:
@@ -2196,10 +2226,16 @@
case Dumpstate::RunStatus::HELP:
break;
case Dumpstate::RunStatus::INVALID_INPUT:
- listener_->onError(android::os::IDumpstateListener::BUGREPORT_ERROR_INVALID_INPUT);
+ listener_->onError(IDumpstateListener::BUGREPORT_ERROR_INVALID_INPUT);
break;
case Dumpstate::RunStatus::ERROR:
- listener_->onError(android::os::IDumpstateListener::BUGREPORT_ERROR_RUNTIME_ERROR);
+ listener_->onError(IDumpstateListener::BUGREPORT_ERROR_RUNTIME_ERROR);
+ break;
+ case Dumpstate::RunStatus::USER_CONSENT_DENIED:
+ listener_->onError(IDumpstateListener::BUGREPORT_ERROR_USER_DENIED_CONSENT);
+ break;
+ case Dumpstate::RunStatus::USER_CONSENT_TIMED_OUT:
+ listener_->onError(IDumpstateListener::BUGREPORT_ERROR_USER_CONSENT_TIMED_OUT);
break;
}
}
@@ -2227,7 +2263,8 @@
* Bugreports are first generated in a local directory and later copied to the caller's fd if
* supplied.
*/
-Dumpstate::RunStatus Dumpstate::RunInternal() {
+Dumpstate::RunStatus Dumpstate::RunInternal(int32_t calling_uid,
+ const std::string& calling_package) {
LogDumpOptions(*options_);
if (!options_->ValidateOptions()) {
MYLOGE("Invalid options specified\n");
@@ -2265,6 +2302,12 @@
return RunStatus::OK;
}
+ if (options_->bugreport_fd.get() != -1) {
+ // If the output needs to be copied over to the caller's fd, get user consent.
+ android::String16 package(calling_package.c_str());
+ CheckUserConsent(calling_uid, package);
+ }
+
// Redirect output if needed
bool is_redirecting = options_->OutputToFile();
@@ -2416,11 +2459,24 @@
TEMP_FAILURE_RETRY(dup2(dup_stdout_fd, fileno(stdout)));
}
- /* rename or zip the (now complete) .tmp file to its final location */
+ // Rename, and/or zip the (now complete) .tmp file within the internal directory.
if (options_->OutputToFile()) {
FinalizeFile();
}
+ // Share the final file with the caller if the user has consented.
+ Dumpstate::RunStatus status = Dumpstate::RunStatus::OK;
+ if (options_->bugreport_fd.get() != -1) {
+ status = CopyBugreportIfUserConsented();
+ if (status != Dumpstate::RunStatus::OK &&
+ status != Dumpstate::RunStatus::USER_CONSENT_TIMED_OUT) {
+ // Do an early return if there were errors. We make an exception for consent
+ // timing out because it's possible the user got distracted. In this case the
+ // bugreport is not shared but made available for manual retrieval.
+ return status;
+ }
+ }
+
/* vibrate a few but shortly times to let user know it's finished */
if (options_->do_vibrate) {
for (int i = 0; i < 3; i++) {
@@ -2452,7 +2508,73 @@
tombstone_data_.clear();
anr_data_.clear();
- return RunStatus::OK;
+ return (consent_callback_ != nullptr &&
+ consent_callback_->getResult() == UserConsentResult::UNAVAILABLE)
+ ? USER_CONSENT_TIMED_OUT
+ : RunStatus::OK;
+}
+
+void Dumpstate::CheckUserConsent(int32_t calling_uid, const android::String16& calling_package) {
+ consent_callback_ = new ConsentCallback();
+ const String16 incidentcompanion("incidentcompanion");
+ sp<android::IBinder> ics(defaultServiceManager()->getService(incidentcompanion));
+ if (ics != nullptr) {
+ MYLOGD("Checking user consent via incidentcompanion service\n");
+ android::interface_cast<android::os::IIncidentCompanion>(ics)->authorizeReport(
+ calling_uid, calling_package, 0x1 /* FLAG_CONFIRMATION_DIALOG */,
+ consent_callback_.get());
+ } else {
+ MYLOGD("Unable to check user consent; incidentcompanion service unavailable\n");
+ }
+}
+
+void Dumpstate::CleanupFiles() {
+ android::os::UnlinkAndLogOnError(tmp_path_);
+ android::os::UnlinkAndLogOnError(screenshot_path_);
+ android::os::UnlinkAndLogOnError(path_);
+}
+
+Dumpstate::RunStatus Dumpstate::HandleUserConsentDenied() {
+ MYLOGD("User denied consent; deleting files and returning\n");
+ CleanupFiles();
+ return USER_CONSENT_DENIED;
+}
+
+Dumpstate::RunStatus Dumpstate::CopyBugreportIfUserConsented() {
+ // If the caller has asked to copy the bugreport over to their directory, we need explicit
+ // user consent.
+ UserConsentResult consent_result = consent_callback_->getResult();
+ if (consent_result == UserConsentResult::UNAVAILABLE) {
+ // User has not responded yet.
+ uint64_t elapsed_ms = consent_callback_->getElapsedTimeMs();
+ if (elapsed_ms < USER_CONSENT_TIMEOUT_MS) {
+ uint delay_seconds = (USER_CONSENT_TIMEOUT_MS - elapsed_ms) / 1000;
+ MYLOGD("Did not receive user consent yet; going to wait for %d seconds", delay_seconds);
+ sleep(delay_seconds);
+ }
+ consent_result = consent_callback_->getResult();
+ }
+ if (consent_result == UserConsentResult::DENIED) {
+ // User has explicitly denied sharing with the app. To be safe delete the
+ // internal bugreport & tmp files.
+ return HandleUserConsentDenied();
+ }
+ if (consent_result == UserConsentResult::APPROVED) {
+ bool copy_succeeded = android::os::CopyFileToFd(ds.path_, ds.options_->bugreport_fd.get());
+ if (copy_succeeded && remove(ds.path_.c_str())) {
+ MYLOGE("remove(%s): %s", ds.path_.c_str(), strerror(errno));
+ }
+ return copy_succeeded ? Dumpstate::RunStatus::OK : Dumpstate::RunStatus::ERROR;
+ } else if (consent_result == UserConsentResult::UNAVAILABLE) {
+ // consent_result is still UNAVAILABLE. The user has likely not responded yet.
+ // Since we do not have user consent to share the bugreport it does not get
+ // copied over to the calling app but remains in the internal directory from
+ // where the user can manually pull it.
+ return Dumpstate::RunStatus::USER_CONSENT_TIMED_OUT;
+ }
+ // Unknown result; must be a programming error.
+ MYLOGE("Unknown user consent result:%d\n", consent_result);
+ return Dumpstate::RunStatus::ERROR;
}
/* Main entry point for dumpstate binary. */
@@ -2461,7 +2583,14 @@
Dumpstate::RunStatus status = options->Initialize(argc, argv);
if (status == Dumpstate::RunStatus::OK) {
ds.SetOptions(std::move(options));
- status = ds.Run();
+ // When directly running dumpstate binary, the output is not expected to be written
+ // to any external file descriptor.
+ assert(ds.options_->bugreport_fd.get() == -1);
+
+ // calling_uid and calling_package are for user consent to share the bugreport with
+ // an app; they are irrelvant here because bugreport is only written to a local
+ // directory, and not shared.
+ status = ds.Run(-1 /* calling_uid */, "" /* calling_package */);
}
switch (status) {
@@ -2475,9 +2604,10 @@
ShowUsage();
exit(1);
case Dumpstate::RunStatus::ERROR:
- exit(2);
- default:
- fprintf(stderr, "Unknown status: %d\n", status);
+ FALLTHROUGH_INTENDED;
+ case Dumpstate::RunStatus::USER_CONSENT_DENIED:
+ FALLTHROUGH_INTENDED;
+ case Dumpstate::RunStatus::USER_CONSENT_TIMED_OUT:
exit(2);
}
}