/* * Copyright (C) 2019 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.dynsystem; import android.annotation.NonNull; import android.content.Context; import android.gsi.AvbPublicKey; import android.gsi.IGsiService; import android.net.Uri; import android.os.AsyncTask; import android.os.Build; import android.os.SharedMemory; import android.os.SystemProperties; import android.os.image.DynamicSystemManager; import android.service.persistentdata.PersistentDataBlockManager; import android.system.ErrnoException; import android.util.Log; import android.util.Pair; import android.util.Range; import android.webkit.URLUtil; import org.json.JSONException; import java.io.BufferedInputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.nio.ByteBuffer; import java.util.Arrays; import java.util.Enumeration; import java.util.List; import java.util.Locale; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.zip.GZIPInputStream; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import java.util.zip.ZipInputStream; class InstallationAsyncTask extends AsyncTask { private static final String TAG = "InstallationAsyncTask"; private static final int MIN_SHARED_MEMORY_SIZE = 8 << 10; // 8KiB private static final int MAX_SHARED_MEMORY_SIZE = 8 << 20; // 8MiB private static final int DEFAULT_SHARED_MEMORY_SIZE = 512 << 10; // 512KiB private static final String SHARED_MEMORY_SIZE_PROP = "dynamic_system.data_transfer.shared_memory.size"; private static final long MIN_PROGRESS_TO_PUBLISH = 1 << 27; private static final List UNSUPPORTED_PARTITIONS = Arrays.asList( "vbmeta", "boot", "userdata", "dtbo", "super_empty", "system_other", "scratch"); private class UnsupportedUrlException extends Exception { private UnsupportedUrlException(String message) { super(message); } } private class UnsupportedFormatException extends Exception { private UnsupportedFormatException(String message) { super(message); } } static class ImageValidationException extends Exception { ImageValidationException(String message) { super(message); } ImageValidationException(Throwable cause) { super(cause); } } static class RevocationListFetchException extends ImageValidationException { RevocationListFetchException(Throwable cause) { super(cause); } } static class KeyRevokedException extends ImageValidationException { KeyRevokedException(String message) { super(message); } } static class PublicKeyException extends ImageValidationException { PublicKeyException(String message) { super(message); } } static class InsufficientSpaceException extends IOException { InsufficientSpaceException(String message) { super(message); } } /** UNSET means the installation is not completed */ static final int RESULT_UNSET = 0; static final int RESULT_OK = 1; static final int RESULT_CANCELLED = 2; static final int RESULT_ERROR_IO = 3; static final int RESULT_ERROR_UNSUPPORTED_URL = 4; static final int RESULT_ERROR_UNSUPPORTED_FORMAT = 5; static final int RESULT_ERROR_EXCEPTION = 6; static class Progress { public final String partitionName; public final long installedBytes; public final long totalBytes; public final int partitionNumber; public final int totalPartitionNumber; public final int totalProgressPercentage; Progress( String partitionName, long installedBytes, long totalBytes, int partitionNumber, int totalPartitionNumber, int totalProgressPercentage) { this.partitionName = partitionName; this.installedBytes = installedBytes; this.totalBytes = totalBytes; this.partitionNumber = partitionNumber; this.totalPartitionNumber = totalPartitionNumber; this.totalProgressPercentage = totalProgressPercentage; } } interface ProgressListener { void onProgressUpdate(Progress progress); void onResult(int resultCode, Throwable detail); } private static class MappedMemoryBuffer implements AutoCloseable { public ByteBuffer mBuffer; MappedMemoryBuffer(@NonNull ByteBuffer buffer) { mBuffer = buffer; } @Override public void close() { if (mBuffer != null) { SharedMemory.unmap(mBuffer); mBuffer = null; } } } private final int mSharedMemorySize; private final String mUrl; private final String mDsuSlot; private final String mPublicKey; private final long mSystemSize; private final long mUserdataSize; private final Context mContext; private final DynamicSystemManager mDynSystem; private final ProgressListener mListener; private final boolean mIsNetworkUrl; private final boolean mIsDeviceBootloaderUnlocked; private final boolean mWantScratchPartition; private int mCreatePartitionStatus; private DynamicSystemManager.Session mInstallationSession; private KeyRevocationList mKeyRevocationList; private boolean mIsZip; private boolean mIsCompleted; private InputStream mStream; private ZipFile mZipFile; private static final double PROGRESS_READONLY_PARTITION_WEIGHT = 0.8; private static final double PROGRESS_WRITABLE_PARTITION_WEIGHT = 0.2; private String mProgressPartitionName; private long mProgressTotalBytes; private int mProgressPartitionNumber; private boolean mProgressPartitionIsReadonly; private int mProgressCompletedReadonlyPartitions; private int mProgressCompletedWritablePartitions; private int mTotalReadonlyPartitions; private int mTotalWritablePartitions; private int mTotalPartitionNumber; InstallationAsyncTask( String url, String dsuSlot, String publicKey, long systemSize, long userdataSize, Context context, DynamicSystemManager dynSystem, ProgressListener listener) { mSharedMemorySize = Range.create(MIN_SHARED_MEMORY_SIZE, MAX_SHARED_MEMORY_SIZE) .clamp( SystemProperties.getInt( SHARED_MEMORY_SIZE_PROP, DEFAULT_SHARED_MEMORY_SIZE)); mUrl = url; mDsuSlot = dsuSlot; mPublicKey = publicKey; mSystemSize = systemSize; mUserdataSize = userdataSize; mContext = context; mDynSystem = dynSystem; mListener = listener; mIsNetworkUrl = URLUtil.isNetworkUrl(mUrl); PersistentDataBlockManager pdbManager = (PersistentDataBlockManager) mContext.getSystemService(Context.PERSISTENT_DATA_BLOCK_SERVICE); mIsDeviceBootloaderUnlocked = (pdbManager != null) && (pdbManager.getFlashLockState() == PersistentDataBlockManager.FLASH_LOCK_UNLOCKED); mWantScratchPartition = Build.IS_DEBUGGABLE; } @Override protected Throwable doInBackground(String... voids) { Log.d(TAG, "Start doInBackground(), URL: " + mUrl); try { // call DynamicSystemManager to cleanup stuff mDynSystem.remove(); verifyAndPrepare(); mDynSystem.startInstallation(mDsuSlot); installUserdata(); if (isCancelled()) { mDynSystem.remove(); return null; } if (mUrl == null) { mDynSystem.finishInstallation(); return null; } installImages(); if (isCancelled()) { mDynSystem.remove(); return null; } if (mWantScratchPartition) { // If host is debuggable, then install a scratch partition so that we can do // adb remount in the guest system. try { installScratch(); } catch (IOException e) { // Failing to install overlayFS scratch shouldn't be fatal. // Just ignore the error and skip installing the scratch partition. Log.w(TAG, e.toString(), e); } if (isCancelled()) { mDynSystem.remove(); return null; } } mDynSystem.finishInstallation(); } catch (Exception e) { Log.e(TAG, e.toString(), e); mDynSystem.remove(); return e; } finally { close(); } return null; } @Override protected void onPostExecute(Throwable detail) { int result = RESULT_UNSET; if (detail == null) { result = RESULT_OK; mIsCompleted = true; } else if (detail instanceof IOException) { result = RESULT_ERROR_IO; } else if (detail instanceof UnsupportedUrlException) { result = RESULT_ERROR_UNSUPPORTED_URL; } else if (detail instanceof UnsupportedFormatException) { result = RESULT_ERROR_UNSUPPORTED_FORMAT; } else { result = RESULT_ERROR_EXCEPTION; } Log.d(TAG, "onPostExecute(), URL: " + mUrl + ", result: " + result); mListener.onResult(result, detail); } @Override protected void onCancelled() { Log.d(TAG, "onCancelled(), URL: " + mUrl); if (mDynSystem.abort()) { Log.d(TAG, "Installation aborted"); } else { Log.w(TAG, "DynamicSystemManager.abort() returned false"); } mListener.onResult(RESULT_CANCELLED, null); } @Override protected void onProgressUpdate(Long... progress) { final long installedBytes = progress[0]; int totalProgressPercentage = 0; if (mTotalPartitionNumber > 0) { final double readonlyPartitionWeight = mTotalReadonlyPartitions > 0 ? PROGRESS_READONLY_PARTITION_WEIGHT / mTotalReadonlyPartitions : 0; final double writablePartitionWeight = mTotalWritablePartitions > 0 ? PROGRESS_WRITABLE_PARTITION_WEIGHT / mTotalWritablePartitions : 0; double totalProgress = 0.0; if (mProgressTotalBytes > 0) { totalProgress += (mProgressPartitionIsReadonly ? readonlyPartitionWeight : writablePartitionWeight) * installedBytes / mProgressTotalBytes; } totalProgress += readonlyPartitionWeight * mProgressCompletedReadonlyPartitions; totalProgress += writablePartitionWeight * mProgressCompletedWritablePartitions; totalProgressPercentage = (int) (totalProgress * 100); } mListener.onProgressUpdate( new Progress( mProgressPartitionName, installedBytes, mProgressTotalBytes, mProgressPartitionNumber, mTotalPartitionNumber, totalProgressPercentage)); } private void initPartitionProgress(String partitionName, long totalBytes, boolean readonly) { if (mProgressPartitionNumber > 0) { // Assume previous partition completed successfully. if (mProgressPartitionIsReadonly) { ++mProgressCompletedReadonlyPartitions; } else { ++mProgressCompletedWritablePartitions; } } mProgressPartitionName = partitionName; mProgressTotalBytes = totalBytes; mProgressPartitionIsReadonly = readonly; ++mProgressPartitionNumber; } private void verifyAndPrepare() throws Exception { if (mUrl == null) { return; } String extension = mUrl.substring(mUrl.lastIndexOf('.') + 1); if ("gz".equals(extension) || "gzip".equals(extension)) { mIsZip = false; } else if ("zip".equals(extension)) { mIsZip = true; } else { throw new UnsupportedFormatException( String.format(Locale.US, "Unsupported file format: %s", mUrl)); } if (mIsNetworkUrl) { mStream = new URL(mUrl).openStream(); } else if (URLUtil.isFileUrl(mUrl)) { if (mIsZip) { mZipFile = new ZipFile(new File(new URL(mUrl).toURI())); } else { mStream = new URL(mUrl).openStream(); } } else if (URLUtil.isContentUrl(mUrl)) { mStream = mContext.getContentResolver().openInputStream(Uri.parse(mUrl)); } else { throw new UnsupportedUrlException( String.format(Locale.US, "Unsupported URL: %s", mUrl)); } boolean hasTotalPartitionNumber = false; if (mIsZip) { if (mZipFile != null) { // {*.img in zip} + {userdata} hasTotalPartitionNumber = true; mTotalReadonlyPartitions = calculateNumberOfImagesInLocalZip(mZipFile); mTotalWritablePartitions = 1; } else { // TODO: Come up with a way to retrieve the number of total partitions from // network URL. } } else { // gzip has exactly two partitions, {system, userdata} hasTotalPartitionNumber = true; mTotalReadonlyPartitions = 1; mTotalWritablePartitions = 1; } if (hasTotalPartitionNumber) { if (mWantScratchPartition) { // {scratch} ++mTotalWritablePartitions; } mTotalPartitionNumber = mTotalReadonlyPartitions + mTotalWritablePartitions; } try { String listUrl = mContext.getString(R.string.key_revocation_list_url); mKeyRevocationList = KeyRevocationList.fromUrl(new URL(listUrl)); } catch (IOException | JSONException e) { mKeyRevocationList = new KeyRevocationList(); imageValidationThrowOrWarning(new RevocationListFetchException(e)); } if (mKeyRevocationList.isRevoked(mPublicKey)) { imageValidationThrowOrWarning(new KeyRevokedException(mPublicKey)); } } private void imageValidationThrowOrWarning(ImageValidationException e) throws ImageValidationException { if (mIsDeviceBootloaderUnlocked || !mIsNetworkUrl) { // If device is OEM unlocked or DSU is being installed from a local file URI, // then be permissive. Log.w(TAG, e.toString()); } else { throw e; } } private void installWritablePartition(final String partitionName, final long partitionSize) throws IOException { Log.d(TAG, "Creating writable partition: " + partitionName + ", size: " + partitionSize); mCreatePartitionStatus = 0; mInstallationSession = null; Thread thread = new Thread() { @Override public void run() { Pair result = mDynSystem.createPartition( partitionName, partitionSize, /* readOnly = */ false); mCreatePartitionStatus = result.first; mInstallationSession = result.second; } }; initPartitionProgress(partitionName, partitionSize, /* readonly = */ false); publishProgress(/* installedSize = */ 0L); long prevInstalledSize = 0; thread.start(); while (thread.isAlive()) { if (isCancelled()) { return; } final long installedSize = mDynSystem.getInstallationProgress().bytes_processed; if (installedSize > prevInstalledSize + MIN_PROGRESS_TO_PUBLISH) { publishProgress(installedSize); prevInstalledSize = installedSize; } try { Thread.sleep(100); } catch (InterruptedException e) { // Ignore the error. } } if (mInstallationSession == null) { if (mCreatePartitionStatus == IGsiService.INSTALL_ERROR_NO_SPACE || mCreatePartitionStatus == IGsiService.INSTALL_ERROR_FILE_SYSTEM_CLUTTERED) { throw new InsufficientSpaceException( "Failed to create " + partitionName + " partition: storage media has insufficient free space"); } else { throw new IOException( "Failed to start installation with requested size: " + partitionSize); } } // Reset installation session and verify that installation completes successfully. mInstallationSession = null; if (!mDynSystem.closePartition()) { throw new IOException("Failed to complete partition installation: " + partitionName); } // Ensure a 100% mark is published. if (prevInstalledSize != partitionSize) { publishProgress(partitionSize); } } private void installScratch() throws IOException { installWritablePartition("scratch", mDynSystem.suggestScratchSize()); } private void installUserdata() throws IOException { installWritablePartition("userdata", mUserdataSize); } private void installImages() throws ExecutionException, IOException, ImageValidationException { if (mStream != null) { if (mIsZip) { installStreamingZipUpdate(); } else { installStreamingGzUpdate(); } } else { installLocalZipUpdate(); } } private void installStreamingGzUpdate() throws ExecutionException, IOException, ImageValidationException { Log.d(TAG, "To install a streaming GZ update"); installImage("system", mSystemSize, new GZIPInputStream(mStream)); } private boolean shouldInstallEntry(String name) { if (!name.endsWith(".img")) { return false; } String partitionName = name.substring(0, name.length() - 4); if (UNSUPPORTED_PARTITIONS.contains(partitionName)) { return false; } return true; } private int calculateNumberOfImagesInLocalZip(ZipFile zipFile) { int total = 0; Enumeration entries = zipFile.entries(); while (entries.hasMoreElements()) { ZipEntry entry = entries.nextElement(); if (shouldInstallEntry(entry.getName())) { ++total; } } return total; } private void installStreamingZipUpdate() throws ExecutionException, IOException, ImageValidationException { Log.d(TAG, "To install a streaming ZIP update"); ZipInputStream zis = new ZipInputStream(mStream); ZipEntry entry = null; while ((entry = zis.getNextEntry()) != null) { String name = entry.getName(); if (shouldInstallEntry(name)) { installImageFromAnEntry(entry, zis); } else { Log.d(TAG, name + " installation is not supported, skip it."); } if (isCancelled()) { break; } } } private void installLocalZipUpdate() throws ExecutionException, IOException, ImageValidationException { Log.d(TAG, "To install a local ZIP update"); Enumeration entries = mZipFile.entries(); while (entries.hasMoreElements()) { ZipEntry entry = entries.nextElement(); String name = entry.getName(); if (shouldInstallEntry(name)) { installImageFromAnEntry(entry, mZipFile.getInputStream(entry)); } else { Log.d(TAG, name + " installation is not supported, skip it."); } if (isCancelled()) { break; } } } private void installImageFromAnEntry(ZipEntry entry, InputStream is) throws ExecutionException, IOException, ImageValidationException { String name = entry.getName(); Log.d(TAG, "ZipEntry: " + name); String partitionName = name.substring(0, name.length() - 4); long uncompressedSize = entry.getSize(); installImage(partitionName, uncompressedSize, is); } private void installImage(String partitionName, long uncompressedSize, InputStream is) throws ExecutionException, IOException, ImageValidationException { SparseInputStream sis = new SparseInputStream(new BufferedInputStream(is)); long unsparseSize = sis.getUnsparseSize(); final long partitionSize; if (unsparseSize != -1) { partitionSize = unsparseSize; Log.d(TAG, partitionName + " is sparse, raw size = " + unsparseSize); } else if (uncompressedSize != -1) { partitionSize = uncompressedSize; Log.d(TAG, partitionName + " is already unsparse, raw size = " + uncompressedSize); } else { throw new IOException("Cannot get raw size for " + partitionName); } mCreatePartitionStatus = 0; mInstallationSession = null; Thread thread = new Thread() { @Override public void run() { Pair result = mDynSystem.createPartition( partitionName, partitionSize, /* readOnly = */ true); mCreatePartitionStatus = result.first; mInstallationSession = result.second; } }; Log.d(TAG, "Start creating partition: " + partitionName); thread.start(); while (thread.isAlive()) { if (isCancelled()) { return; } try { Thread.sleep(100); } catch (InterruptedException e) { // Ignore the error. } } if (mInstallationSession == null) { if (mCreatePartitionStatus == IGsiService.INSTALL_ERROR_NO_SPACE || mCreatePartitionStatus == IGsiService.INSTALL_ERROR_FILE_SYSTEM_CLUTTERED) { throw new InsufficientSpaceException( "Failed to create " + partitionName + " partition: storage media has insufficient free space"); } else { throw new IOException( "Failed to start installation with requested size: " + partitionSize); } } Log.d(TAG, "Start installing: " + partitionName); long prevInstalledSize = 0; try (SharedMemory sharedMemory = SharedMemory.create("dsu_buffer_" + partitionName, mSharedMemorySize); MappedMemoryBuffer mappedBuffer = new MappedMemoryBuffer(sharedMemory.mapReadWrite())) { mInstallationSession.setAshmem(sharedMemory.getFdDup(), sharedMemory.getSize()); initPartitionProgress(partitionName, partitionSize, /* readonly = */ true); publishProgress(/* installedSize = */ 0L); long installedSize = 0; byte[] readBuffer = new byte[sharedMemory.getSize()]; ByteBuffer buffer = mappedBuffer.mBuffer; ExecutorService executor = Executors.newSingleThreadExecutor(); Future submitPromise = null; while (true) { final int numBytesRead = sis.read(readBuffer, 0, readBuffer.length); if (submitPromise != null) { // Wait until the previous submit task is complete. while (true) { try { if (!submitPromise.get()) { throw new IOException("Failed submitFromAshmem() to DynamicSystem"); } break; } catch (InterruptedException e) { // Ignore. } } // Publish the progress of the previous submit task. if (installedSize > prevInstalledSize + MIN_PROGRESS_TO_PUBLISH) { publishProgress(installedSize); prevInstalledSize = installedSize; } } // Ensure the previous submit task (submitPromise) is complete before exiting the // loop. if (numBytesRead < 0) { break; } if (isCancelled()) { return; } buffer.position(0); buffer.put(readBuffer, 0, numBytesRead); submitPromise = executor.submit(() -> mInstallationSession.submitFromAshmem(numBytesRead)); // Even though we update the bytes counter here, the actual progress is updated only // after the submit task (submitPromise) is complete. installedSize += numBytesRead; } } catch (ErrnoException e) { e.rethrowAsIOException(); } AvbPublicKey avbPublicKey = new AvbPublicKey(); if (!mInstallationSession.getAvbPublicKey(avbPublicKey)) { imageValidationThrowOrWarning(new PublicKeyException("getAvbPublicKey() failed")); } else { String publicKey = toHexString(avbPublicKey.sha1); if (mKeyRevocationList.isRevoked(publicKey)) { imageValidationThrowOrWarning(new KeyRevokedException(publicKey)); } } // Reset installation session and verify that installation completes successfully. mInstallationSession = null; if (!mDynSystem.closePartition()) { throw new IOException("Failed to complete partition installation: " + partitionName); } // Ensure a 100% mark is published. if (prevInstalledSize != partitionSize) { publishProgress(partitionSize); } } private static String toHexString(byte[] bytes) { StringBuilder sb = new StringBuilder(); for (byte b : bytes) { sb.append(String.format("%02x", b)); } return sb.toString(); } private void close() { try { if (mStream != null) { mStream.close(); mStream = null; } if (mZipFile != null) { mZipFile.close(); mZipFile = null; } } catch (IOException e) { // ignore } } boolean isCompleted() { return mIsCompleted; } boolean commit(boolean oneShot) { return mDynSystem.setEnable(true, oneShot); } }