Add Context.getNoBackupFilesDir()

This is an app-private filesystem space exactly like the one
reported by Context.getFilesDir(), with one exception:  files
placed here are never backed up by the full-backup infrastructure.
If an app attempts to back up any of its contents via the normal
API it's immediately ignored with a logged warning.

The restriction is also enforced on the restore side, because
apps using support libraries might wind up creating full backup
archives containing no_backup subdirs on pre-L devices (via
adb backup, Helium, &c.).  We check for this before passing the
restore data to the app, and drop it if we detect the situation
so that the app never sees the bits.

Bug 16240573

Change-Id: I11216a391f1d32117ec7ce15aafc9cd93d0337de
This commit is contained in:
Christopher Tate
2014-07-11 17:25:57 -07:00
committed by Christopher Tate
parent 4ad58a4738
commit a7835b6b6b
8 changed files with 142 additions and 37 deletions

View File

@ -7061,6 +7061,7 @@ package android.content {
method public abstract java.io.File getFileStreamPath(java.lang.String);
method public abstract java.io.File getFilesDir();
method public abstract android.os.Looper getMainLooper();
method public abstract java.io.File getNoBackupFilesDir();
method public abstract java.io.File getObbDir();
method public abstract java.io.File[] getObbDirs();
method public abstract java.lang.String getPackageCodePath();
@ -7230,6 +7231,7 @@ package android.content {
method public java.io.File getFileStreamPath(java.lang.String);
method public java.io.File getFilesDir();
method public android.os.Looper getMainLooper();
method public java.io.File getNoBackupFilesDir();
method public java.io.File getObbDir();
method public java.io.File[] getObbDirs();
method public java.lang.String getPackageCodePath();
@ -29309,6 +29311,7 @@ package android.test.mock {
method public java.io.File getFileStreamPath(java.lang.String);
method public java.io.File getFilesDir();
method public android.os.Looper getMainLooper();
method public java.io.File getNoBackupFilesDir();
method public java.io.File getObbDir();
method public java.io.File[] getObbDirs();
method public java.lang.String getPackageCodePath();

View File

@ -247,6 +247,8 @@ class ContextImpl extends Context {
@GuardedBy("mSync")
private File mFilesDir;
@GuardedBy("mSync")
private File mNoBackupFilesDir;
@GuardedBy("mSync")
private File mCacheDir;
@GuardedBy("mSync")
@ -963,27 +965,42 @@ class ContextImpl extends Context {
return f.delete();
}
// Common-path handling of app data dir creation
private static File createFilesDirLocked(File file) {
if (!file.exists()) {
if (!file.mkdirs()) {
if (file.exists()) {
// spurious failure; probably racing with another process for this app
return file;
}
Log.w(TAG, "Unable to create files subdir " + file.getPath());
return null;
}
FileUtils.setPermissions(
file.getPath(),
FileUtils.S_IRWXU|FileUtils.S_IRWXG|FileUtils.S_IXOTH,
-1, -1);
}
return file;
}
@Override
public File getFilesDir() {
synchronized (mSync) {
if (mFilesDir == null) {
mFilesDir = new File(getDataDirFile(), "files");
}
if (!mFilesDir.exists()) {
if(!mFilesDir.mkdirs()) {
if (mFilesDir.exists()) {
// spurious failure; probably racing with another process for this app
return mFilesDir;
}
Log.w(TAG, "Unable to create files directory " + mFilesDir.getPath());
return null;
}
FileUtils.setPermissions(
mFilesDir.getPath(),
FileUtils.S_IRWXU|FileUtils.S_IRWXG|FileUtils.S_IXOTH,
-1, -1);
return createFilesDirLocked(mFilesDir);
}
}
@Override
public File getNoBackupFilesDir() {
synchronized (mSync) {
if (mNoBackupFilesDir == null) {
mNoBackupFilesDir = new File(getDataDirFile(), "no_backup");
}
return mFilesDir;
return createFilesDirLocked(mNoBackupFilesDir);
}
}
@ -1035,22 +1052,8 @@ class ContextImpl extends Context {
if (mCacheDir == null) {
mCacheDir = new File(getDataDirFile(), "cache");
}
if (!mCacheDir.exists()) {
if(!mCacheDir.mkdirs()) {
if (mCacheDir.exists()) {
// spurious failure; probably racing with another process for this app
return mCacheDir;
}
Log.w(TAG, "Unable to create cache directory " + mCacheDir.getAbsolutePath());
return null;
}
FileUtils.setPermissions(
mCacheDir.getPath(),
FileUtils.S_IRWXU|FileUtils.S_IRWXG|FileUtils.S_IXOTH,
-1, -1);
}
return createFilesDirLocked(mCacheDir);
}
return mCacheDir;
}
@Override

View File

@ -247,12 +247,40 @@ public abstract class BackupAgent extends ContextWrapper {
throws IOException;
/**
* The default implementation backs up the entirety of the application's "owned"
* file system trees to the output.
* The application is having its entire file system contents backed up. {@code data}
* points to the backup destination, and the app has the opportunity to choose which
* files are to be stored. To commit a file as part of the backup, call the
* {@link #fullBackupFile(File, FullBackupDataOutput)} helper method. After all file
* data is written to the output, the agent returns from this method and the backup
* operation concludes.
*
* <p>Certain parts of the app's data are never backed up even if the app explicitly
* sends them to the output:
*
* <ul>
* <li>The contents of the {@link #getCacheDir()} directory</li>
* <li>The contents of the {@link #getNoBackupFilesDir()} directory</li>
* <li>The contents of the app's shared library directory</li>
* </ul>
*
* <p>The default implementation of this method backs up the entirety of the
* application's "owned" file system trees to the output other than the few exceptions
* listed above. Apps only need to override this method if they need to impose special
* limitations on which files are being stored beyond the control that
* {@link #getNoBackupFilesDir()} offers.
*
* @param data A structured wrapper pointing to the backup destination.
* @throws IOException
*
* @see Context#getNoBackupFilesDir()
* @see #fullBackupFile(File, FullBackupDataOutput)
* @see #onRestoreFile(ParcelFileDescriptor, long, File, int, long, long)
*/
public void onFullBackup(FullBackupDataOutput data) throws IOException {
ApplicationInfo appInfo = getApplicationInfo();
// Note that we don't need to think about the no_backup dir because it's outside
// all of the ones we will be traversing
String rootDir = new File(appInfo.dataDir).getCanonicalPath();
String filesDir = getFilesDir().getCanonicalPath();
String databaseDir = getDatabasePath("foo").getParentFile().getCanonicalPath();
@ -311,6 +339,10 @@ public abstract class BackupAgent extends ContextWrapper {
* to place it with the proper location and permissions on the device where the
* data is restored.
*
* <p class="note">It is safe to explicitly back up files underneath your application's
* {@link #getNoBackupFilesDir()} directory, and they will be restored to that
* location correctly.
*
* @param file The file to be backed up. The file must exist and be readable by
* the caller.
* @param output The destination to which the backed-up file data will be sent.
@ -319,6 +351,7 @@ public abstract class BackupAgent extends ContextWrapper {
// Look up where all of our various well-defined dir trees live on this device
String mainDir;
String filesDir;
String nbFilesDir;
String dbDir;
String spDir;
String cacheDir;
@ -331,6 +364,7 @@ public abstract class BackupAgent extends ContextWrapper {
try {
mainDir = new File(appInfo.dataDir).getCanonicalPath();
filesDir = getFilesDir().getCanonicalPath();
nbFilesDir = getNoBackupFilesDir().getCanonicalPath();
dbDir = getDatabasePath("foo").getParentFile().getCanonicalPath();
spDir = getSharedPrefsFile("foo").getParentFile().getCanonicalPath();
cacheDir = getCacheDir().getCanonicalPath();
@ -354,8 +388,10 @@ public abstract class BackupAgent extends ContextWrapper {
return;
}
if (filePath.startsWith(cacheDir) || filePath.startsWith(libDir)) {
Log.w(TAG, "lib and cache files are not backed up");
if (filePath.startsWith(cacheDir)
|| filePath.startsWith(libDir)
|| filePath.startsWith(nbFilesDir)) {
Log.w(TAG, "lib, cache, and no_backup files are not backed up");
return;
}
@ -508,6 +544,8 @@ public abstract class BackupAgent extends ContextWrapper {
mode = -1; // < 0 is a token to skip attempting a chmod()
}
}
} else if (domain.equals(FullBackup.NO_BACKUP_TREE_TOKEN)) {
basePath = getNoBackupFilesDir().getCanonicalPath();
} else {
// Not a supported location
Log.i(TAG, "Unrecognized domain " + domain);

View File

@ -40,6 +40,7 @@ public class FullBackup {
public static final String OBB_TREE_TOKEN = "obb";
public static final String ROOT_TREE_TOKEN = "r";
public static final String DATA_TREE_TOKEN = "f";
public static final String NO_BACKUP_TREE_TOKEN = "nb";
public static final String DATABASE_TREE_TOKEN = "db";
public static final String SHAREDPREFS_TREE_TOKEN = "sp";
public static final String MANAGED_EXTERNAL_TREE_TOKEN = "ef";

View File

@ -646,6 +646,26 @@ public abstract class Context {
*/
public abstract File getFilesDir();
/**
* Returns the absolute path to the directory on the filesystem similar to
* {@link #getFilesDir()}. The difference is that files placed under this
* directory will be excluded from automatic backup to remote storage. See
* {@link android.app.backup.BackupAgent BackupAgent} for a full discussion
* of the automatic backup mechanism in Android.
*
* <p>No permissions are required to read or write to the returned path, since this
* path is internal storage.
*
* @return The path of the directory holding application files that will not be
* automatically backed up to remote storage.
*
* @see #openFileOutput
* @see #getFileStreamPath
* @see #getDir
* @see android.app.backup.BackupAgent
*/
public abstract File getNoBackupFilesDir();
/**
* Returns the absolute path to the directory on the primary external filesystem
* (that is somewhere on {@link android.os.Environment#getExternalStorageDirectory()

View File

@ -200,7 +200,12 @@ public class ContextWrapper extends Context {
public File getFilesDir() {
return mBase.getFilesDir();
}
@Override
public File getNoBackupFilesDir() {
return mBase.getNoBackupFilesDir();
}
@Override
public File getExternalFilesDir(String type) {
return mBase.getExternalFilesDir(type);

View File

@ -3922,6 +3922,11 @@ public class BackupManagerService extends IBackupManager.Stub {
break;
}
// Is it a *file* we need to drop?
if (!isRestorableFile(info)) {
okay = false;
}
// If the policy is satisfied, go ahead and set up to pipe the
// data to the agent.
if (DEBUG && okay && mAgent != null) {
@ -4082,9 +4087,9 @@ public class BackupManagerService extends IBackupManager.Stub {
}
}
// Problems setting up the agent communication, or an already-
// ignored package: skip to the next tar stream entry by
// reading and discarding this file.
// Problems setting up the agent communication, an explicitly
// dropped file, or an already-ignored package: skip to the
// next stream entry by reading and discarding this file.
if (!okay) {
if (DEBUG) Slog.d(TAG, "[discarding file content]");
long bytesToConsume = (info.size + 511) & ~511;
@ -4691,6 +4696,31 @@ public class BackupManagerService extends IBackupManager.Stub {
return info;
}
private boolean isRestorableFile(FileMetadata info) {
if (FullBackup.CACHE_TREE_TOKEN.equals(info.domain)) {
if (MORE_DEBUG) {
Slog.i(TAG, "Dropping cache file path " + info.path);
}
return false;
}
if (FullBackup.ROOT_TREE_TOKEN.equals(info.domain)) {
// It's possible this is "no-backup" dir contents in an archive stream
// produced on a device running a version of the OS that predates that
// API. Respect the no-backup intention and don't let the data get to
// the app.
if (info.path.startsWith("no_backup/")) {
if (MORE_DEBUG) {
Slog.i(TAG, "Dropping no_backup file path " + info.path);
}
return false;
}
}
// Otherwise we think this file is good to go
return true;
}
private void HEXLOG(byte[] block) {
int offset = 0;
int todo = block.length;

View File

@ -174,6 +174,11 @@ public class MockContext extends Context {
throw new UnsupportedOperationException();
}
@Override
public File getNoBackupFilesDir() {
throw new UnsupportedOperationException();
}
@Override
public File getExternalFilesDir(String type) {
throw new UnsupportedOperationException();