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