1 /*
2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.server.selinux;
17 
18 import static com.android.sdksandbox.flags.Flags.selinuxSdkSandboxAudit;
19 
20 import android.app.job.JobInfo;
21 import android.app.job.JobParameters;
22 import android.app.job.JobScheduler;
23 import android.app.job.JobService;
24 import android.content.ComponentName;
25 import android.content.Context;
26 import android.provider.DeviceConfig;
27 import android.provider.DeviceConfig.Properties;
28 import android.util.EventLog;
29 import android.util.Slog;
30 
31 import java.time.Duration;
32 import java.util.Set;
33 import java.util.concurrent.ExecutorService;
34 import java.util.concurrent.Executors;
35 import java.util.concurrent.TimeUnit;
36 
37 /**
38  * Scheduled jobs related to logging of SELinux denials and audits. The job runs daily on idle
39  * devices.
40  */
41 public class SelinuxAuditLogsService extends JobService {
42 
43     private static final String TAG = "SelinuxAuditLogs";
44     private static final String SELINUX_AUDIT_NAMESPACE = "SelinuxAuditLogsNamespace";
45 
46     static final int AUDITD_TAG_CODE = EventLog.getTagCode("auditd");
47 
48     private static final String CONFIG_SELINUX_AUDIT_JOB_FREQUENCY_HOURS =
49             "selinux_audit_job_frequency_hours";
50     private static final String CONFIG_SELINUX_ENABLE_AUDIT_JOB = "selinux_enable_audit_job";
51     private static final String CONFIG_SELINUX_AUDIT_CAP = "selinux_audit_cap";
52     private static final int MAX_PERMITS_CAP_DEFAULT = 50000;
53 
54     private static final int SELINUX_AUDIT_JOB_ID = 25327386;
55     private static final ComponentName SELINUX_AUDIT_JOB_COMPONENT =
56             new ComponentName("android", SelinuxAuditLogsService.class.getName());
57 
58     private static final ExecutorService EXECUTOR_SERVICE = Executors.newSingleThreadExecutor();
59 
60     // Audit logging is subject to both rate and quota limiting. A {@link RateLimiter} makes sure
61     // that we push no more than one atom every 10 milliseconds. A {@link QuotaLimiter} caps the
62     // number of atoms pushed per day to CONFIG_SELINUX_AUDIT_CAP. The quota limiter is static
63     // because new job executions happen in a new instance of this class. Making the quota limiter
64     // an instance reference would reset the quota limitations between jobs executions.
65     private static final Duration RATE_LIMITER_WINDOW = Duration.ofMillis(10);
66     private static final QuotaLimiter QUOTA_LIMITER =
67             new QuotaLimiter(
68                     DeviceConfig.getInt(
69                             DeviceConfig.NAMESPACE_ADSERVICES,
70                             CONFIG_SELINUX_AUDIT_CAP,
71                             MAX_PERMITS_CAP_DEFAULT));
72     private static final SelinuxAuditLogsJob LOGS_COLLECTOR_JOB =
73             new SelinuxAuditLogsJob(
74                     new SelinuxAuditLogsCollector(
75                             new RateLimiter(RATE_LIMITER_WINDOW), QUOTA_LIMITER));
76 
77     /** Schedule jobs with the {@link JobScheduler}. */
schedule(Context context)78     public static void schedule(Context context) {
79         if (!selinuxSdkSandboxAudit()) {
80             Slog.d(TAG, "SelinuxAuditLogsService not enabled");
81             return;
82         }
83 
84         if (AUDITD_TAG_CODE == -1) {
85             Slog.e(TAG, "auditd is not a registered tag on this system");
86             return;
87         }
88 
89         LogsCollectorJobScheduler propertiesListener =
90                 new LogsCollectorJobScheduler(
91                         context.getSystemService(JobScheduler.class)
92                                 .forNamespace(SELINUX_AUDIT_NAMESPACE));
93         propertiesListener.schedule();
94         DeviceConfig.addOnPropertiesChangedListener(
95                 DeviceConfig.NAMESPACE_ADSERVICES, context.getMainExecutor(), propertiesListener);
96     }
97 
98     @Override
onStartJob(JobParameters params)99     public boolean onStartJob(JobParameters params) {
100         if (params.getJobId() != SELINUX_AUDIT_JOB_ID) {
101             Slog.e(TAG, "The job id does not match the expected selinux job id.");
102             return false;
103         }
104         if (!selinuxSdkSandboxAudit()) {
105             Slog.i(TAG, "Selinux audit job disabled.");
106             return false;
107         }
108 
109         EXECUTOR_SERVICE.execute(() -> LOGS_COLLECTOR_JOB.start(this, params));
110         return true; // the job is running
111     }
112 
113     @Override
onStopJob(JobParameters params)114     public boolean onStopJob(JobParameters params) {
115         if (params.getJobId() != SELINUX_AUDIT_JOB_ID) {
116             return false;
117         }
118 
119         if (LOGS_COLLECTOR_JOB.isRunning()) {
120             LOGS_COLLECTOR_JOB.requestStop();
121             return true;
122         }
123         return false;
124     }
125 
126     /**
127      * This class is in charge of scheduling the job service, and keeping the scheduling up to date
128      * when the parameters change.
129      */
130     private static final class LogsCollectorJobScheduler
131             implements DeviceConfig.OnPropertiesChangedListener {
132 
133         private final JobScheduler mJobScheduler;
134 
LogsCollectorJobScheduler(JobScheduler jobScheduler)135         private LogsCollectorJobScheduler(JobScheduler jobScheduler) {
136             mJobScheduler = jobScheduler;
137         }
138 
139         @Override
onPropertiesChanged(Properties changedProperties)140         public void onPropertiesChanged(Properties changedProperties) {
141             Set<String> keyset = changedProperties.getKeyset();
142 
143             if (keyset.contains(CONFIG_SELINUX_AUDIT_CAP)) {
144                 QUOTA_LIMITER.setMaxPermits(
145                         changedProperties.getInt(
146                                 CONFIG_SELINUX_AUDIT_CAP, MAX_PERMITS_CAP_DEFAULT));
147             }
148 
149             if (keyset.contains(CONFIG_SELINUX_ENABLE_AUDIT_JOB)) {
150                 boolean enabled =
151                         changedProperties.getBoolean(
152                                 CONFIG_SELINUX_ENABLE_AUDIT_JOB, /* defaultValue= */ false);
153                 if (enabled) {
154                     schedule();
155                 } else {
156                     mJobScheduler.cancel(SELINUX_AUDIT_JOB_ID);
157                 }
158             } else if (keyset.contains(CONFIG_SELINUX_AUDIT_JOB_FREQUENCY_HOURS)) {
159                 // The job frequency changed, reschedule.
160                 schedule();
161             }
162         }
163 
schedule()164         private void schedule() {
165             long frequencyMillis =
166                     TimeUnit.HOURS.toMillis(
167                             DeviceConfig.getInt(
168                                     DeviceConfig.NAMESPACE_ADSERVICES,
169                                     CONFIG_SELINUX_AUDIT_JOB_FREQUENCY_HOURS,
170                                     24));
171             if (mJobScheduler.schedule(
172                             new JobInfo.Builder(SELINUX_AUDIT_JOB_ID, SELINUX_AUDIT_JOB_COMPONENT)
173                                     .setPeriodic(frequencyMillis)
174                                     .setRequiresDeviceIdle(true)
175                                     .setRequiresBatteryNotLow(true)
176                                     .build())
177                     == JobScheduler.RESULT_FAILURE) {
178                 Slog.e(TAG, "SelinuxAuditLogsService could not be scheduled.");
179             } else {
180                 Slog.d(TAG, "SelinuxAuditLogsService scheduled successfully.");
181             }
182         }
183     }
184 }
185