From 1ebbca10fa49f3b7d31173cb6a12de36c5f9f6f4 Mon Sep 17 00:00:00 2001 From: loki5512344 Date: Tue, 13 Oct 2026 13:18:13 +0200 Subject: [PATCH] analysis: lock mixin classes and plugin again (revert mixin rename) Renaming mixin classes and patching mixins.json (90506a4) breaks Fabric mods at runtime, proven on LoVisual (2026-10-13): - When obfuscated packages are randomized per class the config patcher computes an empty common prefix and writes `package: ""`. Fabric/Mixin then drops every mixin as "orphaned", so `RenderPipelineMixin implements IRenderPipeline` never applies, the interface is not injected into `RenderPipeline`, and the entrypoint dies with `ClassCastException: RenderPipeline cannot be cast to IRenderPipeline`. - `IMixinConfigPlugin.shouldApplyMixin` compares the runtime mixin class name against string literals (`Set.of("dev.loki...Mixin")`); renaming desyncs those sets and optional sodium/iris/xaero mixins get applied when the target mod is absent. Re-enable absorb_mixins: mixin entry classes and the config plugin keep their names, and locking a class freezes its members too (needed for @Accessor/@Invoker/@Overwrite name lookups). Calls from locked mixin bytecode are still rewritten consistently, and the family mechanism keeps interface method names (IRenderPipeline) in sync with the frozen mixin methods. patch_mixins_json stays but is identity for locked classes. Verified: LoVisual obf jar loads all 221 mixins (0 orphaned), the main menu renders, the entrypoint runs. --- crates/noxis-analysis/src/keep/mod.rs | 1 + crates/noxis-analysis/src/keep/scans.rs | 23 +++++++------- crates/noxis-analysis/tests/mixins.rs | 40 ++++++++++++++++++------- 3 files changed, 43 insertions(+), 21 deletions(-) diff --git a/crates/noxis-analysis/src/keep/mod.rs b/crates/noxis-analysis/src/keep/mod.rs index 7cea2bf..498b9b5 100644 --- a/crates/noxis-analysis/src/keep/mod.rs +++ b/crates/noxis-analysis/src/keep/mod.rs @@ -168,6 +168,7 @@ impl Analysis { keep.absorb(&entrypoints); keep.absorb_bootstrap(program); keep.absorb_databind(program); + keep.absorb_mixins(program); let locked_classes = program .classes diff --git a/crates/noxis-analysis/src/keep/scans.rs b/crates/noxis-analysis/src/keep/scans.rs index 83132de..0ce16a6 100644 --- a/crates/noxis-analysis/src/keep/scans.rs +++ b/crates/noxis-analysis/src/keep/scans.rs @@ -31,18 +31,19 @@ impl KeepRules { /// Lock every Mixin class (and the plugin) listed in the configs. /// /// The Mixin framework resolves mixin classes and their members by name - /// at runtime, so renaming them breaks the mod at startup. Entries use - /// simple names relative to the `package` field, which generic text - /// patching can never see - locking is the only sound answer. Locking - /// the class also freezes its members (locked classes skip member - /// renaming entirely), which the `@Accessor`/`@Invoker`/`@Overwrite` - /// lookups need. Mixins configs are found two ways: any resource ending - /// in `mixins.json`, plus whatever `fabric.mod.json` lists. + /// at runtime, so renaming them breaks the mod at startup. Locking the + /// class also freezes its members (locked classes skip member renaming + /// entirely), which the `@Accessor`/`@Invoker`/`@Overwrite` name lookups + /// and `IMixinConfigPlugin.shouldApplyMixin` string sets both need. /// - /// DISABLED: the resources pass now patches mixins.json properly - /// (rewrites package + entries via common prefix), so mixin classes - /// can be renamed like any other class. Reference: OBF fabric-obfuscator. - #[allow(dead_code)] + /// Renaming mixins and patching the config was tried and reverted: when + /// obfuscated packages are randomized per class the config patcher writes + /// an empty `package`, and Fabric/Mixin drops every mixin as "orphaned" + /// (real case: injected-interface cast fails with `ClassCastException`). + /// Entries use simple names relative to `package`, which generic text + /// patching can never see - locking is the only sound answer. Mixin + /// configs are found two ways: any resource ending in `mixins.json`, + /// plus whatever `fabric.mod.json` lists. pub fn absorb_mixins(&mut self, program: &Program) { let mut listed: Vec = Vec::new(); for resource in &program.resources { diff --git a/crates/noxis-analysis/tests/mixins.rs b/crates/noxis-analysis/tests/mixins.rs index 14009b5..898786b 100644 --- a/crates/noxis-analysis/tests/mixins.rs +++ b/crates/noxis-analysis/tests/mixins.rs @@ -1,7 +1,20 @@ -//! Mixin keep: classes listed in `*.mixins.json` are now renameable. +//! Mixin keep: classes listed in `*.mixins.json` are locked, not renamed. //! -//! The resources pass patches mixins.json (rewrites package + entries), -//! so mixin classes can be renamed. Reference: OBF fabric-obfuscator. +//! Real-world proof (LoVisual, 2026-10-13): renaming mixin classes and +//! patching the config breaks the mod at runtime. Two independent failures: +//! 1. When obf packages are randomized per class, the config patcher writes +//! `package: ""`, and Fabric/Mixin then drops *every* mixin as "orphaned" +//! (`RenderPipelineMixin implements IRenderPipeline` never applies, so the +//! injected-interface cast fails with `ClassCastException`). +//! 2. `IMixinConfigPlugin.shouldApplyMixin` compares the runtime mixin class +//! name against string literals (`Set.of("dev.loki...Mixin")`); renaming +//! the mixins desyncs those sets and optional mixins get applied when the +//! target mod is absent. +//! +//! So mixin classes and their plugin stay under their original names, and +//! locking the class freezes its members too - which `@Accessor`/`@Invoker`/ +//! `@Overwrite` name lookups require. Other program classes still rename, and +//! calls from locked mixin bytecode are rewritten consistently. #![allow(clippy::expect_used, clippy::unwrap_used)] @@ -54,20 +67,27 @@ fn sample() -> Program { } #[test] -fn mixin_classes_are_renameable() { +fn mixin_classes_and_plugin_are_locked() { let program = sample(); let analysis = Analysis::run(&program, &Config::default()).expect("analysis"); - // Mixin classes are no longer locked - resources pass patches the config. - for renameable in [ + // Mixin entry points and the config plugin are resolved by name at + // runtime and must keep their names (and, being locked, their members). + for locked in [ "demo/mixin/TargetMixin", "demo/mixin/other/Helper", "demo/mixin/ClientMixin", "demo/mixin/Plugin", ] { - assert!( - !analysis.is_locked(renameable), - "{renameable} should be renameable now" - ); + assert!(analysis.is_locked(locked), "{locked} should be locked"); } } + +#[test] +fn non_mixin_classes_stay_renameable() { + let program = sample(); + let analysis = Analysis::run(&program, &Config::default()).expect("analysis"); + + // Plain program classes are not named by any mixin config. + assert!(!analysis.is_locked("demo/App")); +}