Improve launcher injection and hook reliability
Find Fabric through the render thread's class loader and make bytecode transformations safer for launcher-provided class definitions. Improve hook reporting, process-picker refresh behavior, and self-destruct cleanup; document Dawn and Lunar 1.21.11 setup.
This commit is contained in:
1 parent
38ff6d387c
commit
036b24f2ae
12 files changed
+186
-48
No files matched your search
@@ -56,9 +56,6 @@ impl Jni {
|
||||
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 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
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ unsafe fn install() -> Result<(), String> {
|
||||
log!("payload written to {}", jar.display());
|
||||
|
||||
let loader = add_to_knot(&jni, &jar)?;
|
||||
log!("payload appended to the Fabric Knot class loader");
|
||||
log!("payload appended to the game class loader: {}", describe(&jni, loader));
|
||||
|
||||
register_natives(&jni, loader)?;
|
||||
log!("natives registered");
|
||||
@@ -111,7 +111,7 @@ 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.
|
||||
/// Returns a local reference to the Fabric launcher's target class loader.
|
||||
unsafe fn add_to_knot(jni: &Jni, jar: &std::path::Path) -> Result<JObject, String> {
|
||||
jni.ensure_local_capacity(4096);
|
||||
let (base, fabric_loader) = locate_fabric(jni)?;
|
||||
@@ -149,23 +149,30 @@ unsafe fn add_to_knot(jni: &Jni, jar: &std::path::Path) -> Result<JObject, Strin
|
||||
if loader.is_null() {
|
||||
return Err("Knot target class loader is null".into());
|
||||
}
|
||||
Ok(jni.new_global_ref(loader))
|
||||
Ok(loader)
|
||||
}
|
||||
|
||||
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).
|
||||
/// Prefer the render thread's context loader when a launcher supplies its own loader hierarchy.
|
||||
/// Fall back to the system loader and then the other thread context loaders.
|
||||
unsafe fn locate_fabric(jni: &Jni) -> Result<(JClass, JObject), String> {
|
||||
let (render, loaders) = thread_loaders(jni)?;
|
||||
if let Some(loader) = render {
|
||||
if let Ok(class) = class_for_name(jni, FABRIC_BASE, loader, false) {
|
||||
log!("fabric-loader found through the render thread: {}", describe(jni, loader));
|
||||
return Ok((class, loader));
|
||||
}
|
||||
}
|
||||
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 render.is_some_and(|r| jni.is_same_object(r, loader)) {
|
||||
continue;
|
||||
}
|
||||
if let Ok(class) = class_for_name(jni, FABRIC_BASE, loader, false) {
|
||||
log!("fabric-loader found through {}", describe(jni, loader));
|
||||
return Ok((class, loader));
|
||||
@@ -180,10 +187,11 @@ unsafe fn locate_fabric(jni: &Jni) -> Result<(JClass, JObject), String> {
|
||||
}
|
||||
|
||||
/// 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> {
|
||||
unsafe fn thread_loaders(jni: &Jni) -> Result<(Option<JObject>, 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 get_name = jni.get_method_id(thread_class, "getName", "()Ljava/lang/String;");
|
||||
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");
|
||||
@@ -195,6 +203,7 @@ unsafe fn thread_loaders(jni: &Jni) -> Result<Vec<JObject>, String> {
|
||||
let threads = jni.call_object(jni_non_null(set)?, to_array, &[]);
|
||||
jni.check("thread list")?;
|
||||
let mut loaders: Vec<JObject> = Vec::new();
|
||||
let mut render = None;
|
||||
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() {
|
||||
@@ -204,11 +213,17 @@ unsafe fn thread_loaders(jni: &Jni) -> Result<Vec<JObject>, String> {
|
||||
if jni.take_exception().is_some() || loader.is_null() {
|
||||
continue;
|
||||
}
|
||||
let name = jni.call_object(thread, get_name, &[]);
|
||||
if jni.take_exception().is_none() && !name.is_null() {
|
||||
if matches!(jni.get_string_utf(name).as_str(), "Render thread" | "Client thread") {
|
||||
render = Some(loader);
|
||||
}
|
||||
}
|
||||
if !loaders.iter().any(|&known| jni.is_same_object(known, loader)) {
|
||||
loaders.push(loader);
|
||||
}
|
||||
}
|
||||
Ok(loaders)
|
||||
Ok((render, loaders))
|
||||
}
|
||||
|
||||
unsafe fn jni_non_null(o: JObject) -> Result<JObject, String> {
|
||||
|
||||
Reference in new issue
Block a user