1 /*
2  * Copyright (C) 2019 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.dynsystem;
18 
19 import android.content.Context;
20 import android.gsi.AvbPublicKey;
21 import android.net.Uri;
22 import android.os.AsyncTask;
23 import android.os.Build;
24 import android.os.MemoryFile;
25 import android.os.ParcelFileDescriptor;
26 import android.os.image.DynamicSystemManager;
27 import android.service.persistentdata.PersistentDataBlockManager;
28 import android.util.Log;
29 import android.webkit.URLUtil;
30 
31 import org.json.JSONException;
32 
33 import java.io.BufferedInputStream;
34 import java.io.File;
35 import java.io.IOException;
36 import java.io.InputStream;
37 import java.net.URL;
38 import java.util.Arrays;
39 import java.util.Enumeration;
40 import java.util.List;
41 import java.util.Locale;
42 import java.util.zip.GZIPInputStream;
43 import java.util.zip.ZipEntry;
44 import java.util.zip.ZipFile;
45 import java.util.zip.ZipInputStream;
46 
47 class InstallationAsyncTask extends AsyncTask<String, InstallationAsyncTask.Progress, Throwable> {
48 
49     private static final String TAG = "InstallationAsyncTask";
50 
51     private static final int READ_BUFFER_SIZE = 1 << 13;
52     private static final long MIN_PROGRESS_TO_PUBLISH = 1 << 27;
53 
54     private static final List<String> UNSUPPORTED_PARTITIONS =
55             Arrays.asList(
56                     "vbmeta", "boot", "userdata", "dtbo", "super_empty", "system_other", "scratch");
57 
58     private class UnsupportedUrlException extends Exception {
UnsupportedUrlException(String message)59         private UnsupportedUrlException(String message) {
60             super(message);
61         }
62     }
63 
64     private class UnsupportedFormatException extends Exception {
UnsupportedFormatException(String message)65         private UnsupportedFormatException(String message) {
66             super(message);
67         }
68     }
69 
70     static class ImageValidationException extends Exception {
ImageValidationException(String message)71         ImageValidationException(String message) {
72             super(message);
73         }
74 
ImageValidationException(Throwable cause)75         ImageValidationException(Throwable cause) {
76             super(cause);
77         }
78     }
79 
80     static class RevocationListFetchException extends ImageValidationException {
RevocationListFetchException(Throwable cause)81         RevocationListFetchException(Throwable cause) {
82             super(cause);
83         }
84     }
85 
86     static class KeyRevokedException extends ImageValidationException {
KeyRevokedException(String message)87         KeyRevokedException(String message) {
88             super(message);
89         }
90     }
91 
92     static class PublicKeyException extends ImageValidationException {
PublicKeyException(String message)93         PublicKeyException(String message) {
94             super(message);
95         }
96     }
97 
98     /** UNSET means the installation is not completed */
99     static final int RESULT_UNSET = 0;
100     static final int RESULT_OK = 1;
101     static final int RESULT_CANCELLED = 2;
102     static final int RESULT_ERROR_IO = 3;
103     static final int RESULT_ERROR_UNSUPPORTED_URL = 4;
104     static final int RESULT_ERROR_UNSUPPORTED_FORMAT = 5;
105     static final int RESULT_ERROR_EXCEPTION = 6;
106 
107     static class Progress {
108         public final String partitionName;
109         public final long partitionSize;
110         public final int numInstalledPartitions;
111         public long installedSize;
112 
Progress(String partitionName, long partitionSize, int numInstalledPartitions)113         Progress(String partitionName, long partitionSize, int numInstalledPartitions) {
114             this.partitionName = partitionName;
115             this.partitionSize = partitionSize;
116             this.numInstalledPartitions = numInstalledPartitions;
117         }
118     }
119 
120     interface ProgressListener {
onProgressUpdate(Progress progress)121         void onProgressUpdate(Progress progress);
122 
onResult(int resultCode, Throwable detail)123         void onResult(int resultCode, Throwable detail);
124     }
125 
126     private final String mUrl;
127     private final String mDsuSlot;
128     private final String mPublicKey;
129     private final long mSystemSize;
130     private final long mUserdataSize;
131     private final Context mContext;
132     private final DynamicSystemManager mDynSystem;
133     private final ProgressListener mListener;
134     private final boolean mIsNetworkUrl;
135     private final boolean mIsDeviceBootloaderUnlocked;
136     private DynamicSystemManager.Session mInstallationSession;
137     private KeyRevocationList mKeyRevocationList;
138 
139     private boolean mIsZip;
140     private boolean mIsCompleted;
141 
142     private int mNumInstalledPartitions;
143 
144     private InputStream mStream;
145     private ZipFile mZipFile;
146 
InstallationAsyncTask( String url, String dsuSlot, String publicKey, long systemSize, long userdataSize, Context context, DynamicSystemManager dynSystem, ProgressListener listener)147     InstallationAsyncTask(
148             String url,
149             String dsuSlot,
150             String publicKey,
151             long systemSize,
152             long userdataSize,
153             Context context,
154             DynamicSystemManager dynSystem,
155             ProgressListener listener) {
156         mUrl = url;
157         mDsuSlot = dsuSlot;
158         mPublicKey = publicKey;
159         mSystemSize = systemSize;
160         mUserdataSize = userdataSize;
161         mContext = context;
162         mDynSystem = dynSystem;
163         mListener = listener;
164         mIsNetworkUrl = URLUtil.isNetworkUrl(mUrl);
165         PersistentDataBlockManager pdbManager =
166                 (PersistentDataBlockManager)
167                         mContext.getSystemService(Context.PERSISTENT_DATA_BLOCK_SERVICE);
168         mIsDeviceBootloaderUnlocked =
169                 (pdbManager != null)
170                         && (pdbManager.getFlashLockState()
171                                 == PersistentDataBlockManager.FLASH_LOCK_UNLOCKED);
172     }
173 
174     @Override
doInBackground(String... voids)175     protected Throwable doInBackground(String... voids) {
176         Log.d(TAG, "Start doInBackground(), URL: " + mUrl);
177 
178         try {
179             // call DynamicSystemManager to cleanup stuff
180             mDynSystem.remove();
181 
182             verifyAndPrepare();
183 
184             mDynSystem.startInstallation(mDsuSlot);
185 
186             installUserdata();
187             if (isCancelled()) {
188                 mDynSystem.remove();
189                 return null;
190             }
191             if (mUrl == null) {
192                 mDynSystem.finishInstallation();
193                 return null;
194             }
195             installImages();
196             if (isCancelled()) {
197                 mDynSystem.remove();
198                 return null;
199             }
200 
201             if (Build.IS_DEBUGGABLE) {
202                 // If host is debuggable, then install a scratch partition so that we can do
203                 // adb remount in the guest system.
204                 try {
205                     installScratch();
206                 } catch (IOException e) {
207                     // Failing to install overlayFS scratch shouldn't be fatal.
208                     // Just ignore the error and skip installing the scratch partition.
209                     Log.w(TAG, e.toString(), e);
210                 }
211                 if (isCancelled()) {
212                     mDynSystem.remove();
213                     return null;
214                 }
215             }
216 
217             mDynSystem.finishInstallation();
218         } catch (Exception e) {
219             Log.e(TAG, e.toString(), e);
220             mDynSystem.remove();
221             return e;
222         } finally {
223             close();
224         }
225 
226         return null;
227     }
228 
229     @Override
onPostExecute(Throwable detail)230     protected void onPostExecute(Throwable detail) {
231         int result = RESULT_UNSET;
232 
233         if (detail == null) {
234             result = RESULT_OK;
235             mIsCompleted = true;
236         } else if (detail instanceof IOException) {
237             result = RESULT_ERROR_IO;
238         } else if (detail instanceof UnsupportedUrlException) {
239             result = RESULT_ERROR_UNSUPPORTED_URL;
240         } else if (detail instanceof UnsupportedFormatException) {
241             result = RESULT_ERROR_UNSUPPORTED_FORMAT;
242         } else {
243             result = RESULT_ERROR_EXCEPTION;
244         }
245 
246         Log.d(TAG, "onPostExecute(), URL: " + mUrl + ", result: " + result);
247 
248         mListener.onResult(result, detail);
249     }
250 
251     @Override
onCancelled()252     protected void onCancelled() {
253         Log.d(TAG, "onCancelled(), URL: " + mUrl);
254 
255         if (mDynSystem.abort()) {
256             Log.d(TAG, "Installation aborted");
257         } else {
258             Log.w(TAG, "DynamicSystemManager.abort() returned false");
259         }
260 
261         mListener.onResult(RESULT_CANCELLED, null);
262     }
263 
264     @Override
onProgressUpdate(Progress... values)265     protected void onProgressUpdate(Progress... values) {
266         Progress progress = values[0];
267         mListener.onProgressUpdate(progress);
268     }
269 
verifyAndPrepare()270     private void verifyAndPrepare() throws Exception {
271         if (mUrl == null) {
272             return;
273         }
274         String extension = mUrl.substring(mUrl.lastIndexOf('.') + 1);
275 
276         if ("gz".equals(extension) || "gzip".equals(extension)) {
277             mIsZip = false;
278         } else if ("zip".equals(extension)) {
279             mIsZip = true;
280         } else {
281             throw new UnsupportedFormatException(
282                 String.format(Locale.US, "Unsupported file format: %s", mUrl));
283         }
284 
285         if (mIsNetworkUrl) {
286             mStream = new URL(mUrl).openStream();
287         } else if (URLUtil.isFileUrl(mUrl)) {
288             if (mIsZip) {
289                 mZipFile = new ZipFile(new File(new URL(mUrl).toURI()));
290             } else {
291                 mStream = new URL(mUrl).openStream();
292             }
293         } else if (URLUtil.isContentUrl(mUrl)) {
294             mStream = mContext.getContentResolver().openInputStream(Uri.parse(mUrl));
295         } else {
296             throw new UnsupportedUrlException(
297                     String.format(Locale.US, "Unsupported URL: %s", mUrl));
298         }
299 
300         try {
301             String listUrl = mContext.getString(R.string.key_revocation_list_url);
302             mKeyRevocationList = KeyRevocationList.fromUrl(new URL(listUrl));
303         } catch (IOException | JSONException e) {
304             mKeyRevocationList = new KeyRevocationList();
305             imageValidationThrowOrWarning(new RevocationListFetchException(e));
306         }
307         if (mKeyRevocationList.isRevoked(mPublicKey)) {
308             imageValidationThrowOrWarning(new KeyRevokedException(mPublicKey));
309         }
310     }
311 
imageValidationThrowOrWarning(ImageValidationException e)312     private void imageValidationThrowOrWarning(ImageValidationException e)
313             throws ImageValidationException {
314         if (mIsDeviceBootloaderUnlocked || !mIsNetworkUrl) {
315             // If device is OEM unlocked or DSU is being installed from a local file URI,
316             // then be permissive.
317             Log.w(TAG, e.toString());
318         } else {
319             throw e;
320         }
321     }
322 
installWritablePartition(final String partitionName, final long partitionSize)323     private void installWritablePartition(final String partitionName, final long partitionSize)
324             throws IOException {
325         Log.d(TAG, "Creating writable partition: " + partitionName + ", size: " + partitionSize);
326 
327         Thread thread = new Thread() {
328             @Override
329             public void run() {
330                 mInstallationSession =
331                         mDynSystem.createPartition(
332                                 partitionName, partitionSize, /* readOnly= */ false);
333             }
334         };
335 
336         thread.start();
337         Progress progress = new Progress(partitionName, partitionSize, mNumInstalledPartitions++);
338 
339         while (thread.isAlive()) {
340             if (isCancelled()) {
341                 return;
342             }
343 
344             final long installedSize = mDynSystem.getInstallationProgress().bytes_processed;
345 
346             if (installedSize > progress.installedSize + MIN_PROGRESS_TO_PUBLISH) {
347                 progress.installedSize = installedSize;
348                 publishProgress(progress);
349             }
350 
351             try {
352                 Thread.sleep(100);
353             } catch (InterruptedException e) {
354                 // Ignore the error.
355             }
356         }
357 
358         if (mInstallationSession == null) {
359             throw new IOException(
360                     "Failed to start installation with requested size: " + partitionSize);
361         }
362 
363         // Reset installation session and verify that installation completes successfully.
364         mInstallationSession = null;
365         if (!mDynSystem.closePartition()) {
366             throw new IOException("Failed to complete partition installation: " + partitionName);
367         }
368     }
369 
installScratch()370     private void installScratch() throws IOException {
371         installWritablePartition("scratch", mDynSystem.suggestScratchSize());
372     }
373 
installUserdata()374     private void installUserdata() throws IOException {
375         installWritablePartition("userdata", mUserdataSize);
376     }
377 
installImages()378     private void installImages() throws IOException, ImageValidationException {
379         if (mStream != null) {
380             if (mIsZip) {
381                 installStreamingZipUpdate();
382             } else {
383                 installStreamingGzUpdate();
384             }
385         } else {
386             installLocalZipUpdate();
387         }
388     }
389 
installStreamingGzUpdate()390     private void installStreamingGzUpdate() throws IOException, ImageValidationException {
391         Log.d(TAG, "To install a streaming GZ update");
392         installImage("system", mSystemSize, new GZIPInputStream(mStream));
393     }
394 
installStreamingZipUpdate()395     private void installStreamingZipUpdate() throws IOException, ImageValidationException {
396         Log.d(TAG, "To install a streaming ZIP update");
397 
398         ZipInputStream zis = new ZipInputStream(mStream);
399         ZipEntry zipEntry = null;
400 
401         while ((zipEntry = zis.getNextEntry()) != null) {
402             installImageFromAnEntry(zipEntry, zis);
403 
404             if (isCancelled()) {
405                 break;
406             }
407         }
408     }
409 
installLocalZipUpdate()410     private void installLocalZipUpdate() throws IOException, ImageValidationException {
411         Log.d(TAG, "To install a local ZIP update");
412 
413         Enumeration<? extends ZipEntry> entries = mZipFile.entries();
414 
415         while (entries.hasMoreElements()) {
416             ZipEntry entry = entries.nextElement();
417             installImageFromAnEntry(entry, mZipFile.getInputStream(entry));
418 
419             if (isCancelled()) {
420                 break;
421             }
422         }
423     }
424 
installImageFromAnEntry(ZipEntry entry, InputStream is)425     private boolean installImageFromAnEntry(ZipEntry entry, InputStream is)
426             throws IOException, ImageValidationException {
427         String name = entry.getName();
428 
429         Log.d(TAG, "ZipEntry: " + name);
430 
431         if (!name.endsWith(".img")) {
432             return false;
433         }
434 
435         String partitionName = name.substring(0, name.length() - 4);
436 
437         if (UNSUPPORTED_PARTITIONS.contains(partitionName)) {
438             Log.d(TAG, name + " installation is not supported, skip it.");
439             return false;
440         }
441 
442         long uncompressedSize = entry.getSize();
443 
444         installImage(partitionName, uncompressedSize, is);
445 
446         return true;
447     }
448 
installImage(String partitionName, long uncompressedSize, InputStream is)449     private void installImage(String partitionName, long uncompressedSize, InputStream is)
450             throws IOException, ImageValidationException {
451 
452         SparseInputStream sis = new SparseInputStream(new BufferedInputStream(is));
453 
454         long unsparseSize = sis.getUnsparseSize();
455 
456         final long partitionSize;
457 
458         if (unsparseSize != -1) {
459             partitionSize = unsparseSize;
460             Log.d(TAG, partitionName + " is sparse, raw size = " + unsparseSize);
461         } else if (uncompressedSize != -1) {
462             partitionSize = uncompressedSize;
463             Log.d(TAG, partitionName + " is already unsparse, raw size = " + uncompressedSize);
464         } else {
465             throw new IOException("Cannot get raw size for " + partitionName);
466         }
467 
468         Thread thread = new Thread(() -> {
469             mInstallationSession =
470                     mDynSystem.createPartition(partitionName, partitionSize, true);
471         });
472 
473         Log.d(TAG, "Start creating partition: " + partitionName);
474         thread.start();
475 
476         while (thread.isAlive()) {
477             if (isCancelled()) {
478                 return;
479             }
480 
481             try {
482                 Thread.sleep(100);
483             } catch (InterruptedException e) {
484                 // Ignore the error.
485             }
486         }
487 
488         if (mInstallationSession == null) {
489             throw new IOException(
490                     "Failed to start installation with requested size: " + partitionSize);
491         }
492 
493         Log.d(TAG, "Start installing: " + partitionName);
494 
495         MemoryFile memoryFile = new MemoryFile("dsu_" + partitionName, READ_BUFFER_SIZE);
496         ParcelFileDescriptor pfd = new ParcelFileDescriptor(memoryFile.getFileDescriptor());
497 
498         mInstallationSession.setAshmem(pfd, READ_BUFFER_SIZE);
499 
500         Progress progress = new Progress(partitionName, partitionSize, mNumInstalledPartitions++);
501 
502         long installedSize = 0;
503         byte[] bytes = new byte[READ_BUFFER_SIZE];
504         int numBytesRead;
505 
506         while ((numBytesRead = sis.read(bytes, 0, READ_BUFFER_SIZE)) != -1) {
507             if (isCancelled()) {
508                 return;
509             }
510 
511             memoryFile.writeBytes(bytes, 0, 0, numBytesRead);
512 
513             if (!mInstallationSession.submitFromAshmem(numBytesRead)) {
514                 throw new IOException("Failed write() to DynamicSystem");
515             }
516 
517             installedSize += numBytesRead;
518 
519             if (installedSize > progress.installedSize + MIN_PROGRESS_TO_PUBLISH) {
520                 progress.installedSize = installedSize;
521                 publishProgress(progress);
522             }
523         }
524 
525         AvbPublicKey avbPublicKey = new AvbPublicKey();
526         if (!mInstallationSession.getAvbPublicKey(avbPublicKey)) {
527             imageValidationThrowOrWarning(new PublicKeyException("getAvbPublicKey() failed"));
528         } else {
529             String publicKey = toHexString(avbPublicKey.sha1);
530             if (mKeyRevocationList.isRevoked(publicKey)) {
531                 imageValidationThrowOrWarning(new KeyRevokedException(publicKey));
532             }
533         }
534 
535         // Reset installation session and verify that installation completes successfully.
536         mInstallationSession = null;
537         if (!mDynSystem.closePartition()) {
538             throw new IOException("Failed to complete partition installation: " + partitionName);
539         }
540     }
541 
toHexString(byte[] bytes)542     private static String toHexString(byte[] bytes) {
543         StringBuilder sb = new StringBuilder();
544         for (byte b : bytes) {
545             sb.append(String.format("%02x", b));
546         }
547         return sb.toString();
548     }
549 
close()550     private void close() {
551         try {
552             if (mStream != null) {
553                 mStream.close();
554                 mStream = null;
555             }
556             if (mZipFile != null) {
557                 mZipFile.close();
558                 mZipFile = null;
559             }
560         } catch (IOException e) {
561             // ignore
562         }
563     }
564 
isCompleted()565     boolean isCompleted() {
566         return mIsCompleted;
567     }
568 
commit()569     boolean commit() {
570         return mDynSystem.setEnable(true, true);
571     }
572 }
573