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