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.
347 lines
13 KiB
347 lines
13 KiB
// 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));
|
|
auto it = funcs->proxy_vm->map->find(prop);
|
|
if (it != funcs->proxy_vm->map->end()) {
|
|
const std::string& val = it->second;
|
|
std::string str_prop(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;
|
|
}
|
|
auto it = funcs->proxy_vm->map->find(prop);
|
|
if (it != funcs->proxy_vm->map->end()) {
|
|
it->second = 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,
|
|
const 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
|
|
|