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