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