JdwpSecurityHostTest.java revision 423ee7387f9ed3c2de6fe607ebe7c2564b62bfbe
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.tradefed.build.IBuildInfo;
20import com.android.tradefed.build.IFolderBuildInfo;
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 IFolderBuildInfo mBuildInfo;
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        mBuildInfo = (IFolderBuildInfo) 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 = MigrationHelper.getTestFile(mBuildInfo, 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}
285