1 /* 2 * Copyright (C) 2018 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 android.security.cts; 18 19 import com.android.tradefed.device.CollectingOutputReceiver; 20 import com.android.tradefed.device.DeviceNotAvailableException; 21 import com.android.tradefed.device.ITestDevice; 22 import com.android.tradefed.testtype.DeviceTestCase; 23 import com.android.tradefed.device.BackgroundDeviceAction; 24 25 import android.platform.test.annotations.RootPermissionTest; 26 27 import java.io.BufferedOutputStream; 28 import java.io.File; 29 import java.io.FileOutputStream; 30 import java.io.IOException; 31 import java.io.InputStream; 32 import java.io.OutputStream; 33 import java.util.Scanner; 34 import java.util.regex.Pattern; 35 import java.util.regex.Matcher; 36 import java.util.Map; 37 import java.util.HashMap; 38 import java.util.concurrent.ConcurrentHashMap; 39 import com.android.ddmlib.MultiLineReceiver; 40 import com.android.ddmlib.Log; 41 import com.android.ddmlib.TimeoutException; 42 import java.lang.ref.WeakReference; 43 44 /** 45 * A utility to monitor the device lowmemory state and reboot when low. Without this, tests that 46 * cause an OOM can sometimes cause ADB to become unresponsive indefinitely. Usage is to create an 47 * instance per instance of SecurityTestCase and call start() and stop() matching to 48 * SecurityTestCase setup() and teardown(). 49 */ 50 public class HostsideOomCatcher { 51 52 private static final String LOG_TAG = "HostsideOomCatcher"; 53 54 private static final long LOW_MEMORY_DEVICE_THRESHOLD_KB = 1024 * 1024; // 1GB 55 private static Map<String, WeakReference<BackgroundDeviceAction>> oomCatchers = 56 new ConcurrentHashMap<>(); 57 private static Map<String, Long> totalMemories = new ConcurrentHashMap<>(); 58 59 private boolean isLowMemoryDevice = false; 60 61 private SecurityTestCase context; 62 63 /** 64 * test behavior when oom is detected. 65 */ 66 public enum OomBehavior { 67 FAIL_AND_LOG, // normal behavior 68 PASS_AND_LOG, // skip tests that oom low memory devices 69 FAIL_NO_LOG, // tests that check for oom 70 } 71 private OomBehavior oomBehavior = OomBehavior.FAIL_AND_LOG; // accessed across threads 72 private boolean oomDetected = false; // accessed across threads 73 HostsideOomCatcher(SecurityTestCase context)74 public HostsideOomCatcher(SecurityTestCase context) { 75 this.context = context; 76 } 77 78 /** 79 * Utility to get the device memory total by reading /proc/meminfo and returning MemTotal 80 */ getMemTotal(ITestDevice device)81 private static long getMemTotal(ITestDevice device) throws DeviceNotAvailableException { 82 // cache device TotalMem to avoid an adb shell for every test. 83 String serial = device.getSerialNumber(); 84 Long totalMemory = totalMemories.get(serial); 85 if (totalMemory == null) { 86 String memInfo = device.executeShellCommand("cat /proc/meminfo"); 87 Pattern pattern = Pattern.compile("MemTotal:\\s*(.*?)\\s*[kK][bB]"); 88 Matcher matcher = pattern.matcher(memInfo); 89 if (matcher.find()) { 90 totalMemory = Long.parseLong(matcher.group(1)); 91 } else { 92 throw new RuntimeException("Could not get device memory total."); 93 } 94 Log.logAndDisplay(Log.LogLevel.INFO, LOG_TAG, 95 "Device " + serial + " has " + totalMemory + "KB total memory."); 96 totalMemories.put(serial, totalMemory); 97 } 98 return totalMemory; 99 } 100 101 /** 102 * Start the hostside oom catcher thread for the test. 103 * Match this call to SecurityTestCase.setup(). 104 */ start()105 public synchronized void start() throws Exception { 106 long totalMemory = getMemTotal(getDevice()); 107 isLowMemoryDevice = totalMemory < LOW_MEMORY_DEVICE_THRESHOLD_KB; 108 109 // reset test oom behavior 110 // Devices should fail tests that OOM so that they'll be ran again with --retry. 111 // If the test OOMs because previous tests used the memory, it will likely pass 112 // on a second try. 113 oomBehavior = OomBehavior.FAIL_AND_LOG; 114 oomDetected = false; 115 116 // Cache OOM detection in separate persistent threads for each device. 117 WeakReference<BackgroundDeviceAction> reference = 118 oomCatchers.get(getDevice().getSerialNumber()); 119 BackgroundDeviceAction oomCatcher = null; 120 if (reference != null) { 121 oomCatcher = reference.get(); 122 } 123 if (oomCatcher == null || !oomCatcher.isAlive() || oomCatcher.isCancelled()) { 124 AdbUtils.runCommandLine("am start com.android.cts.oomcatcher/.OomCatcher", getDevice()); 125 126 oomCatcher = new BackgroundDeviceAction( 127 "logcat -c && logcat OomCatcher:V *:S", 128 "Oom Catcher background thread", 129 getDevice(), new OomReceiver(getDevice()), 0); 130 131 oomCatchers.put(getDevice().getSerialNumber(), new WeakReference<>(oomCatcher)); 132 oomCatcher.start(); 133 } 134 } 135 136 /** 137 * Stop the hostside oom catcher thread. 138 * Match this call to SecurityTestCase.setup(). 139 */ 140 public static void stop(String serial) { 141 WeakReference<BackgroundDeviceAction> reference = oomCatchers.get(serial); 142 if (reference != null) { 143 BackgroundDeviceAction oomCatcher = reference.get(); 144 if (oomCatcher != null) { 145 oomCatcher.cancel(); 146 } 147 } 148 } 149 150 /** 151 * Check every test teardown to see if the device oomed during the test. 152 */ 153 public synchronized boolean isOomDetected() { 154 return oomDetected; 155 } 156 157 /** 158 * Return the current test behavior for when oom is detected. 159 */ 160 public synchronized OomBehavior getOomBehavior() { 161 return oomBehavior; 162 } 163 164 /** 165 * Flag meaning the test will likely fail on devices with low memory. 166 */ 167 public synchronized void setHighMemoryTest() { 168 if (isLowMemoryDevice) { 169 oomBehavior = OomBehavior.PASS_AND_LOG; 170 } else { 171 oomBehavior = OomBehavior.FAIL_AND_LOG; 172 } 173 } 174 175 /** 176 * Flag meaning the test uses the OOM catcher to fail the test because the test vulnerability 177 * intentionally OOMs the device. 178 */ 179 public synchronized void setOomTest() { 180 oomBehavior = OomBehavior.FAIL_NO_LOG; 181 } 182 183 private ITestDevice getDevice() { 184 return context.getDevice(); 185 } 186 187 /** 188 * Read through logcat to find when the OomCatcher app reports low memory. Once detected, reboot 189 * the device to prevent a soft reset with the possiblity of ADB becomming unresponsive. 190 */ 191 class OomReceiver extends MultiLineReceiver { 192 193 private ITestDevice device = null; 194 private boolean isCancelled = false; 195 196 public OomReceiver(ITestDevice device) { 197 this.device = device; 198 } 199 200 @Override 201 public void processNewLines(String[] lines) { 202 for (String line : lines) { 203 if (Pattern.matches(".*Low memory.*", line)) { 204 // low memory detected, reboot device to clear memory and pass test 205 isCancelled = true; 206 Log.logAndDisplay(Log.LogLevel.INFO, LOG_TAG, 207 "lowmemorykiller detected; rebooting device."); 208 synchronized (HostsideOomCatcher.this) { // synchronized for oomDetected 209 oomDetected = true; // set HostSideOomCatcher var 210 } 211 try { 212 device.nonBlockingReboot(); 213 device.waitForDeviceOnline(60 * 2 * 1000); // 2 minutes 214 } catch (Exception e) { 215 Log.e(LOG_TAG, e.toString()); 216 } 217 return; // we don't need to process remaining lines in the array 218 } 219 } 220 } 221 222 @Override 223 public boolean isCancelled() { 224 return isCancelled; 225 } 226 } 227 } 228 229