blob: 8e276ed56c163c76e5f0273efc836ceb89dc7b11 [file] [log] [blame]
Sebastien Hertz477edf42015-08-11 15:39:02 +02001/*
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.tradefed.build.CtsBuildHelper;
20import com.android.tradefed.build.IBuildInfo;
21import com.android.tradefed.device.DeviceNotAvailableException;
22import com.android.tradefed.log.LogUtil.CLog;
23import com.android.tradefed.testtype.DeviceTestCase;
24import com.android.tradefed.testtype.IBuildReceiver;
25import com.android.tradefed.util.ArrayUtil;
26import com.android.tradefed.util.RunUtil;
27
28import java.io.BufferedReader;
29import java.io.EOFException;
30import java.io.File;
31import java.io.IOException;
32import java.io.InputStream;
33import java.io.InputStreamReader;
34import java.io.PrintWriter;
35import java.util.ArrayList;
36import java.util.List;
37
38/**
39 * Test to check non-zygote apps do not have an active JDWP connection.
40 */
41public 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
50 private static String getDeviceScriptFilepath() {
51 return DEVICE_LOCATION + File.separator + DEVICE_SCRIPT_FILENAME;
52 }
53
54 private static String getDeviceJarFilepath() {
55 return DEVICE_LOCATION + File.separator + DEVICE_JAR_FILENAME;
56 }
57
58 @Override
59 public void setBuild(IBuildInfo buildInfo) {
60 mCtsBuild = CtsBuildHelper.createBuildHelper(buildInfo);
61 }
62
63 @Override
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
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 */
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
142 private Process runScriptInBackground(String scriptFilepath) throws IOException {
143 String[] shellScriptCommand = buildAdbCommand("shell", scriptFilepath);
144 return RunUtil.getDefault().runCmdInBackground(shellScriptCommand);
145 }
146
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
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 */
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
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
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
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
265 private String[] buildAdbCommand(String... args) {
266 return ArrayUtil.buildArray(new String[] {"adb", "-s", getDevice().getSerialNumber()},
267 args);
268 }
269
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}