Define upper-bound disk quotas for all apps.
Abusive or broken apps can go crazy and try allocating all of the
disk space on the device. To mitigate the impact on system health,
set hard limits to block any given app from using more than 90% of
disk blocks, or 50% of disk inodes.
Also define the hard limit for AID_MEDIA_RW to avoid filling up the
device via the SD card.
Kick QUOTAON when scanning devices, since ext4 doesn't toggle
DQUOT_LIMITS_ENABLED during initial mount.
Test: cts-tradefed run commandAndExit cts-dev -m CtsAppSecurityHostTestCases -t android.appsecurity.cts.StorageHostTest
Test: cts-tradefed run commandAndExit cts-dev -m CtsOsTestCases -t android.os.cts.EnvironmentTest#testSaneInodes
Bug: 36450358
Change-Id: Iaa2bc6a2f0bc9047ee54c1d1a49bbda92142457a
diff --git a/cmds/installd/InstalldNativeService.cpp b/cmds/installd/InstalldNativeService.cpp
index 381e90c..c604ca0 100644
--- a/cmds/installd/InstalldNativeService.cpp
+++ b/cmds/installd/InstalldNativeService.cpp
@@ -289,6 +289,46 @@
return 0;
}
+/**
+ * Ensure that we have a hard-limit quota to protect against abusive apps;
+ * they should never use more than 90% of blocks or 50% of inodes.
+ */
+static int prepare_app_quota(const std::unique_ptr<std::string>& uuid, const std::string& device,
+ uid_t uid) {
+ if (device.empty()) return 0;
+
+ struct dqblk dq;
+ if (quotactl(QCMD(Q_GETQUOTA, USRQUOTA), device.c_str(), uid,
+ reinterpret_cast<char*>(&dq)) != 0) {
+ PLOG(WARNING) << "Failed to find quota for " << uid;
+ return -1;
+ }
+
+ if ((dq.dqb_bhardlimit == 0) || (dq.dqb_ihardlimit == 0)) {
+ auto path = create_data_path(uuid ? uuid->c_str() : nullptr);
+ struct statvfs stat;
+ if (statvfs(path.c_str(), &stat) != 0) {
+ PLOG(WARNING) << "Failed to statvfs " << path;
+ return -1;
+ }
+
+ dq.dqb_valid = QIF_LIMITS;
+ dq.dqb_bhardlimit = (((stat.f_blocks * stat.f_frsize) / 10) * 9) / QIF_DQBLKSIZE;
+ dq.dqb_ihardlimit = (stat.f_files / 2);
+ if (quotactl(QCMD(Q_SETQUOTA, USRQUOTA), device.c_str(), uid,
+ reinterpret_cast<char*>(&dq)) != 0) {
+ PLOG(WARNING) << "Failed to set hard quota for " << uid;
+ return -1;
+ } else {
+ LOG(DEBUG) << "Applied hard quotas for " << uid;
+ return 0;
+ }
+ } else {
+ // Hard quota already set; assume it's reasonable
+ return 0;
+ }
+}
+
binder::Status InstalldNativeService::createAppData(const std::unique_ptr<std::string>& uuid,
const std::string& packageName, int32_t userId, int32_t flags, int32_t appId,
const std::string& seInfo, int32_t targetSdkVersion, int64_t* _aidl_return) {
@@ -358,6 +398,10 @@
return error("Failed to restorecon " + path);
}
+ if (prepare_app_quota(uuid, findQuotaDeviceForUuid(uuid), uid)) {
+ return error("Failed to set hard quota " + path);
+ }
+
if (property_get_bool("dalvik.vm.usejitprofiles", false)) {
const std::string profile_dir =
create_primary_current_profile_package_dir_path(userId, pkgname);
@@ -709,6 +753,14 @@
}
}
}
+
+ // Data under /data/media doesn't have an app, but we still want
+ // to limit it to prevent abuse.
+ if (prepare_app_quota(uuid, findQuotaDeviceForUuid(uuid),
+ multiuser_get_uid(userId, AID_MEDIA_RW))) {
+ return error("Failed to set hard quota for media_rw");
+ }
+
return ok();
}
@@ -2006,6 +2058,17 @@
reinterpret_cast<char*>(&dq)) == 0) {
LOG(DEBUG) << "Found " << source << " with quota";
mQuotaDevices[target] = source;
+
+ // ext4 only enables DQUOT_USAGE_ENABLED by default, so we
+ // need to kick it again to enable DQUOT_LIMITS_ENABLED.
+ if (quotactl(QCMD(Q_QUOTAON, USRQUOTA), source.c_str(), QFMT_VFS_V1, nullptr) != 0
+ && errno != EBUSY) {
+ PLOG(ERROR) << "Failed to enable USRQUOTA on " << source;
+ }
+ if (quotactl(QCMD(Q_QUOTAON, GRPQUOTA), source.c_str(), QFMT_VFS_V1, nullptr) != 0
+ && errno != EBUSY) {
+ PLOG(ERROR) << "Failed to enable GRPQUOTA on " << source;
+ }
}
}
}