Merge "Protect shortcut files from corruption." into udc-dev am: 18c20c4dbc
Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/21566608 Change-Id: I23a04cae3099ef3a06eaf0b70a8472851a57c916 Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
@@ -119,10 +119,9 @@ import com.android.modules.utils.TypedXmlPullParser;
|
||||
import com.android.modules.utils.TypedXmlSerializer;
|
||||
import com.android.server.LocalServices;
|
||||
import com.android.server.SystemService;
|
||||
import com.android.server.security.FileIntegrity;
|
||||
import com.android.server.uri.UriGrantsManagerInternal;
|
||||
|
||||
import libcore.io.IoUtils;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
@@ -207,6 +206,10 @@ public class ShortcutService extends IShortcutService.Stub {
|
||||
@VisibleForTesting
|
||||
static final String FILENAME_USER_PACKAGES = "shortcuts.xml";
|
||||
|
||||
@VisibleForTesting
|
||||
static final String FILENAME_USER_PACKAGES_RESERVE_COPY =
|
||||
FILENAME_USER_PACKAGES + ".reservecopy";
|
||||
|
||||
static final String DIRECTORY_BITMAPS = "bitmaps";
|
||||
|
||||
private static final String TAG_ROOT = "root";
|
||||
@@ -1055,6 +1058,11 @@ public class ShortcutService extends IShortcutService.Stub {
|
||||
return new File(injectUserDataPath(userId), FILENAME_USER_PACKAGES);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
final File getReserveCopyUserFile(@UserIdInt int userId) {
|
||||
return new File(injectUserDataPath(userId), FILENAME_USER_PACKAGES_RESERVE_COPY);
|
||||
}
|
||||
|
||||
@GuardedBy("mLock")
|
||||
private void saveUserLocked(@UserIdInt int userId) {
|
||||
final File path = getUserFile(userId);
|
||||
@@ -1062,6 +1070,9 @@ public class ShortcutService extends IShortcutService.Stub {
|
||||
Slog.d(TAG, "Saving to " + path);
|
||||
}
|
||||
|
||||
final File reservePath = getReserveCopyUserFile(userId);
|
||||
reservePath.delete();
|
||||
|
||||
path.getParentFile().mkdirs();
|
||||
final AtomicFile file = new AtomicFile(path);
|
||||
FileOutputStream os = null;
|
||||
@@ -1079,6 +1090,23 @@ public class ShortcutService extends IShortcutService.Stub {
|
||||
file.failWrite(os);
|
||||
}
|
||||
|
||||
// Store the reserve copy of the file.
|
||||
try (FileInputStream in = new FileInputStream(path);
|
||||
FileOutputStream out = new FileOutputStream(reservePath)) {
|
||||
FileUtils.copy(in, out);
|
||||
FileUtils.sync(out);
|
||||
} catch (IOException e) {
|
||||
Slog.e(TAG, "Failed to write reserve copy: " + path, e);
|
||||
}
|
||||
|
||||
// Protect both primary and reserve copy with fs-verity.
|
||||
try {
|
||||
FileIntegrity.setUpFsVerity(path);
|
||||
FileIntegrity.setUpFsVerity(reservePath);
|
||||
} catch (IOException e) {
|
||||
Slog.e(TAG, "Failed to verity-protect", e);
|
||||
}
|
||||
|
||||
getUserShortcutsLocked(userId).logSharingShortcutStats(mMetricsLogger);
|
||||
}
|
||||
|
||||
@@ -1117,26 +1145,25 @@ public class ShortcutService extends IShortcutService.Stub {
|
||||
if (DEBUG || DEBUG_REBOOT) {
|
||||
Slog.d(TAG, "Loading from " + path);
|
||||
}
|
||||
final AtomicFile file = new AtomicFile(path);
|
||||
|
||||
final FileInputStream in;
|
||||
try {
|
||||
in = file.openRead();
|
||||
try (FileInputStream in = new AtomicFile(path).openRead()) {
|
||||
return loadUserInternal(userId, in, /* forBackup= */ false);
|
||||
} catch (FileNotFoundException e) {
|
||||
if (DEBUG || DEBUG_REBOOT) {
|
||||
Slog.d(TAG, "Not found " + path);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
final ShortcutUser ret = loadUserInternal(userId, in, /* forBackup= */ false);
|
||||
return ret;
|
||||
} catch (IOException | XmlPullParserException | InvalidFileFormatException e) {
|
||||
Slog.e(TAG, "Failed to read file " + file.getBaseFile(), e);
|
||||
return null;
|
||||
} finally {
|
||||
IoUtils.closeQuietly(in);
|
||||
} catch (Exception e) {
|
||||
final File reservePath = getReserveCopyUserFile(userId);
|
||||
Slog.e(TAG, "Reading from reserve copy: " + reservePath, e);
|
||||
try (FileInputStream in = new AtomicFile(reservePath).openRead()) {
|
||||
return loadUserInternal(userId, in, /* forBackup= */ false);
|
||||
} catch (Exception exceptionReadingReserveFile) {
|
||||
Slog.e(TAG, "Failed to read reserve copy: " + reservePath,
|
||||
exceptionReadingReserveFile);
|
||||
}
|
||||
Slog.e(TAG, "Failed to read file " + path, e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private ShortcutUser loadUserInternal(@UserIdInt int userId, InputStream is,
|
||||
|
||||
@@ -118,10 +118,12 @@ import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStream;
|
||||
import java.io.Writer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@@ -4005,6 +4007,156 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest {
|
||||
// TODO Check all other fields
|
||||
}
|
||||
|
||||
public void testSaveCorruptAndLoadUser() throws Exception {
|
||||
// First, create some shortcuts and save.
|
||||
runWithCaller(CALLING_PACKAGE_1, UserHandle.USER_SYSTEM, () -> {
|
||||
final Icon icon1 = Icon.createWithResource(getTestContext(), R.drawable.black_64x16);
|
||||
final Icon icon2 = Icon.createWithBitmap(BitmapFactory.decodeResource(
|
||||
getTestContext().getResources(), R.drawable.icon2));
|
||||
|
||||
final ShortcutInfo si1 = makeShortcut(
|
||||
"s1",
|
||||
"title1-1",
|
||||
makeComponent(ShortcutActivity.class),
|
||||
icon1,
|
||||
makeIntent(Intent.ACTION_ASSIST, ShortcutActivity2.class,
|
||||
"key1", "val1", "nest", makeBundle("key", 123)),
|
||||
/* weight */ 10);
|
||||
|
||||
final ShortcutInfo si2 = makeShortcut(
|
||||
"s2",
|
||||
"title1-2",
|
||||
/* activity */ null,
|
||||
icon2,
|
||||
makeIntent(Intent.ACTION_ASSIST, ShortcutActivity3.class),
|
||||
/* weight */ 12);
|
||||
|
||||
assertTrue(mManager.setDynamicShortcuts(list(si1, si2)));
|
||||
|
||||
assertEquals(START_TIME + INTERVAL, mManager.getRateLimitResetTime());
|
||||
assertEquals(2, mManager.getRemainingCallCount());
|
||||
});
|
||||
runWithCaller(CALLING_PACKAGE_2, UserHandle.USER_SYSTEM, () -> {
|
||||
final Icon icon1 = Icon.createWithResource(getTestContext(), R.drawable.black_16x64);
|
||||
final Icon icon2 = Icon.createWithBitmap(BitmapFactory.decodeResource(
|
||||
getTestContext().getResources(), R.drawable.icon2));
|
||||
|
||||
final ShortcutInfo si1 = makeShortcut(
|
||||
"s1",
|
||||
"title2-1",
|
||||
makeComponent(ShortcutActivity.class),
|
||||
icon1,
|
||||
makeIntent(Intent.ACTION_ASSIST, ShortcutActivity2.class,
|
||||
"key1", "val1", "nest", makeBundle("key", 123)),
|
||||
/* weight */ 10);
|
||||
|
||||
final ShortcutInfo si2 = makeShortcut(
|
||||
"s2",
|
||||
"title2-2",
|
||||
/* activity */ null,
|
||||
icon2,
|
||||
makeIntent(Intent.ACTION_ASSIST, ShortcutActivity3.class),
|
||||
/* weight */ 12);
|
||||
|
||||
assertTrue(mManager.setDynamicShortcuts(list(si1, si2)));
|
||||
|
||||
assertEquals(START_TIME + INTERVAL, mManager.getRateLimitResetTime());
|
||||
assertEquals(2, mManager.getRemainingCallCount());
|
||||
});
|
||||
|
||||
mRunningUsers.put(USER_10, true);
|
||||
|
||||
runWithCaller(CALLING_PACKAGE_1, USER_10, () -> {
|
||||
final Icon icon1 = Icon.createWithResource(getTestContext(), R.drawable.black_64x64);
|
||||
final Icon icon2 = Icon.createWithBitmap(BitmapFactory.decodeResource(
|
||||
getTestContext().getResources(), R.drawable.icon2));
|
||||
|
||||
final ShortcutInfo si1 = makeShortcut(
|
||||
"s1",
|
||||
"title10-1-1",
|
||||
makeComponent(ShortcutActivity.class),
|
||||
icon1,
|
||||
makeIntent(Intent.ACTION_ASSIST, ShortcutActivity2.class,
|
||||
"key1", "val1", "nest", makeBundle("key", 123)),
|
||||
/* weight */ 10);
|
||||
|
||||
final ShortcutInfo si2 = makeShortcut(
|
||||
"s2",
|
||||
"title10-1-2",
|
||||
/* activity */ null,
|
||||
icon2,
|
||||
makeIntent(Intent.ACTION_ASSIST, ShortcutActivity3.class),
|
||||
/* weight */ 12);
|
||||
|
||||
assertTrue(mManager.setDynamicShortcuts(list(si1, si2)));
|
||||
|
||||
assertEquals(START_TIME + INTERVAL, mManager.getRateLimitResetTime());
|
||||
assertEquals(2, mManager.getRemainingCallCount());
|
||||
});
|
||||
|
||||
// Save and corrupt the primary files.
|
||||
mService.saveDirtyInfo();
|
||||
try (Writer os = new FileWriter(mService.getUserFile(UserHandle.USER_SYSTEM))) {
|
||||
os.write("<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\n"
|
||||
+ "<user locales=\"en\" last-app-scan-time2=\"14400000");
|
||||
}
|
||||
try (Writer os = new FileWriter(mService.getUserFile(USER_10))) {
|
||||
os.write("<?xml version='1.0' encoding='utf");
|
||||
}
|
||||
|
||||
// Restore.
|
||||
initService();
|
||||
|
||||
// Before the load, the map should be empty.
|
||||
assertEquals(0, mService.getShortcutsForTest().size());
|
||||
|
||||
// this will pre-load the per-user info.
|
||||
mService.handleUnlockUser(UserHandle.USER_SYSTEM);
|
||||
|
||||
// Now it's loaded.
|
||||
assertEquals(1, mService.getShortcutsForTest().size());
|
||||
|
||||
runWithCaller(CALLING_PACKAGE_1, UserHandle.USER_SYSTEM, () -> {
|
||||
assertShortcutIds(assertAllDynamic(assertAllHaveIntents(assertAllHaveIcon(
|
||||
mManager.getDynamicShortcuts()))), "s1", "s2");
|
||||
assertEquals(2, mManager.getRemainingCallCount());
|
||||
|
||||
assertEquals("title1-1", getCallerShortcut("s1").getTitle());
|
||||
assertEquals("title1-2", getCallerShortcut("s2").getTitle());
|
||||
});
|
||||
runWithCaller(CALLING_PACKAGE_2, UserHandle.USER_SYSTEM, () -> {
|
||||
assertShortcutIds(assertAllDynamic(assertAllHaveIntents(assertAllHaveIcon(
|
||||
mManager.getDynamicShortcuts()))), "s1", "s2");
|
||||
assertEquals(2, mManager.getRemainingCallCount());
|
||||
|
||||
assertEquals("title2-1", getCallerShortcut("s1").getTitle());
|
||||
assertEquals("title2-2", getCallerShortcut("s2").getTitle());
|
||||
});
|
||||
|
||||
// Start another user
|
||||
mService.handleUnlockUser(USER_10);
|
||||
|
||||
// Now the size is 2.
|
||||
assertEquals(2, mService.getShortcutsForTest().size());
|
||||
|
||||
runWithCaller(CALLING_PACKAGE_1, USER_10, () -> {
|
||||
assertShortcutIds(assertAllDynamic(assertAllHaveIntents(assertAllHaveIcon(
|
||||
mManager.getDynamicShortcuts()))), "s1", "s2");
|
||||
assertEquals(2, mManager.getRemainingCallCount());
|
||||
|
||||
assertEquals("title10-1-1", getCallerShortcut("s1").getTitle());
|
||||
assertEquals("title10-1-2", getCallerShortcut("s2").getTitle());
|
||||
});
|
||||
|
||||
// Try stopping the user
|
||||
mService.handleStopUser(USER_10);
|
||||
|
||||
// Now it's unloaded.
|
||||
assertEquals(1, mService.getShortcutsForTest().size());
|
||||
|
||||
// TODO Check all other fields
|
||||
}
|
||||
|
||||
public void testCleanupPackage() {
|
||||
runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
|
||||
assertTrue(mManager.setDynamicShortcuts(list(
|
||||
|
||||
Reference in New Issue
Block a user