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:
bbldrizzy committed 2026-11-03 11:40:43 +00:00
1 parent 38ff6d387c
commit 036b24f2ae
12 files changed
+186 -48

No files matched your search

+25 -10
View File
@@ -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> {