DB Wipe detection
- Create a check file for each database in order to detect
1) an unexpected DB file removal
2) DB wipe caused by a DB corruption.
- Either case, do a WTF to collect information on APR.
- Also print file timestamps in "dumpsys dbinfo". Example:
=====================
Database files in /data/system:
locksettings.db 20480b ctime=2018-10-23T22:48:35Z mtime=2018-10-23T22:48:35Z atime=2018-10-23T18:54:12Z
locksettings.db-wipecheck 0b ctime=2018-10-23T18:54:12Z mtime=2018-10-23T18:54:12Z atime=2018-10-23T18:54:12Z
notification_log.db 45056b ctime=2018-10-23T22:48:08Z mtime=2018-10-23T22:48:08Z atime=2018-10-23T18:54:13Z
:
=====================
Change-Id: I77fbeb0bb635c787aba797412f116475fecbe41c
Fixes: 117886381
Test: manual test
Test 1: corruption
1. Stop CP2 process (adb shell killall android.process.acore)
2. shell 'echo abc > /data/user/0/com.android.providers.contacts/databases/contacts2.db'
3. Launch the contacts app.
Test 2: Unexpected file removal
1. Stop CP2 process (adb shell killall android.process.acore)
2. shell 'rm -f /data/user/0/com.android.providers.contacts/databases/contacts2.db'
3. Launch the contacts app.
In both cases, logcat shows a client side stacktrace and also a WTF. (am_wtf)
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index 9d3c5c6..4756bf4 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -1425,60 +1425,10 @@
PrintWriter pw = new FastPrintWriter(
new FileOutputStream(pfd.getFileDescriptor()));
PrintWriterPrinter printer = new PrintWriterPrinter(pw);
- SQLiteDebug.dump(printer, args);
-
- if (isSystem) {
- dumpDatabaseFileSizes(pw, Environment.getDataSystemDirectory(), true);
- dumpDatabaseFileSizes(pw, Environment.getDataSystemDeDirectory(), true);
- dumpDatabaseFileSizes(pw, Environment.getDataSystemCeDirectory(), true);
- } else {
- Context context = getApplication();
- if (context != null) {
- dumpDatabaseFileSizes(pw,
- getDatabasesDir(context.createDeviceProtectedStorageContext()),
- false);
- dumpDatabaseFileSizes(pw,
- getDatabasesDir(context.createCredentialProtectedStorageContext()),
- false);
- }
- }
+ SQLiteDebug.dump(printer, args, isSystem);
pw.flush();
}
- private void dumpDatabaseFileSizes(PrintWriter pw, File dir, boolean isSystem) {
- final File[] files = dir.listFiles();
- if (files == null || files.length == 0) {
- return;
- }
- Arrays.sort(files, (a, b) -> a.getName().compareTo(b.getName()));
-
- boolean needHeader = true;
- for (File f : files) {
- if (isSystem) {
- // If it's the system server, the directory contains other files too, so
- // filter by file extensions.
- // (If it's an app, just print all files because they may not use *.db
- // extension.)
- final String name = f.getName();
- if (!(name.endsWith(".db") || name.endsWith(".db-wal")
- || name.endsWith(".db-journal"))) {
- continue;
- }
- }
- if (needHeader) {
- pw.println();
- pw.println("Database files in " + dir.getAbsolutePath() + ":");
- needHeader = false;
- }
-
- pw.print(" ");
- pw.print(f.getName());
- pw.print(" ");
- pw.print(f.length());
- pw.println(" bytes");
- }
- }
-
@Override
public void dumpDbInfo(final ParcelFileDescriptor pfd, final String[] args) {
if (mSystemThread) {
diff --git a/core/java/android/content/ContentResolver.java b/core/java/android/content/ContentResolver.java
index 599c2d2..a2a6b9b 100644
--- a/core/java/android/content/ContentResolver.java
+++ b/core/java/android/content/ContentResolver.java
@@ -36,11 +36,9 @@
import android.database.Cursor;
import android.database.IContentObserver;
import android.graphics.Bitmap;
-import android.graphics.Canvas;
import android.graphics.ImageDecoder;
import android.graphics.ImageDecoder.ImageInfo;
import android.graphics.ImageDecoder.Source;
-import android.graphics.PixelFormat;
import android.graphics.Point;
import android.graphics.drawable.Drawable;
import android.net.Uri;
@@ -55,7 +53,6 @@
import android.os.ServiceManager;
import android.os.SystemClock;
import android.os.UserHandle;
-import android.provider.DocumentsContract;
import android.text.TextUtils;
import android.util.EventLog;
import android.util.Log;
@@ -3255,4 +3252,13 @@
}
});
}
+
+ /** {@hide} */
+ public static void onDbCorruption(String tag, String message, Throwable stacktrace) {
+ try {
+ getContentService().onDbCorruption(tag, message, Log.getStackTraceString(stacktrace));
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ }
}
diff --git a/core/java/android/content/IContentService.aidl b/core/java/android/content/IContentService.aidl
index a55dd31..1d02375 100644
--- a/core/java/android/content/IContentService.aidl
+++ b/core/java/android/content/IContentService.aidl
@@ -185,4 +185,6 @@
Bundle getCache(in String packageName, in Uri key, int userId);
void resetTodayStats();
+
+ void onDbCorruption(String tag, String message, String stacktrace);
}
diff --git a/core/java/android/database/DefaultDatabaseErrorHandler.java b/core/java/android/database/DefaultDatabaseErrorHandler.java
index 7fa2b40..cf019e1 100755
--- a/core/java/android/database/DefaultDatabaseErrorHandler.java
+++ b/core/java/android/database/DefaultDatabaseErrorHandler.java
@@ -15,14 +15,14 @@
*/
package android.database;
-import java.io.File;
-import java.util.List;
-
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.util.Log;
import android.util.Pair;
+import java.io.File;
+import java.util.List;
+
/**
* Default class used to define the action to take when database corruption is reported
* by sqlite.
@@ -52,6 +52,7 @@
*/
public void onCorruption(SQLiteDatabase dbObj) {
Log.e(TAG, "Corruption reported by sqlite on database: " + dbObj.getPath());
+ SQLiteDatabase.wipeDetected(dbObj.getPath(), "corruption");
// is the corruption detected even before database could be 'opened'?
if (!dbObj.isOpen()) {
@@ -99,7 +100,7 @@
}
Log.e(TAG, "deleting the database file: " + fileName);
try {
- SQLiteDatabase.deleteDatabase(new File(fileName));
+ SQLiteDatabase.deleteDatabase(new File(fileName), /*removeCheckFile=*/ false);
} catch (Exception e) {
/* print warning and ignore exception */
Log.w(TAG, "delete failed: " + e.getMessage());
diff --git a/core/java/android/database/sqlite/SQLiteConnection.java b/core/java/android/database/sqlite/SQLiteConnection.java
index 5c4f16a..20505ca 100644
--- a/core/java/android/database/sqlite/SQLiteConnection.java
+++ b/core/java/android/database/sqlite/SQLiteConnection.java
@@ -34,6 +34,7 @@
import dalvik.system.CloseGuard;
import java.io.File;
+import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
@@ -414,6 +415,10 @@
final String newLocale = mConfiguration.locale.toString();
nativeRegisterLocalizedCollators(mConnectionPtr, newLocale);
+ if (!mConfiguration.isInMemoryDb()) {
+ checkDatabaseWiped();
+ }
+
// If the database is read-only, we cannot modify the android metadata table
// or existing indexes.
if (mIsReadOnlyConnection) {
@@ -449,6 +454,36 @@
}
}
+ private void checkDatabaseWiped() {
+ if (!SQLiteGlobal.checkDbWipe()) {
+ return;
+ }
+ try {
+ final File checkFile = new File(mConfiguration.path
+ + SQLiteGlobal.WIPE_CHECK_FILE_SUFFIX);
+
+ final boolean hasMetadataTable = executeForLong(
+ "SELECT count(*) FROM sqlite_master"
+ + " WHERE type='table' AND name='android_metadata'", null, null) > 0;
+ final boolean hasCheckFile = checkFile.exists();
+
+ if (!mIsReadOnlyConnection && !hasCheckFile) {
+ // Create the check file, unless it's a readonly connection,
+ // in which case we can't create the metadata table anyway.
+ checkFile.createNewFile();
+ }
+
+ if (!hasMetadataTable && hasCheckFile) {
+ // Bad. The DB is gone unexpectedly.
+ SQLiteDatabase.wipeDetected(mConfiguration.path, "unknown");
+ }
+
+ } catch (RuntimeException | IOException ex) {
+ SQLiteDatabase.wtfAsSystemServer(TAG,
+ "Unexpected exception while checking for wipe", ex);
+ }
+ }
+
// Called by SQLiteConnectionPool only.
void reconfigure(SQLiteDatabaseConfiguration configuration) {
mOnlyAllowReadOnlyOperations = false;
diff --git a/core/java/android/database/sqlite/SQLiteConnectionPool.java b/core/java/android/database/sqlite/SQLiteConnectionPool.java
index 3ee348b..dbc1766 100644
--- a/core/java/android/database/sqlite/SQLiteConnectionPool.java
+++ b/core/java/android/database/sqlite/SQLiteConnectionPool.java
@@ -24,6 +24,7 @@
import android.os.OperationCanceledException;
import android.os.SystemClock;
import android.text.TextUtils;
+import android.util.ArraySet;
import android.util.Log;
import android.util.PrefixPrinter;
import android.util.Printer;
@@ -34,6 +35,7 @@
import dalvik.system.CloseGuard;
import java.io.Closeable;
+import java.io.File;
import java.util.ArrayList;
import java.util.Map;
import java.util.WeakHashMap;
@@ -1105,9 +1107,12 @@
* @param printer The printer to receive the dump, not null.
* @param verbose True to dump more verbose information.
*/
- public void dump(Printer printer, boolean verbose) {
+ public void dump(Printer printer, boolean verbose, ArraySet<String> directories) {
Printer indentedPrinter = PrefixPrinter.create(printer, " ");
synchronized (mLock) {
+ if (directories != null) {
+ directories.add(new File(mConfiguration.path).getParent());
+ }
printer.println("Connection pool for " + mConfiguration.path + ":");
printer.println(" Open: " + mIsOpen);
printer.println(" Max connections: " + mMaxConnectionPoolSize);
diff --git a/core/java/android/database/sqlite/SQLiteDatabase.java b/core/java/android/database/sqlite/SQLiteDatabase.java
index eb5c720..f9c2c3e 100644
--- a/core/java/android/database/sqlite/SQLiteDatabase.java
+++ b/core/java/android/database/sqlite/SQLiteDatabase.java
@@ -22,6 +22,8 @@
import android.annotation.Nullable;
import android.annotation.UnsupportedAppUsage;
import android.app.ActivityManager;
+import android.app.ActivityThread;
+import android.content.ContentResolver;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.DatabaseErrorHandler;
@@ -34,6 +36,7 @@
import android.os.OperationCanceledException;
import android.os.SystemProperties;
import android.text.TextUtils;
+import android.util.ArraySet;
import android.util.EventLog;
import android.util.Log;
import android.util.Pair;
@@ -45,9 +48,14 @@
import java.io.File;
import java.io.FileFilter;
+import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
+import java.nio.file.FileSystems;
+import java.nio.file.Files;
+import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
@@ -808,6 +816,12 @@
* @return True if the database was successfully deleted.
*/
public static boolean deleteDatabase(@NonNull File file) {
+ return deleteDatabase(file, /*removeCheckFile=*/ true);
+ }
+
+
+ /** @hide */
+ public static boolean deleteDatabase(@NonNull File file, boolean removeCheckFile) {
if (file == null) {
throw new IllegalArgumentException("file must not be null");
}
@@ -818,6 +832,9 @@
deleted |= new File(file.getPath() + "-shm").delete();
deleted |= new File(file.getPath() + "-wal").delete();
+ // This file is not a standard SQLite file, so don't update the deleted flag.
+ new File(file.getPath() + SQLiteGlobal.WIPE_CHECK_FILE_SUFFIX).delete();
+
File dir = file.getParentFile();
if (dir != null) {
final String prefix = file.getName() + "-mj";
@@ -2170,21 +2187,61 @@
* Dump detailed information about all open databases in the current process.
* Used by bug report.
*/
- static void dumpAll(Printer printer, boolean verbose) {
+ static void dumpAll(Printer printer, boolean verbose, boolean isSystem) {
+ // Use this ArraySet to collect file paths.
+ final ArraySet<String> directories = new ArraySet<>();
+
for (SQLiteDatabase db : getActiveDatabases()) {
- db.dump(printer, verbose);
+ db.dump(printer, verbose, isSystem, directories);
+ }
+
+ // Dump DB files in the directories.
+ if (directories.size() > 0) {
+ final String[] dirs = directories.toArray(new String[directories.size()]);
+ Arrays.sort(dirs);
+ for (String dir : dirs) {
+ dumpDatabaseDirectory(printer, new File(dir), isSystem);
+ }
}
}
- private void dump(Printer printer, boolean verbose) {
+ private void dump(Printer printer, boolean verbose, boolean isSystem, ArraySet directories) {
synchronized (mLock) {
if (mConnectionPoolLocked != null) {
printer.println("");
- mConnectionPoolLocked.dump(printer, verbose);
+ mConnectionPoolLocked.dump(printer, verbose, directories);
}
}
}
+ private static void dumpDatabaseDirectory(Printer pw, File dir, boolean isSystem) {
+ pw.println("");
+ pw.println("Database files in " + dir.getAbsolutePath() + ":");
+ final File[] files = dir.listFiles();
+ if (files == null || files.length == 0) {
+ pw.println(" [none]");
+ return;
+ }
+ Arrays.sort(files, (a, b) -> a.getName().compareTo(b.getName()));
+
+ for (File f : files) {
+ if (isSystem) {
+ // If called within the system server, the directory contains other files too, so
+ // filter by file extensions.
+ // (If it's an app, just print all files because they may not use *.db
+ // extension.)
+ final String name = f.getName();
+ if (!(name.endsWith(".db") || name.endsWith(".db-wal")
+ || name.endsWith(".db-journal")
+ || name.endsWith(SQLiteGlobal.WIPE_CHECK_FILE_SUFFIX))) {
+ continue;
+ }
+ }
+ pw.println(String.format(" %-40s %7db %s", f.getName(), f.length(),
+ SQLiteDatabase.getFileTimestamps(f.getAbsolutePath())));
+ }
+ }
+
/**
* Returns list of full pathnames of all attached databases including the main database
* by executing 'pragma database_list' on the database.
@@ -2611,7 +2668,7 @@
return this;
}
- /**
+ /**w
* Sets <a href="https://sqlite.org/pragma.html#pragma_synchronous">synchronous mode</a>
* .
* @return
@@ -2646,5 +2703,34 @@
@Retention(RetentionPolicy.SOURCE)
public @interface DatabaseOpenFlags {}
+ /** @hide */
+ public static void wipeDetected(String filename, String reason) {
+ wtfAsSystemServer(TAG, "DB wipe detected:"
+ + " package=" + ActivityThread.currentPackageName()
+ + " reason=" + reason
+ + " file=" + filename
+ + " " + getFileTimestamps(filename)
+ + " checkfile " + getFileTimestamps(filename + SQLiteGlobal.WIPE_CHECK_FILE_SUFFIX),
+ new Throwable("STACKTRACE"));
+ }
+
+ /** @hide */
+ public static String getFileTimestamps(String path) {
+ try {
+ BasicFileAttributes attr = Files.readAttributes(
+ FileSystems.getDefault().getPath(path), BasicFileAttributes.class);
+ return "ctime=" + attr.creationTime()
+ + " mtime=" + attr.lastModifiedTime()
+ + " atime=" + attr.lastAccessTime();
+ } catch (IOException e) {
+ return "[unable to obtain timestamp]";
+ }
+ }
+
+ /** @hide */
+ static void wtfAsSystemServer(String tag, String message, Throwable stacktrace) {
+ Log.e(tag, message, stacktrace);
+ ContentResolver.onDbCorruption(tag, message, stacktrace);
+ }
}
diff --git a/core/java/android/database/sqlite/SQLiteDebug.java b/core/java/android/database/sqlite/SQLiteDebug.java
index 1c66204..f220205 100644
--- a/core/java/android/database/sqlite/SQLiteDebug.java
+++ b/core/java/android/database/sqlite/SQLiteDebug.java
@@ -189,6 +189,11 @@
* @param args Command-line arguments supplied to dumpsys dbinfo
*/
public static void dump(Printer printer, String[] args) {
+ dump(printer, args, false);
+ }
+
+ /** @hide */
+ public static void dump(Printer printer, String[] args, boolean isSystem) {
boolean verbose = false;
for (String arg : args) {
if (arg.equals("-v")) {
@@ -196,6 +201,6 @@
}
}
- SQLiteDatabase.dumpAll(printer, verbose);
+ SQLiteDatabase.dumpAll(printer, verbose, isSystem);
}
}
diff --git a/core/java/android/database/sqlite/SQLiteGlobal.java b/core/java/android/database/sqlite/SQLiteGlobal.java
index 67e5f65..ff286fd 100644
--- a/core/java/android/database/sqlite/SQLiteGlobal.java
+++ b/core/java/android/database/sqlite/SQLiteGlobal.java
@@ -42,6 +42,9 @@
/** @hide */
public static final String SYNC_MODE_FULL = "FULL";
+ /** @hide */
+ static final String WIPE_CHECK_FILE_SUFFIX = "-wipecheck";
+
private static final Object sLock = new Object();
private static int sDefaultPageSize;
@@ -181,4 +184,8 @@
com.android.internal.R.integer.db_wal_truncate_size));
}
+ /** @hide */
+ public static boolean checkDbWipe() {
+ return true;
+ }
}
diff --git a/services/core/java/com/android/server/content/ContentService.java b/services/core/java/com/android/server/content/ContentService.java
index 5698fdf..5ed6263 100644
--- a/services/core/java/com/android/server/content/ContentService.java
+++ b/services/core/java/com/android/server/content/ContentService.java
@@ -1615,6 +1615,15 @@
}
@Override
+ public void onDbCorruption(String tag, String message, String stacktrace) {
+ Slog.e(tag, message);
+ Slog.e(tag, "at " + stacktrace);
+
+ // TODO: Figure out a better way to report it. b/117886381
+ Slog.wtf(tag, message);
+ }
+
+ @Override
public void onShellCommand(FileDescriptor in, FileDescriptor out,
FileDescriptor err, String[] args, ShellCallback callback,
ResultReceiver resultReceiver) {