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