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)
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user