diff --git a/api/current.txt b/api/current.txt index c641a8c65ece8..f6132d8844583 100644 --- a/api/current.txt +++ b/api/current.txt @@ -33208,6 +33208,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(); @@ -33611,6 +33612,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 213260fa91626..42c637a83e5d2 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 ea76c9a31bc12..a76c49fbfcb48 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; @@ -859,6 +863,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 @@ -993,6 +1000,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 @@ -1154,6 +1183,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)) { @@ -1741,6 +1780,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 @@ -1907,6 +1974,8 @@ public final class StrictMode { VMRuntime.setNonSdkApiUsageConsumer(null); VMRuntime.setDedupeHiddenApiWarnings(true); } + + setBlockGuardVmPolicy(sVmPolicy.mask); } } @@ -1969,6 +2038,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)); @@ -2046,6 +2120,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 sLastVmViolationTime = new HashMap<>(); diff --git a/core/java/android/os/storage/StorageManager.java b/core/java/android/os/storage/StorageManager.java index aeced951f3825..8b8ae1cbbc142 100644 --- a/core/java/android/os/storage/StorageManager.java +++ b/core/java/android/os/storage/StorageManager.java @@ -71,6 +71,8 @@ import com.android.internal.os.RoSystemProperties; import com.android.internal.os.SomeArgs; import com.android.internal.util.Preconditions; +import dalvik.system.BlockGuard; + import java.io.File; import java.io.FileDescriptor; import java.io.FileNotFoundException; @@ -1131,6 +1133,7 @@ public class StorageManager { /** {@hide} */ public void mkdirs(File file) { + BlockGuard.getVmPolicy().onPathAccess(file.getAbsolutePath()); try { mStorageManager.mkdirs(mContext.getOpPackageName(), file.getAbsolutePath()); } catch (RemoteException e) { diff --git a/core/java/android/os/strictmode/CredentialProtectedWhileLockedViolation.java b/core/java/android/os/strictmode/CredentialProtectedWhileLockedViolation.java new file mode 100644 index 0000000000000..12503f650ab58 --- /dev/null +++ b/core/java/android/os/strictmode/CredentialProtectedWhileLockedViolation.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2018 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. + */ + +package android.os.strictmode; + +import android.content.Context; + +/** + * Subclass of {@code Violation} that is used when a process accesses 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 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);