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

Commit ee91033f authored by Sandro Montanari's avatar Sandro Montanari Committed by Android (Google) Code Review
Browse files

Merge "Add SelinuxAuditLogs collection to SystemServer" into main

parents 7b955054 dee33192
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