analysis: drop shaded-package lock heuristic (superseded by the family-split fix)
This commit is contained in:
parent
3709d3ad35
commit
2069aa6646
3 changed files with 1 additions and 190 deletions
|
|
@ -166,7 +166,6 @@ impl Analysis {
|
||||||
keep.absorb_bootstrap(program);
|
keep.absorb_bootstrap(program);
|
||||||
keep.absorb_databind(program);
|
keep.absorb_databind(program);
|
||||||
keep.absorb_mixins(program);
|
keep.absorb_mixins(program);
|
||||||
keep.absorb_shaded(program);
|
|
||||||
|
|
||||||
let locked_classes = program
|
let locked_classes = program
|
||||||
.classes
|
.classes
|
||||||
|
|
|
||||||
|
|
@ -1,32 +1,9 @@
|
||||||
//! Program scans feeding keep rules: bootstrap methods, databind literals, mixins.
|
//! 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;
|
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 {
|
impl KeepRules {
|
||||||
/// Methods referenced by `BootstrapMethods` (`invokedynamic`).
|
/// 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<String, Vec<&noxis_classfile::ClassFile>> = 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
|
/// Lock every field of every class passed as an `X.class` literal to a
|
||||||
/// databinding call. Gson/Jackson bind fields by reflection from the
|
/// databinding call. Gson/Jackson bind fields by reflection from the
|
||||||
/// literal; renaming them silently breaks deserialization, while the
|
/// literal; renaming them silently breaks deserialization, while the
|
||||||
|
|
|
||||||
|
|
@ -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"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue