diff options
9 files changed, 669 insertions, 132 deletions
diff --git a/services/core/java/com/android/server/pm/DynamicCodeLoggingService.java b/services/core/java/com/android/server/pm/DynamicCodeLoggingService.java index 2ae424dd4b1b..5b765dfee3a4 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.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.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 static final boolean DEBUG = false; - private volatile boolean mStopRequested = false; + private static final int IDLE_LOGGING_JOB_ID = 2030028; + private static final int AUDIT_WATCHING_JOB_ID = 203142925; - private static final boolean DEBUG = false; + 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); + } + 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; } - mStopRequested = false; - new IdleLoggingThread(params).start(); - return true; // Job is running on another thread } @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 @@ public class DynamicCodeLoggingService extends JobService { @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 @@ public class DynamicCodeLoggingService extends JobService { 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 78fa82c6bcdd..59cc0cfeef45 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.DynamicCodeFile; 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 @@ public class DexLogger { } 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 @@ public class DexLogger { + ": " + 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 @@ public class DexLogger { } if (loadingUid != -1) { - writeDclEvent(loadingUid, message); + writeDclEvent(subtag, loadingUid, message); } } } @@ -175,21 +184,58 @@ public class DexLogger { } } + 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 b54683673e7b..1a2b11559446 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 @@ public class DexManager { 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 6d4bc8291611..cc26c9b5f76c 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 @@ class PackageDynamicCodeLoading extends AbstractStatsBase<Void> { // 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 @@ class PackageDynamicCodeLoading extends AbstractStatsBase<Void> { */ 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 @@ class PackageDynamicCodeLoading extends AbstractStatsBase<Void> { } } + 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 @@ class PackageDynamicCodeLoading extends AbstractStatsBase<Void> { 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 f817e8e33b31..6da202b93065 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.doThrow; 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 @@ import org.mockito.stubbing.Stubber; 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 @@ public class DexLoggerTests { }; // 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 @@ public class DexLoggerTests { @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 @@ public class DexLoggerTests { @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 @@ public class DexLoggerTests { @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 @@ public class DexLoggerTests { } @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 @@ public class DexLoggerTests { @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 @@ public class DexLoggerTests { } @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 @@ public class DexLoggerTests { @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 @@ public class DexLoggerTests { @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 @@ public class DexLoggerTests { } 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 ee2ec0a80b03..979d13ac9405 100644 --- a/tests/DexLoggerIntegrationTests/Android.mk +++ b/tests/DexLoggerIntegrationTests/Android.mk @@ -29,6 +29,35 @@ include $(BUILD_JAVA_LIBRARY) 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_MODULE_TAGS := tests 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 75ee0896c23a..d68769b378b9 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.os.SystemClock; 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 @@ public final class DexLoggerIntegrationTests { // 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 @@ public final class DexLoggerIntegrationTests { // 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 @@ public final class DexLoggerIntegrationTests { // 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 @@ public final class DexLoggerIntegrationTests { // And make sure we log events about it long previousEventNanos = mostRecentEventTimeNanos(); - runDexLogger(); + runDynamicCodeLoggingJob(IDLE_LOGGING_JOB_ID); + + assertDclLoggedSince(previousEventNanos, DCL_DEX_SUBTAG, + expectedNameHash, expectedContentHash); + } + + @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, expectedNameHash, expectedContentHash); + assertDclLoggedSince(previousEventNanos, DCL_NATIVE_SUBTAG, + expectedNameHash, expectedContentHash); } - private static File fileForJar(String name) { - return new File(sContext.getDir("jars", Context.MODE_PRIVATE), name); + @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); } - private static String copyAndHashJar(File copyTo) throws Exception { + @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 @@ public final class DexLoggerIntegrationTests { 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 @@ public final class DexLoggerIntegrationTests { 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(); + + Object data = event.getData(); + if (!(data instanceof Object[])) { + continue; + } + Object[] fields = (Object[]) data; // We only care about DCL events that we generated. - String subTag = (String) data[0]; - if (!DCL_SUBTAG.equals(subTag)) { + String subTag = (String) fields[0]; + if (!expectedSubTag.equals(subTag)) { continue; } - int uid = (int) data[1]; + 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 000000000000..060888310b51 --- /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 000000000000..ad025e696dec --- /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; +} |