passes: junk classes with real bodies and no fingerprint

NoisePass now builds branching helper methods (counted int loop,
random arithmetic, even/odd merge, non-throwing ops only) whose
results the entry sums into a private static field, instead of an
empty run()V. Names come from the same NameGenerator styles as
real code and live in a random real package (no more shadow/decoy
uniformity for grep to catch); every generated method gets fresh
StackMapTable frames. weave_first_reachable takes (class, entry)
pairs. New NoiseConfig.junk_methods_per_class (default 3).

Tests assert invariants (unique valid identifiers, calls, field
store, determinism) plus real-JVM verification of junk classes;
paranoid+junk_ratio=1 smoke on lorep: 18 junk classes, all load.
This commit is contained in:
Loki 2026-10-09 14:04:24 +02:00
parent 74ad4552e4
commit 2cbb9db31e
Signed by: boba
GPG key ID: 253067914055423B
11 changed files with 648 additions and 159 deletions

View file

@ -33,7 +33,7 @@ fn negate_cond(cond: Cond) -> Cond {
/// Turn `if-cond T` into `if-not SKIP; goto T; SKIP:` (`butterfly`).
///
/// `rate` is a 0–100 percentage applied per branch. Semantics are unchanged:
/// `rate` is a 0-100 percentage applied per branch. Semantics are unchanged:
/// the inverted branch skips a `goto` to the original target, so the taken /
/// not-taken destinations are exactly the old ones. A `nop` pads the fresh
/// mark: whatever follows (an original mark, even a handler entry with its

View file

@ -0,0 +1,259 @@
//! Junk method bodies: plausible branching integer code.
//!
//! A helper is `private static int helper()`: two int locals (`acc`, `i`),
//! a counted loop with a few random binary ops plus one even/odd if/else
//! merge for a real branching shape. Everything is stack-neutral by
//! construction and uses only non-throwing integer ops: woven junk really
//! runs, so it must never throw.
//!
//! Labels start at fixed small numbers: bodies are built from scratch, so
//! nothing collides (labels are per-method).
use noxis_classfile::{Arith, BranchCond, Code, Cond, Const, Insn, Prim, SlotKind};
use noxis_core::Rng;
/// Non-throwing integer ops for junk arithmetic (`Div`/`Rem` could trap).
const OPS: [Arith; 8] = [
Arith::Add,
Arith::Sub,
Arith::Mul,
Arith::And,
Arith::Or,
Arith::Xor,
Arith::Shl,
Arith::Shr,
];
/// Fresh-label block for one helper (head, end, odd, merge).
const HEAD: u32 = 1;
const END: u32 = 2;
const ODD: u32 = 3;
const MERGE: u32 = 4;
/// Build one helper body.
pub(super) fn helper_body(rng: &mut Rng) -> Code {
let bound = 4 + rng.below(29) as i32;
let mut insns = vec![
Insn::Const(Const::Int(0)),
Insn::Store {
slot: 0,
kind: SlotKind::Int,
},
Insn::Const(Const::Int(0)),
Insn::Store {
slot: 1,
kind: SlotKind::Int,
},
Insn::Mark(HEAD),
Insn::Load {
slot: 1,
kind: SlotKind::Int,
},
Insn::Const(Const::Int(bound)),
Insn::Branch(BranchCond::Int(Cond::Ge), END),
];
// Straight-line accumulator churn: `acc = acc op i`, two to four ops.
for _ in 0..2 + rng.below(3) {
let op = OPS[rng.below(OPS.len() as u64) as usize];
insns.extend([
Insn::Load {
slot: 0,
kind: SlotKind::Int,
},
Insn::Load {
slot: 1,
kind: SlotKind::Int,
},
Insn::Binary {
prim: Prim::Int,
op,
},
Insn::Store {
slot: 0,
kind: SlotKind::Int,
},
]);
}
// Even/odd merge: `if ((i & 1) == 0) acc += c1 else acc ^= c2`.
let (even, odd) = (rng.next_u64() as i32, rng.next_u64() as i32);
insns.extend([
Insn::Load {
slot: 1,
kind: SlotKind::Int,
},
Insn::Const(Const::Int(1)),
Insn::Binary {
prim: Prim::Int,
op: Arith::And,
},
Insn::Branch(BranchCond::Zero(Cond::Ne), ODD),
Insn::Load {
slot: 0,
kind: SlotKind::Int,
},
Insn::Const(Const::Int(even)),
Insn::Binary {
prim: Prim::Int,
op: Arith::Add,
},
Insn::Store {
slot: 0,
kind: SlotKind::Int,
},
Insn::Goto(MERGE),
Insn::Mark(ODD),
Insn::Load {
slot: 0,
kind: SlotKind::Int,
},
Insn::Const(Const::Int(odd)),
Insn::Binary {
prim: Prim::Int,
op: Arith::Xor,
},
Insn::Store {
slot: 0,
kind: SlotKind::Int,
},
Insn::Mark(MERGE),
]);
insns.extend([
Insn::Iinc { index: 1, delta: 1 },
Insn::Goto(HEAD),
Insn::Mark(END),
Insn::Load {
slot: 0,
kind: SlotKind::Int,
},
Insn::Return(Some(Prim::Int)),
]);
Code {
max_stack: 4,
max_locals: 2,
insns,
exception_table: Vec::new(),
frames: Vec::new(),
attributes: Vec::new(),
}
}
/// Build the entry body: call every helper, sum the results into the static
/// field (a bare `void` call with a dropped result is a recognizable junk
/// pattern on its own). With no helpers the entry is a bare return.
pub(super) fn entry_body(callees: &[(String, String)], owner: &str, field: &str) -> Code {
let mut insns = Vec::new();
let mut first = true;
for (callee_owner, callee_name) in callees {
insns.push(Insn::Invoke {
kind: noxis_classfile::InvokeKind::Static,
owner: callee_owner.clone(),
name: callee_name.clone(),
descriptor: "()I".to_string(),
itf: false,
});
if first {
first = false;
} else {
insns.push(Insn::Binary {
prim: Prim::Int,
op: Arith::Add,
});
}
}
if callees.is_empty() {
insns.push(Insn::Return(None));
} else {
insns.push(Insn::PutStatic {
owner: owner.to_string(),
name: field.to_string(),
descriptor: "I".to_string(),
});
insns.push(Insn::Return(None));
}
Code {
max_stack: 2,
max_locals: 0,
insns,
exception_table: Vec::new(),
frames: Vec::new(),
attributes: Vec::new(),
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used, clippy::unwrap_used)]
use super::*;
/// Helpers branch and loop instead of returning immediately.
#[test]
fn helper_has_branches_and_loop() {
let mut rng = Rng::from_seed(3);
let code = helper_body(&mut rng);
let has = |pred: &dyn Fn(&Insn) -> bool| code.insns.iter().any(pred);
assert!(has(&|i| matches!(i, Insn::Branch(..))), "no branch");
assert!(has(&|i| matches!(i, Insn::Goto(_))), "no loop back-edge");
assert!(has(&|i| matches!(i, Insn::Binary { .. })), "no arithmetic");
assert!(has(&|i| matches!(i, Insn::Iinc { .. })), "no counter");
assert!(
has(&|i| matches!(i, Insn::Return(Some(Prim::Int)))),
"no int return"
);
assert_eq!(code.max_locals, 2);
// Every jump target is declared.
let marks: std::collections::BTreeSet<u32> = code
.insns
.iter()
.filter_map(|i| match i {
Insn::Mark(label) => Some(*label),
_ => None,
})
.collect();
for insn in &code.insns {
for target in insn.targets() {
assert!(marks.contains(&target), "dangling label {target}");
}
}
}
/// Same seed, same body.
#[test]
fn helper_is_deterministic() {
let first = helper_body(&mut Rng::from_seed(9));
let second = helper_body(&mut Rng::from_seed(9));
assert_eq!(first.insns, second.insns);
let third = helper_body(&mut Rng::from_seed(10));
assert_ne!(first.insns, third.insns);
}
/// The entry calls every helper and stores the sum into the field.
#[test]
fn entry_calls_helpers_and_stores() {
let callees = vec![
("junk/A".to_string(), "h0".to_string()),
("junk/A".to_string(), "h1".to_string()),
];
let code = entry_body(&callees, "junk/A", "f");
let invokes = code
.insns
.iter()
.filter(|i| matches!(i, Insn::Invoke { .. }))
.count();
assert_eq!(invokes, 2);
assert!(
code.insns
.iter()
.any(|i| matches!(i, Insn::PutStatic { name, .. } if name == "f")),
"no field store"
);
assert!(matches!(code.insns.last(), Some(Insn::Return(None))));
}
/// No helpers, no calls: a bare return, still valid.
#[test]
fn entry_without_helpers_returns() {
let code = entry_body(&[], "junk/A", "f");
assert_eq!(code.insns, vec![Insn::Return(None)]);
}
}

View file

@ -1,26 +1,38 @@
//! Пасс шума: junk-классы и вплетание их в граф достижимости.
//! Noise pass: junk classes with real bodies, woven into reachability.
//!
//! Мёртвый код выкидывается shrink-анализом, поэтому каждый junk-класс
//! получает ребро из живого метода (`invokestatic shadow/decoyN.run()V`).
//! Junk-аннотации не генерируются: сырые атрибуты не несут свежие индексы пула.
//! Dead code gets dropped by shrink analysis, so every junk class gets an
//! edge from live code (an `invokestatic` of its entry). Junk names come
//! from the same generator (and styles) as real code: self-describing
//! `shadow/decoy` names with a uniform `run` entry would be a grep pattern
//! of their own.
mod body;
mod weave;
use noxis_classfile::{ClassBuilder, Program, Version, empty_code};
use noxis_core::{Context, CoreError, Pass, PassId, PassReport};
#[cfg(test)]
mod tests;
use crate::namegen::NameGenerator;
use crate::rename::assign::parent_package;
use body::{entry_body, helper_body};
use noxis_analysis::ClassHierarchy;
use noxis_classfile::{ClassBuilder, Field, Program, Version, compute_frames};
use noxis_core::{Context, CoreError, Pass, PassId, PassReport};
use weave::weave_first_reachable;
/// Префикс детерминированных имён junk-классов.
const JUNK_PREFIX: &str = "shadow/decoy";
/// Имя и дескриптор точки входа junk-класса.
const JUNK_ENTRY: (&str, &str) = ("run", "()V");
/// Флаги `public static` для junk-метода.
/// Flags for the junk entry (`public static`, callable from live code).
const ACC_PUBLIC_STATIC: u16 = 0x0009;
/// Flags for helpers and the accumulator field (`private static`).
const ACC_PRIVATE_STATIC: u16 = 0x000A;
/// Пасс шума.
/// The entry descriptor (always `()V`).
const ENTRY_DESCRIPTOR: &str = "()V";
/// Helper methods return the accumulator (`int`).
const HELPER_DESCRIPTOR: &str = "()I";
/// The accumulator field type.
const FIELD_DESCRIPTOR: &str = "I";
/// The noise pass.
#[derive(Debug, Default, Clone, Copy)]
pub struct NoisePass;
@ -31,6 +43,7 @@ impl Pass for NoisePass {
fn apply(&self, ctx: &mut Context, program: &mut Program) -> Result<PassReport, CoreError> {
let ratio = ctx.config.noise.junk_ratio;
let methods_per_class = ctx.config.noise.junk_methods_per_class;
let annotations = ctx.config.noise.junk_annotations;
let weave = ctx.config.noise.weave_reachable;
@ -39,7 +52,7 @@ impl Pass for NoisePass {
pass: PassId::Noise,
transformed: 0,
skipped: 0,
notes: vec!["шум выключен: junk_ratio=0, junk_annotations=0".to_string()],
notes: vec!["noise off: junk_ratio=0, junk_annotations=0".to_string()],
});
}
@ -50,45 +63,99 @@ impl Pass for NoisePass {
let real_classes = program.classes.len();
let count = ratio.saturating_mul(real_classes);
let mut junk_names = Vec::with_capacity(count);
let mut suffix = 0usize;
for _ in 0..count {
let name = loop {
let candidate = format!("{JUNK_PREFIX}{suffix}");
suffix = suffix.saturating_add(1);
if program.class(&candidate).is_none() {
break candidate;
}
};
let junk = ClassBuilder::new(name.clone(), None)
.version(version)
.method_with_code(ACC_PUBLIC_STATIC, JUNK_ENTRY.0, JUNK_ENTRY.1, empty_code(0))
.build();
program.classes.push(junk);
junk_names.push(name);
let mut class_names =
NameGenerator::new(ctx.rng("noise/classes"), ctx.config.rename.class_style)
.for_classes();
for class in &program.classes {
class_names.reserve(class.this_class.clone());
}
let mut rng = ctx.rng("noise");
// (class, entry) pairs for weaving, in creation order.
let mut entries: Vec<(String, String)> = Vec::with_capacity(count);
// (class index, method index) of every generated method, for frames.
let mut fresh_methods: Vec<(usize, usize)> = Vec::new();
for index in 0..count {
let package = if real_classes > 0 {
let pick = &program.classes[rng.below(real_classes as u64) as usize].this_class;
parent_package(pick)
} else {
String::new()
};
let simple = class_names.next_name();
let name = if package.is_empty() {
simple
} else {
format!("{package}/{simple}")
};
let mut member_names = NameGenerator::new(
ctx.rng_nth("noise/members", index as u64),
ctx.config.rename.member_style,
);
let entry_name = member_names.next_name();
let field_name = member_names.next_name();
let mut helpers: Vec<(String, String)> = Vec::new();
let mut builder = ClassBuilder::new(name.clone(), None).version(version);
for _ in 0..methods_per_class {
let helper_name = member_names.next_name();
let body = helper_body(&mut rng);
builder = builder.method_with_code(
ACC_PRIVATE_STATIC,
helper_name.clone(),
HELPER_DESCRIPTOR,
body,
);
helpers.push((name.clone(), helper_name));
}
let callees: Vec<(String, String)> = helpers.clone();
let entry = entry_body(&callees, &name, &field_name);
builder = builder.method_with_code(
ACC_PUBLIC_STATIC,
entry_name.clone(),
ENTRY_DESCRIPTOR,
entry,
);
let mut junk = builder.build();
junk.fields.push(Field {
access_flags: ACC_PRIVATE_STATIC,
name: field_name,
descriptor: FIELD_DESCRIPTOR.to_string(),
attributes: Vec::new(),
});
let base = program.classes.len();
for mi in 0..junk.methods.len() {
fresh_methods.push((base, mi));
}
program.classes.push(junk);
entries.push((name, entry_name));
}
let added = entries.len();
// Frames for every generated branching method.
let hierarchy = ClassHierarchy::build(program);
for (ci, mi) in &fresh_methods {
refresh_frames(program, *ci, *mi, &hierarchy)?;
}
let added = junk_names.len();
let mut woven = 0usize;
if weave && added > 0 {
woven = weave_first_reachable(program, real_classes, &junk_names)?;
woven = weave_first_reachable(program, real_classes, &entries)?;
}
let mut skipped = 0usize;
let mut notes = Vec::new();
if added > 0 {
notes.push(format!("добавлено junk-классов: {added}"));
notes.push(format!("junk classes added: {added}"));
}
if woven > 0 {
notes.push(format!("вплетено вызовов: {woven}"));
notes.push(format!("woven calls: {woven}"));
}
if added == 0 && woven == 0 {
notes.push("нечего шуметь: нет классов или методов с кодом".to_string());
notes.push("nothing to fuzz: no classes or methods with code".to_string());
}
if annotations > 0 {
skipped = annotations.saturating_mul(program.classes.len());
notes.push(format!(
"junk_annotations пропущен: сырые атрибуты не могут нести свежие индексы пула (пропущено: {skipped})"
"junk_annotations skipped: raw attributes cannot carry fresh pool indices (skipped: {skipped})"
));
}
@ -105,85 +172,35 @@ impl Pass for NoisePass {
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used, clippy::unwrap_used)]
use noxis_classfile::{Insn, InvokeKind, Resource};
use noxis_core::{Config, NoiseConfig};
use super::*;
fn sample() -> Program {
Program {
classes: vec![
ClassBuilder::new("demo/App", Some("java/lang/Object"))
.version(Version::JAVA_21)
.method_with_code(ACC_PUBLIC_STATIC, "run", "()V", empty_code(0))
.build(),
],
resources: vec![Resource {
path: "app.json".into(),
data: br#"{}"#.to_vec(),
}],
}
}
fn run(program: &mut Program, noise: NoiseConfig) -> PassReport {
let config = Config {
seed: Some(42),
noise,
..Config::default()
};
let mut ctx = Context::new(config);
NoisePass.apply(&mut ctx, program).expect("noise pass")
}
#[test]
fn adds_junk_and_weaves_call() {
let mut program = sample();
let noise = NoiseConfig {
junk_ratio: 2,
junk_annotations: 0,
weave_reachable: true,
};
let report = run(&mut program, noise);
assert_eq!(program.classes.len(), 3);
assert_eq!(program.classes[1].this_class, "shadow/decoy0");
assert_eq!(program.classes[2].this_class, "shadow/decoy1");
let insns = &program.classes[0].methods[0].code.as_ref().unwrap().insns;
assert_eq!(insns.len(), 2);
match &insns[0] {
Insn::Invoke {
kind,
owner,
name,
descriptor,
itf,
} => {
assert_eq!(*kind, InvokeKind::Static);
assert_eq!(owner, "shadow/decoy0");
assert_eq!(name, "run");
assert_eq!(descriptor, "()V");
assert!(!itf);
}
other => panic!("ожидался Invoke, получено: {other:?}"),
}
assert!(matches!(insns[1], Insn::Return(None)));
assert_eq!(report.transformed, 3);
assert_eq!(report.skipped, 0);
}
#[test]
fn disabled_ratio_changes_nothing() {
let mut program = sample();
let before = program.clone();
let report = run(&mut program, NoiseConfig::default());
assert_eq!(program, before);
assert_eq!(report.transformed, 0);
assert_eq!(report.skipped, 0);
}
/// Recompute the frames of a generated method.
fn refresh_frames(
program: &mut Program,
ci: usize,
mi: usize,
hierarchy: &ClassHierarchy,
) -> Result<(), CoreError> {
let (name, snapshot) = {
let class = program
.classes
.get(ci)
.ok_or_else(|| CoreError::pass(PassId::Noise, "class vanished"))?;
(
class.this_class.clone(),
class
.methods
.get(mi)
.cloned()
.ok_or_else(|| CoreError::pass(PassId::Noise, "method vanished"))?,
)
};
let frames = compute_frames(&name, &snapshot, hierarchy)
.map_err(|e| CoreError::pass(PassId::Noise, e.to_string()))?;
program
.classes
.get_mut(ci)
.and_then(|c| c.methods.get_mut(mi))
.and_then(|m| m.code.as_mut())
.ok_or_else(|| CoreError::pass(PassId::Noise, "method vanished"))?
.frames = frames;
Ok(())
}

View file

@ -0,0 +1,208 @@
//! Noise pass tests: junk shape, names, weaving, determinism.
#![allow(clippy::expect_used, clippy::unwrap_used)]
use noxis_classfile::{
ClassBuilder, Insn, InvokeKind, Program, Resource, Version, empty_code, format,
};
use noxis_core::{Config, NoiseConfig};
use super::*;
fn sample() -> Program {
Program {
classes: vec![
ClassBuilder::new("demo/App", Some("java/lang/Object"))
.version(Version::JAVA_21)
.method_with_code(0x0009, "run", "()V", empty_code(0))
.build(),
],
resources: vec![Resource {
path: "app.json".into(),
data: br#"{}"#.to_vec(),
}],
}
}
fn noisy() -> NoiseConfig {
NoiseConfig {
junk_ratio: 2,
junk_methods_per_class: 2,
junk_annotations: 0,
weave_reachable: true,
}
}
fn run(program: &mut Program, noise: NoiseConfig) -> PassReport {
let config = Config {
seed: Some(42),
noise,
..Config::default()
};
let mut ctx = Context::new(config);
NoisePass.apply(&mut ctx, program).expect("noise pass")
}
fn is_identifier(name: &str) -> bool {
let mut chars = name.chars();
match chars.next() {
Some(first) if first.is_alphabetic() || first == '_' || first == '$' => {}
_ => return false,
}
chars.all(|c| c.is_alphanumeric() || c == '_' || c == '$')
}
#[test]
fn adds_junk_with_bodies_and_weaves_call() {
let mut program = sample();
let report = run(&mut program, noisy());
assert_eq!(program.classes.len(), 3);
let junks = &program.classes[1..];
let mut names = Vec::new();
for junk in junks {
// No self-describing fingerprints, valid identifiers, unique.
assert!(
!junk.this_class.contains("shadow") && !junk.this_class.contains("decoy"),
"fingerprint survived: {}",
junk.this_class
);
for part in junk.this_class.split('/') {
assert!(is_identifier(part), "bad segment in {}", junk.this_class);
}
names.push(junk.this_class.clone());
// One accumulator field, fed by the entry.
assert_eq!(junk.fields.len(), 1);
assert_eq!(junk.fields[0].descriptor, "I");
let field = junk.fields[0].name.clone();
// Helpers have real branching bodies, not bare returns.
let helpers: Vec<_> = junk
.methods
.iter()
.filter(|m| m.descriptor == HELPER_DESCRIPTOR)
.collect();
assert_eq!(helpers.len(), 2);
for helper in helpers {
let code = helper.code.as_ref().expect("code");
assert!(
code.insns.iter().any(|i| matches!(i, Insn::Branch(..))),
"helper without branches"
);
assert!(
code.insns.iter().any(|i| matches!(i, Insn::Binary { .. })),
"helper without arithmetic"
);
assert!(!code.frames.is_empty(), "helper frames were not computed");
}
// The entry calls the helpers and stores into the field.
let entry = junk
.methods
.iter()
.find(|m| m.descriptor == ENTRY_DESCRIPTOR)
.expect("entry");
let entry_code = entry.code.as_ref().expect("code");
let calls = entry_code
.insns
.iter()
.filter(|i| matches!(i, Insn::Invoke { .. }))
.count();
assert_eq!(calls, 2, "entry must call every helper");
assert!(
entry_code
.insns
.iter()
.any(|i| matches!(i, Insn::PutStatic { name, .. } if name == &field)),
"entry must feed the field"
);
}
assert_ne!(names[0], names[1]);
// The live method opens with a call into the first junk entry.
let live = program.classes[0].methods[0].code.as_ref().unwrap();
match &live.insns[0] {
Insn::Invoke {
kind,
owner,
descriptor,
itf,
..
} => {
assert_eq!(*kind, InvokeKind::Static);
assert_eq!(owner, &names[0]);
assert_eq!(descriptor, "()V");
assert!(!itf);
}
other => panic!("expected Invoke, got: {other:?}"),
}
assert_eq!(report.transformed, 3);
assert_eq!(report.skipped, 0);
}
#[test]
fn disabled_ratio_changes_nothing() {
let mut program = sample();
let before = program.clone();
let report = run(&mut program, NoiseConfig::default());
assert_eq!(program, before);
assert_eq!(report.transformed, 0);
assert_eq!(report.skipped, 0);
}
#[test]
fn junk_is_deterministic() {
let mut first = sample();
let mut second = sample();
run(&mut first, noisy());
run(&mut second, noisy());
assert_eq!(first, second);
}
/// Junk bodies carry real branches: they must verify under a real JVM.
#[test]
fn junk_verifies_under_java() {
if std::process::Command::new("java")
.arg("-version")
.output()
.is_err()
{
eprintln!("SKIP: no java");
return;
}
let mut program = sample();
run(
&mut program,
NoiseConfig {
junk_ratio: 1,
..noisy()
},
);
let dir = std::env::temp_dir().join(format!("noxis-noise-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
for class in &program.classes {
let bytes = format::write(class).expect("write");
let mut path = dir.clone();
for part in class.this_class.split('/') {
path.push(part);
}
path.set_extension("class");
std::fs::create_dir_all(path.parent().expect("parent")).expect("mkdir");
std::fs::write(&path, bytes).expect("write class");
}
for class in &program.classes {
let dotted = class.this_class.replace('/', ".");
let output = std::process::Command::new("java")
.arg("-Xverify:all")
.arg("-cp")
.arg(&dir)
.arg(&dotted)
.output()
.expect("run java");
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
!stderr.contains("VerifyError") && !stderr.contains("ClassFormatError"),
"{dotted}: {stderr}"
);
}
let _ = std::fs::remove_dir_all(&dir);
}

View file

@ -1,22 +1,23 @@
//! Вплетание junk-классов в граф достижимости живых методов.
//! Weaving junk classes into the reachability graph of live methods.
use noxis_analysis::ClassHierarchy;
use noxis_classfile::{Insn, InvokeKind, Program, compute_frames};
use noxis_core::{CoreError, PassId};
use super::JUNK_ENTRY;
/// The woven call descriptor (entries are always `()V`).
const WOVEN_DESCRIPTOR: &str = "()V";
/// Вплетает вызов первого junk-класса в первый метод с кодом.
/// Weave a call of the first junk entry into the first method with code.
///
/// Вызов встаёт перед терминалом (`Return`/`AReturn`/`AThrow`), чтобы не
/// породить недостижимый хвост без фреймов; фреймы пересчитываются.
/// The call lands before the terminal (`Return`/`AReturn`/`AThrow`) so it
/// never strands an unreachable tail without frames; frames are recomputed.
pub(super) fn weave_first_reachable(
program: &mut Program,
real_classes: usize,
junk_names: &[String],
entries: &[(String, String)],
) -> Result<usize, CoreError> {
let missing = || CoreError::pass(PassId::Noise, "цель вплетания пропала");
let Some(owner) = junk_names.first() else {
let missing = || CoreError::pass(PassId::Noise, "weave target vanished");
let Some((owner, entry)) = entries.first() else {
return Err(missing());
};
let target = program.classes[..real_classes.min(program.classes.len())]
@ -39,8 +40,8 @@ pub(super) fn weave_first_reachable(
let call = Insn::Invoke {
kind: InvokeKind::Static,
owner: owner.clone(),
name: JUNK_ENTRY.0.to_string(),
descriptor: JUNK_ENTRY.1.to_string(),
name: entry.clone(),
descriptor: WOVEN_DESCRIPTOR.to_string(),
itf: false,
};
{

View file

@ -158,7 +158,7 @@ fn params_of(descriptor: &str) -> Option<String> {
Some(descriptor[..=close].to_string())
}
pub(super) fn parent_package(internal: &str) -> String {
pub(crate) fn parent_package(internal: &str) -> String {
match internal.rfind('/') {
Some(i) => internal[..i].to_string(),
None => String::new(),

View file

@ -1,20 +1,20 @@
//! Пасс переименования: классы, поля, методы, локальные переменные, дескрипторы.
//! The rename pass: classes, fields, methods, locals, descriptors.
//!
//! Это самая дешёвая и самая отдающая техника (см. `info/evo-weaknesses.md`:
//! у EvoVis члены не переименовывались вообще, и класс читался по `javap -p`).
//! The cheapest technique with the highest payoff (see `info/evo-weaknesses.md`:
//! EvoVis never renamed members at all, so the class read plainly via `javap -p`).
//!
//! Тонкости, которые здесь учитываются:
//! - вызов `Sub.staticMethod()` может указывать владельцем подкласс - нужны
//! псевдонимы (`alias`) для всей иерархии;
//! - мостовые методы (`ACC_BRIDGE`) и ковариантные возвраты живут в одном
//! «семействе» метода - переименовывать их нужно разом;
//! - `serialVersionUID`, JNI, рефлексия, сериализация - не трогаем.
//! Subtleties handled here:
//! - a `Sub.staticMethod()` call may name a subclass as the owner - aliases
//! (`alias`) are needed across the hierarchy;
//! - bridge methods (`ACC_BRIDGE`) and covariant returns live in one method
//! "family" - they must be renamed together;
//! - `serialVersionUID`, JNI, reflection, serialization stay untouched.
/// Карты и стратегия назначения имён.
mod assign;
/// Переписывание тел методов и атрибутов.
/// Name maps and the assignment strategy.
pub(crate) mod assign;
/// Rewriting method bodies and attributes.
mod rewrite;
/// Интеграционные тесты пасса.
/// Pass integration tests.
#[cfg(test)]
mod tests;
@ -35,7 +35,7 @@ const ACC_STATIC: u16 = 0x0008;
const ACC_FINAL: u16 = 0x0010;
const ACC_ENUM: u16 = 0x4000;
/// Пасс переименования.
/// The rename pass.
#[derive(Debug, Default, Clone, Copy)]
pub struct RenamePass;
@ -58,7 +58,7 @@ impl Pass for RenamePass {
let mut maps = RenameMaps::default();
let mut renamed_classes = 0usize;
// 1. Имена классов.
// 1. Class names.
{
let packages = generate_packages(&cfg, &mut ctx.rng("rename/packages"));
let mut names =
@ -91,7 +91,7 @@ impl Pass for RenamePass {
}
}
// 2. Простые (не-виртуальные) члены: private/static/final.
// 2. Simple (non-virtual) members: private/static/final.
let mut renamed_methods = 0usize;
let mut renamed_fields = 0usize;
@ -138,8 +138,8 @@ impl Pass for RenamePass {
);
renamed_methods += 1;
}
// Виртуальный не-final: переименовывается только вместе
// со всем семейством (см. rename_virtual_families).
// Non-final virtual: renamed only together with the whole
// family (see rename_virtual_families).
_ => method_names.reserve(method.name.clone()),
}
}
@ -179,28 +179,28 @@ impl Pass for RenamePass {
}
}
// 3. Виртуальные семейства - только в агрессивном режиме.
// 3. Virtual families - aggressive mode only.
if cfg.rename.rename_public_members {
renamed_methods += rename_virtual_families(program, &analysis, &mut maps, &cfg, ctx);
}
// 4. Псевдоними для наследников.
// 4. Aliases for subclasses.
maps.expand(&hierarchy, &owners);
// 5. Переписать определения и все ссылки.
// 5. Rewrite definitions and all references.
let mut local_counter = 0u64;
for class in &mut program.classes {
let orig_owner = class.this_class.clone();
rewrite_class(class, &orig_owner, &maps, &cfg, &mut local_counter, ctx);
}
// 6. Ширинирование доступа: после переезда в другие пакеты иначе
// возможен IllegalAccessError.
// 6. Access widening: moving across packages could otherwise
// cause IllegalAccessError.
if cfg.rename.widen_access {
widen_all(program);
}
// 7. Маппинги в отчёт (обратная деобфускация stack trace'ей).
// 7. Mappings for the report (stack-trace deobfuscation).
for ((owner, name, desc), new_name) in maps.methods.iter() {
ctx.mapping
.methods
@ -217,7 +217,7 @@ impl Pass for RenamePass {
let locked = analysis.locked_classes.len();
if locked > 0 {
ctx.note(format!("rename: {locked} классов в keep (не тронуты)"));
ctx.note(format!("rename: {locked} keep-listed classes (untouched)"));
}
Ok(PassReport {
@ -225,7 +225,7 @@ impl Pass for RenamePass {
transformed: renamed_classes + renamed_methods + renamed_fields,
skipped: locked,
notes: vec![format!(
"классов: {renamed_classes}, методов: {renamed_methods}, полей: {renamed_fields}"
"classes: {renamed_classes}, methods: {renamed_methods}, fields: {renamed_fields}"
)],
})
}

View file

@ -7,7 +7,7 @@
use noxis_classfile::{Const, Insn, InvokeKind};
use noxis_runtime::{DECODER_DESC, DECODER_METHOD};
/// Разбить строку на чанки по границе символов (1–3 штуки).
/// Разбить строку на чанки по границе символов (1-3 штуки).
pub(super) fn split_chunks(text: &str, split_above: usize) -> Vec<String> {
let chars: Vec<char> = text.chars().collect();
if chars.len() <= split_above.max(1) {