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

Commit d1122015 authored by Robert Horvath's avatar Robert Horvath
Browse files

Batch log access requests from same client

For multiple log access requests from the same client
(same UID + package name) within a short timeframe, show only one
confirmation prompt to the user.

When access has been approved/denied, further requests will
automatically be approved/denied until another timeout expires, after
which a new request will show a prompt again.

If the prompt is shown but the request isn't approved or denied within a
certain time, the client will automatically be denied access.

Moved the approve/decline methods out of ILogcatManagerService into a
local service, so that they can only be called from within the system
server.

Bug: 229976778
Test: atest FrameworksServicesTests:LogcatManagerServiceTest
Change-Id: I6a3f56bdcbb84e64b1b24e73476bd24f32b75f24
parent 2b6a2340
Loading
Loading
Loading
Loading
+0 −27
Original line number Diff line number Diff line
@@ -42,31 +42,4 @@ oneway interface ILogcatManagerService {
     * @param fd  The FD (Socket) of client who makes the request.
     */
    void finishThread(in int uid, in int gid, in int pid, in int fd);


    /**
     * The function is called by UX component to notify
     * LogcatManagerService that the user approved
     * the privileged log data access.
     *
     * @param uid The UID of client who makes the request.
     * @param gid The GID of client who makes the request.
     * @param pid The PID of client who makes the request.
     * @param fd  The FD (Socket) of client who makes the request.
     */
    void approve(in int uid, in int gid, in int pid, in int fd);


    /**
     * The function is called by UX component to notify
     * LogcatManagerService that the user declined
     * the privileged log data access.
     *
     * @param uid The UID of client who makes the request.
     * @param gid The GID of client who makes the request.
     * @param pid The PID of client who makes the request.
     * @param fd  The FD (Socket) of client who makes the request.
     */
    void decline(in int uid, in int gid, in int pid, in int fd);
}
+7 −43
Original line number Diff line number Diff line
@@ -16,11 +16,6 @@

package com.android.server.logcat;

import static com.android.server.logcat.LogcatManagerService.EXTRA_FD;
import static com.android.server.logcat.LogcatManagerService.EXTRA_GID;
import static com.android.server.logcat.LogcatManagerService.EXTRA_PID;
import static com.android.server.logcat.LogcatManagerService.EXTRA_UID;

import android.annotation.StyleRes;
import android.app.Activity;
import android.app.AlertDialog;
@@ -32,10 +27,7 @@ import android.content.res.Configuration;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.UserHandle;
import android.os.logcat.ILogcatManagerService;
import android.util.Slog;
import android.view.ContextThemeWrapper;
import android.view.InflateException;
@@ -45,6 +37,7 @@ import android.widget.Button;
import android.widget.TextView;

import com.android.internal.R;
import com.android.server.LocalServices;

/**
 * Dialog responsible for obtaining user consent per-use log access
@@ -56,14 +49,11 @@ public class LogAccessDialogActivity extends Activity implements
    private static final int DIALOG_TIME_OUT = Build.IS_DEBUGGABLE ? 60000 : 300000;
    private static final int MSG_DISMISS_DIALOG = 0;

    private final ILogcatManagerService mLogcatManagerService =
            ILogcatManagerService.Stub.asInterface(ServiceManager.getService("logcat"));
    private final LogcatManagerService.LogcatManagerServiceInternal mLogcatManagerInternal =
            LocalServices.getService(LogcatManagerService.LogcatManagerServiceInternal.class);

    private String mPackageName;
    private int mUid;
    private int mGid;
    private int mPid;
    private int mFd;

    private String mAlertTitle;
    private AlertDialog.Builder mAlertDialog;
@@ -133,30 +123,12 @@ public class LogAccessDialogActivity extends Activity implements
            return false;
        }

        if (!intent.hasExtra(EXTRA_UID)) {
        if (!intent.hasExtra(Intent.EXTRA_UID)) {
            Slog.e(TAG, "Missing EXTRA_UID");
            return false;
        }

        if (!intent.hasExtra(EXTRA_GID)) {
            Slog.e(TAG, "Missing EXTRA_GID");
            return false;
        }

        if (!intent.hasExtra(EXTRA_PID)) {
            Slog.e(TAG, "Missing EXTRA_PID");
            return false;
        }

        if (!intent.hasExtra(EXTRA_FD)) {
            Slog.e(TAG, "Missing EXTRA_FD");
            return false;
        }

        mUid = intent.getIntExtra(EXTRA_UID, 0);
        mGid = intent.getIntExtra(EXTRA_GID, 0);
        mPid = intent.getIntExtra(EXTRA_PID, 0);
        mFd = intent.getIntExtra(EXTRA_FD, 0);
        mUid = intent.getIntExtra(Intent.EXTRA_UID, 0);

        return true;
    }
@@ -223,11 +195,7 @@ public class LogAccessDialogActivity extends Activity implements
    public void onClick(View view) {
        switch (view.getId()) {
            case R.id.log_access_dialog_allow_button:
                try {
                    mLogcatManagerService.approve(mUid, mGid, mPid, mFd);
                } catch (RemoteException e) {
                    Slog.e(TAG, "Fails to call remote functions", e);
                }
                mLogcatManagerInternal.approveAccessForClient(mUid, mPackageName);
                finish();
                break;
            case R.id.log_access_dialog_deny_button:
@@ -238,10 +206,6 @@ public class LogAccessDialogActivity extends Activity implements
    }

    private void declineLogAccess() {
        try {
            mLogcatManagerService.decline(mUid, mGid, mPid, mFd);
        } catch (RemoteException e) {
            Slog.e(TAG, "Fails to call remote functions", e);
        }
        mLogcatManagerInternal.declineAccessForClient(mUid, mPackageName);
    }
}
+398 −116

File changed.

Preview size limit exceeded, changes collapsed.

+322 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2022 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.server.logcat;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import android.app.ActivityManager;
import android.app.ActivityManagerInternal;
import android.content.ContextWrapper;
import android.os.ILogd;
import android.os.Looper;
import android.os.UserHandle;
import android.os.test.TestLooper;

import androidx.test.core.app.ApplicationProvider;

import com.android.server.LocalServices;
import com.android.server.logcat.LogcatManagerService.Injector;
import com.android.server.testutils.OffsettableClock;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import java.util.function.Supplier;

/**
 * Tests for {@link com.android.server.logcat.LogcatManagerService}.
 *
 * Build/Install/Run:
 * atest FrameworksServicesTests:LogcatManagerServiceTest
 */
@SuppressWarnings("GuardedBy")
public class LogcatManagerServiceTest {
    private static final String APP1_PACKAGE_NAME = "app1";
    private static final int APP1_UID = 10001;
    private static final int APP1_GID = 10001;
    private static final int APP1_PID = 10001;
    private static final String APP2_PACKAGE_NAME = "app2";
    private static final int APP2_UID = 10002;
    private static final int APP2_GID = 10002;
    private static final int APP2_PID = 10002;
    private static final int FD1 = 10;
    private static final int FD2 = 11;

    @Mock
    private ActivityManagerInternal mActivityManagerInternalMock;
    @Mock
    private ILogd mLogdMock;

    private LogcatManagerService mService;
    private LogcatManagerService.LogcatManagerServiceInternal mLocalService;
    private ContextWrapper mContextSpy;
    private OffsettableClock mClock;
    private TestLooper mTestLooper;

    @Before
    public void setUp() throws Exception {
        MockitoAnnotations.initMocks(this);
        addLocalServiceMock(ActivityManagerInternal.class, mActivityManagerInternalMock);
        mContextSpy = spy(new ContextWrapper(ApplicationProvider.getApplicationContext()));
        mClock = new OffsettableClock.Stopped();
        mTestLooper = new TestLooper(mClock::now);

        when(mActivityManagerInternalMock.getPackageNameByPid(APP1_PID)).thenReturn(
                APP1_PACKAGE_NAME);
        when(mActivityManagerInternalMock.getPackageNameByPid(APP2_PID)).thenReturn(
                APP2_PACKAGE_NAME);

        mService = new LogcatManagerService(mContextSpy, new Injector() {
            @Override
            protected Supplier<Long> createClock() {
                return mClock::now;
            }

            @Override
            protected Looper getLooper() {
                return mTestLooper.getLooper();
            }

            @Override
            protected ILogd getLogdService() {
                return mLogdMock;
            }
        });
        mLocalService = mService.getLocalService();
        mService.onStart();
    }

    @After
    public void tearDown() throws Exception {
        LocalServices.removeServiceForTest(ActivityManagerInternal.class);
    }

    /**
     * Creates a mock and registers it to {@link LocalServices}.
     */
    private static <T> void addLocalServiceMock(Class<T> clazz, T mock) {
        LocalServices.removeServiceForTest(clazz);
        LocalServices.addService(clazz, mock);
    }

    @Test
    public void test_RequestFromBackground_DeclinedWithoutPrompt() throws Exception {
        when(mActivityManagerInternalMock.getUidProcessState(APP1_UID)).thenReturn(
                ActivityManager.PROCESS_STATE_RECEIVER);
        mService.getBinderService().startThread(APP1_UID, APP1_GID, APP1_PID, FD1);
        mTestLooper.dispatchAll();

        verify(mLogdMock).decline(APP1_UID, APP1_GID, APP1_PID, FD1);
        verify(mLogdMock, never()).approve(APP1_UID, APP1_GID, APP1_PID, FD1);
        verify(mContextSpy, never()).startActivityAsUser(any(), any());
    }

    @Test
    public void test_RequestFromForegroundService_DeclinedWithoutPrompt() throws Exception {
        when(mActivityManagerInternalMock.getUidProcessState(APP1_UID)).thenReturn(
                ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE);
        mService.getBinderService().startThread(APP1_UID, APP1_GID, APP1_PID, FD1);
        mTestLooper.dispatchAll();

        verify(mLogdMock).decline(APP1_UID, APP1_GID, APP1_PID, FD1);
        verify(mLogdMock, never()).approve(APP1_UID, APP1_GID, APP1_PID, FD1);
        verify(mContextSpy, never()).startActivityAsUser(any(), any());
    }

    @Test
    public void test_RequestFromTop_ShowsPrompt() throws Exception {
        when(mActivityManagerInternalMock.getUidProcessState(APP1_UID)).thenReturn(
                ActivityManager.PROCESS_STATE_TOP);
        mService.getBinderService().startThread(APP1_UID, APP1_GID, APP1_PID, FD1);
        mTestLooper.dispatchAll();

        verify(mLogdMock, never()).approve(APP1_UID, APP1_GID, APP1_PID, FD1);
        verify(mLogdMock, never()).decline(APP1_UID, APP1_GID, APP1_PID, FD1);
        verify(mContextSpy, times(1)).startActivityAsUser(any(), eq(UserHandle.SYSTEM));
    }

    @Test
    public void test_RequestFromTop_NoInteractionWithPrompt_DeclinesAfterTimeout()
            throws Exception {
        when(mActivityManagerInternalMock.getUidProcessState(APP1_UID)).thenReturn(
                ActivityManager.PROCESS_STATE_TOP);
        mService.getBinderService().startThread(APP1_UID, APP1_GID, APP1_PID, FD1);
        mTestLooper.dispatchAll();

        advanceTime(LogcatManagerService.PENDING_CONFIRMATION_TIMEOUT_MILLIS);

        verify(mLogdMock, never()).approve(APP1_UID, APP1_GID, APP1_PID, FD1);
        verify(mLogdMock).decline(APP1_UID, APP1_GID, APP1_PID, FD1);
    }

    @Test
    public void test_RequestFromTop_Approved() throws Exception {
        when(mActivityManagerInternalMock.getUidProcessState(APP1_UID)).thenReturn(
                ActivityManager.PROCESS_STATE_TOP);
        mService.getBinderService().startThread(APP1_UID, APP1_GID, APP1_PID, FD1);
        mTestLooper.dispatchAll();
        verify(mContextSpy, times(1)).startActivityAsUser(any(), eq(UserHandle.SYSTEM));

        mLocalService.approveAccessForClient(APP1_UID, APP1_PACKAGE_NAME);
        mTestLooper.dispatchAll();

        verify(mLogdMock, times(1)).approve(APP1_UID, APP1_GID, APP1_PID, FD1);
        verify(mLogdMock, never()).decline(APP1_UID, APP1_GID, APP1_PID, FD1);
    }

    @Test
    public void test_RequestFromTop_Declined() throws Exception {
        when(mActivityManagerInternalMock.getUidProcessState(APP1_UID)).thenReturn(
                ActivityManager.PROCESS_STATE_TOP);
        mService.getBinderService().startThread(APP1_UID, APP1_GID, APP1_PID, FD1);
        mTestLooper.dispatchAll();
        verify(mContextSpy, times(1)).startActivityAsUser(any(), eq(UserHandle.SYSTEM));

        mLocalService.declineAccessForClient(APP1_UID, APP1_PACKAGE_NAME);
        mTestLooper.dispatchAll();

        verify(mLogdMock, never()).approve(APP1_UID, APP1_GID, APP1_PID, FD1);
        verify(mLogdMock, times(1)).decline(APP1_UID, APP1_GID, APP1_PID, FD1);
    }

    @Test
    public void test_RequestFromTop_MultipleRequestsApprovedTogether() throws Exception {
        when(mActivityManagerInternalMock.getUidProcessState(APP1_UID)).thenReturn(
                ActivityManager.PROCESS_STATE_TOP);
        mService.getBinderService().startThread(APP1_UID, APP1_GID, APP1_PID, FD1);
        mService.getBinderService().startThread(APP1_UID, APP1_GID, APP1_PID, FD2);
        mTestLooper.dispatchAll();
        verify(mContextSpy, times(1)).startActivityAsUser(any(), eq(UserHandle.SYSTEM));
        verify(mLogdMock, never()).approve(eq(APP1_UID), eq(APP1_GID), eq(APP1_PID), anyInt());
        verify(mLogdMock, never()).decline(eq(APP1_UID), eq(APP1_GID), eq(APP1_PID), anyInt());

        mLocalService.approveAccessForClient(APP1_UID, APP1_PACKAGE_NAME);
        mTestLooper.dispatchAll();

        verify(mLogdMock, times(1)).approve(APP1_UID, APP1_GID, APP1_PID, FD1);
        verify(mLogdMock, times(1)).approve(APP1_UID, APP1_GID, APP1_PID, FD2);
        verify(mLogdMock, never()).decline(APP1_UID, APP1_GID, APP1_PID, FD1);
        verify(mLogdMock, never()).decline(APP1_UID, APP1_GID, APP1_PID, FD2);
    }

    @Test
    public void test_RequestFromTop_MultipleRequestsDeclinedTogether() throws Exception {
        when(mActivityManagerInternalMock.getUidProcessState(APP1_UID)).thenReturn(
                ActivityManager.PROCESS_STATE_TOP);
        mService.getBinderService().startThread(APP1_UID, APP1_GID, APP1_PID, FD1);
        mService.getBinderService().startThread(APP1_UID, APP1_GID, APP1_PID, FD2);
        mTestLooper.dispatchAll();
        verify(mContextSpy, times(1)).startActivityAsUser(any(), eq(UserHandle.SYSTEM));
        verify(mLogdMock, never()).approve(eq(APP1_UID), eq(APP1_GID), eq(APP1_PID), anyInt());
        verify(mLogdMock, never()).decline(eq(APP1_UID), eq(APP1_GID), eq(APP1_PID), anyInt());

        mLocalService.declineAccessForClient(APP1_UID, APP1_PACKAGE_NAME);
        mTestLooper.dispatchAll();

        verify(mLogdMock, times(1)).decline(APP1_UID, APP1_GID, APP1_PID, FD1);
        verify(mLogdMock, times(1)).decline(APP1_UID, APP1_GID, APP1_PID, FD2);
        verify(mLogdMock, never()).approve(APP1_UID, APP1_GID, APP1_PID, FD1);
        verify(mLogdMock, never()).approve(APP1_UID, APP1_GID, APP1_PID, FD2);
    }

    @Test
    public void test_RequestFromTop_Approved_DoesNotShowPromptAgain() throws Exception {
        when(mActivityManagerInternalMock.getUidProcessState(APP1_UID)).thenReturn(
                ActivityManager.PROCESS_STATE_TOP);
        mService.getBinderService().startThread(APP1_UID, APP1_GID, APP1_PID, FD1);
        mTestLooper.dispatchAll();
        mLocalService.approveAccessForClient(APP1_UID, APP1_PACKAGE_NAME);
        mTestLooper.dispatchAll();

        mService.getBinderService().startThread(APP1_UID, APP1_GID, APP1_PID, FD2);
        mTestLooper.dispatchAll();

        verify(mContextSpy, times(1)).startActivityAsUser(any(), eq(UserHandle.SYSTEM));
        verify(mLogdMock, times(1)).approve(APP1_UID, APP1_GID, APP1_PID, FD1);
        verify(mLogdMock, times(1)).approve(APP1_UID, APP1_GID, APP1_PID, FD2);
        verify(mLogdMock, never()).decline(APP1_UID, APP1_GID, APP1_PID, FD2);
    }

    @Test
    public void test_RequestFromTop_Declined_DoesNotShowPromptAgain() throws Exception {
        when(mActivityManagerInternalMock.getUidProcessState(APP1_UID)).thenReturn(
                ActivityManager.PROCESS_STATE_TOP);
        mService.getBinderService().startThread(APP1_UID, APP1_GID, APP1_PID, FD1);
        mTestLooper.dispatchAll();
        mLocalService.declineAccessForClient(APP1_UID, APP1_PACKAGE_NAME);
        mTestLooper.dispatchAll();

        mService.getBinderService().startThread(APP1_UID, APP1_GID, APP1_PID, FD2);
        mTestLooper.dispatchAll();

        verify(mContextSpy, times(1)).startActivityAsUser(any(), eq(UserHandle.SYSTEM));
        verify(mLogdMock, times(1)).decline(APP1_UID, APP1_GID, APP1_PID, FD1);
        verify(mLogdMock, times(1)).decline(APP1_UID, APP1_GID, APP1_PID, FD2);
        verify(mLogdMock, never()).approve(APP1_UID, APP1_GID, APP1_PID, FD2);
    }

    @Test
    public void test_RequestFromTop_Approved_ShowsPromptForDifferentClient() throws Exception {
        when(mActivityManagerInternalMock.getUidProcessState(APP1_UID)).thenReturn(
                ActivityManager.PROCESS_STATE_TOP);
        when(mActivityManagerInternalMock.getUidProcessState(APP2_UID)).thenReturn(
                ActivityManager.PROCESS_STATE_TOP);
        mService.getBinderService().startThread(APP1_UID, APP1_GID, APP1_PID, FD1);
        mTestLooper.dispatchAll();
        mLocalService.approveAccessForClient(APP1_UID, APP1_PACKAGE_NAME);
        mTestLooper.dispatchAll();

        mService.getBinderService().startThread(APP2_UID, APP2_GID, APP2_PID, FD2);
        mTestLooper.dispatchAll();

        verify(mContextSpy, times(2)).startActivityAsUser(any(), eq(UserHandle.SYSTEM));
        verify(mLogdMock, never()).decline(APP2_UID, APP2_GID, APP2_PID, FD2);
        verify(mLogdMock, never()).approve(APP2_UID, APP2_GID, APP2_PID, FD2);
    }

    @Test
    public void test_RequestFromTop_Approved_ShowPromptAgainAfterTimeout() throws Exception {
        when(mActivityManagerInternalMock.getUidProcessState(APP1_UID)).thenReturn(
                ActivityManager.PROCESS_STATE_TOP);
        mService.getBinderService().startThread(APP1_UID, APP1_GID, APP1_PID, FD1);
        mTestLooper.dispatchAll();
        mLocalService.declineAccessForClient(APP1_UID, APP1_PACKAGE_NAME);
        mTestLooper.dispatchAll();

        advanceTime(LogcatManagerService.STATUS_EXPIRATION_TIMEOUT_MILLIS);

        mService.getBinderService().startThread(APP1_UID, APP1_GID, APP1_PID, FD1);
        mTestLooper.dispatchAll();

        verify(mContextSpy, times(2)).startActivityAsUser(any(), eq(UserHandle.SYSTEM));
    }

    private void advanceTime(long timeMs) {
        mClock.fastForward(timeMs);
        mTestLooper.dispatchAll();
    }
}