You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
318 lines
9.8 KiB
318 lines
9.8 KiB
// 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 jclass FindClass(jvmtiEnv* jvmti, JNIEnv* env, const std::string& name) {
|
|
jclass res = env->FindClass(name.c_str());
|
|
if (res != nullptr) {
|
|
return res;
|
|
}
|
|
ScopedLocalRef<jthrowable> exc(env, env->ExceptionOccurred());
|
|
env->ExceptionClear();
|
|
// Try to find it in other classloaders.
|
|
env->PushLocalFrame(1 << 18);
|
|
do {
|
|
jint cnt;
|
|
jclass* klasses;
|
|
if (jvmti->GetLoadedClasses(&cnt, &klasses) != JVMTI_ERROR_NONE) {
|
|
LOG(ERROR) << "Unable to get loaded classes!";
|
|
break;
|
|
}
|
|
for (jint i = 0; i < cnt; i++) {
|
|
char* sig;
|
|
if (jvmti->GetClassSignature(klasses[i], &sig, nullptr) != JVMTI_ERROR_NONE) {
|
|
continue;
|
|
}
|
|
if (sig[0] == 'L' && DescriptorToFQCN(sig) == name) {
|
|
res = klasses[i];
|
|
break;
|
|
}
|
|
}
|
|
jvmti->Deallocate(reinterpret_cast<unsigned char*>(klasses));
|
|
} while (false);
|
|
res = reinterpret_cast<jclass>(env->PopLocalFrame(res));
|
|
if (res == nullptr && exc.get() != nullptr) {
|
|
env->Throw(exc.get());
|
|
}
|
|
return res;
|
|
}
|
|
|
|
static void RedefineClass(jvmtiEnv* jvmti, JNIEnv* env, const std::string& klass_name) {
|
|
jclass klass = nullptr;
|
|
if ((klass = FindClass(jvmti, env, klass_name)) == nullptr) {
|
|
LOG(WARNING) << "Failed to find class for " << klass_name;
|
|
env->ExceptionDescribe();
|
|
env->ExceptionClear();
|
|
return;
|
|
}
|
|
jvmti->RetransformClasses(1, &klass);
|
|
env->DeleteLocalRef(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{
|
|
.VMInit = CbVmInit,
|
|
.ClassFileLoadHook = CbClassFileLoadHook,
|
|
};
|
|
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
|