Merge "Add logging of dynamic loading of native code."
diff --git a/services/core/java/com/android/server/pm/DynamicCodeLoggingService.java b/services/core/java/com/android/server/pm/DynamicCodeLoggingService.java
index 2ae424d..5b765df 100644
--- a/services/core/java/com/android/server/pm/DynamicCodeLoggingService.java
+++ b/services/core/java/com/android/server/pm/DynamicCodeLoggingService.java
@@ -22,63 +22,117 @@
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.DexLogger;
+import java.util.ArrayList;
+import java.util.List;
import java.util.concurrent.TimeUnit;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
/**
- * Scheduled job to trigger logging of app dynamic code loading. This runs daily while idle and
- * charging. The actual logging is performed by {@link DexLogger}.
+ * Scheduled jobs related to logging of app dynamic code loading. The idle logging job runs daily
+ * while idle and charging and calls {@link DexLogger} 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 DexLogger}.
* {@hide}
*/
public class DynamicCodeLoggingService extends JobService {
private static final String TAG = DynamicCodeLoggingService.class.getName();
- private static final int JOB_ID = 2030028;
- private static final long PERIOD_MILLIS = TimeUnit.DAYS.toMillis(1);
-
- private volatile boolean mStopRequested = false;
-
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_2(?:5|7):.*"
+ + "\\btcontext=u:object_r:app_data_file:.*"
+ + "\\btclass=file\\b.*");
+
+ private volatile boolean mIdleLoggingStopRequested = false;
+ private volatile boolean mAuditWatchingStopRequested = false;
+
/**
- * Schedule our job with the {@link JobScheduler}.
+ * 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(JOB_ID, serviceName)
+ js.schedule(new JobInfo.Builder(IDLE_LOGGING_JOB_ID, serviceName)
.setRequiresDeviceIdle(true)
.setRequiresCharging(true)
- .setPeriodic(PERIOD_MILLIS)
+ .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, "Job scheduled");
+ Log.d(TAG, "Jobs scheduled");
}
}
@Override
public boolean onStartJob(JobParameters params) {
+ int jobId = params.getJobId();
if (DEBUG) {
- Log.d(TAG, "onStartJob");
+ Log.d(TAG, "onStartJob " + jobId);
}
- mStopRequested = false;
- new IdleLoggingThread(params).start();
- return true; // Job is running on another thread
+ 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");
+ Log.d(TAG, "onStopJob " + jobId);
}
- mStopRequested = true;
- return true; // Requests job be re-scheduled.
+ 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 DexLogger getDexLogger() {
+ PackageManagerService pm = (PackageManagerService) ServiceManager.getService("package");
+ return pm.getDexManager().getDexLogger();
}
private class IdleLoggingThread extends Thread {
@@ -92,14 +146,13 @@
@Override
public void run() {
if (DEBUG) {
- Log.d(TAG, "Starting logging run");
+ Log.d(TAG, "Starting IdleLoggingJob run");
}
- PackageManagerService pm = (PackageManagerService) ServiceManager.getService("package");
- DexLogger dexLogger = pm.getDexManager().getDexLogger();
+ DexLogger dexLogger = getDexLogger();
for (String packageName : dexLogger.getAllPackagesWithDynamicCodeLoading()) {
- if (mStopRequested) {
- Log.w(TAG, "Stopping logging run at scheduler request");
+ if (mIdleLoggingStopRequested) {
+ Log.w(TAG, "Stopping IdleLoggingJob run at scheduler request");
return;
}
@@ -108,8 +161,128 @@
jobFinished(mParams, /* reschedule */ false);
if (DEBUG) {
- Log.d(TAG, "Finished logging run");
+ 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 DexLogger.
+ //
+ // 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 DexLogger.
+ //
+ // 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;
+ }
+
+ DexLogger dexLogger = getDexLogger();
+
+ 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));
+ }
+ dexLogger.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);
+ }
}
diff --git a/services/core/java/com/android/server/pm/dex/DexLogger.java b/services/core/java/com/android/server/pm/dex/DexLogger.java
index 78fa82c..59cc0cf 100644
--- a/services/core/java/com/android/server/pm/dex/DexLogger.java
+++ b/services/core/java/com/android/server/pm/dex/DexLogger.java
@@ -16,11 +16,15 @@
package com.android.server.pm.dex;
+import static com.android.server.pm.dex.PackageDynamicCodeLoading.FILE_TYPE_DEX;
+import static com.android.server.pm.dex.PackageDynamicCodeLoading.FILE_TYPE_NATIVE;
+
import android.content.pm.ApplicationInfo;
import android.content.pm.IPackageManager;
import android.content.pm.PackageInfo;
import android.os.FileUtils;
import android.os.RemoteException;
+import android.os.UserHandle;
import android.os.storage.StorageManager;
import android.util.ByteStringUtils;
import android.util.EventLog;
@@ -35,20 +39,23 @@
import com.android.server.pm.dex.PackageDynamicCodeLoading.PackageDynamicCode;
import java.io.File;
+import java.io.IOException;
import java.util.Map;
import java.util.Set;
/**
- * This class is responsible for logging data about secondary dex files.
- * The data logged includes hashes of the name and content of each file.
+ * This class is responsible for logging data about secondary dex files and, despite the name,
+ * native code executed from an app's private directory. The data logged includes hashes of the
+ * name and content of each file.
*/
public class DexLogger {
private static final String TAG = "DexLogger";
- // Event log tag & subtag used for SafetyNet logging of dynamic
- // code loading (DCL) - see b/63927552.
+ // Event log tag & subtags used for SafetyNet logging of dynamic code loading (DCL) -
+ // see b/63927552.
private static final int SNET_TAG = 0x534e4554;
- private static final String DCL_SUBTAG = "dcl";
+ private static final String DCL_DEX_SUBTAG = "dcl";
+ private static final String DCL_NATIVE_SUBTAG = "dcln";
private final IPackageManager mPackageManager;
private final PackageDynamicCodeLoading mPackageDynamicCodeLoading;
@@ -114,12 +121,11 @@
}
int storageFlags;
- if (appInfo.deviceProtectedDataDir != null
- && FileUtils.contains(appInfo.deviceProtectedDataDir, filePath)) {
- storageFlags = StorageManager.FLAG_STORAGE_DE;
- } else if (appInfo.credentialProtectedDataDir != null
- && FileUtils.contains(appInfo.credentialProtectedDataDir, filePath)) {
+
+ if (fileIsUnder(filePath, appInfo.credentialProtectedDataDir)) {
storageFlags = StorageManager.FLAG_STORAGE_CE;
+ } else if (fileIsUnder(filePath, appInfo.deviceProtectedDataDir)) {
+ storageFlags = StorageManager.FLAG_STORAGE_DE;
} else {
Slog.e(TAG, "Could not infer CE/DE storage for path " + filePath);
needWrite |= mPackageDynamicCodeLoading.removeFile(packageName, filePath, userId);
@@ -139,6 +145,9 @@
+ ": " + e.getMessage());
}
+ String subtag = fileInfo.mFileType == FILE_TYPE_DEX
+ ? DCL_DEX_SUBTAG
+ : DCL_NATIVE_SUBTAG;
String fileName = new File(filePath).getName();
String message = PackageUtils.computeSha256Digest(fileName.getBytes());
@@ -165,7 +174,7 @@
}
if (loadingUid != -1) {
- writeDclEvent(loadingUid, message);
+ writeDclEvent(subtag, loadingUid, message);
}
}
}
@@ -175,21 +184,58 @@
}
}
+ private boolean fileIsUnder(String filePath, String directoryPath) {
+ if (directoryPath == null) {
+ return false;
+ }
+
+ try {
+ return FileUtils.contains(new File(directoryPath).getCanonicalPath(),
+ new File(filePath).getCanonicalPath());
+ } catch (IOException e) {
+ return false;
+ }
+ }
+
@VisibleForTesting
PackageDynamicCode getPackageDynamicCodeInfo(String packageName) {
return mPackageDynamicCodeLoading.getPackageDynamicCodeInfo(packageName);
}
@VisibleForTesting
- void writeDclEvent(int uid, String message) {
- EventLog.writeEvent(SNET_TAG, DCL_SUBTAG, uid, message);
+ void writeDclEvent(String subtag, int uid, String message) {
+ EventLog.writeEvent(SNET_TAG, subtag, uid, message);
}
- void record(int loaderUserId, String dexPath,
- String owningPackageName, String loadingPackageName) {
+ void recordDex(int loaderUserId, String dexPath, String owningPackageName,
+ String loadingPackageName) {
if (mPackageDynamicCodeLoading.record(owningPackageName, dexPath,
- PackageDynamicCodeLoading.FILE_TYPE_DEX, loaderUserId,
- loadingPackageName)) {
+ FILE_TYPE_DEX, loaderUserId, loadingPackageName)) {
+ mPackageDynamicCodeLoading.maybeWriteAsync();
+ }
+ }
+
+ /**
+ * Record that an app running in the specified uid has executed native code from the file at
+ * {@link path}.
+ */
+ public void recordNative(int loadingUid, String path) {
+ String[] packages;
+ try {
+ packages = mPackageManager.getPackagesForUid(loadingUid);
+ if (packages == null || packages.length == 0) {
+ return;
+ }
+ } catch (RemoteException e) {
+ // Can't happen, we're local.
+ return;
+ }
+
+ String loadingPackageName = packages[0];
+ int loadingUserId = UserHandle.getUserId(loadingUid);
+
+ if (mPackageDynamicCodeLoading.record(loadingPackageName, path,
+ FILE_TYPE_NATIVE, loadingUserId, loadingPackageName)) {
mPackageDynamicCodeLoading.maybeWriteAsync();
}
}
diff --git a/services/core/java/com/android/server/pm/dex/DexManager.java b/services/core/java/com/android/server/pm/dex/DexManager.java
index b546836..1a2b115 100644
--- a/services/core/java/com/android/server/pm/dex/DexManager.java
+++ b/services/core/java/com/android/server/pm/dex/DexManager.java
@@ -235,7 +235,7 @@
continue;
}
- mDexLogger.record(loaderUserId, dexPath, searchResult.mOwningPackageName,
+ mDexLogger.recordDex(loaderUserId, dexPath, searchResult.mOwningPackageName,
loadingAppInfo.packageName);
if (classLoaderContexts != null) {
diff --git a/services/core/java/com/android/server/pm/dex/PackageDynamicCodeLoading.java b/services/core/java/com/android/server/pm/dex/PackageDynamicCodeLoading.java
index 6d4bc82..cc26c9b 100644
--- a/services/core/java/com/android/server/pm/dex/PackageDynamicCodeLoading.java
+++ b/services/core/java/com/android/server/pm/dex/PackageDynamicCodeLoading.java
@@ -53,6 +53,9 @@
// is represented in the text file format.)
static final int FILE_TYPE_DEX = 'D';
+ // Type code to indicate a secondary file containing native code.
+ static final int FILE_TYPE_NATIVE = 'N';
+
private static final String TAG = "PackageDynamicCodeLoading";
private static final String FILE_VERSION_HEADER = "DCL1";
@@ -107,7 +110,7 @@
*/
boolean record(String owningPackageName, String filePath, int fileType, int ownerUserId,
String loadingPackageName) {
- if (fileType != FILE_TYPE_DEX) {
+ if (!isValidFileType(fileType)) {
throw new IllegalArgumentException("Bad file type: " + fileType);
}
synchronized (mLock) {
@@ -120,6 +123,10 @@
}
}
+ private static boolean isValidFileType(int fileType) {
+ return fileType == FILE_TYPE_DEX || fileType == FILE_TYPE_NATIVE;
+ }
+
/**
* Return all packages that contain records of secondary dex files. (Note that data updates
* asynchronously, so {@link #getPackageDynamicCodeInfo} may still return null if passed
@@ -407,7 +414,7 @@
if (packages.length == 0) {
throw new IOException("Malformed line: " + line);
}
- if (type != FILE_TYPE_DEX) {
+ if (!isValidFileType(type)) {
throw new IOException("Unknown file type: " + line);
}
diff --git a/services/tests/servicestests/src/com/android/server/pm/dex/DexLoggerTests.java b/services/tests/servicestests/src/com/android/server/pm/dex/DexLoggerTests.java
index f817e8e..6da202b 100644
--- a/services/tests/servicestests/src/com/android/server/pm/dex/DexLoggerTests.java
+++ b/services/tests/servicestests/src/com/android/server/pm/dex/DexLoggerTests.java
@@ -16,8 +16,6 @@
package com.android.server.pm.dex;
-import static com.android.server.pm.dex.PackageDynamicCodeLoading.FILE_TYPE_DEX;
-
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.atMost;
@@ -26,10 +24,12 @@
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
import android.content.pm.ApplicationInfo;
import android.content.pm.IPackageManager;
import android.content.pm.PackageInfo;
+import android.os.UserHandle;
import android.os.storage.StorageManager;
import androidx.test.filters.SmallTest;
@@ -56,40 +56,44 @@
public class DexLoggerTests {
private static final String OWNING_PACKAGE_NAME = "package.name";
private static final String VOLUME_UUID = "volUuid";
- private static final String DEX_PATH = "/bar/foo.jar";
+ private static final String FILE_PATH = "/bar/foo.jar";
private static final int STORAGE_FLAGS = StorageManager.FLAG_STORAGE_DE;
private static final int OWNER_UID = 43;
private static final int OWNER_USER_ID = 44;
// Obtained via: echo -n "foo.jar" | sha256sum
- private static final String DEX_FILENAME_HASH =
+ private static final String FILENAME_HASH =
"91D7B844D7CC9673748FF057D8DC83972280FC28537D381AA42015A9CF214B9F";
- private static final byte[] CONTENT_HASH_BYTES = new byte[] {
- 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
- 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32
+ private static final byte[] CONTENT_HASH_BYTES = new byte[]{
+ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
+ 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32
};
private static final String CONTENT_HASH =
"0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F20";
private static final byte[] EMPTY_BYTES = {};
- @Rule public MockitoRule mockito = MockitoJUnit.rule().strictness(Strictness.STRICT_STUBS);
+ private static final String EXPECTED_MESSAGE_WITHOUT_CONTENT_HASH =
+ "dcl:" + FILENAME_HASH;
+ private static final String EXPECTED_MESSAGE_WITH_CONTENT_HASH =
+ EXPECTED_MESSAGE_WITHOUT_CONTENT_HASH + " " + CONTENT_HASH;
+ private static final String EXPECTED_MESSAGE_NATIVE_WITH_CONTENT_HASH =
+ "dcln:" + FILENAME_HASH + " " + CONTENT_HASH;
+
+ @Rule public MockitoRule mockito = MockitoJUnit.rule().strictness(Strictness.LENIENT);
@Mock IPackageManager mPM;
@Mock Installer mInstaller;
- private PackageDynamicCodeLoading mPackageDynamicCodeLoading;
private DexLogger mDexLogger;
private final ListMultimap<Integer, String> mMessagesForUid = ArrayListMultimap.create();
private boolean mWriteTriggered = false;
- private static final String EXPECTED_MESSAGE_WITH_CONTENT_HASH =
- DEX_FILENAME_HASH + " " + CONTENT_HASH;
@Before
public void setup() throws Exception {
// Disable actually attempting to do file writes.
- mPackageDynamicCodeLoading = new PackageDynamicCodeLoading() {
+ PackageDynamicCodeLoading packageDynamicCodeLoading = new PackageDynamicCodeLoading() {
@Override
void maybeWriteAsync() {
mWriteTriggered = true;
@@ -102,13 +106,13 @@
};
// For test purposes capture log messages as well as sending to the event log.
- mDexLogger = new DexLogger(mPM, mInstaller, mPackageDynamicCodeLoading) {
+ mDexLogger = new DexLogger(mPM, mInstaller, packageDynamicCodeLoading) {
@Override
- void writeDclEvent(int uid, String message) {
- super.writeDclEvent(uid, message);
- mMessagesForUid.put(uid, message);
- }
- };
+ void writeDclEvent(String subtag, int uid, String message) {
+ super.writeDclEvent(subtag, uid, message);
+ mMessagesForUid.put(uid, subtag + ":" + message);
+ }
+ };
// Make the owning package exist in our mock PackageManager.
ApplicationInfo appInfo = new ApplicationInfo();
@@ -124,9 +128,9 @@
@Test
public void testOneLoader_ownFile_withFileHash() throws Exception {
- whenFileIsHashed(DEX_PATH, doReturn(CONTENT_HASH_BYTES));
+ whenFileIsHashed(FILE_PATH, doReturn(CONTENT_HASH_BYTES));
- recordLoad(OWNING_PACKAGE_NAME, DEX_PATH);
+ recordLoad(OWNING_PACKAGE_NAME, FILE_PATH);
mDexLogger.logDynamicCodeLoading(OWNING_PACKAGE_NAME);
assertThat(mMessagesForUid.keys()).containsExactly(OWNER_UID);
@@ -139,13 +143,13 @@
@Test
public void testOneLoader_ownFile_noFileHash() throws Exception {
- whenFileIsHashed(DEX_PATH, doReturn(EMPTY_BYTES));
+ whenFileIsHashed(FILE_PATH, doReturn(EMPTY_BYTES));
- recordLoad(OWNING_PACKAGE_NAME, DEX_PATH);
+ recordLoad(OWNING_PACKAGE_NAME, FILE_PATH);
mDexLogger.logDynamicCodeLoading(OWNING_PACKAGE_NAME);
assertThat(mMessagesForUid.keys()).containsExactly(OWNER_UID);
- assertThat(mMessagesForUid).containsEntry(OWNER_UID, DEX_FILENAME_HASH);
+ assertThat(mMessagesForUid).containsEntry(OWNER_UID, EXPECTED_MESSAGE_WITHOUT_CONTENT_HASH);
// File should be removed from the DCL list, since we can't hash it.
assertThat(mWriteTriggered).isTrue();
@@ -154,13 +158,14 @@
@Test
public void testOneLoader_ownFile_hashingFails() throws Exception {
- whenFileIsHashed(DEX_PATH, doThrow(new InstallerException("Intentional failure for test")));
+ whenFileIsHashed(FILE_PATH,
+ doThrow(new InstallerException("Intentional failure for test")));
- recordLoad(OWNING_PACKAGE_NAME, DEX_PATH);
+ recordLoad(OWNING_PACKAGE_NAME, FILE_PATH);
mDexLogger.logDynamicCodeLoading(OWNING_PACKAGE_NAME);
assertThat(mMessagesForUid.keys()).containsExactly(OWNER_UID);
- assertThat(mMessagesForUid).containsEntry(OWNER_UID, DEX_FILENAME_HASH);
+ assertThat(mMessagesForUid).containsEntry(OWNER_UID, EXPECTED_MESSAGE_WITHOUT_CONTENT_HASH);
// File should be removed from the DCL list, since we can't hash it.
assertThat(mWriteTriggered).isTrue();
@@ -178,11 +183,23 @@
}
@Test
+ public void testOneLoader_pathTraversal() throws Exception {
+ String filePath = "/bar/../secret/foo.jar";
+ whenFileIsHashed(filePath, doReturn(CONTENT_HASH_BYTES));
+ setPackageUid(OWNING_PACKAGE_NAME, -1);
+
+ recordLoad(OWNING_PACKAGE_NAME, filePath);
+ mDexLogger.logDynamicCodeLoading(OWNING_PACKAGE_NAME);
+
+ assertThat(mMessagesForUid).isEmpty();
+ }
+
+ @Test
public void testOneLoader_differentOwner() throws Exception {
- whenFileIsHashed(DEX_PATH, doReturn(CONTENT_HASH_BYTES));
+ whenFileIsHashed(FILE_PATH, doReturn(CONTENT_HASH_BYTES));
setPackageUid("other.package.name", 1001);
- recordLoad("other.package.name", DEX_PATH);
+ recordLoad("other.package.name", FILE_PATH);
mDexLogger.logDynamicCodeLoading(OWNING_PACKAGE_NAME);
assertThat(mMessagesForUid.keys()).containsExactly(1001);
@@ -192,10 +209,10 @@
@Test
public void testOneLoader_differentOwner_uninstalled() throws Exception {
- whenFileIsHashed(DEX_PATH, doReturn(CONTENT_HASH_BYTES));
+ whenFileIsHashed(FILE_PATH, doReturn(CONTENT_HASH_BYTES));
setPackageUid("other.package.name", -1);
- recordLoad("other.package.name", DEX_PATH);
+ recordLoad("other.package.name", FILE_PATH);
mDexLogger.logDynamicCodeLoading(OWNING_PACKAGE_NAME);
assertThat(mMessagesForUid).isEmpty();
@@ -203,22 +220,38 @@
}
@Test
+ public void testNativeCodeLoad() throws Exception {
+ whenFileIsHashed(FILE_PATH, doReturn(CONTENT_HASH_BYTES));
+
+ recordLoadNative(FILE_PATH);
+ mDexLogger.logDynamicCodeLoading(OWNING_PACKAGE_NAME);
+
+ assertThat(mMessagesForUid.keys()).containsExactly(OWNER_UID);
+ assertThat(mMessagesForUid)
+ .containsEntry(OWNER_UID, EXPECTED_MESSAGE_NATIVE_WITH_CONTENT_HASH);
+
+ assertThat(mWriteTriggered).isFalse();
+ assertThat(mDexLogger.getAllPackagesWithDynamicCodeLoading())
+ .containsExactly(OWNING_PACKAGE_NAME);
+ }
+
+ @Test
public void testMultipleLoadersAndFiles() throws Exception {
String otherDexPath = "/bar/nosuchdir/foo.jar";
- whenFileIsHashed(DEX_PATH, doReturn(CONTENT_HASH_BYTES));
+ whenFileIsHashed(FILE_PATH, doReturn(CONTENT_HASH_BYTES));
whenFileIsHashed(otherDexPath, doReturn(EMPTY_BYTES));
setPackageUid("other.package.name1", 1001);
setPackageUid("other.package.name2", 1002);
- recordLoad("other.package.name1", DEX_PATH);
+ recordLoad("other.package.name1", FILE_PATH);
recordLoad("other.package.name1", otherDexPath);
- recordLoad("other.package.name2", DEX_PATH);
- recordLoad(OWNING_PACKAGE_NAME, DEX_PATH);
+ recordLoad("other.package.name2", FILE_PATH);
+ recordLoad(OWNING_PACKAGE_NAME, FILE_PATH);
mDexLogger.logDynamicCodeLoading(OWNING_PACKAGE_NAME);
assertThat(mMessagesForUid.keys()).containsExactly(1001, 1001, 1002, OWNER_UID);
assertThat(mMessagesForUid).containsEntry(1001, EXPECTED_MESSAGE_WITH_CONTENT_HASH);
- assertThat(mMessagesForUid).containsEntry(1001, DEX_FILENAME_HASH);
+ assertThat(mMessagesForUid).containsEntry(1001, EXPECTED_MESSAGE_WITHOUT_CONTENT_HASH);
assertThat(mMessagesForUid).containsEntry(1002, EXPECTED_MESSAGE_WITH_CONTENT_HASH);
assertThat(mMessagesForUid).containsEntry(OWNER_UID, EXPECTED_MESSAGE_WITH_CONTENT_HASH);
@@ -233,7 +266,7 @@
@Test
public void testUnknownOwner() {
reset(mPM);
- recordLoad(OWNING_PACKAGE_NAME, DEX_PATH);
+ recordLoad(OWNING_PACKAGE_NAME, FILE_PATH);
mDexLogger.logDynamicCodeLoading("other.package.name");
assertThat(mMessagesForUid).isEmpty();
@@ -244,7 +277,7 @@
@Test
public void testUninstalledPackage() {
reset(mPM);
- recordLoad(OWNING_PACKAGE_NAME, DEX_PATH);
+ recordLoad(OWNING_PACKAGE_NAME, FILE_PATH);
mDexLogger.logDynamicCodeLoading(OWNING_PACKAGE_NAME);
assertThat(mMessagesForUid).isEmpty();
@@ -262,7 +295,16 @@
}
private void recordLoad(String loadingPackageName, String dexPath) {
- mPackageDynamicCodeLoading.record(
- OWNING_PACKAGE_NAME, dexPath, FILE_TYPE_DEX, OWNER_USER_ID, loadingPackageName);
+ mDexLogger.recordDex(OWNER_USER_ID, dexPath, OWNING_PACKAGE_NAME, loadingPackageName);
+ mWriteTriggered = false;
+ }
+
+ private void recordLoadNative(String nativePath) throws Exception {
+ int loadingUid = UserHandle.getUid(OWNER_USER_ID, OWNER_UID);
+ String[] packageNames = { OWNING_PACKAGE_NAME };
+ when(mPM.getPackagesForUid(loadingUid)).thenReturn(packageNames);
+
+ mDexLogger.recordNative(loadingUid, nativePath);
+ mWriteTriggered = false;
}
}
diff --git a/tests/DexLoggerIntegrationTests/Android.mk b/tests/DexLoggerIntegrationTests/Android.mk
index ee2ec0a..979d13a 100644
--- a/tests/DexLoggerIntegrationTests/Android.mk
+++ b/tests/DexLoggerIntegrationTests/Android.mk
@@ -29,6 +29,35 @@
dexloggertest_jar := $(LOCAL_BUILT_MODULE)
+# Also build a native library that the test app can dynamically load
+
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_TAGS := tests
+LOCAL_MODULE := DexLoggerNativeTestLibrary
+LOCAL_MULTILIB := first
+LOCAL_SRC_FILES := src/cpp/com_android_dcl_Jni.cpp
+LOCAL_C_INCLUDES += \
+ $(JNI_H_INCLUDE)
+LOCAL_SDK_VERSION := 28
+LOCAL_NDK_STL_VARIANT := c++_static
+
+include $(BUILD_SHARED_LIBRARY)
+
+dexloggertest_so := $(LOCAL_BUILT_MODULE)
+
+# And a standalone native executable that we can exec.
+
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_TAGS := tests
+LOCAL_MODULE := DexLoggerNativeExecutable
+LOCAL_SRC_FILES := src/cpp/test_executable.cpp
+
+include $(BUILD_EXECUTABLE)
+
+dexloggertest_executable := $(LOCAL_BUILT_MODULE)
+
# Build the test app itself
include $(CLEAR_VARS)
@@ -37,14 +66,18 @@
LOCAL_PACKAGE_NAME := DexLoggerIntegrationTests
LOCAL_SDK_VERSION := current
LOCAL_COMPATIBILITY_SUITE := device-tests
-LOCAL_CERTIFICATE := platform
+LOCAL_CERTIFICATE := shared
LOCAL_SRC_FILES := $(call all-java-files-under, src/com/android/server/pm)
LOCAL_STATIC_JAVA_LIBRARIES := \
android-support-test \
truth-prebuilt \
-# This gets us the javalib.jar built by DexLoggerTestLibrary above.
-LOCAL_JAVA_RESOURCE_FILES := $(dexloggertest_jar)
+# This gets us the javalib.jar built by DexLoggerTestLibrary above as well as the various
+# native binaries.
+LOCAL_JAVA_RESOURCE_FILES := \
+ $(dexloggertest_jar) \
+ $(dexloggertest_so) \
+ $(dexloggertest_executable)
include $(BUILD_PACKAGE)
diff --git a/tests/DexLoggerIntegrationTests/src/com/android/server/pm/dex/DexLoggerIntegrationTests.java b/tests/DexLoggerIntegrationTests/src/com/android/server/pm/dex/DexLoggerIntegrationTests.java
index 75ee089..d68769b 100644
--- a/tests/DexLoggerIntegrationTests/src/com/android/server/pm/dex/DexLoggerIntegrationTests.java
+++ b/tests/DexLoggerIntegrationTests/src/com/android/server/pm/dex/DexLoggerIntegrationTests.java
@@ -17,6 +17,7 @@
package com.android.server.pm.dex;
import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
import android.app.UiAutomation;
import android.content.Context;
@@ -25,6 +26,7 @@
import android.support.test.InstrumentationRegistry;
import android.support.test.filters.LargeTest;
import android.util.EventLog;
+import android.util.EventLog.Event;
import dalvik.system.DexClassLoader;
@@ -65,14 +67,13 @@
// Event log tag used for SNET related events
private static final int SNET_TAG = 0x534e4554;
- // Subtag used to distinguish dynamic code loading events
- private static final String DCL_SUBTAG = "dcl";
+ // Subtags used to distinguish dynamic code loading events
+ private static final String DCL_DEX_SUBTAG = "dcl";
+ private static final String DCL_NATIVE_SUBTAG = "dcln";
- // All the tags we care about
- private static final int[] TAG_LIST = new int[] { SNET_TAG };
-
- // This is {@code DynamicCodeLoggingService#JOB_ID}
- private static final int DYNAMIC_CODE_LOGGING_JOB_ID = 2030028;
+ // These are job IDs from DynamicCodeLoggingService
+ private static final int IDLE_LOGGING_JOB_ID = 2030028;
+ private static final int AUDIT_WATCHING_JOB_ID = 203142925;
private static Context sContext;
private static int sMyUid;
@@ -89,15 +90,20 @@
// Without this the first test passes and others don't - we don't see new events in the
// log. The exact reason is unclear.
EventLog.writeEvent(SNET_TAG, "Dummy event");
+
+ // Audit log messages are throttled by the kernel (at the request of logd) to 5 per
+ // second, so running the tests too quickly in sequence means we lose some and get
+ // spurious failures. Sigh.
+ SystemClock.sleep(1000);
}
@Test
- public void testDexLoggerGeneratesEvents() throws Exception {
- File privateCopyFile = fileForJar("copied.jar");
+ public void testDexLoggerGeneratesEvents_standardClassLoader() throws Exception {
+ File privateCopyFile = privateFile("copied.jar");
// Obtained via "echo -n copied.jar | sha256sum"
String expectedNameHash =
"1B6C71DB26F36582867432CCA12FB6A517470C9F9AABE9198DD4C5C030D6DC0C";
- String expectedContentHash = copyAndHashJar(privateCopyFile);
+ String expectedContentHash = copyAndHashResource("/javalib.jar", privateCopyFile);
// Feed the jar to a class loader and make sure it contains what we expect.
ClassLoader parentClassLoader = sContext.getClass().getClassLoader();
@@ -107,18 +113,18 @@
// And make sure we log events about it
long previousEventNanos = mostRecentEventTimeNanos();
- runDexLogger();
+ runDynamicCodeLoggingJob(IDLE_LOGGING_JOB_ID);
- assertDclLoggedSince(previousEventNanos, expectedNameHash, expectedContentHash);
+ assertDclLoggedSince(previousEventNanos, DCL_DEX_SUBTAG,
+ expectedNameHash, expectedContentHash);
}
@Test
-
public void testDexLoggerGeneratesEvents_unknownClassLoader() throws Exception {
- File privateCopyFile = fileForJar("copied2.jar");
+ File privateCopyFile = privateFile("copied2.jar");
String expectedNameHash =
"202158B6A3169D78F1722487205A6B036B3F2F5653FDCFB4E74710611AC7EB93";
- String expectedContentHash = copyAndHashJar(privateCopyFile);
+ String expectedContentHash = copyAndHashResource("/javalib.jar", privateCopyFile);
// This time make sure an unknown class loader is an ancestor of the class loader we use.
ClassLoader knownClassLoader = sContext.getClass().getClassLoader();
@@ -129,22 +135,185 @@
// And make sure we log events about it
long previousEventNanos = mostRecentEventTimeNanos();
- runDexLogger();
+ runDynamicCodeLoggingJob(IDLE_LOGGING_JOB_ID);
- assertDclLoggedSince(previousEventNanos, expectedNameHash, expectedContentHash);
+ assertDclLoggedSince(previousEventNanos, DCL_DEX_SUBTAG,
+ expectedNameHash, expectedContentHash);
}
- private static File fileForJar(String name) {
- return new File(sContext.getDir("jars", Context.MODE_PRIVATE), name);
+ @Test
+ public void testDexLoggerGeneratesEvents_nativeLibrary() throws Exception {
+ File privateCopyFile = privateFile("copied.so");
+ String expectedNameHash =
+ "996223BAD4B4FE75C57A3DEC61DB9C0B38E0A7AD479FC95F33494F4BC55A0F0E";
+ String expectedContentHash =
+ copyAndHashResource("/DexLoggerNativeTestLibrary.so", privateCopyFile);
+
+ System.load(privateCopyFile.toString());
+
+ // Run the job to scan generated audit log entries
+ runDynamicCodeLoggingJob(AUDIT_WATCHING_JOB_ID);
+
+ // And then make sure we log events about it
+ long previousEventNanos = mostRecentEventTimeNanos();
+ runDynamicCodeLoggingJob(IDLE_LOGGING_JOB_ID);
+
+ assertDclLoggedSince(previousEventNanos, DCL_NATIVE_SUBTAG,
+ expectedNameHash, expectedContentHash);
}
- private static String copyAndHashJar(File copyTo) throws Exception {
+ @Test
+ public void testDexLoggerGeneratesEvents_nativeLibrary_escapedName() throws Exception {
+ // A file name with a space will be escaped in the audit log; verify we un-escape it
+ // correctly.
+ File privateCopyFile = privateFile("second copy.so");
+ String expectedNameHash =
+ "8C39990C560B4F36F83E208E279F678746FE23A790E4C50F92686584EA2041CA";
+ String expectedContentHash =
+ copyAndHashResource("/DexLoggerNativeTestLibrary.so", privateCopyFile);
+
+ System.load(privateCopyFile.toString());
+
+ // Run the job to scan generated audit log entries
+ runDynamicCodeLoggingJob(AUDIT_WATCHING_JOB_ID);
+
+ // And then make sure we log events about it
+ long previousEventNanos = mostRecentEventTimeNanos();
+ runDynamicCodeLoggingJob(IDLE_LOGGING_JOB_ID);
+
+ assertDclLoggedSince(previousEventNanos, DCL_NATIVE_SUBTAG,
+ expectedNameHash, expectedContentHash);
+ }
+
+ @Test
+ public void testDexLoggerGeneratesEvents_nativeExecutable() throws Exception {
+ File privateCopyFile = privateFile("test_executable");
+ String expectedNameHash =
+ "3FBEC3F925A132D18F347F11AE9A5BB8DE1238828F8B4E064AA86EB68BD46DCF";
+ String expectedContentHash =
+ copyAndHashResource("/DexLoggerNativeExecutable", privateCopyFile);
+ assertThat(privateCopyFile.setExecutable(true)).isTrue();
+
+ Process process = Runtime.getRuntime().exec(privateCopyFile.toString());
+ int exitCode = process.waitFor();
+ assertThat(exitCode).isEqualTo(0);
+
+ // Run the job to scan generated audit log entries
+ runDynamicCodeLoggingJob(AUDIT_WATCHING_JOB_ID);
+
+ // And then make sure we log events about it
+ long previousEventNanos = mostRecentEventTimeNanos();
+ runDynamicCodeLoggingJob(IDLE_LOGGING_JOB_ID);
+
+ assertDclLoggedSince(previousEventNanos, DCL_NATIVE_SUBTAG,
+ expectedNameHash, expectedContentHash);
+ }
+
+ @Test
+ public void testDexLoggerGeneratesEvents_spoofed_validFile() throws Exception {
+ File privateCopyFile = privateFile("spoofed");
+
+ String expectedContentHash =
+ copyAndHashResource("/DexLoggerNativeExecutable", privateCopyFile);
+
+ EventLog.writeEvent(EventLog.getTagCode("auditd"),
+ "type=1400 avc: granted { execute_no_trans } "
+ + "path=\"" + privateCopyFile + "\" "
+ + "scontext=u:r:untrusted_app_27: "
+ + "tcontext=u:object_r:app_data_file: "
+ + "tclass=file ");
+
+ String expectedNameHash =
+ "1CF36F503A02877BB775DC23C1C5A47A95F2684B6A1A83B11795B856D88861E3";
+
+ // Run the job to scan generated audit log entries
+ runDynamicCodeLoggingJob(AUDIT_WATCHING_JOB_ID);
+
+ // And then make sure we log events about it
+ long previousEventNanos = mostRecentEventTimeNanos();
+ runDynamicCodeLoggingJob(IDLE_LOGGING_JOB_ID);
+
+ assertDclLoggedSince(previousEventNanos, DCL_NATIVE_SUBTAG,
+ expectedNameHash, expectedContentHash);
+ }
+
+ @Test
+ public void testDexLoggerGeneratesEvents_spoofed_pathTraversal() throws Exception {
+ File privateDir = privateFile("x").getParentFile();
+
+ // Transform /a/b/c -> /a/b/c/../../.. so we get back to the root
+ File pathTraversalToRoot = privateDir;
+ File root = new File("/");
+ while (!privateDir.equals(root)) {
+ pathTraversalToRoot = new File(pathTraversalToRoot, "..");
+ privateDir = privateDir.getParentFile();
+ }
+
+ File spoofedFile = new File(pathTraversalToRoot, "dev/urandom");
+
+ assertWithMessage("Expected " + spoofedFile + " to be readable")
+ .that(spoofedFile.canRead()).isTrue();
+
+ EventLog.writeEvent(EventLog.getTagCode("auditd"),
+ "type=1400 avc: granted { execute_no_trans } "
+ + "path=\"" + spoofedFile + "\" "
+ + "scontext=u:r:untrusted_app_27: "
+ + "tcontext=u:object_r:app_data_file: "
+ + "tclass=file ");
+
+ String expectedNameHash =
+ "65528FE876BD676B0DFCC9A8ACA8988E026766F99EEC1E1FB48F46B2F635E225";
+
+ // Run the job to scan generated audit log entries
+ runDynamicCodeLoggingJob(AUDIT_WATCHING_JOB_ID);
+
+ // And then trigger generating DCL events
+ long previousEventNanos = mostRecentEventTimeNanos();
+ runDynamicCodeLoggingJob(IDLE_LOGGING_JOB_ID);
+
+ assertNoDclLoggedSince(previousEventNanos, DCL_NATIVE_SUBTAG, expectedNameHash);
+ }
+
+ @Test
+ public void testDexLoggerGeneratesEvents_spoofed_otherAppFile() throws Exception {
+ File ourPath = sContext.getDatabasePath("android_pay");
+ File targetPath = new File(ourPath.toString()
+ .replace("com.android.frameworks.dexloggertest", "com.google.android.gms"));
+
+ assertWithMessage("Expected " + targetPath + " to not be readable")
+ .that(targetPath.canRead()).isFalse();
+
+ EventLog.writeEvent(EventLog.getTagCode("auditd"),
+ "type=1400 avc: granted { execute_no_trans } "
+ + "path=\"" + targetPath + "\" "
+ + "scontext=u:r:untrusted_app_27: "
+ + "tcontext=u:object_r:app_data_file: "
+ + "tclass=file ");
+
+ String expectedNameHash =
+ "CBE04E8AB9E7199FC19CBAAF9C774B88E56B3B19E823F2251693380AD6F515E6";
+
+ // Run the job to scan generated audit log entries
+ runDynamicCodeLoggingJob(AUDIT_WATCHING_JOB_ID);
+
+ // And then trigger generating DCL events
+ long previousEventNanos = mostRecentEventTimeNanos();
+ runDynamicCodeLoggingJob(IDLE_LOGGING_JOB_ID);
+
+ assertNoDclLoggedSince(previousEventNanos, DCL_NATIVE_SUBTAG, expectedNameHash);
+ }
+
+ private static File privateFile(String name) {
+ return new File(sContext.getDir("dcl", Context.MODE_PRIVATE), name);
+ }
+
+ private static String copyAndHashResource(String resourcePath, File copyTo) throws Exception {
MessageDigest hasher = MessageDigest.getInstance("SHA-256");
// Copy the jar from our Java resources to a private data directory
Class<?> thisClass = DexLoggerIntegrationTests.class;
- try (InputStream input = thisClass.getResourceAsStream("/javalib.jar");
- OutputStream output = new FileOutputStream(copyTo)) {
+ try (InputStream input = thisClass.getResourceAsStream(resourcePath);
+ OutputStream output = new FileOutputStream(copyTo)) {
byte[] buffer = new byte[1024];
while (true) {
int numRead = input.read(buffer);
@@ -166,24 +335,18 @@
return formatter.toString();
}
- private static long mostRecentEventTimeNanos() throws Exception {
- List<EventLog.Event> events = new ArrayList<>();
-
- EventLog.readEvents(TAG_LIST, events);
- return events.isEmpty() ? 0 : events.get(events.size() - 1).getTimeNanos();
- }
-
- private static void runDexLogger() throws Exception {
- // This forces {@code DynamicCodeLoggingService} to start now.
- runCommand("cmd jobscheduler run -f android " + DYNAMIC_CODE_LOGGING_JOB_ID);
+ private static void runDynamicCodeLoggingJob(int jobId) throws Exception {
+ // This forces the DynamicCodeLoggingService job to start now.
+ runCommand("cmd jobscheduler run -f android " + jobId);
// Wait for the job to have run.
long startTime = SystemClock.elapsedRealtime();
while (true) {
String response = runCommand(
- "cmd jobscheduler get-job-state android " + DYNAMIC_CODE_LOGGING_JOB_ID);
+ "cmd jobscheduler get-job-state android " + jobId);
if (!response.contains("pending") && !response.contains("active")) {
break;
}
+ // Don't wait forever - if it's taken > 10s then something is very wrong.
if (SystemClock.elapsedRealtime() - startTime > TimeUnit.SECONDS.toMillis(10)) {
throw new AssertionError("Job has not completed: " + response);
}
@@ -208,37 +371,68 @@
return response.toString("UTF-8");
}
- private static void assertDclLoggedSince(long previousEventNanos, String expectedNameHash,
- String expectedContentHash) throws Exception {
- List<EventLog.Event> events = new ArrayList<>();
- EventLog.readEvents(TAG_LIST, events);
- int found = 0;
- for (EventLog.Event event : events) {
+ private static long mostRecentEventTimeNanos() throws Exception {
+ List<Event> events = readSnetEvents();
+ return events.isEmpty() ? 0 : events.get(events.size() - 1).getTimeNanos();
+ }
+
+ private static void assertDclLoggedSince(long previousEventNanos, String expectedSubTag,
+ String expectedNameHash, String expectedContentHash) throws Exception {
+ List<String> messages =
+ findMatchingEvents(previousEventNanos, expectedSubTag, expectedNameHash);
+
+ assertWithMessage("Expected exactly one matching log entry").that(messages).hasSize(1);
+ assertThat(messages.get(0)).endsWith(expectedContentHash);
+ }
+
+ private static void assertNoDclLoggedSince(long previousEventNanos, String expectedSubTag,
+ String expectedNameHash) throws Exception {
+ List<String> messages =
+ findMatchingEvents(previousEventNanos, expectedSubTag, expectedNameHash);
+
+ assertWithMessage("Expected no matching log entries").that(messages).isEmpty();
+ }
+
+ private static List<String> findMatchingEvents(long previousEventNanos, String expectedSubTag,
+ String expectedNameHash) throws Exception {
+ List<String> messages = new ArrayList<>();
+
+ for (Event event : readSnetEvents()) {
if (event.getTimeNanos() <= previousEventNanos) {
continue;
}
- Object[] data = (Object[]) event.getData();
- // We only care about DCL events that we generated.
- String subTag = (String) data[0];
- if (!DCL_SUBTAG.equals(subTag)) {
+ Object data = event.getData();
+ if (!(data instanceof Object[])) {
continue;
}
- int uid = (int) data[1];
+ Object[] fields = (Object[]) data;
+
+ // We only care about DCL events that we generated.
+ String subTag = (String) fields[0];
+ if (!expectedSubTag.equals(subTag)) {
+ continue;
+ }
+ int uid = (int) fields[1];
if (uid != sMyUid) {
continue;
}
- String message = (String) data[2];
+ String message = (String) fields[2];
if (!message.startsWith(expectedNameHash)) {
continue;
}
- assertThat(message).endsWith(expectedContentHash);
- ++found;
+ messages.add(message);
+ //assertThat(message).endsWith(expectedContentHash);
}
+ return messages;
+ }
- assertThat(found).isEqualTo(1);
+ private static List<Event> readSnetEvents() throws Exception {
+ List<Event> events = new ArrayList<>();
+ EventLog.readEvents(new int[] { SNET_TAG }, events);
+ return events;
}
/**
diff --git a/tests/DexLoggerIntegrationTests/src/cpp/com_android_dcl_Jni.cpp b/tests/DexLoggerIntegrationTests/src/cpp/com_android_dcl_Jni.cpp
new file mode 100644
index 0000000..0608883
--- /dev/null
+++ b/tests/DexLoggerIntegrationTests/src/cpp/com_android_dcl_Jni.cpp
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2019 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.
+ */
+
+#include "jni.h"
+
+extern "C" jint JNI_OnLoad(JavaVM* /* vm */, void* /* reserved */)
+{
+ return JNI_VERSION_1_6;
+}
diff --git a/tests/DexLoggerIntegrationTests/src/cpp/test_executable.cpp b/tests/DexLoggerIntegrationTests/src/cpp/test_executable.cpp
new file mode 100644
index 0000000..ad025e6
--- /dev/null
+++ b/tests/DexLoggerIntegrationTests/src/cpp/test_executable.cpp
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2019 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.
+ */
+
+int main() {
+ // This program just has to run, it doesn't need to do anything. So we don't.
+ return 0;
+}