Add FridaUtils class.
Bug: 197228468
Bug: 215372727
Bug: 202200523
Test: Build STS, run a test using FridaUtils class and observe behavior.
Change-Id: I16ab4be53f3978b6d758a908e27f6595c081dfa8
diff --git a/libraries/sts-common-util/host-side/Android.bp b/libraries/sts-common-util/host-side/Android.bp
index f151cd3..7a07ef4 100644
--- a/libraries/sts-common-util/host-side/Android.bp
+++ b/libraries/sts-common-util/host-side/Android.bp
@@ -24,6 +24,7 @@
static_libs: [
"sts-common-util-lib",
+ "xz-java",
],
libs: [
diff --git a/libraries/sts-common-util/host-side/src/com/android/sts/common/FridaUtils.java b/libraries/sts-common-util/host-side/src/com/android/sts/common/FridaUtils.java
new file mode 100644
index 0000000..328fec4
--- /dev/null
+++ b/libraries/sts-common-util/host-side/src/com/android/sts/common/FridaUtils.java
@@ -0,0 +1,215 @@
+/*
+ * Copyright (C) 2022 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 com.android.sts.common;
+
+import static com.android.sts.common.CommandUtil.runAndCheck;
+import static java.util.stream.Collectors.toList;
+import static org.junit.Assert.assertEquals;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.net.URL;
+import java.net.URLConnection;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Stream;
+import org.tukaani.xz.XZInputStream;
+
+import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper;
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.util.FileUtil;
+import com.android.tradefed.util.RunUtil;
+
+public class FridaUtils implements AutoCloseable {
+ private static final String PRODUCT_CPU_ABI_KEY = "ro.product.cpu.abi";
+ private static final String PRODUCT_CPU_ABILIST_KEY = "ro.product.cpu.abilist";
+ private static final String FRIDA_PACKAGE = "frida-inject";
+ private static final String FRIDA_OS = "android";
+ private static final String TMP_PATH = "/data/local/tmp/";
+
+ private final ITestDevice device;
+ private final CompatibilityBuildHelper buildHelper;
+ private final String remoteFridaExeName;
+ private List<Integer> runningPids = new ArrayList<>();
+ private List<String> fridaFiles = new ArrayList<>();
+
+ private FridaUtils(ITestDevice device, IBuildInfo buildInfo, String fridaVersion)
+ throws DeviceNotAvailableException, UnsupportedOperationException, IOException {
+ this.device = device;
+ this.buildHelper = new CompatibilityBuildHelper(buildInfo);
+
+ // Figure out which Frida arch we should be using for our device
+ String fridaAbi = getFridaAbiFor(device);
+ String fridaExeName =
+ String.format("%s-%s-%s-%s", FRIDA_PACKAGE, fridaVersion, FRIDA_OS, fridaAbi);
+
+ // Download Frida if needed
+ File localFridaExe;
+ try {
+ localFridaExe = buildHelper.getTestFile(fridaExeName);
+ CLog.d("%s found at %s", fridaExeName, localFridaExe.getAbsolutePath());
+ } catch (FileNotFoundException e) {
+ String fridaUrl =
+ String.format(
+ "https://github.com/frida/frida/releases/download/%s/%s.xz",
+ fridaVersion, fridaExeName);
+ CLog.d("%s not found. Downloading from %s", fridaExeName, fridaUrl);
+ try {
+ URL url = new URL(fridaUrl);
+ URLConnection conn = url.openConnection();
+ XZInputStream in = new XZInputStream(conn.getInputStream());
+ File tmpOutput = FileUtil.createTempFile("STS", fridaExeName);
+ FileUtil.writeToFile(in, tmpOutput);
+ localFridaExe = new File(buildHelper.getTestsDir(), fridaExeName);
+ FileUtil.copyFile(tmpOutput, localFridaExe);
+ tmpOutput.delete();
+ } catch (Exception e2) {
+ CLog.e(
+ "Could not download Frida. Please manually download '%s' and extract to "
+ + "'%s', renaming the file to '%s' as necessary.",
+ fridaUrl, buildHelper.getTestsDir(), fridaExeName);
+ throw e2;
+ }
+ }
+
+ // Upload Frida binary to device
+ device.enableAdbRoot();
+ remoteFridaExeName = new File(TMP_PATH, localFridaExe.getName()).getAbsolutePath();
+ device.pushFile(localFridaExe, remoteFridaExeName);
+ runAndCheck(device, String.format("chmod a+x '%s'", remoteFridaExeName));
+ fridaFiles.add(remoteFridaExeName);
+ device.disableAdbRoot();
+ }
+
+ /**
+ * Find out which Frida binary we need and download it if needed.
+ *
+ * @param device device to use Frida on
+ * @param buildInfo test device build info (from test.getBuild())
+ * @return an AutoCloseable FridaUtils object that can be used to run Frida scripts with
+ */
+ public static FridaUtils withFrida(
+ ITestDevice device, IBuildInfo buildInfo, String fridaVersion)
+ throws DeviceNotAvailableException, UnsupportedOperationException, IOException {
+ return new FridaUtils(device, buildInfo, fridaVersion);
+ }
+
+ /**
+ * Upload and run frida script on given process.
+ *
+ * @param fridaJsScriptContent Content of the Frida JS script. Note: this is not a file name
+ * @param pid PID of the process to attach Frida to
+ * @return ByteArrayOutputStream containing stdout and stderr of frida command
+ */
+ public ByteArrayOutputStream withFridaScript(final String fridaJsScriptContent, int pid)
+ throws DeviceNotAvailableException, FileNotFoundException, IOException,
+ TimeoutException, InterruptedException {
+ // Upload Frida script to device
+ device.enableAdbRoot();
+ String uuid = UUID.randomUUID().toString();
+ String remoteFridaJsScriptName =
+ new File(TMP_PATH, "frida_" + uuid + ".js").getAbsolutePath();
+ device.pushString(fridaJsScriptContent, remoteFridaJsScriptName);
+ fridaFiles.add(remoteFridaJsScriptName);
+
+ // Execute Frida, binding to given PID, in the background
+ List<String> cmd =
+ List.of(
+ "adb",
+ "-s",
+ device.getSerialNumber(),
+ "shell",
+ remoteFridaExeName,
+ "-p",
+ String.valueOf(pid),
+ "-s",
+ remoteFridaJsScriptName,
+ "--runtime=v8");
+ ByteArrayOutputStream output = new ByteArrayOutputStream();
+ RunUtil.getDefault().runCmdInBackground(cmd, output);
+
+ // frida can fail to attach after a short pause so wait for that
+ TimeUnit.SECONDS.sleep(5);
+ try {
+ Map<Integer, String> pids =
+ ProcessUtil.waitProcessRunning(device, "^" + remoteFridaExeName);
+ assertEquals("Unexpected Frida processes with the same name", 1, pids.size());
+ runningPids.add(pids.keySet().iterator().next());
+ } catch (Exception e) {
+ CLog.e(e);
+ CLog.e("Frida attach output: %s", output.toString(StandardCharsets.UTF_8));
+ throw e;
+ }
+ device.disableAdbRoot();
+ return output;
+ }
+
+ @Override
+ /** Kill all running Frida processes and delete all files uploaded. */
+ public void close() throws DeviceNotAvailableException, TimeoutException {
+ device.enableAdbRoot();
+ for (Integer pid : runningPids) {
+ ProcessUtil.killPid(device, pid.intValue(), 10_000L);
+ }
+ for (String file : fridaFiles) {
+ device.deleteFile(file);
+ }
+ device.disableAdbRoot();
+ }
+
+ /**
+ * Return the best ABI of Frida that we should download for given device.
+ *
+ * <p>Throw UnsupportedOperationException if Frida does not support device's ABI.
+ */
+ private String getFridaAbiFor(ITestDevice device)
+ throws DeviceNotAvailableException, UnsupportedOperationException {
+ for (String abi : getSupportedAbis(device)) {
+ if (abi.startsWith("arm64")) {
+ return "arm64";
+ } else if (abi.startsWith("armeabi")) {
+ return "arm";
+ } else if (abi.startsWith("x86_64")) {
+ return "x86_64";
+ } else if (abi.startsWith("x86")) {
+ return "x86";
+ }
+ }
+ throw new UnsupportedOperationException(
+ String.format("Device %s is not supported by Frida", device.getSerialNumber()));
+ }
+
+ /** Return a list of supported ABIs by the device in order of preference. */
+ private List<String> getSupportedAbis(ITestDevice device) throws DeviceNotAvailableException {
+ String primaryAbi = device.getProperty(PRODUCT_CPU_ABI_KEY);
+ String[] supportedAbis = device.getProperty(PRODUCT_CPU_ABILIST_KEY).split(",");
+ return Stream.concat(Stream.of(primaryAbi), Arrays.stream(supportedAbis))
+ .distinct()
+ .collect(toList());
+ }
+}