blob: 351a5172cb476e07b9093cec582a65e0307b3d09 [file] [log] [blame]
// 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 <android-base/logging.h>
#include <android-base/macros.h>
#include <fcntl.h>
#include <jni.h>
#include <jvmti.h>
#include <nativehelper/scoped_local_ref.h>
#include <unistd.h>
#include <cstddef>
#include <fstream>
#include <memory>
#include <mutex>
#include <sstream>
#include <string>
#include <unordered_set>
// 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(const 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,
[[maybe_unused]] JNIEnv* env,
[[maybe_unused]] jclass classBeingRedefined,
[[maybe_unused]] jobject loader,
const char* name,
[[maybe_unused]] jobject protectionDomain,
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, [[maybe_unused]] void* arg) {
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, [[maybe_unused]] jthread thr) {
// 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, [[maybe_unused]] void* reserved) {
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