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