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

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

Merge "Add flags to parametrize Selinux logs collection" into main

parents b48b9c73 db828d14
Loading
Loading
Loading
Loading
+7 −3
Original line number Diff line number Diff line
@@ -34,10 +34,10 @@ public class QuotaLimiter {

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

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

    @VisibleForTesting
    QuotaLimiter(Clock clock, Duration windowSize, int maxPermits) {
@@ -75,4 +75,8 @@ public class QuotaLimiter {

        return false;
    }

    public void setMaxPermits(int maxPermits) {
        this.mMaxPermits = maxPermits;
    }
}
+52 −21
Original line number Diff line number Diff line
@@ -15,35 +15,66 @@
 */
package com.android.server.selinux;

import android.provider.DeviceConfig;
import android.text.TextUtils;
import android.util.Slog;

import com.android.internal.annotations.VisibleForTesting;

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.regex.PatternSyntaxException;
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("");
    private static final String TAG = "SelinuxAuditLogs";

    static final Matcher TCONTEXT_MATCHER =
            Pattern.compile("u:object_r:(?<ttype>\\w+):s0(:c)?(?<tcategories>((,c)?\\d+)+)*")
                    .matcher("");
    // This config indicates which Selinux logs for source domains to collect. The string will be
    // inserted into a regex, so it must follow the regex syntax. For example, a valid value would
    // be "system_server|untrusted_app".
    @VisibleForTesting static final String CONFIG_SELINUX_AUDIT_DOMAIN = "selinux_audit_domain";
    private static final Matcher NO_OP_MATCHER = Pattern.compile("no-op^").matcher("");
    private static final String TCONTEXT_PATTERN =
            "u:object_r:(?<ttype>\\w+):s0(:c)?(?<tcategories>((,c)?\\d+)+)*";
    private static final String PATH_PATTERN = "\"(?<path>/\\w+(/\\w+)?)(/\\w+)*\"";

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

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

    SelinuxAuditLogBuilder() {
        Matcher scontextMatcher = NO_OP_MATCHER;
        Matcher tcontextMatcher = NO_OP_MATCHER;
        Matcher pathMatcher = NO_OP_MATCHER;
        try {
            scontextMatcher =
                    Pattern.compile(
                                    TextUtils.formatSimple(
                                            "u:r:(?<stype>%s):s0(:c)?(?<scategories>((,c)?\\d+)+)*",
                                            DeviceConfig.getString(
                                                    DeviceConfig.NAMESPACE_ADSERVICES,
                                                    CONFIG_SELINUX_AUDIT_DOMAIN,
                                                    "no_match^")))
                            .matcher("");
            tcontextMatcher = Pattern.compile(TCONTEXT_PATTERN).matcher("");
            pathMatcher = Pattern.compile(PATH_PATTERN).matcher("");
        } catch (PatternSyntaxException e) {
            Slog.e(TAG, "Invalid pattern, setting every matcher to no-op.", e);
        }

        mScontextMatcher = scontextMatcher;
        mTcontextMatcher = tcontextMatcher;
        mPathMatcher = pathMatcher;
    }

    void reset(String denialString) {
        mTokens =
                Arrays.asList(
@@ -82,18 +113,18 @@ class SelinuxAuditLogBuilder {
                    mAuditLog.mPermissions = permissionsStream.build().toArray(String[]::new);
                    break;
                case "scontext":
                    if (!nextTokenMatches(SCONTEXT_MATCHER)) {
                    if (!nextTokenMatches(mScontextMatcher)) {
                        return null;
                    }
                    mAuditLog.mSType = SCONTEXT_MATCHER.group("stype");
                    mAuditLog.mSCategories = toCategories(SCONTEXT_MATCHER.group("scategories"));
                    mAuditLog.mSType = mScontextMatcher.group("stype");
                    mAuditLog.mSCategories = toCategories(mScontextMatcher.group("scategories"));
                    break;
                case "tcontext":
                    if (!nextTokenMatches(TCONTEXT_MATCHER)) {
                    if (!nextTokenMatches(mTcontextMatcher)) {
                        return null;
                    }
                    mAuditLog.mTType = TCONTEXT_MATCHER.group("ttype");
                    mAuditLog.mTCategories = toCategories(TCONTEXT_MATCHER.group("tcategories"));
                    mAuditLog.mTType = mTcontextMatcher.group("ttype");
                    mAuditLog.mTCategories = toCategories(mTcontextMatcher.group("tcategories"));
                    break;
                case "tclass":
                    if (!mTokens.hasNext()) {
@@ -102,8 +133,8 @@ class SelinuxAuditLogBuilder {
                    mAuditLog.mTClass = mTokens.next();
                    break;
                case "path":
                    if (nextTokenMatches(PATH_MATCHER)) {
                        mAuditLog.mPath = PATH_MATCHER.group("path");
                    if (nextTokenMatches(mPathMatcher)) {
                        mAuditLog.mPath = mPathMatcher.group("path");
                    }
                    break;
                case "permissive":
+18 −3
Original line number Diff line number Diff line
@@ -18,10 +18,12 @@ package com.android.server.selinux;
import android.util.EventLog;
import android.util.EventLog.Event;
import android.util.Log;
import android.util.Slog;

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

import java.io.IOException;
import java.time.Instant;
@@ -37,6 +39,7 @@ import java.util.regex.Pattern;
class SelinuxAuditLogsCollector {

    private static final String TAG = "SelinuxAuditLogs";
    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);

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

@@ -48,13 +51,17 @@ class SelinuxAuditLogsCollector {

    @VisibleForTesting Instant mLastWrite = Instant.MIN;

    final AtomicBoolean mStopRequested = new AtomicBoolean(false);
    AtomicBoolean mStopRequested = new AtomicBoolean(false);

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

    public void setStopRequested(boolean stopRequested) {
        mStopRequested.set(stopRequested);
    }

    /**
     * Collect and push SELinux audit logs for the provided {@code tagCode}.
     *
@@ -66,7 +73,7 @@ class SelinuxAuditLogsCollector {

        boolean quotaExceeded = writeAuditLogs(logLines);
        if (quotaExceeded) {
            Log.w(TAG, "Too many SELinux logs in the queue, I am giving up.");
            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();
        }
@@ -79,7 +86,7 @@ class SelinuxAuditLogsCollector {
        try {
            EventLog.readEvents(new int[] {tagCode}, events);
        } catch (IOException e) {
            Log.e(TAG, "Error reading event logs", e);
            Slog.e(TAG, "Error reading event logs", e);
        }

        Instant latestTimestamp = mLastWrite;
@@ -102,6 +109,7 @@ class SelinuxAuditLogsCollector {

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

        while (!mStopRequested.get() && !logLines.isEmpty()) {
            Event event = logLines.poll();
@@ -118,6 +126,9 @@ class SelinuxAuditLogsCollector {
            }

            if (!mQuotaLimiter.acquire()) {
                if (DEBUG) {
                    Slogf.d(TAG, "Running out of quota after %d logs.", auditsWritten);
                }
                return true;
            }
            mRateLimiter.acquire();
@@ -133,12 +144,16 @@ class SelinuxAuditLogsCollector {
                    auditLog.mTClass,
                    auditLog.mPath,
                    auditLog.mPermissive);
            auditsWritten++;

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

        if (DEBUG) {
            Slogf.d(TAG, "Written %d logs", auditsWritten);
        }
        return false;
    }
}
+60 −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;

import android.app.job.JobParameters;
import android.app.job.JobService;
import android.util.Slog;

import java.util.concurrent.atomic.AtomicBoolean;

/**
 * This class handles the start and stop requests for the logs collector job, in particular making
 * sure that at most one job is running at any given moment.
 */
final class SelinuxAuditLogsJob {

    private static final String TAG = "SelinuxAuditLogs";

    private final AtomicBoolean mIsRunning = new AtomicBoolean(false);
    private final SelinuxAuditLogsCollector mAuditLogsCollector;

    SelinuxAuditLogsJob(SelinuxAuditLogsCollector auditLogsCollector) {
        mAuditLogsCollector = auditLogsCollector;
    }

    void requestStop() {
        mAuditLogsCollector.mStopRequested.set(true);
    }

    boolean isRunning() {
        return mIsRunning.get();
    }

    public void start(JobService jobService, JobParameters params) {
        mAuditLogsCollector.mStopRequested.set(false);
        if (mIsRunning.get()) {
            Slog.i(TAG, "Selinux audit job is already running, ignore start request.");
            return;
        }
        mIsRunning.set(true);
        boolean done = mAuditLogsCollector.collect(SelinuxAuditLogsService.AUDITD_TAG_CODE);
        if (done) {
            jobService.jobFinished(params, /* wantsReschedule= */ false);
        }
        mIsRunning.set(false);
    }
}
+100 −48
Original line number Diff line number Diff line
@@ -23,14 +23,16 @@ import android.app.job.JobScheduler;
import android.app.job.JobService;
import android.content.ComponentName;
import android.content.Context;
import android.provider.DeviceConfig;
import android.provider.DeviceConfig.Properties;
import android.util.EventLog;
import android.util.Log;
import android.util.Slog;

import java.time.Duration;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;

/**
 * Scheduled jobs related to logging of SELinux denials and audits. The job runs daily on idle
@@ -43,58 +45,68 @@ public class SelinuxAuditLogsService extends JobService {

    static final int AUDITD_TAG_CODE = EventLog.getTagCode("auditd");

    private static final String CONFIG_SELINUX_AUDIT_JOB_FREQUENCY_HOURS =
            "selinux_audit_job_frequency_hours";
    private static final String CONFIG_SELINUX_ENABLE_AUDIT_JOB = "selinux_enable_audit_job";
    private static final String CONFIG_SELINUX_AUDIT_CAP = "selinux_audit_cap";
    private static final int MAX_PERMITS_CAP_DEFAULT = 50000;

    private static final int SELINUX_AUDIT_JOB_ID = 25327386;
    private static final JobInfo SELINUX_AUDIT_JOB =
            new JobInfo.Builder(
                            SELINUX_AUDIT_JOB_ID,
                            new ComponentName("android", SelinuxAuditLogsService.class.getName()))
                    .setPeriodic(TimeUnit.DAYS.toMillis(1))
                    .setRequiresDeviceIdle(true)
                    .setRequiresCharging(true)
                    .setRequiresBatteryNotLow(true)
                    .build();
    private static final ComponentName SELINUX_AUDIT_JOB_COMPONENT =
            new ComponentName("android", SelinuxAuditLogsService.class.getName());

    private static final ExecutorService EXECUTOR_SERVICE = Executors.newSingleThreadExecutor();
    private static final AtomicReference<Boolean> IS_RUNNING = new AtomicReference<>(false);

    // Audit logging is subject to both rate and quota limiting. We can only push one atom every 10
    // milliseconds, and no more than 50K atoms can be pushed each day.
    private static final SelinuxAuditLogsCollector AUDIT_LOGS_COLLECTOR =
    // Audit logging is subject to both rate and quota limiting. A {@link RateLimiter} makes sure
    // that we push no more than one atom every 10 milliseconds. A {@link QuotaLimiter} caps the
    // number of atoms pushed per day to CONFIG_SELINUX_AUDIT_CAP. The quota limiter is static
    // because new job executions happen in a new instance of this class. Making the quota limiter
    // an instance reference would reset the quota limitations between jobs executions.
    private static final Duration RATE_LIMITER_WINDOW = Duration.ofMillis(10);
    private static final QuotaLimiter QUOTA_LIMITER =
            new QuotaLimiter(
                    DeviceConfig.getInt(
                            DeviceConfig.NAMESPACE_ADSERVICES,
                            CONFIG_SELINUX_AUDIT_CAP,
                            MAX_PERMITS_CAP_DEFAULT));
    private static final SelinuxAuditLogsJob LOGS_COLLECTOR_JOB =
            new SelinuxAuditLogsJob(
                    new SelinuxAuditLogsCollector(
                    new RateLimiter(/* window= */ Duration.ofMillis(10)),
                    new QuotaLimiter(/* maxPermitsPerDay= */ 50000));
                            new RateLimiter(RATE_LIMITER_WINDOW), QUOTA_LIMITER));

    /** Schedule jobs with the {@link JobScheduler}. */
    public static void schedule(Context context) {
        if (!selinuxSdkSandboxAudit()) {
            Log.d(TAG, "SelinuxAuditLogsService not enabled");
            Slog.d(TAG, "SelinuxAuditLogsService not enabled");
            return;
        }

        if (AUDITD_TAG_CODE == -1) {
            Log.e(TAG, "auditd is not a registered tag on this system");
            Slog.e(TAG, "auditd is not a registered tag on this system");
            return;
        }

        if (context.getSystemService(JobScheduler.class)
                        .forNamespace(SELINUX_AUDIT_NAMESPACE)
                        .schedule(SELINUX_AUDIT_JOB)
                == JobScheduler.RESULT_FAILURE) {
            Log.e(TAG, "SelinuxAuditLogsService could not be started.");
        }
        LogsCollectorJobScheduler propertiesListener =
                new LogsCollectorJobScheduler(
                        context.getSystemService(JobScheduler.class)
                                .forNamespace(SELINUX_AUDIT_NAMESPACE));
        propertiesListener.schedule();
        DeviceConfig.addOnPropertiesChangedListener(
                DeviceConfig.NAMESPACE_ADSERVICES, context.getMainExecutor(), propertiesListener);
    }

    @Override
    public boolean onStartJob(JobParameters params) {
        if (params.getJobId() != SELINUX_AUDIT_JOB_ID) {
            Log.e(TAG, "The job id does not match the expected selinux job id.");
            Slog.e(TAG, "The job id does not match the expected selinux job id.");
            return false;
        }
        if (!selinuxSdkSandboxAudit()) {
            Slog.i(TAG, "Selinux audit job disabled.");
            return false;
        }

        AUDIT_LOGS_COLLECTOR.mStopRequested.set(false);
        IS_RUNNING.set(true);
        EXECUTOR_SERVICE.execute(new LogsCollectorJob(this, params));

        EXECUTOR_SERVICE.execute(() -> LOGS_COLLECTOR_JOB.start(this, params));
        return true; // the job is running
    }

@@ -104,29 +116,69 @@ public class SelinuxAuditLogsService extends JobService {
            return false;
        }

        AUDIT_LOGS_COLLECTOR.mStopRequested.set(true);
        return IS_RUNNING.get();
        if (LOGS_COLLECTOR_JOB.isRunning()) {
            LOGS_COLLECTOR_JOB.requestStop();
            return true;
        }
        return false;
    }

    private static class LogsCollectorJob implements Runnable {
        private final JobService mAuditLogService;
        private final JobParameters mParams;
    /**
     * This class is in charge of scheduling the job service, and keeping the scheduling up to date
     * when the parameters change.
     */
    private static final class LogsCollectorJobScheduler
            implements DeviceConfig.OnPropertiesChangedListener {

        LogsCollectorJob(JobService auditLogService, JobParameters params) {
            mAuditLogService = auditLogService;
            mParams = params;
        private final JobScheduler mJobScheduler;

        private LogsCollectorJobScheduler(JobScheduler jobScheduler) {
            mJobScheduler = jobScheduler;
        }

        @Override
        public void run() {
            IS_RUNNING.updateAndGet(
                    isRunning -> {
                        boolean done = AUDIT_LOGS_COLLECTOR.collect(AUDITD_TAG_CODE);
                        if (done) {
                            mAuditLogService.jobFinished(mParams, /* wantsReschedule= */ false);
                        }
                        return !done;
                    });
        public void onPropertiesChanged(Properties changedProperties) {
            Set<String> keyset = changedProperties.getKeyset();

            if (keyset.contains(CONFIG_SELINUX_AUDIT_CAP)) {
                QUOTA_LIMITER.setMaxPermits(
                        changedProperties.getInt(
                                CONFIG_SELINUX_AUDIT_CAP, MAX_PERMITS_CAP_DEFAULT));
            }

            if (keyset.contains(CONFIG_SELINUX_ENABLE_AUDIT_JOB)) {
                boolean enabled =
                        changedProperties.getBoolean(
                                CONFIG_SELINUX_ENABLE_AUDIT_JOB, /* defaultValue= */ false);
                if (enabled) {
                    schedule();
                } else {
                    mJobScheduler.cancel(SELINUX_AUDIT_JOB_ID);
                }
            } else if (keyset.contains(CONFIG_SELINUX_AUDIT_JOB_FREQUENCY_HOURS)) {
                // The job frequency changed, reschedule.
                schedule();
            }
        }

        private void schedule() {
            long frequencyMillis =
                    TimeUnit.HOURS.toMillis(
                            DeviceConfig.getInt(
                                    DeviceConfig.NAMESPACE_ADSERVICES,
                                    CONFIG_SELINUX_AUDIT_JOB_FREQUENCY_HOURS,
                                    24));
            if (mJobScheduler.schedule(
                            new JobInfo.Builder(SELINUX_AUDIT_JOB_ID, SELINUX_AUDIT_JOB_COMPONENT)
                                    .setPeriodic(frequencyMillis)
                                    .setRequiresDeviceIdle(true)
                                    .setRequiresBatteryNotLow(true)
                                    .build())
                    == JobScheduler.RESULT_FAILURE) {
                Slog.e(TAG, "SelinuxAuditLogsService could not be scheduled.");
            } else {
                Slog.d(TAG, "SelinuxAuditLogsService scheduled successfully.");
            }
        }
    }
}
Loading