diff options
| author | 2018-10-18 16:24:13 -0700 | |
|---|---|---|
| committer | 2018-10-24 08:34:04 -0700 | |
| commit | ee93ad28ff2fd3bc0a99dfbe1d1393d092f46d7b (patch) | |
| tree | bc1dc2bc521d0f0f1104a428925737931870fd92 | |
| parent | 7e77dbb1be8a4fae4fc846035ef7715200a518e3 (diff) | |
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)
10 files changed, 171 insertions, 65 deletions
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index 9d3c5c6417af..4756bf40bad3 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -1425,60 +1425,10 @@ public final class ActivityThread extends ClientTransactionHandler { 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 599c2d2d3594..a2a6b9b4a762 100644 --- a/core/java/android/content/ContentResolver.java +++ b/core/java/android/content/ContentResolver.java @@ -36,11 +36,9 @@ import android.database.CrossProcessCursorWrapper; 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.RemoteException; 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 @@ public abstract class ContentResolver { } }); } + + /** {@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 a55dd31f265f..1d0237502812 100644 --- a/core/java/android/content/IContentService.aidl +++ b/core/java/android/content/IContentService.aidl @@ -185,4 +185,6 @@ interface IContentService { 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 7fa2b409c59d..cf019e134bc2 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 final class DefaultDatabaseErrorHandler implements DatabaseErrorHandler { */ 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 @@ public final class DefaultDatabaseErrorHandler implements DatabaseErrorHandler { } 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 5c4f16a7cf3d..20505ca803dc 100644 --- a/core/java/android/database/sqlite/SQLiteConnection.java +++ b/core/java/android/database/sqlite/SQLiteConnection.java @@ -34,6 +34,7 @@ import dalvik.system.BlockGuard; 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 @@ public final class SQLiteConnection implements CancellationSignal.OnCancelListen 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 @@ public final class SQLiteConnection implements CancellationSignal.OnCancelListen } } + 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 3ee348ba4865..dbc176614daf 100644 --- a/core/java/android/database/sqlite/SQLiteConnectionPool.java +++ b/core/java/android/database/sqlite/SQLiteConnectionPool.java @@ -24,6 +24,7 @@ import android.os.Message; 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 com.android.internal.annotations.VisibleForTesting; 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 @@ public final class SQLiteConnectionPool implements Closeable { * @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 eb5c720d6309..f9c2c3e2c983 100644 --- a/core/java/android/database/sqlite/SQLiteDatabase.java +++ b/core/java/android/database/sqlite/SQLiteDatabase.java @@ -22,6 +22,8 @@ import android.annotation.NonNull; 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.Looper; 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 dalvik.system.CloseGuard; 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 @@ public final class SQLiteDatabase extends SQLiteClosable { * @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 @@ public final class SQLiteDatabase extends SQLiteClosable { 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 @@ public final class SQLiteDatabase extends SQLiteClosable { * 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 @@ public final class SQLiteDatabase extends SQLiteClosable { return this; } - /** + /**w * Sets <a href="https://sqlite.org/pragma.html#pragma_synchronous">synchronous mode</a> * . * @return @@ -2646,5 +2703,34 @@ public final class SQLiteDatabase extends SQLiteClosable { @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 1c6620467b7c..f220205b5f26 100644 --- a/core/java/android/database/sqlite/SQLiteDebug.java +++ b/core/java/android/database/sqlite/SQLiteDebug.java @@ -189,6 +189,11 @@ public final class SQLiteDebug { * @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 @@ public final class SQLiteDebug { } } - 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 67e5f65d5a1f..ff286fdb42df 100644 --- a/core/java/android/database/sqlite/SQLiteGlobal.java +++ b/core/java/android/database/sqlite/SQLiteGlobal.java @@ -42,6 +42,9 @@ public final class SQLiteGlobal { /** @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 @@ public final class SQLiteGlobal { 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 5698fdf439a8..5ed626342d31 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 @@ public final class ContentService extends IContentService.Stub { } @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) { |