blob: fe18fbf2a7822804ab6e356186881f724a289d67 [file] [log] [blame]
/*
* Copyright (C) 2016 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.server.recoverysystem;
import android.content.Context;
import android.net.LocalSocket;
import android.net.LocalSocketAddress;
import android.os.IRecoverySystem;
import android.os.IRecoverySystemProgressListener;
import android.os.PowerManager;
import android.os.RecoverySystem;
import android.os.RemoteException;
import android.os.SystemProperties;
import android.util.Slog;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.SystemService;
import libcore.io.IoUtils;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
/**
* The recovery system service is responsible for coordinating recovery related
* functions on the device. It sets up (or clears) the bootloader control block
* (BCB), which will be read by the bootloader and the recovery image. It also
* triggers /system/bin/uncrypt via init to de-encrypt an OTA package on the
* /data partition so that it can be accessed under the recovery image.
*/
public class RecoverySystemService extends IRecoverySystem.Stub {
private static final String TAG = "RecoverySystemService";
private static final boolean DEBUG = false;
// The socket at /dev/socket/uncrypt to communicate with uncrypt.
private static final String UNCRYPT_SOCKET = "uncrypt";
// The init services that communicate with /system/bin/uncrypt.
@VisibleForTesting
static final String INIT_SERVICE_UNCRYPT = "init.svc.uncrypt";
@VisibleForTesting
static final String INIT_SERVICE_SETUP_BCB = "init.svc.setup-bcb";
@VisibleForTesting
static final String INIT_SERVICE_CLEAR_BCB = "init.svc.clear-bcb";
private static final Object sRequestLock = new Object();
private static final int SOCKET_CONNECTION_MAX_RETRY = 30;
private final Injector mInjector;
private final Context mContext;
static class Injector {
protected final Context mContext;
Injector(Context context) {
mContext = context;
}
public Context getContext() {
return mContext;
}
public PowerManager getPowerManager() {
return (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
}
public String systemPropertiesGet(String key) {
return SystemProperties.get(key);
}
public void systemPropertiesSet(String key, String value) {
SystemProperties.set(key, value);
}
public boolean uncryptPackageFileDelete() {
return RecoverySystem.UNCRYPT_PACKAGE_FILE.delete();
}
public String getUncryptPackageFileName() {
return RecoverySystem.UNCRYPT_PACKAGE_FILE.getName();
}
public FileWriter getUncryptPackageFileWriter() throws IOException {
return new FileWriter(RecoverySystem.UNCRYPT_PACKAGE_FILE);
}
public UncryptSocket connectService() {
UncryptSocket socket = new UncryptSocket();
if (!socket.connectService()) {
socket.close();
return null;
}
return socket;
}
public void threadSleep(long millis) throws InterruptedException {
Thread.sleep(millis);
}
}
/**
* Handles the lifecycle events for the RecoverySystemService.
*/
public static final class Lifecycle extends SystemService {
public Lifecycle(Context context) {
super(context);
}
@Override
public void onStart() {
RecoverySystemService recoverySystemService = new RecoverySystemService(getContext());
publishBinderService(Context.RECOVERY_SERVICE, recoverySystemService);
}
}
private RecoverySystemService(Context context) {
this(new Injector(context));
}
@VisibleForTesting
RecoverySystemService(Injector injector) {
mInjector = injector;
mContext = injector.getContext();
}
@Override // Binder call
public boolean uncrypt(String filename, IRecoverySystemProgressListener listener) {
if (DEBUG) Slog.d(TAG, "uncrypt: " + filename);
synchronized (sRequestLock) {
mContext.enforceCallingOrSelfPermission(android.Manifest.permission.RECOVERY, null);
if (!checkAndWaitForUncryptService()) {
Slog.e(TAG, "uncrypt service is unavailable.");
return false;
}
// Write the filename into uncrypt package file to be read by
// uncrypt.
mInjector.uncryptPackageFileDelete();
try (FileWriter uncryptFile = mInjector.getUncryptPackageFileWriter()) {
uncryptFile.write(filename + "\n");
} catch (IOException e) {
Slog.e(TAG, "IOException when writing \""
+ mInjector.getUncryptPackageFileName() + "\":", e);
return false;
}
// Trigger uncrypt via init.
mInjector.systemPropertiesSet("ctl.start", "uncrypt");
// Connect to the uncrypt service socket.
UncryptSocket socket = mInjector.connectService();
if (socket == null) {
Slog.e(TAG, "Failed to connect to uncrypt socket");
return false;
}
// Read the status from the socket.
try {
int lastStatus = Integer.MIN_VALUE;
while (true) {
int status = socket.getPercentageUncrypted();
// Avoid flooding the log with the same message.
if (status == lastStatus && lastStatus != Integer.MIN_VALUE) {
continue;
}
lastStatus = status;
if (status >= 0 && status <= 100) {
// Update status
Slog.i(TAG, "uncrypt read status: " + status);
if (listener != null) {
try {
listener.onProgress(status);
} catch (RemoteException ignored) {
Slog.w(TAG, "RemoteException when posting progress");
}
}
if (status == 100) {
Slog.i(TAG, "uncrypt successfully finished.");
// Ack receipt of the final status code. uncrypt
// waits for the ack so the socket won't be
// destroyed before we receive the code.
socket.sendAck();
break;
}
} else {
// Error in /system/bin/uncrypt.
Slog.e(TAG, "uncrypt failed with status: " + status);
// Ack receipt of the final status code. uncrypt waits
// for the ack so the socket won't be destroyed before
// we receive the code.
socket.sendAck();
return false;
}
}
} catch (IOException e) {
Slog.e(TAG, "IOException when reading status: ", e);
return false;
} finally {
socket.close();
}
return true;
}
}
@Override // Binder call
public boolean clearBcb() {
if (DEBUG) Slog.d(TAG, "clearBcb");
synchronized (sRequestLock) {
return setupOrClearBcb(false, null);
}
}
@Override // Binder call
public boolean setupBcb(String command) {
if (DEBUG) Slog.d(TAG, "setupBcb: [" + command + "]");
synchronized (sRequestLock) {
return setupOrClearBcb(true, command);
}
}
@Override // Binder call
public void rebootRecoveryWithCommand(String command) {
if (DEBUG) Slog.d(TAG, "rebootRecoveryWithCommand: [" + command + "]");
synchronized (sRequestLock) {
if (!setupOrClearBcb(true, command)) {
return;
}
// Having set up the BCB, go ahead and reboot.
PowerManager pm = mInjector.getPowerManager();
pm.reboot(PowerManager.REBOOT_RECOVERY);
}
}
/**
* Check if any of the init services is still running. If so, we cannot
* start a new uncrypt/setup-bcb/clear-bcb service right away; otherwise
* it may break the socket communication since init creates / deletes
* the socket (/dev/socket/uncrypt) on service start / exit.
*/
private boolean checkAndWaitForUncryptService() {
for (int retry = 0; retry < SOCKET_CONNECTION_MAX_RETRY; retry++) {
final String uncryptService = mInjector.systemPropertiesGet(INIT_SERVICE_UNCRYPT);
final String setupBcbService = mInjector.systemPropertiesGet(INIT_SERVICE_SETUP_BCB);
final String clearBcbService = mInjector.systemPropertiesGet(INIT_SERVICE_CLEAR_BCB);
final boolean busy = "running".equals(uncryptService)
|| "running".equals(setupBcbService) || "running".equals(clearBcbService);
if (DEBUG) {
Slog.i(TAG, "retry: " + retry + " busy: " + busy
+ " uncrypt: [" + uncryptService + "]"
+ " setupBcb: [" + setupBcbService + "]"
+ " clearBcb: [" + clearBcbService + "]");
}
if (!busy) {
return true;
}
try {
mInjector.threadSleep(1000);
} catch (InterruptedException e) {
Slog.w(TAG, "Interrupted:", e);
}
}
return false;
}
private boolean setupOrClearBcb(boolean isSetup, String command) {
mContext.enforceCallingOrSelfPermission(android.Manifest.permission.RECOVERY, null);
final boolean available = checkAndWaitForUncryptService();
if (!available) {
Slog.e(TAG, "uncrypt service is unavailable.");
return false;
}
if (isSetup) {
mInjector.systemPropertiesSet("ctl.start", "setup-bcb");
} else {
mInjector.systemPropertiesSet("ctl.start", "clear-bcb");
}
// Connect to the uncrypt service socket.
UncryptSocket socket = mInjector.connectService();
if (socket == null) {
Slog.e(TAG, "Failed to connect to uncrypt socket");
return false;
}
try {
// Send the BCB commands if it's to setup BCB.
if (isSetup) {
socket.sendCommand(command);
}
// Read the status from the socket.
int status = socket.getPercentageUncrypted();
// Ack receipt of the status code. uncrypt waits for the ack so
// the socket won't be destroyed before we receive the code.
socket.sendAck();
if (status == 100) {
Slog.i(TAG, "uncrypt " + (isSetup ? "setup" : "clear")
+ " bcb successfully finished.");
} else {
// Error in /system/bin/uncrypt.
Slog.e(TAG, "uncrypt failed with status: " + status);
return false;
}
} catch (IOException e) {
Slog.e(TAG, "IOException when communicating with uncrypt:", e);
return false;
} finally {
socket.close();
}
return true;
}
/**
* Provides a wrapper for the low-level details of framing packets sent to the uncrypt
* socket.
*/
public static class UncryptSocket {
private LocalSocket mLocalSocket;
private DataInputStream mInputStream;
private DataOutputStream mOutputStream;
/**
* Attempt to connect to the uncrypt service. Connection will be retried for up to
* {@link #SOCKET_CONNECTION_MAX_RETRY} times. If the connection is unsuccessful, the
* socket will be closed. If the connection is successful, the connection must be closed
* by the caller.
*
* @return true if connection was successful, false if unsuccessful
*/
public boolean connectService() {
mLocalSocket = new LocalSocket();
boolean done = false;
// The uncrypt socket will be created by init upon receiving the
// service request. It may not be ready by this point. So we will
// keep retrying until success or reaching timeout.
for (int retry = 0; retry < SOCKET_CONNECTION_MAX_RETRY; retry++) {
try {
mLocalSocket.connect(new LocalSocketAddress(UNCRYPT_SOCKET,
LocalSocketAddress.Namespace.RESERVED));
done = true;
break;
} catch (IOException ignored) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Slog.w(TAG, "Interrupted:", e);
}
}
}
if (!done) {
Slog.e(TAG, "Timed out connecting to uncrypt socket");
close();
return false;
}
try {
mInputStream = new DataInputStream(mLocalSocket.getInputStream());
mOutputStream = new DataOutputStream(mLocalSocket.getOutputStream());
} catch (IOException e) {
close();
return false;
}
return true;
}
/**
* Sends a command to the uncrypt service.
*
* @param command command to send to the uncrypt service
* @throws IOException if the socket is closed or there was an error writing to the socket
*/
public void sendCommand(String command) throws IOException {
if (mLocalSocket.isClosed()) {
throw new IOException("socket is closed");
}
byte[] cmdUtf8 = command.getBytes(StandardCharsets.UTF_8);
mOutputStream.writeInt(cmdUtf8.length);
mOutputStream.write(cmdUtf8, 0, cmdUtf8.length);
}
/**
* Reads the status from the uncrypt service which is usually represented as a percentage.
* @return an integer representing the percentage completed
* @throws IOException if the socket was closed or there was an error reading the socket
*/
public int getPercentageUncrypted() throws IOException {
if (mLocalSocket.isClosed()) {
throw new IOException("socket is closed");
}
return mInputStream.readInt();
}
/**
* Sends a confirmation to the uncrypt service.
* @throws IOException if the socket was closed or there was an error writing to the socket
*/
public void sendAck() throws IOException {
if (mLocalSocket.isClosed()) {
throw new IOException("socket is closed");
}
mOutputStream.writeInt(0);
}
/**
* Closes the socket and all underlying data streams.
*/
public void close() {
IoUtils.closeQuietly(mInputStream);
IoUtils.closeQuietly(mOutputStream);
IoUtils.closeQuietly(mLocalSocket);
}
}
}