1 /* 2 * Copyright 2020 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 package com.android.server.blob; 17 18 import static android.app.blob.BlobStoreManager.COMMIT_RESULT_ERROR; 19 import static android.app.blob.XmlTags.ATTR_CREATION_TIME_MS; 20 import static android.app.blob.XmlTags.ATTR_ID; 21 import static android.app.blob.XmlTags.ATTR_PACKAGE; 22 import static android.app.blob.XmlTags.ATTR_UID; 23 import static android.app.blob.XmlTags.TAG_ACCESS_MODE; 24 import static android.app.blob.XmlTags.TAG_BLOB_HANDLE; 25 import static android.os.Trace.TRACE_TAG_SYSTEM_SERVER; 26 import static android.system.OsConstants.O_CREAT; 27 import static android.system.OsConstants.O_RDONLY; 28 import static android.system.OsConstants.O_RDWR; 29 import static android.system.OsConstants.SEEK_SET; 30 import static android.text.format.Formatter.FLAG_IEC_UNITS; 31 import static android.text.format.Formatter.formatFileSize; 32 33 import static com.android.server.blob.BlobStoreConfig.TAG; 34 import static com.android.server.blob.BlobStoreConfig.XML_VERSION_ADD_SESSION_CREATION_TIME; 35 import static com.android.server.blob.BlobStoreConfig.getMaxPermittedPackages; 36 import static com.android.server.blob.BlobStoreConfig.hasSessionExpired; 37 38 import android.annotation.BytesLong; 39 import android.annotation.NonNull; 40 import android.annotation.Nullable; 41 import android.app.blob.BlobHandle; 42 import android.app.blob.IBlobCommitCallback; 43 import android.app.blob.IBlobStoreSession; 44 import android.content.Context; 45 import android.os.Binder; 46 import android.os.FileUtils; 47 import android.os.LimitExceededException; 48 import android.os.ParcelFileDescriptor; 49 import android.os.ParcelableException; 50 import android.os.RemoteException; 51 import android.os.RevocableFileDescriptor; 52 import android.os.Trace; 53 import android.os.storage.StorageManager; 54 import android.system.ErrnoException; 55 import android.system.Os; 56 import android.util.ExceptionUtils; 57 import android.util.IndentingPrintWriter; 58 import android.util.Slog; 59 60 import com.android.internal.annotations.GuardedBy; 61 import com.android.internal.annotations.VisibleForTesting; 62 import com.android.internal.util.FrameworkStatsLog; 63 import com.android.internal.util.Preconditions; 64 import com.android.internal.util.XmlUtils; 65 import com.android.server.blob.BlobStoreManagerService.DumpArgs; 66 import com.android.server.blob.BlobStoreManagerService.SessionStateChangeListener; 67 68 import libcore.io.IoUtils; 69 70 import org.xmlpull.v1.XmlPullParser; 71 import org.xmlpull.v1.XmlPullParserException; 72 import org.xmlpull.v1.XmlSerializer; 73 74 import java.io.File; 75 import java.io.FileDescriptor; 76 import java.io.IOException; 77 import java.security.NoSuchAlgorithmException; 78 import java.util.ArrayList; 79 import java.util.Arrays; 80 import java.util.Objects; 81 82 /** 83 * Class to represent the state corresponding to an ongoing 84 * {@link android.app.blob.BlobStoreManager.Session} 85 */ 86 @VisibleForTesting 87 class BlobStoreSession extends IBlobStoreSession.Stub { 88 89 static final int STATE_OPENED = 1; 90 static final int STATE_CLOSED = 0; 91 static final int STATE_ABANDONED = 2; 92 static final int STATE_COMMITTED = 3; 93 static final int STATE_VERIFIED_VALID = 4; 94 static final int STATE_VERIFIED_INVALID = 5; 95 96 private final Object mSessionLock = new Object(); 97 98 private final Context mContext; 99 private final SessionStateChangeListener mListener; 100 101 private final BlobHandle mBlobHandle; 102 private final long mSessionId; 103 private final int mOwnerUid; 104 private final String mOwnerPackageName; 105 private final long mCreationTimeMs; 106 107 // Do not access this directly, instead use getSessionFile(). 108 private File mSessionFile; 109 110 @GuardedBy("mRevocableFds") 111 private final ArrayList<RevocableFileDescriptor> mRevocableFds = new ArrayList<>(); 112 113 // This will be accessed from only one thread at any point of time, so no need to grab 114 // a lock for this. 115 private byte[] mDataDigest; 116 117 @GuardedBy("mSessionLock") 118 private int mState = STATE_CLOSED; 119 120 @GuardedBy("mSessionLock") 121 private final BlobAccessMode mBlobAccessMode = new BlobAccessMode(); 122 123 @GuardedBy("mSessionLock") 124 private IBlobCommitCallback mBlobCommitCallback; 125 BlobStoreSession(Context context, long sessionId, BlobHandle blobHandle, int ownerUid, String ownerPackageName, long creationTimeMs, SessionStateChangeListener listener)126 private BlobStoreSession(Context context, long sessionId, BlobHandle blobHandle, 127 int ownerUid, String ownerPackageName, long creationTimeMs, 128 SessionStateChangeListener listener) { 129 this.mContext = context; 130 this.mBlobHandle = blobHandle; 131 this.mSessionId = sessionId; 132 this.mOwnerUid = ownerUid; 133 this.mOwnerPackageName = ownerPackageName; 134 this.mCreationTimeMs = creationTimeMs; 135 this.mListener = listener; 136 } 137 BlobStoreSession(Context context, long sessionId, BlobHandle blobHandle, int ownerUid, String ownerPackageName, SessionStateChangeListener listener)138 BlobStoreSession(Context context, long sessionId, BlobHandle blobHandle, 139 int ownerUid, String ownerPackageName, SessionStateChangeListener listener) { 140 this(context, sessionId, blobHandle, ownerUid, ownerPackageName, 141 System.currentTimeMillis(), listener); 142 } 143 getBlobHandle()144 public BlobHandle getBlobHandle() { 145 return mBlobHandle; 146 } 147 getSessionId()148 public long getSessionId() { 149 return mSessionId; 150 } 151 getOwnerUid()152 public int getOwnerUid() { 153 return mOwnerUid; 154 } 155 getOwnerPackageName()156 public String getOwnerPackageName() { 157 return mOwnerPackageName; 158 } 159 hasAccess(int callingUid, String callingPackageName)160 boolean hasAccess(int callingUid, String callingPackageName) { 161 return mOwnerUid == callingUid && mOwnerPackageName.equals(callingPackageName); 162 } 163 open()164 void open() { 165 synchronized (mSessionLock) { 166 if (isFinalized()) { 167 throw new IllegalStateException("Not allowed to open session with state: " 168 + stateToString(mState)); 169 } 170 mState = STATE_OPENED; 171 } 172 } 173 getState()174 int getState() { 175 synchronized (mSessionLock) { 176 return mState; 177 } 178 } 179 sendCommitCallbackResult(int result)180 void sendCommitCallbackResult(int result) { 181 synchronized (mSessionLock) { 182 try { 183 mBlobCommitCallback.onResult(result); 184 } catch (RemoteException e) { 185 Slog.d(TAG, "Error sending the callback result", e); 186 } 187 mBlobCommitCallback = null; 188 } 189 } 190 getBlobAccessMode()191 BlobAccessMode getBlobAccessMode() { 192 synchronized (mSessionLock) { 193 return mBlobAccessMode; 194 } 195 } 196 isFinalized()197 boolean isFinalized() { 198 synchronized (mSessionLock) { 199 return mState == STATE_COMMITTED || mState == STATE_ABANDONED; 200 } 201 } 202 isExpired()203 boolean isExpired() { 204 final long lastModifiedTimeMs = getSessionFile().lastModified(); 205 return hasSessionExpired(lastModifiedTimeMs == 0 206 ? mCreationTimeMs : lastModifiedTimeMs); 207 } 208 209 @Override 210 @NonNull openWrite(@ytesLong long offsetBytes, @BytesLong long lengthBytes)211 public ParcelFileDescriptor openWrite(@BytesLong long offsetBytes, 212 @BytesLong long lengthBytes) { 213 Preconditions.checkArgumentNonnegative(offsetBytes, "offsetBytes must not be negative"); 214 215 assertCallerIsOwner(); 216 synchronized (mSessionLock) { 217 if (mState != STATE_OPENED) { 218 throw new IllegalStateException("Not allowed to write in state: " 219 + stateToString(mState)); 220 } 221 } 222 223 FileDescriptor fd = null; 224 try { 225 fd = openWriteInternal(offsetBytes, lengthBytes); 226 final RevocableFileDescriptor revocableFd = new RevocableFileDescriptor(mContext, fd); 227 synchronized (mSessionLock) { 228 if (mState != STATE_OPENED) { 229 IoUtils.closeQuietly(fd); 230 throw new IllegalStateException("Not allowed to write in state: " 231 + stateToString(mState)); 232 } 233 trackRevocableFdLocked(revocableFd); 234 return revocableFd.getRevocableFileDescriptor(); 235 } 236 } catch (IOException e) { 237 IoUtils.closeQuietly(fd); 238 throw ExceptionUtils.wrap(e); 239 } 240 } 241 242 @NonNull openWriteInternal(@ytesLong long offsetBytes, @BytesLong long lengthBytes)243 private FileDescriptor openWriteInternal(@BytesLong long offsetBytes, 244 @BytesLong long lengthBytes) throws IOException { 245 // TODO: Add limit on active open sessions/writes/reads 246 try { 247 final File sessionFile = getSessionFile(); 248 if (sessionFile == null) { 249 throw new IllegalStateException("Couldn't get the file for this session"); 250 } 251 final FileDescriptor fd = Os.open(sessionFile.getPath(), O_CREAT | O_RDWR, 0600); 252 if (offsetBytes > 0) { 253 final long curOffset = Os.lseek(fd, offsetBytes, SEEK_SET); 254 if (curOffset != offsetBytes) { 255 throw new IllegalStateException("Failed to seek " + offsetBytes 256 + "; curOffset=" + offsetBytes); 257 } 258 } 259 if (lengthBytes > 0) { 260 mContext.getSystemService(StorageManager.class).allocateBytes(fd, lengthBytes); 261 } 262 return fd; 263 } catch (ErrnoException e) { 264 throw e.rethrowAsIOException(); 265 } 266 } 267 268 @Override 269 @NonNull openRead()270 public ParcelFileDescriptor openRead() { 271 assertCallerIsOwner(); 272 synchronized (mSessionLock) { 273 if (mState != STATE_OPENED) { 274 throw new IllegalStateException("Not allowed to read in state: " 275 + stateToString(mState)); 276 } 277 if (!BlobStoreConfig.shouldUseRevocableFdForReads()) { 278 try { 279 return new ParcelFileDescriptor(openReadInternal()); 280 } catch (IOException e) { 281 throw ExceptionUtils.wrap(e); 282 } 283 } 284 } 285 286 FileDescriptor fd = null; 287 try { 288 fd = openReadInternal(); 289 final RevocableFileDescriptor revocableFd = new RevocableFileDescriptor(mContext, fd); 290 synchronized (mSessionLock) { 291 if (mState != STATE_OPENED) { 292 IoUtils.closeQuietly(fd); 293 throw new IllegalStateException("Not allowed to read in state: " 294 + stateToString(mState)); 295 } 296 trackRevocableFdLocked(revocableFd); 297 return revocableFd.getRevocableFileDescriptor(); 298 } 299 } catch (IOException e) { 300 IoUtils.closeQuietly(fd); 301 throw ExceptionUtils.wrap(e); 302 } 303 } 304 305 @NonNull openReadInternal()306 private FileDescriptor openReadInternal() throws IOException { 307 try { 308 final File sessionFile = getSessionFile(); 309 if (sessionFile == null) { 310 throw new IllegalStateException("Couldn't get the file for this session"); 311 } 312 final FileDescriptor fd = Os.open(sessionFile.getPath(), O_RDONLY, 0); 313 return fd; 314 } catch (ErrnoException e) { 315 throw e.rethrowAsIOException(); 316 } 317 } 318 319 @Override 320 @BytesLong getSize()321 public long getSize() { 322 return getSessionFile().length(); 323 } 324 325 @Override allowPackageAccess(@onNull String packageName, @NonNull byte[] certificate)326 public void allowPackageAccess(@NonNull String packageName, 327 @NonNull byte[] certificate) { 328 assertCallerIsOwner(); 329 Objects.requireNonNull(packageName, "packageName must not be null"); 330 synchronized (mSessionLock) { 331 if (mState != STATE_OPENED) { 332 throw new IllegalStateException("Not allowed to change access type in state: " 333 + stateToString(mState)); 334 } 335 if (mBlobAccessMode.getAllowedPackagesCount() >= getMaxPermittedPackages()) { 336 throw new ParcelableException(new LimitExceededException( 337 "Too many packages permitted to access the blob: " 338 + mBlobAccessMode.getAllowedPackagesCount())); 339 } 340 mBlobAccessMode.allowPackageAccess(packageName, certificate); 341 } 342 } 343 344 @Override allowSameSignatureAccess()345 public void allowSameSignatureAccess() { 346 assertCallerIsOwner(); 347 synchronized (mSessionLock) { 348 if (mState != STATE_OPENED) { 349 throw new IllegalStateException("Not allowed to change access type in state: " 350 + stateToString(mState)); 351 } 352 mBlobAccessMode.allowSameSignatureAccess(); 353 } 354 } 355 356 @Override allowPublicAccess()357 public void allowPublicAccess() { 358 assertCallerIsOwner(); 359 synchronized (mSessionLock) { 360 if (mState != STATE_OPENED) { 361 throw new IllegalStateException("Not allowed to change access type in state: " 362 + stateToString(mState)); 363 } 364 mBlobAccessMode.allowPublicAccess(); 365 } 366 } 367 368 @Override isPackageAccessAllowed(@onNull String packageName, @NonNull byte[] certificate)369 public boolean isPackageAccessAllowed(@NonNull String packageName, 370 @NonNull byte[] certificate) { 371 assertCallerIsOwner(); 372 Objects.requireNonNull(packageName, "packageName must not be null"); 373 Preconditions.checkByteArrayNotEmpty(certificate, "certificate"); 374 375 synchronized (mSessionLock) { 376 if (mState != STATE_OPENED) { 377 throw new IllegalStateException("Not allowed to get access type in state: " 378 + stateToString(mState)); 379 } 380 return mBlobAccessMode.isPackageAccessAllowed(packageName, certificate); 381 } 382 } 383 384 @Override isSameSignatureAccessAllowed()385 public boolean isSameSignatureAccessAllowed() { 386 assertCallerIsOwner(); 387 synchronized (mSessionLock) { 388 if (mState != STATE_OPENED) { 389 throw new IllegalStateException("Not allowed to get access type in state: " 390 + stateToString(mState)); 391 } 392 return mBlobAccessMode.isSameSignatureAccessAllowed(); 393 } 394 } 395 396 @Override isPublicAccessAllowed()397 public boolean isPublicAccessAllowed() { 398 assertCallerIsOwner(); 399 synchronized (mSessionLock) { 400 if (mState != STATE_OPENED) { 401 throw new IllegalStateException("Not allowed to get access type in state: " 402 + stateToString(mState)); 403 } 404 return mBlobAccessMode.isPublicAccessAllowed(); 405 } 406 } 407 408 @Override close()409 public void close() { 410 closeSession(STATE_CLOSED, false /* sendCallback */); 411 } 412 413 @Override abandon()414 public void abandon() { 415 closeSession(STATE_ABANDONED, true /* sendCallback */); 416 } 417 418 @Override commit(IBlobCommitCallback callback)419 public void commit(IBlobCommitCallback callback) { 420 synchronized (mSessionLock) { 421 mBlobCommitCallback = callback; 422 423 closeSession(STATE_COMMITTED, true /* sendCallback */); 424 } 425 } 426 closeSession(int state, boolean sendCallback)427 private void closeSession(int state, boolean sendCallback) { 428 assertCallerIsOwner(); 429 synchronized (mSessionLock) { 430 if (mState != STATE_OPENED) { 431 if (state == STATE_CLOSED) { 432 // Just trying to close the session which is already deleted or abandoned, 433 // ignore. 434 return; 435 } else { 436 throw new IllegalStateException("Not allowed to delete or abandon a session" 437 + " with state: " + stateToString(mState)); 438 } 439 } 440 441 mState = state; 442 revokeAllFds(); 443 444 if (sendCallback) { 445 mListener.onStateChanged(this); 446 } 447 } 448 } 449 computeDigest()450 void computeDigest() { 451 try { 452 Trace.traceBegin(TRACE_TAG_SYSTEM_SERVER, 453 "computeBlobDigest-i" + mSessionId + "-l" + getSessionFile().length()); 454 mDataDigest = FileUtils.digest(getSessionFile(), mBlobHandle.algorithm); 455 } catch (IOException | NoSuchAlgorithmException e) { 456 Slog.e(TAG, "Error computing the digest", e); 457 } finally { 458 Trace.traceEnd(TRACE_TAG_SYSTEM_SERVER); 459 } 460 } 461 verifyBlobData()462 void verifyBlobData() { 463 synchronized (mSessionLock) { 464 if (mDataDigest != null && Arrays.equals(mDataDigest, mBlobHandle.digest)) { 465 mState = STATE_VERIFIED_VALID; 466 // Commit callback will be sent once the data is persisted. 467 } else { 468 Slog.d(TAG, "Digest of the data (" 469 + (mDataDigest == null ? "null" : BlobHandle.safeDigest(mDataDigest)) 470 + ") didn't match the given BlobHandle.digest (" 471 + BlobHandle.safeDigest(mBlobHandle.digest) + ")"); 472 mState = STATE_VERIFIED_INVALID; 473 474 FrameworkStatsLog.write(FrameworkStatsLog.BLOB_COMMITTED, getOwnerUid(), mSessionId, 475 getSize(), FrameworkStatsLog.BLOB_COMMITTED__RESULT__DIGEST_MISMATCH); 476 sendCommitCallbackResult(COMMIT_RESULT_ERROR); 477 } 478 mListener.onStateChanged(this); 479 } 480 } 481 destroy()482 void destroy() { 483 revokeAllFds(); 484 getSessionFile().delete(); 485 } 486 revokeAllFds()487 private void revokeAllFds() { 488 synchronized (mRevocableFds) { 489 for (int i = mRevocableFds.size() - 1; i >= 0; --i) { 490 mRevocableFds.get(i).revoke(); 491 } 492 mRevocableFds.clear(); 493 } 494 } 495 496 @GuardedBy("mSessionLock") trackRevocableFdLocked(RevocableFileDescriptor revocableFd)497 private void trackRevocableFdLocked(RevocableFileDescriptor revocableFd) { 498 synchronized (mRevocableFds) { 499 mRevocableFds.add(revocableFd); 500 } 501 revocableFd.addOnCloseListener((e) -> { 502 synchronized (mRevocableFds) { 503 mRevocableFds.remove(revocableFd); 504 } 505 }); 506 } 507 508 @Nullable getSessionFile()509 File getSessionFile() { 510 if (mSessionFile == null) { 511 mSessionFile = BlobStoreConfig.prepareBlobFile(mSessionId); 512 } 513 return mSessionFile; 514 } 515 516 @NonNull stateToString(int state)517 static String stateToString(int state) { 518 switch (state) { 519 case STATE_OPENED: 520 return "<opened>"; 521 case STATE_CLOSED: 522 return "<closed>"; 523 case STATE_ABANDONED: 524 return "<abandoned>"; 525 case STATE_COMMITTED: 526 return "<committed>"; 527 case STATE_VERIFIED_VALID: 528 return "<verified_valid>"; 529 case STATE_VERIFIED_INVALID: 530 return "<verified_invalid>"; 531 default: 532 Slog.wtf(TAG, "Unknown state: " + state); 533 return "<unknown>"; 534 } 535 } 536 537 @Override toString()538 public String toString() { 539 return "BlobStoreSession {" 540 + "id:" + mSessionId 541 + ",handle:" + mBlobHandle 542 + ",uid:" + mOwnerUid 543 + ",pkg:" + mOwnerPackageName 544 + "}"; 545 } 546 assertCallerIsOwner()547 private void assertCallerIsOwner() { 548 final int callingUid = Binder.getCallingUid(); 549 if (callingUid != mOwnerUid) { 550 throw new SecurityException(mOwnerUid + " is not the session owner"); 551 } 552 } 553 dump(IndentingPrintWriter fout, DumpArgs dumpArgs)554 void dump(IndentingPrintWriter fout, DumpArgs dumpArgs) { 555 synchronized (mSessionLock) { 556 fout.println("state: " + stateToString(mState)); 557 fout.println("ownerUid: " + mOwnerUid); 558 fout.println("ownerPkg: " + mOwnerPackageName); 559 fout.println("creation time: " + BlobStoreUtils.formatTime(mCreationTimeMs)); 560 fout.println("size: " + formatFileSize(mContext, getSize(), FLAG_IEC_UNITS)); 561 562 fout.println("blobHandle:"); 563 fout.increaseIndent(); 564 mBlobHandle.dump(fout, dumpArgs.shouldDumpFull()); 565 fout.decreaseIndent(); 566 567 fout.println("accessMode:"); 568 fout.increaseIndent(); 569 mBlobAccessMode.dump(fout); 570 fout.decreaseIndent(); 571 572 fout.println("Open fds: #" + mRevocableFds.size()); 573 } 574 } 575 writeToXml(@onNull XmlSerializer out)576 void writeToXml(@NonNull XmlSerializer out) throws IOException { 577 synchronized (mSessionLock) { 578 XmlUtils.writeLongAttribute(out, ATTR_ID, mSessionId); 579 XmlUtils.writeStringAttribute(out, ATTR_PACKAGE, mOwnerPackageName); 580 XmlUtils.writeIntAttribute(out, ATTR_UID, mOwnerUid); 581 XmlUtils.writeLongAttribute(out, ATTR_CREATION_TIME_MS, mCreationTimeMs); 582 583 out.startTag(null, TAG_BLOB_HANDLE); 584 mBlobHandle.writeToXml(out); 585 out.endTag(null, TAG_BLOB_HANDLE); 586 587 out.startTag(null, TAG_ACCESS_MODE); 588 mBlobAccessMode.writeToXml(out); 589 out.endTag(null, TAG_ACCESS_MODE); 590 } 591 } 592 593 @Nullable createFromXml(@onNull XmlPullParser in, int version, @NonNull Context context, @NonNull SessionStateChangeListener stateChangeListener)594 static BlobStoreSession createFromXml(@NonNull XmlPullParser in, int version, 595 @NonNull Context context, @NonNull SessionStateChangeListener stateChangeListener) 596 throws IOException, XmlPullParserException { 597 final long sessionId = XmlUtils.readLongAttribute(in, ATTR_ID); 598 final String ownerPackageName = XmlUtils.readStringAttribute(in, ATTR_PACKAGE); 599 final int ownerUid = XmlUtils.readIntAttribute(in, ATTR_UID); 600 final long creationTimeMs = version >= XML_VERSION_ADD_SESSION_CREATION_TIME 601 ? XmlUtils.readLongAttribute(in, ATTR_CREATION_TIME_MS) 602 : System.currentTimeMillis(); 603 604 final int depth = in.getDepth(); 605 BlobHandle blobHandle = null; 606 BlobAccessMode blobAccessMode = null; 607 while (XmlUtils.nextElementWithin(in, depth)) { 608 if (TAG_BLOB_HANDLE.equals(in.getName())) { 609 blobHandle = BlobHandle.createFromXml(in); 610 } else if (TAG_ACCESS_MODE.equals(in.getName())) { 611 blobAccessMode = BlobAccessMode.createFromXml(in); 612 } 613 } 614 615 if (blobHandle == null) { 616 Slog.wtf(TAG, "blobHandle should be available"); 617 return null; 618 } 619 if (blobAccessMode == null) { 620 Slog.wtf(TAG, "blobAccessMode should be available"); 621 return null; 622 } 623 624 final BlobStoreSession blobStoreSession = new BlobStoreSession(context, sessionId, 625 blobHandle, ownerUid, ownerPackageName, creationTimeMs, stateChangeListener); 626 blobStoreSession.mBlobAccessMode.allow(blobAccessMode); 627 return blobStoreSession; 628 } 629 } 630