1 /* 2 * Copyright (C) 2023 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.server.pm; 18 19 import android.annotation.NonNull; 20 import android.annotation.Nullable; 21 import android.os.FileUtils; 22 import android.os.ParcelFileDescriptor; 23 import android.util.Log; 24 import android.util.Slog; 25 26 import com.android.server.security.FileIntegrity; 27 28 import libcore.io.IoUtils; 29 30 import java.io.Closeable; 31 import java.io.File; 32 import java.io.FileInputStream; 33 import java.io.FileOutputStream; 34 import java.io.IOException; 35 36 final class ResilientAtomicFile implements Closeable { 37 private static final String LOG_TAG = "ResilientAtomicFile"; 38 39 private final File mFile; 40 41 private final File mTemporaryBackup; 42 43 private final File mReserveCopy; 44 45 private final int mFileMode; 46 47 private final String mDebugName; 48 49 private final ReadEventLogger mReadEventLogger; 50 51 // Write state. 52 private FileOutputStream mMainOutStream = null; 53 private FileInputStream mMainInStream = null; 54 private FileOutputStream mReserveOutStream = null; 55 private FileInputStream mReserveInStream = null; 56 57 // Read state. 58 private File mCurrentFile = null; 59 private FileInputStream mCurrentInStream = null; 60 finalizeOutStream(FileOutputStream str)61 private void finalizeOutStream(FileOutputStream str) throws IOException { 62 // Flash/sync + set permissions. 63 str.flush(); 64 FileUtils.sync(str); 65 FileUtils.setPermissions(str.getFD(), mFileMode, -1, -1); 66 } 67 ResilientAtomicFile(@onNull File file, @NonNull File temporaryBackup, @NonNull File reserveCopy, int fileMode, String debugName, @Nullable ReadEventLogger readEventLogger)68 ResilientAtomicFile(@NonNull File file, @NonNull File temporaryBackup, 69 @NonNull File reserveCopy, int fileMode, String debugName, 70 @Nullable ReadEventLogger readEventLogger) { 71 mFile = file; 72 mTemporaryBackup = temporaryBackup; 73 mReserveCopy = reserveCopy; 74 mFileMode = fileMode; 75 mDebugName = debugName; 76 mReadEventLogger = readEventLogger; 77 } 78 getBaseFile()79 public File getBaseFile() { 80 return mFile; 81 } 82 startWrite()83 public FileOutputStream startWrite() throws IOException { 84 if (mMainOutStream != null) { 85 throw new IllegalStateException("Duplicate startWrite call?"); 86 } 87 88 new File(mFile.getParent()).mkdirs(); 89 90 if (mFile.exists()) { 91 // Presence of backup settings file indicates that we failed 92 // to persist packages earlier. So preserve the older 93 // backup for future reference since the current packages 94 // might have been corrupted. 95 if (!mTemporaryBackup.exists()) { 96 if (!mFile.renameTo(mTemporaryBackup)) { 97 throw new IOException("Unable to backup " + mDebugName 98 + " file, current changes will be lost at reboot"); 99 } 100 } else { 101 mFile.delete(); 102 Slog.w(LOG_TAG, "Preserving older " + mDebugName + " backup"); 103 } 104 } 105 // Reserve copy is not valid anymore. 106 mReserveCopy.delete(); 107 108 // In case of MT access, it's possible the files get overwritten during write. 109 // Let's open all FDs we need now. 110 mMainOutStream = new FileOutputStream(mFile); 111 mMainInStream = new FileInputStream(mFile); 112 mReserveOutStream = new FileOutputStream(mReserveCopy); 113 mReserveInStream = new FileInputStream(mReserveCopy); 114 115 return mMainOutStream; 116 } 117 finishWrite(FileOutputStream str)118 public void finishWrite(FileOutputStream str) throws IOException { 119 if (mMainOutStream != str) { 120 throw new IllegalStateException("Invalid incoming stream."); 121 } 122 123 // Flush and set permissions. 124 try (FileOutputStream mainOutStream = mMainOutStream) { 125 mMainOutStream = null; 126 finalizeOutStream(mainOutStream); 127 } 128 // New file successfully written, old one are no longer needed. 129 mTemporaryBackup.delete(); 130 131 try (FileInputStream mainInStream = mMainInStream; 132 FileInputStream reserveInStream = mReserveInStream) { 133 mMainInStream = null; 134 mReserveInStream = null; 135 136 // Copy main file to reserve. 137 try (FileOutputStream reserveOutStream = mReserveOutStream) { 138 mReserveOutStream = null; 139 FileUtils.copy(mainInStream, reserveOutStream); 140 finalizeOutStream(reserveOutStream); 141 } 142 143 // Protect both main and reserve using fs-verity. 144 try (ParcelFileDescriptor mainPfd = ParcelFileDescriptor.dup(mainInStream.getFD()); 145 ParcelFileDescriptor copyPfd = ParcelFileDescriptor.dup(reserveInStream.getFD())) { 146 FileIntegrity.setUpFsVerity(mainPfd); 147 FileIntegrity.setUpFsVerity(copyPfd); 148 } catch (IOException e) { 149 Slog.e(LOG_TAG, "Failed to verity-protect " + mDebugName, e); 150 } 151 } catch (IOException e) { 152 Slog.e(LOG_TAG, "Failed to write reserve copy " + mDebugName + ": " + mReserveCopy, e); 153 } 154 } 155 failWrite(FileOutputStream str)156 public void failWrite(FileOutputStream str) { 157 if (mMainOutStream != str) { 158 throw new IllegalStateException("Invalid incoming stream."); 159 } 160 161 // Close all FDs. 162 close(); 163 164 // Clean up partially written files 165 if (mFile.exists()) { 166 if (!mFile.delete()) { 167 Slog.i(LOG_TAG, "Failed to clean up mangled file: " + mFile); 168 } 169 } 170 } 171 openRead()172 public FileInputStream openRead() throws IOException { 173 if (mTemporaryBackup.exists()) { 174 try { 175 mCurrentFile = mTemporaryBackup; 176 mCurrentInStream = new FileInputStream(mCurrentFile); 177 if (mReadEventLogger != null) { 178 mReadEventLogger.logEvent(Log.INFO, 179 "Need to read from backup " + mDebugName + " file"); 180 } 181 if (mFile.exists()) { 182 // If both the backup and normal file exist, we 183 // ignore the normal one since it might have been 184 // corrupted. 185 Slog.w(LOG_TAG, "Cleaning up " + mDebugName + " file " + mFile); 186 mFile.delete(); 187 } 188 // Ignore reserve copy as well. 189 mReserveCopy.delete(); 190 } catch (java.io.IOException e) { 191 // We'll try for the normal settings file. 192 } 193 } 194 195 if (mCurrentInStream != null) { 196 return mCurrentInStream; 197 } 198 199 if (mFile.exists()) { 200 mCurrentFile = mFile; 201 mCurrentInStream = new FileInputStream(mCurrentFile); 202 } else if (mReserveCopy.exists()) { 203 mCurrentFile = mReserveCopy; 204 mCurrentInStream = new FileInputStream(mCurrentFile); 205 if (mReadEventLogger != null) { 206 mReadEventLogger.logEvent(Log.INFO, 207 "Need to read from reserve copy " + mDebugName + " file"); 208 } 209 } 210 211 if (mCurrentInStream == null) { 212 if (mReadEventLogger != null) { 213 mReadEventLogger.logEvent(Log.INFO, "No " + mDebugName + " file"); 214 } 215 } 216 217 return mCurrentInStream; 218 } 219 failRead(FileInputStream str, Exception e)220 public void failRead(FileInputStream str, Exception e) { 221 if (mCurrentInStream != str) { 222 throw new IllegalStateException("Invalid incoming stream."); 223 } 224 mCurrentInStream = null; 225 IoUtils.closeQuietly(str); 226 227 if (mReadEventLogger != null) { 228 mReadEventLogger.logEvent(Log.ERROR, 229 "Error reading " + mDebugName + ", removing " + mCurrentFile + '\n' 230 + Log.getStackTraceString(e)); 231 } 232 233 if (!mCurrentFile.delete()) { 234 throw new IllegalStateException("Failed to remove " + mCurrentFile); 235 } 236 mCurrentFile = null; 237 } 238 delete()239 public void delete() { 240 mFile.delete(); 241 mTemporaryBackup.delete(); 242 mReserveCopy.delete(); 243 } 244 245 @Override close()246 public void close() { 247 IoUtils.closeQuietly(mMainOutStream); 248 IoUtils.closeQuietly(mMainInStream); 249 IoUtils.closeQuietly(mReserveOutStream); 250 IoUtils.closeQuietly(mReserveInStream); 251 IoUtils.closeQuietly(mCurrentInStream); 252 mMainOutStream = null; 253 mMainInStream = null; 254 mReserveOutStream = null; 255 mReserveInStream = null; 256 mCurrentInStream = null; 257 mCurrentFile = null; 258 } 259 toString()260 public String toString() { 261 return mFile.getPath(); 262 } 263 264 interface ReadEventLogger { logEvent(int priority, String msg)265 void logEvent(int priority, String msg); 266 } 267 } 268