Loading core/res/AndroidManifest.xml +4 −0 Original line number Diff line number Diff line Loading @@ -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> Loading services/core/java/com/android/server/selinux/QuotaLimiter.java 0 → 100644 +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; } } services/core/java/com/android/server/selinux/RateLimiter.java 0 → 100644 +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; } } services/core/java/com/android/server/selinux/SelinuxAuditLogBuilder.java 0 → 100644 +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; } } } services/core/java/com/android/server/selinux/SelinuxAuditLogsCollector.java 0 → 100644 +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
core/res/AndroidManifest.xml +4 −0 Original line number Diff line number Diff line Loading @@ -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> Loading
services/core/java/com/android/server/selinux/QuotaLimiter.java 0 → 100644 +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; } }
services/core/java/com/android/server/selinux/RateLimiter.java 0 → 100644 +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; } }
services/core/java/com/android/server/selinux/SelinuxAuditLogBuilder.java 0 → 100644 +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; } } }
services/core/java/com/android/server/selinux/SelinuxAuditLogsCollector.java 0 → 100644 +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; } }