fixes
This commit is contained in:
1 parent
83aea53e57
commit
aa4f6b6ddc
842 files changed
+47284
-91563
No files matched your search
@@ -0,0 +1,8 @@
|
||||
[package]
|
||||
name = "prestige_native"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
build = "build.rs"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
@@ -0,0 +1,35 @@
|
||||
//! Locates the remapped Prestige payload jar (built by `gradlew remapJar`) so lib.rs can embed it with include_bytes!.
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn main() {
|
||||
println!("cargo:rerun-if-env-changed=PRESTIGE_PAYLOAD");
|
||||
let jar = match env::var("PRESTIGE_PAYLOAD") {
|
||||
Ok(p) => PathBuf::from(p),
|
||||
Err(_) => newest_remapped_jar(),
|
||||
};
|
||||
if !jar.is_file() {
|
||||
panic!("payload jar not found: {} (run `gradlew remapJar` or set PRESTIGE_PAYLOAD)", jar.display());
|
||||
}
|
||||
println!("cargo:rerun-if-changed={}", jar.display());
|
||||
println!("cargo:rustc-env=PRESTIGE_PAYLOAD_JAR={}", jar.display());
|
||||
}
|
||||
|
||||
fn newest_remapped_jar() -> PathBuf {
|
||||
let libs = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()).join("../../../build/libs");
|
||||
let mut best: Option<(std::time::SystemTime, PathBuf)> = None;
|
||||
if let Ok(entries) = fs::read_dir(&libs) {
|
||||
for entry in entries.flatten() {
|
||||
let name = entry.file_name().to_string_lossy().to_string();
|
||||
if !name.starts_with("prestige-") || !name.ends_with(".jar") || name.ends_with("-dev.jar") || name.ends_with("-sources.jar") {
|
||||
continue;
|
||||
}
|
||||
let time = entry.metadata().and_then(|m| m.modified()).unwrap_or(std::time::UNIX_EPOCH);
|
||||
if best.as_ref().map_or(true, |(t, _)| time > *t) {
|
||||
best = Some((time, entry.path()));
|
||||
}
|
||||
}
|
||||
}
|
||||
best.map(|(_, p)| p).unwrap_or_else(|| libs.join("prestige-0.1.0.jar"))
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
//! 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::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);
|
||||
|
||||
/// Acquires the JVMTI capabilities and enables the class file load hook. Called once from the worker thread.
|
||||
pub unsafe fn init_jvmti(vm: VmPtr) -> Result<(), String> {
|
||||
let jvmti = get_jvmti(vm)?;
|
||||
let mut caps = Capabilities::default();
|
||||
for bit in [CAN_REDEFINE_CLASSES, CAN_REDEFINE_ANY_CLASS, CAN_GENERATE_ALL_CLASS_HOOK_EVENTS, CAN_RETRANSFORM_CLASSES, CAN_RETRANSFORM_ANY_CLASS] {
|
||||
caps.set(bit);
|
||||
}
|
||||
let rc = add_capabilities(jvmti, &caps);
|
||||
if rc != 0 {
|
||||
return Err(format!("AddCapabilities failed ({rc})"));
|
||||
}
|
||||
enable_class_file_load_hook(jvmti, class_file_load_hook)?;
|
||||
JVMTI.store(jvmti, Ordering::SeqCst);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Copies the class bytes the JVM hands to us while a classBytes() call retransforms its target; never modifies anything.
|
||||
unsafe extern "system" fn class_file_load_hook(
|
||||
_jvmti: JvmtiPtr,
|
||||
env: EnvPtr,
|
||||
class_being_redefined: JClass,
|
||||
_loader: JObject,
|
||||
_name: *const c_char,
|
||||
_domain: JObject,
|
||||
data_len: JInt,
|
||||
data: *const u8,
|
||||
_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() {
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// byte[] classBytes(Class): the bytes the JVM currently holds for the class (HotSpot reconstitutes them for retransformation).
|
||||
pub unsafe extern "system" fn native_class_bytes(env: EnvPtr, _cls: JClass, target: JClass) -> JObject {
|
||||
let jvmti = JVMTI.load(Ordering::SeqCst);
|
||||
if jvmti.is_null() {
|
||||
return std::ptr::null_mut();
|
||||
}
|
||||
if let Ok(mut slot) = CAPTURED.lock() {
|
||||
*slot = 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();
|
||||
}
|
||||
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);
|
||||
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());
|
||||
}
|
||||
array
|
||||
}
|
||||
|
||||
/// 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);
|
||||
if jvmti.is_null() || bytes.is_null() {
|
||||
return -1;
|
||||
}
|
||||
let jni = Jni::new(env);
|
||||
let len = jni.get_array_length(bytes);
|
||||
let mut buf = vec![0u8; len as usize];
|
||||
jni.get_byte_array_region(bytes, 0, len, buf.as_mut_ptr());
|
||||
let def = ClassDefinition { klass: target, class_byte_count: len, class_bytes: buf.as_ptr() };
|
||||
let rc = redefine_class(jvmti, &def);
|
||||
if rc != 0 {
|
||||
crate::log!("RedefineClasses failed ({rc})");
|
||||
}
|
||||
rc
|
||||
}
|
||||
|
||||
/// void log(String): lets the Java side write progress lines into prestige-native.log.
|
||||
pub unsafe extern "system" fn native_log(env: EnvPtr, _cls: JClass, message: JObject) {
|
||||
if message.is_null() {
|
||||
return;
|
||||
}
|
||||
let text = Jni::new(env).get_string_utf(message);
|
||||
crate::log!("[java] {text}");
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
//! Minimal raw JNI / JVMTI bindings. Function-table indices were checked against the JDK 21 jni.h / jvmti.h.
|
||||
use std::ffi::{c_char, c_void, CStr, CString};
|
||||
|
||||
pub type JObject = *mut c_void;
|
||||
pub type JClass = JObject;
|
||||
pub type JMethodId = *mut c_void;
|
||||
pub type JValue = u64;
|
||||
pub type JInt = i32;
|
||||
pub type JBool = u8;
|
||||
|
||||
/// JNIEnv*: pointer to a pointer to the function table.
|
||||
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)]
|
||||
pub struct NativeMethod {
|
||||
pub name: *const c_char,
|
||||
pub signature: *const c_char,
|
||||
pub fn_ptr: *mut c_void,
|
||||
}
|
||||
|
||||
/// Packs a JNI argument into the 8-byte jvalue slot.
|
||||
pub fn obj(o: JObject) -> JValue {
|
||||
o as usize as u64
|
||||
}
|
||||
pub fn boolean(b: bool) -> JValue {
|
||||
b as u64
|
||||
}
|
||||
|
||||
pub struct Jni {
|
||||
pub env: EnvPtr,
|
||||
}
|
||||
|
||||
macro_rules! f {
|
||||
($self:expr, $idx:expr, $ty:ty) => {
|
||||
std::mem::transmute::<*const c_void, $ty>(*(*$self.env).add($idx))
|
||||
};
|
||||
}
|
||||
|
||||
impl Jni {
|
||||
/// # Safety
|
||||
/// `env` must be a valid JNIEnv* of the current thread.
|
||||
pub unsafe fn new(env: EnvPtr) -> Self {
|
||||
Jni { env }
|
||||
}
|
||||
|
||||
pub unsafe fn find_class(&self, name: &str) -> JClass {
|
||||
let n = CString::new(name).unwrap();
|
||||
f!(self, 6, unsafe extern "system" fn(EnvPtr, *const c_char) -> JClass)(self.env, n.as_ptr())
|
||||
}
|
||||
pub unsafe fn exception_occurred(&self) -> JObject {
|
||||
f!(self, 15, unsafe extern "system" fn(EnvPtr) -> JObject)(self.env)
|
||||
}
|
||||
pub unsafe fn exception_clear(&self) {
|
||||
f!(self, 17, unsafe extern "system" fn(EnvPtr))(self.env)
|
||||
}
|
||||
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
|
||||
}
|
||||
pub unsafe fn get_object_class(&self, o: JObject) -> JClass {
|
||||
f!(self, 31, unsafe extern "system" fn(EnvPtr, JObject) -> JClass)(self.env, o)
|
||||
}
|
||||
pub unsafe fn get_method_id(&self, c: JClass, name: &str, sig: &str) -> JMethodId {
|
||||
let (n, s) = (CString::new(name).unwrap(), CString::new(sig).unwrap());
|
||||
f!(self, 33, unsafe extern "system" fn(EnvPtr, JClass, *const c_char, *const c_char) -> JMethodId)(self.env, c, n.as_ptr(), s.as_ptr())
|
||||
}
|
||||
pub unsafe fn new_object(&self, c: JClass, ctor: JMethodId, args: &[JValue]) -> JObject {
|
||||
f!(self, 30, unsafe extern "system" fn(EnvPtr, JClass, JMethodId, *const JValue) -> JObject)(self.env, c, ctor, args.as_ptr())
|
||||
}
|
||||
pub unsafe fn call_object(&self, o: JObject, m: JMethodId, args: &[JValue]) -> JObject {
|
||||
f!(self, 36, unsafe extern "system" fn(EnvPtr, JObject, JMethodId, *const JValue) -> JObject)(self.env, o, m, args.as_ptr())
|
||||
}
|
||||
pub unsafe fn call_void(&self, o: JObject, m: JMethodId, args: &[JValue]) {
|
||||
f!(self, 63, unsafe extern "system" fn(EnvPtr, JObject, JMethodId, *const JValue))(self.env, o, m, args.as_ptr())
|
||||
}
|
||||
pub unsafe fn get_static_method_id(&self, c: JClass, name: &str, sig: &str) -> JMethodId {
|
||||
let (n, s) = (CString::new(name).unwrap(), CString::new(sig).unwrap());
|
||||
f!(self, 113, unsafe extern "system" fn(EnvPtr, JClass, *const c_char, *const c_char) -> JMethodId)(self.env, c, n.as_ptr(), s.as_ptr())
|
||||
}
|
||||
pub unsafe fn call_static_object(&self, c: JClass, m: JMethodId, args: &[JValue]) -> JObject {
|
||||
f!(self, 116, unsafe extern "system" fn(EnvPtr, JClass, JMethodId, *const JValue) -> JObject)(self.env, c, m, args.as_ptr())
|
||||
}
|
||||
pub unsafe fn call_static_void(&self, c: JClass, m: JMethodId, args: &[JValue]) {
|
||||
f!(self, 143, unsafe extern "system" fn(EnvPtr, JClass, JMethodId, *const JValue))(self.env, c, m, args.as_ptr())
|
||||
}
|
||||
pub unsafe fn new_string(&self, s: &str) -> JObject {
|
||||
let c = CString::new(s).unwrap();
|
||||
f!(self, 167, unsafe extern "system" fn(EnvPtr, *const c_char) -> JObject)(self.env, c.as_ptr())
|
||||
}
|
||||
pub unsafe fn get_string_utf(&self, s: JObject) -> String {
|
||||
let chars = f!(self, 169, unsafe extern "system" fn(EnvPtr, JObject, *mut JBool) -> *const c_char)(self.env, s, std::ptr::null_mut());
|
||||
if chars.is_null() {
|
||||
return String::new();
|
||||
}
|
||||
let out = CStr::from_ptr(chars).to_string_lossy().into_owned();
|
||||
f!(self, 170, unsafe extern "system" fn(EnvPtr, JObject, *const c_char))(self.env, s, chars);
|
||||
out
|
||||
}
|
||||
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 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)
|
||||
}
|
||||
pub unsafe fn new_byte_array(&self, len: JInt) -> JObject {
|
||||
f!(self, 176, unsafe extern "system" fn(EnvPtr, JInt) -> JObject)(self.env, len)
|
||||
}
|
||||
pub unsafe fn get_byte_array_region(&self, a: JObject, start: JInt, len: JInt, buf: *mut u8) {
|
||||
f!(self, 200, unsafe extern "system" fn(EnvPtr, JObject, JInt, JInt, *mut u8))(self.env, a, start, len, buf)
|
||||
}
|
||||
pub unsafe fn set_byte_array_region(&self, a: JObject, start: JInt, len: JInt, buf: *const u8) {
|
||||
f!(self, 208, unsafe extern "system" fn(EnvPtr, JObject, JInt, JInt, *const u8))(self.env, a, start, len, buf)
|
||||
}
|
||||
pub unsafe fn register_natives(&self, c: JClass, methods: &[NativeMethod]) -> JInt {
|
||||
f!(self, 215, unsafe extern "system" fn(EnvPtr, JClass, *const NativeMethod, JInt) -> JInt)(self.env, c, methods.as_ptr(), methods.len() as JInt)
|
||||
}
|
||||
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> {
|
||||
let ex = self.exception_occurred();
|
||||
if ex.is_null() {
|
||||
return None;
|
||||
}
|
||||
self.exception_clear();
|
||||
let cls = self.get_object_class(ex);
|
||||
let to_string = self.get_method_id(cls, "toString", "()Ljava/lang/String;");
|
||||
let text = if to_string.is_null() {
|
||||
"<exception>".to_string()
|
||||
} else {
|
||||
let s = self.call_object(ex, to_string, &[]);
|
||||
self.exception_clear();
|
||||
if s.is_null() { "<exception>".to_string() } else { self.get_string_utf(s) }
|
||||
};
|
||||
Some(text)
|
||||
}
|
||||
|
||||
/// Converts a pending Java exception into an Err carrying `context`.
|
||||
pub unsafe fn check(&self, context: &str) -> Result<(), String> {
|
||||
match self.take_exception() {
|
||||
Some(e) => Err(format!("{context}: {e}")),
|
||||
None => Ok(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// JavaVM*: pointer to a pointer to the invoke interface.
|
||||
pub type VmPtr = *mut *const *const c_void;
|
||||
|
||||
pub unsafe fn attach_current_thread(vm: VmPtr) -> Result<EnvPtr, String> {
|
||||
let attach = std::mem::transmute::<*const c_void, unsafe extern "system" fn(VmPtr, *mut EnvPtr, *mut c_void) -> JInt>(*(*vm).add(4));
|
||||
let mut env: EnvPtr = std::ptr::null_mut();
|
||||
let rc = attach(vm, &mut env, std::ptr::null_mut());
|
||||
if rc != JNI_OK || env.is_null() {
|
||||
return Err(format!("AttachCurrentThread failed ({rc})"));
|
||||
}
|
||||
Ok(env)
|
||||
}
|
||||
|
||||
pub unsafe fn get_jvmti(vm: VmPtr) -> Result<JvmtiPtr, String> {
|
||||
let get_env = std::mem::transmute::<*const c_void, unsafe extern "system" fn(VmPtr, *mut *mut c_void, JInt) -> JInt>(*(*vm).add(6));
|
||||
let mut out: *mut c_void = std::ptr::null_mut();
|
||||
let rc = get_env(vm, &mut out, JVMTI_VERSION_1_2);
|
||||
if rc != JNI_OK || out.is_null() {
|
||||
return Err(format!("GetEnv(JVMTI) failed ({rc})"));
|
||||
}
|
||||
Ok(out as JvmtiPtr)
|
||||
}
|
||||
|
||||
/// jvmtiEnv*: pointer to a pointer to the JVMTI function table (entry n of the spec is index n-1).
|
||||
pub type JvmtiPtr = *mut *const *const c_void;
|
||||
|
||||
#[repr(C)]
|
||||
pub struct ClassDefinition {
|
||||
pub klass: JClass,
|
||||
pub class_byte_count: JInt,
|
||||
pub class_bytes: *const u8,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Default, Clone, Copy)]
|
||||
pub struct Capabilities {
|
||||
pub bits: [u32; 4],
|
||||
}
|
||||
|
||||
impl Capabilities {
|
||||
pub fn set(&mut self, bit: u32) {
|
||||
self.bits[(bit / 32) as usize] |= 1 << (bit % 32);
|
||||
}
|
||||
}
|
||||
|
||||
// capability bit positions, counted in jvmtiCapabilities declaration order
|
||||
pub const CAN_REDEFINE_CLASSES: u32 = 9;
|
||||
pub const CAN_REDEFINE_ANY_CLASS: u32 = 21;
|
||||
pub const CAN_GENERATE_ALL_CLASS_HOOK_EVENTS: u32 = 26;
|
||||
pub const CAN_RETRANSFORM_CLASSES: u32 = 37;
|
||||
pub const CAN_RETRANSFORM_ANY_CLASS: u32 = 38;
|
||||
|
||||
pub const EVENT_CLASS_FILE_LOAD_HOOK: JInt = 54;
|
||||
|
||||
pub type ClassFileLoadHook = unsafe extern "system" fn(
|
||||
JvmtiPtr,
|
||||
EnvPtr,
|
||||
JClass,
|
||||
JObject,
|
||||
*const c_char,
|
||||
JObject,
|
||||
JInt,
|
||||
*const u8,
|
||||
*mut JInt,
|
||||
*mut *mut u8,
|
||||
);
|
||||
|
||||
pub unsafe fn add_capabilities(j: JvmtiPtr, caps: &Capabilities) -> JInt {
|
||||
let f = std::mem::transmute::<*const c_void, unsafe extern "system" fn(JvmtiPtr, *const Capabilities) -> JInt>(*(*j).add(141));
|
||||
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> {
|
||||
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));
|
||||
let rc = set_callbacks(j, callbacks.as_ptr(), std::mem::size_of_val(&callbacks) as JInt);
|
||||
if rc != 0 {
|
||||
return Err(format!("SetEventCallbacks failed ({rc})"));
|
||||
}
|
||||
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());
|
||||
if rc != 0 {
|
||||
return Err(format!("SetEventNotificationMode failed ({rc})"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub unsafe fn retransform_class(j: JvmtiPtr, class: 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)
|
||||
}
|
||||
|
||||
pub unsafe fn redefine_class(j: JvmtiPtr, def: &ClassDefinition) -> JInt {
|
||||
let f = std::mem::transmute::<*const c_void, unsafe extern "system" fn(JvmtiPtr, JInt, *const ClassDefinition) -> JInt>(*(*j).add(86));
|
||||
f(j, 1, def)
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
//! prestige_native.dll: loaded into a running Minecraft 1.21.11 (Fabric) JVM by PrestigeInjector.exe.
|
||||
//!
|
||||
//! The DLL worker waits for the JVM, appends the embedded Prestige payload jar to Fabric's Knot class loader, registers the
|
||||
//! JVMTI-backed natives of `dev.zprestige.prestige.inject.NativeBridge` and starts `Bootstrap`, which transforms the already
|
||||
//! loaded game classes and initialises the client.
|
||||
mod bridge;
|
||||
mod jni;
|
||||
mod log;
|
||||
mod worker;
|
||||
|
||||
use std::ffi::c_void;
|
||||
|
||||
const DLL_PROCESS_ATTACH: u32 = 1;
|
||||
|
||||
extern "system" {
|
||||
fn DisableThreadLibraryCalls(module: *mut c_void) -> i32;
|
||||
}
|
||||
|
||||
pub(crate) static mut MODULE: *mut c_void = std::ptr::null_mut();
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "system" fn DllMain(module: *mut c_void, reason: u32, _reserved: *mut c_void) -> i32 {
|
||||
if reason == DLL_PROCESS_ATTACH {
|
||||
unsafe {
|
||||
MODULE = module;
|
||||
DisableThreadLibraryCalls(module);
|
||||
}
|
||||
// Nothing but thread creation may happen under the loader lock.
|
||||
let _ = std::thread::Builder::new().name("prestige-loader".into()).spawn(worker::run);
|
||||
}
|
||||
1
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
use std::ffi::c_void;
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::OnceLock;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
extern "system" {
|
||||
fn GetModuleFileNameW(module: *mut c_void, buf: *mut u16, size: u32) -> u32;
|
||||
}
|
||||
|
||||
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")
|
||||
})
|
||||
}
|
||||
|
||||
/// Appends a line to prestige-native.log beside the DLL.
|
||||
pub fn log(message: &str) {
|
||||
let secs = SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0);
|
||||
if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(log_path()) {
|
||||
let _ = writeln!(file, "[{secs}] {message}");
|
||||
}
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! log {
|
||||
($($arg:tt)*) => { $crate::log::log(&format!($($arg)*)) };
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
//! 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};
|
||||
use crate::jni::*;
|
||||
use crate::log;
|
||||
use std::ffi::{c_char, c_void};
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
|
||||
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"));
|
||||
|
||||
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() });
|
||||
match unsafe { install() } {
|
||||
Ok(()) => log!("bootstrap returned"),
|
||||
Err(e) => log!("FAILED: {e}"),
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn install() -> Result<(), String> {
|
||||
let vm = find_vm()?;
|
||||
let env = attach_current_thread(vm)?;
|
||||
let jni = Jni::new(env);
|
||||
log!("attached to the JVM");
|
||||
|
||||
init_jvmti(vm)?;
|
||||
log!("JVMTI ready");
|
||||
|
||||
let jar = write_payload()?;
|
||||
log!("payload written to {}", jar.display());
|
||||
|
||||
let loader = add_to_knot(&jni, &jar)?;
|
||||
log!("payload appended to the Fabric Knot class loader");
|
||||
|
||||
register_natives(&jni, loader)?;
|
||||
log!("natives registered");
|
||||
|
||||
let bootstrap = class_for_name(&jni, BOOTSTRAP, loader)?;
|
||||
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.
|
||||
jni.call_static_void(bootstrap, start, &[]);
|
||||
jni.check("Bootstrap.start")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Waits for jvm.dll and a created JavaVM (the injector may run before Minecraft has started its JVM).
|
||||
unsafe fn find_vm() -> Result<VmPtr, String> {
|
||||
let mut module = std::ptr::null_mut();
|
||||
for _ in 0..600 {
|
||||
module = GetModuleHandleA(c"jvm.dll".as_ptr());
|
||||
if !module.is_null() {
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(100));
|
||||
}
|
||||
if module.is_null() {
|
||||
return Err("jvm.dll is not loaded in this process".into());
|
||||
}
|
||||
let get_vms = GetProcAddress(module, c"JNI_GetCreatedJavaVMs".as_ptr());
|
||||
if get_vms.is_null() {
|
||||
return Err("JNI_GetCreatedJavaVMs export not found".into());
|
||||
}
|
||||
let get_vms: unsafe extern "system" fn(*mut VmPtr, JInt, *mut JInt) -> JInt = std::mem::transmute(get_vms);
|
||||
for _ in 0..600 {
|
||||
let mut vm: VmPtr = std::ptr::null_mut();
|
||||
let mut count: JInt = 0;
|
||||
if get_vms(&mut vm, 1, &mut count) == JNI_OK && count > 0 && !vm.is_null() {
|
||||
return Ok(vm);
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(100));
|
||||
}
|
||||
Err("no JavaVM was created".into())
|
||||
}
|
||||
|
||||
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() }));
|
||||
std::fs::write(&path, PAYLOAD).map_err(|e| format!("write {}: {e}", path.display()))?;
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
/// 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?)")?;
|
||||
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, &[]);
|
||||
jni.check("getLauncher()")?;
|
||||
if launcher.is_null() {
|
||||
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 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;");
|
||||
jni.check("FabricLauncher.getTargetClassLoader")?;
|
||||
|
||||
// Path path = new java.io.File(jar).toPath()
|
||||
let file_class = jni.find_class("java/io/File");
|
||||
let file_ctor = jni.get_method_id(file_class, "<init>", "(Ljava/lang/String;)V");
|
||||
let to_path = jni.get_method_id(file_class, "toPath", "()Ljava/nio/file/Path;");
|
||||
jni.check("java.io.File")?;
|
||||
let jar_string = jni.new_string(&jar.to_string_lossy());
|
||||
let file = jni.new_object(file_class, file_ctor, &[obj(jar_string)]);
|
||||
let path = jni.call_object(file, to_path, &[]);
|
||||
jni.check("File.toPath")?;
|
||||
|
||||
let string_class = jni.find_class("java/lang/String");
|
||||
let prefixes = jni.new_object_array(0, string_class, std::ptr::null_mut());
|
||||
jni.call_void(launcher, add, &[obj(path), obj(prefixes)]);
|
||||
jni.check("addToClassPath")?;
|
||||
|
||||
let loader = jni.call_object(launcher, target, &[]);
|
||||
jni.check("getTargetClassLoader()")?;
|
||||
if loader.is_null() {
|
||||
return Err("Knot target class loader is null".into());
|
||||
}
|
||||
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> {
|
||||
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)]);
|
||||
jni.check(&format!("load {name}"))?;
|
||||
if class.is_null() {
|
||||
return Err(format!("{name} not found"));
|
||||
}
|
||||
Ok(class)
|
||||
}
|
||||
|
||||
unsafe fn register_natives(jni: &Jni, loader: JObject) -> Result<(), String> {
|
||||
let class = class_for_name(jni, NATIVE_BRIDGE, loader)?;
|
||||
let methods = [
|
||||
NativeMethod {
|
||||
name: c"classBytes".as_ptr(),
|
||||
signature: c"(Ljava/lang/Class;)[B".as_ptr(),
|
||||
fn_ptr: native_class_bytes as *const () as usize as *mut c_void,
|
||||
},
|
||||
NativeMethod {
|
||||
name: c"redefine".as_ptr(),
|
||||
signature: c"(Ljava/lang/Class;[B)I".as_ptr(),
|
||||
fn_ptr: native_redefine as *const () as usize as *mut c_void,
|
||||
},
|
||||
NativeMethod {
|
||||
name: c"log".as_ptr(),
|
||||
signature: c"(Ljava/lang/String;)V".as_ptr(),
|
||||
fn_ptr: native_log as *const () as usize as *mut c_void,
|
||||
},
|
||||
];
|
||||
let rc = jni.register_natives(class, &methods);
|
||||
if rc != JNI_OK {
|
||||
let _ = jni.take_exception();
|
||||
return Err(format!("RegisterNatives failed ({rc})"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Reference in new issue
Block a user