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 try { 111 mMainOutStream = new FileOutputStream(mFile); 112 mMainInStream = new FileInputStream(mFile); 113 mReserveOutStream = new FileOutputStream(mReserveCopy); 114 mReserveInStream = new FileInputStream(mReserveCopy); 115 } catch (IOException e) { 116 close(); 117 throw e; 118 } 119 120 return mMainOutStream; 121 } 122 finishWrite(FileOutputStream str)123 public void finishWrite(FileOutputStream str) throws IOException { 124 if (mMainOutStream != str) { 125 throw new IllegalStateException("Invalid incoming stream."); 126 } 127 128 // Flush and set permissions. 129 try (FileOutputStream mainOutStream = mMainOutStream) { 130 mMainOutStream = null; 131 finalizeOutStream(mainOutStream); 132 } 133 // New file successfully written, old one are no longer needed. 134 mTemporaryBackup.delete(); 135 136 try (FileInputStream mainInStream = mMainInStream; 137 FileInputStream reserveInStream = mReserveInStream) { 138 mMainInStream = null; 139 mReserveInStream = null; 140 141 // Copy main file to reserve. 142 try (FileOutputStream reserveOutStream = mReserveOutStream) { 143 mReserveOutStream = null; 144 FileUtils.copy(mainInStream, reserveOutStream); 145 finalizeOutStream(reserveOutStream); 146 } 147 148 // Protect both main and reserve using fs-verity. 149 try (ParcelFileDescriptor mainPfd = ParcelFileDescriptor.dup(mainInStream.getFD()); 150 ParcelFileDescriptor copyPfd = ParcelFileDescriptor.dup(reserveInStream.getFD())) { 151 FileIntegrity.setUpFsVerity(mainPfd); 152 FileIntegrity.setUpFsVerity(copyPfd); 153 } catch (IOException e) { 154 Slog.e(LOG_TAG, "Failed to verity-protect " + mDebugName, e); 155 } 156 } catch (IOException e) { 157 Slog.e(LOG_TAG, "Failed to write reserve copy " + mDebugName + ": " + mReserveCopy, e); 158 } 159 } 160 failWrite(FileOutputStream str)161 public void failWrite(FileOutputStream str) { 162 if (mMainOutStream != str) { 163 throw new IllegalStateException("Invalid incoming stream."); 164 } 165 166 // Close all FDs. 167 close(); 168 169 // Clean up partially written files 170 if (mFile.exists()) { 171 if (!mFile.delete()) { 172 Slog.i(LOG_TAG, "Failed to clean up mangled file: " + mFile); 173 } 174 } 175 } 176 openRead()177 public FileInputStream openRead() throws IOException { 178 if (mTemporaryBackup.exists()) { 179 try { 180 mCurrentFile = mTemporaryBackup; 181 mCurrentInStream = new FileInputStream(mCurrentFile); 182 if (mReadEventLogger != null) { 183 mReadEventLogger.logEvent(Log.INFO, 184 "Need to read from backup " + mDebugName + " file"); 185 } 186 if (mFile.exists()) { 187 // If both the backup and normal file exist, we 188 // ignore the normal one since it might have been 189 // corrupted. 190 Slog.w(LOG_TAG, "Cleaning up " + mDebugName + " file " + mFile); 191 mFile.delete(); 192 } 193 // Ignore reserve copy as well. 194 mReserveCopy.delete(); 195 } catch (java.io.IOException e) { 196 // We'll try for the normal settings file. 197 } 198 } 199 200 if (mCurrentInStream != null) { 201 return mCurrentInStream; 202 } 203 204 if (mFile.exists()) { 205 mCurrentFile = mFile; 206 mCurrentInStream = new FileInputStream(mCurrentFile); 207 } else if (mReserveCopy.exists()) { 208 mCurrentFile = mReserveCopy; 209 mCurrentInStream = new FileInputStream(mCurrentFile); 210 if (mReadEventLogger != null) { 211 mReadEventLogger.logEvent(Log.INFO, 212 "Need to read from reserve copy " + mDebugName + " file"); 213 } 214 } 215 216 if (mCurrentInStream == null) { 217 if (mReadEventLogger != null) { 218 mReadEventLogger.logEvent(Log.INFO, "No " + mDebugName + " file"); 219 } 220 } 221 222 return mCurrentInStream; 223 } 224 failRead(FileInputStream str, Exception e)225 public void failRead(FileInputStream str, Exception e) { 226 if (mCurrentInStream != str) { 227 throw new IllegalStateException("Invalid incoming stream."); 228 } 229 mCurrentInStream = null; 230 IoUtils.closeQuietly(str); 231 232 if (mReadEventLogger != null) { 233 mReadEventLogger.logEvent(Log.ERROR, 234 "Error reading " + mDebugName + ", removing " + mCurrentFile + '\n' 235 + Log.getStackTraceString(e)); 236 } 237 238 if (!mCurrentFile.delete()) { 239 throw new IllegalStateException("Failed to remove " + mCurrentFile); 240 } 241 mCurrentFile = null; 242 } 243 delete()244 public void delete() { 245 mFile.delete(); 246 mTemporaryBackup.delete(); 247 mReserveCopy.delete(); 248 } 249 250 @Override close()251 public void close() { 252 IoUtils.closeQuietly(mMainOutStream); 253 IoUtils.closeQuietly(mMainInStream); 254 IoUtils.closeQuietly(mReserveOutStream); 255 IoUtils.closeQuietly(mReserveInStream); 256 IoUtils.closeQuietly(mCurrentInStream); 257 mMainOutStream = null; 258 mMainInStream = null; 259 mReserveOutStream = null; 260 mReserveInStream = null; 261 mCurrentInStream = null; 262 mCurrentFile = null; 263 } 264 toString()265 public String toString() { 266 return mFile.getPath(); 267 } 268 269 interface ReadEventLogger { logEvent(int priority, String msg)270 void logEvent(int priority, String msg); 271 } 272 } 273