diff --git a/crates/noxis-analysis/src/keep/mod.rs b/crates/noxis-analysis/src/keep/mod.rs index 13a4916..1247f3c 100644 --- a/crates/noxis-analysis/src/keep/mod.rs +++ b/crates/noxis-analysis/src/keep/mod.rs @@ -166,7 +166,6 @@ impl Analysis { keep.absorb_bootstrap(program); keep.absorb_databind(program); keep.absorb_mixins(program); - keep.absorb_shaded(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 4d69aa1..89a9184 100644 --- a/crates/noxis-analysis/src/keep/scans.rs +++ b/crates/noxis-analysis/src/keep/scans.rs @@ -1,32 +1,9 @@ //! Program scans feeding keep rules: bootstrap methods, databind literals, mixins. -use noxis_classfile::{Attribute, Const, Insn, Method, Program}; +use noxis_classfile::{Attribute, Const, Insn, Program}; use super::KeepRules; -/// Whether an invoke reaches reflective method lookup or invocation. -fn is_reflective_dispatch(owner: &str, name: &str) -> bool { - matches!( - (owner, name), - ("java/lang/Class", "getDeclaredMethod") - | ("java/lang/Class", "getMethod") - | ("java/lang/reflect/Method", "invoke") - ) -} - -/// The package prefix (`com/foo/bar`) of an internal class name. -fn package_of(internal_name: &str) -> &str { - match internal_name.rfind('/') { - Some(index) => &internal_name[..index], - None => "", - } -} - -/// Whether this method is `public abstract` (a `TypeAdapter`-style hook). -fn is_public_abstract(method: &Method) -> bool { - method.access_flags & 0x0401 == 0x0401 -} - impl KeepRules { /// Methods referenced by `BootstrapMethods` (`invokedynamic`). /// @@ -87,53 +64,6 @@ impl KeepRules { } } - /// Lock shaded reflective libraries: a package holding both a - /// `public abstract` class (the `TypeAdapter`-style hook) and another - /// class that looks up or invokes methods reflectively (`getDeclaredMethod`, - /// `getMethod`, `Method.invoke`). Gson/Jackson/Moshi dispatch their adapter - /// methods through reflection, so renaming an abstract `read`/`write` - /// while the wrapper still calls it reflectively yields - /// `AbstractMethodError` at runtime. Package-locking the whole subtree is - /// the only sound answer - a member-level rule cannot see the reflective - /// call site on a renamed adapter. - pub fn absorb_shaded(&mut self, program: &Program) { - use std::collections::BTreeMap; - let mut packages: BTreeMap> = BTreeMap::new(); - for class in &program.classes { - let package = package_of(&class.this_class); - packages.entry(package.to_string()).or_default().push(class); - } - for (package, classes) in &packages { - if package.is_empty() { - continue; - } - let has_abstract = classes - .iter() - .any(|class| class.methods.iter().any(is_public_abstract)); - let has_reflective = classes.iter().any(|class| { - class.methods.iter().any(|method| { - method.code.as_ref().is_some_and(|code| { - code.insns.iter().any(|insn| { - matches!(insn, Insn::Invoke { owner, name, .. } - if is_reflective_dispatch(owner.as_str(), name.as_str())) - }) - }) - }) - }); - if !(has_abstract && has_reflective) { - continue; - } - for class in classes { - self.keep_exact(class.this_class.clone()); - for method in &class.methods { - if is_public_abstract(method) { - self.keep_member(method.name.clone()); - } - } - } - } - } - /// Lock every field of every class passed as an `X.class` literal to a /// databinding call. Gson/Jackson bind fields by reflection from the /// literal; renaming them silently breaks deserialization, while the diff --git a/crates/noxis-analysis/tests/shaded.rs b/crates/noxis-analysis/tests/shaded.rs deleted file mode 100644 index 88aa701..0000000 --- a/crates/noxis-analysis/tests/shaded.rs +++ /dev/null @@ -1,118 +0,0 @@ -//! Shaded reflective libraries: a package containing a public abstract class -//! whose virtual methods are dispatched reflectively by another class in the -//! same package must keep its class and member names. -//! -//! Gson's `TypeAdapter` is the archetype: `TypeAdapterRuntimeTypeWrapper` -//! calls `getDeclaredMethod`/`Method.invoke` on an adapter instance. Renaming -//! the `read` method in `TypeAdapter` while the wrapper invokes it reflectively -//! yields `AbstractMethodError` at runtime. Package-locking the whole shaded -//! subtree is the only sound answer. - -#![allow(clippy::expect_used, clippy::unwrap_used)] - -use noxis_analysis::Analysis; -use noxis_classfile::{ - ClassBuilder, Code, Const, Field, Insn, InvokeKind, Method, Program, Version, -}; -use noxis_core::Config; - -fn ret_code() -> Code { - Code { - max_stack: 0, - max_locals: 0, - insns: vec![Insn::Return(None)], - exception_table: Vec::new(), - frames: Vec::new(), - attributes: Vec::new(), - } -} - -/// `libs/gson/TypeAdapter` (abstract, public) with an abstract `read`, -/// plus `libs/gson/TypeAdapterRuntimeTypeWrapper` invoking `read` -/// reflectively through `getDeclaredMethod`/`invoke`. -fn sample() -> Program { - let mut abstract_adapter = ClassBuilder::new("libs/gson/TypeAdapter", Some("java/lang/Object")) - .version(Version::JAVA_21) - .build(); - abstract_adapter.methods.push(Method { - access_flags: 0x0401, // public abstract - name: "read".to_string(), - descriptor: "(Ljava/io/InputStream;)Ljava/lang/Object;".to_string(), - code: None, - attributes: Vec::new(), - }); - - let wrapper_code = Code { - max_stack: 4, - max_locals: 2, - insns: vec![ - Insn::Load { - slot: 1, - kind: noxis_classfile::SlotKind::Ref, - }, - Insn::Const(Const::String("read".to_string())), - Insn::Const(Const::Class("java/io/InputStream".to_string())), - Insn::Invoke { - kind: InvokeKind::Virtual, - owner: "java/lang/Class".to_string(), - name: "getDeclaredMethod".to_string(), - descriptor: "(Ljava/lang/String;[Ljava/lang/Class;)Ljava/lang/reflect/Method;" - .to_string(), - itf: false, - }, - Insn::Pop, - Insn::Return(None), - ], - exception_table: Vec::new(), - frames: Vec::new(), - attributes: Vec::new(), - }; - let wrapper = ClassBuilder::new( - "libs/gson/TypeAdapterRuntimeTypeWrapper", - Some("java/lang/Object"), - ) - .version(Version::JAVA_21) - .method_with_code(0x0001, "wrap", "(Ljava/lang/Object;)V", wrapper_code) - .build(); - - let mut state = ClassBuilder::new("demo/State", Some("java/lang/Object")) - .version(Version::JAVA_21) - .build(); - state.fields.push(Field { - access_flags: 0x0002, - name: "nickname".to_string(), - descriptor: "Ljava/lang/String;".to_string(), - attributes: Vec::new(), - }); - let app = ClassBuilder::new("demo/App", Some("java/lang/Object")) - .version(Version::JAVA_21) - .method_with_code(0x0009, "load", "()V", ret_code()) - .build(); - - Program { - classes: vec![abstract_adapter, wrapper, state, app], - resources: Vec::new(), - } -} - -#[test] -fn shaded_reflective_package_is_locked() { - let program = sample(); - let analysis = Analysis::run(&program, &Config::default()).expect("analysis"); - assert!( - analysis.is_locked("libs/gson/TypeAdapter"), - "abstract adapter in a reflective package must stay" - ); - assert!( - analysis.is_locked("libs/gson/TypeAdapterRuntimeTypeWrapper"), - "the reflective wrapper must stay too" - ); - assert!( - analysis.keep.keeps_member("read"), - "the reflectively dispatched member must keep its name" - ); - assert!( - !analysis.is_locked("demo/State"), - "unrelated classes must still rename" - ); -}