1 /* 2 * Copyright (C) 2019 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 17 package com.android.sts.common.tradefed.testtype; 18 19 import static com.google.common.truth.Truth.assertWithMessage; 20 21 import static org.junit.Assert.assertFalse; 22 import static org.junit.Assume.assumeFalse; 23 import static org.junit.Assume.assumeTrue; 24 25 import com.android.compatibility.common.util.MetricsReportLog; 26 import com.android.compatibility.common.util.ResultType; 27 import com.android.compatibility.common.util.ResultUnit; 28 import com.android.ddmlib.Log.LogLevel; 29 import com.android.sts.common.HostsideMainlineModuleDetector; 30 import com.android.sts.common.PocPusher; 31 import com.android.sts.common.RegexUtils; 32 import com.android.tradefed.build.IBuildInfo; 33 import com.android.tradefed.config.Option; 34 import com.android.tradefed.device.DeviceNotAvailableException; 35 import com.android.tradefed.device.ITestDevice; 36 import com.android.tradefed.device.WifiHelper; 37 import com.android.tradefed.log.LogUtil.CLog; 38 import com.android.tradefed.testtype.IAbi; 39 40 import org.junit.After; 41 import org.junit.Before; 42 import org.junit.Rule; 43 import org.junit.rules.TestName; 44 45 import java.math.BigInteger; 46 import java.util.HashMap; 47 import java.util.HashSet; 48 import java.util.Map; 49 import java.util.Set; 50 import java.util.concurrent.Callable; 51 import java.util.regex.Matcher; 52 import java.util.regex.Pattern; 53 54 /** 55 * Base test class for all STS tests. 56 * 57 * <p>Use {@link RootSecurityTestCase} or {@link NonRootSecurityTestCase} instead. 58 */ 59 public class SecurityTestCase extends StsExtraBusinessLogicHostTestBase { 60 61 private static final String LOG_TAG = "SecurityTestCase"; 62 private static final int RADIX_HEX = 16; 63 64 protected static final int TIMEOUT_DEFAULT = 60; 65 // account for the poc timer of 5 minutes (+15 seconds for safety) 66 public static final int TIMEOUT_NONDETERMINISTIC = 315; 67 68 private long kernelStartTime = -1; 69 70 private HostsideMainlineModuleDetector mainlineModuleDetector = 71 new HostsideMainlineModuleDetector(this); 72 73 @Rule public TestName testName = new TestName(); 74 @Rule public PocPusher pocPusher = new PocPusher(); 75 76 private static Map<ITestDevice, IBuildInfo> sBuildInfo = new HashMap<>(); 77 private static Map<ITestDevice, IAbi> sAbi = new HashMap<>(); 78 private static Map<ITestDevice, String> sTestName = new HashMap<>(); 79 private static Map<ITestDevice, PocPusher> sPocPusher = new HashMap<>(); 80 81 @Option( 82 name = "set-kptr_restrict", 83 description = "If kptr_restrict should be set to 2 after every reboot") 84 private boolean setKptr_restrict = false; 85 86 @Option( 87 name = "wifi-connect-timeout", 88 description = "time in milliseconds to timeout while enabling wifi") 89 private long wifiConnectTimeout = 15_000; 90 91 @Option( 92 name = "skip-wifi-failure", 93 description = 94 "Whether to throw an assumption failure instead of an assertion failure when" 95 + " wifi cannot be enabled") 96 private boolean skipWifiFailure = false; 97 98 private boolean ignoreKernelAddress = false; 99 100 /** Waits for device to be online, marks the most recent boottime of the device */ 101 @Before setUp()102 public void setUp() throws Exception { 103 getDevice().waitForDeviceAvailable(); 104 updateKernelStartTime(); 105 // TODO:(badash@): Watch for other things to track. 106 // Specifically time when app framework starts 107 108 sBuildInfo.put(getDevice(), getBuild()); 109 sAbi.put(getDevice(), getAbi()); 110 sTestName.put(getDevice(), testName.getMethodName()); 111 112 pocPusher.setDevice(getDevice()).setBuild(getBuild()).setAbi(getAbi()); 113 sPocPusher.put(getDevice(), pocPusher); 114 115 if (setKptr_restrict) { 116 boolean wasRoot = getDevice().isAdbRoot(); 117 118 if (wasRoot || getDevice().enableAdbRoot()) { 119 CLog.i("setting kptr_restrict to 2"); 120 getDevice().executeShellCommand("echo 2 > /proc/sys/kernel/kptr_restrict"); 121 if (!wasRoot) { 122 getDevice().disableAdbRoot(); 123 } 124 } else { 125 CLog.i("Not a rootable device - could not set kptr_restrict to 2"); 126 ignoreKernelAddress = true; 127 } 128 } 129 } 130 131 /** Makes sure the phone is online and checks if the device crashed */ 132 @After tearDown()133 public void tearDown() throws Exception { 134 try { 135 getDevice().waitForDeviceAvailable(90 * 1000); 136 } catch (DeviceNotAvailableException e) { 137 // Force a disconnection of all existing sessions to see if that unsticks adbd. 138 getDevice().executeAdbCommand("reconnect"); 139 getDevice().waitForDeviceAvailable(30 * 1000); 140 } 141 142 logAndTerminateTestProcesses(); 143 144 long lastKernelStartTime = kernelStartTime; 145 kernelStartTime = -1; 146 // only test when the kernel start time is valid 147 if (lastKernelStartTime != -1) { 148 long currentKernelStartTime = getKernelStartTime(); 149 String bootReason = "(could not get bootreason)"; 150 try { 151 bootReason = getDevice().getProperty("ro.boot.bootreason"); 152 } catch (DeviceNotAvailableException e) { 153 CLog.e("Could not get ro.boot.bootreason", e); 154 } 155 assertWithMessage( 156 "The device has unexpectedly rebooted (%s seconds after last recorded" 157 + " boot time, bootreason: %s)", 158 currentKernelStartTime - lastKernelStartTime, bootReason) 159 .that(currentKernelStartTime) 160 .isLessThan(lastKernelStartTime + 10); 161 } 162 } 163 getBuildInfo(ITestDevice device)164 public static IBuildInfo getBuildInfo(ITestDevice device) { 165 return sBuildInfo.get(device); 166 } 167 getAbi(ITestDevice device)168 public static IAbi getAbi(ITestDevice device) { 169 return sAbi.get(device); 170 } 171 getTestName(ITestDevice device)172 public static String getTestName(ITestDevice device) { 173 return sTestName.get(device); 174 } 175 getPocPusher(ITestDevice device)176 public static PocPusher getPocPusher(ITestDevice device) { 177 return sPocPusher.get(device); 178 } 179 180 // TODO convert existing assertMatches*() to RegexUtils.assertMatches*() 181 // b/123237827 182 @Deprecated assertMatches(String pattern, String input)183 public void assertMatches(String pattern, String input) throws Exception { 184 RegexUtils.assertContains(pattern, input); 185 } 186 187 @Deprecated assertMatchesMultiLine(String pattern, String input)188 public void assertMatchesMultiLine(String pattern, String input) throws Exception { 189 RegexUtils.assertContainsMultiline(pattern, input); 190 } 191 192 @Deprecated assertNotMatches(String pattern, String input)193 public void assertNotMatches(String pattern, String input) throws Exception { 194 RegexUtils.assertNotContains(pattern, input); 195 } 196 197 @Deprecated assertNotMatchesMultiLine(String pattern, String input)198 public void assertNotMatchesMultiLine(String pattern, String input) throws Exception { 199 RegexUtils.assertNotContainsMultiline(pattern, input); 200 } 201 202 /** 203 * Runs a provided function that collects a String to test against kernel pointer leaks. The 204 * getPtrFunction function implementation must return a String that starts with the pointer. 205 * i.e. "01234567". Trailing characters are allowed except for [0-9a-fA-F]. In the event that 206 * the pointer appears to be vulnerable, a JUnit assert is thrown. Since kernel pointers can be 207 * hashed, there is a possibility the hashed pointer overlaps into the normal kernel space. The 208 * test re-runs to make false positives statistically insignificant. When kernel pointers won't 209 * change without a reboot, provide a device to reboot. 210 * 211 * @param getPtrFunction a function that returns a string that starts with a pointer 212 * @param deviceToReboot device to reboot when kernel pointers won't change 213 */ assertNotKernelPointer(Callable<String> getPtrFunction, ITestDevice deviceToReboot)214 public void assertNotKernelPointer(Callable<String> getPtrFunction, ITestDevice deviceToReboot) 215 throws Exception { 216 assumeFalse("Could not set kptr_restrict to 2, ignoring kptr test.", ignoreKernelAddress); 217 218 MetricsReportLog reportLog = buildMetricsReportLog(getDevice()); 219 220 int kptrRestrict; 221 try { 222 kptrRestrict = 223 Integer.parseInt( 224 getDevice() 225 .executeShellV2Command("cat /proc/sys/kernel/kptr_restrict") 226 .getStdout() 227 .trim()); 228 } catch (NumberFormatException e) { 229 kptrRestrict = -1; 230 } 231 reportLog.addValue("kptr_restrict", kptrRestrict, ResultType.NEUTRAL, ResultUnit.NONE); 232 233 boolean isKernelPointer = true; 234 String ptr = null; 235 for (int i = 0; i < 4; i++) { // ~0.4% chance of false positive 236 ptr = getPtrFunction.call(); 237 if (ptr == null) { 238 isKernelPointer = false; 239 break; 240 } 241 reportLog.addValue("address" + i, ptr, ResultType.NEUTRAL, ResultUnit.NONE); 242 243 if (!isKptr(ptr)) { 244 // quit early because the ptr is likely hashed or zeroed. 245 isKernelPointer = false; 246 break; 247 } 248 if (deviceToReboot != null) { 249 deviceToReboot.nonBlockingReboot(); 250 deviceToReboot.waitForDeviceAvailable(); 251 updateKernelStartTime(); 252 } 253 } 254 reportLog.addValue( 255 "is_kernel_pointer", isKernelPointer, ResultType.NEUTRAL, ResultUnit.NONE); 256 reportLog.submit(); 257 assertFalse( 258 String.format( 259 "\"%s\" is an exposed kernel pointer. The device kptr_restrict is" 260 + " \"%d\".", 261 ptr, kptrRestrict) 262 + "Please check the help center FAQ#2 at " 263 + "https://support.google.com/androidpartners_security/answer/9144408?hl=en&ref_topic=7534918", 264 isKernelPointer); 265 } 266 isKptr(String ptr)267 private boolean isKptr(String ptr) { 268 Matcher m = Pattern.compile("[0-9a-fA-F]*").matcher(ptr); 269 if (!m.find() || m.start() != 0) { 270 // ptr string is malformed 271 return false; 272 } 273 int length = m.end(); 274 275 if (length == 8) { 276 // 32-bit pointer 277 BigInteger address = new BigInteger(ptr.substring(0, length), RADIX_HEX); 278 // 32-bit kernel memory range: 0xC0000000 -> 0xffffffff 279 // 0x3fffffff bytes = 1GB / 0xffffffff = 4 GB 280 // 1 in 4 collision for hashed pointers 281 return address.compareTo(new BigInteger("C0000000", RADIX_HEX)) >= 0; 282 } else if (length == 16) { 283 // 64-bit pointer 284 BigInteger address = new BigInteger(ptr.substring(0, length), RADIX_HEX); 285 // 64-bit kernel memory range: 0x8000000000000000 -> 0xffffffffffffffff 286 // 48-bit implementation: 0xffff800000000000; 1 in 131,072 collision 287 // 56-bit implementation: 0xff80000000000000; 1 in 512 collision 288 // 64-bit implementation: 0x8000000000000000; 1 in 2 collision 289 return address.compareTo(new BigInteger("ff80000000000000", RADIX_HEX)) >= 0; 290 } 291 292 return false; 293 } 294 295 /** Check if a driver is present and readable. */ containsDriver(ITestDevice device, String driver)296 protected boolean containsDriver(ITestDevice device, String driver) throws Exception { 297 return containsDriver(device, driver, true); 298 } 299 300 /** Check if a driver is present on a machine. */ containsDriver(ITestDevice device, String driver, boolean checkReadable)301 protected boolean containsDriver(ITestDevice device, String driver, boolean checkReadable) 302 throws Exception { 303 boolean containsDriver = false; 304 if (driver.contains("*")) { 305 // -A list all files but . and .. 306 // -d directory, not contents 307 // -1 list one file per line 308 // -f unsorted 309 String ls = "ls -A -d -1 -f " + driver; 310 if (device.executeShellV2Command(ls).getExitCode().intValue() == 0) { 311 String[] expanded = device.executeShellCommand(ls).split("\\R"); 312 for (String expandedDriver : expanded) { 313 containsDriver |= containsDriver(device, expandedDriver, checkReadable); 314 } 315 } 316 } else { 317 if (checkReadable) { 318 containsDriver = 319 device.executeShellV2Command("test -r " + driver).getExitCode().intValue() 320 == 0; 321 } else { 322 containsDriver = 323 device.executeShellV2Command("test -e " + driver).getExitCode().intValue() 324 == 0; 325 } 326 } 327 328 MetricsReportLog reportLog = buildMetricsReportLog(getDevice()); 329 reportLog.addValue("path", driver, ResultType.NEUTRAL, ResultUnit.NONE); 330 reportLog.addValue("exists", containsDriver, ResultType.NEUTRAL, ResultUnit.NONE); 331 reportLog.submit(); 332 333 return containsDriver; 334 } 335 buildMetricsReportLog(ITestDevice device)336 public static MetricsReportLog buildMetricsReportLog(ITestDevice device) { 337 IBuildInfo buildInfo = getBuildInfo(device); 338 IAbi abi = getAbi(device); 339 String testName = getTestName(device); 340 341 StackTraceElement[] stacktraces = Thread.currentThread().getStackTrace(); 342 int stackDepth = 2; // 0: getStackTrace(), 1: buildMetricsReportLog, 2: caller 343 String className = stacktraces[stackDepth].getClassName(); 344 String methodName = stacktraces[stackDepth].getMethodName(); 345 String classMethodName = String.format("%s#%s", className, methodName); 346 347 // The stream name must be snake_case or else json formatting breaks 348 String streamName = methodName.replaceAll("(\\p{Upper})", "_$1").toLowerCase(); 349 350 MetricsReportLog reportLog = 351 new MetricsReportLog( 352 buildInfo, 353 abi.getName(), 354 classMethodName, 355 "StsHostTestCases", 356 streamName, 357 true); 358 reportLog.addValue("test_name", testName, ResultType.NEUTRAL, ResultUnit.NONE); 359 return reportLog; 360 } 361 getDeviceUptime()362 private long getDeviceUptime() throws DeviceNotAvailableException { 363 String uptime = null; 364 int attempts = 5; 365 do { 366 if (attempts-- <= 0) { 367 throw new RuntimeException("could not get device uptime"); 368 } 369 getDevice().waitForDeviceAvailable(); 370 uptime = getDevice().executeShellCommand("cat /proc/uptime").trim(); 371 } while (uptime.isEmpty()); 372 return Long.parseLong(uptime.substring(0, uptime.indexOf('.'))); 373 } 374 safeReboot()375 public void safeReboot() throws DeviceNotAvailableException { 376 getDevice().nonBlockingReboot(); 377 getDevice().waitForDeviceAvailable(); 378 updateKernelStartTime(); 379 } 380 getKernelStartTime()381 private long getKernelStartTime() throws DeviceNotAvailableException { 382 long uptime = getDeviceUptime(); 383 return (System.currentTimeMillis() / 1000) - uptime; 384 } 385 386 /** Allows a test to pass if called after a planned reboot. */ updateKernelStartTime()387 public void updateKernelStartTime() throws DeviceNotAvailableException { 388 kernelStartTime = getKernelStartTime(); 389 } 390 391 /** 392 * Queries the device for any test binaries which are still running. Those found will be dumped 393 * to stdout, then killed. 394 */ logAndTerminateTestProcesses()395 private void logAndTerminateTestProcesses() { 396 ITestDevice device = getDevice(); 397 398 Set<String> danglingPgids = new HashSet<String>(); 399 400 try { 401 // Get all pid, command pairs. 402 String rawProcessList = device.executeShellCommand("ps -Ao pid,name,pgid"); 403 String[] processLines = rawProcessList.split("\n"); 404 405 // Extract all PIDs and commands. Format of line is "PID COMMAND" 406 for (int i = 0; i < processLines.length; ++i) { 407 String[] tokens = processLines[i].trim().split("\\s+"); 408 if (3 != tokens.length) { 409 CLog.i( 410 "process entry doesn't tokenize as expected, skipping: " 411 + processLines[i]); 412 continue; 413 } 414 String pid = tokens[0]; 415 // Strip any brackets from the process name 416 String name = tokens[1].replaceAll("\\[|\\]", ""); 417 String pgid = tokens[2]; 418 // All STS poc binaries are stored in /data/local/tmp 419 if (name.startsWith("Bug") || name.startsWith("CVE")) { 420 danglingPgids.add(pgid); 421 CLog.w("Found dangling test process %s with PID %s, PGID %s", name, pid, pgid); 422 } 423 } 424 } catch (DeviceNotAvailableException e) { 425 CLog.logAndDisplay( 426 LogLevel.ERROR, 427 "DeviceNotAvailableException encountered while querying device for " 428 + "dangling test processes."); 429 return; 430 } 431 432 try { 433 if (danglingPgids.size() > 0) { 434 CLog.logAndDisplay( 435 LogLevel.WARN, 436 "Found " 437 + danglingPgids.size() 438 + " dangling test process group(s). Terminating..."); 439 440 for (String pgid : danglingPgids) { 441 if (Long.parseLong(pgid) <= 1) { 442 CLog.e("PGID %s allegedly a dangling STS group, ignoring.", pgid); 443 continue; 444 } 445 String killCommand = "kill -9 -" + pgid; 446 CLog.i(killCommand); 447 String killOutput = device.executeShellCommand(killCommand); 448 CLog.i(killOutput); 449 } 450 } 451 } catch (DeviceNotAvailableException e) { 452 CLog.logAndDisplay( 453 LogLevel.ERROR, 454 "DeviceNotAvailableException encountered while attempting to terminate " 455 + "dangling test processes."); 456 } 457 } 458 459 /** 460 * Return true if a module is play managed. 461 * 462 * <p>Example of skipping a test based on mainline modules: 463 * 464 * <pre> 465 * {@literal @}Test 466 * public void testPocCVE_1234_5678() throws Exception { 467 * // This will skip the test if MODULE_METADATA mainline module is play managed. 468 * assumeFalse(moduleIsPlayManaged("com.google.android.captiveportallogin")); 469 * // Do testing... 470 * } 471 * </pre> 472 */ moduleIsPlayManaged(String modulePackageName)473 public boolean moduleIsPlayManaged(String modulePackageName) throws Exception { 474 return mainlineModuleDetector.getPlayManagedModules().contains(modulePackageName); 475 } 476 assumeIsSupportedNfcDevice(ITestDevice device)477 public void assumeIsSupportedNfcDevice(ITestDevice device) throws Exception { 478 String supportedDrivers[] = { 479 "/dev/nq-nci*", "/dev/pn54*", "/dev/pn551*", "/dev/pn553*", 480 "/dev/pn557*", "/dev/pn65*", "/dev/pn66*", "/dev/pn67*", 481 "/dev/pn80*", "/dev/pn81*", "/dev/sn100*", "/dev/sn220*", 482 "/dev/st54j*", "/dev/st21nfc*" 483 }; 484 boolean isDriverFound = false; 485 for (String supportedDriver : supportedDrivers) { 486 if (containsDriver(device, supportedDriver, false)) { 487 isDriverFound = true; 488 break; 489 } 490 } 491 String[] output = device.executeShellCommand("ls -la /dev | grep nfc").split("\\n"); 492 String nfcDevice = null; 493 for (String line : output) { 494 if (line.contains("nfc")) { 495 String text[] = line.split("\\s+"); 496 nfcDevice = text[text.length - 1]; 497 } 498 } 499 assumeTrue( 500 "NFC device " + nfcDevice + " is not supported. Hence skipping the test", 501 isDriverFound); 502 } 503 createWifiHelper()504 public WifiHelper createWifiHelper() throws DeviceNotAvailableException { 505 ITestDevice device = getDevice(); 506 return new WifiHelper(device, device.getOptions().getWifiUtilAPKPath(), /* doSetup */ true); 507 } 508 509 /** 510 * Asserts the wifi connection status is connected. Because STS can reboot a device immediately 511 * before running a test, wifi might not be connected before the test runs. We poll wifi until 512 * we hit a timeout or wifi is connected. 513 * 514 * @param device device to be ran on 515 */ assertWifiConnected(ITestDevice device)516 public void assertWifiConnected(ITestDevice device) throws Exception { 517 assumeTrue("Wi-Fi hardware not detected", device.hasFeature("android.hardware.wifi")); 518 519 WifiHelper wifiHelper = createWifiHelper(); 520 wifiHelper.enableWifi(); 521 522 long endTime = System.currentTimeMillis() + wifiConnectTimeout; 523 do { 524 // tests require that device are connected to a wifi network, but not requiring 525 // internet connectivity 526 if (!"null".equals(wifiHelper.getBSSID())) { 527 return; 528 } 529 Thread.sleep(1000); 530 } while (System.currentTimeMillis() < endTime); 531 532 assumeFalse("Wi-Fi could not be enabled on the device; skipping", skipWifiFailure); 533 // enable with one of the following: 534 // '<option name="compatibility-build-provider:build-attribute" key="sts-skip-wifi-failures" 535 // value="true" />' 536 // '--compatibility-build-provider:build-attribute sts-skip-wifi-failures=true' 537 boolean buildAttributeSkipWifiFailure = 538 Boolean.parseBoolean(getBuild().getBuildAttributes().get("sts-skip-wifi-failures")); 539 assumeFalse( 540 "Wi-Fi could not be enabled on the device; skipping", 541 buildAttributeSkipWifiFailure); 542 throw new AssertionError( 543 "This test requires a Wi-Fi connection on-device. " 544 + "Please consult the CTS setup guide: " 545 + "https://source.android.com/compatibility/cts/setup#wifi\n" 546 + "Also ensure \"Stay Awake\" is enabled in developer options: " 547 + "https://source.android.com/compatibility/cts/setup#config_device"); 548 } 549 } 550