1 /*
2  * Copyright (C) 2022 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 
17 package com.android.server.sdksandbox;
18 
19 import android.app.sdksandbox.LoadSdkException;
20 import android.app.sdksandbox.SandboxLatencyInfo;
21 import android.content.Context;
22 import android.content.pm.ApplicationInfo;
23 import android.content.pm.PackageManager.NameNotFoundException;
24 import android.os.Binder;
25 import android.os.ParcelFileDescriptor;
26 import android.os.Process;
27 import android.os.UserHandle;
28 import android.util.ArraySet;
29 import android.util.Log;
30 
31 import com.android.internal.annotations.VisibleForTesting;
32 import com.android.modules.utils.BasicShellCommandHandler;
33 import com.android.sdksandbox.ISdkSandboxService;
34 import com.android.server.sdksandbox.SdkSandboxManagerService.LocalImpl;
35 
36 import java.io.IOException;
37 import java.io.PrintWriter;
38 import java.util.Arrays;
39 import java.util.concurrent.CountDownLatch;
40 import java.util.concurrent.TimeUnit;
41 
42 class SdkSandboxShellCommand extends BasicShellCommandHandler {
43 
44     @VisibleForTesting static final String ADSERVICES_CMD = "adservices-cmd";
45     private static final String TAG = SdkSandboxShellCommand.class.getSimpleName();
46 
47     private final SdkSandboxManagerService mService;
48     private final Context mContext;
49     private final Injector mInjector;
50     private final boolean mSupportsAdServicesShellCmd;
51 
52     private int mUserId = UserHandle.CURRENT.getIdentifier();
53     private CallingInfo mCallingInfo;
54 
55     static class Injector {
getCallingUid()56         int getCallingUid() {
57             return Binder.getCallingUid();
58         }
59     }
60 
61     @VisibleForTesting
SdkSandboxShellCommand( SdkSandboxManagerService service, Context context, boolean supportsAdServicesShellCmd, Injector injector)62     SdkSandboxShellCommand(
63             SdkSandboxManagerService service,
64             Context context,
65             boolean supportsAdServicesShellCmd,
66             Injector injector) {
67         mService = service;
68         mContext = context;
69         mSupportsAdServicesShellCmd = supportsAdServicesShellCmd;
70         mInjector = injector;
71     }
72 
73     @VisibleForTesting
SdkSandboxShellCommand(SdkSandboxManagerService service, Context context, Injector injector)74     SdkSandboxShellCommand(SdkSandboxManagerService service, Context context, Injector injector) {
75         this(service, context, /* supportsAdServicesShellCmd= */ false, injector);
76     }
77 
SdkSandboxShellCommand( SdkSandboxManagerService service, Context context, boolean supportsAdServicesShellCmd)78     SdkSandboxShellCommand(
79             SdkSandboxManagerService service, Context context, boolean supportsAdServicesShellCmd) {
80         this(service, context, supportsAdServicesShellCmd, new Injector());
81     }
82 
83     @Override
onCommand(String cmd)84     public int onCommand(String cmd) {
85         int callingUid = mInjector.getCallingUid();
86 
87         if (callingUid != Process.ROOT_UID && callingUid != Process.SHELL_UID) {
88             throw new SecurityException("sdk_sandbox shell command is only callable by ADB");
89         }
90         final long token = Binder.clearCallingIdentity();
91 
92         int result;
93         try {
94             if (cmd == null) {
95                 result = handleDefaultCommands(null);
96             } else {
97                 switch (cmd) {
98                     case "start":
99                         result = runStart();
100                         break;
101                     case "stop":
102                         result = runStop();
103                         break;
104                     case "set-state":
105                         result = runSetState();
106                         break;
107                     case ADSERVICES_CMD:
108                         result = runAdServicesShellCommand();
109                         break;
110                     case "append-test-allowlist":
111                         result = runAppendTestAllowlistComponent();
112                         break;
113                     case "clear-test-allowlists":
114                         result = runClearTestAllowlists();
115                         break;
116                     case "get-test-allowlist":
117                         result = getTestAllowlist();
118                         break;
119                     default:
120                         result = handleDefaultCommands(cmd);
121                 }
122             }
123         } finally {
124             Binder.restoreCallingIdentity(token);
125         }
126         return result;
127     }
128 
runAppendTestAllowlistComponent()129     private int runAppendTestAllowlistComponent() {
130         LocalImpl localManager = (LocalImpl) mService.getLocalManager();
131         String allowlistType = getNextArgRequired();
132         if (allowlistType.equals("content-provider")) {
133             localManager.appendTestContentProviderAllowlist(peekRemainingArgs());
134         } else if (allowlistType.equals("send-broadcast")) {
135             localManager.appendTestSendBroadcastAllowlist(peekRemainingArgs());
136         } else {
137             throw new IllegalArgumentException(
138                     "Unknown argument provided to SDK sandbox shell command");
139         }
140 
141         return 0;
142     }
143 
getTestAllowlist()144     private int getTestAllowlist() {
145         LocalImpl localManager = (LocalImpl) mService.getLocalManager();
146         String allowlistType = getNextArgRequired();
147         ArraySet<String> allowlist;
148         if (allowlistType.equals("content-provider")) {
149             allowlist = localManager.getTestContentProviderAllowlist();
150         } else if (allowlistType.equals("send-broadcast")) {
151             allowlist = localManager.getTestSendBroadcastAllowlist();
152         } else {
153             throw new IllegalArgumentException(
154                     "Unknown argument provided to SDK sandbox shell command");
155         }
156 
157         getOutPrintWriter().println(String.join(" ", allowlist));
158         return 0;
159     }
160 
runClearTestAllowlists()161     private int runClearTestAllowlists() {
162         LocalImpl localManager = (LocalImpl) mService.getLocalManager();
163         localManager.clearTestAllowlists();
164         return 0;
165     }
166 
167     /* Delegates the shell command and args to adservice manager, executes the shell
168     command and returns the result back. */
runAdServicesShellCommand()169     private int runAdServicesShellCommand() {
170         int result = -1;
171         if (!mSupportsAdServicesShellCmd) {
172             getErrPrintWriter()
173                     .println(
174                             "AdServices shell command not supported through sdk_sandbox service."
175                                     + " Trying calling it using adservices_manager service.");
176             return result;
177         }
178         String[] args = getAllArgs();
179         // strip "adservices-cmd" which is the first argument from the args .
180         String[] realArgs = new String[args.length - 1];
181         System.arraycopy(args, 1, realArgs, 0, args.length - 1);
182 
183         try (ParcelFileDescriptor pfdIn = ParcelFileDescriptor.dup(getInFileDescriptor());
184                 ParcelFileDescriptor pfdOut = ParcelFileDescriptor.dup(getOutFileDescriptor());
185                 ParcelFileDescriptor pfdErr = ParcelFileDescriptor.dup(getErrFileDescriptor())) {
186             Binder adServicesBinder = (Binder) mService.getAdServicesManager();
187             result = adServicesBinder.handleShellCommand(pfdIn, pfdOut, pfdErr, realArgs);
188         } catch (IOException e) {
189             Log.e(TAG, "Failed to copy file descriptor for cmd: " + Arrays.toString(args), e);
190         }
191         return result;
192     }
193 
194     // Suppress lint warning for context.getUser in R since this code is unused in R
195     @SuppressWarnings("NewApi")
handleSandboxArguments()196     private void handleSandboxArguments() {
197         String opt;
198         while ((opt = getNextOption()) != null) {
199             if (opt.equals("--user")) {
200                 mUserId = parseUserArg(getNextArgRequired());
201             } else {
202                 throw new IllegalArgumentException("Unknown option: " + opt);
203             }
204         }
205 
206         if (mUserId == UserHandle.CURRENT.getIdentifier()) {
207             mUserId = mContext.getUser().getIdentifier();
208         }
209 
210         String callingPackageName = getNextArgRequired();
211         try {
212             ApplicationInfo info = mContext.getPackageManager().getApplicationInfoAsUser(
213                     callingPackageName, /* flags */ 0, UserHandle.of(mUserId));
214 
215             if ((info.flags & ApplicationInfo.FLAG_DEBUGGABLE) == 0) {
216                 throw new IllegalArgumentException(
217                         "Package " + callingPackageName + " must be debuggable.");
218             }
219             mCallingInfo = new CallingInfo(info.uid, callingPackageName);
220         } catch (NameNotFoundException e) {
221             throw new IllegalArgumentException(
222                     "No such package " + callingPackageName + " for user " + mUserId);
223         }
224     }
225 
226     // Suppress lint warning for context.getUser in R since this code is unused in R
227     @SuppressWarnings("NewApi")
parseUserArg(String arg)228     private int parseUserArg(String arg) {
229         switch (arg) {
230             case "all":
231                 throw new IllegalArgumentException("Cannot run sdk_sandbox command for user 'all'");
232             case "current":
233                 return mContext.getUser().getIdentifier();
234             default:
235                 try {
236                     return Integer.parseInt(arg);
237                 } catch (NumberFormatException e) {
238                     throw new IllegalArgumentException("Bad user number: " + arg);
239                 }
240         }
241     }
242 
243     /** Callback for binding sandbox. Provides blocking interface {@link #isSuccessful()}. */
244     private class LatchSandboxServiceConnectionCallback
245             implements SdkSandboxManagerService.SandboxBindingCallback {
246 
247         private final CountDownLatch mLatch = new CountDownLatch(1);
248         private boolean mSuccess = false;
249         public static final int SANDBOX_BIND_TIMEOUT_S = 5;
250 
251         @Override
onBindingSuccessful( ISdkSandboxService service, SandboxLatencyInfo sandboxLatencyInfo)252         public void onBindingSuccessful(
253                 ISdkSandboxService service, SandboxLatencyInfo sandboxLatencyInfo) {
254             mSuccess = true;
255             mLatch.countDown();
256         }
257 
258         @Override
onBindingFailed(LoadSdkException e, SandboxLatencyInfo sandboxLatencyInfo)259         public void onBindingFailed(LoadSdkException e, SandboxLatencyInfo sandboxLatencyInfo) {
260             mLatch.countDown();
261         }
262 
isSuccessful()263         public boolean isSuccessful() {
264             try {
265                 boolean completed = mLatch.await(SANDBOX_BIND_TIMEOUT_S, TimeUnit.SECONDS);
266                 if (!completed) {
267                     getErrPrintWriter()
268                             .println(
269                                     "Error: Sdk sandbox failed to start in "
270                                             + SANDBOX_BIND_TIMEOUT_S
271                                             + " seconds");
272                     return false;
273                 }
274                 if (!mSuccess) {
275                     getErrPrintWriter().println("Error: Sdk sandbox failed to start");
276                     return false;
277                 }
278                 return true;
279             } catch (InterruptedException e) {
280                 return false;
281             }
282         }
283     }
284 
runStart()285     private int runStart() {
286         handleSandboxArguments();
287         if (mService.isSdkSandboxServiceRunning(mCallingInfo)) {
288             getErrPrintWriter().println("Error: Sdk sandbox already running for "
289                     + mCallingInfo.getPackageName() + " and user " + mUserId);
290             return -1;
291         }
292 
293         LatchSandboxServiceConnectionCallback callback =
294                 new LatchSandboxServiceConnectionCallback();
295         final SandboxLatencyInfo sandboxLatencyInfo = new SandboxLatencyInfo();
296 
297         mService.startSdkSandboxIfNeeded(mCallingInfo, callback, sandboxLatencyInfo);
298         if (callback.isSuccessful()) {
299             if (mService.isSdkSandboxDisabled()) {
300                 getErrPrintWriter().println("Error: SDK sandbox is disabled.");
301                 mService.stopSdkSandboxService(
302                         mCallingInfo,
303                         "Shell command `sdk_sandbox start` failed due to sandbox disabled.");
304                 return -1;
305             }
306             return 0;
307         }
308         getErrPrintWriter()
309                 .println("Error: Could not start SDK sandbox for " + mCallingInfo.getPackageName());
310         return -1;
311     }
312 
runStop()313     private int runStop() {
314         handleSandboxArguments();
315         if (!mService.isSdkSandboxServiceRunning(mCallingInfo)) {
316             getErrPrintWriter().println("Sdk sandbox not running for "
317                     + mCallingInfo.getPackageName() + " and user " + mUserId);
318             return -1;
319         }
320         mService.stopSdkSandboxService(mCallingInfo, "Shell command 'sdk_sandbox stop' issued");
321         return 0;
322     }
323 
runSetState()324     private int runSetState() {
325         String opt;
326         if ((opt = getNextOption()) != null) {
327             switch (opt) {
328                 case "--enabled":
329                     mService.forceEnableSandbox();
330                     break;
331                 case "--reset":
332                     mService.clearSdkSandboxState();
333                     break;
334                 default:
335                     throw new IllegalArgumentException("Unknown argument: " + opt);
336             }
337         } else {
338             throw new IllegalArgumentException("No argument supplied to `sdk_sandbox set-state`");
339         }
340         return 0;
341     }
342 
343     @Override
onHelp()344     public void onHelp() {
345         final PrintWriter pw = getOutPrintWriter();
346         pw.println("SDK sandbox (sdk_sandbox) commands: ");
347         pw.println("    help: ");
348         pw.println("        Prints this help text.");
349         pw.println();
350         pw.println("    start [--user <USER_ID> | current] <PACKAGE>");
351         pw.println("        Start the SDK sandbox for the app <PACKAGE>. Options are:");
352         pw.println("        --user <USER_ID> | current: Specify user for app; uses current user");
353         pw.println("            if not specified");
354         pw.println();
355         pw.println("    stop [--user <USER_ID> | current] <PACKAGE>");
356         pw.println("        Stop the SDK sandbox for the app <PACKAGE>. Options are:");
357         pw.println("        --user <USER_ID> | current: Specify user for app; uses current user");
358         pw.println("            if not specified");
359         pw.println();
360         pw.println("    set-state [--enabled | --reset]");
361         pw.println("        Sets the SDK sandbox state for testing purposes. Options are:");
362         pw.println("        --enabled: Sets the state to enabled");
363         pw.println("        --reset: Resets the state. It will be calculated the next time an");
364         pw.println("                 SDK is loaded");
365     }
366 }
367