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:
TreeHugger Robot
2023-02-25 00:48:23 +00:00
committed by Automerger Merge Worker
2 changed files with 195 additions and 16 deletions

View File

@@ -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,

View File

@@ -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(