• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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