classfile: fix condy ldc category (ldc_w vs ldc2_w by descriptor), split attrs tests

This commit is contained in:
Loki 2026-10-15 12:53:22 +02:00
parent 6dd9c220d4
commit a4450b69ba
Signed by: boba
GPG key ID: 253067914055423B
3 changed files with 169 additions and 1 deletions

View file

@ -0,0 +1,154 @@
//! `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:?}"
);
}