/*
|
* 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) {
|
byte[] cmdUtf8 = command.getBytes("UTF-8");
|
dos.writeInt(cmdUtf8.length);
|
dos.write(cmdUtf8, 0, cmdUtf8.length);
|
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;
|
}
|
}
|
}
|