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

Commit dee33192 authored by Sandro Montanari's avatar Sandro Montanari
Browse files

Add SelinuxAuditLogs collection to SystemServer

Bug: 295861450
Test: atest SelinuxAuditLogsCollectorTest
Change-Id: I2e0b6b537c513bd4f9580c414e3f4a9b1f23df62
parent b363ed1d
Loading
Loading
Loading
Loading
+4 −0
Original line number Diff line number Diff line
@@ -8415,6 +8415,10 @@
                 android:permission="android.permission.BIND_JOB_SERVICE">
        </service>

        <service android:name="com.android.server.selinux.SelinuxAuditLogsService"
                 android:permission="android.permission.BIND_JOB_SERVICE">
        </service>

        <service android:name="com.android.server.compos.IsolatedCompilationJobService"
                 android:permission="android.permission.BIND_JOB_SERVICE">
        </service>
+78 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 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;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.os.Clock;

import java.time.Duration;
import java.time.Instant;

/**
 * A QuotaLimiter allows to define a maximum number of Atom pushes within a specific time window.
 *
 * <p>The limiter divides the time line in windows of a fixed size. Every time a new permit is
 * requested, the limiter checks whether the previous request was in the same time window as the
 * current one. If the two windows are the same, it grants a permit only if the number of permits
 * granted within the window does not exceed the quota. If the two windows are different, it resets
 * the quota.
 */
public class QuotaLimiter {

    private final Clock mClock;
    private final Duration mWindowSize;
    private final int mMaxPermits;

    private long mCurrentWindow = 0;
    private int mPermitsGranted = 0;

    @VisibleForTesting
    QuotaLimiter(Clock clock, Duration windowSize, int maxPermits) {
        mClock = clock;
        mWindowSize = windowSize;
        mMaxPermits = maxPermits;
    }

    public QuotaLimiter(Duration windowSize, int maxPermits) {
        this(Clock.SYSTEM_CLOCK, windowSize, maxPermits);
    }

    public QuotaLimiter(int maxPermitsPerDay) {
        this(Clock.SYSTEM_CLOCK, Duration.ofDays(1), maxPermitsPerDay);
    }

    /**
     * Acquires a permit if there is one available in the current time window.
     *
     * @return true if a permit was acquired.
     */
    boolean acquire() {
        long nowWindow =
                Duration.between(Instant.EPOCH, Instant.ofEpochMilli(mClock.currentTimeMillis()))
                        .dividedBy(mWindowSize);
        if (nowWindow > mCurrentWindow) {
            mCurrentWindow = nowWindow;
            mPermitsGranted = 0;
        }

        if (mPermitsGranted < mMaxPermits) {
            mPermitsGranted++;
            return true;
        }

        return false;
    }
}
+85 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 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;

import android.os.SystemClock;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.os.Clock;

import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;

/**
 * Rate limiter to ensure Atoms are pushed only within the allowed QPS window. This class is not
 * thread-safe.
 *
 * <p>The rate limiter is smoothed, meaning that a rate limiter allowing X permits per second (or X
 * QPS) will grant permits at a ratio of one every 1/X seconds.
 */
public final class RateLimiter {

    private Instant mNextPermit = Instant.EPOCH;

    private final Clock mClock;
    private final Duration mWindow;

    @VisibleForTesting
    RateLimiter(Clock clock, Duration window) {
        mClock = clock;
        // Truncating because the system clock does not support units smaller than milliseconds.
        mWindow = window;
    }

    /**
     * Create a rate limiter generating one permit every {@code window} of time, using the {@link
     * Clock.SYSTEM_CLOCK}.
     */
    public RateLimiter(Duration window) {
        this(Clock.SYSTEM_CLOCK, window);
    }

    /**
     * Acquire a permit if allowed by the rate limiter. If not, wait until a permit becomes
     * available.
     */
    public void acquire() {
        Instant now = Instant.ofEpochMilli(mClock.currentTimeMillis());

        if (mNextPermit.isAfter(now)) { // Sleep until we can acquire.
            SystemClock.sleep(ChronoUnit.MILLIS.between(now, mNextPermit));
            mNextPermit = mNextPermit.plus(mWindow);
        } else {
            mNextPermit = now.plus(mWindow);
        }
    }

    /**
     * Try to acquire a permit if allowed by the rate limiter. Non-blocking.
     *
     * @return true if a permit was acquired. Otherwise, return false.
     */
    public boolean tryAcquire() {
        final Instant now = Instant.ofEpochMilli(mClock.currentTimeMillis());

        if (mNextPermit.isAfter(now)) {
            return false;
        }
        mNextPermit = now.plus(mWindow);
        return true;
    }
}
+155 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 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;

import java.util.Arrays;
import java.util.Iterator;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;

/** Builder for SelinuxAuditLogs. */
class SelinuxAuditLogBuilder {

    // Currently logs collection is hardcoded for the sdk_sandbox_audit.
    private static final String SDK_SANDBOX_AUDIT = "sdk_sandbox_audit";
    static final Matcher SCONTEXT_MATCHER =
            Pattern.compile(
                            "u:r:(?<stype>"
                                    + SDK_SANDBOX_AUDIT
                                    + "):s0(:c)?(?<scategories>((,c)?\\d+)+)*")
                    .matcher("");

    static final Matcher TCONTEXT_MATCHER =
            Pattern.compile("u:object_r:(?<ttype>\\w+):s0(:c)?(?<tcategories>((,c)?\\d+)+)*")
                    .matcher("");

    static final Matcher PATH_MATCHER =
            Pattern.compile("\"(?<path>/\\w+(/\\w+)?)(/\\w+)*\"").matcher("");

    private Iterator<String> mTokens;
    private final SelinuxAuditLog mAuditLog = new SelinuxAuditLog();

    void reset(String denialString) {
        mTokens =
                Arrays.asList(
                                Optional.ofNullable(denialString)
                                        .map(s -> s.split("\\s+|="))
                                        .orElse(new String[0]))
                        .iterator();
        mAuditLog.reset();
    }

    SelinuxAuditLog build() {
        while (mTokens.hasNext()) {
            final String token = mTokens.next();

            switch (token) {
                case "granted":
                    mAuditLog.mGranted = true;
                    break;
                case "denied":
                    mAuditLog.mGranted = false;
                    break;
                case "{":
                    Stream.Builder<String> permissionsStream = Stream.builder();
                    boolean closed = false;
                    while (!closed && mTokens.hasNext()) {
                        String permission = mTokens.next();
                        if ("}".equals(permission)) {
                            closed = true;
                        } else {
                            permissionsStream.add(permission);
                        }
                    }
                    if (!closed) {
                        return null;
                    }
                    mAuditLog.mPermissions = permissionsStream.build().toArray(String[]::new);
                    break;
                case "scontext":
                    if (!nextTokenMatches(SCONTEXT_MATCHER)) {
                        return null;
                    }
                    mAuditLog.mSType = SCONTEXT_MATCHER.group("stype");
                    mAuditLog.mSCategories = toCategories(SCONTEXT_MATCHER.group("scategories"));
                    break;
                case "tcontext":
                    if (!nextTokenMatches(TCONTEXT_MATCHER)) {
                        return null;
                    }
                    mAuditLog.mTType = TCONTEXT_MATCHER.group("ttype");
                    mAuditLog.mTCategories = toCategories(TCONTEXT_MATCHER.group("tcategories"));
                    break;
                case "tclass":
                    if (!mTokens.hasNext()) {
                        return null;
                    }
                    mAuditLog.mTClass = mTokens.next();
                    break;
                case "path":
                    if (nextTokenMatches(PATH_MATCHER)) {
                        mAuditLog.mPath = PATH_MATCHER.group("path");
                    }
                    break;
                case "permissive":
                    if (!mTokens.hasNext()) {
                        return null;
                    }
                    mAuditLog.mPermissive = "1".equals(mTokens.next());
                    break;
                default:
                    break;
            }
        }
        return mAuditLog;
    }

    boolean nextTokenMatches(Matcher matcher) {
        return mTokens.hasNext() && matcher.reset(mTokens.next()).matches();
    }

    static int[] toCategories(String categories) {
        return categories == null
                ? null
                : Arrays.stream(categories.split(",c")).mapToInt(Integer::parseInt).toArray();
    }

    static class SelinuxAuditLog {
        boolean mGranted = false;
        String[] mPermissions = null;
        String mSType = null;
        int[] mSCategories = null;
        String mTType = null;
        int[] mTCategories = null;
        String mTClass = null;
        String mPath = null;
        boolean mPermissive = false;

        private void reset() {
            mGranted = false;
            mPermissions = null;
            mSType = null;
            mSCategories = null;
            mTType = null;
            mTCategories = null;
            mTClass = null;
            mPath = null;
            mPermissive = false;
        }
    }
}
+144 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 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;

import android.util.EventLog;
import android.util.EventLog.Event;
import android.util.Log;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.FrameworkStatsLog;
import com.android.server.selinux.SelinuxAuditLogBuilder.SelinuxAuditLog;

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.concurrent.atomic.AtomicBoolean;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/** Class in charge of collecting SELinux audit logs and push the SELinux atoms. */
class SelinuxAuditLogsCollector {

    private static final String TAG = "SelinuxAuditLogs";

    private static final String SELINUX_PATTERN = "^.*\\bavc:\\s+(?<denial>.*)$";

    @VisibleForTesting
    static final Matcher SELINUX_MATCHER = Pattern.compile(SELINUX_PATTERN).matcher("");

    private final RateLimiter mRateLimiter;
    private final QuotaLimiter mQuotaLimiter;

    @VisibleForTesting Instant mLastWrite = Instant.MIN;

    final AtomicBoolean mStopRequested = new AtomicBoolean(false);

    SelinuxAuditLogsCollector(RateLimiter rateLimiter, QuotaLimiter quotaLimiter) {
        mRateLimiter = rateLimiter;
        mQuotaLimiter = quotaLimiter;
    }

    /**
     * Collect and push SELinux audit logs for the provided {@code tagCode}.
     *
     * @return true if the job was completed. If the job was interrupted, return false.
     */
    boolean collect(int tagCode) {
        Queue<Event> logLines = new ArrayDeque<>();
        Instant latestTimestamp = collectLogLines(tagCode, logLines);

        boolean quotaExceeded = writeAuditLogs(logLines);
        if (quotaExceeded) {
            Log.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();
        }

        return logLines.isEmpty();
    }

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

        Instant latestTimestamp = mLastWrite;
        for (Event event : events) {
            Instant eventTime = Instant.ofEpochSecond(0, event.getTimeNanos());
            if (eventTime.isAfter(latestTimestamp)) {
                latestTimestamp = eventTime;
            }
            if (eventTime.isBefore(mLastWrite)) {
                continue;
            }
            Object eventData = event.getData();
            if (!(eventData instanceof String)) {
                continue;
            }
            logLines.add(event);
        }
        return latestTimestamp;
    }

    private boolean writeAuditLogs(Queue<Event> logLines) {
        final SelinuxAuditLogBuilder auditLogBuilder = new SelinuxAuditLogBuilder();

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

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

            if (!mQuotaLimiter.acquire()) {
                return true;
            }
            mRateLimiter.acquire();

            FrameworkStatsLog.write(
                    FrameworkStatsLog.SELINUX_AUDIT_LOG,
                    auditLog.mGranted,
                    auditLog.mPermissions,
                    auditLog.mSType,
                    auditLog.mSCategories,
                    auditLog.mTType,
                    auditLog.mTCategories,
                    auditLog.mTClass,
                    auditLog.mPath,
                    auditLog.mPermissive);

            if (logTime.isAfter(mLastWrite)) {
                mLastWrite = logTime;
            }
        }

        return false;
    }
}
Loading