Merge "Add permissions data validation in AppOpsService" into rvc-dev am: 1a023125c9
Change-Id: Ic5ac56dc9ab7a2817e72053a142c8384a12aa0bd
This commit is contained in:
@@ -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
|
||||
*
|
||||
* <p> 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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<NoteOpTrace> 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<Pair<String, Integer>, ArrayList<AsyncNotedAppOp>>
|
||||
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<AppOpsManager.OpEntry> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user