Alan Stokes | 761d618 | 2017-12-19 11:48:19 +0000 | [diff] [blame] | 1 | /* |
| 2 | * Copyright 2017 The Android Open Source Project |
| 3 | * |
| 4 | * Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | * you may not use this file except in compliance with the License. |
| 6 | * You may obtain a copy of the License at |
| 7 | * |
| 8 | * http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | * |
| 10 | * Unless required by applicable law or agreed to in writing, software |
| 11 | * distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | * See the License for the specific language governing permissions and |
| 14 | * limitations under the License. |
| 15 | */ |
| 16 | |
Alan Stokes | 55d11f6 | 2018-01-09 10:04:21 +0000 | [diff] [blame] | 17 | package com.android.server.pm.dex; |
Alan Stokes | 761d618 | 2017-12-19 11:48:19 +0000 | [diff] [blame] | 18 | |
| 19 | import static com.google.common.truth.Truth.assertThat; |
Alan Stokes | e9d3314 | 2018-12-19 09:31:21 +0000 | [diff] [blame] | 20 | import static com.google.common.truth.Truth.assertWithMessage; |
Alan Stokes | 761d618 | 2017-12-19 11:48:19 +0000 | [diff] [blame] | 21 | |
Alan Stokes | be9ec97 | 2018-12-10 14:07:47 +0000 | [diff] [blame] | 22 | import android.app.UiAutomation; |
Alan Stokes | 761d618 | 2017-12-19 11:48:19 +0000 | [diff] [blame] | 23 | import android.content.Context; |
Alan Stokes | a303953 | 2019-01-25 14:07:23 +0000 | [diff] [blame] | 24 | import android.os.Build; |
Alan Stokes | be9ec97 | 2018-12-10 14:07:47 +0000 | [diff] [blame] | 25 | import android.os.ParcelFileDescriptor; |
| 26 | import android.os.SystemClock; |
Alan Stokes | 761d618 | 2017-12-19 11:48:19 +0000 | [diff] [blame] | 27 | import android.support.test.InstrumentationRegistry; |
Alan Stokes | 55d11f6 | 2018-01-09 10:04:21 +0000 | [diff] [blame] | 28 | import android.support.test.filters.LargeTest; |
Alan Stokes | 761d618 | 2017-12-19 11:48:19 +0000 | [diff] [blame] | 29 | import android.util.EventLog; |
Alan Stokes | e9d3314 | 2018-12-19 09:31:21 +0000 | [diff] [blame] | 30 | import android.util.EventLog.Event; |
Alan Stokes | be9ec97 | 2018-12-10 14:07:47 +0000 | [diff] [blame] | 31 | |
Alan Stokes | 761d618 | 2017-12-19 11:48:19 +0000 | [diff] [blame] | 32 | import dalvik.system.DexClassLoader; |
| 33 | |
Alan Stokes | 761d618 | 2017-12-19 11:48:19 +0000 | [diff] [blame] | 34 | import org.junit.Before; |
| 35 | import org.junit.BeforeClass; |
| 36 | import org.junit.Test; |
| 37 | import org.junit.runner.RunWith; |
| 38 | import org.junit.runners.JUnit4; |
| 39 | |
Alan Stokes | be9ec97 | 2018-12-10 14:07:47 +0000 | [diff] [blame] | 40 | import java.io.ByteArrayOutputStream; |
Alan Stokes | 761d618 | 2017-12-19 11:48:19 +0000 | [diff] [blame] | 41 | import java.io.File; |
| 42 | import java.io.FileOutputStream; |
| 43 | import java.io.InputStream; |
| 44 | import java.io.OutputStream; |
| 45 | import java.security.MessageDigest; |
| 46 | import java.util.ArrayList; |
| 47 | import java.util.Formatter; |
| 48 | import java.util.List; |
Alan Stokes | be9ec97 | 2018-12-10 14:07:47 +0000 | [diff] [blame] | 49 | import java.util.concurrent.TimeUnit; |
Alan Stokes | 761d618 | 2017-12-19 11:48:19 +0000 | [diff] [blame] | 50 | |
| 51 | /** |
Alan Stokes | 8d3b7a4 | 2019-02-19 17:14:42 +0000 | [diff] [blame] | 52 | * Integration tests for {@link DynamicCodeLogger}. |
Alan Stokes | 761d618 | 2017-12-19 11:48:19 +0000 | [diff] [blame] | 53 | * |
| 54 | * The setup for the test dynamically loads code in a jar extracted |
| 55 | * from our assets (a secondary dex file). |
| 56 | * |
Alan Stokes | be9ec97 | 2018-12-10 14:07:47 +0000 | [diff] [blame] | 57 | * We then use shell commands to trigger dynamic code logging (and wait |
Alan Stokes | 96efc028 | 2019-02-05 15:10:36 +0000 | [diff] [blame] | 58 | * for it to complete). This causes DynamicCodeLogger to log the hash of the |
Alan Stokes | be9ec97 | 2018-12-10 14:07:47 +0000 | [diff] [blame] | 59 | * file's name and content. We verify that this message appears in |
| 60 | * the event log. |
Alan Stokes | 761d618 | 2017-12-19 11:48:19 +0000 | [diff] [blame] | 61 | * |
Alan Stokes | 8d3b7a4 | 2019-02-19 17:14:42 +0000 | [diff] [blame] | 62 | * Run with "atest DynamicCodeLoggerIntegrationTests". |
Alan Stokes | 761d618 | 2017-12-19 11:48:19 +0000 | [diff] [blame] | 63 | */ |
Alan Stokes | 55d11f6 | 2018-01-09 10:04:21 +0000 | [diff] [blame] | 64 | @LargeTest |
Alan Stokes | 761d618 | 2017-12-19 11:48:19 +0000 | [diff] [blame] | 65 | @RunWith(JUnit4.class) |
Alan Stokes | 8d3b7a4 | 2019-02-19 17:14:42 +0000 | [diff] [blame] | 66 | public final class DynamicCodeLoggerIntegrationTests { |
Alan Stokes | 761d618 | 2017-12-19 11:48:19 +0000 | [diff] [blame] | 67 | |
Alan Stokes | 206a69d | 2019-02-04 15:32:12 +0000 | [diff] [blame] | 68 | private static final String SHA_256 = "SHA-256"; |
| 69 | |
Alan Stokes | 1e24bf6 | 2018-01-05 14:36:00 +0000 | [diff] [blame] | 70 | // Event log tag used for SNET related events |
Alan Stokes | 761d618 | 2017-12-19 11:48:19 +0000 | [diff] [blame] | 71 | private static final int SNET_TAG = 0x534e4554; |
Alan Stokes | be9ec97 | 2018-12-10 14:07:47 +0000 | [diff] [blame] | 72 | |
Alan Stokes | e9d3314 | 2018-12-19 09:31:21 +0000 | [diff] [blame] | 73 | // Subtags used to distinguish dynamic code loading events |
| 74 | private static final String DCL_DEX_SUBTAG = "dcl"; |
| 75 | private static final String DCL_NATIVE_SUBTAG = "dcln"; |
Alan Stokes | 761d618 | 2017-12-19 11:48:19 +0000 | [diff] [blame] | 76 | |
Alan Stokes | e9d3314 | 2018-12-19 09:31:21 +0000 | [diff] [blame] | 77 | // These are job IDs from DynamicCodeLoggingService |
| 78 | private static final int IDLE_LOGGING_JOB_ID = 2030028; |
| 79 | private static final int AUDIT_WATCHING_JOB_ID = 203142925; |
Alan Stokes | be9ec97 | 2018-12-10 14:07:47 +0000 | [diff] [blame] | 80 | |
Alan Stokes | 206a69d | 2019-02-04 15:32:12 +0000 | [diff] [blame] | 81 | // For tests that rely on parsing audit logs, how often to retry. (There are many reasons why |
| 82 | // we might not see the audit logs, including throttling and delays in log generation, so to |
| 83 | // avoid flakiness we run these tests multiple times, allowing progressively longer between |
| 84 | // code loading and checking the logs on each try.) |
| 85 | private static final int AUDIT_LOG_RETRIES = 10; |
| 86 | private static final int RETRY_DELAY_MS = 2_000; |
| 87 | |
Alan Stokes | be9ec97 | 2018-12-10 14:07:47 +0000 | [diff] [blame] | 88 | private static Context sContext; |
| 89 | private static int sMyUid; |
Alan Stokes | 761d618 | 2017-12-19 11:48:19 +0000 | [diff] [blame] | 90 | |
| 91 | @BeforeClass |
Alan Stokes | be9ec97 | 2018-12-10 14:07:47 +0000 | [diff] [blame] | 92 | public static void setUpAll() { |
| 93 | sContext = InstrumentationRegistry.getTargetContext(); |
| 94 | sMyUid = android.os.Process.myUid(); |
| 95 | } |
| 96 | |
| 97 | @Before |
| 98 | public void primeEventLog() { |
| 99 | // Force a round trip to logd to make sure everything is up to date. |
| 100 | // Without this the first test passes and others don't - we don't see new events in the |
| 101 | // log. The exact reason is unclear. |
| 102 | EventLog.writeEvent(SNET_TAG, "Dummy event"); |
Alan Stokes | e9d3314 | 2018-12-19 09:31:21 +0000 | [diff] [blame] | 103 | |
| 104 | // Audit log messages are throttled by the kernel (at the request of logd) to 5 per |
| 105 | // second, so running the tests too quickly in sequence means we lose some and get |
| 106 | // spurious failures. Sigh. |
| 107 | SystemClock.sleep(1000); |
Alan Stokes | be9ec97 | 2018-12-10 14:07:47 +0000 | [diff] [blame] | 108 | } |
| 109 | |
| 110 | @Test |
Alan Stokes | 96efc028 | 2019-02-05 15:10:36 +0000 | [diff] [blame] | 111 | public void testGeneratesEvents_standardClassLoader() throws Exception { |
Alan Stokes | e9d3314 | 2018-12-19 09:31:21 +0000 | [diff] [blame] | 112 | File privateCopyFile = privateFile("copied.jar"); |
Alan Stokes | be9ec97 | 2018-12-10 14:07:47 +0000 | [diff] [blame] | 113 | // Obtained via "echo -n copied.jar | sha256sum" |
| 114 | String expectedNameHash = |
| 115 | "1B6C71DB26F36582867432CCA12FB6A517470C9F9AABE9198DD4C5C030D6DC0C"; |
Alan Stokes | e9d3314 | 2018-12-19 09:31:21 +0000 | [diff] [blame] | 116 | String expectedContentHash = copyAndHashResource("/javalib.jar", privateCopyFile); |
Alan Stokes | be9ec97 | 2018-12-10 14:07:47 +0000 | [diff] [blame] | 117 | |
| 118 | // Feed the jar to a class loader and make sure it contains what we expect. |
| 119 | ClassLoader parentClassLoader = sContext.getClass().getClassLoader(); |
| 120 | ClassLoader loader = |
| 121 | new DexClassLoader(privateCopyFile.toString(), null, null, parentClassLoader); |
| 122 | loader.loadClass("com.android.dcl.Simple"); |
| 123 | |
| 124 | // And make sure we log events about it |
| 125 | long previousEventNanos = mostRecentEventTimeNanos(); |
Alan Stokes | e9d3314 | 2018-12-19 09:31:21 +0000 | [diff] [blame] | 126 | runDynamicCodeLoggingJob(IDLE_LOGGING_JOB_ID); |
Alan Stokes | be9ec97 | 2018-12-10 14:07:47 +0000 | [diff] [blame] | 127 | |
Alan Stokes | e9d3314 | 2018-12-19 09:31:21 +0000 | [diff] [blame] | 128 | assertDclLoggedSince(previousEventNanos, DCL_DEX_SUBTAG, |
| 129 | expectedNameHash, expectedContentHash); |
Alan Stokes | be9ec97 | 2018-12-10 14:07:47 +0000 | [diff] [blame] | 130 | } |
| 131 | |
| 132 | @Test |
Alan Stokes | 96efc028 | 2019-02-05 15:10:36 +0000 | [diff] [blame] | 133 | public void testGeneratesEvents_unknownClassLoader() throws Exception { |
Alan Stokes | e9d3314 | 2018-12-19 09:31:21 +0000 | [diff] [blame] | 134 | File privateCopyFile = privateFile("copied2.jar"); |
Alan Stokes | be9ec97 | 2018-12-10 14:07:47 +0000 | [diff] [blame] | 135 | String expectedNameHash = |
| 136 | "202158B6A3169D78F1722487205A6B036B3F2F5653FDCFB4E74710611AC7EB93"; |
Alan Stokes | e9d3314 | 2018-12-19 09:31:21 +0000 | [diff] [blame] | 137 | String expectedContentHash = copyAndHashResource("/javalib.jar", privateCopyFile); |
Alan Stokes | be9ec97 | 2018-12-10 14:07:47 +0000 | [diff] [blame] | 138 | |
| 139 | // This time make sure an unknown class loader is an ancestor of the class loader we use. |
| 140 | ClassLoader knownClassLoader = sContext.getClass().getClassLoader(); |
| 141 | ClassLoader unknownClassLoader = new UnknownClassLoader(knownClassLoader); |
| 142 | ClassLoader loader = |
| 143 | new DexClassLoader(privateCopyFile.toString(), null, null, unknownClassLoader); |
| 144 | loader.loadClass("com.android.dcl.Simple"); |
| 145 | |
| 146 | // And make sure we log events about it |
| 147 | long previousEventNanos = mostRecentEventTimeNanos(); |
Alan Stokes | e9d3314 | 2018-12-19 09:31:21 +0000 | [diff] [blame] | 148 | runDynamicCodeLoggingJob(IDLE_LOGGING_JOB_ID); |
Alan Stokes | be9ec97 | 2018-12-10 14:07:47 +0000 | [diff] [blame] | 149 | |
Alan Stokes | e9d3314 | 2018-12-19 09:31:21 +0000 | [diff] [blame] | 150 | assertDclLoggedSince(previousEventNanos, DCL_DEX_SUBTAG, |
| 151 | expectedNameHash, expectedContentHash); |
Alan Stokes | be9ec97 | 2018-12-10 14:07:47 +0000 | [diff] [blame] | 152 | } |
| 153 | |
Alan Stokes | e9d3314 | 2018-12-19 09:31:21 +0000 | [diff] [blame] | 154 | @Test |
Alan Stokes | 96efc028 | 2019-02-05 15:10:36 +0000 | [diff] [blame] | 155 | public void testGeneratesEvents_nativeLibrary() throws Exception { |
Alan Stokes | 206a69d | 2019-02-04 15:32:12 +0000 | [diff] [blame] | 156 | new TestNativeCodeWithRetries() { |
| 157 | @Override |
| 158 | protected void loadNativeCode(int tryNumber) throws Exception { |
| 159 | // We need to use a different file name for each retry, because once a file is |
| 160 | // loaded, re-loading it has no effect. |
| 161 | String privateCopyName = "copied" + tryNumber + ".so"; |
| 162 | File privateCopyFile = privateFile(privateCopyName); |
| 163 | mExpectedNameHash = hashOf(privateCopyName); |
| 164 | mExpectedContentHash = copyAndHashResource( |
Alan Stokes | 8d3b7a4 | 2019-02-19 17:14:42 +0000 | [diff] [blame] | 165 | libraryPath("DynamicCodeLoggerNativeTestLibrary.so"), privateCopyFile); |
Alan Stokes | e9d3314 | 2018-12-19 09:31:21 +0000 | [diff] [blame] | 166 | |
Alan Stokes | 206a69d | 2019-02-04 15:32:12 +0000 | [diff] [blame] | 167 | System.load(privateCopyFile.toString()); |
| 168 | } |
| 169 | }.runTest(); |
Alan Stokes | be9ec97 | 2018-12-10 14:07:47 +0000 | [diff] [blame] | 170 | } |
| 171 | |
Alan Stokes | e9d3314 | 2018-12-19 09:31:21 +0000 | [diff] [blame] | 172 | @Test |
Alan Stokes | 96efc028 | 2019-02-05 15:10:36 +0000 | [diff] [blame] | 173 | public void testGeneratesEvents_nativeLibrary_escapedName() throws Exception { |
Alan Stokes | 206a69d | 2019-02-04 15:32:12 +0000 | [diff] [blame] | 174 | new TestNativeCodeWithRetries() { |
| 175 | @Override |
| 176 | protected void loadNativeCode(int tryNumber) throws Exception { |
| 177 | // A file name with a space will be escaped in the audit log; verify we un-escape it |
| 178 | // correctly. |
| 179 | String privateCopyName = "second copy " + tryNumber + ".so"; |
| 180 | File privateCopyFile = privateFile(privateCopyName); |
| 181 | mExpectedNameHash = hashOf(privateCopyName); |
| 182 | mExpectedContentHash = copyAndHashResource( |
Alan Stokes | 8d3b7a4 | 2019-02-19 17:14:42 +0000 | [diff] [blame] | 183 | libraryPath("DynamicCodeLoggerNativeTestLibrary.so"), privateCopyFile); |
Alan Stokes | e9d3314 | 2018-12-19 09:31:21 +0000 | [diff] [blame] | 184 | |
Alan Stokes | 206a69d | 2019-02-04 15:32:12 +0000 | [diff] [blame] | 185 | System.load(privateCopyFile.toString()); |
| 186 | } |
| 187 | }.runTest(); |
Alan Stokes | e9d3314 | 2018-12-19 09:31:21 +0000 | [diff] [blame] | 188 | } |
| 189 | |
| 190 | @Test |
Alan Stokes | 96efc028 | 2019-02-05 15:10:36 +0000 | [diff] [blame] | 191 | public void testGeneratesEvents_nativeExecutable() throws Exception { |
Alan Stokes | 206a69d | 2019-02-04 15:32:12 +0000 | [diff] [blame] | 192 | new TestNativeCodeWithRetries() { |
| 193 | @Override |
| 194 | protected void loadNativeCode(int tryNumber) throws Exception { |
| 195 | String privateCopyName = "test_executable" + tryNumber; |
| 196 | File privateCopyFile = privateFile(privateCopyName); |
| 197 | mExpectedNameHash = hashOf(privateCopyName); |
| 198 | mExpectedContentHash = copyAndHashResource( |
Alan Stokes | 8d3b7a4 | 2019-02-19 17:14:42 +0000 | [diff] [blame] | 199 | "/DynamicCodeLoggerNativeExecutable", privateCopyFile); |
Alan Stokes | 206a69d | 2019-02-04 15:32:12 +0000 | [diff] [blame] | 200 | assertThat(privateCopyFile.setExecutable(true)).isTrue(); |
Alan Stokes | e9d3314 | 2018-12-19 09:31:21 +0000 | [diff] [blame] | 201 | |
Alan Stokes | 206a69d | 2019-02-04 15:32:12 +0000 | [diff] [blame] | 202 | Process process = Runtime.getRuntime().exec(privateCopyFile.toString()); |
| 203 | int exitCode = process.waitFor(); |
| 204 | assertThat(exitCode).isEqualTo(0); |
| 205 | } |
| 206 | }.runTest(); |
Alan Stokes | e9d3314 | 2018-12-19 09:31:21 +0000 | [diff] [blame] | 207 | } |
| 208 | |
| 209 | @Test |
Alan Stokes | 96efc028 | 2019-02-05 15:10:36 +0000 | [diff] [blame] | 210 | public void testGeneratesEvents_spoofed_validFile() throws Exception { |
Alan Stokes | e9d3314 | 2018-12-19 09:31:21 +0000 | [diff] [blame] | 211 | File privateCopyFile = privateFile("spoofed"); |
| 212 | |
Alan Stokes | 206a69d | 2019-02-04 15:32:12 +0000 | [diff] [blame] | 213 | String expectedContentHash = copyAndHashResource( |
Alan Stokes | 8d3b7a4 | 2019-02-19 17:14:42 +0000 | [diff] [blame] | 214 | "/DynamicCodeLoggerNativeExecutable", privateCopyFile); |
Alan Stokes | e9d3314 | 2018-12-19 09:31:21 +0000 | [diff] [blame] | 215 | |
| 216 | EventLog.writeEvent(EventLog.getTagCode("auditd"), |
| 217 | "type=1400 avc: granted { execute_no_trans } " |
| 218 | + "path=\"" + privateCopyFile + "\" " |
| 219 | + "scontext=u:r:untrusted_app_27: " |
| 220 | + "tcontext=u:object_r:app_data_file: " |
| 221 | + "tclass=file "); |
| 222 | |
| 223 | String expectedNameHash = |
| 224 | "1CF36F503A02877BB775DC23C1C5A47A95F2684B6A1A83B11795B856D88861E3"; |
| 225 | |
| 226 | // Run the job to scan generated audit log entries |
| 227 | runDynamicCodeLoggingJob(AUDIT_WATCHING_JOB_ID); |
| 228 | |
| 229 | // And then make sure we log events about it |
| 230 | long previousEventNanos = mostRecentEventTimeNanos(); |
| 231 | runDynamicCodeLoggingJob(IDLE_LOGGING_JOB_ID); |
| 232 | |
| 233 | assertDclLoggedSince(previousEventNanos, DCL_NATIVE_SUBTAG, |
| 234 | expectedNameHash, expectedContentHash); |
| 235 | } |
| 236 | |
| 237 | @Test |
Alan Stokes | 96efc028 | 2019-02-05 15:10:36 +0000 | [diff] [blame] | 238 | public void testGeneratesEvents_spoofed_pathTraversal() throws Exception { |
Alan Stokes | e9d3314 | 2018-12-19 09:31:21 +0000 | [diff] [blame] | 239 | File privateDir = privateFile("x").getParentFile(); |
| 240 | |
| 241 | // Transform /a/b/c -> /a/b/c/../../.. so we get back to the root |
| 242 | File pathTraversalToRoot = privateDir; |
| 243 | File root = new File("/"); |
| 244 | while (!privateDir.equals(root)) { |
| 245 | pathTraversalToRoot = new File(pathTraversalToRoot, ".."); |
| 246 | privateDir = privateDir.getParentFile(); |
| 247 | } |
| 248 | |
| 249 | File spoofedFile = new File(pathTraversalToRoot, "dev/urandom"); |
| 250 | |
| 251 | assertWithMessage("Expected " + spoofedFile + " to be readable") |
| 252 | .that(spoofedFile.canRead()).isTrue(); |
| 253 | |
| 254 | EventLog.writeEvent(EventLog.getTagCode("auditd"), |
| 255 | "type=1400 avc: granted { execute_no_trans } " |
| 256 | + "path=\"" + spoofedFile + "\" " |
| 257 | + "scontext=u:r:untrusted_app_27: " |
| 258 | + "tcontext=u:object_r:app_data_file: " |
| 259 | + "tclass=file "); |
| 260 | |
| 261 | String expectedNameHash = |
| 262 | "65528FE876BD676B0DFCC9A8ACA8988E026766F99EEC1E1FB48F46B2F635E225"; |
| 263 | |
| 264 | // Run the job to scan generated audit log entries |
| 265 | runDynamicCodeLoggingJob(AUDIT_WATCHING_JOB_ID); |
| 266 | |
| 267 | // And then trigger generating DCL events |
| 268 | long previousEventNanos = mostRecentEventTimeNanos(); |
| 269 | runDynamicCodeLoggingJob(IDLE_LOGGING_JOB_ID); |
| 270 | |
| 271 | assertNoDclLoggedSince(previousEventNanos, DCL_NATIVE_SUBTAG, expectedNameHash); |
| 272 | } |
| 273 | |
| 274 | @Test |
Alan Stokes | 96efc028 | 2019-02-05 15:10:36 +0000 | [diff] [blame] | 275 | public void testGeneratesEvents_spoofed_otherAppFile() throws Exception { |
Alan Stokes | e9d3314 | 2018-12-19 09:31:21 +0000 | [diff] [blame] | 276 | File ourPath = sContext.getDatabasePath("android_pay"); |
| 277 | File targetPath = new File(ourPath.toString() |
Alan Stokes | 8d3b7a4 | 2019-02-19 17:14:42 +0000 | [diff] [blame] | 278 | .replace("com.android.frameworks.dynamiccodeloggertest", "com.google.android.gms")); |
Alan Stokes | e9d3314 | 2018-12-19 09:31:21 +0000 | [diff] [blame] | 279 | |
| 280 | assertWithMessage("Expected " + targetPath + " to not be readable") |
| 281 | .that(targetPath.canRead()).isFalse(); |
| 282 | |
| 283 | EventLog.writeEvent(EventLog.getTagCode("auditd"), |
| 284 | "type=1400 avc: granted { execute_no_trans } " |
| 285 | + "path=\"" + targetPath + "\" " |
| 286 | + "scontext=u:r:untrusted_app_27: " |
| 287 | + "tcontext=u:object_r:app_data_file: " |
| 288 | + "tclass=file "); |
| 289 | |
| 290 | String expectedNameHash = |
| 291 | "CBE04E8AB9E7199FC19CBAAF9C774B88E56B3B19E823F2251693380AD6F515E6"; |
| 292 | |
| 293 | // Run the job to scan generated audit log entries |
| 294 | runDynamicCodeLoggingJob(AUDIT_WATCHING_JOB_ID); |
| 295 | |
| 296 | // And then trigger generating DCL events |
| 297 | long previousEventNanos = mostRecentEventTimeNanos(); |
| 298 | runDynamicCodeLoggingJob(IDLE_LOGGING_JOB_ID); |
| 299 | |
| 300 | assertNoDclLoggedSince(previousEventNanos, DCL_NATIVE_SUBTAG, expectedNameHash); |
| 301 | } |
| 302 | |
Alan Stokes | 206a69d | 2019-02-04 15:32:12 +0000 | [diff] [blame] | 303 | // Abstract out the logic for running a native code loading test multiple times if needed and |
| 304 | // leaving time for audit messages to reach the log. |
| 305 | private abstract class TestNativeCodeWithRetries { |
| 306 | String mExpectedContentHash; |
| 307 | String mExpectedNameHash; |
| 308 | |
| 309 | abstract void loadNativeCode(int tryNumber) throws Exception; |
| 310 | |
| 311 | final void runTest() throws Exception { |
| 312 | List<String> messages = null; |
| 313 | |
| 314 | for (int i = 0; i < AUDIT_LOG_RETRIES; i++) { |
| 315 | loadNativeCode(i); |
| 316 | |
| 317 | SystemClock.sleep(i * RETRY_DELAY_MS); |
| 318 | |
| 319 | // Run the job to scan generated audit log entries |
| 320 | runDynamicCodeLoggingJob(AUDIT_WATCHING_JOB_ID); |
| 321 | |
| 322 | // And then make sure we log events about it |
| 323 | long previousEventNanos = mostRecentEventTimeNanos(); |
| 324 | runDynamicCodeLoggingJob(IDLE_LOGGING_JOB_ID); |
| 325 | |
| 326 | messages = findMatchingEvents( |
| 327 | previousEventNanos, DCL_NATIVE_SUBTAG, mExpectedNameHash); |
| 328 | if (!messages.isEmpty()) { |
| 329 | break; |
| 330 | } |
| 331 | } |
| 332 | |
| 333 | assertHasDclLog(messages, mExpectedContentHash); |
| 334 | } |
| 335 | } |
| 336 | |
Alan Stokes | e9d3314 | 2018-12-19 09:31:21 +0000 | [diff] [blame] | 337 | private static File privateFile(String name) { |
| 338 | return new File(sContext.getDir("dcl", Context.MODE_PRIVATE), name); |
| 339 | } |
| 340 | |
Alan Stokes | a303953 | 2019-01-25 14:07:23 +0000 | [diff] [blame] | 341 | private String libraryPath(final String libraryName) { |
| 342 | // This may be deprecated. but it tells us the ABI of this process which is exactly what we |
| 343 | // want. |
| 344 | return "/lib/" + Build.CPU_ABI + "/" + libraryName; |
| 345 | } |
| 346 | |
Alan Stokes | e9d3314 | 2018-12-19 09:31:21 +0000 | [diff] [blame] | 347 | private static String copyAndHashResource(String resourcePath, File copyTo) throws Exception { |
Alan Stokes | 206a69d | 2019-02-04 15:32:12 +0000 | [diff] [blame] | 348 | MessageDigest hasher = MessageDigest.getInstance(SHA_256); |
Alan Stokes | 761d618 | 2017-12-19 11:48:19 +0000 | [diff] [blame] | 349 | |
| 350 | // Copy the jar from our Java resources to a private data directory |
Alan Stokes | 8d3b7a4 | 2019-02-19 17:14:42 +0000 | [diff] [blame] | 351 | Class<?> thisClass = DynamicCodeLoggerIntegrationTests.class; |
Alan Stokes | e9d3314 | 2018-12-19 09:31:21 +0000 | [diff] [blame] | 352 | try (InputStream input = thisClass.getResourceAsStream(resourcePath); |
| 353 | OutputStream output = new FileOutputStream(copyTo)) { |
Alan Stokes | 761d618 | 2017-12-19 11:48:19 +0000 | [diff] [blame] | 354 | byte[] buffer = new byte[1024]; |
| 355 | while (true) { |
| 356 | int numRead = input.read(buffer); |
| 357 | if (numRead < 0) { |
| 358 | break; |
| 359 | } |
| 360 | output.write(buffer, 0, numRead); |
| 361 | hasher.update(buffer, 0, numRead); |
| 362 | } |
| 363 | } |
| 364 | |
Alan Stokes | be9ec97 | 2018-12-10 14:07:47 +0000 | [diff] [blame] | 365 | // Compute the SHA-256 of the file content so we can check that it is the same as the value |
| 366 | // we see logged. |
Alan Stokes | 206a69d | 2019-02-04 15:32:12 +0000 | [diff] [blame] | 367 | return toHexString(hasher); |
| 368 | } |
| 369 | |
| 370 | private String hashOf(String input) throws Exception { |
| 371 | MessageDigest hasher = MessageDigest.getInstance(SHA_256); |
| 372 | hasher.update(input.getBytes()); |
| 373 | return toHexString(hasher); |
| 374 | } |
| 375 | |
| 376 | private static String toHexString(MessageDigest hasher) { |
Alan Stokes | 761d618 | 2017-12-19 11:48:19 +0000 | [diff] [blame] | 377 | Formatter formatter = new Formatter(); |
| 378 | for (byte b : hasher.digest()) { |
| 379 | formatter.format("%02X", b); |
| 380 | } |
Alan Stokes | 761d618 | 2017-12-19 11:48:19 +0000 | [diff] [blame] | 381 | |
Alan Stokes | be9ec97 | 2018-12-10 14:07:47 +0000 | [diff] [blame] | 382 | return formatter.toString(); |
Alan Stokes | 761d618 | 2017-12-19 11:48:19 +0000 | [diff] [blame] | 383 | } |
| 384 | |
Alan Stokes | e9d3314 | 2018-12-19 09:31:21 +0000 | [diff] [blame] | 385 | private static void runDynamicCodeLoggingJob(int jobId) throws Exception { |
| 386 | // This forces the DynamicCodeLoggingService job to start now. |
| 387 | runCommand("cmd jobscheduler run -f android " + jobId); |
Alan Stokes | be9ec97 | 2018-12-10 14:07:47 +0000 | [diff] [blame] | 388 | // Wait for the job to have run. |
| 389 | long startTime = SystemClock.elapsedRealtime(); |
| 390 | while (true) { |
| 391 | String response = runCommand( |
Alan Stokes | e9d3314 | 2018-12-19 09:31:21 +0000 | [diff] [blame] | 392 | "cmd jobscheduler get-job-state android " + jobId); |
Alan Stokes | be9ec97 | 2018-12-10 14:07:47 +0000 | [diff] [blame] | 393 | if (!response.contains("pending") && !response.contains("active")) { |
| 394 | break; |
| 395 | } |
Alan Stokes | e9d3314 | 2018-12-19 09:31:21 +0000 | [diff] [blame] | 396 | // Don't wait forever - if it's taken > 10s then something is very wrong. |
Alan Stokes | be9ec97 | 2018-12-10 14:07:47 +0000 | [diff] [blame] | 397 | if (SystemClock.elapsedRealtime() - startTime > TimeUnit.SECONDS.toMillis(10)) { |
| 398 | throw new AssertionError("Job has not completed: " + response); |
| 399 | } |
| 400 | SystemClock.sleep(100); |
| 401 | } |
| 402 | } |
Alan Stokes | 761d618 | 2017-12-19 11:48:19 +0000 | [diff] [blame] | 403 | |
Alan Stokes | be9ec97 | 2018-12-10 14:07:47 +0000 | [diff] [blame] | 404 | private static String runCommand(String command) throws Exception { |
| 405 | ByteArrayOutputStream response = new ByteArrayOutputStream(); |
| 406 | byte[] buffer = new byte[1000]; |
| 407 | UiAutomation ui = InstrumentationRegistry.getInstrumentation().getUiAutomation(); |
| 408 | ParcelFileDescriptor fd = ui.executeShellCommand(command); |
| 409 | try (InputStream input = new ParcelFileDescriptor.AutoCloseInputStream(fd)) { |
| 410 | while (true) { |
| 411 | int count = input.read(buffer); |
| 412 | if (count == -1) { |
| 413 | break; |
| 414 | } |
| 415 | response.write(buffer, 0, count); |
| 416 | } |
| 417 | } |
| 418 | return response.toString("UTF-8"); |
| 419 | } |
Alan Stokes | 761d618 | 2017-12-19 11:48:19 +0000 | [diff] [blame] | 420 | |
Alan Stokes | e9d3314 | 2018-12-19 09:31:21 +0000 | [diff] [blame] | 421 | private static long mostRecentEventTimeNanos() throws Exception { |
| 422 | List<Event> events = readSnetEvents(); |
| 423 | return events.isEmpty() ? 0 : events.get(events.size() - 1).getTimeNanos(); |
| 424 | } |
| 425 | |
| 426 | private static void assertDclLoggedSince(long previousEventNanos, String expectedSubTag, |
| 427 | String expectedNameHash, String expectedContentHash) throws Exception { |
| 428 | List<String> messages = |
| 429 | findMatchingEvents(previousEventNanos, expectedSubTag, expectedNameHash); |
| 430 | |
Alan Stokes | 206a69d | 2019-02-04 15:32:12 +0000 | [diff] [blame] | 431 | assertHasDclLog(messages, expectedContentHash); |
| 432 | } |
| 433 | |
| 434 | private static void assertHasDclLog(List<String> messages, String expectedContentHash) { |
Alan Stokes | e9d3314 | 2018-12-19 09:31:21 +0000 | [diff] [blame] | 435 | assertWithMessage("Expected exactly one matching log entry").that(messages).hasSize(1); |
| 436 | assertThat(messages.get(0)).endsWith(expectedContentHash); |
| 437 | } |
| 438 | |
| 439 | private static void assertNoDclLoggedSince(long previousEventNanos, String expectedSubTag, |
| 440 | String expectedNameHash) throws Exception { |
| 441 | List<String> messages = |
| 442 | findMatchingEvents(previousEventNanos, expectedSubTag, expectedNameHash); |
| 443 | |
| 444 | assertWithMessage("Expected no matching log entries").that(messages).isEmpty(); |
| 445 | } |
| 446 | |
| 447 | private static List<String> findMatchingEvents(long previousEventNanos, String expectedSubTag, |
| 448 | String expectedNameHash) throws Exception { |
| 449 | List<String> messages = new ArrayList<>(); |
| 450 | |
| 451 | for (Event event : readSnetEvents()) { |
Alan Stokes | 761d618 | 2017-12-19 11:48:19 +0000 | [diff] [blame] | 452 | if (event.getTimeNanos() <= previousEventNanos) { |
| 453 | continue; |
| 454 | } |
Alan Stokes | 761d618 | 2017-12-19 11:48:19 +0000 | [diff] [blame] | 455 | |
Alan Stokes | e9d3314 | 2018-12-19 09:31:21 +0000 | [diff] [blame] | 456 | Object data = event.getData(); |
| 457 | if (!(data instanceof Object[])) { |
Alan Stokes | 761d618 | 2017-12-19 11:48:19 +0000 | [diff] [blame] | 458 | continue; |
| 459 | } |
Alan Stokes | e9d3314 | 2018-12-19 09:31:21 +0000 | [diff] [blame] | 460 | Object[] fields = (Object[]) data; |
| 461 | |
| 462 | // We only care about DCL events that we generated. |
| 463 | String subTag = (String) fields[0]; |
| 464 | if (!expectedSubTag.equals(subTag)) { |
| 465 | continue; |
| 466 | } |
| 467 | int uid = (int) fields[1]; |
Alan Stokes | be9ec97 | 2018-12-10 14:07:47 +0000 | [diff] [blame] | 468 | if (uid != sMyUid) { |
Alan Stokes | 761d618 | 2017-12-19 11:48:19 +0000 | [diff] [blame] | 469 | continue; |
| 470 | } |
| 471 | |
Alan Stokes | e9d3314 | 2018-12-19 09:31:21 +0000 | [diff] [blame] | 472 | String message = (String) fields[2]; |
Alan Stokes | be9ec97 | 2018-12-10 14:07:47 +0000 | [diff] [blame] | 473 | if (!message.startsWith(expectedNameHash)) { |
| 474 | continue; |
| 475 | } |
| 476 | |
Alan Stokes | e9d3314 | 2018-12-19 09:31:21 +0000 | [diff] [blame] | 477 | messages.add(message); |
| 478 | //assertThat(message).endsWith(expectedContentHash); |
Alan Stokes | 761d618 | 2017-12-19 11:48:19 +0000 | [diff] [blame] | 479 | } |
Alan Stokes | e9d3314 | 2018-12-19 09:31:21 +0000 | [diff] [blame] | 480 | return messages; |
| 481 | } |
Alan Stokes | 761d618 | 2017-12-19 11:48:19 +0000 | [diff] [blame] | 482 | |
Alan Stokes | e9d3314 | 2018-12-19 09:31:21 +0000 | [diff] [blame] | 483 | private static List<Event> readSnetEvents() throws Exception { |
| 484 | List<Event> events = new ArrayList<>(); |
| 485 | EventLog.readEvents(new int[] { SNET_TAG }, events); |
| 486 | return events; |
Alan Stokes | be9ec97 | 2018-12-10 14:07:47 +0000 | [diff] [blame] | 487 | } |
| 488 | |
| 489 | /** |
| 490 | * A class loader that does nothing useful, but importantly doesn't extend BaseDexClassLoader. |
| 491 | */ |
| 492 | private static class UnknownClassLoader extends ClassLoader { |
| 493 | UnknownClassLoader(ClassLoader parent) { |
| 494 | super(parent); |
| 495 | } |
Alan Stokes | 761d618 | 2017-12-19 11:48:19 +0000 | [diff] [blame] | 496 | } |
| 497 | } |