1 /* 2 * Copyright (C) 2019 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.pm; 18 19 import android.annotation.NonNull; 20 import android.content.ComponentName; 21 import android.content.pm.ArchivedPackageParcel; 22 import android.content.pm.DataLoaderParams; 23 import android.content.pm.InstallationFile; 24 import android.content.pm.PackageInstaller; 25 import android.os.Parcel; 26 import android.os.ParcelFileDescriptor; 27 import android.os.ShellCommand; 28 import android.service.dataloader.DataLoaderService; 29 import android.util.Slog; 30 import android.util.SparseArray; 31 32 import com.android.internal.annotations.VisibleForTesting; 33 34 import libcore.io.IoUtils; 35 36 import java.io.IOException; 37 import java.lang.ref.WeakReference; 38 import java.nio.ByteBuffer; 39 import java.nio.ByteOrder; 40 import java.nio.charset.StandardCharsets; 41 import java.security.SecureRandom; 42 import java.util.Arrays; 43 import java.util.Collection; 44 import java.util.concurrent.atomic.AtomicLong; 45 46 /** 47 * Callback data loader for PackageManagerShellCommand installations. 48 */ 49 public class PackageManagerShellCommandDataLoader extends DataLoaderService { 50 public static final String TAG = "PackageManagerShellCommandDataLoader"; 51 52 private static final String PACKAGE = "android"; 53 private static final String CLASS = PackageManagerShellCommandDataLoader.class.getName(); 54 55 static final SecureRandom sRandom = new SecureRandom(); 56 static final SparseArray<WeakReference<ShellCommand>> sShellCommands = new SparseArray<>(); 57 58 private static final char ARGS_DELIM = '&'; 59 private static final String SHELL_COMMAND_ID_PREFIX = "shellCommandId="; 60 private static final int INVALID_SHELL_COMMAND_ID = -1; 61 private static final int TOO_MANY_PENDING_SHELL_COMMANDS = 10; 62 63 private static final String STDIN_PATH = "-"; 64 getDataLoaderParamsArgs(ShellCommand shellCommand)65 private static String getDataLoaderParamsArgs(ShellCommand shellCommand) { 66 nativeInitialize(); 67 68 int commandId; 69 synchronized (sShellCommands) { 70 // Clean up old references. 71 for (int i = sShellCommands.size() - 1; i >= 0; i--) { 72 WeakReference<ShellCommand> oldRef = sShellCommands.valueAt(i); 73 if (oldRef.get() == null) { 74 sShellCommands.removeAt(i); 75 } 76 } 77 78 if (sShellCommands.size() > TOO_MANY_PENDING_SHELL_COMMANDS) { 79 Slog.e(TAG, "Too many pending shell commands: " + sShellCommands.size()); 80 } 81 82 // Generate new id and put ref to the array. 83 do { 84 commandId = sRandom.nextInt(Integer.MAX_VALUE - 1) + 1; 85 } while (sShellCommands.contains(commandId)); 86 87 sShellCommands.put(commandId, new WeakReference<>(shellCommand)); 88 } 89 90 return SHELL_COMMAND_ID_PREFIX + commandId; 91 } 92 getStreamingDataLoaderParams(ShellCommand shellCommand)93 static DataLoaderParams getStreamingDataLoaderParams(ShellCommand shellCommand) { 94 return DataLoaderParams.forStreaming(new ComponentName(PACKAGE, CLASS), 95 getDataLoaderParamsArgs(shellCommand)); 96 } 97 getIncrementalDataLoaderParams(ShellCommand shellCommand)98 static DataLoaderParams getIncrementalDataLoaderParams(ShellCommand shellCommand) { 99 return DataLoaderParams.forIncremental(new ComponentName(PACKAGE, CLASS), 100 getDataLoaderParamsArgs(shellCommand)); 101 } 102 extractShellCommandId(String args)103 private static int extractShellCommandId(String args) { 104 int sessionIdIdx = args.indexOf(SHELL_COMMAND_ID_PREFIX); 105 if (sessionIdIdx < 0) { 106 Slog.e(TAG, "Missing shell command id param."); 107 return INVALID_SHELL_COMMAND_ID; 108 } 109 sessionIdIdx += SHELL_COMMAND_ID_PREFIX.length(); 110 int delimIdx = args.indexOf(ARGS_DELIM, sessionIdIdx); 111 try { 112 if (delimIdx < 0) { 113 return Integer.parseInt(args.substring(sessionIdIdx)); 114 } else { 115 return Integer.parseInt(args.substring(sessionIdIdx, delimIdx)); 116 } 117 } catch (NumberFormatException e) { 118 Slog.e(TAG, "Incorrect shell command id format.", e); 119 return INVALID_SHELL_COMMAND_ID; 120 } 121 } 122 123 /** @hide */ 124 @VisibleForTesting 125 public static class Metadata { 126 /** 127 * Full files read from stdin. 128 */ 129 static final byte STDIN = 0; 130 /** 131 * Full files read from local file. 132 */ 133 static final byte LOCAL_FILE = 1; 134 /** 135 * Signature tree read from stdin, data streamed. 136 */ 137 static final byte DATA_ONLY_STREAMING = 2; 138 /** 139 * Everything streamed. 140 */ 141 static final byte STREAMING = 3; 142 /** 143 * Archived install. 144 */ 145 static final byte ARCHIVED = 4; 146 147 private final byte mMode; 148 private final byte[] mData; 149 private final String mSalt; 150 151 private static final AtomicLong sGlobalSalt = 152 new AtomicLong((new SecureRandom()).nextLong()); nextGlobalSalt()153 private static Long nextGlobalSalt() { 154 return sGlobalSalt.incrementAndGet(); 155 } 156 forStdIn(String fileId)157 static Metadata forStdIn(String fileId) { 158 return new Metadata(STDIN, fileId); 159 } 160 161 /** @hide */ 162 @VisibleForTesting forLocalFile(String filePath)163 public static Metadata forLocalFile(String filePath) { 164 return new Metadata(LOCAL_FILE, filePath, nextGlobalSalt().toString()); 165 } 166 167 /** @hide */ 168 @VisibleForTesting forArchived(ArchivedPackageParcel archivedPackage)169 public static Metadata forArchived(ArchivedPackageParcel archivedPackage) { 170 return new Metadata(ARCHIVED, writeArchivedPackageParcel(archivedPackage), null); 171 } 172 forDataOnlyStreaming(String fileId)173 static Metadata forDataOnlyStreaming(String fileId) { 174 return new Metadata(DATA_ONLY_STREAMING, fileId); 175 } 176 forStreaming(String fileId)177 static Metadata forStreaming(String fileId) { 178 return new Metadata(STREAMING, fileId); 179 } 180 Metadata(byte mode, String data)181 private Metadata(byte mode, String data) { 182 this(mode, data, null); 183 } 184 Metadata(byte mode, String data, String salt)185 private Metadata(byte mode, String data, String salt) { 186 this(mode, (data != null ? data : "").getBytes(StandardCharsets.UTF_8), salt); 187 } 188 Metadata(byte mode, byte[] data, String salt)189 private Metadata(byte mode, byte[] data, String salt) { 190 this.mMode = mode; 191 this.mData = data; 192 this.mSalt = salt; 193 } 194 fromByteArray(byte[] bytes)195 static Metadata fromByteArray(byte[] bytes) throws IOException { 196 if (bytes == null || bytes.length < 5) { 197 return null; 198 } 199 int offset = 0; 200 final byte mode = bytes[offset]; 201 offset += 1; 202 final byte[] data; 203 final String salt; 204 switch (mode) { 205 case LOCAL_FILE: { 206 int dataSize = ByteBuffer.wrap(bytes, offset, 4).order( 207 ByteOrder.LITTLE_ENDIAN).getInt(); 208 offset += 4; 209 data = Arrays.copyOfRange(bytes, offset, offset + dataSize); 210 offset += dataSize; 211 salt = new String(bytes, offset, bytes.length - offset, 212 StandardCharsets.UTF_8); 213 break; 214 } 215 default: 216 data = Arrays.copyOfRange(bytes, offset, bytes.length); 217 salt = null; 218 break; 219 } 220 return new Metadata(mode, data, salt); 221 } 222 223 /** @hide */ 224 @VisibleForTesting toByteArray()225 public byte[] toByteArray() { 226 final byte[] result; 227 final byte[] dataBytes = this.mData; 228 switch (this.mMode) { 229 case LOCAL_FILE: { 230 int dataSize = dataBytes.length; 231 byte[] saltBytes = this.mSalt.getBytes(StandardCharsets.UTF_8); 232 result = new byte[1 + 4 + dataSize + saltBytes.length]; 233 int offset = 0; 234 result[offset] = this.mMode; 235 offset += 1; 236 ByteBuffer.wrap(result, offset, 4).order(ByteOrder.LITTLE_ENDIAN).putInt( 237 dataSize); 238 offset += 4; 239 System.arraycopy(dataBytes, 0, result, offset, dataSize); 240 offset += dataSize; 241 System.arraycopy(saltBytes, 0, result, offset, saltBytes.length); 242 break; 243 } 244 default: 245 result = new byte[1 + dataBytes.length]; 246 result[0] = this.mMode; 247 System.arraycopy(dataBytes, 0, result, 1, dataBytes.length); 248 break; 249 } 250 return result; 251 } 252 getMode()253 byte getMode() { 254 return this.mMode; 255 } 256 getData()257 byte[] getData() { 258 return this.mData; 259 } 260 getArchivedPackage()261 ArchivedPackageParcel getArchivedPackage() { 262 if (getMode() != ARCHIVED) { 263 throw new IllegalStateException("Not an archived package metadata."); 264 } 265 return readArchivedPackageParcel(this.mData); 266 } 267 readArchivedPackageParcel(byte[] bytes)268 static ArchivedPackageParcel readArchivedPackageParcel(byte[] bytes) { 269 Parcel parcel = Parcel.obtain(); 270 ArchivedPackageParcel result; 271 try { 272 parcel.unmarshall(bytes, 0, bytes.length); 273 parcel.setDataPosition(0); 274 result = parcel.readParcelable(ArchivedPackageParcel.class.getClassLoader()); 275 } finally { 276 parcel.recycle(); 277 } 278 return result; 279 } 280 writeArchivedPackageParcel(ArchivedPackageParcel archivedPackage)281 static byte[] writeArchivedPackageParcel(ArchivedPackageParcel archivedPackage) { 282 Parcel parcel = Parcel.obtain(); 283 try { 284 parcel.writeParcelable(archivedPackage, 0); 285 return parcel.marshall(); 286 } finally { 287 parcel.recycle(); 288 } 289 } 290 } 291 292 private static class DataLoader implements DataLoaderService.DataLoader { 293 private DataLoaderParams mParams = null; 294 private FileSystemConnector mConnector = null; 295 296 @Override onCreate(@onNull DataLoaderParams dataLoaderParams, @NonNull FileSystemConnector connector)297 public boolean onCreate(@NonNull DataLoaderParams dataLoaderParams, 298 @NonNull FileSystemConnector connector) { 299 mParams = dataLoaderParams; 300 mConnector = connector; 301 return true; 302 } 303 304 @Override onPrepareImage(@onNull Collection<InstallationFile> addedFiles, @NonNull Collection<String> removedFiles)305 public boolean onPrepareImage(@NonNull Collection<InstallationFile> addedFiles, 306 @NonNull Collection<String> removedFiles) { 307 ShellCommand shellCommand = lookupShellCommand(mParams.getArguments()); 308 try { 309 for (InstallationFile file : addedFiles) { 310 Metadata metadata = Metadata.fromByteArray(file.getMetadata()); 311 if (metadata == null) { 312 Slog.e(TAG, "Invalid metadata for file: " + file.getName()); 313 return false; 314 } 315 switch (metadata.getMode()) { 316 case Metadata.STDIN: { 317 if (shellCommand == null) { 318 Slog.e(TAG, "Missing shell command for Metadata.STDIN."); 319 return false; 320 } 321 final ParcelFileDescriptor inFd = getStdInPFD(shellCommand); 322 mConnector.writeData(file.getName(), 0, file.getLengthBytes(), inFd); 323 break; 324 } 325 case Metadata.LOCAL_FILE: { 326 if (shellCommand == null) { 327 Slog.e(TAG, "Missing shell command for Metadata.LOCAL_FILE."); 328 return false; 329 } 330 ParcelFileDescriptor incomingFd = null; 331 try { 332 final String filePath = new String(metadata.getData(), 333 StandardCharsets.UTF_8); 334 incomingFd = getLocalFilePFD(shellCommand, filePath); 335 mConnector.writeData(file.getName(), 0, incomingFd.getStatSize(), 336 incomingFd); 337 } finally { 338 IoUtils.closeQuietly(incomingFd); 339 } 340 break; 341 } 342 case Metadata.ARCHIVED: { 343 // Do nothing, metadata already contains everything needed for install. 344 break; 345 } 346 default: 347 Slog.e(TAG, "Unsupported metadata mode: " + metadata.getMode()); 348 return false; 349 } 350 } 351 return true; 352 } catch (IOException e) { 353 Slog.e(TAG, "Exception while streaming files", e); 354 return false; 355 } 356 } 357 } 358 lookupShellCommand(String args)359 static ShellCommand lookupShellCommand(String args) { 360 final int commandId = extractShellCommandId(args); 361 if (commandId == INVALID_SHELL_COMMAND_ID) { 362 return null; 363 } 364 365 final WeakReference<ShellCommand> shellCommandRef; 366 synchronized (sShellCommands) { 367 shellCommandRef = sShellCommands.get(commandId, null); 368 } 369 final ShellCommand shellCommand = 370 shellCommandRef != null ? shellCommandRef.get() : null; 371 372 return shellCommand; 373 } 374 getStdInPFD(ShellCommand shellCommand)375 static ParcelFileDescriptor getStdInPFD(ShellCommand shellCommand) { 376 try { 377 return ParcelFileDescriptor.dup(shellCommand.getInFileDescriptor()); 378 } catch (IOException e) { 379 Slog.e(TAG, "Exception while obtaining STDIN fd", e); 380 return null; 381 } 382 } 383 getLocalFilePFD(ShellCommand shellCommand, String filePath)384 static ParcelFileDescriptor getLocalFilePFD(ShellCommand shellCommand, String filePath) { 385 return shellCommand.openFileForSystem(filePath, "r"); 386 } 387 getStdIn(ShellCommand shellCommand)388 static int getStdIn(ShellCommand shellCommand) { 389 ParcelFileDescriptor pfd = getStdInPFD(shellCommand); 390 return pfd == null ? -1 : pfd.detachFd(); 391 } 392 getLocalFile(ShellCommand shellCommand, String filePath)393 static int getLocalFile(ShellCommand shellCommand, String filePath) { 394 ParcelFileDescriptor pfd = getLocalFilePFD(shellCommand, filePath); 395 return pfd == null ? -1 : pfd.detachFd(); 396 } 397 398 @Override onCreateDataLoader( @onNull DataLoaderParams dataLoaderParams)399 public DataLoaderService.DataLoader onCreateDataLoader( 400 @NonNull DataLoaderParams dataLoaderParams) { 401 if (dataLoaderParams.getType() == PackageInstaller.DATA_LOADER_TYPE_STREAMING) { 402 // This DataLoader only supports streaming installations. 403 return new DataLoader(); 404 } 405 return null; 406 } 407 408 /* Native methods */ nativeInitialize()409 private static native void nativeInitialize(); 410 } 411