blob: fe18fbf2a7822804ab6e356186881f724a289d67 [file] [log] [blame]
Tao Baoe8a403d2015-12-31 07:44:55 -08001/*
2 * Copyright (C) 2016 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
Kenny Rootf96836e2019-11-19 17:11:34 -080017package com.android.server.recoverysystem;
Tao Baoe8a403d2015-12-31 07:44:55 -080018
19import android.content.Context;
Tao Baodd3baae2016-02-26 10:28:58 -080020import android.net.LocalSocket;
21import android.net.LocalSocketAddress;
Tao Baoe8a403d2015-12-31 07:44:55 -080022import android.os.IRecoverySystem;
23import android.os.IRecoverySystemProgressListener;
Tao Bao794c8b02016-09-27 11:15:42 -070024import android.os.PowerManager;
Tao Baoe8a403d2015-12-31 07:44:55 -080025import android.os.RecoverySystem;
26import android.os.RemoteException;
27import android.os.SystemProperties;
Tao Baoe8a403d2015-12-31 07:44:55 -080028import android.util.Slog;
29
Kenny Root96690bc2019-11-15 10:20:59 -080030import com.android.internal.annotations.VisibleForTesting;
Kenny Rootf96836e2019-11-19 17:11:34 -080031import com.android.server.SystemService;
32
Tao Baodd3baae2016-02-26 10:28:58 -080033import libcore.io.IoUtils;
34
35import java.io.DataInputStream;
36import java.io.DataOutputStream;
Tao Baoe8a403d2015-12-31 07:44:55 -080037import java.io.FileWriter;
38import java.io.IOException;
Kenny Root96690bc2019-11-15 10:20:59 -080039import java.nio.charset.StandardCharsets;
Tao Baoe8a403d2015-12-31 07:44:55 -080040
41/**
42 * The recovery system service is responsible for coordinating recovery related
43 * functions on the device. It sets up (or clears) the bootloader control block
44 * (BCB), which will be read by the bootloader and the recovery image. It also
45 * triggers /system/bin/uncrypt via init to de-encrypt an OTA package on the
46 * /data partition so that it can be accessed under the recovery image.
47 */
Kenny Root96690bc2019-11-15 10:20:59 -080048public class RecoverySystemService extends IRecoverySystem.Stub {
Tao Baoe8a403d2015-12-31 07:44:55 -080049 private static final String TAG = "RecoverySystemService";
50 private static final boolean DEBUG = false;
51
Tao Baodd3baae2016-02-26 10:28:58 -080052 // The socket at /dev/socket/uncrypt to communicate with uncrypt.
53 private static final String UNCRYPT_SOCKET = "uncrypt";
54
Tao Bao794c8b02016-09-27 11:15:42 -070055 // The init services that communicate with /system/bin/uncrypt.
Kenny Root96690bc2019-11-15 10:20:59 -080056 @VisibleForTesting
57 static final String INIT_SERVICE_UNCRYPT = "init.svc.uncrypt";
58 @VisibleForTesting
59 static final String INIT_SERVICE_SETUP_BCB = "init.svc.setup-bcb";
60 @VisibleForTesting
61 static final String INIT_SERVICE_CLEAR_BCB = "init.svc.clear-bcb";
Tao Baoe8a403d2015-12-31 07:44:55 -080062
Tao Bao794c8b02016-09-27 11:15:42 -070063 private static final Object sRequestLock = new Object();
64
Kenny Root96690bc2019-11-15 10:20:59 -080065 private static final int SOCKET_CONNECTION_MAX_RETRY = 30;
Tao Baoe8a403d2015-12-31 07:44:55 -080066
Kenny Root96690bc2019-11-15 10:20:59 -080067 private final Injector mInjector;
68 private final Context mContext;
69
70 static class Injector {
71 protected final Context mContext;
72
73 Injector(Context context) {
74 mContext = context;
75 }
76
77 public Context getContext() {
78 return mContext;
79 }
80
81 public PowerManager getPowerManager() {
82 return (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
83 }
84
85 public String systemPropertiesGet(String key) {
86 return SystemProperties.get(key);
87 }
88
89 public void systemPropertiesSet(String key, String value) {
90 SystemProperties.set(key, value);
91 }
92
93 public boolean uncryptPackageFileDelete() {
94 return RecoverySystem.UNCRYPT_PACKAGE_FILE.delete();
95 }
96
97 public String getUncryptPackageFileName() {
98 return RecoverySystem.UNCRYPT_PACKAGE_FILE.getName();
99 }
100
101 public FileWriter getUncryptPackageFileWriter() throws IOException {
102 return new FileWriter(RecoverySystem.UNCRYPT_PACKAGE_FILE);
103 }
104
105 public UncryptSocket connectService() {
106 UncryptSocket socket = new UncryptSocket();
107 if (!socket.connectService()) {
108 socket.close();
109 return null;
110 }
111 return socket;
112 }
113
114 public void threadSleep(long millis) throws InterruptedException {
115 Thread.sleep(millis);
116 }
Tao Baoe8a403d2015-12-31 07:44:55 -0800117 }
118
Kenny Root96690bc2019-11-15 10:20:59 -0800119 /**
120 * Handles the lifecycle events for the RecoverySystemService.
121 */
122 public static final class Lifecycle extends SystemService {
123 public Lifecycle(Context context) {
124 super(context);
125 }
126
127 @Override
128 public void onStart() {
129 RecoverySystemService recoverySystemService = new RecoverySystemService(getContext());
130 publishBinderService(Context.RECOVERY_SERVICE, recoverySystemService);
131 }
Tao Baoe8a403d2015-12-31 07:44:55 -0800132 }
133
Kenny Root96690bc2019-11-15 10:20:59 -0800134 private RecoverySystemService(Context context) {
135 this(new Injector(context));
136 }
Tao Baoe8a403d2015-12-31 07:44:55 -0800137
Kenny Root96690bc2019-11-15 10:20:59 -0800138 @VisibleForTesting
139 RecoverySystemService(Injector injector) {
140 mInjector = injector;
141 mContext = injector.getContext();
142 }
Tao Baoe8a403d2015-12-31 07:44:55 -0800143
Kenny Root96690bc2019-11-15 10:20:59 -0800144 @Override // Binder call
145 public boolean uncrypt(String filename, IRecoverySystemProgressListener listener) {
146 if (DEBUG) Slog.d(TAG, "uncrypt: " + filename);
Tao Baoe8a403d2015-12-31 07:44:55 -0800147
Kenny Root96690bc2019-11-15 10:20:59 -0800148 synchronized (sRequestLock) {
149 mContext.enforceCallingOrSelfPermission(android.Manifest.permission.RECOVERY, null);
Tao Bao794c8b02016-09-27 11:15:42 -0700150
Kenny Root96690bc2019-11-15 10:20:59 -0800151 if (!checkAndWaitForUncryptService()) {
152 Slog.e(TAG, "uncrypt service is unavailable.");
153 return false;
154 }
Tao Bao794c8b02016-09-27 11:15:42 -0700155
Kenny Root96690bc2019-11-15 10:20:59 -0800156 // Write the filename into uncrypt package file to be read by
157 // uncrypt.
158 mInjector.uncryptPackageFileDelete();
Tao Bao794c8b02016-09-27 11:15:42 -0700159
Kenny Root96690bc2019-11-15 10:20:59 -0800160 try (FileWriter uncryptFile = mInjector.getUncryptPackageFileWriter()) {
161 uncryptFile.write(filename + "\n");
162 } catch (IOException e) {
163 Slog.e(TAG, "IOException when writing \""
164 + mInjector.getUncryptPackageFileName() + "\":", e);
165 return false;
166 }
Tao Bao794c8b02016-09-27 11:15:42 -0700167
Kenny Root96690bc2019-11-15 10:20:59 -0800168 // Trigger uncrypt via init.
169 mInjector.systemPropertiesSet("ctl.start", "uncrypt");
Tao Bao794c8b02016-09-27 11:15:42 -0700170
Kenny Root96690bc2019-11-15 10:20:59 -0800171 // Connect to the uncrypt service socket.
172 UncryptSocket socket = mInjector.connectService();
173 if (socket == null) {
174 Slog.e(TAG, "Failed to connect to uncrypt socket");
175 return false;
176 }
177
178 // Read the status from the socket.
179 try {
180 int lastStatus = Integer.MIN_VALUE;
181 while (true) {
182 int status = socket.getPercentageUncrypted();
183 // Avoid flooding the log with the same message.
184 if (status == lastStatus && lastStatus != Integer.MIN_VALUE) {
185 continue;
Tao Bao794c8b02016-09-27 11:15:42 -0700186 }
Kenny Root96690bc2019-11-15 10:20:59 -0800187 lastStatus = status;
Tao Bao794c8b02016-09-27 11:15:42 -0700188
Kenny Root96690bc2019-11-15 10:20:59 -0800189 if (status >= 0 && status <= 100) {
190 // Update status
191 Slog.i(TAG, "uncrypt read status: " + status);
192 if (listener != null) {
193 try {
194 listener.onProgress(status);
195 } catch (RemoteException ignored) {
196 Slog.w(TAG, "RemoteException when posting progress");
197 }
198 }
199 if (status == 100) {
200 Slog.i(TAG, "uncrypt successfully finished.");
201 // Ack receipt of the final status code. uncrypt
202 // waits for the ack so the socket won't be
203 // destroyed before we receive the code.
204 socket.sendAck();
205 break;
206 }
207 } else {
208 // Error in /system/bin/uncrypt.
209 Slog.e(TAG, "uncrypt failed with status: " + status);
210 // Ack receipt of the final status code. uncrypt waits
211 // for the ack so the socket won't be destroyed before
212 // we receive the code.
213 socket.sendAck();
214 return false;
215 }
216 }
217 } catch (IOException e) {
218 Slog.e(TAG, "IOException when reading status: ", e);
219 return false;
220 } finally {
221 socket.close();
222 }
223
224 return true;
225 }
226 }
227
228 @Override // Binder call
229 public boolean clearBcb() {
230 if (DEBUG) Slog.d(TAG, "clearBcb");
231 synchronized (sRequestLock) {
232 return setupOrClearBcb(false, null);
233 }
234 }
235
236 @Override // Binder call
237 public boolean setupBcb(String command) {
238 if (DEBUG) Slog.d(TAG, "setupBcb: [" + command + "]");
239 synchronized (sRequestLock) {
240 return setupOrClearBcb(true, command);
241 }
242 }
243
244 @Override // Binder call
245 public void rebootRecoveryWithCommand(String command) {
246 if (DEBUG) Slog.d(TAG, "rebootRecoveryWithCommand: [" + command + "]");
247 synchronized (sRequestLock) {
248 if (!setupOrClearBcb(true, command)) {
249 return;
250 }
251
252 // Having set up the BCB, go ahead and reboot.
253 PowerManager pm = mInjector.getPowerManager();
254 pm.reboot(PowerManager.REBOOT_RECOVERY);
255 }
256 }
257
258 /**
259 * Check if any of the init services is still running. If so, we cannot
260 * start a new uncrypt/setup-bcb/clear-bcb service right away; otherwise
261 * it may break the socket communication since init creates / deletes
262 * the socket (/dev/socket/uncrypt) on service start / exit.
263 */
264 private boolean checkAndWaitForUncryptService() {
265 for (int retry = 0; retry < SOCKET_CONNECTION_MAX_RETRY; retry++) {
266 final String uncryptService = mInjector.systemPropertiesGet(INIT_SERVICE_UNCRYPT);
267 final String setupBcbService = mInjector.systemPropertiesGet(INIT_SERVICE_SETUP_BCB);
268 final String clearBcbService = mInjector.systemPropertiesGet(INIT_SERVICE_CLEAR_BCB);
269 final boolean busy = "running".equals(uncryptService)
270 || "running".equals(setupBcbService) || "running".equals(clearBcbService);
271 if (DEBUG) {
272 Slog.i(TAG, "retry: " + retry + " busy: " + busy
273 + " uncrypt: [" + uncryptService + "]"
274 + " setupBcb: [" + setupBcbService + "]"
275 + " clearBcb: [" + clearBcbService + "]");
276 }
277
278 if (!busy) {
Tao Bao794c8b02016-09-27 11:15:42 -0700279 return true;
280 }
Tao Baoe8a403d2015-12-31 07:44:55 -0800281
Kenny Root96690bc2019-11-15 10:20:59 -0800282 try {
283 mInjector.threadSleep(1000);
284 } catch (InterruptedException e) {
285 Slog.w(TAG, "Interrupted:", e);
Tao Bao794c8b02016-09-27 11:15:42 -0700286 }
Tao Baoe8a403d2015-12-31 07:44:55 -0800287 }
288
Kenny Root96690bc2019-11-15 10:20:59 -0800289 return false;
290 }
Tao Bao794c8b02016-09-27 11:15:42 -0700291
Kenny Root96690bc2019-11-15 10:20:59 -0800292 private boolean setupOrClearBcb(boolean isSetup, String command) {
293 mContext.enforceCallingOrSelfPermission(android.Manifest.permission.RECOVERY, null);
Tao Bao794c8b02016-09-27 11:15:42 -0700294
Kenny Root96690bc2019-11-15 10:20:59 -0800295 final boolean available = checkAndWaitForUncryptService();
296 if (!available) {
297 Slog.e(TAG, "uncrypt service is unavailable.");
Tao Bao794c8b02016-09-27 11:15:42 -0700298 return false;
Tao Baoe8a403d2015-12-31 07:44:55 -0800299 }
300
Kenny Root96690bc2019-11-15 10:20:59 -0800301 if (isSetup) {
302 mInjector.systemPropertiesSet("ctl.start", "setup-bcb");
303 } else {
304 mInjector.systemPropertiesSet("ctl.start", "clear-bcb");
305 }
306
307 // Connect to the uncrypt service socket.
308 UncryptSocket socket = mInjector.connectService();
309 if (socket == null) {
310 Slog.e(TAG, "Failed to connect to uncrypt socket");
311 return false;
312 }
313
314 try {
315 // Send the BCB commands if it's to setup BCB.
316 if (isSetup) {
317 socket.sendCommand(command);
318 }
319
320 // Read the status from the socket.
321 int status = socket.getPercentageUncrypted();
322
323 // Ack receipt of the status code. uncrypt waits for the ack so
324 // the socket won't be destroyed before we receive the code.
325 socket.sendAck();
326
327 if (status == 100) {
328 Slog.i(TAG, "uncrypt " + (isSetup ? "setup" : "clear")
329 + " bcb successfully finished.");
330 } else {
331 // Error in /system/bin/uncrypt.
332 Slog.e(TAG, "uncrypt failed with status: " + status);
333 return false;
334 }
335 } catch (IOException e) {
336 Slog.e(TAG, "IOException when communicating with uncrypt:", e);
337 return false;
338 } finally {
339 socket.close();
340 }
341
342 return true;
343 }
344
345 /**
346 * Provides a wrapper for the low-level details of framing packets sent to the uncrypt
347 * socket.
348 */
349 public static class UncryptSocket {
350 private LocalSocket mLocalSocket;
351 private DataInputStream mInputStream;
352 private DataOutputStream mOutputStream;
353
354 /**
355 * Attempt to connect to the uncrypt service. Connection will be retried for up to
356 * {@link #SOCKET_CONNECTION_MAX_RETRY} times. If the connection is unsuccessful, the
357 * socket will be closed. If the connection is successful, the connection must be closed
358 * by the caller.
359 *
360 * @return true if connection was successful, false if unsuccessful
361 */
362 public boolean connectService() {
363 mLocalSocket = new LocalSocket();
Tao Baodd3baae2016-02-26 10:28:58 -0800364 boolean done = false;
365 // The uncrypt socket will be created by init upon receiving the
366 // service request. It may not be ready by this point. So we will
367 // keep retrying until success or reaching timeout.
368 for (int retry = 0; retry < SOCKET_CONNECTION_MAX_RETRY; retry++) {
369 try {
Kenny Root96690bc2019-11-15 10:20:59 -0800370 mLocalSocket.connect(new LocalSocketAddress(UNCRYPT_SOCKET,
Tao Baodd3baae2016-02-26 10:28:58 -0800371 LocalSocketAddress.Namespace.RESERVED));
372 done = true;
373 break;
Tao Bao12844822016-03-22 10:42:32 -0700374 } catch (IOException ignored) {
Tao Baodd3baae2016-02-26 10:28:58 -0800375 try {
376 Thread.sleep(1000);
377 } catch (InterruptedException e) {
Tao Bao794c8b02016-09-27 11:15:42 -0700378 Slog.w(TAG, "Interrupted:", e);
Tao Baodd3baae2016-02-26 10:28:58 -0800379 }
Tao Baoe8a403d2015-12-31 07:44:55 -0800380 }
381 }
Tao Baodd3baae2016-02-26 10:28:58 -0800382 if (!done) {
383 Slog.e(TAG, "Timed out connecting to uncrypt socket");
Kenny Root96690bc2019-11-15 10:20:59 -0800384 close();
Tao Bao794c8b02016-09-27 11:15:42 -0700385 return false;
386 }
387
Tao Bao12844822016-03-22 10:42:32 -0700388 try {
Kenny Root96690bc2019-11-15 10:20:59 -0800389 mInputStream = new DataInputStream(mLocalSocket.getInputStream());
390 mOutputStream = new DataOutputStream(mLocalSocket.getOutputStream());
Tao Baodd3baae2016-02-26 10:28:58 -0800391 } catch (IOException e) {
Kenny Root96690bc2019-11-15 10:20:59 -0800392 close();
Tao Baodd3baae2016-02-26 10:28:58 -0800393 return false;
Tao Baodd3baae2016-02-26 10:28:58 -0800394 }
395
Tao Baoe8a403d2015-12-31 07:44:55 -0800396 return true;
397 }
Kenny Root96690bc2019-11-15 10:20:59 -0800398
399 /**
400 * Sends a command to the uncrypt service.
401 *
402 * @param command command to send to the uncrypt service
403 * @throws IOException if the socket is closed or there was an error writing to the socket
404 */
405 public void sendCommand(String command) throws IOException {
406 if (mLocalSocket.isClosed()) {
407 throw new IOException("socket is closed");
408 }
409
410 byte[] cmdUtf8 = command.getBytes(StandardCharsets.UTF_8);
411 mOutputStream.writeInt(cmdUtf8.length);
412 mOutputStream.write(cmdUtf8, 0, cmdUtf8.length);
413 }
414
415 /**
416 * Reads the status from the uncrypt service which is usually represented as a percentage.
417 * @return an integer representing the percentage completed
418 * @throws IOException if the socket was closed or there was an error reading the socket
419 */
420 public int getPercentageUncrypted() throws IOException {
421 if (mLocalSocket.isClosed()) {
422 throw new IOException("socket is closed");
423 }
424
425 return mInputStream.readInt();
426 }
427
428 /**
429 * Sends a confirmation to the uncrypt service.
430 * @throws IOException if the socket was closed or there was an error writing to the socket
431 */
432 public void sendAck() throws IOException {
433 if (mLocalSocket.isClosed()) {
434 throw new IOException("socket is closed");
435 }
436
437 mOutputStream.writeInt(0);
438 }
439
440 /**
441 * Closes the socket and all underlying data streams.
442 */
443 public void close() {
444 IoUtils.closeQuietly(mInputStream);
445 IoUtils.closeQuietly(mOutputStream);
446 IoUtils.closeQuietly(mLocalSocket);
447 }
Tao Baoe8a403d2015-12-31 07:44:55 -0800448 }
449}