diff --git a/api/current.txt b/api/current.txt index c1846fc2e9603..8892b247f31cc 100644 --- a/api/current.txt +++ b/api/current.txt @@ -33210,6 +33210,7 @@ package android.os { method public android.os.StrictMode.VmPolicy.Builder detectAll(); method public android.os.StrictMode.VmPolicy.Builder detectCleartextNetwork(); method public android.os.StrictMode.VmPolicy.Builder detectContentUriWithoutPermission(); + method public android.os.StrictMode.VmPolicy.Builder detectCredentialProtectedWhileLocked(); method public android.os.StrictMode.VmPolicy.Builder detectFileUriExposure(); method public android.os.StrictMode.VmPolicy.Builder detectImplicitDirectBoot(); method public android.os.StrictMode.VmPolicy.Builder detectLeakedClosableObjects(); @@ -33613,6 +33614,9 @@ package android.os.strictmode { public final class ContentUriWithoutPermissionViolation extends android.os.strictmode.Violation { } + public final class CredentialProtectedWhileLockedViolation extends android.os.strictmode.Violation { + } + public final class CustomViolation extends android.os.strictmode.Violation { } diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java index 82088dc569e74..2eafb32d088f3 100644 --- a/core/java/android/app/ContextImpl.java +++ b/core/java/android/app/ContextImpl.java @@ -80,6 +80,8 @@ import android.view.autofill.AutofillManager.AutofillClient; import com.android.internal.annotations.GuardedBy; import com.android.internal.util.Preconditions; +import dalvik.system.BlockGuard; + import libcore.io.Memory; import java.io.File; @@ -2521,7 +2523,11 @@ class ContextImpl extends Context { private File makeFilename(File base, String name) { if (name.indexOf(File.separatorChar) < 0) { - return new File(base, name); + final File res = new File(base, name); + // We report as filesystem access here to give us the best shot at + // detecting apps that will pass the path down to native code. + BlockGuard.getVmPolicy().onPathAccess(res.getPath()); + return res; } throw new IllegalArgumentException( "File " + name + " contains a path separator"); diff --git a/core/java/android/os/Environment.java b/core/java/android/os/Environment.java index bdd6cfe35fd38..e32ed9deb270f 100644 --- a/core/java/android/os/Environment.java +++ b/core/java/android/os/Environment.java @@ -33,6 +33,8 @@ import java.util.LinkedList; public class Environment { private static final String TAG = "Environment"; + // NOTE: keep credential-protected paths in sync with StrictMode.java + private static final String ENV_EXTERNAL_STORAGE = "EXTERNAL_STORAGE"; private static final String ENV_ANDROID_ROOT = "ANDROID_ROOT"; private static final String ENV_ANDROID_DATA = "ANDROID_DATA"; diff --git a/core/java/android/os/StrictMode.java b/core/java/android/os/StrictMode.java index 1ff8c6818ddf6..5ff6e558736ea 100644 --- a/core/java/android/os/StrictMode.java +++ b/core/java/android/os/StrictMode.java @@ -31,8 +31,10 @@ import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.net.TrafficStats; import android.net.Uri; +import android.os.storage.IStorageManager; import android.os.strictmode.CleartextNetworkViolation; import android.os.strictmode.ContentUriWithoutPermissionViolation; +import android.os.strictmode.CredentialProtectedWhileLockedViolation; import android.os.strictmode.CustomViolation; import android.os.strictmode.DiskReadViolation; import android.os.strictmode.DiskWriteViolation; @@ -284,6 +286,8 @@ public final class StrictMode { private static final int DETECT_VM_NON_SDK_API_USAGE = 1 << 9; /** @hide */ private static final int DETECT_VM_IMPLICIT_DIRECT_BOOT = 1 << 10; + /** @hide */ + private static final int DETECT_VM_CREDENTIAL_PROTECTED_WHILE_LOCKED = 1 << 11; /** @hide */ private static final int DETECT_VM_ALL = 0x0000ffff; @@ -860,6 +864,9 @@ public final class StrictMode { detectContentUriWithoutPermission(); detectUntaggedSockets(); } + if (targetSdk >= Build.VERSION_CODES.Q) { + detectCredentialProtectedWhileLocked(); + } // TODO: Decide whether to detect non SDK API usage beyond a certain API level. // TODO: enable detectImplicitDirectBoot() once system is less noisy @@ -994,6 +1001,28 @@ public final class StrictMode { return disable(DETECT_VM_IMPLICIT_DIRECT_BOOT); } + /** + * Detect access to filesystem paths stored in credential protected + * storage areas while the user is locked. + *
+ * When a user is locked, credential protected storage is
+ * unavailable, and files stored in these locations appear to not
+ * exist, which can result in subtle app bugs if they assume default
+ * behaviors or empty states. Instead, apps should store data needed
+ * while a user is locked under device protected storage areas.
+ *
+ * @see Context#createCredentialProtectedStorageContext()
+ * @see Context#createDeviceProtectedStorageContext()
+ */
+ public Builder detectCredentialProtectedWhileLocked() {
+ return enable(DETECT_VM_CREDENTIAL_PROTECTED_WHILE_LOCKED);
+ }
+
+ /** @hide */
+ public Builder permitCredentialProtectedWhileLocked() {
+ return disable(DETECT_VM_CREDENTIAL_PROTECTED_WHILE_LOCKED);
+ }
+
/**
* Crashes the whole process on violation. This penalty runs at the end of all enabled
* penalties so you'll still get your logging or other violations before the process
@@ -1155,6 +1184,16 @@ public final class StrictMode {
androidPolicy.setThreadPolicyMask(threadPolicyMask);
}
+ private static void setBlockGuardVmPolicy(@VmPolicyMask int vmPolicyMask) {
+ // We only need to install BlockGuard for a small subset of VM policies
+ vmPolicyMask &= DETECT_VM_CREDENTIAL_PROTECTED_WHILE_LOCKED;
+ if (vmPolicyMask != 0) {
+ BlockGuard.setVmPolicy(VM_ANDROID_POLICY);
+ } else {
+ BlockGuard.setVmPolicy(BlockGuard.LAX_VM_POLICY);
+ }
+ }
+
// Sets up CloseGuard in Dalvik/libcore
private static void setCloseGuardEnabled(boolean enabled) {
if (!(CloseGuard.getReporter() instanceof AndroidCloseGuardReporter)) {
@@ -1742,6 +1781,34 @@ public final class StrictMode {
}
}
+ private static final BlockGuard.VmPolicy VM_ANDROID_POLICY = new BlockGuard.VmPolicy() {
+ @Override
+ public void onPathAccess(String path) {
+ if (path == null) return;
+
+ // NOTE: keep credential-protected paths in sync with Environment.java
+ if (path.startsWith("/data/user/")
+ || path.startsWith("/data/media/")
+ || path.startsWith("/data/system_ce/")
+ || path.startsWith("/data/misc_ce/")
+ || path.startsWith("/data/vendor_ce/")
+ || path.startsWith("/storage/emulated/")) {
+ final int second = path.indexOf('/', 1);
+ final int third = path.indexOf('/', second + 1);
+ final int fourth = path.indexOf('/', third + 1);
+ if (fourth == -1) return;
+
+ try {
+ final int userId = Integer.parseInt(path.substring(third + 1, fourth));
+ onCredentialProtectedPathAccess(path, userId);
+ } catch (NumberFormatException ignored) {
+ }
+ } else if (path.startsWith("/data/data/")) {
+ onCredentialProtectedPathAccess(path, UserHandle.USER_SYSTEM);
+ }
+ }
+ };
+
/**
* In the common case, as set by conditionallyEnableDebugLogging, we're just dropboxing any
* violations but not showing a dialog, not loggging, and not killing the process. In these
@@ -1909,6 +1976,8 @@ public final class StrictMode {
VMRuntime.setNonSdkApiUsageConsumer(null);
VMRuntime.setDedupeHiddenApiWarnings(true);
}
+
+ setBlockGuardVmPolicy(sVmPolicy.mask);
}
}
@@ -1971,6 +2040,11 @@ public final class StrictMode {
return (sVmPolicy.mask & DETECT_VM_IMPLICIT_DIRECT_BOOT) != 0;
}
+ /** @hide */
+ public static boolean vmCredentialProtectedWhileLockedEnabled() {
+ return (sVmPolicy.mask & DETECT_VM_CREDENTIAL_PROTECTED_WHILE_LOCKED) != 0;
+ }
+
/** @hide */
public static void onSqliteObjectLeaked(String message, Throwable originStack) {
onVmPolicyViolation(new SqliteObjectLeakedViolation(message, originStack));
@@ -2044,6 +2118,42 @@ public final class StrictMode {
onVmPolicyViolation(new ImplicitDirectBootViolation());
}
+ /** Assume locked until we hear otherwise */
+ private static volatile boolean sUserKeyUnlocked = false;
+
+ private static boolean isUserKeyUnlocked(int userId) {
+ final IStorageManager storage = IStorageManager.Stub
+ .asInterface(ServiceManager.getService("mount"));
+ if (storage != null) {
+ try {
+ return storage.isUserKeyUnlocked(userId);
+ } catch (RemoteException ignored) {
+ }
+ }
+ return false;
+ }
+
+ /** @hide */
+ private static void onCredentialProtectedPathAccess(String path, int userId) {
+ // We can cache the unlocked state for the userId we're running as,
+ // since any relocking of that user will always result in our
+ // process being killed to release any CE FDs we're holding onto.
+ if (userId == UserHandle.myUserId()) {
+ if (sUserKeyUnlocked) {
+ return;
+ } else if (isUserKeyUnlocked(userId)) {
+ sUserKeyUnlocked = true;
+ return;
+ }
+ } else if (isUserKeyUnlocked(userId)) {
+ return;
+ }
+
+ onVmPolicyViolation(new CredentialProtectedWhileLockedViolation(
+ "Accessed credential protected path " + path + " while user " + userId
+ + " was locked"));
+ }
+
// Map from VM violation fingerprint to uptime millis.
private static final HashMap
+ * When a user is locked, credential protected storage is unavailable, and files
+ * stored in these locations appear to not exist, which can result in subtle app
+ * bugs if they assume default behaviors or empty states. Instead, apps should
+ * store data needed while a user is locked under device protected storage
+ * areas.
+ *
+ * @see Context#createCredentialProtectedStorageContext()
+ * @see Context#createDeviceProtectedStorageContext()
+ */
+public final class CredentialProtectedWhileLockedViolation extends Violation {
+ /** @hide */
+ public CredentialProtectedWhileLockedViolation(String message) {
+ super(message);
+ }
+}
diff --git a/services/core/java/com/android/server/pm/Installer.java b/services/core/java/com/android/server/pm/Installer.java
index f0807b9b36d33..72f11f78d71dd 100644
--- a/services/core/java/com/android/server/pm/Installer.java
+++ b/services/core/java/com/android/server/pm/Installer.java
@@ -34,6 +34,7 @@ import android.util.Slog;
import com.android.internal.os.BackgroundThread;
import com.android.server.SystemService;
+import dalvik.system.BlockGuard;
import dalvik.system.VMRuntime;
import java.io.FileDescriptor;
@@ -239,6 +240,11 @@ public class Installer extends SystemService {
long[] ceDataInodes, String[] codePaths, PackageStats stats)
throws InstallerException {
if (!checkBeforeRemote()) return;
+ if (codePaths != null) {
+ for (String codePath : codePaths) {
+ BlockGuard.getVmPolicy().onPathAccess(codePath);
+ }
+ }
try {
final long[] res = mInstalld.getAppSize(uuid, packageNames, userId, flags,
appId, ceDataInodes, codePaths);
@@ -296,6 +302,9 @@ public class Installer extends SystemService {
@Nullable String profileName, @Nullable String dexMetadataPath,
@Nullable String compilationReason) throws InstallerException {
assertValidInstructionSet(instructionSet);
+ BlockGuard.getVmPolicy().onPathAccess(apkPath);
+ BlockGuard.getVmPolicy().onPathAccess(outputPath);
+ BlockGuard.getVmPolicy().onPathAccess(dexMetadataPath);
if (!checkBeforeRemote()) return;
try {
mInstalld.dexopt(apkPath, uid, pkgName, instructionSet, dexoptNeeded, outputPath,
@@ -319,6 +328,7 @@ public class Installer extends SystemService {
public boolean dumpProfiles(int uid, String packageName, String profileName, String codePath)
throws InstallerException {
if (!checkBeforeRemote()) return false;
+ BlockGuard.getVmPolicy().onPathAccess(codePath);
try {
return mInstalld.dumpProfiles(uid, packageName, profileName, codePath);
} catch (Exception e) {
@@ -339,6 +349,8 @@ public class Installer extends SystemService {
public void idmap(String targetApkPath, String overlayApkPath, int uid)
throws InstallerException {
if (!checkBeforeRemote()) return;
+ BlockGuard.getVmPolicy().onPathAccess(targetApkPath);
+ BlockGuard.getVmPolicy().onPathAccess(overlayApkPath);
try {
mInstalld.idmap(targetApkPath, overlayApkPath, uid);
} catch (Exception e) {
@@ -348,6 +360,7 @@ public class Installer extends SystemService {
public void removeIdmap(String overlayApkPath) throws InstallerException {
if (!checkBeforeRemote()) return;
+ BlockGuard.getVmPolicy().onPathAccess(overlayApkPath);
try {
mInstalld.removeIdmap(overlayApkPath);
} catch (Exception e) {
@@ -358,6 +371,7 @@ public class Installer extends SystemService {
public void rmdex(String codePath, String instructionSet) throws InstallerException {
assertValidInstructionSet(instructionSet);
if (!checkBeforeRemote()) return;
+ BlockGuard.getVmPolicy().onPathAccess(codePath);
try {
mInstalld.rmdex(codePath, instructionSet);
} catch (Exception e) {
@@ -367,6 +381,7 @@ public class Installer extends SystemService {
public void rmPackageDir(String packageDir) throws InstallerException {
if (!checkBeforeRemote()) return;
+ BlockGuard.getVmPolicy().onPathAccess(packageDir);
try {
mInstalld.rmPackageDir(packageDir);
} catch (Exception e) {
@@ -439,6 +454,7 @@ public class Installer extends SystemService {
public void linkNativeLibraryDirectory(String uuid, String packageName, String nativeLibPath32,
int userId) throws InstallerException {
if (!checkBeforeRemote()) return;
+ BlockGuard.getVmPolicy().onPathAccess(nativeLibPath32);
try {
mInstalld.linkNativeLibraryDirectory(uuid, packageName, nativeLibPath32, userId);
} catch (Exception e) {
@@ -459,6 +475,8 @@ public class Installer extends SystemService {
public void linkFile(String relativePath, String fromBase, String toBase)
throws InstallerException {
if (!checkBeforeRemote()) return;
+ BlockGuard.getVmPolicy().onPathAccess(fromBase);
+ BlockGuard.getVmPolicy().onPathAccess(toBase);
try {
mInstalld.linkFile(relativePath, fromBase, toBase);
} catch (Exception e) {
@@ -469,6 +487,8 @@ public class Installer extends SystemService {
public void moveAb(String apkPath, String instructionSet, String outputPath)
throws InstallerException {
if (!checkBeforeRemote()) return;
+ BlockGuard.getVmPolicy().onPathAccess(apkPath);
+ BlockGuard.getVmPolicy().onPathAccess(outputPath);
try {
mInstalld.moveAb(apkPath, instructionSet, outputPath);
} catch (Exception e) {
@@ -479,6 +499,8 @@ public class Installer extends SystemService {
public void deleteOdex(String apkPath, String instructionSet, String outputPath)
throws InstallerException {
if (!checkBeforeRemote()) return;
+ BlockGuard.getVmPolicy().onPathAccess(apkPath);
+ BlockGuard.getVmPolicy().onPathAccess(outputPath);
try {
mInstalld.deleteOdex(apkPath, instructionSet, outputPath);
} catch (Exception e) {
@@ -489,6 +511,7 @@ public class Installer extends SystemService {
public void installApkVerity(String filePath, FileDescriptor verityInput, int contentSize)
throws InstallerException {
if (!checkBeforeRemote()) return;
+ BlockGuard.getVmPolicy().onPathAccess(filePath);
try {
mInstalld.installApkVerity(filePath, verityInput, contentSize);
} catch (Exception e) {
@@ -499,6 +522,7 @@ public class Installer extends SystemService {
public void assertFsverityRootHashMatches(String filePath, @NonNull byte[] expectedHash)
throws InstallerException {
if (!checkBeforeRemote()) return;
+ BlockGuard.getVmPolicy().onPathAccess(filePath);
try {
mInstalld.assertFsverityRootHashMatches(filePath, expectedHash);
} catch (Exception e) {
@@ -512,6 +536,7 @@ public class Installer extends SystemService {
assertValidInstructionSet(isas[i]);
}
if (!checkBeforeRemote()) return false;
+ BlockGuard.getVmPolicy().onPathAccess(apkPath);
try {
return mInstalld.reconcileSecondaryDexFile(apkPath, packageName, uid, isas,
volumeUuid, flags);
@@ -523,6 +548,7 @@ public class Installer extends SystemService {
public byte[] hashSecondaryDexFile(String dexPath, String packageName, int uid,
@Nullable String volumeUuid, int flags) throws InstallerException {
if (!checkBeforeRemote()) return new byte[0];
+ BlockGuard.getVmPolicy().onPathAccess(dexPath);
try {
return mInstalld.hashSecondaryDexFile(dexPath, packageName, uid, volumeUuid, flags);
} catch (Exception e) {
@@ -571,6 +597,8 @@ public class Installer extends SystemService {
public boolean prepareAppProfile(String pkg, @UserIdInt int userId, @AppIdInt int appId,
String profileName, String codePath, String dexMetadataPath) throws InstallerException {
if (!checkBeforeRemote()) return false;
+ BlockGuard.getVmPolicy().onPathAccess(codePath);
+ BlockGuard.getVmPolicy().onPathAccess(dexMetadataPath);
try {
return mInstalld.prepareAppProfile(pkg, userId, appId, profileName, codePath,
dexMetadataPath);