1 /* 2 * Copyright (C) 2015 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.jdwpsecurity.cts; 18 19 import com.android.cts.tradefed.build.CtsBuildHelper; 20 import com.android.tradefed.build.IBuildInfo; 21 import com.android.tradefed.device.DeviceNotAvailableException; 22 import com.android.tradefed.log.LogUtil.CLog; 23 import com.android.tradefed.testtype.DeviceTestCase; 24 import com.android.tradefed.testtype.IBuildReceiver; 25 import com.android.tradefed.util.ArrayUtil; 26 import com.android.tradefed.util.RunUtil; 27 28 import java.io.BufferedReader; 29 import java.io.EOFException; 30 import java.io.File; 31 import java.io.IOException; 32 import java.io.InputStream; 33 import java.io.InputStreamReader; 34 import java.io.PrintWriter; 35 import java.util.ArrayList; 36 import java.util.List; 37 38 /** 39 * Test to check non-zygote apps do not have an active JDWP connection. 40 */ 41 public class JdwpSecurityHostTest extends DeviceTestCase implements IBuildReceiver { 42 43 private static final String DEVICE_LOCATION = "/data/local/tmp/jdwpsecurity"; 44 private static final String DEVICE_SCRIPT_FILENAME = "jdwptest"; 45 private static final String DEVICE_JAR_FILENAME = "CtsJdwpApp.jar"; 46 private static final String JAR_MAIN_CLASS_NAME = "com.android.cts.jdwpsecurity.JdwpTest"; 47 48 private CtsBuildHelper mCtsBuild; 49 getDeviceScriptFilepath()50 private static String getDeviceScriptFilepath() { 51 return DEVICE_LOCATION + File.separator + DEVICE_SCRIPT_FILENAME; 52 } 53 getDeviceJarFilepath()54 private static String getDeviceJarFilepath() { 55 return DEVICE_LOCATION + File.separator + DEVICE_JAR_FILENAME; 56 } 57 58 @Override setBuild(IBuildInfo buildInfo)59 public void setBuild(IBuildInfo buildInfo) { 60 mCtsBuild = CtsBuildHelper.createBuildHelper(buildInfo); 61 } 62 63 @Override setUp()64 protected void setUp() throws Exception { 65 super.setUp(); 66 67 // Create test directory on the device. 68 createRemoteDir(DEVICE_LOCATION); 69 70 // Also create the dalvik-cache directory. It needs to exist before the runtime starts. 71 createRemoteDir(DEVICE_LOCATION + File.separator + "dalvik-cache"); 72 73 // Create and push script on the device. 74 File tempFile = createScriptTempFile(); 75 try { 76 boolean success = getDevice().pushFile(tempFile, getDeviceScriptFilepath()); 77 assertTrue("Failed to push script to " + getDeviceScriptFilepath(), success); 78 } finally { 79 if (tempFile != null) { 80 tempFile.delete(); 81 } 82 } 83 84 // Make the script executable. 85 getDevice().executeShellCommand("chmod 755 " + getDeviceScriptFilepath()); 86 87 // Push jar file. 88 File jarFile = mCtsBuild.getTestApp(DEVICE_JAR_FILENAME); 89 boolean success = getDevice().pushFile(jarFile, getDeviceJarFilepath()); 90 assertTrue("Failed to push jar file to " + getDeviceScriptFilepath(), success); 91 } 92 93 @Override tearDown()94 protected void tearDown() throws Exception { 95 // Delete the whole test directory on the device. 96 getDevice().executeShellCommand(String.format("rm -r %s", DEVICE_LOCATION)); 97 98 super.tearDown(); 99 } 100 101 /** 102 * Tests a non-zygote app does not have a JDWP connection, thus not being 103 * debuggable. 104 * 105 * Runs a script executing a Java app (jar file) with app_process, 106 * without forking from zygote. Then checks its pid is not returned 107 * by 'adb jdwp', meaning it has no JDWP connection and cannot be 108 * debugged. 109 * 110 * @throws Exception 111 */ testNonZygoteProgramIsNotDebuggable()112 public void testNonZygoteProgramIsNotDebuggable() throws Exception { 113 String scriptFilepath = getDeviceScriptFilepath(); 114 Process scriptProcess = null; 115 String scriptPid = null; 116 List<String> activeJdwpPids = null; 117 try { 118 // Run the script on the background so it's running when we collect the list of 119 // pids with a JDWP connection using 'adb jdwp'. 120 // command. 121 scriptProcess = runScriptInBackground(scriptFilepath); 122 123 // On startup, the script will print its pid on its output. 124 scriptPid = readScriptPid(scriptProcess); 125 126 // Collect the list of pids with a JDWP connection. 127 activeJdwpPids = getJdwpPids(); 128 } finally { 129 // Stop the script. 130 if (scriptProcess != null) { 131 scriptProcess.destroy(); 132 } 133 } 134 135 assertNotNull("Failed to get script pid", scriptPid); 136 assertNotNull("Failed to get active JDWP pids", activeJdwpPids); 137 assertFalse("Test app should not have an active JDWP connection" + 138 " (pid " + scriptPid + " is returned by 'adb jdwp')", 139 activeJdwpPids.contains(scriptPid)); 140 } 141 runScriptInBackground(String scriptFilepath)142 private Process runScriptInBackground(String scriptFilepath) throws IOException { 143 String[] shellScriptCommand = buildAdbCommand("shell", scriptFilepath); 144 return RunUtil.getDefault().runCmdInBackground(shellScriptCommand); 145 } 146 readScriptPid(Process scriptProcess)147 private String readScriptPid(Process scriptProcess) throws IOException { 148 BufferedReader br = null; 149 try { 150 br = new BufferedReader(new InputStreamReader(scriptProcess.getInputStream())); 151 // We only expect to read one line containing the pid. 152 return br.readLine(); 153 } finally { 154 if (br != null) { 155 br.close(); 156 } 157 } 158 } 159 getJdwpPids()160 private List<String> getJdwpPids() throws Exception { 161 return new AdbJdwpOutputReader().listPidsWithAdbJdwp(); 162 } 163 164 /** 165 * Creates the script file on the host so it can be pushed onto the device. 166 * 167 * @return the script file 168 * @throws IOException 169 */ createScriptTempFile()170 private static File createScriptTempFile() throws IOException { 171 File tempFile = File.createTempFile("jdwptest", ".tmp"); 172 173 PrintWriter pw = null; 174 try { 175 pw = new PrintWriter(tempFile); 176 177 // We need a dalvik-cache in /data/local/tmp so we have read-write access. 178 // Note: this will cause the runtime to optimize the DEX file (contained in 179 // the jar file) before executing it. 180 pw.println(String.format("export ANDROID_DATA=%s", DEVICE_LOCATION)); 181 pw.println(String.format("export CLASSPATH=%s", getDeviceJarFilepath())); 182 pw.println(String.format("exec app_process /system/bin %s \"$@\"", 183 JAR_MAIN_CLASS_NAME)); 184 } finally { 185 if (pw != null) { 186 pw.close(); 187 } 188 } 189 190 return tempFile; 191 } 192 193 /** 194 * Helper class collecting all pids returned by 'adb jdwp' command. 195 */ 196 private class AdbJdwpOutputReader implements Runnable { 197 /** 198 * A list of all pids with a JDWP connection returned by 'adb jdwp'. 199 */ 200 private final List<String> lines = new ArrayList<String>(); 201 202 /** 203 * The input stream of the process running 'adb jdwp'. 204 */ 205 private InputStream in; 206 listPidsWithAdbJdwp()207 public List<String> listPidsWithAdbJdwp() throws Exception { 208 // The 'adb jdwp' command does not return normally, it only terminates with Ctrl^C. 209 // Therefore we cannot use ITestDevice.executeAdbCommand but need to run that command 210 // in the background. Since we know the tested app is already running, we only need to 211 // capture the output for a short amount of time before stopping the 'adb jdwp' 212 // command. 213 String[] adbJdwpCommand = buildAdbCommand("jdwp"); 214 Process adbProcess = RunUtil.getDefault().runCmdInBackground(adbJdwpCommand); 215 in = adbProcess.getInputStream(); 216 217 // Read the output for 5s in a separate thread before stopping the command. 218 Thread t = new Thread(this); 219 t.start(); 220 Thread.sleep(5000); 221 222 // Kill the 'adb jdwp' process and wait for the thread to stop. 223 adbProcess.destroy(); 224 t.join(); 225 226 return lines; 227 } 228 229 @Override run()230 public void run() { 231 BufferedReader br = null; 232 try { 233 br = new BufferedReader(new InputStreamReader(in)); 234 String line; 235 while ((line = readLineIgnoreException(br)) != null) { 236 lines.add(line); 237 } 238 } catch (IOException e) { 239 CLog.e(e); 240 } finally { 241 if (br != null) { 242 try { 243 br.close(); 244 } catch (IOException e) { 245 // Ignore it. 246 } 247 } 248 } 249 } 250 readLineIgnoreException(BufferedReader reader)251 private String readLineIgnoreException(BufferedReader reader) throws IOException { 252 try { 253 return reader.readLine(); 254 } catch (IOException e) { 255 if (e instanceof EOFException) { 256 // This is expected when the process's input stream is closed. 257 return null; 258 } else { 259 throw e; 260 } 261 } 262 } 263 } 264 buildAdbCommand(String... args)265 private String[] buildAdbCommand(String... args) { 266 return ArrayUtil.buildArray(new String[] {"adb", "-s", getDevice().getSerialNumber()}, 267 args); 268 } 269 createRemoteDir(String remoteFilePath)270 private boolean createRemoteDir(String remoteFilePath) throws DeviceNotAvailableException { 271 if (getDevice().doesFileExist(remoteFilePath)) { 272 return true; 273 } 274 File remoteFile = new File(remoteFilePath); 275 String parentPath = remoteFile.getParent(); 276 if (parentPath != null) { 277 if (!createRemoteDir(parentPath)) { 278 return false; 279 } 280 } 281 getDevice().executeShellCommand(String.format("mkdir %s", remoteFilePath)); 282 return getDevice().doesFileExist(remoteFilePath); 283 } 284 } 285