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

Commit 701a0e7e authored by Mitch Phillips's avatar Mitch Phillips
Browse files

Allow AppExitInfoTracker to have multiple ExitInfos per pid.

Currently the AppExitInfoTracker only allows a single
ApplicationExitInfo entry per pid. This caused some problems with
Recoverable GWP-ASan, where we'd report a native crash from Recoverable
GWP-ASan, that would end up in the AppExitInfoTracker, then the process
would die of other causes (or natural causes) for the same pid, and the
GWP-ASan data would get lost. This was fixed in aosp/2520518 by having a
separate tracking database specifically for Recoverable GWP-ASan
crashes.

Unfortunately that didn't fix the whole problem. For regular GWP-ASan,
where the process actually does crash, there's still an outstanding
issue where an app will encounter a native crash in a test, and there's
a race condition where the AppExitInfoTracker will receive the
"instrumentation finished" from the test, and overwrite the regular
GWP-ASan crash.

To support recoverable GWP-ASan, and to avoid this race with regular
GWP-ASan, I've modified the container to allow multiple AppExitInfos per
pid.

Bug: 286667521
Test: Boot the device, atest CtsGwpAsanTestCases --iterations=10,
atest ApplicationExitInfoTest.

Change-Id: Ifd73d0b5e4bde8bf87caed0aee51291cc6be8d32
parent eacb2c63
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -1036,7 +1036,7 @@ message AppsExitInfoProto {

            optional int32 uid = 1;
            repeated .android.app.ApplicationExitInfoProto app_exit_info = 2;
            repeated .android.app.ApplicationExitInfoProto app_recoverable_crash = 3;
            repeated .android.app.ApplicationExitInfoProto app_recoverable_crash = 3 [deprecated=true];
        }
        repeated User users = 2;
    }
+185 −232

File changed.

Preview size limit exceeded, changes collapsed.

+226 −0
Original line number Diff line number Diff line
@@ -84,7 +84,11 @@ import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Random;
import java.util.function.Function;
import java.util.zip.GZIPInputStream;

/**
@@ -940,6 +944,228 @@ public class ApplicationExitInfoTest {
        }
    }

    private ApplicationExitInfo createExitInfo(int i) {
        ApplicationExitInfo info = new ApplicationExitInfo();
        info.setPid(i);
        info.setTimestamp(1000 + i);
        info.setPackageUid(2000);
        return info;
    }

    @SuppressWarnings("GuardedBy")
    private ArrayList<ApplicationExitInfo> getExitInfosHelper(
            AppExitInfoTracker.AppExitInfoContainer container, int filterPid, int maxNum) {
        ArrayList<ApplicationExitInfo> infos = new ArrayList<ApplicationExitInfo>();
        container.getExitInfosLocked(filterPid, maxNum, infos);
        return infos;
    }

    @SuppressWarnings("GuardedBy")
    private void checkAreHelper(AppExitInfoTracker.AppExitInfoContainer container, int filterPid,
            int maxNum, List<Integer> expected, Function<ApplicationExitInfo, Integer> func) {
        ArrayList<Integer> values = new ArrayList<Integer>();
        getExitInfosHelper(container, filterPid, maxNum)
                .forEach((exitInfo) -> values.add(func.apply(exitInfo)));
        assertEquals(values, expected);

        HashMap<Integer, Integer> expectedMultiset = new HashMap<Integer, Integer>();
        expected.forEach(
                (elem) -> expectedMultiset.put(elem, expectedMultiset.getOrDefault(elem, 0) + 1));
        // `maxNum` isn't a parameter supported by `forEachRecordLocked()s`, but we can emulate it
        // by stopping iteration when we've seen enough elements.
        int[] numElementsToObserveWrapped = {maxNum};
        container.forEachRecordLocked((exitInfo) -> {
            // Same thing as above, `filterPid` isn't a parameter supported out of the box for
            // `forEachRecordLocked()`, but we emulate it here.
            if (filterPid > 0 && filterPid != exitInfo.getPid()) {
                return AppExitInfoTracker.FOREACH_ACTION_NONE;
            }

            Integer key = func.apply(exitInfo);
            assertTrue(expectedMultiset.toString(), expectedMultiset.containsKey(key));
            Integer references = expectedMultiset.get(key);
            if (references == 1) {
                expectedMultiset.remove(key);
            } else {
                expectedMultiset.put(key, references - 1);
            }
            if (--numElementsToObserveWrapped[0] == 0) {
                return AppExitInfoTracker.FOREACH_ACTION_STOP_ITERATION;
            }
            return AppExitInfoTracker.FOREACH_ACTION_NONE;
        });
        assertEquals(expectedMultiset.size(), 0);
    }

    private void checkPidsAre(AppExitInfoTracker.AppExitInfoContainer container, int filterPid,
            int maxNum, List<Integer> expectedPids) {
        checkAreHelper(container, filterPid, maxNum, expectedPids, (exitInfo) -> exitInfo.getPid());
    }

    private void checkPidsAre(
            AppExitInfoTracker.AppExitInfoContainer container, List<Integer> expectedPids) {
        checkPidsAre(container, 0, 0, expectedPids);
    }

    private void checkTimestampsAre(AppExitInfoTracker.AppExitInfoContainer container,
            int filterPid, int maxNum, List<Integer> expectedTimestamps) {
        checkAreHelper(container, filterPid, maxNum, expectedTimestamps,
                (exitInfo) -> (int) exitInfo.getTimestamp());
    }

    private void checkTimestampsAre(
            AppExitInfoTracker.AppExitInfoContainer container, List<Integer> expectedTimestamps) {
        checkTimestampsAre(container, 0, 0, expectedTimestamps);
    }

    @SuppressWarnings("GuardedBy")
    private AppExitInfoTracker.AppExitInfoContainer createBasicContainer() {
        AppExitInfoTracker.AppExitInfoContainer container =
                mAppExitInfoTracker.new AppExitInfoContainer(3);
        container.addExitInfoLocked(createExitInfo(10));
        container.addExitInfoLocked(createExitInfo(30));
        container.addExitInfoLocked(createExitInfo(20));
        return container;
    }

    @Test
    @SuppressWarnings("GuardedBy")
    public void testContainerGetExitInfosIsSortedNewestFirst() throws Exception {
        AppExitInfoTracker.AppExitInfoContainer container = createBasicContainer();
        checkPidsAre(container, Arrays.asList(30, 20, 10));
    }

    @Test
    @SuppressWarnings("GuardedBy")
    public void testContainerRemovesOldestReports() throws Exception {
        AppExitInfoTracker.AppExitInfoContainer container = createBasicContainer();
        container.addExitInfoLocked(createExitInfo(40));
        checkPidsAre(container, Arrays.asList(40, 30, 20));

        container.addExitInfoLocked(createExitInfo(50));
        checkPidsAre(container, Arrays.asList(50, 40, 30));

        container.addExitInfoLocked(createExitInfo(45));
        checkPidsAre(container, Arrays.asList(50, 45, 40));

        // Adding an older report shouldn't remove the newer ones.
        container.addExitInfoLocked(createExitInfo(15));
        checkPidsAre(container, Arrays.asList(50, 45, 40));
    }

    @Test
    @SuppressWarnings("GuardedBy")
    public void testContainerFilterByPid() throws Exception {
        AppExitInfoTracker.AppExitInfoContainer container = createBasicContainer();
        assertEquals(1, getExitInfosHelper(container, 30, 0).size());
        assertEquals(30, getExitInfosHelper(container, 0, 0).get(0).getPid());

        assertEquals(1, getExitInfosHelper(container, 30, 0).size());
        assertEquals(20, getExitInfosHelper(container, 20, 0).get(0).getPid());

        assertEquals(1, getExitInfosHelper(container, 10, 0).size());
        assertEquals(10, getExitInfosHelper(container, 10, 0).get(0).getPid());

        assertEquals(0, getExitInfosHelper(container, 1337, 0).size());
    }

    @Test
    @SuppressWarnings("GuardedBy")
    public void testContainerLimitQuantityOfResults() throws Exception {
        AppExitInfoTracker.AppExitInfoContainer container = createBasicContainer();
        checkPidsAre(container, /* filterPid */ 30, /* maxNum */ 1, Arrays.asList(30));
        checkPidsAre(container, /* filterPid */ 30, /* maxNum */ 1000, Arrays.asList(30));

        checkPidsAre(container, /* filterPid */ 20, /* maxNum */ 1, Arrays.asList(20));
        checkPidsAre(container, /* filterPid */ 20, /* maxNum */ 1000, Arrays.asList(20));

        checkPidsAre(container, /* filterPid */ 10, /* maxNum */ 1, Arrays.asList(10));
        checkPidsAre(container, /* filterPid */ 10, /* maxNum */ 1000, Arrays.asList(10));

        checkPidsAre(container, /* filterPid */ 1337, /* maxNum */ 1, Arrays.asList());
        checkPidsAre(container, /* filterPid */ 1337, /* maxNum */ 1000, Arrays.asList());
    }

    @Test
    @SuppressWarnings("GuardedBy")
    public void testContainerLastExitInfoForPid() throws Exception {
        AppExitInfoTracker.AppExitInfoContainer container = createBasicContainer();
        assertEquals(30, container.getLastExitInfoForPid(30).getPid());
        assertEquals(20, container.getLastExitInfoForPid(20).getPid());
        assertEquals(10, container.getLastExitInfoForPid(10).getPid());
        assertEquals(null, container.getLastExitInfoForPid(1337));
    }

    @Test
    @SuppressWarnings("GuardedBy")
    public void testContainerCanHoldMultipleFromSamePid() throws Exception {
        AppExitInfoTracker.AppExitInfoContainer container = createBasicContainer();
        ApplicationExitInfo info = createExitInfo(100);
        ApplicationExitInfo info2 = createExitInfo(100);
        ApplicationExitInfo info3 = createExitInfo(100);
        info2.setTimestamp(1337);
        info3.setTimestamp(31337);

        container.addExitInfoLocked(info);
        assertEquals(1100, container.getLastExitInfoForPid(100).getTimestamp());
        container.addExitInfoLocked(info2);
        assertEquals(1337, container.getLastExitInfoForPid(100).getTimestamp());
        container.addExitInfoLocked(info3);
        assertEquals(31337, container.getLastExitInfoForPid(100).getTimestamp());

        checkPidsAre(container, Arrays.asList(100, 100, 100));
        checkTimestampsAre(container, Arrays.asList(31337, 1337, 1100));

        checkPidsAre(container, /* filterPid */ 100, /* maxNum */ 0, Arrays.asList(100, 100, 100));
        checkTimestampsAre(
                container, /* filterPid */ 100, /* maxNum */ 0, Arrays.asList(31337, 1337, 1100));

        checkPidsAre(container, /* filterPid */ 100, /* maxNum */ 2, Arrays.asList(100, 100));
        checkTimestampsAre(
                container, /* filterPid */ 100, /* maxNum */ 2, Arrays.asList(31337, 1337));
    }

    @Test
    @SuppressWarnings("GuardedBy")
    public void testContainerIteration() throws Exception {
        AppExitInfoTracker.AppExitInfoContainer container = createBasicContainer();
        checkPidsAre(container, Arrays.asList(30, 20, 10));

        // Unfortunately relying on order for this test, which is implemented as "last inserted" ->
        // "first inserted". Note that this is insertion order, not timestamp. Thus, it's 20 -> 30
        // -> 10, as defined by `createBasicContainer()`.
        List<Integer> elements = Arrays.asList(20, 30, 10);
        for (int i = 0, size = elements.size(); i < size; i++) {
            ArrayList<Integer> processedEntries = new ArrayList<Integer>();
            final int finalIndex = i;
            container.forEachRecordLocked((exitInfo) -> {
                processedEntries.add(new Integer(exitInfo.getPid()));
                if (exitInfo.getPid() == elements.get(finalIndex)) {
                    return AppExitInfoTracker.FOREACH_ACTION_STOP_ITERATION;
                }
                return AppExitInfoTracker.FOREACH_ACTION_NONE;
            });
            assertEquals(processedEntries, elements.subList(0, i + 1));
        }
    }

    @Test
    @SuppressWarnings("GuardedBy")
    public void testContainerIterationRemove() throws Exception {
        for (int pidToRemove : Arrays.asList(30, 20, 10)) {
            AppExitInfoTracker.AppExitInfoContainer container = createBasicContainer();
            container.forEachRecordLocked((exitInfo) -> {
                if (exitInfo.getPid() == pidToRemove) {
                    return AppExitInfoTracker.FOREACH_ACTION_REMOVE_ITEM;
                }
                return AppExitInfoTracker.FOREACH_ACTION_NONE;
            });
            ArrayList<Integer> pidsRemaining = new ArrayList<Integer>(Arrays.asList(30, 20, 10));
            pidsRemaining.remove(new Integer(pidToRemove));
            checkPidsAre(container, pidsRemaining);
        }
    }

    private static int makeExitStatus(int exitCode) {
        return (exitCode << 8) & 0xff00;
    }