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