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

Commit 9bd42174 authored by Alex Buynytskyy's avatar Alex Buynytskyy
Browse files

Protect package-restrictions from corruption.

+ refactor to encapulate all logic into ResilientAtomicFile

Bug: 253568736
Test: atest PackageManagerSettingsTests com.android.server.pm.test.SettingsTest
Change-Id: I2bba7d623c56f40bc8ed93a21098c9184698609d
parent cd48412b
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);
    }
}
Loading