blob: 8e276ed56c163c76e5f0273efc836ceb89dc7b11 [file] [log] [blame]
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.jdwpsecurity.cts;
import com.android.cts.tradefed.build.CtsBuildHelper;
import com.android.tradefed.build.IBuildInfo;
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.testtype.DeviceTestCase;
import com.android.tradefed.testtype.IBuildReceiver;
import com.android.tradefed.util.ArrayUtil;
import com.android.tradefed.util.RunUtil;
import java.io.BufferedReader;
import java.io.EOFException;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
/**
* Test to check non-zygote apps do not have an active JDWP connection.
*/
public class JdwpSecurityHostTest extends DeviceTestCase implements IBuildReceiver {
private static final String DEVICE_LOCATION = "/data/local/tmp/jdwpsecurity";
private static final String DEVICE_SCRIPT_FILENAME = "jdwptest";
private static final String DEVICE_JAR_FILENAME = "CtsJdwpApp.jar";
private static final String JAR_MAIN_CLASS_NAME = "com.android.cts.jdwpsecurity.JdwpTest";
private CtsBuildHelper mCtsBuild;
private static String getDeviceScriptFilepath() {
return DEVICE_LOCATION + File.separator + DEVICE_SCRIPT_FILENAME;
}
private static String getDeviceJarFilepath() {
return DEVICE_LOCATION + File.separator + DEVICE_JAR_FILENAME;
}
@Override
public void setBuild(IBuildInfo buildInfo) {
mCtsBuild = CtsBuildHelper.createBuildHelper(buildInfo);
}
@Override
protected void setUp() throws Exception {
super.setUp();
// Create test directory on the device.
createRemoteDir(DEVICE_LOCATION);
// Also create the dalvik-cache directory. It needs to exist before the runtime starts.
createRemoteDir(DEVICE_LOCATION + File.separator + "dalvik-cache");
// Create and push script on the device.
File tempFile = createScriptTempFile();
try {
boolean success = getDevice().pushFile(tempFile, getDeviceScriptFilepath());
assertTrue("Failed to push script to " + getDeviceScriptFilepath(), success);
} finally {
if (tempFile != null) {
tempFile.delete();
}
}
// Make the script executable.
getDevice().executeShellCommand("chmod 755 " + getDeviceScriptFilepath());
// Push jar file.
File jarFile = mCtsBuild.getTestApp(DEVICE_JAR_FILENAME);
boolean success = getDevice().pushFile(jarFile, getDeviceJarFilepath());
assertTrue("Failed to push jar file to " + getDeviceScriptFilepath(), success);
}
@Override
protected void tearDown() throws Exception {
// Delete the whole test directory on the device.
getDevice().executeShellCommand(String.format("rm -r %s", DEVICE_LOCATION));
super.tearDown();
}
/**
* Tests a non-zygote app does not have a JDWP connection, thus not being
* debuggable.
*
* Runs a script executing a Java app (jar file) with app_process,
* without forking from zygote. Then checks its pid is not returned
* by 'adb jdwp', meaning it has no JDWP connection and cannot be
* debugged.
*
* @throws Exception
*/
public void testNonZygoteProgramIsNotDebuggable() throws Exception {
String scriptFilepath = getDeviceScriptFilepath();
Process scriptProcess = null;
String scriptPid = null;
List<String> activeJdwpPids = null;
try {
// Run the script on the background so it's running when we collect the list of
// pids with a JDWP connection using 'adb jdwp'.
// command.
scriptProcess = runScriptInBackground(scriptFilepath);
// On startup, the script will print its pid on its output.
scriptPid = readScriptPid(scriptProcess);
// Collect the list of pids with a JDWP connection.
activeJdwpPids = getJdwpPids();
} finally {
// Stop the script.
if (scriptProcess != null) {
scriptProcess.destroy();
}
}
assertNotNull("Failed to get script pid", scriptPid);
assertNotNull("Failed to get active JDWP pids", activeJdwpPids);
assertFalse("Test app should not have an active JDWP connection" +
" (pid " + scriptPid + " is returned by 'adb jdwp')",
activeJdwpPids.contains(scriptPid));
}
private Process runScriptInBackground(String scriptFilepath) throws IOException {
String[] shellScriptCommand = buildAdbCommand("shell", scriptFilepath);
return RunUtil.getDefault().runCmdInBackground(shellScriptCommand);
}
private String readScriptPid(Process scriptProcess) throws IOException {
BufferedReader br = null;
try {
br = new BufferedReader(new InputStreamReader(scriptProcess.getInputStream()));
// We only expect to read one line containing the pid.
return br.readLine();
} finally {
if (br != null) {
br.close();
}
}
}
private List<String> getJdwpPids() throws Exception {
return new AdbJdwpOutputReader().listPidsWithAdbJdwp();
}
/**
* Creates the script file on the host so it can be pushed onto the device.
*
* @return the script file
* @throws IOException
*/
private static File createScriptTempFile() throws IOException {
File tempFile = File.createTempFile("jdwptest", ".tmp");
PrintWriter pw = null;
try {
pw = new PrintWriter(tempFile);
// We need a dalvik-cache in /data/local/tmp so we have read-write access.
// Note: this will cause the runtime to optimize the DEX file (contained in
// the jar file) before executing it.
pw.println(String.format("export ANDROID_DATA=%s", DEVICE_LOCATION));
pw.println(String.format("export CLASSPATH=%s", getDeviceJarFilepath()));
pw.println(String.format("exec app_process /system/bin %s \"$@\"",
JAR_MAIN_CLASS_NAME));
} finally {
if (pw != null) {
pw.close();
}
}
return tempFile;
}
/**
* Helper class collecting all pids returned by 'adb jdwp' command.
*/
private class AdbJdwpOutputReader implements Runnable {
/**
* A list of all pids with a JDWP connection returned by 'adb jdwp'.
*/
private final List<String> lines = new ArrayList<String>();
/**
* The input stream of the process running 'adb jdwp'.
*/
private InputStream in;
public List<String> listPidsWithAdbJdwp() throws Exception {
// The 'adb jdwp' command does not return normally, it only terminates with Ctrl^C.
// Therefore we cannot use ITestDevice.executeAdbCommand but need to run that command
// in the background. Since we know the tested app is already running, we only need to
// capture the output for a short amount of time before stopping the 'adb jdwp'
// command.
String[] adbJdwpCommand = buildAdbCommand("jdwp");
Process adbProcess = RunUtil.getDefault().runCmdInBackground(adbJdwpCommand);
in = adbProcess.getInputStream();
// Read the output for 5s in a separate thread before stopping the command.
Thread t = new Thread(this);
t.start();
Thread.sleep(5000);
// Kill the 'adb jdwp' process and wait for the thread to stop.
adbProcess.destroy();
t.join();
return lines;
}
@Override
public void run() {
BufferedReader br = null;
try {
br = new BufferedReader(new InputStreamReader(in));
String line;
while ((line = readLineIgnoreException(br)) != null) {
lines.add(line);
}
} catch (IOException e) {
CLog.e(e);
} finally {
if (br != null) {
try {
br.close();
} catch (IOException e) {
// Ignore it.
}
}
}
}
private String readLineIgnoreException(BufferedReader reader) throws IOException {
try {
return reader.readLine();
} catch (IOException e) {
if (e instanceof EOFException) {
// This is expected when the process's input stream is closed.
return null;
} else {
throw e;
}
}
}
}
private String[] buildAdbCommand(String... args) {
return ArrayUtil.buildArray(new String[] {"adb", "-s", getDevice().getSerialNumber()},
args);
}
private boolean createRemoteDir(String remoteFilePath) throws DeviceNotAvailableException {
if (getDevice().doesFileExist(remoteFilePath)) {
return true;
}
File remoteFile = new File(remoteFilePath);
String parentPath = remoteFile.getParent();
if (parentPath != null) {
if (!createRemoteDir(parentPath)) {
return false;
}
}
getDevice().executeShellCommand(String.format("mkdir %s", remoteFilePath));
return getDevice().doesFileExist(remoteFilePath);
}
}