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