blob: 42b620abe734e59d4c155a591ace4aadeb9750c2 [file] [log] [blame]
/*
* 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<String, Long, Throwable> {
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<String> 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<Integer, DynamicSystemManager.Session> 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<? extends ZipEntry> 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<? extends ZipEntry> 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<Integer, DynamicSystemManager.Session> 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<Boolean> 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);
}
}