154 lines
6 KiB
Rust
154 lines
6 KiB
Rust
//! `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:?}"
|
||
);
|
||
}
|