From 2ead966e7b5ef649e77e068072a9cbaf1da0333c Mon Sep 17 00:00:00 2001 From: David Cheung Date: Wed, 19 Feb 2020 16:11:06 -0800 Subject: [PATCH] Add permissions data validation in AppOpsService Added functionality to collect noteOp noteProxyOp startOp operations for permissions data validation, this functionality is for developers and can be enabled by modifying the flag. This data will be utilized to ensure permissions are requested only when necessary. Bug: 150890258 Test: Manually tested on crosshatch to ensure files are written/formatted properly with the necessary data and does not interfere with normal behavior Design Document: https://docs.google.com/document/d/1RRs3cPgCzF5S1TkTD11MBKJedUp2DAUEGtCQXtrk0XQ/edit?usp=sharing Change-Id: Ia7fba6ec5e47b7ddd13ca964ae5f6c1afa1cc186 --- core/java/android/app/AppOpsManager.java | 32 +++ .../android/internal/app/IAppOpsService.aidl | 2 + .../android/server/appop/AppOpsService.java | 186 ++++++++++++++++++ 3 files changed, 220 insertions(+) diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java index 2399e374540de..f613df2ac595f 100644 --- a/core/java/android/app/AppOpsManager.java +++ b/core/java/android/app/AppOpsManager.java @@ -385,6 +385,15 @@ public class AppOpsManager { */ public static final int WATCH_FOREGROUND_CHANGES = 1 << 0; + + /** + * Flag to determine whether we should log noteOp/startOp calls to make sure they + * are correctly used + * + * @hide + */ + public static final boolean NOTE_OP_COLLECTION_ENABLED = false; + /** * @hide */ @@ -7103,6 +7112,7 @@ public class AppOpsManager { public int noteOpNoThrow(int op, int uid, @Nullable String packageName, @Nullable String featureId, @Nullable String message) { try { + collectNoteOpCallsForValidation(op); int collectionMode = getNotedOpCollectionMode(uid, packageName, op); if (collectionMode == COLLECT_ASYNC) { if (message == null) { @@ -7263,6 +7273,7 @@ public class AppOpsManager { int myUid = Process.myUid(); try { + collectNoteOpCallsForValidation(op); int collectionMode = getNotedOpCollectionMode(proxiedUid, proxiedPackageName, op); if (collectionMode == COLLECT_ASYNC) { if (message == null) { @@ -7583,6 +7594,7 @@ public class AppOpsManager { public int startOpNoThrow(int op, int uid, @NonNull String packageName, boolean startIfModeDefault, @Nullable String featureId, @Nullable String message) { try { + collectNoteOpCallsForValidation(op); int collectionMode = getNotedOpCollectionMode(uid, packageName, op); if (collectionMode == COLLECT_ASYNC) { if (message == null) { @@ -8492,4 +8504,24 @@ public class AppOpsManager { public static int leftCircularDistance(int from, int to, int size) { return (to + size - from) % size; } + + /** + * Helper method for noteOp, startOp and noteProxyOp to call AppOpsService to collect/log + * stack traces + * + *

For each call, the stacktrace op code, package name and long version code will be + * passed along where it will be logged/collected + * + * @param op The operation to note + */ + private void collectNoteOpCallsForValidation(int op) { + if (NOTE_OP_COLLECTION_ENABLED) { + try { + mService.collectNoteOpCallsForValidation(getFormattedStackTrace(), + op, mContext.getOpPackageName(), mContext.getApplicationInfo().longVersionCode); + } catch (RemoteException e) { + // Swallow error, only meant for logging ops, should not affect flow of the code + } + } + } } diff --git a/core/java/com/android/internal/app/IAppOpsService.aidl b/core/java/com/android/internal/app/IAppOpsService.aidl index 1c1c25459b663..907ea55d52a00 100644 --- a/core/java/com/android/internal/app/IAppOpsService.aidl +++ b/core/java/com/android/internal/app/IAppOpsService.aidl @@ -103,4 +103,6 @@ interface IAppOpsService { int checkOperationRaw(int code, int uid, String packageName); void reloadNonHistoricalState(); + + void collectNoteOpCallsForValidation(String stackTrace, int op, String packageName, long version); } diff --git a/services/core/java/com/android/server/appop/AppOpsService.java b/services/core/java/com/android/server/appop/AppOpsService.java index 45c3aebe8bd5e..7774633fa1bea 100644 --- a/services/core/java/com/android/server/appop/AppOpsService.java +++ b/services/core/java/com/android/server/appop/AppOpsService.java @@ -138,6 +138,7 @@ import android.util.TimeUtils; import android.util.Xml; import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.Immutable; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.app.IAppOpsActiveCallback; import com.android.internal.app.IAppOpsAsyncNotedCallback; @@ -155,11 +156,14 @@ import com.android.internal.util.function.pooled.PooledLambda; import com.android.server.LocalServices; import com.android.server.LockGuard; import com.android.server.SystemServerInitThreadPool; +import com.android.server.SystemServiceManager; import com.android.server.pm.PackageList; import com.android.server.pm.parsing.pkg.AndroidPackage; import libcore.util.EmptyArray; +import org.json.JSONException; +import org.json.JSONObject; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlSerializer; @@ -169,6 +173,7 @@ import java.io.FileDescriptor; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; +import java.io.FileWriter; import java.io.IOException; import java.io.PrintWriter; import java.nio.charset.StandardCharsets; @@ -184,6 +189,7 @@ import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Scanner; import java.util.concurrent.ThreadLocalRandom; import java.util.function.Consumer; @@ -191,6 +197,11 @@ public class AppOpsService extends IAppOpsService.Stub { static final String TAG = "AppOps"; static final boolean DEBUG = false; + /** + * Used for data access validation collection, we wish to only log a specific access once + */ + private final ArraySet mNoteOpCallerStacktraces = new ArraySet<>(); + private static final int NO_VERSION = -1; /** Increment by one every time and add the corresponding upgrade logic in * {@link #upgradeLocked(int)} below. The first version was 1 */ @@ -241,6 +252,7 @@ public class AppOpsService extends IAppOpsService.Stub { final Context mContext; final AtomicFile mFile; + private final @Nullable File mNoteOpCallerStacktracesFile; final Handler mHandler; /** Pool for {@link OpEventProxyInfoPool} to avoid to constantly reallocate new objects */ @@ -278,6 +290,8 @@ public class AppOpsService extends IAppOpsService.Stub { private final ArrayMap, ArrayList> mUnforwardedAsyncNotedOps = new ArrayMap<>(); + boolean mWriteNoteOpsScheduled; + boolean mWriteScheduled; boolean mFastWriteScheduled; final Runnable mWriteRunner = new Runnable() { @@ -1397,11 +1411,42 @@ public class AppOpsService extends IAppOpsService.Stub { featureOp.onClientDeath(clientId); } + + /** + * Loads the OpsValidation file results into a hashmap {@link #mNoteOpCallerStacktraces} + * so that we do not log the same operation twice between instances + */ + private void readNoteOpCallerStackTraces() { + try { + if (!mNoteOpCallerStacktracesFile.exists()) { + mNoteOpCallerStacktracesFile.createNewFile(); + return; + } + + try (Scanner read = new Scanner(mNoteOpCallerStacktracesFile)) { + read.useDelimiter("\\},"); + while (read.hasNext()) { + String jsonOps = read.next(); + mNoteOpCallerStacktraces.add(NoteOpTrace.fromJson(jsonOps)); + } + } + } catch (Exception e) { + Slog.e(TAG, "Cannot parse traces noteOps", e); + } + } + public AppOpsService(File storagePath, Handler handler, Context context) { mContext = context; LockGuard.installLock(this, LockGuard.INDEX_APP_OPS); mFile = new AtomicFile(storagePath, "appops"); + if (AppOpsManager.NOTE_OP_COLLECTION_ENABLED) { + mNoteOpCallerStacktracesFile = new File(SystemServiceManager.ensureSystemDir(), + "noteOpStackTraces.json"); + readNoteOpCallerStackTraces(); + } else { + mNoteOpCallerStacktracesFile = null; + } mHandler = handler; mConstants = new Constants(mHandler); readState(); @@ -1802,6 +1847,9 @@ public class AppOpsService extends IAppOpsService.Stub { if (doWrite) { writeState(); } + if (AppOpsManager.NOTE_OP_COLLECTION_ENABLED && mWriteNoteOpsScheduled) { + writeNoteOps(); + } } private ArrayList collectOps(Ops pkgOps, int[] ops) { @@ -6051,4 +6099,142 @@ public class AppOpsService extends IAppOpsService.Stub { setMode(code, uid, packageName, mode, callback); } } + + + /** + * Async task for writing note op stack trace, op code, package name and version to file + * More specifically, writes all the collected ops from {@link #mNoteOpCallerStacktraces} + */ + private void writeNoteOps() { + synchronized (this) { + mWriteNoteOpsScheduled = false; + } + synchronized (mNoteOpCallerStacktracesFile) { + try (FileWriter writer = new FileWriter(mNoteOpCallerStacktracesFile)) { + int numTraces = mNoteOpCallerStacktraces.size(); + for (int i = 0; i < numTraces; i++) { + // Writing json formatted string into file + writer.write(mNoteOpCallerStacktraces.valueAt(i).asJson()); + // Comma separation, so we can wrap the entire log as a JSON object + // when all results are collected + writer.write(","); + } + } catch (IOException e) { + Slog.w(TAG, "Failed to load opsValidation file for FileWriter", e); + } + } + } + + /** + * This class represents a NoteOp Trace object amd contains the necessary fields that will + * be written to file to use for permissions data validation in JSON format + */ + @Immutable + static class NoteOpTrace { + static final String STACKTRACE = "stackTrace"; + static final String OP = "op"; + static final String PACKAGENAME = "packageName"; + static final String VERSION = "version"; + + private final @NonNull String mStackTrace; + private final int mOp; + private final @Nullable String mPackageName; + private final long mVersion; + + /** + * Initialize a NoteOp object using a JSON object containing the necessary fields + * + * @param jsonTrace JSON object represented as a string + * + * @return NoteOpTrace object initialized with JSON fields + */ + static NoteOpTrace fromJson(String jsonTrace) { + try { + // Re-add closing bracket which acted as a delimiter by the reader + JSONObject obj = new JSONObject(jsonTrace.concat("}")); + return new NoteOpTrace(obj.getString(STACKTRACE), obj.getInt(OP), + obj.getString(PACKAGENAME), obj.getLong(VERSION)); + } catch (JSONException e) { + // Swallow error, only meant for logging ops, should not affect flow of the code + Slog.e(TAG, "Error constructing NoteOpTrace object " + + "JSON trace format incorrect", e); + return null; + } + } + + NoteOpTrace(String stackTrace, int op, String packageName, long version) { + mStackTrace = stackTrace; + mOp = op; + mPackageName = packageName; + mVersion = version; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + NoteOpTrace that = (NoteOpTrace) o; + return mOp == that.mOp + && mVersion == that.mVersion + && mStackTrace.equals(that.mStackTrace) + && Objects.equals(mPackageName, that.mPackageName); + } + + @Override + public int hashCode() { + return Objects.hash(mStackTrace, mOp, mPackageName, mVersion); + } + + /** + * The object is formatted as a JSON object and returned as a String + * + * @return JSON formatted string + */ + public String asJson() { + return "{" + + "\"" + STACKTRACE + "\":\"" + mStackTrace.replace("\n", "\\n") + + '\"' + ",\"" + OP + "\":" + mOp + + ",\"" + PACKAGENAME + "\":\"" + mPackageName + '\"' + + ",\"" + VERSION + "\":" + mVersion + + '}'; + } + } + + /** + * Collects noteOps, noteProxyOps and startOps from AppOpsManager and writes it into a file + * which will be used for permissions data validation, the given parameters to this method + * will be logged in json format + * + * @param stackTrace stacktrace from the most recent call in AppOpsManager + * @param op op code + * @param packageName package making call + * @param version android version for this call + */ + @Override + public void collectNoteOpCallsForValidation(String stackTrace, int op, String packageName, + long version) { + if (!AppOpsManager.NOTE_OP_COLLECTION_ENABLED) { + return; + } + + Objects.requireNonNull(stackTrace); + Preconditions.checkArgument(op >= 0); + Preconditions.checkArgument(op < AppOpsManager._NUM_OP); + Objects.requireNonNull(version); + + NoteOpTrace noteOpTrace = new NoteOpTrace(stackTrace, op, packageName, version); + + boolean noteOpSetWasChanged; + synchronized (this) { + noteOpSetWasChanged = mNoteOpCallerStacktraces.add(noteOpTrace); + if (noteOpSetWasChanged && !mWriteNoteOpsScheduled) { + mWriteNoteOpsScheduled = true; + mHandler.postDelayed(PooledLambda.obtainRunnable((that) -> { + AsyncTask.execute(() -> { + that.writeNoteOps(); + }); + }, this), 2500); + } + } + } }