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

Commit 59fbf3ea authored by Thiébaud Weksteen's avatar Thiébaud Weksteen Committed by Android (Google) Code Review
Browse files

Merge "Refactor SelinuxAuditLogsCollector to avoid OOM" into main

parents 1a43e3ba af03d58a
Loading
Loading
Loading
Loading
+23 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.selinux;

/** An exception raised when the quota has been reached.
 *
 * This exception is raised in EventLogCollection.add(). See QuotaLimiter
 * for the implementation details.
 */
class QuotaExceededException extends Exception {}
+89 −58
Original line number Diff line number Diff line
@@ -28,10 +28,8 @@ import com.android.server.utils.Slogf;

import java.io.IOException;
import java.time.Instant;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.List;
import java.util.Queue;
import java.util.AbstractCollection;
import java.util.Iterator;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Supplier;
import java.util.regex.Matcher;
@@ -57,6 +55,7 @@ class SelinuxAuditLogsCollector {
    private final Supplier<String> mAuditDomainSupplier;
    private final RateLimiter mRateLimiter;
    private final QuotaLimiter mQuotaLimiter;
    private EventLogCollection mEventCollection;

    @VisibleForTesting Instant mLastWrite = Instant.MIN;

@@ -69,6 +68,7 @@ class SelinuxAuditLogsCollector {
        mAuditDomainSupplier = auditDomainSupplier;
        mRateLimiter = rateLimiter;
        mQuotaLimiter = quotaLimiter;
        mEventCollection = new EventLogCollection();
    }

    SelinuxAuditLogsCollector(RateLimiter rateLimiter, QuotaLimiter quotaLimiter) {
@@ -86,75 +86,72 @@ class SelinuxAuditLogsCollector {
        mStopRequested.set(stopRequested);
    }

    /**
     * Collect and push SELinux audit logs for the provided {@code tagCode}.
    /** A Collection to work around EventLog.readEvents() constraints.
     *
     * This collection only supports add(). Any other method inherited from
     * Collection will throw an UnsupportedOperationException exception.
     *
     * @return true if the job was completed. If the job was interrupted, return false.
     * This collection ensures that we are processing one event at a time and
     * avoid collecting all the event objects before processing (e.g.,
     * ArrayList), which could lead to an OOM situation.
     */
    boolean collect(int tagCode) {
        Queue<Event> logLines = new ArrayDeque<>();
        Instant latestTimestamp = collectLogLines(tagCode, logLines);
    class EventLogCollection extends AbstractCollection<Event> {

        boolean quotaExceeded = writeAuditLogs(logLines);
        if (quotaExceeded) {
            Slog.w(TAG, "Too many SELinux logs in the queue, I am giving up.");
            mLastWrite = latestTimestamp; // next run we will ignore all these logs.
            logLines.clear();
        SelinuxAuditLogBuilder mAuditLogBuilder;
        int mAuditsWritten = 0;
        Instant mLatestTimestamp;

        void reset() {
            mAuditsWritten = 0;
            mLatestTimestamp = mLastWrite;
            mAuditLogBuilder = new SelinuxAuditLogBuilder(mAuditDomainSupplier.get());
        }

        return logLines.isEmpty();
        int getAuditsWritten() {
            return mAuditsWritten;
        }

    private Instant collectLogLines(int tagCode, Queue<Event> logLines) {
        List<Event> events = new ArrayList<>();
        try {
            EventLog.readEvents(new int[] {tagCode}, events);
        } catch (IOException e) {
            Slog.e(TAG, "Error reading event logs", e);
        Instant getLatestTimestamp() {
            return mLatestTimestamp;
        }

        @Override
        public Iterator<Event> iterator() {
            throw new UnsupportedOperationException();
        }

        @Override
        public int size() {
            throw new UnsupportedOperationException();
        }

        Instant latestTimestamp = mLastWrite;
        for (Event event : events) {
            Instant eventTime = Instant.ofEpochSecond(0, event.getTimeNanos());
            if (eventTime.isAfter(latestTimestamp)) {
                latestTimestamp = eventTime;
        @Override
        public boolean add(Event event) {
            if (mStopRequested.get()) {
                throw new IllegalStateException(new InterruptedException());
            }

            Instant eventTime = Instant.ofEpochSecond(/* epochSecond= */ 0, event.getTimeNanos());
            if (eventTime.compareTo(mLastWrite) <= 0) {
                continue;
                return true;
            }
            Object eventData = event.getData();
            if (!(eventData instanceof String)) {
                continue;
            }
            logLines.add(event);
        }
        return latestTimestamp;
                return true;
            }

    private boolean writeAuditLogs(Queue<Event> logLines) {
        final SelinuxAuditLogBuilder auditLogBuilder =
                new SelinuxAuditLogBuilder(mAuditDomainSupplier.get());
        int auditsWritten = 0;

        while (!mStopRequested.get() && !logLines.isEmpty()) {
            Event event = logLines.poll();
            String logLine = (String) event.getData();
            Instant logTime = Instant.ofEpochSecond(0, event.getTimeNanos());
            String logLine = (String) eventData;
            if (!SELINUX_MATCHER.reset(logLine).matches()) {
                continue;
                return true;
            }

            auditLogBuilder.reset(SELINUX_MATCHER.group("denial"));
            final SelinuxAuditLog auditLog = auditLogBuilder.build();
            mAuditLogBuilder.reset(SELINUX_MATCHER.group("denial"));
            final SelinuxAuditLog auditLog = mAuditLogBuilder.build();
            if (auditLog == null) {
                continue;
                return true;
            }

            if (!mQuotaLimiter.acquire()) {
                if (DEBUG) {
                    Slogf.d(TAG, "Running out of quota after %d logs.", auditsWritten);
                }
                return true;
                throw new IllegalStateException(new QuotaExceededException());
            }
            mRateLimiter.acquire();

@@ -169,16 +166,50 @@ class SelinuxAuditLogsCollector {
                    auditLog.mTClass,
                    auditLog.mPath,
                    auditLog.mPermissive);
            auditsWritten++;

            if (logTime.isAfter(mLastWrite)) {
                mLastWrite = logTime;
            mAuditsWritten++;
            if (eventTime.isAfter(mLatestTimestamp)) {
                mLatestTimestamp = eventTime;
            }

            return true;
        }
    }

    /**
     * Collect and push SELinux audit logs for the provided {@code tagCode}.
     *
     * @return true if the job was completed. If the job was interrupted or
     * failed because of IOException, return false.
     * @throws QuotaExceededException if it ran out of quota.
     */
    boolean collect(int tagCode) throws QuotaExceededException {
        mEventCollection.reset();
        try {
            EventLog.readEvents(new int[] {tagCode}, mEventCollection);
        } catch (IllegalStateException e) {
            if (e.getCause() instanceof QuotaExceededException) {
                if (DEBUG) {
            Slogf.d(TAG, "Written %d logs", auditsWritten);
                    Slogf.d(TAG, "Running out of quota after %d logs.",
                            mEventCollection.getAuditsWritten());
                }
                // next run we will ignore all these logs.
                mLastWrite = mEventCollection.getLatestTimestamp();
                throw (QuotaExceededException) e.getCause();
            } else if (e.getCause() instanceof InterruptedException) {
                mLastWrite = mEventCollection.getLatestTimestamp();
                return false;
            }
            throw e;
        } catch (IOException e) {
            Slog.e(TAG, "Error reading event logs", e);
            return false;
        }

        mLastWrite = mEventCollection.getLatestTimestamp();
        if (DEBUG) {
            Slogf.d(TAG, "Written %d logs", mEventCollection.getAuditsWritten());
        }
        return true;
    }
}
+6 −2
Original line number Diff line number Diff line
@@ -51,10 +51,14 @@ final class SelinuxAuditLogsJob {
            return;
        }
        mIsRunning.set(true);
        try {
            boolean done = mAuditLogsCollector.collect(SelinuxAuditLogsService.AUDITD_TAG_CODE);
            if (done) {
                jobService.jobFinished(params, /* wantsReschedule= */ false);
            }
        } catch (QuotaExceededException e) {
            jobService.jobFinished(params, /* wantsReschedule= */ false);
        }
        mIsRunning.set(false);
    }
}
+18 −20
Original line number Diff line number Diff line
@@ -20,6 +20,7 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;

import static com.google.common.truth.Truth.assertThat;

import static org.junit.Assert.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
@@ -81,7 +82,7 @@ public class SelinuxAuditLogsCollectorTest {
    }

    @Test
    public void testWriteAuditLogs() {
    public void testWriteAuditLogs() throws Exception {
        writeTestLog("granted", "perm", TEST_DOMAIN, "ttype", "tclass");
        writeTestLog("denied", "perm1", TEST_DOMAIN, "ttype1", "tclass1");

@@ -117,7 +118,7 @@ public class SelinuxAuditLogsCollectorTest {
    }

    @Test
    public void testWriteAuditLogs_multiplePerms() {
    public void testWriteAuditLogs_multiplePerms() throws Exception {
        writeTestLog("denied", "perm1 perm2", TEST_DOMAIN, "ttype", "tclass");
        writeTestLog("denied", "perm3 perm4", TEST_DOMAIN, "ttype", "tclass");

@@ -153,7 +154,7 @@ public class SelinuxAuditLogsCollectorTest {
    }

    @Test
    public void testWriteAuditLogs_withPaths() {
    public void testWriteAuditLogs_withPaths() throws Exception {
        writeTestLog("denied", "perm", TEST_DOMAIN, "ttype", "tclass", "/good/path");
        writeTestLog("denied", "perm", TEST_DOMAIN, "ttype", "tclass", "/very/long/path");
        writeTestLog("denied", "perm", TEST_DOMAIN, "ttype", "tclass", "/short_path");
@@ -217,7 +218,7 @@ public class SelinuxAuditLogsCollectorTest {
    }

    @Test
    public void testWriteAuditLogs_withCategories() {
    public void testWriteAuditLogs_withCategories() throws Exception {
        writeTestLog("denied", "perm", TEST_DOMAIN, new int[] {123}, "ttype", null, "tclass");
        writeTestLog("denied", "perm", TEST_DOMAIN, new int[] {123, 456}, "ttype", null, "tclass");
        writeTestLog("denied", "perm", TEST_DOMAIN, null, "ttype", new int[] {666}, "tclass");
@@ -288,7 +289,7 @@ public class SelinuxAuditLogsCollectorTest {
    }

    @Test
    public void testWriteAuditLogs_withPathAndCategories() {
    public void testWriteAuditLogs_withPathAndCategories() throws Exception {
        writeTestLog(
                "denied",
                "perm",
@@ -318,7 +319,7 @@ public class SelinuxAuditLogsCollectorTest {
    }

    @Test
    public void testWriteAuditLogs_permissive() {
    public void testWriteAuditLogs_permissive() throws Exception {
        writeTestLog("denied", "perm", TEST_DOMAIN, "ttype", "tclass");
        writeTestLog("denied", "perm", TEST_DOMAIN, "ttype", "tclass", true);
        writeTestLog("denied", "perm", TEST_DOMAIN, "ttype", "tclass", false);
@@ -356,7 +357,7 @@ public class SelinuxAuditLogsCollectorTest {
    }

    @Test
    public void testNotWriteAuditLogs_notTestDomain() {
    public void testNotWriteAuditLogs_notTestDomain() throws Exception {
        writeTestLog("denied", "perm", "stype", "ttype", "tclass");

        boolean done = mSelinuxAutidLogsCollector.collect(ANSWER_TAG);
@@ -379,7 +380,7 @@ public class SelinuxAuditLogsCollectorTest {
    }

    @Test
    public void testWriteAuditLogs_upToQuota() {
    public void testWriteAuditLogs_upToQuota() throws Exception {
        writeTestLog("denied", "perm", TEST_DOMAIN, "ttype", "tclass");
        writeTestLog("denied", "perm", TEST_DOMAIN, "ttype", "tclass");
        writeTestLog("denied", "perm", TEST_DOMAIN, "ttype", "tclass");
@@ -389,9 +390,9 @@ public class SelinuxAuditLogsCollectorTest {
        writeTestLog("denied", "perm", TEST_DOMAIN, "ttype", "tclass");
        writeTestLog("denied", "perm", TEST_DOMAIN, "ttype", "tclass");

        boolean done = mSelinuxAutidLogsCollector.collect(ANSWER_TAG);
        assertThrows(QuotaExceededException.class, () ->
                mSelinuxAutidLogsCollector.collect(ANSWER_TAG));

        assertThat(done).isTrue();
        verify(
                () ->
                        FrameworkStatsLog.write(
@@ -409,7 +410,7 @@ public class SelinuxAuditLogsCollectorTest {
    }

    @Test
    public void testWriteAuditLogs_resetQuota() {
    public void testWriteAuditLogs_resetQuota() throws Exception {
        writeTestLog("denied", "perm", TEST_DOMAIN, "ttype", "tclass");
        writeTestLog("denied", "perm", TEST_DOMAIN, "ttype", "tclass");
        writeTestLog("denied", "perm", TEST_DOMAIN, "ttype", "tclass");
@@ -418,8 +419,8 @@ public class SelinuxAuditLogsCollectorTest {
        writeTestLog("denied", "perm", TEST_DOMAIN, "ttype", "tclass");
        writeTestLog("denied", "perm", TEST_DOMAIN, "ttype", "tclass");

        boolean done = mSelinuxAutidLogsCollector.collect(ANSWER_TAG);
        assertThat(done).isTrue();
        assertThrows(QuotaExceededException.class, () ->
                mSelinuxAutidLogsCollector.collect(ANSWER_TAG));
        verify(
                () ->
                        FrameworkStatsLog.write(
@@ -442,8 +443,8 @@ public class SelinuxAuditLogsCollectorTest {
        writeTestLog("denied", "perm", TEST_DOMAIN, "ttype", "tclass");
        // move the clock forward to reset the quota limiter.
        mClock.currentTimeMillis += Duration.ofHours(1).toMillis();
        done = mSelinuxAutidLogsCollector.collect(ANSWER_TAG);
        assertThat(done).isTrue();
        assertThrows(QuotaExceededException.class, () ->
                mSelinuxAutidLogsCollector.collect(ANSWER_TAG));
        verify(
                () ->
                        FrameworkStatsLog.write(
@@ -461,14 +462,11 @@ public class SelinuxAuditLogsCollectorTest {
    }

    @Test
    public void testNotWriteAuditLogs_stopRequested() {
        writeTestLog("denied", "perm", TEST_DOMAIN, "ttype", "tclass");
    public void testNotWriteAuditLogs_stopRequested() throws Exception {
        writeTestLog("denied", "perm", TEST_DOMAIN, "ttype", "tclass");
        writeTestLog("denied", "perm", TEST_DOMAIN, "ttype", "tclass");
        writeTestLog("denied", "perm", TEST_DOMAIN, "ttype", "tclass");
        writeTestLog("denied", "perm", TEST_DOMAIN, "ttype", "tclass");
        // These are not pushed.
        writeTestLog("denied", "perm", TEST_DOMAIN, "ttype", "tclass");
        writeTestLog("denied", "perm", TEST_DOMAIN, "ttype", "tclass");

        mSelinuxAutidLogsCollector.setStopRequested(true);
@@ -509,7 +507,7 @@ public class SelinuxAuditLogsCollectorTest {
    }

    @Test
    public void testAuditLogs_resumeJobDoesNotExceedLimit() {
    public void testAuditLogs_resumeJobDoesNotExceedLimit() throws Exception {
        writeTestLog("denied", "perm", TEST_DOMAIN, "ttype", "tclass");
        mSelinuxAutidLogsCollector.setStopRequested(true);

+4 −4
Original line number Diff line number Diff line
@@ -53,7 +53,7 @@ public class SelinuxAuditLogsJobTest {
    }

    @Test
    public void testFinishSuccessfully() {
    public void testFinishSuccessfully() throws Exception {
        when(mAuditLogsCollector.collect(anyInt())).thenReturn(true);

        mAuditLogsJob.start(mJobService, mParams);
@@ -63,7 +63,7 @@ public class SelinuxAuditLogsJobTest {
    }

    @Test
    public void testInterrupt() {
    public void testInterrupt() throws Exception {
        when(mAuditLogsCollector.collect(anyInt())).thenReturn(false);

        mAuditLogsJob.start(mJobService, mParams);
@@ -73,7 +73,7 @@ public class SelinuxAuditLogsJobTest {
    }

    @Test
    public void testInterruptAndResume() {
    public void testInterruptAndResume() throws Exception {
        when(mAuditLogsCollector.collect(anyInt())).thenReturn(false);
        mAuditLogsJob.start(mJobService, mParams);
        verify(mJobService, never()).jobFinished(any(), anyBoolean());
@@ -85,7 +85,7 @@ public class SelinuxAuditLogsJobTest {
    }

    @Test
    public void testRequestStop() throws InterruptedException {
    public void testRequestStop() throws Exception {
        Semaphore isRunning = new Semaphore(0);
        Semaphore stopRequested = new Semaphore(0);
        AtomicReference<Throwable> uncaughtException = new AtomicReference<>();