obfuskator/crates/noxis-classfile/tests/roundtrip/attrs/condy.rs

154 lines
6 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! `CONSTANT_Dynamic` (condy) round trips: the writer must be able to emit a
//! brand-new `ldc` on a `Const::Dynamic`, not just pass one through on read.
//!
//! Why this matters: a generic sandbox-based deobfuscator (narumii/SSVM)
//! confirmed empirically (`todo.md`, 2026-10-14) resolves plain arithmetic
//! and runtime-decrypt calls, but does not resolve `invokedynamic`/condy at
//! all - 0 changes on a condy-only test class. Strings/Constants moving to
//! condy is the first concrete fix that follows from that finding.
#![allow(clippy::expect_used, clippy::unwrap_used)]
use super::super::round_trip;
use noxis_classfile::{
Attribute, BootstrapMethod, ClassBuilder, Code, Const, Insn, Version, format,
};
use ristretto_classfile::attributes::{
Attribute as RtAttr, BootstrapMethod as RtBsm, Instruction as Rt,
};
use ristretto_classfile::{
ClassAccessFlags, ClassFile as RtClass, ConstantPool, JAVA_21, Method as RtMethod,
MethodAccessFlags, ReferenceKind,
};
/// `CONSTANT_Dynamic` (condy, `ldc`) survives the round trip - needed for
/// passes that emit strings/constants through a bootstrap method instead of
/// a plain `ldc` literal.
#[test]
fn condy_ldc_round_trip() {
let mut pool = ConstantPool::default();
let this = pool.add_class("demo/Condy").expect("add_class");
let object = pool.add_class("java/lang/Object").expect("add_object");
let owner = pool.add_class("demo/RT").expect("owner");
let method_ref = pool
.add_method_ref(
owner,
"boot",
"(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/Class;)Ljava/lang/String;",
)
.expect("ref");
let handle = pool
.add_method_handle(ReferenceKind::InvokeStatic, method_ref)
.expect("handle");
let bsm_name = pool.add_utf8("BootstrapMethods").expect("attr");
let name = pool.add_utf8("run").expect("name");
let desc = pool.add_utf8("()Ljava/lang/String;").expect("desc");
let code_name = pool.add_utf8("Code").expect("attr");
let dynamic = pool
.add_dynamic(0, "_", "Ljava/lang/String;")
.expect("condy");
let rt = RtClass {
version: JAVA_21,
constant_pool: pool,
access_flags: ClassAccessFlags::PUBLIC,
this_class: this,
super_class: object,
interfaces: vec![],
fields: vec![],
methods: vec![RtMethod {
access_flags: MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC,
name_index: name,
descriptor_index: desc,
attributes: vec![RtAttr::Code {
name_index: code_name,
max_stack: 1,
max_locals: 0,
code: vec![Rt::Ldc_w(dynamic), Rt::Areturn],
exception_table: vec![],
attributes: vec![],
}],
}],
attributes: vec![RtAttr::BootstrapMethods {
name_index: bsm_name,
methods: vec![RtBsm {
bootstrap_method_ref: handle,
arguments: vec![],
}],
}],
..RtClass::default()
};
let ir = round_trip(&rt);
assert_eq!(ir.attributes.len(), 1);
assert!(format!("{:?}", ir.attributes[0]).contains("BootstrapMethods"));
let code = ir.methods[0].code.as_ref().expect("code");
assert!(
format!("{:?}", code.insns).contains("Dynamic"),
"condy ldc должен пережить round-trip как Const::Dynamic: {:?}",
code.insns
);
}
/// Регрессия: писатель раньше эмитил `Const::Dynamic` ВСЕГДА через
/// `ldc2_w` (категория 2 - long/double, 2 слота стека), даже когда BSM
/// возвращает `String` (категория 1). `ldc2_w` на ссылочном типе -
/// категория инструкции не совпадает с категорией значения, кадры стека
/// и верификатор на реальной JVM это не пропустят. Категория должна
/// читаться из дескриптора condy-константы (JVMS §4.4.10), не быть
/// захардкожена.
#[test]
fn condy_string_writes_ldc_w_not_ldc2_w() {
let class = ClassBuilder::new("demo/CondyOp", None)
.version(Version::JAVA_21)
.attribute(Attribute::BootstrapMethods(vec![BootstrapMethod {
kind: 6, // InvokeStatic
owner: "demo/RT".to_string(),
name: "boot".to_string(),
descriptor: "(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/Class;)Ljava/lang/String;"
.to_string(),
itf: false,
args: vec![],
}]))
.method_with_code(
0x0009,
"run",
"()Ljava/lang/String;",
Code {
max_stack: 1,
max_locals: 0,
insns: vec![
Insn::Const(Const::Dynamic {
name: "_".to_string(),
descriptor: "Ljava/lang/String;".to_string(),
bootstrap: 0,
}),
Insn::AReturn,
],
exception_table: Vec::new(),
frames: Vec::new(),
attributes: Vec::new(),
},
)
.build();
let bytes = format::write(&class).expect("write");
let written = RtClass::from_bytes(&bytes).expect("parse back");
let method = &written.methods[0];
let RtAttr::Code { code, .. } = method
.attributes
.iter()
.find(|a| matches!(a, RtAttr::Code { .. }))
.expect("Code attribute")
else {
unreachable!()
};
assert!(
matches!(code.first(), Some(Rt::Ldc_w(_))),
"condy String должен писаться через ldc_w (категория 1), не ldc2_w: {code:?}"
);
assert!(
!code.iter().any(|insn| matches!(insn, Rt::Ldc2_w(_))),
"ldc2_w не должен появляться для condy-строки: {code:?}"
);
}