blob: 3c8c699a65bb73dd442b390991f772c595857e0f [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;
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.system.ErrnoException;
import android.system.Os;
import android.util.Slog;
import libcore.io.IoUtils;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
/**
* 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 final class RecoverySystemService extends SystemService {
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.
private static final String INIT_SERVICE_UNCRYPT = "init.svc.uncrypt";
private static final String INIT_SERVICE_SETUP_BCB = "init.svc.setup-bcb";
private static final String INIT_SERVICE_CLEAR_BCB = "init.svc.clear-bcb";
private static final int SOCKET_CONNECTION_MAX_RETRY = 30;
private static final Object sRequestLock = new Object();
private Context mContext;
public RecoverySystemService(Context context) {
super(context);
mContext = context;
}
@Override
public void onStart() {
publishBinderService(Context.RECOVERY_SERVICE, new BinderService());
}
private final class BinderService extends IRecoverySystem.Stub {
@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);
final boolean available = checkAndWaitForUncryptService();
if (!available) {
Slog.e(TAG, "uncrypt service is unavailable.");
return false;
}
// Write the filename into UNCRYPT_PACKAGE_FILE to be read by
// uncrypt.
RecoverySystem.UNCRYPT_PACKAGE_FILE.delete();
try (FileWriter uncryptFile = new FileWriter(RecoverySystem.UNCRYPT_PACKAGE_FILE)) {
uncryptFile.write(filename + "\n");
} catch (IOException e) {
Slog.e(TAG, "IOException when writing \"" +
RecoverySystem.UNCRYPT_PACKAGE_FILE + "\":", e);
return false;
}
// Trigger uncrypt via init.
SystemProperties.set("ctl.start", "uncrypt");
// Connect to the uncrypt service socket.
LocalSocket socket = connectService();
if (socket == null) {
Slog.e(TAG, "Failed to connect to uncrypt socket");
return false;
}
// Read the status from the socket.
DataInputStream dis = null;
DataOutputStream dos = null;
try {
dis = new DataInputStream(socket.getInputStream());
dos = new DataOutputStream(socket.getOutputStream());
int lastStatus = Integer.MIN_VALUE;
while (true) {
int status = dis.readInt();
// 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.
dos.writeInt(0);
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.
dos.writeInt(0);
return false;
}
}
} catch (IOException e) {
Slog.e(TAG, "IOException when reading status: ", e);
return false;
} finally {
IoUtils.closeQuietly(dis);
IoUtils.closeQuietly(dos);
IoUtils.closeQuietly(socket);
}
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 = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
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 = SystemProperties.get(INIT_SERVICE_UNCRYPT);
final String setupBcbService = SystemProperties.get(INIT_SERVICE_SETUP_BCB);
final String clearBcbService = SystemProperties.get(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 {
Thread.sleep(1000);
} catch (InterruptedException e) {
Slog.w(TAG, "Interrupted:", e);
}
}
return false;
}
private LocalSocket connectService() {
LocalSocket socket = 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 {
socket.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");
return null;
}
return socket;
}
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) {
SystemProperties.set("ctl.start", "setup-bcb");
} else {
SystemProperties.set("ctl.start", "clear-bcb");
}
// Connect to the uncrypt service socket.
LocalSocket socket = connectService();
if (socket == null) {
Slog.e(TAG, "Failed to connect to uncrypt socket");
return false;
}
DataInputStream dis = null;
DataOutputStream dos = null;
try {
dis = new DataInputStream(socket.getInputStream());
dos = new DataOutputStream(socket.getOutputStream());
// Send the BCB commands if it's to setup BCB.
if (isSetup) {
dos.writeInt(command.length());
dos.writeBytes(command);
dos.flush();
}
// Read the status from the socket.
int status = dis.readInt();
// Ack receipt of the status code. uncrypt waits for the ack so
// the socket won't be destroyed before we receive the code.
dos.writeInt(0);
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 {
IoUtils.closeQuietly(dis);
IoUtils.closeQuietly(dos);
IoUtils.closeQuietly(socket);
}
return true;
}
}
}