UPDATE
Add Linux and macOS support, batch class transformations with a disk cache, and add cross-platform release packaging. Also improve runtime namespace handling, keybind editing, and startup behavior.
This commit is contained in:
1 parent
910a126f04
commit
38ff6d387c
29 files changed
+2234
-491
No files matched your search
@@ -1,14 +1,16 @@
|
||||
//! JVMTI state and the two natives of dev.zprestige.prestige.inject.NativeBridge.
|
||||
use crate::jni::*;
|
||||
use std::ffi::{c_char, c_void};
|
||||
use std::sync::atomic::{AtomicPtr, AtomicUsize, Ordering};
|
||||
use std::sync::atomic::{AtomicPtr, Ordering};
|
||||
use std::sync::Mutex;
|
||||
|
||||
static JVMTI: AtomicPtr<*const *const c_void> = AtomicPtr::new(std::ptr::null_mut());
|
||||
/// jclass whose bytes the ClassFileLoadHook should capture during the current RetransformClasses call (0 = none).
|
||||
static CAPTURE_TARGET: AtomicUsize = AtomicUsize::new(0);
|
||||
static CAPTURED: Mutex<Option<Vec<u8>>> = Mutex::new(None);
|
||||
/// Serialises classBytes calls: CAPTURE_TARGET holds a thread-local jclass reference, so only one thread may capture at a time.
|
||||
/// jclass references (as addresses) whose bytes the ClassFileLoadHook captures during the current RetransformClasses call.
|
||||
/// Never held across the JVMTI call: the hook runs on the calling thread and locks it again.
|
||||
static CAPTURE_TARGETS: Mutex<Vec<usize>> = Mutex::new(Vec::new());
|
||||
/// Captured bytes, index-aligned with CAPTURE_TARGETS.
|
||||
static CAPTURED: Mutex<Vec<Option<Vec<u8>>>> = Mutex::new(Vec::new());
|
||||
/// Serialises classBytes calls: CAPTURE_TARGETS holds thread-local jclass references, so only one thread may capture at a time.
|
||||
static CAPTURE_LOCK: Mutex<()> = Mutex::new(());
|
||||
|
||||
/// Acquires the JVMTI capabilities and enables the class file load hook. Called once from the worker thread.
|
||||
@@ -22,7 +24,7 @@ pub unsafe fn init_jvmti(vm: VmPtr) -> Result<(), String> {
|
||||
if rc != 0 {
|
||||
return Err(format!("AddCapabilities failed ({rc})"));
|
||||
}
|
||||
enable_class_file_load_hook(jvmti, class_file_load_hook)?;
|
||||
install_class_file_load_hook(jvmti, class_file_load_hook)?;
|
||||
JVMTI.store(jvmti, Ordering::SeqCst);
|
||||
Ok(())
|
||||
}
|
||||
@@ -40,14 +42,22 @@ unsafe extern "system" fn class_file_load_hook(
|
||||
_new_len: *mut JInt,
|
||||
_new_data: *mut *mut u8,
|
||||
) {
|
||||
let target = CAPTURE_TARGET.load(Ordering::SeqCst);
|
||||
if target == 0 || class_being_redefined.is_null() || data.is_null() {
|
||||
if class_being_redefined.is_null() || data.is_null() {
|
||||
return;
|
||||
}
|
||||
let targets: Vec<usize> = match CAPTURE_TARGETS.lock() {
|
||||
Ok(t) if !t.is_empty() => t.clone(),
|
||||
_ => return,
|
||||
};
|
||||
let jni = Jni::new(env);
|
||||
if jni.is_same_object(class_being_redefined, target as JObject) {
|
||||
if let Ok(mut slot) = CAPTURED.lock() {
|
||||
*slot = Some(std::slice::from_raw_parts(data, data_len as usize).to_vec());
|
||||
for (i, &target) in targets.iter().enumerate() {
|
||||
if jni.is_same_object(class_being_redefined, target as JObject) {
|
||||
if let Ok(mut slots) = CAPTURED.lock() {
|
||||
if let Some(slot) = slots.get_mut(i) {
|
||||
*slot = Some(std::slice::from_raw_parts(data, data_len as usize).to_vec());
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -58,22 +68,39 @@ pub unsafe extern "system" fn native_class_bytes(env: EnvPtr, _cls: JClass, targ
|
||||
if jvmti.is_null() {
|
||||
return std::ptr::null_mut();
|
||||
}
|
||||
let _serial = CAPTURE_LOCK.lock();
|
||||
if let Ok(mut slot) = CAPTURED.lock() {
|
||||
*slot = None;
|
||||
match capture(jvmti, &[target]).and_then(|mut v| v.pop().flatten()) {
|
||||
Some(bytes) => to_byte_array(&Jni::new(env), &bytes),
|
||||
None => std::ptr::null_mut(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Retransforms `classes` in one JVMTI call and returns the captured bytes, index-aligned (None = not captured).
|
||||
unsafe fn capture(jvmti: JvmtiPtr, classes: &[JClass]) -> Option<Vec<Option<Vec<u8>>>> {
|
||||
let _serial = CAPTURE_LOCK.lock();
|
||||
if let Ok(mut c) = CAPTURED.lock() {
|
||||
*c = vec![None; classes.len()];
|
||||
}
|
||||
if let Ok(mut t) = CAPTURE_TARGETS.lock() {
|
||||
*t = classes.iter().map(|&c| c as usize).collect();
|
||||
}
|
||||
let enabled = set_class_file_load_hook_enabled(jvmti, true);
|
||||
let rc = if enabled.is_ok() { retransform_classes(jvmti, classes) } else { -1 };
|
||||
let _ = set_class_file_load_hook_enabled(jvmti, false);
|
||||
if let Ok(mut t) = CAPTURE_TARGETS.lock() {
|
||||
t.clear();
|
||||
}
|
||||
if let Err(e) = enabled {
|
||||
crate::log!("classBytes: {e}");
|
||||
return None;
|
||||
}
|
||||
CAPTURE_TARGET.store(target as usize, Ordering::SeqCst);
|
||||
let rc = retransform_class(jvmti, target);
|
||||
CAPTURE_TARGET.store(0, Ordering::SeqCst);
|
||||
if rc != 0 {
|
||||
crate::log!("RetransformClasses failed ({rc})");
|
||||
return std::ptr::null_mut();
|
||||
return None;
|
||||
}
|
||||
let bytes = CAPTURED.lock().ok().and_then(|mut slot| slot.take());
|
||||
let Some(bytes) = bytes else {
|
||||
return std::ptr::null_mut();
|
||||
};
|
||||
let jni = Jni::new(env);
|
||||
CAPTURED.lock().ok().map(|mut c| std::mem::take(&mut *c))
|
||||
}
|
||||
|
||||
unsafe fn to_byte_array(jni: &Jni, bytes: &[u8]) -> JObject {
|
||||
let array = jni.new_byte_array(bytes.len() as JInt);
|
||||
if !array.is_null() {
|
||||
jni.set_byte_array_region(array, 0, bytes.len() as JInt, bytes.as_ptr());
|
||||
@@ -81,6 +108,80 @@ pub unsafe extern "system" fn native_class_bytes(env: EnvPtr, _cls: JClass, targ
|
||||
array
|
||||
}
|
||||
|
||||
/// byte[][] classBytesBatch(Class[]): classBytes for many classes with a single RetransformClasses call (one safepoint, one pass
|
||||
/// over the other agents' transformers). Entries are null for classes that were not captured; the result is null on failure.
|
||||
pub unsafe extern "system" fn native_class_bytes_batch(env: EnvPtr, _cls: JClass, targets: JObject) -> JObject {
|
||||
let jvmti = JVMTI.load(Ordering::SeqCst);
|
||||
if jvmti.is_null() || targets.is_null() {
|
||||
return std::ptr::null_mut();
|
||||
}
|
||||
let jni = Jni::new(env);
|
||||
let n = jni.get_array_length(targets);
|
||||
if n <= 0 {
|
||||
return std::ptr::null_mut();
|
||||
}
|
||||
jni.ensure_local_capacity(2 * n + 16);
|
||||
let classes: Vec<JClass> = (0..n).map(|i| jni.get_object_array_element(targets, i)).collect();
|
||||
let Some(captured) = capture(jvmti, &classes) else {
|
||||
return std::ptr::null_mut();
|
||||
};
|
||||
let byte_array_class = jni.find_class("[B");
|
||||
let result = jni.new_object_array(n, byte_array_class, std::ptr::null_mut());
|
||||
if result.is_null() {
|
||||
return std::ptr::null_mut();
|
||||
}
|
||||
for (i, bytes) in captured.iter().enumerate() {
|
||||
if let Some(bytes) = bytes {
|
||||
let array = to_byte_array(&jni, bytes);
|
||||
if !array.is_null() {
|
||||
jni.set_object_array_element(result, i as JInt, array);
|
||||
}
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// int redefineBatch(Class[], byte[][]): JVMTI RedefineClasses of many classes in one call (one safepoint). It is all or nothing:
|
||||
/// the JVMTI error code is returned (0 = success) and no class is changed on failure.
|
||||
pub unsafe extern "system" fn native_redefine_batch(env: EnvPtr, _cls: JClass, targets: JObject, bytes: JObject) -> JInt {
|
||||
let jvmti = JVMTI.load(Ordering::SeqCst);
|
||||
if jvmti.is_null() || targets.is_null() || bytes.is_null() {
|
||||
return -1;
|
||||
}
|
||||
let jni = Jni::new(env);
|
||||
let n = jni.get_array_length(targets);
|
||||
if n <= 0 || jni.get_array_length(bytes) != n {
|
||||
return -1;
|
||||
}
|
||||
jni.ensure_local_capacity(2 * n + 16);
|
||||
let mut buffers: Vec<Vec<u8>> = Vec::with_capacity(n as usize);
|
||||
let mut classes: Vec<JClass> = Vec::with_capacity(n as usize);
|
||||
for i in 0..n {
|
||||
let array = jni.get_object_array_element(bytes, i);
|
||||
if array.is_null() {
|
||||
return -1;
|
||||
}
|
||||
let len = jni.get_array_length(array);
|
||||
if len < 0 {
|
||||
return -1;
|
||||
}
|
||||
let mut buf = vec![0u8; len as usize];
|
||||
jni.get_byte_array_region(array, 0, len, buf.as_mut_ptr());
|
||||
buffers.push(buf);
|
||||
classes.push(jni.get_object_array_element(targets, i));
|
||||
}
|
||||
let defs: Vec<ClassDefinition> = classes
|
||||
.iter()
|
||||
.zip(&buffers)
|
||||
.map(|(&klass, buf)| ClassDefinition { klass, class_byte_count: buf.len() as JInt, class_bytes: buf.as_ptr() })
|
||||
.collect();
|
||||
let rc = redefine_classes(jvmti, &defs);
|
||||
if rc != 0 {
|
||||
crate::log!("RedefineClasses (batch of {n}) failed ({rc})");
|
||||
}
|
||||
rc
|
||||
}
|
||||
|
||||
/// int redefine(Class, byte[]): JVMTI RedefineClasses of a single class; returns the JVMTI error code (0 = success).
|
||||
pub unsafe extern "system" fn native_redefine(env: EnvPtr, _cls: JClass, target: JClass, bytes: JObject) -> JInt {
|
||||
let jvmti = JVMTI.load(Ordering::SeqCst);
|
||||
@@ -111,6 +212,7 @@ pub unsafe extern "system" fn native_log(env: EnvPtr, _cls: JClass, message: JOb
|
||||
crate::log!("[java] {text}");
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
extern "system" {
|
||||
fn FreeLibraryAndExitThread(module: *mut c_void, exit_code: u32) -> !;
|
||||
}
|
||||
@@ -129,11 +231,18 @@ pub unsafe extern "system" fn native_release(env: EnvPtr, cls: JClass) {
|
||||
if rc != JNI_OK {
|
||||
crate::log!("release: UnregisterNatives failed ({rc})");
|
||||
}
|
||||
crate::log!("[java] released, unloading the loader");
|
||||
let module = crate::MODULE as usize;
|
||||
// The thread below runs code from this DLL until FreeLibraryAndExitThread; the delay lets in-flight native calls return.
|
||||
let _ = std::thread::Builder::new().name("prestige-unload".into()).spawn(move || {
|
||||
std::thread::sleep(std::time::Duration::from_millis(2000));
|
||||
FreeLibraryAndExitThread(module as *mut c_void, 0);
|
||||
});
|
||||
#[cfg(windows)]
|
||||
{
|
||||
crate::log!("[java] released, unloading the loader");
|
||||
let module = crate::MODULE as usize;
|
||||
// The thread below runs code from this DLL until FreeLibraryAndExitThread; the delay lets in-flight native calls return.
|
||||
let _ = std::thread::Builder::new().name("prestige-unload".into()).spawn(move || {
|
||||
std::thread::sleep(std::time::Duration::from_millis(2000));
|
||||
FreeLibraryAndExitThread(module as *mut c_void, 0);
|
||||
});
|
||||
}
|
||||
// On unix the library is deliberately NOT dlclose'd: a thread of ours would return into unmapped code (and the
|
||||
// JVM may still hold the agent handle). The hook and natives are gone; the code just stays resident.
|
||||
#[cfg(unix)]
|
||||
crate::log!("[java] released (library stays resident on this platform)");
|
||||
}
|
||||
@@ -12,7 +12,6 @@ pub type JBool = u8;
|
||||
pub type EnvPtr = *mut *const *const c_void;
|
||||
|
||||
pub const JNI_OK: JInt = 0;
|
||||
pub const JNI_VERSION_1_8: JInt = 0x0001_0008;
|
||||
pub const JVMTI_VERSION_1_2: JInt = 0x3001_0200;
|
||||
|
||||
#[repr(C)]
|
||||
@@ -60,9 +59,6 @@ impl Jni {
|
||||
pub unsafe fn new_global_ref(&self, o: JObject) -> JObject {
|
||||
f!(self, 21, unsafe extern "system" fn(EnvPtr, JObject) -> JObject)(self.env, o)
|
||||
}
|
||||
pub unsafe fn delete_local_ref(&self, o: JObject) {
|
||||
f!(self, 23, unsafe extern "system" fn(EnvPtr, JObject))(self.env, o)
|
||||
}
|
||||
pub unsafe fn is_same_object(&self, a: JObject, b: JObject) -> bool {
|
||||
f!(self, 24, unsafe extern "system" fn(EnvPtr, JObject, JObject) -> JBool)(self.env, a, b) != 0
|
||||
}
|
||||
@@ -108,6 +104,15 @@ impl Jni {
|
||||
pub unsafe fn get_array_length(&self, a: JObject) -> JInt {
|
||||
f!(self, 171, unsafe extern "system" fn(EnvPtr, JObject) -> JInt)(self.env, a)
|
||||
}
|
||||
pub unsafe fn get_object_array_element(&self, a: JObject, i: JInt) -> JObject {
|
||||
f!(self, 173, unsafe extern "system" fn(EnvPtr, JObject, JInt) -> JObject)(self.env, a, i)
|
||||
}
|
||||
pub unsafe fn set_object_array_element(&self, a: JObject, i: JInt, v: JObject) {
|
||||
f!(self, 174, unsafe extern "system" fn(EnvPtr, JObject, JInt, JObject))(self.env, a, i, v)
|
||||
}
|
||||
pub unsafe fn ensure_local_capacity(&self, capacity: JInt) -> JInt {
|
||||
f!(self, 26, unsafe extern "system" fn(EnvPtr, JInt) -> JInt)(self.env, capacity)
|
||||
}
|
||||
pub unsafe fn new_object_array(&self, len: JInt, c: JClass, init: JObject) -> JObject {
|
||||
f!(self, 172, unsafe extern "system" fn(EnvPtr, JInt, JClass, JObject) -> JObject)(self.env, len, c, init)
|
||||
}
|
||||
@@ -126,9 +131,6 @@ impl Jni {
|
||||
pub unsafe fn unregister_natives(&self, c: JClass) -> JInt {
|
||||
f!(self, 216, unsafe extern "system" fn(EnvPtr, JClass) -> JInt)(self.env, c)
|
||||
}
|
||||
pub unsafe fn exception_check(&self) -> bool {
|
||||
f!(self, 228, unsafe extern "system" fn(EnvPtr) -> JBool)(self.env) != 0
|
||||
}
|
||||
|
||||
/// Returns "ExceptionClass: message" of the pending exception and clears it.
|
||||
pub unsafe fn take_exception(&self) -> Option<String> {
|
||||
@@ -236,8 +238,8 @@ pub unsafe fn add_capabilities(j: JvmtiPtr, caps: &Capabilities) -> JInt {
|
||||
f(j, caps)
|
||||
}
|
||||
|
||||
/// Installs the ClassFileLoadHook callback (field 5 of jvmtiEventCallbacks) and enables the event.
|
||||
pub unsafe fn enable_class_file_load_hook(j: JvmtiPtr, hook: ClassFileLoadHook) -> Result<(), String> {
|
||||
/// Installs the ClassFileLoadHook callback (field 5 of jvmtiEventCallbacks); the event itself stays off until enabled.
|
||||
pub unsafe fn install_class_file_load_hook(j: JvmtiPtr, hook: ClassFileLoadHook) -> Result<(), String> {
|
||||
let mut callbacks = [0usize; 40];
|
||||
callbacks[4] = hook as usize;
|
||||
let set_callbacks = std::mem::transmute::<*const c_void, unsafe extern "system" fn(JvmtiPtr, *const usize, JInt) -> JInt>(*(*j).add(121));
|
||||
@@ -245,17 +247,28 @@ pub unsafe fn enable_class_file_load_hook(j: JvmtiPtr, hook: ClassFileLoadHook)
|
||||
if rc != 0 {
|
||||
return Err(format!("SetEventCallbacks failed ({rc})"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Turns the ClassFileLoadHook event on or off. It is only on while class bytes are being captured: with the event enabled the
|
||||
/// JVM hands the bytes of every class it loads to the agent, which slows class loading down for the whole game.
|
||||
pub unsafe fn set_class_file_load_hook_enabled(j: JvmtiPtr, enabled: bool) -> Result<(), String> {
|
||||
let set_mode = std::mem::transmute::<*const c_void, unsafe extern "C" fn(JvmtiPtr, JInt, JInt, JObject, ...) -> JInt>(*(*j).add(1));
|
||||
let rc = set_mode(j, 1, EVENT_CLASS_FILE_LOAD_HOOK, std::ptr::null_mut());
|
||||
let rc = set_mode(j, enabled as JInt, EVENT_CLASS_FILE_LOAD_HOOK, std::ptr::null_mut());
|
||||
if rc != 0 {
|
||||
return Err(format!("SetEventNotificationMode failed ({rc})"));
|
||||
return Err(format!("SetEventNotificationMode({enabled}) failed ({rc})"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub unsafe fn retransform_class(j: JvmtiPtr, class: JClass) -> JInt {
|
||||
pub unsafe fn retransform_classes(j: JvmtiPtr, classes: &[JClass]) -> JInt {
|
||||
let f = std::mem::transmute::<*const c_void, unsafe extern "system" fn(JvmtiPtr, JInt, *const JClass) -> JInt>(*(*j).add(151));
|
||||
f(j, 1, &class)
|
||||
f(j, classes.len() as JInt, classes.as_ptr())
|
||||
}
|
||||
|
||||
pub unsafe fn redefine_classes(j: JvmtiPtr, defs: &[ClassDefinition]) -> JInt {
|
||||
let f = std::mem::transmute::<*const c_void, unsafe extern "system" fn(JvmtiPtr, JInt, *const ClassDefinition) -> JInt>(*(*j).add(86));
|
||||
f(j, defs.len() as JInt, defs.as_ptr())
|
||||
}
|
||||
|
||||
pub unsafe fn redefine_class(j: JvmtiPtr, def: &ClassDefinition) -> JInt {
|
||||
|
||||
@@ -10,14 +10,18 @@ mod worker;
|
||||
|
||||
use std::ffi::c_void;
|
||||
|
||||
#[cfg(windows)]
|
||||
const DLL_PROCESS_ATTACH: u32 = 1;
|
||||
|
||||
#[cfg(windows)]
|
||||
extern "system" {
|
||||
fn DisableThreadLibraryCalls(module: *mut c_void) -> i32;
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
pub(crate) static mut MODULE: *mut c_void = std::ptr::null_mut();
|
||||
|
||||
#[cfg(windows)]
|
||||
#[no_mangle]
|
||||
pub extern "system" fn DllMain(module: *mut c_void, reason: u32, _reserved: *mut c_void) -> i32 {
|
||||
if reason == DLL_PROCESS_ATTACH {
|
||||
@@ -30,3 +34,30 @@ pub extern "system" fn DllMain(module: *mut c_void, reason: u32, _reserved: *mut
|
||||
}
|
||||
1
|
||||
}
|
||||
|
||||
/// The JavaVM handed to Agent_OnAttach / Agent_OnLoad by the JVM (unix); read by the worker.
|
||||
#[cfg(unix)]
|
||||
pub(crate) static VM: std::sync::atomic::AtomicPtr<c_void> = std::sync::atomic::AtomicPtr::new(std::ptr::null_mut());
|
||||
|
||||
#[cfg(unix)]
|
||||
fn start_worker(vm: *mut c_void) -> i32 {
|
||||
VM.store(vm, std::sync::atomic::Ordering::SeqCst);
|
||||
match std::thread::Builder::new().name("prestige-loader".into()).spawn(worker::run) {
|
||||
Ok(_) => 0,
|
||||
Err(_) => -1,
|
||||
}
|
||||
}
|
||||
|
||||
/// Called by HotSpot when the library is loaded through the attach protocol (JVMTI.agent_load).
|
||||
#[cfg(unix)]
|
||||
#[no_mangle]
|
||||
pub extern "C" fn Agent_OnAttach(vm: *mut c_void, _options: *mut std::ffi::c_char, _reserved: *mut c_void) -> i32 {
|
||||
start_worker(vm)
|
||||
}
|
||||
|
||||
/// Called by the JVM when the library is given as -agentpath at start-up.
|
||||
#[cfg(unix)]
|
||||
#[no_mangle]
|
||||
pub extern "C" fn Agent_OnLoad(vm: *mut c_void, _options: *mut std::ffi::c_char, _reserved: *mut c_void) -> i32 {
|
||||
start_worker(vm)
|
||||
}
|
||||
@@ -1,21 +1,55 @@
|
||||
use std::ffi::c_void;
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::OnceLock;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
#[cfg(windows)]
|
||||
extern "system" {
|
||||
fn GetModuleFileNameW(module: *mut c_void, buf: *mut u16, size: u32) -> u32;
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[repr(C)]
|
||||
struct DlInfo {
|
||||
dli_fname: *const std::ffi::c_char,
|
||||
dli_fbase: *mut c_void,
|
||||
dli_sname: *const std::ffi::c_char,
|
||||
dli_saddr: *mut c_void,
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
extern "C" {
|
||||
fn dladdr(addr: *const c_void, info: *mut DlInfo) -> i32;
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn module_path() -> PathBuf {
|
||||
fn anchor() {}
|
||||
let mut info = DlInfo { dli_fname: std::ptr::null(), dli_fbase: std::ptr::null_mut(), dli_sname: std::ptr::null(), dli_saddr: std::ptr::null_mut() };
|
||||
let ok = unsafe { dladdr(anchor as fn() as usize as *const c_void, &mut info) };
|
||||
if ok != 0 && !info.dli_fname.is_null() {
|
||||
let name = unsafe { std::ffi::CStr::from_ptr(info.dli_fname) };
|
||||
PathBuf::from(std::ffi::OsStr::from_bytes(name.to_bytes()))
|
||||
} else {
|
||||
std::env::temp_dir().join("libprestige_native")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn module_path() -> PathBuf {
|
||||
let mut buf = [0u16; 1024];
|
||||
let len = unsafe { GetModuleFileNameW(crate::MODULE, buf.as_mut_ptr(), buf.len() as u32) } as usize;
|
||||
PathBuf::from(String::from_utf16_lossy(&buf[..len]))
|
||||
}
|
||||
|
||||
fn log_path() -> &'static PathBuf {
|
||||
static PATH: OnceLock<PathBuf> = OnceLock::new();
|
||||
PATH.get_or_init(|| {
|
||||
let mut buf = [0u16; 1024];
|
||||
let len = unsafe { GetModuleFileNameW(crate::MODULE, buf.as_mut_ptr(), buf.len() as u32) } as usize;
|
||||
let dll = PathBuf::from(String::from_utf16_lossy(&buf[..len]));
|
||||
dll.with_file_name("prestige-native.log")
|
||||
module_path().with_file_name("prestige-native.log")
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
//! The loader worker: JVM discovery, payload installation into Fabric's Knot class loader, native registration, bootstrap.
|
||||
use crate::bridge::{init_jvmti, native_class_bytes, native_log, native_redefine, native_release};
|
||||
use crate::bridge::{init_jvmti, native_class_bytes, native_class_bytes_batch, native_log, native_redefine, native_redefine_batch, native_release};
|
||||
use crate::jni::*;
|
||||
use crate::log;
|
||||
use std::ffi::{c_char, c_void};
|
||||
#[cfg(windows)]
|
||||
use std::ffi::c_char;
|
||||
use std::ffi::c_void;
|
||||
use std::path::PathBuf;
|
||||
#[cfg(windows)]
|
||||
use std::time::Duration;
|
||||
|
||||
#[cfg(windows)]
|
||||
extern "system" {
|
||||
fn GetModuleHandleA(name: *const c_char) -> *mut c_void;
|
||||
fn GetProcAddress(module: *mut c_void, name: *const c_char) -> *mut c_void;
|
||||
fn GetCurrentProcessId() -> u32;
|
||||
}
|
||||
|
||||
static PAYLOAD: &[u8] = include_bytes!(env!("PRESTIGE_PAYLOAD_JAR"));
|
||||
@@ -18,7 +21,7 @@ const NATIVE_BRIDGE: &str = "dev.zprestige.prestige.inject.NativeBridge";
|
||||
const BOOTSTRAP: &str = "dev.zprestige.prestige.inject.Bootstrap";
|
||||
|
||||
pub fn run() {
|
||||
log!("worker started, pid {}", unsafe { GetCurrentProcessId() });
|
||||
log!("worker started, pid {}", std::process::id());
|
||||
match unsafe { install() } {
|
||||
Ok(()) => log!("bootstrap returned"),
|
||||
Err(e) => log!("FAILED: {e}"),
|
||||
@@ -50,7 +53,7 @@ unsafe fn install() -> Result<(), String> {
|
||||
register_natives(&jni, loader)?;
|
||||
log!("natives registered");
|
||||
|
||||
let bootstrap = class_for_name(&jni, BOOTSTRAP, loader)?;
|
||||
let bootstrap = class_for_name(&jni, BOOTSTRAP, loader, true)?;
|
||||
let start = jni.get_static_method_id(bootstrap, "start", "()V");
|
||||
jni.check("resolve Bootstrap.start")?;
|
||||
// Bootstrap.start waits for the client, installs the hooks and schedules the client initialisation.
|
||||
@@ -59,7 +62,18 @@ unsafe fn install() -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The JavaVM passed to Agent_OnAttach / Agent_OnLoad.
|
||||
#[cfg(unix)]
|
||||
unsafe fn find_vm() -> Result<VmPtr, String> {
|
||||
let vm = crate::VM.load(std::sync::atomic::Ordering::SeqCst) as VmPtr;
|
||||
if vm.is_null() {
|
||||
return Err("no JavaVM was passed to the agent".into());
|
||||
}
|
||||
Ok(vm)
|
||||
}
|
||||
|
||||
/// Waits for jvm.dll and a created JavaVM (the injector may run before Minecraft has started its JVM).
|
||||
#[cfg(windows)]
|
||||
unsafe fn find_vm() -> Result<VmPtr, String> {
|
||||
let mut module = std::ptr::null_mut();
|
||||
for _ in 0..600 {
|
||||
@@ -91,7 +105,7 @@ unsafe fn find_vm() -> Result<VmPtr, String> {
|
||||
fn write_payload() -> Result<PathBuf, String> {
|
||||
let dir = std::env::temp_dir().join("PrestigeLoader");
|
||||
std::fs::create_dir_all(&dir).map_err(|e| format!("create {}: {e}", dir.display()))?;
|
||||
let path = dir.join(format!("prestige-payload-{}.jar", unsafe { GetCurrentProcessId() }));
|
||||
let path = dir.join(format!("prestige-payload-{}.jar", std::process::id()));
|
||||
std::fs::write(&path, PAYLOAD).map_err(|e| format!("write {}: {e}", path.display()))?;
|
||||
Ok(path)
|
||||
}
|
||||
@@ -99,9 +113,8 @@ fn write_payload() -> Result<PathBuf, String> {
|
||||
/// FabricLauncherBase.getLauncher().addToClassPath(path) so the payload and the game classes share the Knot class loader.
|
||||
/// Returns a global reference to the Knot target class loader.
|
||||
unsafe fn add_to_knot(jni: &Jni, jar: &std::path::Path) -> Result<JObject, String> {
|
||||
// A thread attached from native code resolves classes through the system class loader, which holds fabric-loader.
|
||||
let base = jni.find_class("net/fabricmc/loader/impl/launch/FabricLauncherBase");
|
||||
jni.check("FabricLauncherBase (is this a Fabric instance?)")?;
|
||||
jni.ensure_local_capacity(4096);
|
||||
let (base, fabric_loader) = locate_fabric(jni)?;
|
||||
let get_launcher = jni.get_static_method_id(base, "getLauncher", "()Lnet/fabricmc/loader/impl/launch/FabricLauncher;");
|
||||
jni.check("FabricLauncherBase.getLauncher")?;
|
||||
let launcher = jni.call_static_object(base, get_launcher, &[]);
|
||||
@@ -110,8 +123,7 @@ unsafe fn add_to_knot(jni: &Jni, jar: &std::path::Path) -> Result<JObject, Strin
|
||||
return Err("Fabric launcher is not available yet".into());
|
||||
}
|
||||
|
||||
let launcher_class = jni.find_class("net/fabricmc/loader/impl/launch/FabricLauncher");
|
||||
jni.check("FabricLauncher")?;
|
||||
let launcher_class = load_fabric_class(jni, "net.fabricmc.loader.impl.launch.FabricLauncher", fabric_loader)?;
|
||||
let add = jni.get_method_id(launcher_class, "addToClassPath", "(Ljava/nio/file/Path;[Ljava/lang/String;)V");
|
||||
jni.check("FabricLauncher.addToClassPath")?;
|
||||
let target = jni.get_method_id(launcher_class, "getTargetClassLoader", "()Ljava/lang/ClassLoader;");
|
||||
@@ -140,13 +152,98 @@ unsafe fn add_to_knot(jni: &Jni, jar: &std::path::Path) -> Result<JObject, Strin
|
||||
Ok(jni.new_global_ref(loader))
|
||||
}
|
||||
|
||||
/// Class.forName(name, true, loader)
|
||||
unsafe fn class_for_name(jni: &Jni, name: &str, loader: JObject) -> Result<JClass, String> {
|
||||
const FABRIC_BASE: &str = "net.fabricmc.loader.impl.launch.FabricLauncherBase";
|
||||
|
||||
/// Finds the class loader that can see fabric-loader. The system class loader is tried first (official launcher, most
|
||||
/// others); launchers that run the game from their own URLClassLoader (Prism/MultiMC, many modpack launchers) keep
|
||||
/// fabric-loader out of it, so every thread's context class loader is probed too. Returns the FabricLauncherBase class
|
||||
/// and the loader it was found through (null = FindClass / system loader).
|
||||
unsafe fn locate_fabric(jni: &Jni) -> Result<(JClass, JObject), String> {
|
||||
let base = jni.find_class(&FABRIC_BASE.replace('.', "/"));
|
||||
if jni.take_exception().is_none() && !base.is_null() {
|
||||
return Ok((base, std::ptr::null_mut()));
|
||||
}
|
||||
log!("FabricLauncherBase is not visible to the system class loader, probing thread context class loaders");
|
||||
let loaders = thread_loaders(jni)?;
|
||||
for &loader in &loaders {
|
||||
if let Ok(class) = class_for_name(jni, FABRIC_BASE, loader, false) {
|
||||
log!("fabric-loader found through {}", describe(jni, loader));
|
||||
return Ok((class, loader));
|
||||
}
|
||||
}
|
||||
let seen: Vec<String> = loaders.iter().map(|&l| describe(jni, l)).collect();
|
||||
Err(format!(
|
||||
"net.fabricmc.loader is not visible to any class loader ({} probed: {}); is this a Fabric instance?",
|
||||
seen.len(),
|
||||
seen.join(" | ")
|
||||
))
|
||||
}
|
||||
|
||||
/// Distinct context class loaders of all live threads (the game threads inherit the launcher's loader).
|
||||
unsafe fn thread_loaders(jni: &Jni) -> Result<Vec<JObject>, String> {
|
||||
let thread_class = jni.find_class("java/lang/Thread");
|
||||
let get_all = jni.get_static_method_id(thread_class, "getAllStackTraces", "()Ljava/util/Map;");
|
||||
let get_context = jni.get_method_id(thread_class, "getContextClassLoader", "()Ljava/lang/ClassLoader;");
|
||||
let map_class = jni.find_class("java/util/Map");
|
||||
let key_set = jni.get_method_id(map_class, "keySet", "()Ljava/util/Set;");
|
||||
let set_class = jni.find_class("java/util/Set");
|
||||
let to_array = jni.get_method_id(set_class, "toArray", "()[Ljava/lang/Object;");
|
||||
jni.check("Thread/Map/Set lookup")?;
|
||||
let map = jni.call_static_object(thread_class, get_all, &[]);
|
||||
jni.check("Thread.getAllStackTraces")?;
|
||||
let set = jni.call_object(jni_non_null(map)?, key_set, &[]);
|
||||
let threads = jni.call_object(jni_non_null(set)?, to_array, &[]);
|
||||
jni.check("thread list")?;
|
||||
let mut loaders: Vec<JObject> = Vec::new();
|
||||
for i in 0..jni.get_array_length(jni_non_null(threads)?) {
|
||||
let thread = jni.get_object_array_element(threads, i);
|
||||
if thread.is_null() {
|
||||
continue;
|
||||
}
|
||||
let loader = jni.call_object(thread, get_context, &[]);
|
||||
if jni.take_exception().is_some() || loader.is_null() {
|
||||
continue;
|
||||
}
|
||||
if !loaders.iter().any(|&known| jni.is_same_object(known, loader)) {
|
||||
loaders.push(loader);
|
||||
}
|
||||
}
|
||||
Ok(loaders)
|
||||
}
|
||||
|
||||
unsafe fn jni_non_null(o: JObject) -> Result<JObject, String> {
|
||||
if o.is_null() { Err("unexpected null from the JVM".into()) } else { Ok(o) }
|
||||
}
|
||||
|
||||
/// Object.toString() of a class loader for the log.
|
||||
unsafe fn describe(jni: &Jni, o: JObject) -> String {
|
||||
let class = jni.get_object_class(o);
|
||||
let to_string = jni.get_method_id(class, "toString", "()Ljava/lang/String;");
|
||||
if to_string.is_null() {
|
||||
jni.exception_clear();
|
||||
return "<loader>".into();
|
||||
}
|
||||
let s = jni.call_object(o, to_string, &[]);
|
||||
if jni.take_exception().is_some() || s.is_null() { "<loader>".into() } else { jni.get_string_utf(s) }
|
||||
}
|
||||
|
||||
/// Loads a fabric-loader class through the loader found by `locate_fabric` (null = system class loader).
|
||||
unsafe fn load_fabric_class(jni: &Jni, name: &str, loader: JObject) -> Result<JClass, String> {
|
||||
if loader.is_null() {
|
||||
let class = jni.find_class(&name.replace('.', "/"));
|
||||
jni.check(name)?;
|
||||
return Ok(class);
|
||||
}
|
||||
class_for_name(jni, name, loader, true)
|
||||
}
|
||||
|
||||
/// Class.forName(name, initialize, loader)
|
||||
unsafe fn class_for_name(jni: &Jni, name: &str, loader: JObject, initialize: bool) -> Result<JClass, String> {
|
||||
let class_class = jni.find_class("java/lang/Class");
|
||||
let for_name = jni.get_static_method_id(class_class, "forName", "(Ljava/lang/String;ZLjava/lang/ClassLoader;)Ljava/lang/Class;");
|
||||
jni.check("Class.forName")?;
|
||||
let name_string = jni.new_string(name);
|
||||
let class = jni.call_static_object(class_class, for_name, &[obj(name_string), boolean(true), obj(loader)]);
|
||||
let class = jni.call_static_object(class_class, for_name, &[obj(name_string), boolean(initialize), obj(loader)]);
|
||||
jni.check(&format!("load {name}"))?;
|
||||
if class.is_null() {
|
||||
return Err(format!("{name} not found"));
|
||||
@@ -155,7 +252,7 @@ unsafe fn class_for_name(jni: &Jni, name: &str, loader: JObject) -> Result<JClas
|
||||
}
|
||||
|
||||
unsafe fn register_natives(jni: &Jni, loader: JObject) -> Result<(), String> {
|
||||
let class = class_for_name(jni, NATIVE_BRIDGE, loader)?;
|
||||
let class = class_for_name(jni, NATIVE_BRIDGE, loader, true)?;
|
||||
let methods = [
|
||||
NativeMethod {
|
||||
name: c"classBytes".as_ptr(),
|
||||
@@ -167,6 +264,16 @@ unsafe fn register_natives(jni: &Jni, loader: JObject) -> Result<(), String> {
|
||||
signature: c"(Ljava/lang/Class;[B)I".as_ptr(),
|
||||
fn_ptr: native_redefine as *const () as usize as *mut c_void,
|
||||
},
|
||||
NativeMethod {
|
||||
name: c"classBytesBatch".as_ptr(),
|
||||
signature: c"([Ljava/lang/Class;)[[B".as_ptr(),
|
||||
fn_ptr: native_class_bytes_batch as *const () as usize as *mut c_void,
|
||||
},
|
||||
NativeMethod {
|
||||
name: c"redefineBatch".as_ptr(),
|
||||
signature: c"([Ljava/lang/Class;[[B)I".as_ptr(),
|
||||
fn_ptr: native_redefine_batch as *const () as usize as *mut c_void,
|
||||
},
|
||||
NativeMethod {
|
||||
name: c"release".as_ptr(),
|
||||
signature: c"()V".as_ptr(),
|
||||
|
||||
Reference in new issue
Block a user