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