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 BlobStoreUtils.getRevocableFdHandler()); 228 synchronized (mSessionLock) { 229 if (mState != STATE_OPENED) { 230 IoUtils.closeQuietly(fd); 231 throw new IllegalStateException("Not allowed to write in state: " 232 + stateToString(mState)); 233 } 234 trackRevocableFdLocked(revocableFd); 235 return revocableFd.getRevocableFileDescriptor(); 236 } 237 } catch (IOException e) { 238 IoUtils.closeQuietly(fd); 239 throw ExceptionUtils.wrap(e); 240 } 241 } 242 243 @NonNull openWriteInternal(@ytesLong long offsetBytes, @BytesLong long lengthBytes)244 private FileDescriptor openWriteInternal(@BytesLong long offsetBytes, 245 @BytesLong long lengthBytes) throws IOException { 246 // TODO: Add limit on active open sessions/writes/reads 247 try { 248 final File sessionFile = getSessionFile(); 249 if (sessionFile == null) { 250 throw new IllegalStateException("Couldn't get the file for this session"); 251 } 252 final FileDescriptor fd = Os.open(sessionFile.getPath(), O_CREAT | O_RDWR, 0600); 253 if (offsetBytes > 0) { 254 final long curOffset = Os.lseek(fd, offsetBytes, SEEK_SET); 255 if (curOffset != offsetBytes) { 256 throw new IllegalStateException("Failed to seek " + offsetBytes 257 + "; curOffset=" + offsetBytes); 258 } 259 } 260 if (lengthBytes > 0) { 261 mContext.getSystemService(StorageManager.class).allocateBytes(fd, lengthBytes); 262 } 263 return fd; 264 } catch (ErrnoException e) { 265 throw e.rethrowAsIOException(); 266 } 267 } 268 269 @Override 270 @NonNull openRead()271 public ParcelFileDescriptor openRead() { 272 assertCallerIsOwner(); 273 synchronized (mSessionLock) { 274 if (mState != STATE_OPENED) { 275 throw new IllegalStateException("Not allowed to read in state: " 276 + stateToString(mState)); 277 } 278 if (!BlobStoreConfig.shouldUseRevocableFdForReads()) { 279 try { 280 return new ParcelFileDescriptor(openReadInternal()); 281 } catch (IOException e) { 282 throw ExceptionUtils.wrap(e); 283 } 284 } 285 } 286 287 FileDescriptor fd = null; 288 try { 289 fd = openReadInternal(); 290 final RevocableFileDescriptor revocableFd = new RevocableFileDescriptor(mContext, fd); 291 synchronized (mSessionLock) { 292 if (mState != STATE_OPENED) { 293 IoUtils.closeQuietly(fd); 294 throw new IllegalStateException("Not allowed to read in state: " 295 + stateToString(mState)); 296 } 297 trackRevocableFdLocked(revocableFd); 298 return revocableFd.getRevocableFileDescriptor(); 299 } 300 } catch (IOException e) { 301 IoUtils.closeQuietly(fd); 302 throw ExceptionUtils.wrap(e); 303 } 304 } 305 306 @NonNull openReadInternal()307 private FileDescriptor openReadInternal() throws IOException { 308 try { 309 final File sessionFile = getSessionFile(); 310 if (sessionFile == null) { 311 throw new IllegalStateException("Couldn't get the file for this session"); 312 } 313 final FileDescriptor fd = Os.open(sessionFile.getPath(), O_RDONLY, 0); 314 return fd; 315 } catch (ErrnoException e) { 316 throw e.rethrowAsIOException(); 317 } 318 } 319 320 @Override 321 @BytesLong getSize()322 public long getSize() { 323 return getSessionFile().length(); 324 } 325 326 @Override allowPackageAccess(@onNull String packageName, @NonNull byte[] certificate)327 public void allowPackageAccess(@NonNull String packageName, 328 @NonNull byte[] certificate) { 329 assertCallerIsOwner(); 330 Objects.requireNonNull(packageName, "packageName must not be null"); 331 synchronized (mSessionLock) { 332 if (mState != STATE_OPENED) { 333 throw new IllegalStateException("Not allowed to change access type in state: " 334 + stateToString(mState)); 335 } 336 if (mBlobAccessMode.getAllowedPackagesCount() >= getMaxPermittedPackages()) { 337 throw new ParcelableException(new LimitExceededException( 338 "Too many packages permitted to access the blob: " 339 + mBlobAccessMode.getAllowedPackagesCount())); 340 } 341 mBlobAccessMode.allowPackageAccess(packageName, certificate); 342 } 343 } 344 345 @Override allowSameSignatureAccess()346 public void allowSameSignatureAccess() { 347 assertCallerIsOwner(); 348 synchronized (mSessionLock) { 349 if (mState != STATE_OPENED) { 350 throw new IllegalStateException("Not allowed to change access type in state: " 351 + stateToString(mState)); 352 } 353 mBlobAccessMode.allowSameSignatureAccess(); 354 } 355 } 356 357 @Override allowPublicAccess()358 public void allowPublicAccess() { 359 assertCallerIsOwner(); 360 synchronized (mSessionLock) { 361 if (mState != STATE_OPENED) { 362 throw new IllegalStateException("Not allowed to change access type in state: " 363 + stateToString(mState)); 364 } 365 mBlobAccessMode.allowPublicAccess(); 366 } 367 } 368 369 @Override isPackageAccessAllowed(@onNull String packageName, @NonNull byte[] certificate)370 public boolean isPackageAccessAllowed(@NonNull String packageName, 371 @NonNull byte[] certificate) { 372 assertCallerIsOwner(); 373 Objects.requireNonNull(packageName, "packageName must not be null"); 374 Preconditions.checkByteArrayNotEmpty(certificate, "certificate"); 375 376 synchronized (mSessionLock) { 377 if (mState != STATE_OPENED) { 378 throw new IllegalStateException("Not allowed to get access type in state: " 379 + stateToString(mState)); 380 } 381 return mBlobAccessMode.isPackageAccessAllowed(packageName, certificate); 382 } 383 } 384 385 @Override isSameSignatureAccessAllowed()386 public boolean isSameSignatureAccessAllowed() { 387 assertCallerIsOwner(); 388 synchronized (mSessionLock) { 389 if (mState != STATE_OPENED) { 390 throw new IllegalStateException("Not allowed to get access type in state: " 391 + stateToString(mState)); 392 } 393 return mBlobAccessMode.isSameSignatureAccessAllowed(); 394 } 395 } 396 397 @Override isPublicAccessAllowed()398 public boolean isPublicAccessAllowed() { 399 assertCallerIsOwner(); 400 synchronized (mSessionLock) { 401 if (mState != STATE_OPENED) { 402 throw new IllegalStateException("Not allowed to get access type in state: " 403 + stateToString(mState)); 404 } 405 return mBlobAccessMode.isPublicAccessAllowed(); 406 } 407 } 408 409 @Override close()410 public void close() { 411 closeSession(STATE_CLOSED, false /* sendCallback */); 412 } 413 414 @Override abandon()415 public void abandon() { 416 closeSession(STATE_ABANDONED, true /* sendCallback */); 417 } 418 419 @Override commit(IBlobCommitCallback callback)420 public void commit(IBlobCommitCallback callback) { 421 synchronized (mSessionLock) { 422 mBlobCommitCallback = callback; 423 424 closeSession(STATE_COMMITTED, true /* sendCallback */); 425 } 426 } 427 closeSession(int state, boolean sendCallback)428 private void closeSession(int state, boolean sendCallback) { 429 assertCallerIsOwner(); 430 synchronized (mSessionLock) { 431 if (mState != STATE_OPENED) { 432 if (state == STATE_CLOSED) { 433 // Just trying to close the session which is already deleted or abandoned, 434 // ignore. 435 return; 436 } else { 437 throw new IllegalStateException("Not allowed to delete or abandon a session" 438 + " with state: " + stateToString(mState)); 439 } 440 } 441 442 mState = state; 443 revokeAllFds(); 444 445 if (sendCallback) { 446 mListener.onStateChanged(this); 447 } 448 } 449 } 450 computeDigest()451 void computeDigest() { 452 try { 453 Trace.traceBegin(TRACE_TAG_SYSTEM_SERVER, 454 "computeBlobDigest-i" + mSessionId + "-l" + getSessionFile().length()); 455 mDataDigest = FileUtils.digest(getSessionFile(), mBlobHandle.algorithm); 456 } catch (IOException | NoSuchAlgorithmException e) { 457 Slog.e(TAG, "Error computing the digest", e); 458 } finally { 459 Trace.traceEnd(TRACE_TAG_SYSTEM_SERVER); 460 } 461 } 462 verifyBlobData()463 void verifyBlobData() { 464 synchronized (mSessionLock) { 465 if (mDataDigest != null && Arrays.equals(mDataDigest, mBlobHandle.digest)) { 466 mState = STATE_VERIFIED_VALID; 467 // Commit callback will be sent once the data is persisted. 468 } else { 469 Slog.d(TAG, "Digest of the data (" 470 + (mDataDigest == null ? "null" : BlobHandle.safeDigest(mDataDigest)) 471 + ") didn't match the given BlobHandle.digest (" 472 + BlobHandle.safeDigest(mBlobHandle.digest) + ")"); 473 mState = STATE_VERIFIED_INVALID; 474 475 FrameworkStatsLog.write(FrameworkStatsLog.BLOB_COMMITTED, getOwnerUid(), mSessionId, 476 getSize(), FrameworkStatsLog.BLOB_COMMITTED__RESULT__DIGEST_MISMATCH); 477 sendCommitCallbackResult(COMMIT_RESULT_ERROR); 478 } 479 mListener.onStateChanged(this); 480 } 481 } 482 destroy()483 void destroy() { 484 revokeAllFds(); 485 getSessionFile().delete(); 486 } 487 revokeAllFds()488 private void revokeAllFds() { 489 synchronized (mRevocableFds) { 490 for (int i = mRevocableFds.size() - 1; i >= 0; --i) { 491 mRevocableFds.get(i).revoke(); 492 } 493 mRevocableFds.clear(); 494 } 495 } 496 497 @GuardedBy("mSessionLock") trackRevocableFdLocked(RevocableFileDescriptor revocableFd)498 private void trackRevocableFdLocked(RevocableFileDescriptor revocableFd) { 499 synchronized (mRevocableFds) { 500 mRevocableFds.add(revocableFd); 501 } 502 revocableFd.addOnCloseListener((e) -> { 503 synchronized (mRevocableFds) { 504 mRevocableFds.remove(revocableFd); 505 } 506 }); 507 } 508 509 @Nullable getSessionFile()510 File getSessionFile() { 511 if (mSessionFile == null) { 512 mSessionFile = BlobStoreConfig.prepareBlobFile(mSessionId); 513 } 514 return mSessionFile; 515 } 516 517 @NonNull stateToString(int state)518 static String stateToString(int state) { 519 switch (state) { 520 case STATE_OPENED: 521 return "<opened>"; 522 case STATE_CLOSED: 523 return "<closed>"; 524 case STATE_ABANDONED: 525 return "<abandoned>"; 526 case STATE_COMMITTED: 527 return "<committed>"; 528 case STATE_VERIFIED_VALID: 529 return "<verified_valid>"; 530 case STATE_VERIFIED_INVALID: 531 return "<verified_invalid>"; 532 default: 533 Slog.wtf(TAG, "Unknown state: " + state); 534 return "<unknown>"; 535 } 536 } 537 538 @Override toString()539 public String toString() { 540 return "BlobStoreSession {" 541 + "id:" + mSessionId 542 + ",handle:" + mBlobHandle 543 + ",uid:" + mOwnerUid 544 + ",pkg:" + mOwnerPackageName 545 + "}"; 546 } 547 assertCallerIsOwner()548 private void assertCallerIsOwner() { 549 final int callingUid = Binder.getCallingUid(); 550 if (callingUid != mOwnerUid) { 551 throw new SecurityException(mOwnerUid + " is not the session owner"); 552 } 553 } 554 dump(IndentingPrintWriter fout, DumpArgs dumpArgs)555 void dump(IndentingPrintWriter fout, DumpArgs dumpArgs) { 556 synchronized (mSessionLock) { 557 fout.println("state: " + stateToString(mState)); 558 fout.println("ownerUid: " + mOwnerUid); 559 fout.println("ownerPkg: " + mOwnerPackageName); 560 fout.println("creation time: " + BlobStoreUtils.formatTime(mCreationTimeMs)); 561 fout.println("size: " + formatFileSize(mContext, getSize(), FLAG_IEC_UNITS)); 562 563 fout.println("blobHandle:"); 564 fout.increaseIndent(); 565 mBlobHandle.dump(fout, dumpArgs.shouldDumpFull()); 566 fout.decreaseIndent(); 567 568 fout.println("accessMode:"); 569 fout.increaseIndent(); 570 mBlobAccessMode.dump(fout); 571 fout.decreaseIndent(); 572 573 fout.println("Open fds: #" + mRevocableFds.size()); 574 } 575 } 576 writeToXml(@onNull XmlSerializer out)577 void writeToXml(@NonNull XmlSerializer out) throws IOException { 578 synchronized (mSessionLock) { 579 XmlUtils.writeLongAttribute(out, ATTR_ID, mSessionId); 580 XmlUtils.writeStringAttribute(out, ATTR_PACKAGE, mOwnerPackageName); 581 XmlUtils.writeIntAttribute(out, ATTR_UID, mOwnerUid); 582 XmlUtils.writeLongAttribute(out, ATTR_CREATION_TIME_MS, mCreationTimeMs); 583 584 out.startTag(null, TAG_BLOB_HANDLE); 585 mBlobHandle.writeToXml(out); 586 out.endTag(null, TAG_BLOB_HANDLE); 587 588 out.startTag(null, TAG_ACCESS_MODE); 589 mBlobAccessMode.writeToXml(out); 590 out.endTag(null, TAG_ACCESS_MODE); 591 } 592 } 593 594 @Nullable createFromXml(@onNull XmlPullParser in, int version, @NonNull Context context, @NonNull SessionStateChangeListener stateChangeListener)595 static BlobStoreSession createFromXml(@NonNull XmlPullParser in, int version, 596 @NonNull Context context, @NonNull SessionStateChangeListener stateChangeListener) 597 throws IOException, XmlPullParserException { 598 final long sessionId = XmlUtils.readLongAttribute(in, ATTR_ID); 599 final String ownerPackageName = XmlUtils.readStringAttribute(in, ATTR_PACKAGE); 600 final int ownerUid = XmlUtils.readIntAttribute(in, ATTR_UID); 601 final long creationTimeMs = version >= XML_VERSION_ADD_SESSION_CREATION_TIME 602 ? XmlUtils.readLongAttribute(in, ATTR_CREATION_TIME_MS) 603 : System.currentTimeMillis(); 604 605 final int depth = in.getDepth(); 606 BlobHandle blobHandle = null; 607 BlobAccessMode blobAccessMode = null; 608 while (XmlUtils.nextElementWithin(in, depth)) { 609 if (TAG_BLOB_HANDLE.equals(in.getName())) { 610 blobHandle = BlobHandle.createFromXml(in); 611 } else if (TAG_ACCESS_MODE.equals(in.getName())) { 612 blobAccessMode = BlobAccessMode.createFromXml(in); 613 } 614 } 615 616 if (blobHandle == null) { 617 Slog.wtf(TAG, "blobHandle should be available"); 618 return null; 619 } 620 if (blobAccessMode == null) { 621 Slog.wtf(TAG, "blobAccessMode should be available"); 622 return null; 623 } 624 625 final BlobStoreSession blobStoreSession = new BlobStoreSession(context, sessionId, 626 blobHandle, ownerUid, ownerPackageName, creationTimeMs, stateChangeListener); 627 blobStoreSession.mBlobAccessMode.allow(blobAccessMode); 628 return blobStoreSession; 629 } 630 } 631