Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 544de6ae authored by Alex Buynytskyy's avatar Alex Buynytskyy
Browse files

Protect shortcut files from corruption.

Bug: 269049175
Fixes: 269049175
Test: atest ShortcutManagerTest1
Change-Id: I106648d5311fa02e7eacbb571bc0481d2023d94f
(cherry picked from commit 91e73702)
parent 42139e0e
Loading
Loading
Loading
Loading
+43 −16
Original line number Diff line number Diff line
@@ -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;
        } 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 {
            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);
            Slog.e(TAG, "Failed to read file " + path, e);
        }
        return null;
    }

    private ShortcutUser loadUserInternal(@UserIdInt int userId, InputStream is,
+152 −0
Original line number Diff line number Diff line
@@ -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(