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