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