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