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

Commit 6a83c7e0 authored by Alex Buynytskyy's avatar Alex Buynytskyy Committed by Android (Google) Code Review
Browse files

Merge "Protect package-restrictions from corruption." into udc-dev

parents 6c4a6153 9bd42174
Loading
Loading
Loading
Loading
+265 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.server.pm;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.os.FileUtils;
import android.os.ParcelFileDescriptor;
import android.util.Log;
import android.util.Slog;

import com.android.server.security.FileIntegrity;

import libcore.io.IoUtils;

import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

final class ResilientAtomicFile implements Closeable {
    private static final String LOG_TAG = "ResilientAtomicFile";

    private final File mFile;

    private final File mTemporaryBackup;

    private final File mReserveCopy;

    private final int mFileMode;

    private final String mDebugName;

    private final ReadEventLogger mReadEventLogger;

    // Write state.
    private FileOutputStream mMainOutStream = null;
    private FileInputStream mMainInStream = null;
    private FileOutputStream mReserveOutStream = null;
    private FileInputStream mReserveInStream = null;

    // Read state.
    private File mCurrentFile = null;
    private FileInputStream mCurrentInStream = null;

    private void finalizeOutStream(FileOutputStream str) throws IOException {
        // Flash/sync + set permissions.
        str.flush();
        FileUtils.sync(str);
        FileUtils.setPermissions(str.getFD(), mFileMode, -1, -1);
    }

    ResilientAtomicFile(@NonNull File file, @NonNull File temporaryBackup,
            @NonNull File reserveCopy, int fileMode, String debugName,
            @Nullable ReadEventLogger readEventLogger) {
        mFile = file;
        mTemporaryBackup = temporaryBackup;
        mReserveCopy = reserveCopy;
        mFileMode = fileMode;
        mDebugName = debugName;
        mReadEventLogger = readEventLogger;
    }

    public File getBaseFile() {
        return mFile;
    }

    public FileOutputStream startWrite() throws IOException {
        if (mMainOutStream != null) {
            throw new IllegalStateException("Duplicate startWrite call?");
        }

        new File(mFile.getParent()).mkdirs();

        if (mFile.exists()) {
            // Presence of backup settings file indicates that we failed
            // to persist packages earlier. So preserve the older
            // backup for future reference since the current packages
            // might have been corrupted.
            if (!mTemporaryBackup.exists()) {
                if (!mFile.renameTo(mTemporaryBackup)) {
                    throw new IOException("Unable to backup " + mDebugName
                            + " file, current changes will be lost at reboot");
                }
            } else {
                mFile.delete();
                Slog.w(LOG_TAG, "Preserving older " + mDebugName + " backup");
            }
        }
        // Reserve copy is not valid anymore.
        mReserveCopy.delete();

        // In case of MT access, it's possible the files get overwritten during write.
        // Let's open all FDs we need now.
        mMainOutStream = new FileOutputStream(mFile);
        mMainInStream = new FileInputStream(mFile);
        mReserveOutStream = new FileOutputStream(mReserveCopy);
        mReserveInStream = new FileInputStream(mReserveCopy);

        return mMainOutStream;
    }

    public void finishWrite(FileOutputStream str) throws IOException {
        if (mMainOutStream != str) {
            throw new IllegalStateException("Invalid incoming stream.");
        }

        // Flush and set permissions.
        try (FileOutputStream mainOutStream = mMainOutStream) {
            mMainOutStream = null;
            finalizeOutStream(mainOutStream);
        }
        // New file successfully written, old one are no longer needed.
        mTemporaryBackup.delete();

        try (FileInputStream mainInStream = mMainInStream;
             FileInputStream reserveInStream = mReserveInStream) {
            mMainInStream = null;
            mReserveInStream = null;

            // Copy main file to reserve.
            try (FileOutputStream reserveOutStream = mReserveOutStream) {
                mReserveOutStream = null;
                FileUtils.copy(mainInStream, reserveOutStream);
                finalizeOutStream(reserveOutStream);
            }

            // Protect both main and reserve using fs-verity.
            try (ParcelFileDescriptor mainPfd = ParcelFileDescriptor.dup(mainInStream.getFD());
                 ParcelFileDescriptor copyPfd = ParcelFileDescriptor.dup(reserveInStream.getFD())) {
                FileIntegrity.setUpFsVerity(mainPfd);
                FileIntegrity.setUpFsVerity(copyPfd);
            } catch (IOException e) {
                Slog.e(LOG_TAG, "Failed to verity-protect " + mDebugName, e);
            }
        } catch (IOException e) {
            Slog.e(LOG_TAG, "Failed to write reserve copy " + mDebugName + ": " + mReserveCopy, e);
        }
    }

    public void failWrite(FileOutputStream str) {
        if (mMainOutStream != str) {
            throw new IllegalStateException("Invalid incoming stream.");
        }

        // Close all FDs.
        close();

        // Clean up partially written files
        if (mFile.exists()) {
            if (!mFile.delete()) {
                Slog.i(LOG_TAG, "Failed to clean up mangled file: " + mFile);
            }
        }
    }

    public FileInputStream openRead() throws IOException {
        if (mTemporaryBackup.exists()) {
            try {
                mCurrentFile = mTemporaryBackup;
                mCurrentInStream = new FileInputStream(mCurrentFile);
                if (mReadEventLogger != null) {
                    mReadEventLogger.logEvent(Log.INFO,
                            "Need to read from backup " + mDebugName + " file");
                }
                if (mFile.exists()) {
                    // If both the backup and normal file exist, we
                    // ignore the normal one since it might have been
                    // corrupted.
                    Slog.w(LOG_TAG, "Cleaning up " + mDebugName + " file " + mFile);
                    mFile.delete();
                }
                // Ignore reserve copy as well.
                mReserveCopy.delete();
            } catch (java.io.IOException e) {
                // We'll try for the normal settings file.
            }
        }

        if (mCurrentInStream != null) {
            return mCurrentInStream;
        }

        if (mFile.exists()) {
            mCurrentFile = mFile;
            mCurrentInStream = new FileInputStream(mCurrentFile);
        } else if (mReserveCopy.exists()) {
            mCurrentFile = mReserveCopy;
            mCurrentInStream = new FileInputStream(mCurrentFile);
            if (mReadEventLogger != null) {
                mReadEventLogger.logEvent(Log.INFO,
                        "Need to read from reserve copy " + mDebugName + " file");
            }
        }

        if (mCurrentInStream == null) {
            if (mReadEventLogger != null) {
                mReadEventLogger.logEvent(Log.INFO, "No " + mDebugName + " file");
            }
        }

        return mCurrentInStream;
    }

    public void failRead(FileInputStream str, Exception e) {
        if (mCurrentInStream != str) {
            throw new IllegalStateException("Invalid incoming stream.");
        }
        mCurrentInStream = null;
        IoUtils.closeQuietly(str);

        if (mReadEventLogger != null) {
            mReadEventLogger.logEvent(Log.ERROR,
                    "Error reading " + mDebugName + ", removing " + mCurrentFile + '\n'
                            + Log.getStackTraceString(e));
        }

        mCurrentFile.delete();
        mCurrentFile = null;
    }

    public void delete() {
        mFile.delete();
        mTemporaryBackup.delete();
        mReserveCopy.delete();
    }

    @Override
    public void close() {
        IoUtils.closeQuietly(mMainOutStream);
        IoUtils.closeQuietly(mMainInStream);
        IoUtils.closeQuietly(mReserveOutStream);
        IoUtils.closeQuietly(mReserveInStream);
        IoUtils.closeQuietly(mCurrentInStream);
        mMainOutStream = null;
        mMainInStream = null;
        mReserveOutStream = null;
        mReserveInStream = null;
        mCurrentInStream = null;
        mCurrentFile = null;
    }

    public String toString() {
        return mFile.getPath();
    }

    interface ReadEventLogger {
        void logEvent(int priority, String msg);
    }
}
+580 −738

File changed.

Preview size limit exceeded, changes collapsed.

+7 −0
Original line number Diff line number Diff line
@@ -27,6 +27,7 @@ import android.util.Xml;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.util.Preconditions;
import com.android.modules.utils.TypedXmlSerializer;
import com.android.server.security.FileIntegrity;

import org.json.JSONException;
import org.json.JSONObject;
@@ -180,6 +181,12 @@ abstract class ShortcutPackageItem {

            os.flush();
            file.finishWrite(os);

            try {
                FileIntegrity.setUpFsVerity(path);
            } catch (IOException e) {
                Slog.e(TAG, "Failed to verity-protect " + path, e);
            }
        } catch (XmlPullParserException | IOException e) {
            Slog.e(TAG, "Failed to write to file " + file.getBaseFile(), e);
            file.failWrite(os);
+41 −64
Original line number Diff line number Diff line
@@ -119,7 +119,6 @@ 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 org.json.JSONArray;
@@ -1070,29 +1069,26 @@ public class ShortcutService extends IShortcutService.Stub {
    }

    @VisibleForTesting
    final File getUserFile(@UserIdInt int userId) {
        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);
    final ResilientAtomicFile getUserFile(@UserIdInt int userId) {
        File mainFile = new File(injectUserDataPath(userId), FILENAME_USER_PACKAGES);
        File temporaryBackup = new File(injectUserDataPath(userId),
                FILENAME_USER_PACKAGES + ".backup");
        File reserveCopy = new File(injectUserDataPath(userId),
                FILENAME_USER_PACKAGES_RESERVE_COPY);
        int fileMode = FileUtils.S_IRWXU | FileUtils.S_IRWXG | FileUtils.S_IXOTH;
        return new ResilientAtomicFile(mainFile, temporaryBackup, reserveCopy, fileMode,
                "user shortcut", null);
    }

    @GuardedBy("mLock")
    private void saveUserLocked(@UserIdInt int userId) {
        final File path = getUserFile(userId);
        try (ResilientAtomicFile file = getUserFile(userId)) {
            FileOutputStream os = null;
            try {
                if (DEBUG || DEBUG_REBOOT) {
            Slog.d(TAG, "Saving to " + path);
                    Slog.d(TAG, "Saving to " + file);
                }

        final File reservePath = getReserveCopyUserFile(userId);
        reservePath.delete();

        path.getParentFile().mkdirs();
        final AtomicFile file = new AtomicFile(path);
        FileOutputStream os = null;
        try {
                os = file.startWrite();

                saveUserInternalLocked(userId, os, /* forBackup= */ false);
@@ -1102,25 +1098,9 @@ public class ShortcutService extends IShortcutService.Stub {
                // Remove all dangling bitmap files.
                cleanupDanglingBitmapDirectoriesLocked(userId);
            } catch (XmlPullParserException | IOException e) {
            Slog.e(TAG, "Failed to write to file " + file.getBaseFile(), e);
                Slog.e(TAG, "Failed to write to file " + file, e);
                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);
@@ -1157,29 +1137,26 @@ public class ShortcutService extends IShortcutService.Stub {

    @Nullable
    private ShortcutUser loadUserLocked(@UserIdInt int userId) {
        final File path = getUserFile(userId);
        try (ResilientAtomicFile file = getUserFile(userId)) {
            FileInputStream in = null;
            try {
                if (DEBUG || DEBUG_REBOOT) {
            Slog.d(TAG, "Loading from " + path);
                    Slog.d(TAG, "Loading from " + file);
                }

        try (FileInputStream in = new AtomicFile(path).openRead()) {
            return loadUserInternal(userId, in, /* forBackup= */ false);
        } catch (FileNotFoundException e) {
                in = file.openRead();
                if (in == null) {
                    if (DEBUG || DEBUG_REBOOT) {
                Slog.d(TAG, "Not found " + path);
                        Slog.d(TAG, "Not found " + file);
                    }
                    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);
            } catch (Exception e) {
                // Remove corrupted file and retry.
                file.failRead(in, e);
                return loadUserLocked(userId);
            }
            Slog.e(TAG, "Failed to read file " + path, e);
        }
        return null;
    }

    private ShortcutUser loadUserInternal(@UserIdInt int userId, InputStream is,
+24 −0
Original line number Diff line number Diff line
@@ -565,6 +565,22 @@ public class PackageManagerSettingsTests {
        verifyDistractionFlags(settingsUnderTest);
    }

    @Test
    public void testWriteCorruptReadPackageRestrictions() {
        final Settings settingsUnderTest = makeSettings();

        populateDistractionFlags(settingsUnderTest);
        settingsUnderTest.writePackageRestrictionsLPr(0, /*sync=*/true);

        // Corrupt primary file.
        writeCorruptedPackageRestrictions(0);

        // now read and verify
        populateDefaultSettings(settingsUnderTest);
        settingsUnderTest.readPackageRestrictionsLPr(0, mOrigFirstInstallTimes);
        verifyDistractionFlags(settingsUnderTest);
    }

    @Test
    public void testReadWritePackageRestrictionsAsync() {
        final Settings settingsWrite = makeSettings();
@@ -1811,6 +1827,14 @@ public class PackageManagerSettingsTests {
                        .getBytes());
    }

    private void writeCorruptedPackageRestrictions(final int userId) {
        writeFile(new File(InstrumentationRegistry.getContext().getFilesDir(), "system/users/"
                        + userId + "/package-restrictions.xml"),
                ("<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\n"
                        + "<package-restrictions>\n"
                        + "    <pkg name=\"" + PACKAGE_NAME_1 + "\" ").getBytes());
    }

    private static void writeStoppedPackagesXml() {
        writeFile(new File(InstrumentationRegistry.getContext().getFilesDir(), "system/packages-stopped.xml"),
                ( "<?xml version='1.0' encoding='utf-8' standalone='yes' ?>"
Loading