From e4de5a0d3b6e0c897c1cea0912b58e11db962365 Mon Sep 17 00:00:00 2001 From: Xiaohui Chen Date: Tue, 22 Sep 2015 15:33:31 -0700 Subject: Cleanup OWNER references. Bug: 19913735 Change-Id: I2150c6baaab80fe11312e4401394a2a8da52e595 --- core/java/android/os/RecoverySystem.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'core/java/android/os/RecoverySystem.java') diff --git a/core/java/android/os/RecoverySystem.java b/core/java/android/os/RecoverySystem.java index 8c544f44f244..41de579201a5 100644 --- a/core/java/android/os/RecoverySystem.java +++ b/core/java/android/os/RecoverySystem.java @@ -410,7 +410,7 @@ public class RecoverySystem { Intent intent = new Intent("android.intent.action.MASTER_CLEAR_NOTIFICATION"); intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND); - context.sendOrderedBroadcastAsUser(intent, UserHandle.OWNER, + context.sendOrderedBroadcastAsUser(intent, UserHandle.SYSTEM, android.Manifest.permission.MASTER_CLEAR, new BroadcastReceiver() { @Override -- cgit v1.2.3 From 9ad08ec5be0d1e225c9f463fd395ba852b6b5bba Mon Sep 17 00:00:00 2001 From: Przemyslaw Szczepaniak Date: Thu, 9 Jul 2015 13:28:30 +0100 Subject: Switch RecoverySystem impl to use sun.security.pkcs Due to org.apache.harmony.security package removal, RecoverySystem#verifyPackage was rewritten to use sun.security.pkcs package for verifining package signature. (cherry-picked from 84acbd76f7e1300e8404ac1b94f008826f9cc0fb) Change-Id: I3a2058982beadab1aaae793c25db5c6f7387a72b --- core/java/android/os/RecoverySystem.java | 164 +++++++++++++------------------ 1 file changed, 66 insertions(+), 98 deletions(-) (limited to 'core/java/android/os/RecoverySystem.java') diff --git a/core/java/android/os/RecoverySystem.java b/core/java/android/os/RecoverySystem.java index 45355723149c..407ff08ef2de 100644 --- a/core/java/android/os/RecoverySystem.java +++ b/core/java/android/os/RecoverySystem.java @@ -44,11 +44,8 @@ import java.util.Locale; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; -import org.apache.harmony.security.asn1.BerInputStream; -import org.apache.harmony.security.pkcs7.ContentInfo; -import org.apache.harmony.security.pkcs7.SignedData; -import org.apache.harmony.security.pkcs7.SignerInfo; -import org.apache.harmony.security.x509.Certificate; +import sun.security.pkcs.PKCS7; +import sun.security.pkcs.SignerInfo; /** * RecoverySystem contains methods for interacting with the Android @@ -150,14 +147,13 @@ public class RecoverySystem { ProgressListener listener, File deviceCertsZipFile) throws IOException, GeneralSecurityException { - long fileLen = packageFile.length(); + final long fileLen = packageFile.length(); - RandomAccessFile raf = new RandomAccessFile(packageFile, "r"); + final RandomAccessFile raf = new RandomAccessFile(packageFile, "r"); try { - int lastPercent = 0; - long lastPublishTime = System.currentTimeMillis(); + final long startTimeMillis = System.currentTimeMillis(); if (listener != null) { - listener.onProgress(lastPercent); + listener.onProgress(0); } raf.seek(fileLen - 6); @@ -168,8 +164,8 @@ public class RecoverySystem { throw new SignatureException("no signature in file (no footer)"); } - int commentSize = (footer[4] & 0xff) | ((footer[5] & 0xff) << 8); - int signatureStart = (footer[0] & 0xff) | ((footer[1] & 0xff) << 8); + final int commentSize = (footer[4] & 0xff) | ((footer[5] & 0xff) << 8); + final int signatureStart = (footer[0] & 0xff) | ((footer[1] & 0xff) << 8); byte[] eocd = new byte[commentSize + 22]; raf.seek(fileLen - (commentSize + 22)); @@ -189,51 +185,30 @@ public class RecoverySystem { } } - // The following code is largely copied from - // JarUtils.verifySignature(). We could just *call* that - // method here if that function didn't read the entire - // input (ie, the whole OTA package) into memory just to - // compute its message digest. - - BerInputStream bis = new BerInputStream( - new ByteArrayInputStream(eocd, commentSize+22-signatureStart, signatureStart)); - ContentInfo info = (ContentInfo)ContentInfo.ASN1.decode(bis); - SignedData signedData = info.getSignedData(); - if (signedData == null) { - throw new IOException("signedData is null"); - } - List encCerts = signedData.getCertificates(); - if (encCerts.isEmpty()) { - throw new IOException("encCerts is empty"); - } + // Parse the signature + PKCS7 block = + new PKCS7(new ByteArrayInputStream(eocd, commentSize+22-signatureStart, signatureStart)); + // Take the first certificate from the signature (packages // should contain only one). - Iterator it = encCerts.iterator(); - X509Certificate cert = null; - if (it.hasNext()) { - CertificateFactory cf = CertificateFactory.getInstance("X.509"); - InputStream is = new ByteArrayInputStream(it.next().getEncoded()); - cert = (X509Certificate) cf.generateCertificate(is); - } else { + X509Certificate[] certificates = block.getCertificates(); + if (certificates == null || certificates.length == 0) { throw new SignatureException("signature contains no certificates"); } + X509Certificate cert = certificates[0]; + PublicKey signatureKey = cert.getPublicKey(); - List sigInfos = signedData.getSignerInfos(); - SignerInfo sigInfo; - if (!sigInfos.isEmpty()) { - sigInfo = (SignerInfo)sigInfos.get(0); - } else { - throw new IOException("no signer infos!"); + SignerInfo[] signerInfos = block.getSignerInfos(); + if (signerInfos == null || signerInfos.length == 0) { + throw new SignatureException("signature contains no signedData"); } + SignerInfo signerInfo = signerInfos[0]; // Check that the public key of the certificate contained // in the package equals one of our trusted public keys. - + boolean verified = false; HashSet trusted = getTrustedCerts( deviceCertsZipFile == null ? DEFAULT_KEYSTORE : deviceCertsZipFile); - - PublicKey signatureKey = cert.getPublicKey(); - boolean verified = false; for (X509Certificate c : trusted) { if (c.getPublicKey().equals(signatureKey)) { verified = true; @@ -246,61 +221,54 @@ public class RecoverySystem { // The signature cert matches a trusted key. Now verify that // the digest in the cert matches the actual file data. - - // The verifier in recovery only handles SHA1withRSA and - // SHA256withRSA signatures. SignApk chooses which to use - // based on the signature algorithm of the cert: - // - // "SHA256withRSA" cert -> "SHA256withRSA" signature - // "SHA1withRSA" cert -> "SHA1withRSA" signature - // "MD5withRSA" cert -> "SHA1withRSA" signature (for backwards compatibility) - // any other cert -> SignApk fails - // - // Here we ignore whatever the cert says, and instead use - // whatever algorithm is used by the signature. - - String da = sigInfo.getDigestAlgorithm(); - String dea = sigInfo.getDigestEncryptionAlgorithm(); - String alg = null; - if (da == null || dea == null) { - // fall back to the cert algorithm if the sig one - // doesn't look right. - alg = cert.getSigAlgName(); - } else { - alg = da + "with" + dea; - } - Signature sig = Signature.getInstance(alg); - sig.initVerify(cert); - - // The signature covers all of the OTA package except the - // archive comment and its 2-byte length. - long toRead = fileLen - commentSize - 2; - long soFar = 0; raf.seek(0); - byte[] buffer = new byte[4096]; - boolean interrupted = false; - while (soFar < toRead) { - interrupted = Thread.interrupted(); - if (interrupted) break; - int size = buffer.length; - if (soFar + size > toRead) { - size = (int)(toRead - soFar); + final ProgressListener listenerForInner = listener; + SignerInfo verifyResult = block.verify(signerInfo, new InputStream() { + // The signature covers all of the OTA package except the + // archive comment and its 2-byte length. + long toRead = fileLen - commentSize - 2; + long soFar = 0; + + int lastPercent = 0; + long lastPublishTime = startTimeMillis; + + @Override + public int read() throws IOException { + throw new UnsupportedOperationException(); } - int read = raf.read(buffer, 0, size); - sig.update(buffer, 0, read); - soFar += read; - - if (listener != null) { - long now = System.currentTimeMillis(); - int p = (int)(soFar * 100 / toRead); - if (p > lastPercent && - now - lastPublishTime > PUBLISH_PROGRESS_INTERVAL_MS) { - lastPercent = p; - lastPublishTime = now; - listener.onProgress(lastPercent); + + @Override + public int read(byte[] b, int off, int len) throws IOException { + if (soFar >= toRead) { + return -1; + } + if (Thread.currentThread().isInterrupted()) { + return -1; + } + + int size = len; + if (soFar + size > toRead) { + size = (int)(toRead - soFar); + } + int read = raf.read(b, off, size); + soFar += read; + + if (listenerForInner != null) { + long now = System.currentTimeMillis(); + int p = (int)(soFar * 100 / toRead); + if (p > lastPercent && + now - lastPublishTime > PUBLISH_PROGRESS_INTERVAL_MS) { + lastPercent = p; + lastPublishTime = now; + listenerForInner.onProgress(lastPercent); + } } + + return read; } - } + }); + + final boolean interrupted = Thread.interrupted(); if (listener != null) { listener.onProgress(100); } @@ -309,7 +277,7 @@ public class RecoverySystem { throw new SignatureException("verification was interrupted"); } - if (!sig.verify(sigInfo.getEncryptedDigest())) { + if (verifyResult == null) { throw new SignatureException("signature digest verification failed"); } } finally { -- cgit v1.2.3 From e8217ff4a725004e495ed1506928334f97e5bbf1 Mon Sep 17 00:00:00 2001 From: Tao Bao Date: Mon, 1 Feb 2016 16:08:18 -0800 Subject: Condionally remove the block map file. We used to unconditionally remove the block map file on boot. Because the package might be half-way uncrypt'd in a corrupt state. CL in [1] changes uncrypt to ensure that block.map only gets created at the end of a successful uncrypt. So we can change to keep the fully uncrypt'd package and the block map. This is to reduce the work for GmsCore to avoid re-downloading everything again. [1]: commit 25dd0386fe69460cd1d39de116197dd2c7bf9ec2. Bug: 26883096 Change-Id: I58ca22064141bf5d42fa48146a980712c8ce21d9 --- core/java/android/os/RecoverySystem.java | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) (limited to 'core/java/android/os/RecoverySystem.java') diff --git a/core/java/android/os/RecoverySystem.java b/core/java/android/os/RecoverySystem.java index 407ff08ef2de..154c9bbab312 100644 --- a/core/java/android/os/RecoverySystem.java +++ b/core/java/android/os/RecoverySystem.java @@ -67,6 +67,7 @@ public class RecoverySystem { /** Used to communicate with recovery. See bootable/recovery/recovery.cpp. */ private static File RECOVERY_DIR = new File("/cache/recovery"); + private static File BLOCK_MAP_FILE = new File(RECOVERY_DIR, "block.map"); private static File COMMAND_FILE = new File(RECOVERY_DIR, "command"); private static File UNCRYPT_FILE = new File(RECOVERY_DIR, "uncrypt_file"); private static File LOG_FILE = new File(RECOVERY_DIR, "log"); @@ -473,7 +474,9 @@ public class RecoverySystem { Log.e(TAG, "Error reading recovery log", e); } - if (UNCRYPT_FILE.exists()) { + // Only remove the OTA package if it's partially processed (uncrypt'd). + boolean reservePackage = BLOCK_MAP_FILE.exists(); + if (!reservePackage && UNCRYPT_FILE.exists()) { String filename = null; try { filename = FileUtils.readTextFile(UNCRYPT_FILE, 0, null); @@ -492,11 +495,18 @@ public class RecoverySystem { } } - // Delete everything in RECOVERY_DIR except those beginning - // with LAST_PREFIX + // We keep the update logs (beginning with LAST_PREFIX), and optionally + // the block map file (BLOCK_MAP_FILE) for a package. BLOCK_MAP_FILE + // will be created at the end of a successful uncrypt. If seeing this + // file, we keep the block map file and the file that contains the + // package name (UNCRYPT_FILE). This is to reduce the work for GmsCore + // to avoid re-downloading everything again. String[] names = RECOVERY_DIR.list(); for (int i = 0; names != null && i < names.length; i++) { if (names[i].startsWith(LAST_PREFIX)) continue; + if (reservePackage && names[i].equals(BLOCK_MAP_FILE.getName())) continue; + if (reservePackage && names[i].equals(UNCRYPT_FILE.getName())) continue; + recursiveDelete(new File(RECOVERY_DIR, names[i])); } -- cgit v1.2.3 From e8a403d57c8ea540f8287cdaee8b90f0cf9626a3 Mon Sep 17 00:00:00 2001 From: Tao Bao Date: Thu, 31 Dec 2015 07:44:55 -0800 Subject: Add support for update-on-boot feature. Add a separate system service RecoverySystemService to handle recovery related requests (calling uncrypt to de-encrypt the OTA package on the /data partition, setting up bootloader control block (aka BCB) and etc). We used to trigger uncrypt in ShutdownThread before rebooting into recovery. Now we expose new SystemApi (RecoverySystem.processPackage()) to allow the caller (e.g. GmsCore) to call that upfront before initiating a reboot. This will reduce the reboot time and get rid of the progress bar ("processing update package"). However, we need to reserve the functionality in ShutdownThread to optionally call uncrypt if finding that's still needed. In order to support the update-on-boot feature, we also add new SystemApis scheduleUpdateOnBoot() and cancelScheduledUpdate() into android.os.RecoverySystem. They allow the caller (e.g. GmsCore) to schedule / cancel an update by setting up the BCB, which will be read by the bootloader and the recovery image. With the new SystemApis, an update package can be processed (uncrypt'd) in the background and scheduled to be installed at the next boot. Bug: 26830925 Change-Id: Ic606fcf5b31c54ce54f0ab12c1768fef0fa64560 --- core/java/android/os/RecoverySystem.java | 333 ++++++++++++++++++++++++++----- 1 file changed, 288 insertions(+), 45 deletions(-) (limited to 'core/java/android/os/RecoverySystem.java') diff --git a/core/java/android/os/RecoverySystem.java b/core/java/android/os/RecoverySystem.java index 154c9bbab312..ddcd63520549 100644 --- a/core/java/android/os/RecoverySystem.java +++ b/core/java/android/os/RecoverySystem.java @@ -16,6 +16,7 @@ package android.os; +import android.annotation.SystemApi; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; @@ -66,15 +67,34 @@ public class RecoverySystem { private static final long PUBLISH_PROGRESS_INTERVAL_MS = 500; /** Used to communicate with recovery. See bootable/recovery/recovery.cpp. */ - private static File RECOVERY_DIR = new File("/cache/recovery"); - private static File BLOCK_MAP_FILE = new File(RECOVERY_DIR, "block.map"); - private static File COMMAND_FILE = new File(RECOVERY_DIR, "command"); - private static File UNCRYPT_FILE = new File(RECOVERY_DIR, "uncrypt_file"); - private static File LOG_FILE = new File(RECOVERY_DIR, "log"); - private static String LAST_PREFIX = "last_"; + private static final File RECOVERY_DIR = new File("/cache/recovery"); + private static final File LOG_FILE = new File(RECOVERY_DIR, "log"); + private static final String LAST_PREFIX = "last_"; + + /** + * The recovery image uses this file to identify the location (i.e. blocks) + * of an OTA package on the /data partition. The block map file is + * generated by uncrypt. + * + * @hide + */ + public static final File BLOCK_MAP_FILE = new File(RECOVERY_DIR, "block.map"); + + /** + * UNCRYPT_PACKAGE_FILE stores the filename to be uncrypt'd, which will be + * read by uncrypt. + * + * @hide + */ + public static final File UNCRYPT_PACKAGE_FILE = new File(RECOVERY_DIR, "uncrypt_file"); // Length limits for reading files. - private static int LOG_FILE_MAX_LENGTH = 64 * 1024; + private static final int LOG_FILE_MAX_LENGTH = 64 * 1024; + + // Prevent concurrent execution of requests. + private static final Object sRequestLock = new Object(); + + private final IRecoverySystem mService; /** * Interface definition for a callback to be invoked regularly as @@ -286,6 +306,89 @@ public class RecoverySystem { } } + /** + * Process a given package with uncrypt. No-op if the package is not on the + * /data partition. + * + * @param Context the Context to use + * @param packageFile the package to be processed + * @param listener an object to receive periodic progress updates as + * processing proceeds. May be null. + * @param handler the Handler upon which the callbacks will be + * executed. + * + * @throws IOException if there were any errors processing the package file. + * + * @hide + */ + @SystemApi + public static void processPackage(Context context, + File packageFile, + final ProgressListener listener, + final Handler handler) + throws IOException { + String filename = packageFile.getCanonicalPath(); + if (!filename.startsWith("/data/")) { + return; + } + + RecoverySystem rs = (RecoverySystem) context.getSystemService(Context.RECOVERY_SERVICE); + IRecoverySystemProgressListener progressListener = null; + if (listener != null) { + final Handler progressHandler; + if (handler != null) { + progressHandler = handler; + } else { + progressHandler = new Handler(context.getMainLooper()); + } + progressListener = new IRecoverySystemProgressListener.Stub() { + int lastProgress = 0; + long lastPublishTime = System.currentTimeMillis(); + + @Override + public void onProgress(final int progress) { + final long now = System.currentTimeMillis(); + progressHandler.post(new Runnable() { + @Override + public void run() { + if (progress > lastProgress && + now - lastPublishTime > PUBLISH_PROGRESS_INTERVAL_MS) { + lastProgress = progress; + lastPublishTime = now; + listener.onProgress(progress); + } + } + }); + } + }; + } + + if (!rs.uncrypt(filename, progressListener)) { + throw new IOException("process package failed"); + } + } + + /** + * Process a given package with uncrypt. No-op if the package is not on the + * /data partition. + * + * @param Context the Context to use + * @param packageFile the package to be processed + * @param listener an object to receive periodic progress updates as + * processing proceeds. May be null. + * + * @throws IOException if there were any errors processing the package file. + * + * @hide + */ + @SystemApi + public static void processPackage(Context context, + File packageFile, + final ProgressListener listener) + throws IOException { + processPackage(context, packageFile, listener, null); + } + /** * Reboots the device in order to install the given update * package. @@ -301,30 +404,127 @@ public class RecoverySystem { * fails, or if the reboot itself fails. */ public static void installPackage(Context context, File packageFile) - throws IOException { - String filename = packageFile.getCanonicalPath(); + throws IOException { + installPackage(context, packageFile, false); + } - FileWriter uncryptFile = new FileWriter(UNCRYPT_FILE); - try { - uncryptFile.write(filename + "\n"); - } finally { - uncryptFile.close(); - } - // UNCRYPT_FILE needs to be readable by system server on bootup. - if (!UNCRYPT_FILE.setReadable(true, false)) { - Log.e(TAG, "Error setting readable for " + UNCRYPT_FILE.getCanonicalPath()); + /** + * If the package hasn't been processed (i.e. uncrypt'd), set up + * UNCRYPT_PACKAGE_FILE and delete BLOCK_MAP_FILE to trigger uncrypt during the + * reboot. + * + * @param context the Context to use + * @param packageFile the update package to install. Must be on a + * partition mountable by recovery. + * @param processed if the package has been processed (uncrypt'd). + * + * @throws IOException if writing the recovery command file fails, or if + * the reboot itself fails. + * + * @hide + */ + @SystemApi + public static void installPackage(Context context, File packageFile, boolean processed) + throws IOException { + synchronized (sRequestLock) { + LOG_FILE.delete(); + // Must delete the file in case it was created by system server. + UNCRYPT_PACKAGE_FILE.delete(); + + String filename = packageFile.getCanonicalPath(); + Log.w(TAG, "!!! REBOOTING TO INSTALL " + filename + " !!!"); + + if (!processed && filename.startsWith("/data/")) { + FileWriter uncryptFile = new FileWriter(UNCRYPT_PACKAGE_FILE); + try { + uncryptFile.write(filename + "\n"); + } finally { + uncryptFile.close(); + } + // UNCRYPT_PACKAGE_FILE needs to be readable and writable by system server. + if (!UNCRYPT_PACKAGE_FILE.setReadable(true, false) + || !UNCRYPT_PACKAGE_FILE.setWritable(true, false)) { + Log.e(TAG, "Error setting permission for " + UNCRYPT_PACKAGE_FILE); + } + + BLOCK_MAP_FILE.delete(); + } + + // If the package is on the /data partition, use the block map file as + // the package name instead. + if (filename.startsWith("/data/")) { + filename = "@/cache/recovery/block.map"; + } + + final String filenameArg = "--update_package=" + filename + "\n"; + final String localeArg = "--locale=" + Locale.getDefault().toString() + "\n"; + final String command = filenameArg + localeArg; + + RecoverySystem rs = (RecoverySystem) context.getSystemService( + Context.RECOVERY_SERVICE); + if (!rs.setupBcb(command)) { + throw new IOException("Setup BCB failed"); + } + + // Having set up the BCB (bootloader control block), go ahead and reboot + PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE); + pm.reboot(PowerManager.REBOOT_RECOVERY_UPDATE); + + throw new IOException("Reboot failed (no permissions?)"); } - Log.w(TAG, "!!! REBOOTING TO INSTALL " + filename + " !!!"); + } - // If the package is on the /data partition, write the block map file - // into COMMAND_FILE instead. + /** + * Schedule to install the given package on next boot. The caller needs to + * ensure that the package must have been processed (uncrypt'd) if needed. + * It sets up the command in BCB (bootloader control block), which will + * be read by the bootloader and the recovery image. + * + * @param Context the Context to use. + * @param packageFile the package to be installed. + * + * @throws IOException if there were any errors setting up the BCB. + * + * @hide + */ + @SystemApi + public static void scheduleUpdateOnBoot(Context context, File packageFile) + throws IOException { + String filename = packageFile.getCanonicalPath(); + + // If the package is on the /data partition, use the block map file as + // the package name instead. if (filename.startsWith("/data/")) { filename = "@/cache/recovery/block.map"; } - final String filenameArg = "--update_package=" + filename; - final String localeArg = "--locale=" + Locale.getDefault().toString(); - bootCommand(context, filenameArg, localeArg); + final String filenameArg = "--update_package=" + filename + "\n"; + final String localeArg = "--locale=" + Locale.getDefault().toString() + "\n"; + final String command = filenameArg + localeArg; + + RecoverySystem rs = (RecoverySystem) context.getSystemService(Context.RECOVERY_SERVICE); + if (!rs.setupBcb(command)) { + throw new IOException("schedule update on boot failed"); + } + } + + /** + * Cancel any scheduled update by clearing up the BCB (bootloader control + * block). + * + * @param Context the Context to use. + * + * @throws IOException if there were any errors clearing up the BCB. + * + * @hide + */ + @SystemApi + public static void cancelScheduledUpdate(Context context) + throws IOException { + RecoverySystem rs = (RecoverySystem) context.getSystemService(Context.RECOVERY_SERVICE); + if (!rs.clearBcb()) { + throw new IOException("cancel scheduled update failed"); + } } /** @@ -434,27 +634,28 @@ public class RecoverySystem { * @throws IOException if something goes wrong. */ private static void bootCommand(Context context, String... args) throws IOException { - RECOVERY_DIR.mkdirs(); // In case we need it - COMMAND_FILE.delete(); // In case it's not writable - LOG_FILE.delete(); + synchronized (sRequestLock) { + LOG_FILE.delete(); - FileWriter command = new FileWriter(COMMAND_FILE); - try { + StringBuilder command = new StringBuilder(); for (String arg : args) { if (!TextUtils.isEmpty(arg)) { - command.write(arg); - command.write("\n"); + command.append(arg); + command.append("\n"); } } - } finally { - command.close(); - } - // Having written the command file, go ahead and reboot - PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE); - pm.reboot(PowerManager.REBOOT_RECOVERY); + // Write the command into BCB (bootloader control block). + RecoverySystem rs = (RecoverySystem) context.getSystemService( + Context.RECOVERY_SERVICE); + rs.setupBcb(command.toString()); - throw new IOException("Reboot failed (no permissions?)"); + // Having set up the BCB, go ahead and reboot. + PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE); + pm.reboot(PowerManager.REBOOT_RECOVERY); + + throw new IOException("Reboot failed (no permissions?)"); + } } /** @@ -476,10 +677,10 @@ public class RecoverySystem { // Only remove the OTA package if it's partially processed (uncrypt'd). boolean reservePackage = BLOCK_MAP_FILE.exists(); - if (!reservePackage && UNCRYPT_FILE.exists()) { + if (!reservePackage && UNCRYPT_PACKAGE_FILE.exists()) { String filename = null; try { - filename = FileUtils.readTextFile(UNCRYPT_FILE, 0, null); + filename = FileUtils.readTextFile(UNCRYPT_PACKAGE_FILE, 0, null); } catch (IOException e) { Log.e(TAG, "Error reading uncrypt file", e); } @@ -487,7 +688,7 @@ public class RecoverySystem { // Remove the OTA package on /data that has been (possibly // partially) processed. (Bug: 24973532) if (filename != null && filename.startsWith("/data")) { - if (UNCRYPT_FILE.delete()) { + if (UNCRYPT_PACKAGE_FILE.delete()) { Log.i(TAG, "Deleted: " + filename); } else { Log.e(TAG, "Can't delete: " + filename); @@ -499,13 +700,13 @@ public class RecoverySystem { // the block map file (BLOCK_MAP_FILE) for a package. BLOCK_MAP_FILE // will be created at the end of a successful uncrypt. If seeing this // file, we keep the block map file and the file that contains the - // package name (UNCRYPT_FILE). This is to reduce the work for GmsCore - // to avoid re-downloading everything again. + // package name (UNCRYPT_PACKAGE_FILE). This is to reduce the work for + // GmsCore to avoid re-downloading everything again. String[] names = RECOVERY_DIR.list(); for (int i = 0; names != null && i < names.length; i++) { if (names[i].startsWith(LAST_PREFIX)) continue; if (reservePackage && names[i].equals(BLOCK_MAP_FILE.getName())) continue; - if (reservePackage && names[i].equals(UNCRYPT_FILE.getName())) continue; + if (reservePackage && names[i].equals(UNCRYPT_PACKAGE_FILE.getName())) continue; recursiveDelete(new File(RECOVERY_DIR, names[i])); } @@ -532,6 +733,39 @@ public class RecoverySystem { } } + /** + * Talks to RecoverySystemService via Binder to trigger uncrypt. + */ + private boolean uncrypt(String packageFile, IRecoverySystemProgressListener listener) { + try { + return mService.uncrypt(packageFile, listener); + } catch (RemoteException unused) { + } + return false; + } + + /** + * Talks to RecoverySystemService via Binder to set up the BCB. + */ + private boolean setupBcb(String command) { + try { + return mService.setupBcb(command); + } catch (RemoteException unused) { + } + return false; + } + + /** + * Talks to RecoverySystemService via Binder to clear up the BCB. + */ + private boolean clearBcb() { + try { + return mService.clearBcb(); + } catch (RemoteException unused) { + } + return false; + } + /** * Internally, recovery treats each line of the command file as a separate * argv, so we only need to protect against newlines and nulls. @@ -546,5 +780,14 @@ public class RecoverySystem { /** * @removed Was previously made visible by accident. */ - public RecoverySystem() { } + public RecoverySystem() { + mService = null; + } + + /** + * @hide + */ + public RecoverySystem(IRecoverySystem service) { + mService = service; + } } -- cgit v1.2.3 From 36baafe92cdf9139ec9c2215cbe62d6df1b32b3f Mon Sep 17 00:00:00 2001 From: Tao Bao Date: Mon, 14 Mar 2016 17:11:05 -0700 Subject: Don't reboot into recovery if block map file is missing. We added a third parameter to RecoverySystem.installPackage() to let the caller to indicate the package has been processed (uncrypt'd). We need to ensure the caller's claim is true by checking the existence of the block map. Otherwise the device will fail for sure when booting into recovery. Bug: 27620932 Change-Id: I6325455253480055f14eb0cf020689ac37328602 --- core/java/android/os/RecoverySystem.java | 44 ++++++++++++++++++++------------ 1 file changed, 27 insertions(+), 17 deletions(-) (limited to 'core/java/android/os/RecoverySystem.java') diff --git a/core/java/android/os/RecoverySystem.java b/core/java/android/os/RecoverySystem.java index ddcd63520549..b25b33d00aa8 100644 --- a/core/java/android/os/RecoverySystem.java +++ b/core/java/android/os/RecoverySystem.java @@ -434,25 +434,35 @@ public class RecoverySystem { String filename = packageFile.getCanonicalPath(); Log.w(TAG, "!!! REBOOTING TO INSTALL " + filename + " !!!"); - if (!processed && filename.startsWith("/data/")) { - FileWriter uncryptFile = new FileWriter(UNCRYPT_PACKAGE_FILE); - try { - uncryptFile.write(filename + "\n"); - } finally { - uncryptFile.close(); - } - // UNCRYPT_PACKAGE_FILE needs to be readable and writable by system server. - if (!UNCRYPT_PACKAGE_FILE.setReadable(true, false) - || !UNCRYPT_PACKAGE_FILE.setWritable(true, false)) { - Log.e(TAG, "Error setting permission for " + UNCRYPT_PACKAGE_FILE); - } + // If the package is on the /data partition, the package needs to + // be processed (i.e. uncrypt'd). The caller specifies if that has + // been done in 'processed' parameter. + if (filename.startsWith("/data/")) { + if (processed) { + if (!BLOCK_MAP_FILE.exists()) { + Log.e(TAG, "Package claimed to have been processed but failed to find " + + "the block map file."); + throw new IOException("Failed to find block map file"); + } + } else { + FileWriter uncryptFile = new FileWriter(UNCRYPT_PACKAGE_FILE); + try { + uncryptFile.write(filename + "\n"); + } finally { + uncryptFile.close(); + } + // UNCRYPT_PACKAGE_FILE needs to be readable and writable + // by system server. + if (!UNCRYPT_PACKAGE_FILE.setReadable(true, false) + || !UNCRYPT_PACKAGE_FILE.setWritable(true, false)) { + Log.e(TAG, "Error setting permission for " + UNCRYPT_PACKAGE_FILE); + } - BLOCK_MAP_FILE.delete(); - } + BLOCK_MAP_FILE.delete(); + } - // If the package is on the /data partition, use the block map file as - // the package name instead. - if (filename.startsWith("/data/")) { + // If the package is on the /data partition, use the block map + // file as the package name instead. filename = "@/cache/recovery/block.map"; } -- cgit v1.2.3 From ac75f1effae79d4bccd3faf65f9a281824a2803e Mon Sep 17 00:00:00 2001 From: Tianjie Xu Date: Thu, 28 Apr 2016 16:44:42 -0700 Subject: Set security update label in framework GmsCore will use different filenames to distinguish a security update from a normal update. (update.zip for normal update and update_s.zip for security update.) So, if framework observes the filename as "update_s.zip", write command "--security" to BCB. This cmd ask the recovery image to choose the right background string for update. Bug: 27837319 Change-Id: I2ef12267a6be57d8a81f7f9f34c09aea54530c1f --- core/java/android/os/RecoverySystem.java | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) (limited to 'core/java/android/os/RecoverySystem.java') diff --git a/core/java/android/os/RecoverySystem.java b/core/java/android/os/RecoverySystem.java index b25b33d00aa8..403e06ca8553 100644 --- a/core/java/android/os/RecoverySystem.java +++ b/core/java/android/os/RecoverySystem.java @@ -434,6 +434,9 @@ public class RecoverySystem { String filename = packageFile.getCanonicalPath(); Log.w(TAG, "!!! REBOOTING TO INSTALL " + filename + " !!!"); + // If the package name ends with "_s.zip", it's a security update. + boolean securityUpdate = filename.endsWith("_s.zip"); + // If the package is on the /data partition, the package needs to // be processed (i.e. uncrypt'd). The caller specifies if that has // been done in 'processed' parameter. @@ -468,7 +471,12 @@ public class RecoverySystem { final String filenameArg = "--update_package=" + filename + "\n"; final String localeArg = "--locale=" + Locale.getDefault().toString() + "\n"; - final String command = filenameArg + localeArg; + final String securityArg = "--security\n"; + + String command = filenameArg + localeArg; + if (securityUpdate) { + command += securityArg; + } RecoverySystem rs = (RecoverySystem) context.getSystemService( Context.RECOVERY_SERVICE); @@ -501,6 +509,7 @@ public class RecoverySystem { public static void scheduleUpdateOnBoot(Context context, File packageFile) throws IOException { String filename = packageFile.getCanonicalPath(); + boolean securityUpdate = filename.endsWith("_s.zip"); // If the package is on the /data partition, use the block map file as // the package name instead. @@ -510,7 +519,12 @@ public class RecoverySystem { final String filenameArg = "--update_package=" + filename + "\n"; final String localeArg = "--locale=" + Locale.getDefault().toString() + "\n"; - final String command = filenameArg + localeArg; + final String securityArg = "--security\n"; + + String command = filenameArg + localeArg; + if (securityUpdate) { + command += securityArg; + } RecoverySystem rs = (RecoverySystem) context.getSystemService(Context.RECOVERY_SERVICE); if (!rs.setupBcb(command)) { -- cgit v1.2.3 From dcd3644f224da72ec95c590394ed656915bba481 Mon Sep 17 00:00:00 2001 From: Tianjie Xu Date: Fri, 13 May 2016 12:30:29 -0700 Subject: Report OTA time statistics Read time and I/O for OTA update from last_install, and report the statistics using MetricsLogger.histogram. Bug: 28658632 Change-Id: I7fd06a82cbabd346d6d44f81ebad08f6baf4b8d0 --- core/java/android/os/RecoverySystem.java | 62 +++++++++++++++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) (limited to 'core/java/android/os/RecoverySystem.java') diff --git a/core/java/android/os/RecoverySystem.java b/core/java/android/os/RecoverySystem.java index 403e06ca8553..4abbf0efc81d 100644 --- a/core/java/android/os/RecoverySystem.java +++ b/core/java/android/os/RecoverySystem.java @@ -25,8 +25,10 @@ import android.text.TextUtils; import android.util.Log; import java.io.ByteArrayInputStream; +import java.io.BufferedReader; import java.io.File; import java.io.FileNotFoundException; +import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; import java.io.InputStream; @@ -45,6 +47,8 @@ import java.util.Locale; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; +import com.android.internal.logging.MetricsLogger; + import sun.security.pkcs.PKCS7; import sun.security.pkcs.SignerInfo; @@ -69,6 +73,7 @@ public class RecoverySystem { /** Used to communicate with recovery. See bootable/recovery/recovery.cpp. */ private static final File RECOVERY_DIR = new File("/cache/recovery"); private static final File LOG_FILE = new File(RECOVERY_DIR, "log"); + private static final File LAST_INSTALL_FILE = new File(RECOVERY_DIR, "last_install"); private static final String LAST_PREFIX = "last_"; /** @@ -682,13 +687,64 @@ public class RecoverySystem { } } + // Read last_install; then report time for update and I/O to tron. + // Only report on the reboots immediately after an OTA update. + private static void parseLastInstallLog(Context context) { + try (BufferedReader in = new BufferedReader(new FileReader(LAST_INSTALL_FILE))) { + String line = null; + int bytesWritten = -1, bytesStashed = -1; + int timeTotal = -1; + while ((line = in.readLine()) != null) { + // Here is an example of lines in last_install: + // ... + // time_total: 101 + // bytes_written_vendor: 51074 + // bytes_stashed_vendor: 200 + int numIndex = line.indexOf(':'); + if (numIndex == -1 || numIndex + 1 >= line.length()) { + continue; + } + String numString = line.substring(numIndex + 1).trim(); + int parsedNum; + try { + parsedNum = Integer.parseInt(numString); + } catch (NumberFormatException ignored) { + Log.e(TAG, "Failed to parse numbers in " + line); + continue; + } + + if (line.startsWith("time")) { + timeTotal = parsedNum; + } else if (line.startsWith("bytes_written")) { + bytesWritten = (bytesWritten == -1) ? parsedNum : bytesWritten + parsedNum; + } else if (line.startsWith("bytes_stashed")) { + bytesStashed = (bytesStashed == -1) ? parsedNum : bytesStashed + parsedNum; + } + } + + // Don't report data to tron if corresponding entry isn't found in last_install. + if (timeTotal != -1) { + MetricsLogger.histogram(context, "ota_time_total", timeTotal); + } + if (bytesWritten != -1) { + MetricsLogger.histogram(context, "ota_bytes_written", bytesWritten); + } + if (bytesStashed != -1) { + MetricsLogger.histogram(context, "ota_bytes_stashed", bytesStashed); + } + + } catch (IOException ignored) { + Log.e(TAG, "Failed to read lines in last_install", ignored); + } + } + /** * Called after booting to process and remove recovery-related files. * @return the log file from recovery, or null if none was found. * * @hide */ - public static String handleAftermath() { + public static String handleAftermath(Context context) { // Record the tail of the LOG_FILE String log = null; try { @@ -699,6 +755,10 @@ public class RecoverySystem { Log.e(TAG, "Error reading recovery log", e); } + if (log != null) { + parseLastInstallLog(context); + } + // Only remove the OTA package if it's partially processed (uncrypt'd). boolean reservePackage = BLOCK_MAP_FILE.exists(); if (!reservePackage && UNCRYPT_PACKAGE_FILE.exists()) { -- cgit v1.2.3 From a2fe5517bcd966c22827150287f55d683d493573 Mon Sep 17 00:00:00 2001 From: Tianjie Xu Date: Thu, 2 Jun 2016 12:38:17 -0700 Subject: Change unit of I/O statistics to MiB I/O in bytes are too large and it may cause overflow. Moreover, data with large numbers are grouped in the same bucket of the histogram. This adds difficulty to the analysis. Changing unit of I/O to MiB so that we can have a better data distribution. Bug: 28658632 Change-Id: Id9913d71e62b36ce5d5d2e57676953f4dbd0c7c9 --- core/java/android/os/RecoverySystem.java | 42 ++++++++++++++++++++++---------- 1 file changed, 29 insertions(+), 13 deletions(-) (limited to 'core/java/android/os/RecoverySystem.java') diff --git a/core/java/android/os/RecoverySystem.java b/core/java/android/os/RecoverySystem.java index 4abbf0efc81d..6c21398e280b 100644 --- a/core/java/android/os/RecoverySystem.java +++ b/core/java/android/os/RecoverySystem.java @@ -687,12 +687,13 @@ public class RecoverySystem { } } - // Read last_install; then report time for update and I/O to tron. + // Read last_install; then report time (in seconds) and I/O (in MiB) for + // this update to tron. // Only report on the reboots immediately after an OTA update. private static void parseLastInstallLog(Context context) { try (BufferedReader in = new BufferedReader(new FileReader(LAST_INSTALL_FILE))) { String line = null; - int bytesWritten = -1, bytesStashed = -1; + int bytesWrittenInMiB = -1, bytesStashedInMiB = -1; int timeTotal = -1; while ((line = in.readLine()) != null) { // Here is an example of lines in last_install: @@ -705,20 +706,35 @@ public class RecoverySystem { continue; } String numString = line.substring(numIndex + 1).trim(); - int parsedNum; + long parsedNum; try { - parsedNum = Integer.parseInt(numString); + parsedNum = Long.parseLong(numString); } catch (NumberFormatException ignored) { Log.e(TAG, "Failed to parse numbers in " + line); continue; } + final int MiB = 1024 * 1024; + int scaled; + try { + if (line.startsWith("bytes")) { + scaled = Math.toIntExact(parsedNum / MiB); + } else { + scaled = Math.toIntExact(parsedNum); + } + } catch (ArithmeticException ignored) { + Log.e(TAG, "Number overflows in " + line); + continue; + } + if (line.startsWith("time")) { - timeTotal = parsedNum; + timeTotal = scaled; } else if (line.startsWith("bytes_written")) { - bytesWritten = (bytesWritten == -1) ? parsedNum : bytesWritten + parsedNum; + bytesWrittenInMiB = (bytesWrittenInMiB == -1) ? scaled : + bytesWrittenInMiB + scaled; } else if (line.startsWith("bytes_stashed")) { - bytesStashed = (bytesStashed == -1) ? parsedNum : bytesStashed + parsedNum; + bytesStashedInMiB = (bytesStashedInMiB == -1) ? scaled : + bytesStashedInMiB + scaled; } } @@ -726,15 +742,15 @@ public class RecoverySystem { if (timeTotal != -1) { MetricsLogger.histogram(context, "ota_time_total", timeTotal); } - if (bytesWritten != -1) { - MetricsLogger.histogram(context, "ota_bytes_written", bytesWritten); + if (bytesWrittenInMiB != -1) { + MetricsLogger.histogram(context, "ota_written_in_MiBs", bytesWrittenInMiB); } - if (bytesStashed != -1) { - MetricsLogger.histogram(context, "ota_bytes_stashed", bytesStashed); + if (bytesStashedInMiB != -1) { + MetricsLogger.histogram(context, "ota_stashed_in_MiBs", bytesStashedInMiB); } - } catch (IOException ignored) { - Log.e(TAG, "Failed to read lines in last_install", ignored); + } catch (IOException e) { + Log.e(TAG, "Failed to read lines in last_install", e); } } -- cgit v1.2.3 From c1ff246f1a37d6619c238cf392e486ca4cf299d3 Mon Sep 17 00:00:00 2001 From: Tianjie Xu Date: Wed, 8 Jun 2016 15:02:35 -0700 Subject: Collect statistics of source build version Parse the last_install and report the source build version of an ota update. Related CL in: ag/1121141 Bug: 28658632 Change-Id: I5e835c144aabe97fda681f60397ebf4416f7bd4f --- core/java/android/os/RecoverySystem.java | 6 ++++++ 1 file changed, 6 insertions(+) (limited to 'core/java/android/os/RecoverySystem.java') diff --git a/core/java/android/os/RecoverySystem.java b/core/java/android/os/RecoverySystem.java index 6c21398e280b..7ff01daa6892 100644 --- a/core/java/android/os/RecoverySystem.java +++ b/core/java/android/os/RecoverySystem.java @@ -695,6 +695,7 @@ public class RecoverySystem { String line = null; int bytesWrittenInMiB = -1, bytesStashedInMiB = -1; int timeTotal = -1; + int sourceVersion = -1; while ((line = in.readLine()) != null) { // Here is an example of lines in last_install: // ... @@ -729,6 +730,8 @@ public class RecoverySystem { if (line.startsWith("time")) { timeTotal = scaled; + } else if (line.startsWith("source_version")) { + sourceVersion = scaled; } else if (line.startsWith("bytes_written")) { bytesWrittenInMiB = (bytesWrittenInMiB == -1) ? scaled : bytesWrittenInMiB + scaled; @@ -742,6 +745,9 @@ public class RecoverySystem { if (timeTotal != -1) { MetricsLogger.histogram(context, "ota_time_total", timeTotal); } + if (sourceVersion != -1) { + MetricsLogger.histogram(context, "ota_source_version", sourceVersion); + } if (bytesWrittenInMiB != -1) { MetricsLogger.histogram(context, "ota_written_in_MiBs", bytesWrittenInMiB); } -- cgit v1.2.3 From 5fabe69fc891278b1f70a136d9b47c50ec995f7f Mon Sep 17 00:00:00 2001 From: Tianjie Xu Date: Wed, 15 Jun 2016 12:31:53 -0700 Subject: Fix a naming typo for source_build When parsing the last_install, the variable name is supposed to be *source_build* instead of *source_version*. Bug: 28658632 Change-Id: I1e0ed7150e04885f904b6a3efa18bd5cfe17cc96 --- core/java/android/os/RecoverySystem.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'core/java/android/os/RecoverySystem.java') diff --git a/core/java/android/os/RecoverySystem.java b/core/java/android/os/RecoverySystem.java index 7ff01daa6892..dd7be53d9865 100644 --- a/core/java/android/os/RecoverySystem.java +++ b/core/java/android/os/RecoverySystem.java @@ -730,7 +730,7 @@ public class RecoverySystem { if (line.startsWith("time")) { timeTotal = scaled; - } else if (line.startsWith("source_version")) { + } else if (line.startsWith("source_build")) { sourceVersion = scaled; } else if (line.startsWith("bytes_written")) { bytesWrittenInMiB = (bytesWrittenInMiB == -1) ? scaled : -- cgit v1.2.3