Loading services/core/java/com/android/server/selinux/QuotaLimiter.java +7 −3 Original line number Diff line number Diff line Loading @@ -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) { Loading Loading @@ -75,4 +75,8 @@ public class QuotaLimiter { return false; } public void setMaxPermits(int maxPermits) { this.mMaxPermits = maxPermits; } } services/core/java/com/android/server/selinux/SelinuxAuditLogBuilder.java +52 −21 Original line number Diff line number Diff line Loading @@ -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( Loading Loading @@ -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()) { Loading @@ -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": Loading services/core/java/com/android/server/selinux/SelinuxAuditLogsCollector.java +18 −3 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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>.*)$"; Loading @@ -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}. * Loading @@ -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(); } Loading @@ -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; Loading @@ -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(); Loading @@ -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(); Loading @@ -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; } } services/core/java/com/android/server/selinux/SelinuxAuditLogsJob.java 0 → 100644 +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); } } services/core/java/com/android/server/selinux/SelinuxAuditLogsService.java +100 −48 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 } Loading @@ -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
services/core/java/com/android/server/selinux/QuotaLimiter.java +7 −3 Original line number Diff line number Diff line Loading @@ -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) { Loading Loading @@ -75,4 +75,8 @@ public class QuotaLimiter { return false; } public void setMaxPermits(int maxPermits) { this.mMaxPermits = maxPermits; } }
services/core/java/com/android/server/selinux/SelinuxAuditLogBuilder.java +52 −21 Original line number Diff line number Diff line Loading @@ -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( Loading Loading @@ -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()) { Loading @@ -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": Loading
services/core/java/com/android/server/selinux/SelinuxAuditLogsCollector.java +18 −3 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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>.*)$"; Loading @@ -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}. * Loading @@ -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(); } Loading @@ -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; Loading @@ -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(); Loading @@ -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(); Loading @@ -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; } }
services/core/java/com/android/server/selinux/SelinuxAuditLogsJob.java 0 → 100644 +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); } }
services/core/java/com/android/server/selinux/SelinuxAuditLogsService.java +100 −48 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 } Loading @@ -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."); } } } }