Add simple-force-redefine agent
This adds as an agent 'libforceredefine' that will redefine classes
for us on demand. This can be used to test the behavior of
class-redefinition on apps in various compilation states.
This agent takes an argument a file that contains a new-line separated
list of fully qualified class names (see jni FindClass documentation).
These classes will be redefined to add a NOP before every function.
This transformation will be reapplied if any additional transforms are
performed.
Test: adb shell am attach-agent $(adb shell pidof com.antonioleiva.bandhookkotlin) /data/local/tmp/libforceredefine.so=/data/local/tmp/classlist; \
adb shell am attach-agent $(adb shell pidof com.antonioleiva.bandhookkotlin) /data/local/tmp/libforceredefine.so=/data/local/tmp/classlist; \
adb shell am attach-agent $(adb shell pidof com.antonioleiva.bandhookkotlin) /data/local/tmp/libforceredefine.so=/data/local/tmp/classlist
Bug: 127477438
Change-Id: I0af60ab31970a68abf8e08e6036cc48e2e4438ac
diff --git a/tools/simple-force-redefine/Android.bp b/tools/simple-force-redefine/Android.bp
new file mode 100644
index 0000000..871f210
--- /dev/null
+++ b/tools/simple-force-redefine/Android.bp
@@ -0,0 +1,83 @@
+//
+// Copyright (C) 2019 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.
+//
+
+// Build variants {target,host} x {debug,ndebug} x {32,64}
+cc_defaults {
+ name: "forceredefine-defaults",
+ host_supported: true,
+ srcs: ["forceredefine.cc"],
+ defaults: ["art_defaults"],
+
+ // Note that this tool needs to be built for both 32-bit and 64-bit since it requires
+ // to be same ISA as what it is attached to.
+ compile_multilib: "both",
+
+ shared_libs: [
+ "libz",
+ "liblog",
+ ],
+ header_libs: [
+ "libopenjdkjvmti_headers",
+ // Annoyingly you aren't allowed to include even header-only non-ndk libs into an ndk build.
+ // Instead we put the directories this would bring in below in 'include_dirs'
+ // "libnativehelper_header_only",
+ ],
+ include_dirs: [
+ // NDK headers aren't available in platform NDK builds.
+ "libnativehelper/include_jni",
+ "libnativehelper/header_only_include",
+ ],
+ sdk_version: "current",
+ stl: "libc++_static",
+ target: {
+ android: {
+ static_libs: [
+ "slicer_ndk_no_rtti",
+ "libbase_ndk",
+ ],
+ },
+ host: {
+ static_libs: [
+ "slicer_no_rtti",
+ ],
+ shared_libs: [
+ "libbase",
+ ],
+ },
+ },
+ multilib: {
+ lib32: {
+ suffix: "32",
+ },
+ lib64: {
+ suffix: "64",
+ },
+ },
+ symlink_preferred_arch: true,
+}
+
+art_cc_library {
+ name: "libforceredefine",
+ defaults: ["forceredefine-defaults"],
+}
+
+art_cc_library {
+ name: "libforceredefined",
+ defaults: [
+ "art_debug_defaults",
+ "forceredefine-defaults",
+ ],
+}
diff --git a/tools/simple-force-redefine/README.md b/tools/simple-force-redefine/README.md
new file mode 100644
index 0000000..362c704
--- /dev/null
+++ b/tools/simple-force-redefine/README.md
@@ -0,0 +1,33 @@
+# forceredfine
+
+ForceRedefine is a JVMTI agent designed for testing how redefiniton affects running processes. It
+allows one to force classes to be redefined by writing to a fifo or give a process a list of
+classes to try redefining. Currently the redefinition is limited to adding (or removing) a single
+NOP at the beginning of every function in the class.
+
+# Usage
+### Build
+> `make libforceredefine`
+
+The libraries will be built for 32-bit, 64-bit, host and target. Below examples
+assume you want to use the 64-bit version.
+
+#### ART
+> `adb shell setenforce 0`
+>
+> `adb push $ANDROID_PRODUCT_OUT/system/lib64/libforceredefine.so /data/local/tmp/`
+>
+> `echo java/util/ArrayList > /tmp/classlist`
+> `echo java/util/Arrays >> /tmp/classlist`
+> `adb push /tmp/classlist /data/local/tmp/`
+>
+> `adb shell am attach-agent $(adb shell pidof some.deubggable.app) /data/local/tmp/libforceredefine.so=/data/local/tmp/classlist`
+
+Since the agent has no static state it can be attached multiple times to the same process.
+
+> `adb shell am attach-agent $(adb shell pidof some.deubggable.app) /data/local/tmp/libforceredefine.so=/data/local/tmp/classlist`
+> `adb shell am attach-agent $(adb shell pidof some.deubggable.app) /data/local/tmp/libforceredefine.so=/data/local/tmp/classlist2`
+> `adb shell am attach-agent $(adb shell pidof some.deubggable.app) /data/local/tmp/libforceredefine.so=/data/local/tmp/classlist`
+
+One can also use fifos to send classes interactively to the process. (TODO: Have the agent
+continue reading from the fifo even after it gets an EOF.)
\ No newline at end of file
diff --git a/tools/simple-force-redefine/forceredefine.cc b/tools/simple-force-redefine/forceredefine.cc
new file mode 100644
index 0000000..f96626f
--- /dev/null
+++ b/tools/simple-force-redefine/forceredefine.cc
@@ -0,0 +1,281 @@
+// Copyright (C) 2019 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.
+//
+
+#include "__mutex_base"
+#include <cstddef>
+#include <fcntl.h>
+#include <fstream>
+#include <memory>
+#include <sstream>
+#include <string>
+#include <unistd.h>
+#include <unordered_set>
+
+#include <android-base/logging.h>
+#include <android-base/macros.h>
+
+#include <nativehelper/scoped_local_ref.h>
+
+#include <jni.h>
+#include <jvmti.h>
+
+// Slicer's headers have code that triggers these warnings. b/65298177
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wunused-parameter"
+#pragma clang diagnostic ignored "-Wsign-compare"
+#include <slicer/code_ir.h>
+#include <slicer/dex_bytecode.h>
+#include <slicer/dex_ir.h>
+#include <slicer/dex_ir_builder.h>
+#include <slicer/reader.h>
+#include <slicer/writer.h>
+#pragma clang diagnostic pop
+
+namespace forceredefine {
+
+namespace {
+
+struct AgentInfo {
+ std::fstream stream;
+ std::unordered_set<std::string> classes;
+ std::mutex mutex;
+};
+
+// Converts a class name to a type descriptor
+// (ex. "java.lang.String" to "Ljava/lang/String;")
+std::string classNameToDescriptor(const char* className) {
+ std::stringstream ss;
+ ss << "L";
+ for (auto p = className; *p != '\0'; ++p) {
+ ss << (*p == '.' ? '/' : *p);
+ }
+ ss << ";";
+ return ss.str();
+}
+
+// Converts a descriptor (Lthis/style/of/name;) to a jni-FindClass style Fully-qualified class name
+// (this/style/of/name).
+std::string DescriptorToFQCN(const std::string& descriptor) {
+ return descriptor.substr(1, descriptor.size() - 2);
+}
+
+static AgentInfo* GetAgentInfo(jvmtiEnv* jvmti) {
+ AgentInfo* ai = nullptr;
+ CHECK_EQ(jvmti->GetEnvironmentLocalStorage(reinterpret_cast<void**>(&ai)), JVMTI_ERROR_NONE);
+ CHECK(ai != nullptr);
+ return ai;
+}
+
+class JvmtiAllocator : public dex::Writer::Allocator {
+ public:
+ explicit JvmtiAllocator(jvmtiEnv* jvmti) : jvmti_(jvmti) {}
+ void* Allocate(size_t size) override {
+ unsigned char* res = nullptr;
+ jvmti_->Allocate(size, &res);
+ return res;
+ }
+ void Free(void* ptr) override {
+ jvmti_->Deallocate(reinterpret_cast<unsigned char*>(ptr));
+ }
+
+ private:
+ jvmtiEnv* jvmti_;
+};
+
+static void Transform(std::shared_ptr<ir::DexFile> ir) {
+ std::unique_ptr<ir::Builder> builder;
+ for (auto& method : ir->encoded_methods) {
+ // Do not look into abstract/bridge/native/synthetic methods.
+ if ((method->access_flags &
+ (dex::kAccAbstract | dex::kAccBridge | dex::kAccNative | dex::kAccSynthetic)) != 0) {
+ continue;
+ }
+
+ struct AddNopVisitor : public lir::Visitor {
+ explicit AddNopVisitor(lir::CodeIr* cir) : cir_(cir) {}
+
+ bool Visit(lir::Bytecode* bc) override {
+ if (seen_first_inst) {
+ return false;
+ }
+ seen_first_inst = true;
+ auto new_inst = cir_->Alloc<lir::Bytecode>();
+ new_inst->opcode = dex::OP_NOP;
+ cir_->instructions.InsertBefore(bc, new_inst);
+ return true;
+ }
+
+ lir::CodeIr* cir_;
+ bool seen_first_inst = false;
+ };
+
+ lir::CodeIr c(method.get(), ir);
+ AddNopVisitor visitor(&c);
+ for (auto it = c.instructions.begin(); it != c.instructions.end(); ++it) {
+ lir::Instruction* fi = *it;
+ if (fi->Accept(&visitor)) {
+ break;
+ }
+ }
+ c.Assemble();
+ }
+}
+
+static void CbClassFileLoadHook(jvmtiEnv* jvmti,
+ JNIEnv* env ATTRIBUTE_UNUSED,
+ jclass classBeingRedefined ATTRIBUTE_UNUSED,
+ jobject loader ATTRIBUTE_UNUSED,
+ const char* name,
+ jobject protectionDomain ATTRIBUTE_UNUSED,
+ jint classDataLen,
+ const unsigned char* classData,
+ jint* newClassDataLen,
+ unsigned char** newClassData) {
+ std::string desc(classNameToDescriptor(name));
+ std::string fqcn(DescriptorToFQCN(desc));
+ AgentInfo* ai = GetAgentInfo(jvmti);
+ {
+ std::lock_guard<std::mutex> mu(ai->mutex);
+ if (ai->classes.find(fqcn) == ai->classes.end()) {
+ return;
+ }
+ }
+ LOG(INFO) << "Got CFLH for " << name << " on env " << static_cast<void*>(jvmti);
+ JvmtiAllocator allocator(jvmti);
+ dex::Reader reader(classData, classDataLen);
+ dex::u4 index = reader.FindClassIndex(desc.c_str());
+ reader.CreateClassIr(index);
+ std::shared_ptr<ir::DexFile> ir(reader.GetIr());
+ Transform(ir);
+ dex::Writer writer(ir);
+ size_t new_size;
+ *newClassData = writer.CreateImage(&allocator, &new_size);
+ *newClassDataLen = new_size;
+}
+
+static void RedefineClass(jvmtiEnv* jvmti, JNIEnv* env, const std::string& klass_name) {
+ jclass klass = nullptr;
+ if ((klass = env->FindClass(klass_name.c_str())) == nullptr || env->ExceptionCheck()) {
+ LOG(WARNING) << "Failed to find class for " << klass_name;
+ env->ExceptionDescribe();
+ env->ExceptionClear();
+ return;
+ }
+ jvmti->RetransformClasses(1, &klass);
+}
+
+static void AgentMain(jvmtiEnv* jvmti, JNIEnv* jni, void* arg ATTRIBUTE_UNUSED) {
+ AgentInfo* ai = GetAgentInfo(jvmti);
+ std::string klass_name;
+ jvmti->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_CLASS_FILE_LOAD_HOOK, nullptr);
+ // TODO Replace this with something that can read from a fifo and ignore the 'EOF's.
+ while (std::getline(ai->stream, klass_name, '\n')) {
+ LOG(INFO) << "Redefining class " << klass_name << " with " << static_cast<void*>(jvmti);
+ {
+ std::lock_guard<std::mutex> mu(ai->mutex);
+ ai->classes.insert(klass_name);
+ }
+ RedefineClass(jvmti, jni, klass_name);
+ }
+}
+
+static void CbVmInit(jvmtiEnv* jvmti, JNIEnv* env, jthread thr ATTRIBUTE_UNUSED) {
+ // Create a Thread object.
+ ScopedLocalRef<jobject> thread_name(env, env->NewStringUTF("Agent Thread"));
+ if (thread_name.get() == nullptr) {
+ env->ExceptionDescribe();
+ env->ExceptionClear();
+ return;
+ }
+ ScopedLocalRef<jclass> thread_klass(env, env->FindClass("java/lang/Thread"));
+ if (thread_klass.get() == nullptr) {
+ env->ExceptionDescribe();
+ env->ExceptionClear();
+ return;
+ }
+ ScopedLocalRef<jobject> thread(env, env->AllocObject(thread_klass.get()));
+ if (thread.get() == nullptr) {
+ env->ExceptionDescribe();
+ env->ExceptionClear();
+ return;
+ }
+
+ env->CallNonvirtualVoidMethod(
+ thread.get(),
+ thread_klass.get(),
+ env->GetMethodID(thread_klass.get(), "<init>", "(Ljava/lang/String;)V"),
+ thread_name.get());
+ env->CallVoidMethod(thread.get(), env->GetMethodID(thread_klass.get(), "setPriority", "(I)V"), 1);
+ env->CallVoidMethod(
+ thread.get(), env->GetMethodID(thread_klass.get(), "setDaemon", "(Z)V"), JNI_TRUE);
+
+ jvmti->RunAgentThread(thread.get(), AgentMain, nullptr, JVMTI_THREAD_MIN_PRIORITY);
+}
+
+} // namespace
+
+template <bool kIsOnLoad>
+static jint AgentStart(JavaVM* vm, char* options, void* reserved ATTRIBUTE_UNUSED) {
+ jvmtiEnv* jvmti = nullptr;
+
+ if (vm->GetEnv(reinterpret_cast<void**>(&jvmti), JVMTI_VERSION_1_1) != JNI_OK ||
+ jvmti == nullptr) {
+ LOG(ERROR) << "unable to obtain JVMTI env.";
+ return JNI_ERR;
+ }
+ std::string sopts(options);
+ AgentInfo* ai = new AgentInfo;
+ ai->stream.open(options, std::ios_base::in);
+ if (!ai->stream.is_open()) {
+ PLOG(ERROR) << "Could not open file " << options << " for triggering class-reload";
+ return JNI_ERR;
+ }
+
+ jvmtiCapabilities caps{
+ .can_retransform_classes = 1,
+ };
+ if (jvmti->AddCapabilities(&caps) != JVMTI_ERROR_NONE) {
+ LOG(ERROR) << "Unable to get retransform_classes capability!";
+ return JNI_ERR;
+ }
+ jvmtiEventCallbacks cb{
+ .ClassFileLoadHook = CbClassFileLoadHook,
+ .VMInit = CbVmInit,
+ };
+ jvmti->SetEventCallbacks(&cb, sizeof(cb));
+ jvmti->SetEnvironmentLocalStorage(reinterpret_cast<void*>(ai));
+ if (kIsOnLoad) {
+ jvmti->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_VM_INIT, nullptr);
+ } else {
+ JNIEnv* jni = nullptr;
+ vm->GetEnv(reinterpret_cast<void**>(&jni), JNI_VERSION_1_2);
+ jthread thr;
+ jvmti->GetCurrentThread(&thr);
+ CbVmInit(jvmti, jni, thr);
+ }
+ return JNI_OK;
+}
+
+// Late attachment (e.g. 'am attach-agent').
+extern "C" JNIEXPORT jint JNICALL Agent_OnAttach(JavaVM* vm, char* options, void* reserved) {
+ return AgentStart<false>(vm, options, reserved);
+}
+
+// Early attachment
+extern "C" JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM* jvm, char* options, void* reserved) {
+ return AgentStart<true>(jvm, options, reserved);
+}
+
+} // namespace forceredefine