content: /*
   * Copyright 2018 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.pm;
  
  import android.app.job.JobInfo;
  import android.app.job.JobParameters;
  import android.app.job.JobScheduler;
  import android.app.job.JobService;
  import android.content.ComponentName;
  import android.content.Context;
  import android.os.Process;
  import android.os.ServiceManager;
  import android.util.ByteStringUtils;
  import android.util.EventLog;
  import android.util.Log;
  
  import com.android.server.pm.dex.DynamicCodeLogger;
  
  import java.util.ArrayList;
  import java.util.List;
  import java.util.concurrent.TimeUnit;
  import java.util.regex.Matcher;
  import java.util.regex.Pattern;
  
  /**
   * Scheduled jobs related to logging of app dynamic code loading. The idle logging job runs daily
   * while idle and charging  and calls {@link DynamicCodeLogger} to write dynamic code information
   * to the event log. The audit watching job scans the event log periodically while idle to find AVC
   * audit messages indicating use of dynamic native code and adds the information to
   * {@link DynamicCodeLogger}.
   * {@hide}
   */
  public class DynamicCodeLoggingService extends JobService {
      private static final String TAG = DynamicCodeLoggingService.class.getName();
  
      private static final boolean DEBUG = false;
  
      private static final int IDLE_LOGGING_JOB_ID = 2030028;
      private static final int AUDIT_WATCHING_JOB_ID = 203142925;
  
      private static final long IDLE_LOGGING_PERIOD_MILLIS = TimeUnit.DAYS.toMillis(1);
      private static final long AUDIT_WATCHING_PERIOD_MILLIS = TimeUnit.HOURS.toMillis(2);
  
      private static final int AUDIT_AVC = 1400;  // Defined in linux/audit.h
      private static final String AVC_PREFIX = "type=" + AUDIT_AVC + " ";
  
      private static final Pattern EXECUTE_NATIVE_AUDIT_PATTERN =
              Pattern.compile(".*\\bavc: granted \\{ execute(?:_no_trans|) \\} .*"
                      + "\\bpath=(?:\"([^\" ]*)\"|([0-9A-F]+)) .*"
                      + "\\bscontext=u:r:untrusted_app(?:_25|_27)?:.*"
                      + "\\btcontext=u:object_r:app_data_file:.*"
                      + "\\btclass=file\\b.*");
  
      private volatile boolean mIdleLoggingStopRequested = false;
      private volatile boolean mAuditWatchingStopRequested = false;
  
      /**
       * Schedule our jobs with the {@link JobScheduler}.
       */
      public static void schedule(Context context) {
          ComponentName serviceName = new ComponentName(
                  "android", DynamicCodeLoggingService.class.getName());
  
          JobScheduler js = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
          js.schedule(new JobInfo.Builder(IDLE_LOGGING_JOB_ID, serviceName)
                  .setRequiresDeviceIdle(true)
                  .setRequiresCharging(true)
                  .setPeriodic(IDLE_LOGGING_PERIOD_MILLIS)
                  .build());
          js.schedule(new JobInfo.Builder(AUDIT_WATCHING_JOB_ID, serviceName)
                  .setRequiresDeviceIdle(true)
                  .setRequiresBatteryNotLow(true)
                  .setPeriodic(AUDIT_WATCHING_PERIOD_MILLIS)
                  .build());
  
          if (DEBUG) {
              Log.d(TAG, "Jobs scheduled");
          }
      }
  
      @Override
      public boolean onStartJob(JobParameters params) {
          int jobId = params.getJobId();
          if (DEBUG) {
              Log.d(TAG, "onStartJob " + jobId);
          }
          switch (jobId) {
              case IDLE_LOGGING_JOB_ID:
                  mIdleLoggingStopRequested = false;
                  new IdleLoggingThread(params).start();
                  return true;  // Job is running on another thread
              case AUDIT_WATCHING_JOB_ID:
                  mAuditWatchingStopRequested = false;
                  new AuditWatchingThread(params).start();
                  return true;  // Job is running on another thread
              default:
                  // Shouldn't happen, but indicate nothing is running.
                  return false;
          }
      }
  
      @Override
      public boolean onStopJob(JobParameters params) {
          int jobId = params.getJobId();
          if (DEBUG) {
              Log.d(TAG, "onStopJob " + jobId);
          }
          switch (jobId) {
              case IDLE_LOGGING_JOB_ID:
                  mIdleLoggingStopRequested = true;
                  return true;  // Requests job be re-scheduled.
              case AUDIT_WATCHING_JOB_ID:
                  mAuditWatchingStopRequested = true;
                  return true;  // Requests job be re-scheduled.
              default:
                  return false;
          }
      }
  
      private static DynamicCodeLogger getDynamicCodeLogger() {
          PackageManagerService pm = (PackageManagerService) ServiceManager.getService("package");
          return pm.getDexManager().getDynamicCodeLogger();
      }
  
      private class IdleLoggingThread extends Thread {
          private final JobParameters mParams;
  
          IdleLoggingThread(JobParameters params) {
              super("DynamicCodeLoggingService_IdleLoggingJob");
              mParams = params;
          }
  
          @Override
          public void run() {
              if (DEBUG) {
                  Log.d(TAG, "Starting IdleLoggingJob run");
              }
  
              DynamicCodeLogger dynamicCodeLogger = getDynamicCodeLogger();
              for (String packageName : dynamicCodeLogger.getAllPackagesWithDynamicCodeLoading()) {
                  if (mIdleLoggingStopRequested) {
                      Log.w(TAG, "Stopping IdleLoggingJob run at scheduler request");
                      return;
                  }
  
                  dynamicCodeLogger.logDynamicCodeLoading(packageName);
              }
  
              jobFinished(mParams, /* reschedule */ false);
              if (DEBUG) {
                  Log.d(TAG, "Finished IdleLoggingJob run");
              }
          }
      }
  
      private class AuditWatchingThread extends Thread {
          private final JobParameters mParams;
  
          AuditWatchingThread(JobParameters params) {
              super("DynamicCodeLoggingService_AuditWatchingJob");
              mParams = params;
          }
  
          @Override
          public void run() {
              if (DEBUG) {
                  Log.d(TAG, "Starting AuditWatchingJob run");
              }
  
              if (processAuditEvents()) {
                  jobFinished(mParams, /* reschedule */ false);
                  if (DEBUG) {
                      Log.d(TAG, "Finished AuditWatchingJob run");
                  }
              }
          }
  
          private boolean processAuditEvents() {
              // Scan the event log for SELinux (avc) audit messages indicating when an
              // (untrusted) app has executed native code from an app data
              // file. Matches are recorded in DynamicCodeLogger.
              //
              // These messages come from the kernel audit system via logd. (Note that
              // some devices may not generate these messages at all, or the format may
              // be different, in which case nothing will be recorded.)
              //
              // The messages use the auditd tag and the uid of the app that executed
              // the code.
              //
              // A typical message might look like this:
              // type=1400 audit(0.0:521): avc: granted { execute } for comm="executable"
              //  path="/data/data/com.dummy.app/executable" dev="sda13" ino=1655302
              //  scontext=u:r:untrusted_app_27:s0:c66,c257,c512,c768
              //  tcontext=u:object_r:app_data_file:s0:c66,c257,c512,c768 tclass=file
              //
              // The information we want is the uid and the path. (Note this may be
              // either a quoted string, as shown above, or a sequence of hex-encoded
              // bytes.)
              //
              // On each run we process all the matching events in the log. This may
              // mean re-processing events we have already seen, and in any case there
              // may be duplicate events for the same app+file. These are de-duplicated
              // by DynamicCodeLogger.
              //
              // Note that any app can write a message to the event log, including one
              // that looks exactly like an AVC audit message, so the information may
              // be spoofed by an app; in such a case the uid we see will be the app
              // that generated the spoof message.
  
              try {
                  int[] tags = { EventLog.getTagCode("auditd") };
                  if (tags[0] == -1) {
                      // auditd is not a registered tag on this system, so there can't be any messages
                      // of interest.
                      return true;
                  }
  
                  DynamicCodeLogger dynamicCodeLogger = getDynamicCodeLogger();
  
                  List<EventLog.Event> events = new ArrayList<>();
                  EventLog.readEvents(tags, events);
  
                  for (int i = 0; i < events.size(); ++i) {
                      if (mAuditWatchingStopRequested) {
                          Log.w(TAG, "Stopping AuditWatchingJob run at scheduler request");
                          return false;
                      }
  
                      EventLog.Event event = events.get(i);
  
                      // Discard clearly unrelated messages as quickly as we can.
                      int uid = event.getUid();
                      if (!Process.isApplicationUid(uid)) {
                          continue;
                      }
                      Object data = event.getData();
                      if (!(data instanceof String)) {
                          continue;
                      }
                      String message = (String) data;
                      if (!message.startsWith(AVC_PREFIX)) {
                          continue;
                      }
  
                      // And then use a regular expression to verify it's one of the messages we're
                      // interested in and to extract the path of the file being loaded.
                      Matcher matcher = EXECUTE_NATIVE_AUDIT_PATTERN.matcher(message);
                      if (!matcher.matches()) {
                          continue;
                      }
                      String path = matcher.group(1);
                      if (path == null) {
                          // If the path contains spaces or various weird characters the kernel
                          // hex-encodes the bytes; we need to undo that.
                          path = unhex(matcher.group(2));
                      }
                      dynamicCodeLogger.recordNative(uid, path);
                  }
  
                  return true;
              } catch (Exception e) {
                  Log.e(TAG, "AuditWatchingJob failed", e);
                  return true;
              }
          }
      }
  
      private static String unhex(String hexEncodedPath) {
          byte[] bytes = ByteStringUtils.fromHexToByteArray(hexEncodedPath);
          if (bytes == null || bytes.length == 0) {
              return "";
          }
          return new String(bytes);
      }
  }
  