Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 095fa0d5 authored by Automerger Merge Worker's avatar Automerger Merge Worker
Browse files

Merge "Add permissions data validation in AppOpsService" into rvc-dev am: 1a023125 am: 8e93580f

Change-Id: I95b590daf72da300720f39102722a1fed344d295
parents 5a474fd2 8e93580f
Loading
Loading
Loading
Loading
+32 −0
Original line number Diff line number Diff line
@@ -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
            }
        }
    }
}
+2 −0
Original line number Diff line number Diff line
@@ -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);
}
+186 −0
Original line number Diff line number Diff line
@@ -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);
            }
        }
    }
}