// Copyright (C) 2017 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 <atomic> #include <dlfcn.h> #include <iostream> #include <fstream> #include <iomanip> #include <jni.h> #include <jvmti.h> #include <unordered_map> #include <unordered_set> #include <memory> #include <mutex> #include <sstream> #include <string> #include <vector> namespace wrapagentproperties { using PropMap = std::unordered_map<std::string, std::string>; static constexpr const char* kOnLoad = "Agent_OnLoad"; static constexpr const char* kOnAttach = "Agent_OnAttach"; static constexpr const char* kOnUnload = "Agent_OnUnload"; struct ProxyJavaVM; using AgentLoadFunction = jint (*)(ProxyJavaVM*, const char*, void*); using AgentUnloadFunction = jint (*)(JavaVM*); // Global namespace. Shared by every usage of this wrapper unfortunately. // We need to keep track of them to call Agent_OnUnload. static std::mutex unload_mutex; struct Unloader { AgentUnloadFunction unload; }; static std::vector<Unloader> unload_functions; static jint CreateJvmtiEnv(ProxyJavaVM* vm, void** out_env, jint version); struct ProxyJavaVM { const struct JNIInvokeInterface* functions; JavaVM* real_vm; PropMap* map; void* dlopen_handle; AgentLoadFunction load; AgentLoadFunction attach; ProxyJavaVM(JavaVM* vm, const std::string& agent_lib, PropMap* map) : functions(CreateInvokeInterface()), real_vm(vm), map(map), dlopen_handle(dlopen(agent_lib.c_str(), RTLD_LAZY)), load(nullptr), attach(nullptr) { CHECK(dlopen_handle != nullptr) << "unable to open " << agent_lib; { std::lock_guard<std::mutex> lk(unload_mutex); unload_functions.push_back({ reinterpret_cast<AgentUnloadFunction>(dlsym(dlopen_handle, kOnUnload)), }); } attach = reinterpret_cast<AgentLoadFunction>(dlsym(dlopen_handle, kOnAttach)); load = reinterpret_cast<AgentLoadFunction>(dlsym(dlopen_handle, kOnLoad)); } // TODO Use this to cleanup static jint WrapDestroyJavaVM(ProxyJavaVM* vm) { return vm->real_vm->DestroyJavaVM(); } static jint WrapAttachCurrentThread(ProxyJavaVM* vm, JNIEnv** env, void* res) { return vm->real_vm->AttachCurrentThread(env, res); } static jint WrapDetachCurrentThread(ProxyJavaVM* vm) { return vm->real_vm->DetachCurrentThread(); } static jint WrapAttachCurrentThreadAsDaemon(ProxyJavaVM* vm, JNIEnv** env, void* res) { return vm->real_vm->AttachCurrentThreadAsDaemon(env, res); } static jint WrapGetEnv(ProxyJavaVM* vm, void** out_env, jint version) { switch (version) { case JVMTI_VERSION: case JVMTI_VERSION_1: case JVMTI_VERSION_1_1: case JVMTI_VERSION_1_2: return CreateJvmtiEnv(vm, out_env, version); default: if ((version & 0x30000000) == 0x30000000) { LOG(ERROR) << "Version number 0x" << std::hex << version << " looks like a JVMTI " << "version but it is not one that is recognized. The wrapper might not " << "function correctly! Continuing anyway."; } return vm->real_vm->GetEnv(out_env, version); } } static JNIInvokeInterface* CreateInvokeInterface() { JNIInvokeInterface* out = new JNIInvokeInterface; memset(out, 0, sizeof(JNIInvokeInterface)); out->DestroyJavaVM = reinterpret_cast<jint (*)(JavaVM*)>(WrapDestroyJavaVM); out->AttachCurrentThread = reinterpret_cast<jint(*)(JavaVM*, JNIEnv**, void*)>(WrapAttachCurrentThread); out->DetachCurrentThread = reinterpret_cast<jint(*)(JavaVM*)>(WrapDetachCurrentThread); out->GetEnv = reinterpret_cast<jint(*)(JavaVM*, void**, jint)>(WrapGetEnv); out->AttachCurrentThreadAsDaemon = reinterpret_cast<jint(*)(JavaVM*, JNIEnv**, void*)>(WrapAttachCurrentThreadAsDaemon); return out; } }; struct ExtraJvmtiInterface : public jvmtiInterface_1_ { ProxyJavaVM* proxy_vm; jvmtiInterface_1_ const* original_interface; static jvmtiError WrapDisposeEnvironment(jvmtiEnv* env) { ExtraJvmtiInterface* funcs = reinterpret_cast<ExtraJvmtiInterface*>( const_cast<jvmtiInterface_1_*>(env->functions)); jvmtiInterface_1_** out_iface = const_cast<jvmtiInterface_1_**>(&env->functions); *out_iface = const_cast<jvmtiInterface_1_*>(funcs->original_interface); funcs->original_interface->Deallocate(env, reinterpret_cast<unsigned char*>(funcs)); jvmtiError res = (*out_iface)->DisposeEnvironment(env); return res; } static jvmtiError WrapGetSystemProperty(jvmtiEnv* env, const char* prop, char** out) { ExtraJvmtiInterface* funcs = reinterpret_cast<ExtraJvmtiInterface*>( const_cast<jvmtiInterface_1_*>(env->functions)); if (funcs->proxy_vm->map->find(prop) != funcs->proxy_vm->map->end()) { std::string str_prop(prop); const std::string& val = funcs->proxy_vm->map->at(str_prop); jvmtiError res = env->Allocate(val.size() + 1, reinterpret_cast<unsigned char**>(out)); if (res != JVMTI_ERROR_NONE) { return res; } strcpy(*out, val.c_str()); return JVMTI_ERROR_NONE; } else { return funcs->original_interface->GetSystemProperty(env, prop, out); } } static jvmtiError WrapGetSystemProperties(jvmtiEnv* env, jint* cnt, char*** prop_ptr) { ExtraJvmtiInterface* funcs = reinterpret_cast<ExtraJvmtiInterface*>( const_cast<jvmtiInterface_1_*>(env->functions)); jint init_cnt; char** init_prop_ptr; jvmtiError res = funcs->original_interface->GetSystemProperties(env, &init_cnt, &init_prop_ptr); if (res != JVMTI_ERROR_NONE) { return res; } std::unordered_set<std::string> all_props; for (const auto& p : *funcs->proxy_vm->map) { all_props.insert(p.first); } for (jint i = 0; i < init_cnt; i++) { all_props.insert(init_prop_ptr[i]); env->Deallocate(reinterpret_cast<unsigned char*>(init_prop_ptr[i])); } env->Deallocate(reinterpret_cast<unsigned char*>(init_prop_ptr)); *cnt = all_props.size(); res = env->Allocate(all_props.size() * sizeof(char*), reinterpret_cast<unsigned char**>(prop_ptr)); if (res != JVMTI_ERROR_NONE) { return res; } char** out_prop_ptr = *prop_ptr; jint i = 0; for (const std::string& p : all_props) { res = env->Allocate(p.size() + 1, reinterpret_cast<unsigned char**>(&out_prop_ptr[i])); if (res != JVMTI_ERROR_NONE) { return res; } strcpy(out_prop_ptr[i], p.c_str()); i++; } CHECK_EQ(i, *cnt); return JVMTI_ERROR_NONE; } static jvmtiError WrapSetSystemProperty(jvmtiEnv* env, const char* prop, const char* val) { ExtraJvmtiInterface* funcs = reinterpret_cast<ExtraJvmtiInterface*>( const_cast<jvmtiInterface_1_*>(env->functions)); jvmtiError res = funcs->original_interface->SetSystemProperty(env, prop, val); if (res != JVMTI_ERROR_NONE) { return res; } if (funcs->proxy_vm->map->find(prop) != funcs->proxy_vm->map->end()) { funcs->proxy_vm->map->at(prop) = val; } return JVMTI_ERROR_NONE; } // TODO It would be way better to actually set up a full proxy like we did for JavaVM but the // number of functions makes it not worth it. static jint SetupProxyJvmtiEnv(ProxyJavaVM* vm, jvmtiEnv* real_env) { ExtraJvmtiInterface* new_iface = nullptr; if (JVMTI_ERROR_NONE != real_env->Allocate(sizeof(ExtraJvmtiInterface), reinterpret_cast<unsigned char**>(&new_iface))) { LOG(ERROR) << "Could not allocate extra space for new jvmti interface struct"; return JNI_ERR; } memcpy(new_iface, real_env->functions, sizeof(jvmtiInterface_1_)); new_iface->proxy_vm = vm; new_iface->original_interface = real_env->functions; // Replace these functions with the new ones. new_iface->DisposeEnvironment = WrapDisposeEnvironment; new_iface->GetSystemProperty = WrapGetSystemProperty; new_iface->GetSystemProperties = WrapGetSystemProperties; new_iface->SetSystemProperty = WrapSetSystemProperty; // Replace the functions table with our new one with replaced functions. jvmtiInterface_1_** out_iface = const_cast<jvmtiInterface_1_**>(&real_env->functions); *out_iface = new_iface; return JNI_OK; } }; static jint CreateJvmtiEnv(ProxyJavaVM* vm, void** out_env, jint version) { jint res = vm->real_vm->GetEnv(out_env, version); if (res != JNI_OK) { LOG(WARNING) << "Could not create jvmtiEnv to proxy!"; return res; } return ExtraJvmtiInterface::SetupProxyJvmtiEnv(vm, reinterpret_cast<jvmtiEnv*>(*out_env)); } enum class StartType { OnAttach, OnLoad, }; static jint CallNextAgent(StartType start, ProxyJavaVM* vm, std::string options, void* reserved) { // TODO It might be good to set it up so that the library is unloaded even if no jvmtiEnv's are // created but this isn't expected to be common so we will just not bother. return ((start == StartType::OnLoad) ? vm->load : vm->attach)(vm, options.c_str(), reserved); } static std::string substrOf(const std::string& s, size_t start, size_t end) { if (end == start) { return ""; } else if (end == std::string::npos) { end = s.size(); } return s.substr(start, end - start); } static PropMap* ReadPropMap(const std::string& file) { std::unique_ptr<PropMap> map(new PropMap); std::ifstream prop_file(file, std::ios::in); std::string line; while (std::getline(prop_file, line)) { if (line.size() == 0 || line[0] == '#') { continue; } if (line.find('=') == std::string::npos) { LOG(INFO) << "line: " << line << " didn't have a '='"; return nullptr; } std::string prop = substrOf(line, 0, line.find('=')); std::string val = substrOf(line, line.find('=') + 1, std::string::npos); LOG(INFO) << "Overriding property " << std::quoted(prop) << " new value is " << std::quoted(val); map->insert({prop, val}); } return map.release(); } static bool ParseArgs(const std::string& options, /*out*/std::string* prop_file, /*out*/std::string* agent_lib, /*out*/std::string* agent_options) { if (options.find(',') == std::string::npos) { LOG(ERROR) << "No agent lib in " << options; return false; } *prop_file = substrOf(options, 0, options.find(',')); *agent_lib = substrOf(options, options.find(',') + 1, options.find('=')); if (options.find('=') != std::string::npos) { *agent_options = substrOf(options, options.find('=') + 1, std::string::npos); } else { *agent_options = ""; } return true; } static jint AgentStart(StartType start, JavaVM* vm, char* options, void* reserved) { std::string agent_lib; std::string agent_options; std::string prop_file; if (!ParseArgs(options, /*out*/ &prop_file, /*out*/ &agent_lib, /*out*/ &agent_options)) { return JNI_ERR; } // It would be good to not leak these but since they will live for almost the whole program run // anyway it isn't a huge deal. PropMap* map = ReadPropMap(prop_file); if (map == nullptr) { LOG(ERROR) << "unable to read property file at " << std::quoted(prop_file) << "!"; return JNI_ERR; } ProxyJavaVM* proxy = new ProxyJavaVM(vm, agent_lib, map); LOG(INFO) << "Chaining to next agent[" << std::quoted(agent_lib) << "] options=[" << std::quoted(agent_options) << "]"; return CallNextAgent(start, proxy, agent_options, reserved); } // Late attachment (e.g. 'am attach-agent'). extern "C" JNIEXPORT jint JNICALL Agent_OnAttach(JavaVM *vm, char* options, void* reserved) { return AgentStart(StartType::OnAttach, vm, options, reserved); } // Early attachment // (e.g. 'java -agentpath:/path/to/libwrapagentproperties.so=/path/to/propfile,/path/to/wrapped.so=[ops]'). extern "C" JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM* jvm, char* options, void* reserved) { return AgentStart(StartType::OnLoad, jvm, options, reserved); } extern "C" JNIEXPORT void JNICALL Agent_OnUnload(JavaVM* jvm) { std::lock_guard<std::mutex> lk(unload_mutex); for (const Unloader& u : unload_functions) { u.unload(jvm); // Don't dlclose since some agents expect to still have code loaded after this. } unload_functions.clear(); } } // namespace wrapagentproperties