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/DumpstateService.cpp b/cmds/dumpstate/DumpstateService.cpp
index eb0d50d..b13478c 100644
--- a/cmds/dumpstate/DumpstateService.cpp
+++ b/cmds/dumpstate/DumpstateService.cpp
@@ -31,6 +31,13 @@
namespace {
+struct DumpstateInfo {
+ public:
+ Dumpstate* ds = nullptr;
+ int32_t calling_uid = -1;
+ std::string calling_package;
+};
+
static binder::Status exception(uint32_t code, const std::string& msg) {
MYLOGE("%s (%d) ", msg.c_str(), code);
return binder::Status::fromExceptionCode(code, String8(msg.c_str()));
@@ -42,14 +49,15 @@
}
static void* callAndNotify(void* data) {
- Dumpstate& ds = *static_cast<Dumpstate*>(data);
- ds.Run();
+ DumpstateInfo& ds_info = *static_cast<DumpstateInfo*>(data);
+ ds_info.ds->Run(ds_info.calling_uid, ds_info.calling_package);
MYLOGD("Finished Run()\n");
return nullptr;
}
class DumpstateToken : public BnDumpstateToken {};
-}
+
+} // namespace
DumpstateService::DumpstateService() : ds_(Dumpstate::GetInstance()) {
}
@@ -98,8 +106,8 @@
}
// TODO(b/111441001): Hook up to consent service & copy final br only if user approves.
-binder::Status DumpstateService::startBugreport(int32_t /* calling_uid */,
- const std::string& /* calling_package */,
+binder::Status DumpstateService::startBugreport(int32_t calling_uid,
+ const std::string& calling_package,
const android::base::unique_fd& bugreport_fd,
const android::base::unique_fd& screenshot_fd,
int bugreport_mode,
@@ -132,6 +140,11 @@
ds_.listener_ = listener;
}
+ DumpstateInfo ds_info;
+ ds_info.ds = &ds_;
+ ds_info.calling_uid = calling_uid;
+ ds_info.calling_package = calling_package;
+
pthread_t thread;
status_t err = pthread_create(&thread, nullptr, callAndNotify, &ds_);
if (err != 0) {
diff --git a/cmds/dumpstate/binder/android/os/IDumpstateListener.aidl b/cmds/dumpstate/binder/android/os/IDumpstateListener.aidl
index 8396acd..907a67c 100644
--- a/cmds/dumpstate/binder/android/os/IDumpstateListener.aidl
+++ b/cmds/dumpstate/binder/android/os/IDumpstateListener.aidl
@@ -38,6 +38,9 @@
/* User denied consent to share the bugreport with the specified app */
const int BUGREPORT_ERROR_USER_DENIED_CONSENT = 3;
+ /* The request to get user consent timed out */
+ const int BUGREPORT_ERROR_USER_CONSENT_TIMED_OUT = 4;
+
/**
* Called on an error condition with one of the error codes listed above.
*/
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);
}
}
diff --git a/cmds/dumpstate/dumpstate.h b/cmds/dumpstate/dumpstate.h
index 3dfe4e9..4766d82 100644
--- a/cmds/dumpstate/dumpstate.h
+++ b/cmds/dumpstate/dumpstate.h
@@ -27,6 +27,7 @@
#include <android-base/macros.h>
#include <android-base/unique_fd.h>
+#include <android/os/BnIncidentAuthListener.h>
#include <android/os/IDumpstate.h>
#include <android/os/IDumpstateListener.h>
#include <utils/StrongPointer.h>
@@ -192,7 +193,7 @@
friend class DumpstateTest;
public:
- enum RunStatus { OK, HELP, INVALID_INPUT, ERROR };
+ enum RunStatus { OK, HELP, INVALID_INPUT, ERROR, USER_CONSENT_DENIED, USER_CONSENT_TIMED_OUT };
// The mode under which the bugreport should be run. Each mode encapsulates a few options.
enum BugreportMode {
@@ -319,7 +320,7 @@
struct DumpOptions;
/* Main entry point for running a complete bugreport. */
- RunStatus Run();
+ RunStatus Run(int32_t calling_uid, const std::string& calling_package);
/* Sets runtime options. */
void SetOptions(std::unique_ptr<DumpOptions> options);
@@ -447,12 +448,47 @@
// List of open ANR dump files.
std::vector<DumpData> anr_data_;
+ // A callback to IncidentCompanion service, which checks user consent for sharing the
+ // bugreport with the calling app. If the user has not responded yet to the dialog it will
+ // be neither confirmed nor denied.
+ class ConsentCallback : public android::os::BnIncidentAuthListener {
+ public:
+ ConsentCallback();
+ android::binder::Status onReportApproved() override;
+ android::binder::Status onReportDenied() override;
+
+ enum ConsentResult { APPROVED, DENIED, UNAVAILABLE };
+
+ ConsentResult getResult();
+
+ // Returns the time since creating this listener
+ uint64_t getElapsedTimeMs() const;
+
+ private:
+ ConsentResult result_;
+ uint64_t start_time_;
+ std::mutex lock_;
+ };
+
private:
- RunStatus RunInternal();
+ RunStatus RunInternal(int32_t calling_uid, const std::string& calling_package);
+
+ void CheckUserConsent(int32_t calling_uid, const android::String16& calling_package);
+
+ // Removes the in progress files output files (tmp file, zip/txt file, screenshot),
+ // but leaves the log file alone.
+ void CleanupFiles();
+
+ RunStatus HandleUserConsentDenied();
+
+ // Copies bugreport artifacts over to the caller's directories provided there is user consent.
+ RunStatus CopyBugreportIfUserConsented();
// Used by GetInstance() only.
explicit Dumpstate(const std::string& version = VERSION_CURRENT);
+ android::sp<ConsentCallback> consent_callback_;
+
DISALLOW_COPY_AND_ASSIGN(Dumpstate);
};