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: I2ff4e29cf3a2f4f2d0515ab41c34b841dac1ba6a 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.modules.utils.TypedXmlSerializer;
|
||||||
import com.android.server.LocalServices;
|
import com.android.server.LocalServices;
|
||||||
import com.android.server.SystemService;
|
import com.android.server.SystemService;
|
||||||
|
import com.android.server.security.FileIntegrity;
|
||||||
import com.android.server.uri.UriGrantsManagerInternal;
|
import com.android.server.uri.UriGrantsManagerInternal;
|
||||||
|
|
||||||
import libcore.io.IoUtils;
|
|
||||||
|
|
||||||
import org.json.JSONArray;
|
import org.json.JSONArray;
|
||||||
import org.json.JSONException;
|
import org.json.JSONException;
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
@@ -207,6 +206,10 @@ public class ShortcutService extends IShortcutService.Stub {
|
|||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
static final String FILENAME_USER_PACKAGES = "shortcuts.xml";
|
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";
|
static final String DIRECTORY_BITMAPS = "bitmaps";
|
||||||
|
|
||||||
private static final String TAG_ROOT = "root";
|
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);
|
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")
|
@GuardedBy("mLock")
|
||||||
private void saveUserLocked(@UserIdInt int userId) {
|
private void saveUserLocked(@UserIdInt int userId) {
|
||||||
final File path = getUserFile(userId);
|
final File path = getUserFile(userId);
|
||||||
@@ -1062,6 +1070,9 @@ public class ShortcutService extends IShortcutService.Stub {
|
|||||||
Slog.d(TAG, "Saving to " + path);
|
Slog.d(TAG, "Saving to " + path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final File reservePath = getReserveCopyUserFile(userId);
|
||||||
|
reservePath.delete();
|
||||||
|
|
||||||
path.getParentFile().mkdirs();
|
path.getParentFile().mkdirs();
|
||||||
final AtomicFile file = new AtomicFile(path);
|
final AtomicFile file = new AtomicFile(path);
|
||||||
FileOutputStream os = null;
|
FileOutputStream os = null;
|
||||||
@@ -1079,6 +1090,23 @@ public class ShortcutService extends IShortcutService.Stub {
|
|||||||
file.failWrite(os);
|
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);
|
getUserShortcutsLocked(userId).logSharingShortcutStats(mMetricsLogger);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1117,26 +1145,25 @@ public class ShortcutService extends IShortcutService.Stub {
|
|||||||
if (DEBUG || DEBUG_REBOOT) {
|
if (DEBUG || DEBUG_REBOOT) {
|
||||||
Slog.d(TAG, "Loading from " + path);
|
Slog.d(TAG, "Loading from " + path);
|
||||||
}
|
}
|
||||||
final AtomicFile file = new AtomicFile(path);
|
|
||||||
|
|
||||||
final FileInputStream in;
|
try (FileInputStream in = new AtomicFile(path).openRead()) {
|
||||||
try {
|
return loadUserInternal(userId, in, /* forBackup= */ false);
|
||||||
in = file.openRead();
|
|
||||||
} catch (FileNotFoundException e) {
|
} catch (FileNotFoundException e) {
|
||||||
if (DEBUG || DEBUG_REBOOT) {
|
if (DEBUG || DEBUG_REBOOT) {
|
||||||
Slog.d(TAG, "Not found " + path);
|
Slog.d(TAG, "Not found " + path);
|
||||||
}
|
}
|
||||||
return null;
|
} 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);
|
||||||
}
|
}
|
||||||
try {
|
Slog.e(TAG, "Failed to read file " + path, e);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private ShortcutUser loadUserInternal(@UserIdInt int userId, InputStream is,
|
private ShortcutUser loadUserInternal(@UserIdInt int userId, InputStream is,
|
||||||
|
|||||||
@@ -118,10 +118,12 @@ import java.io.ByteArrayInputStream;
|
|||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.FileWriter;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.InputStreamReader;
|
import java.io.InputStreamReader;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
|
import java.io.Writer;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -4005,6 +4007,156 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest {
|
|||||||
// TODO Check all other fields
|
// 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() {
|
public void testCleanupPackage() {
|
||||||
runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
|
runWithCaller(CALLING_PACKAGE_1, USER_0, () -> {
|
||||||
assertTrue(mManager.setDynamicShortcuts(list(
|
assertTrue(mManager.setDynamicShortcuts(list(
|
||||||
|
|||||||
Reference in New Issue
Block a user