Initial commit: noxis JVM bytecode obfuscator

Workspace of 9 crates (noxis, noxis-core, noxis-classfile, noxis-analysis,
noxis-passes, noxis-runtime, noxis-jar, noxis-verify, noxis-structure-check).
AGPL-3.0-only license, Rust 2024 edition, unsafe_code denied.
This commit is contained in:
Loki 2026-10-08 22:01:03 +02:00
commit a641e69455
140 changed files with 17197 additions and 0 deletions

View file

@ -0,0 +1,121 @@
//! Эмиссия компактных кадров в точках, где их ждёт верификатор.
//!
//! Вид кадра выбирается сравнением с предыдущим кадром (`Same`/`Append`/
//! `Chop`/`SameLocals`, иначе `Full`) — как у `javac`. Хвостовые `Top`
//! при сравнении и записи отрезаются: пустой слот — не тип.
use std::collections::BTreeSet;
use super::merge::Merger;
use super::xfer::State;
use super::{StackMapFrame, VerificationType};
use crate::error::ClassfileError;
/// Кадры во всех точках `targets` (отсортированы — дельты по порядку).
///
/// Дельты — в номерах инструкций (как backend после чтения); writer сам
/// пересчитает их в байты.
pub(super) fn emit_frames(
states: &[Option<State>],
targets: &BTreeSet<u32>,
initial: &[VerificationType],
merger: &Merger<'_>,
) -> Result<Vec<StackMapFrame>, ClassfileError> {
let mut out = Vec::new();
let mut previous_locals = initial.to_vec();
let mut previous_index: Option<u32> = None;
for target in targets {
let at = *target as usize;
let Some(state) = &states[at] else {
continue;
};
let delta = match previous_index {
None => *target,
Some(previous) => target
.checked_sub(previous)
.and_then(|delta| delta.checked_sub(1))
.ok_or_else(|| merger.invalid("кадры не по порядку"))?,
};
out.push(select(delta, &state.locals, &previous_locals, &state.stack));
previous_locals = state.locals.clone();
previous_index = Some(*target);
}
Ok(out)
}
fn select(
delta: u32,
locals: &[VerificationType],
previous: &[VerificationType],
stack: &[VerificationType],
) -> StackMapFrame {
// Сравнение — на гранулярности переменных (`long` — одна запись,
// как у `javac`): иначе половины пар дают `Append{[Top, Long]}`.
let current = vars(locals);
let base = vars(previous);
if current == base {
if stack.is_empty() {
return StackMapFrame::Same {
offset_delta: delta,
};
}
if let [local] = stack {
return StackMapFrame::SameLocals {
offset_delta: delta,
local: local.clone(),
};
}
}
if stack.is_empty()
&& let Some(added) = current.strip_prefix(base.as_slice())
&& (1..=3).contains(&added.len())
&& !added.contains(&VerificationType::Top)
{
return StackMapFrame::Append {
offset_delta: delta,
locals: added.to_vec(),
};
}
if stack.is_empty()
&& let Some(dropped) = base.strip_prefix(current.as_slice())
&& (1..=3).contains(&dropped.len())
&& let Ok(count) = u8::try_from(dropped.len())
{
return StackMapFrame::Chop {
offset_delta: delta,
count,
};
}
StackMapFrame::Full {
offset_delta: delta,
locals: current,
stack: stack.to_vec(),
}
}
/// Слоты → переменные: `[Long, Top]` — одна запись; хвостовые `Top` — прочь.
///
/// Одиночные `Top` внутри (дыры) сохраняются: в `Full` это легальный тип,
/// а в `Append` мы их не пускаем (см. `select`).
fn vars(locals: &[VerificationType]) -> Vec<VerificationType> {
let mut out = Vec::with_capacity(locals.len());
let mut index = 0;
while index < locals.len() {
match &locals[index] {
VerificationType::Long | VerificationType::Double
if locals.get(index + 1) == Some(&VerificationType::Top) =>
{
out.push(locals[index].clone());
index += 2;
}
other => {
out.push(other.clone());
index += 1;
}
}
}
while out.last() == Some(&VerificationType::Top) {
out.pop();
}
out
}

View file

@ -0,0 +1,122 @@
//! CFG тела метода: метки, рёбра, точки кадров, диапазоны handler'ов.
use std::collections::{BTreeMap, BTreeSet};
use super::merge::Merger;
use crate::error::ClassfileError;
use crate::insn::{Insn, Label};
use crate::model::ExceptionHandler;
/// Метки → номера инструкций без маркеров.
pub(super) fn strip_marks<'a>(
insns: &'a [Insn],
merger: &Merger<'_>,
) -> Result<(BTreeMap<Label, u32>, Vec<&'a Insn>), ClassfileError> {
let mut labels = BTreeMap::new();
let mut bare = Vec::with_capacity(insns.len());
for insn in insns {
match insn {
Insn::Mark(label) => {
let index = u32::try_from(bare.len())
.map_err(|_| merger.invalid("слишком много инструкций"))?;
if let Some(prev) = labels.insert(*label, index)
&& prev != index
{
return Err(merger.invalid(&format!("метка {label} объявлена дважды")));
}
}
_ => bare.push(insn),
}
}
Ok((labels, bare))
}
/// Рёбра CFG без исключений: продолжение + цели переходов.
pub(super) fn successors(
insn: &Insn,
index: u32,
len: u32,
labels: &BTreeMap<Label, u32>,
merger: &Merger<'_>,
) -> Result<Vec<u32>, ClassfileError> {
let mut out = Vec::new();
// Без продолжения: переходы без возврата и выходы из метода.
// `Jsr`/`Ret` сюда не доходят (их отвергает `transfer`), но рёбра есть.
let terminal = matches!(
insn,
Insn::Goto(..)
| Insn::Jsr(..)
| Insn::Ret(..)
| Insn::TableSwitch { .. }
| Insn::LookupSwitch { .. }
| Insn::Return(..)
| Insn::AReturn
| Insn::AThrow
);
if !terminal && index + 1 < len {
out.push(index + 1);
}
for target in insn.targets() {
out.push(
labels
.get(&target)
.copied()
.ok_or_else(|| merger.invalid(&format!("нет метки {target}")))?,
);
}
Ok(out)
}
/// Точки, где верификатор требует кадры: цели переходов + входы handler'ов.
pub(super) fn frame_targets(
bare: &[&Insn],
labels: &BTreeMap<Label, u32>,
handlers: &[HandlerRange],
merger: &Merger<'_>,
) -> Result<BTreeSet<u32>, ClassfileError> {
let mut targets = BTreeSet::new();
for insn in bare {
for target in insn.targets() {
targets.insert(
labels
.get(&target)
.copied()
.ok_or_else(|| merger.invalid(&format!("нет метки {target}")))?,
);
}
}
for (_, _, handler, _) in handlers {
targets.insert(*handler);
}
Ok(targets)
}
/// Точка handler'а в номерах инструкций: `(start, end, handler, тип перехвата)`.
///
/// Диапазоны полуоткрытые `[start, end)`, как в JVMS.
pub(super) type HandlerRange = (u32, u32, u32, Option<String>);
/// Handler'ы в номерах инструкций для рёбер исключений.
pub(super) fn handler_ranges(
table: &[ExceptionHandler],
labels: &BTreeMap<Label, u32>,
merger: &Merger<'_>,
) -> Result<Vec<HandlerRange>, ClassfileError> {
table
.iter()
.map(|handler| {
let index_of = |label: Label| -> Result<u32, ClassfileError> {
labels
.get(&label)
.copied()
.ok_or_else(|| merger.invalid(&format!("нет метки {label}")))
};
Ok((
index_of(handler.start)?,
index_of(handler.end)?,
index_of(handler.handler)?,
handler.catch_type.clone(),
))
})
.collect()
}

View file

@ -0,0 +1,158 @@
//! Разбор дескрипторов типов (`(...)V`, `[L...;`).
use super::super::VerificationType;
/// Ошибка разбора дескриптора (контекст добавляет вызывающий код).
#[derive(Debug)]
pub(crate) struct DescError(pub String);
/// Уложить слоты значения в locals с позиции; вернуть следующую позицию.
pub(crate) fn write_local(
locals: &mut [VerificationType],
slot: usize,
value: Vec<VerificationType>,
) -> Result<usize, ()> {
if slot + value.len() > locals.len() {
return Err(());
}
let width = value.len().max(1);
for (offset, item) in value.into_iter().enumerate() {
locals[slot + offset] = item;
}
Ok(slot + width)
}
/// Разобрать дескриптор массива на (глубина, элемент без `;`).
pub(crate) fn split_array(descriptor: &str) -> Option<(usize, &str)> {
let dims = descriptor.bytes().take_while(|&b| b == b'[').count();
if dims == 0 {
return None;
}
Some((dims, descriptor[dims..].trim_end_matches(';')))
}
/// Слоты одного полевого дескриптора (`long`/`double` — парой с `Top`).
pub(crate) fn field_slots(descriptor: &str) -> Result<Vec<VerificationType>, DescError> {
let mut chars = descriptor.chars();
match chars.next() {
Some('B') | Some('C') | Some('I') | Some('S') | Some('Z') => {
Ok(vec![VerificationType::Integer])
}
Some('F') => Ok(vec![VerificationType::Float]),
Some('J') => Ok(vec![VerificationType::Long, VerificationType::Top]),
Some('D') => Ok(vec![VerificationType::Double, VerificationType::Top]),
Some('V') => Ok(Vec::new()),
Some('L') => {
let mut name = String::new();
for char in chars {
if char == ';' {
return Ok(vec![VerificationType::Object(name)]);
}
name.push(char);
}
Err(DescError(format!("нет `;` в {descriptor}")))
}
Some('[') => Ok(vec![VerificationType::Object(descriptor.to_string())]),
_ => Err(DescError(format!("кривой дескриптор {descriptor}"))),
}
}
/// Слоты параметров и возврата (`None` — `void`).
type MethodSlots = (Vec<Vec<VerificationType>>, Option<Vec<VerificationType>>);
/// Дескриптор метода: слоты параметров и слоты возврата (`None` — `void`).
pub(crate) fn method_slots(descriptor: &str) -> Result<MethodSlots, DescError> {
let (params, ret) = descriptor
.split_once('(')
.and_then(|(_, rest)| rest.split_once(')'))
.ok_or_else(|| DescError(format!("кривой дескриптор {descriptor}")))?;
let mut out = Vec::new();
let mut index = 0;
let bytes = params.as_bytes();
while index < bytes.len() {
match bytes[index] as char {
'B' | 'C' | 'I' | 'S' | 'Z' => {
out.push(vec![VerificationType::Integer]);
index += 1;
}
'F' => {
out.push(vec![VerificationType::Float]);
index += 1;
}
'J' => {
out.push(vec![VerificationType::Long, VerificationType::Top]);
index += 1;
}
'D' => {
out.push(vec![VerificationType::Double, VerificationType::Top]);
index += 1;
}
'L' => {
let end = params[index..]
.find(';')
.ok_or_else(|| DescError(format!("нет `;` в {descriptor}")))?;
out.push(vec![VerificationType::Object(
params[index + 1..index + end].to_string(),
)]);
index += end + 1;
}
'[' => {
let mut end = index;
while bytes[end] == b'[' {
end += 1;
}
if bytes[end] == b'L' {
let semi = params[end..]
.find(';')
.ok_or_else(|| DescError(format!("нет `;` в {descriptor}")))?;
end += semi + 1;
} else {
end += 1;
}
if end > bytes.len() {
return Err(DescError(format!("кривой дескриптор {descriptor}")));
}
out.push(vec![VerificationType::Object(
params[index..end].to_string(),
)]);
index = end;
}
_ => return Err(DescError(format!("кривой дескриптор {descriptor}"))),
}
}
let ret = if ret == "V" {
None
} else {
Some(field_slots(ret)?)
};
Ok((out, ret))
}
/// Тип элемента по дескриптору массива в стеке.
pub(crate) fn array_element(
array: &VerificationType,
kind: crate::insn::ArrayKind,
) -> VerificationType {
use crate::insn::ArrayKind::{Boolean, Byte, Char, Double, Float, Int, Long, Reference, Short};
if let VerificationType::Object(descriptor) = array
&& let Some((dims, elem)) = split_array(descriptor)
{
if dims > 1 {
return VerificationType::Object(descriptor[1..].to_string());
}
return match elem {
"Z" | "B" | "C" | "S" | "I" => VerificationType::Integer,
"J" => VerificationType::Long,
"F" => VerificationType::Float,
"D" => VerificationType::Double,
_ => VerificationType::Object(format!("[{elem};")),
};
}
match kind {
Boolean | Byte | Short | Char | Int => VerificationType::Integer,
Long => VerificationType::Long,
Float => VerificationType::Float,
Double => VerificationType::Double,
Reference => VerificationType::Object("java/lang/Object".to_string()),
}
}

View file

@ -0,0 +1,198 @@
//! Слияние типов (LUB) и разбор дескрипторов.
//!
//! Решётка: `Top` — «пусто» (берётся сосед), `Null` впитывается в ссылку,
//! одинаковые — сами в себя, ссылки — наименьший общий предок по иерархии.
//! Несочетаемые инициализированные типы (`int` vs ссылка после `astore`
//! в handler'е) дают `Top`, как обрезанные кадры `javac`: `Top` в кадре
//! принимает любое фактическое значение, поэтому это всегда sound.
//! Исключение — неинициализированные типы: их слияние означает сломанный
//! поток конструирования, здесь честная ошибка, а не молчаливый `Top`.
pub(super) mod descs;
use super::VerificationType;
use super::xfer::State;
use crate::error::ClassfileError;
use crate::model::Method;
use descs::write_local;
/// Иерархия классов для LUB: достаточно цепочки суперклассов.
pub trait Hierarchy {
/// Прямой суперкласс (`None` — `java/lang/Object` или неизвестный).
fn superclass(&self, class: &str) -> Option<String>;
}
/// Контекст слияния: имена для ошибок + иерархия для LCA.
pub(super) struct Merger<'h> {
pub class: String,
pub method: String,
pub hierarchy: &'h dyn Hierarchy,
}
impl Merger<'_> {
pub fn invalid(&self, msg: &str) -> ClassfileError {
ClassfileError::InvalidCode {
class: self.class.clone(),
msg: format!("{}: {msg}", self.method),
}
}
/// Влить входящее состояние; `true` — слот изменился.
pub fn merge_into(
&self,
slot: &mut Option<State>,
incoming: State,
) -> Result<bool, ClassfileError> {
match slot {
None => {
*slot = Some(incoming);
Ok(true)
}
Some(old) => {
let merged = self.lub_states(old, &incoming)?;
if *old == merged {
Ok(false)
} else {
*old = merged;
Ok(true)
}
}
}
}
/// Начальное состояние: параметры (+ `this`) поверх `Top`.
pub fn initial(&self, method: &Method, max_locals: u16) -> Result<State, ClassfileError> {
let is_static = method.access_flags & 0x0008 != 0;
let (params, _) =
descs::method_slots(&method.descriptor).map_err(|e| self.invalid(&e.0))?;
let mut locals = vec![VerificationType::Top; max_locals as usize];
let mut slot = 0;
if !is_static {
let this = if method.name == "<init>" {
VerificationType::UninitializedThis
} else {
VerificationType::Object(self.class.clone())
};
slot = write_local(&mut locals, slot, vec![this])
.map_err(|_| self.invalid("параметры не влезают в locals"))?;
}
for param in params {
slot = write_local(&mut locals, slot, param)
.map_err(|_| self.invalid("параметры не влезают в locals"))?;
}
Ok(State {
locals,
stack: Vec::new(),
})
}
fn lub_states(&self, first: &State, second: &State) -> Result<State, ClassfileError> {
if first.locals.len() != second.locals.len() {
return Err(self.invalid("разная длина locals при слиянии"));
}
if first.stack.len() != second.stack.len() {
return Err(self.invalid("разная глубина стека при слиянии"));
}
Ok(State {
locals: first
.locals
.iter()
.zip(&second.locals)
.map(|(a, b)| self.lub_types(a, b))
.collect::<Result<_, _>>()?,
stack: first
.stack
.iter()
.zip(&second.stack)
.map(|(a, b)| self.lub_types(a, b))
.collect::<Result<_, _>>()?,
})
}
fn lub_types(
&self,
first: &VerificationType,
second: &VerificationType,
) -> Result<VerificationType, ClassfileError> {
use VerificationType::{Double, Float, Integer, Long, Null, Object, Top};
if first == second {
return Ok(first.clone());
}
match (first, second) {
(Top, other) | (other, Top) => Ok(other.clone()),
(Null, other @ Object(_)) | (other @ Object(_), Null) => Ok(other.clone()),
(Null, Null) => Ok(Null),
(Integer, Integer) | (Float, Float) | (Long, Long) | (Double, Double) => {
Ok(first.clone())
}
(Object(a), Object(b)) => Ok(Object(self.common_superclass(a, b))),
// Одинаковые уже разобраны выше; здесь неинициализированные
// всегда разные — это сломанный поток конструирования.
(VerificationType::UninitializedThis | VerificationType::Uninitialized(_), _)
| (_, VerificationType::UninitializedThis | VerificationType::Uninitialized(_)) => {
Err(self.invalid("слияние неинициализированных типов"))
}
// Несочетаемые инициализированные (например, `int` из тела `try`
// и `Throwable` из handler'а в одном слоте): `Top`, как у `javac`.
_ => Ok(Top),
}
}
/// Наименьший общий предок: подъём по цепочкам, запасной — `Object`.
fn common_superclass(&self, first: &str, second: &str) -> String {
if first == second {
return first.to_string();
}
if descs::split_array(first).is_some() || descs::split_array(second).is_some() {
return self.array_lca(first, second);
}
let mut chain = Vec::new();
let mut current = Some(first.to_string());
let mut guard = 0;
while let Some(name) = current {
if name == second {
return name;
}
chain.push(name.clone());
current = self.hierarchy.superclass(&name);
guard += 1;
if guard > 1024 {
break;
}
}
let mut current = Some(second.to_string());
guard = 0;
while let Some(name) = current {
if chain.contains(&name) {
return name;
}
current = self.hierarchy.superclass(&name);
guard += 1;
if guard > 1024 {
break;
}
}
"java/lang/Object".to_string()
}
/// LCA массивов: та же размерность + LCA элементов, иначе `Object`.
fn array_lca(&self, first: &str, second: &str) -> String {
let (dims_a, elem_a) = descs::split_array(first).unwrap_or((0, first));
let (dims_b, elem_b) = descs::split_array(second).unwrap_or((0, second));
if dims_a == 0 || dims_b == 0 || dims_a != dims_b {
return "java/lang/Object".to_string();
}
if elem_a == elem_b {
return first.to_string();
}
if elem_a.len() == 1 || elem_b.len() == 1 {
return "java/lang/Object".to_string();
}
let common = self.common_superclass(
elem_a.strip_prefix('L').unwrap_or(elem_a),
elem_b.strip_prefix('L').unwrap_or(elem_b),
);
format!("{}L{common};", "[".repeat(dims_a))
}
}

View file

@ -0,0 +1,235 @@
//! `StackMapTable`: представление фреймов и вычисление после трансформаций.
//!
//! Фреймы — самая дорогая часть любого обфускатора на Rust: JVM-верификатор
//! (class version ≥ 50) требует точного `StackMapTable`, иначе класс не грузится.
//! Варианты реализации и их цены — `docs/stack.md`.
//!
//! Метод: CFG по меткам → worklist с типами (`xfer`) → слияние LUB (`merge`)
//! → эмиссия компактных кадров (`emit`). Байтовые смещения берутся тем же
//! преобразованием, что использует writer, — иначе кадры разъедутся с байтами.
mod emit;
mod flow;
mod merge;
mod xfer;
#[cfg(test)]
mod tests;
use std::collections::{BTreeMap, VecDeque};
use crate::error::ClassfileError;
use crate::insn::{Insn, Label, Prim};
use crate::model::Method;
use flow::{HandlerRange, frame_targets, handler_ranges, strip_marks, successors};
pub use merge::Hierarchy;
use merge::Merger;
use xfer::{State, transfer};
/// Тип верификации из JVMS §4.10.1.
#[derive(Debug, Clone, PartialEq)]
pub enum VerificationType {
Top,
Integer,
Float,
Double,
Long,
Null,
UninitializedThis,
/// Внутреннее имя класса (`java/lang/Object`) или дескриптор массива.
Object(String),
/// Байтовое смещение инструкции `new`.
Uninitialized(u16),
}
impl VerificationType {
/// Тип, соответствующий примитиву.
pub fn of_prim(p: Prim) -> Self {
match p {
Prim::Int => VerificationType::Integer,
Prim::Float => VerificationType::Float,
Prim::Long => VerificationType::Long,
Prim::Double => VerificationType::Double,
}
}
}
/// Запись `StackMapTable` (JVMS §4.7.4).
#[derive(Debug, Clone, PartialEq)]
pub enum StackMapFrame {
Same {
offset_delta: u32,
},
SameLocals {
offset_delta: u32,
local: VerificationType,
},
Chop {
offset_delta: u32,
count: u8,
},
Append {
offset_delta: u32,
locals: Vec<VerificationType>,
},
Full {
offset_delta: u32,
locals: Vec<VerificationType>,
stack: Vec<VerificationType>,
},
}
/// Вычислить фреймы для тела метода после всех трансформаций.
///
/// Кадры нужны только классам версии ≥ 50; `jsr`/`ret` отвергаются (такие
/// классы старше и кадры им не нужны). Иерархия — для LUB ссылочных типов.
pub fn compute_frames(
class: &str,
method: &Method,
hierarchy: &dyn Hierarchy,
) -> Result<Vec<StackMapFrame>, ClassfileError> {
let code = method
.code
.as_ref()
.ok_or_else(|| invalid(class, &method.name, "нет тела"))?;
if code.insns.is_empty() {
return Ok(Vec::new());
}
let merger = Merger {
class: class.to_string(),
method: method.name.clone(),
hierarchy,
};
let (labels, bare) = strip_marks(&code.insns, &merger)?;
let bytes = layout(&bare, &labels, &merger)?;
let handlers = handler_ranges(&code.exception_table, &labels, &merger)?;
let initial = merger.initial(method, code.max_locals)?;
let mut states: Vec<Option<State>> = vec![None; bare.len()];
states[0] = Some(initial.clone());
run_worklist(&merger, &bare, &bytes, &labels, &handlers, &mut states)?;
let targets = frame_targets(&bare, &labels, &handlers, &merger)?;
emit::emit_frames(&states, &targets, &initial.locals, &merger)
}
/// Байтовое смещение каждой инструкции — тем же кодированием, что в writer'е.
fn layout(
bare: &[&Insn],
labels: &BTreeMap<Label, u32>,
merger: &Merger<'_>,
) -> Result<Vec<u16>, ClassfileError> {
use crate::format::write::{code, pool::Pool};
let mut pool = Pool::new();
let resolve = |label: Label| -> Result<u32, ClassfileError> {
labels
.get(&label)
.copied()
.ok_or_else(|| merger.invalid(&format!("нет метки {label}")))
};
let mut rt_insns = Vec::with_capacity(bare.len());
for (index, insn) in bare.iter().enumerate() {
let index = u32::try_from(index).map_err(|_| merger.invalid("слишком много инструкций"))?;
if let Some(rt) = code::insns::convert(insn, index, &mut pool, &resolve)? {
rt_insns.push(rt);
}
}
let map = code::measure(&rt_insns)?;
(0..bare.len())
.map(|index| {
map.get(&(index as u32))
.copied()
.ok_or_else(|| merger.invalid("замер потерял инструкцию"))
})
.collect()
}
/// Основной цикл: состояния текут по рёбрам CFG до неподвижной точки.
fn run_worklist(
merger: &Merger<'_>,
bare: &[&Insn],
bytes: &[u16],
labels: &BTreeMap<Label, u32>,
handlers: &[HandlerRange],
states: &mut [Option<State>],
) -> Result<(), ClassfileError> {
let mut queue: VecDeque<u32> = VecDeque::from([0]);
let mut fuel = 1_000_000u32;
while let Some(index) = queue.pop_front() {
let at = index as usize;
let state = states[at]
.clone()
.ok_or_else(|| merger.invalid("пустое состояние"))?;
let mut out = state;
transfer(&merger.class, &merger.method, bare[at], bytes[at], &mut out).map_err(
|error| match error {
ClassfileError::InvalidCode { class, msg } => ClassfileError::InvalidCode {
class,
msg: format!("[{at}] {msg}"),
},
other => other,
},
)?;
let nexts = successors(bare[at], index, bare.len() as u32, labels, merger)?;
for next in nexts {
flow_into(merger, states, &mut queue, &mut fuel, next, out.clone())?;
}
for (start, end, handler, catch) in handlers {
if index >= *start && index < *end {
let caught = State {
locals: out.locals.clone(),
stack: vec![catch_type(catch)],
};
flow_into(merger, states, &mut queue, &mut fuel, *handler, caught)?;
}
}
}
Ok(())
}
/// Влить состояние в инструкцию; `true` — изменилось, продолжить обход.
fn flow_into(
merger: &Merger<'_>,
states: &mut [Option<State>],
queue: &mut VecDeque<u32>,
fuel: &mut u32,
next: u32,
state: State,
) -> Result<(), ClassfileError> {
if *fuel == 0 {
return Err(merger.invalid("нет неподвижной точки"));
}
*fuel -= 1;
let slot = states
.get_mut(next as usize)
.ok_or_else(|| merger.invalid("переход вне тела метода"))?;
let changed = merger
.merge_into(slot, state)
.map_err(|error| match error {
ClassfileError::InvalidCode { class, msg } => ClassfileError::InvalidCode {
class,
msg: format!("[->{next}] {msg}"),
},
other => other,
})?;
if changed {
queue.push_back(next);
}
Ok(())
}
/// Рёбра CFG без исключений: продолжение + цели переходов.
/// Тип исключения на входе в handler (`catch-all` — любое throwable).
fn catch_type(catch: &Option<String>) -> VerificationType {
match catch {
Some(name) => VerificationType::Object(name.clone()),
None => VerificationType::Object("java/lang/Throwable".to_string()),
}
}
fn invalid(class: &str, method: &str, msg: &str) -> ClassfileError {
ClassfileError::InvalidCode {
class: class.to_string(),
msg: format!("{method}: {msg}"),
}
}

View file

@ -0,0 +1,179 @@
//! Unit-тесты `compute_frames` на синтезированном IR.
#![allow(clippy::expect_used, clippy::unwrap_used)]
use super::{StackMapFrame, VerificationType, compute_frames};
use crate::Hierarchy;
use crate::insn::{BranchCond, Cond, Const, Insn, Prim, SlotKind};
use crate::model::{Code, ExceptionHandler, Method};
/// Пустая иерархия: всё неизвестное — наследники `Object` (для LUB хватает).
struct NoHierarchy;
impl Hierarchy for NoHierarchy {
fn superclass(&self, _class: &str) -> Option<String> {
None
}
}
fn code_method(
name: &str,
descriptor: &str,
max_stack: u16,
max_locals: u16,
insns: Vec<Insn>,
table: Vec<ExceptionHandler>,
) -> Method {
Method {
access_flags: 0x0008,
name: name.to_string(),
descriptor: descriptor.to_string(),
code: Some(Code {
max_stack,
max_locals,
insns,
exception_table: table,
frames: vec![],
attributes: vec![],
}),
attributes: vec![],
}
}
/// Ветвь → два кадра (`Same`, `SameLocals`).
#[test]
fn branch_emits_same_frames() {
let mut method = code_method(
"max",
"(II)I",
2,
2,
vec![
Insn::Mark(0),
load(0),
load(1),
Insn::Branch(BranchCond::Int(Cond::Ge), 1),
Insn::Const(Const::Int(0)),
Insn::Goto(2),
Insn::Mark(1),
Insn::Const(Const::Int(1)),
Insn::Mark(2),
Insn::Return(Some(Prim::Int)),
],
vec![],
);
method.access_flags = 0x0009;
let frames = compute_frames("demo/Max", &method, &NoHierarchy).expect("frames");
assert_eq!(
frames,
vec![
StackMapFrame::Same { offset_delta: 5 },
StackMapFrame::SameLocals {
offset_delta: 0,
local: VerificationType::Integer,
},
]
);
}
/// `try/catch`: handler получает `Full`, продолжение — `Append`.
#[test]
fn handler_emits_full_frame() {
let method = code_method(
"run",
"()V",
1,
2,
vec![
Insn::Mark(0),
Insn::Const(Const::Int(0)),
store(0),
Insn::Mark(1),
Insn::Const(Const::Int(1)),
store(0),
Insn::Goto(3),
Insn::Mark(2),
store_ref(1),
Insn::Const(Const::Int(2)),
store(0),
Insn::Mark(3),
Insn::Return(None),
],
vec![ExceptionHandler {
start: 1,
end: 2,
handler: 2,
catch_type: Some("java/lang/Exception".to_string()),
}],
);
let frames = compute_frames("demo/Try", &method, &NoHierarchy).expect("frames");
assert_eq!(
frames,
vec![
StackMapFrame::Full {
offset_delta: 5,
locals: vec![VerificationType::Integer],
stack: vec![VerificationType::Object("java/lang/Exception".to_string())],
},
StackMapFrame::Append {
offset_delta: 2,
locals: vec![VerificationType::Object("java/lang/Exception".to_string())],
},
]
);
}
/// Два `new` на разных ветвях нельзя слить — честная ошибка, не `Top`.
#[test]
fn uninitialized_merge_is_rejected() {
let method = code_method(
"pick",
"(Z)Ljava/lang/Object;",
1,
2,
vec![
Insn::Mark(0),
load(0),
Insn::Branch(BranchCond::Zero(Cond::Ne), 1),
Insn::New("a/A".to_string()),
store_ref(1),
Insn::Goto(2),
Insn::Mark(1),
Insn::New("a/A".to_string()),
store_ref(1),
Insn::Mark(2),
Insn::Load {
slot: 1,
kind: SlotKind::Ref,
},
Insn::AReturn,
],
vec![],
);
let err = compute_frames("demo/Pick", &method, &NoHierarchy).expect_err("слияние");
assert!(
err.to_string().contains("неинициализированных"),
"не тот текст: {err}"
);
}
fn load(slot: u16) -> Insn {
Insn::Load {
slot,
kind: SlotKind::Int,
}
}
fn store(slot: u16) -> Insn {
Insn::Store {
slot,
kind: SlotKind::Int,
}
}
fn store_ref(slot: u16) -> Insn {
Insn::Store {
slot,
kind: SlotKind::Ref,
}
}

View file

@ -0,0 +1,133 @@
//! Вызовы и константы: дескрипторы, `<init>`, слоты значений.
use super::super::VerificationType;
use super::super::merge::descs::{DescError, field_slots, method_slots};
use super::State;
use crate::error::ClassfileError;
use crate::insn::{Const, Convert, InvokeKind, Prim};
pub(super) fn const_slots(value: &Const) -> Result<Vec<VerificationType>, DescError> {
Ok(match value {
Const::Int(_) => vec![VerificationType::Integer],
Const::Long(_) => vec![VerificationType::Long, VerificationType::Top],
Const::Float(_) => vec![VerificationType::Float],
Const::Double(_) => vec![VerificationType::Double, VerificationType::Top],
Const::String(_) => vec![VerificationType::Object("java/lang/String".to_string())],
Const::Class(_) => vec![VerificationType::Object("java/lang/Class".to_string())],
Const::MethodType(_) => vec![VerificationType::Object(
"java/lang/invoke/MethodType".to_string(),
)],
Const::MethodHandle { .. } => vec![VerificationType::Object(
"java/lang/invoke/MethodHandle".to_string(),
)],
Const::Dynamic { descriptor, .. } => field_slots(descriptor)?,
})
}
pub(super) fn invoke(
class: &str,
method: &str,
state: &mut State,
kind: InvokeKind,
owner: &str,
name: &str,
descriptor: &str,
) -> Result<(), ClassfileError> {
let (params, ret) = method_slots(descriptor).map_err(|e| context(class, method, e))?;
super::stack::pop_slots(
state,
count_slots(&params),
class,
method,
"аргументы вызова",
)?;
let mut receiver = None;
if kind != InvokeKind::Static {
receiver = super::stack::pop_slots(state, 1, class, method, "objectref")?.pop();
}
// `<init>` инициализирует получателя во ВСЕХ его копиях.
if name == "<init>" {
match receiver {
Some(VerificationType::UninitializedThis) => {
replace(
state,
&VerificationType::UninitializedThis,
&initialized(class),
);
}
Some(VerificationType::Uninitialized(_)) => {
let fresh = VerificationType::Object(owner.to_string());
replace_uninit(state, &fresh);
}
_ => {}
}
}
if let Some(ret) = ret {
super::stack::push(state, ret)?;
}
Ok(())
}
/// Число слотов всех параметров.
pub(super) fn count_slots(params: &[Vec<VerificationType>]) -> usize {
params.iter().map(Vec::len).sum()
}
/// Заменить все вхождения типа новым (инициализация через `<init>`).
fn replace(state: &mut State, from: &VerificationType, to: &VerificationType) {
for slot in state.locals.iter_mut().chain(state.stack.iter_mut()) {
if slot == from {
*slot = to.clone();
}
}
}
/// Любой `Uninitialized` (кроме `this`) → тип владельца (созданный `new`).
fn replace_uninit(state: &mut State, to: &VerificationType) {
for slot in state.locals.iter_mut().chain(state.stack.iter_mut()) {
if matches!(slot, VerificationType::Uninitialized(_)) {
*slot = to.clone();
}
}
}
fn initialized(class: &str) -> VerificationType {
VerificationType::Object(class.to_string())
}
pub(super) fn context(class: &str, method: &str, error: DescError) -> ClassfileError {
super::super::invalid(class, method, &error.0)
}
pub(super) fn prim_slots(prim: Prim) -> u32 {
match prim {
Prim::Int | Prim::Float => 1,
Prim::Long | Prim::Double => 2,
}
}
pub(super) fn prim_push(prim: Prim) -> Vec<VerificationType> {
match VerificationType::of_prim(prim) {
value @ (VerificationType::Long | VerificationType::Double) => {
vec![value, VerificationType::Top]
}
value => vec![value],
}
}
/// `(слоты аргумента, примитив результата)` для конверсии.
pub(super) fn convert_shapes(op: Convert) -> (usize, Prim) {
use Convert::{D2F, D2I, D2L, F2D, F2I, F2L, I2B, I2C, I2D, I2F, I2L, I2S, L2D, L2F, L2I};
use Prim::{Double, Float, Int, Long};
match op {
I2L => (1, Long),
I2D => (1, Double),
I2F | I2B | I2C | I2S => (1, if op == I2F { Float } else { Int }),
L2I | L2F => (2, if op == L2F { Float } else { Int }),
L2D => (2, Double),
F2I | F2L => (1, if op == F2L { Long } else { Int }),
F2D => (1, Double),
D2I | D2L => (2, if op == D2L { Long } else { Int }),
D2F => (2, Float),
}
}

View file

@ -0,0 +1,233 @@
//! Состояние типов и transfer-функции инструкций.
//!
//! Стек растёт вправо (`push` — в конец). `long`/`double` занимают два слота:
//! значение + `Top`. Проверяется только ФОРМА (глубина, категории), не
//! соответствие типов — неверные типы честно упадут на верификаторе JVM.
pub(super) mod calls;
pub(super) mod stack;
use super::merge::descs::{array_element, field_slots, method_slots};
use super::{VerificationType, invalid};
use crate::error::ClassfileError;
use crate::insn::{Arith, ArrayKind, BranchCond, Insn, SlotKind};
use calls::{const_slots, context, convert_shapes, count_slots, invoke, prim_push, prim_slots};
use stack::{dup, dup2, local_get, pop_slots, push, store_local, wide_push};
/// Типы locals + operand stack в одной точке программы.
#[derive(Debug, Clone, Default, PartialEq)]
pub(super) struct State {
pub locals: Vec<VerificationType>,
pub stack: Vec<VerificationType>,
}
/// Применить инструкцию к состоянию (на месте).
pub(super) fn transfer(
class: &str,
method: &str,
insn: &Insn,
offset: u16,
state: &mut State,
) -> Result<(), ClassfileError> {
match insn {
Insn::Mark(label) => Err(invalid(class, method, &format!("маркер {label} не снят"))),
Insn::Nop => Ok(()),
Insn::AconstNull => push(state, vec![VerificationType::Null]),
Insn::Const(value) => {
let slots = const_slots(value).map_err(|e| context(class, method, e))?;
push(state, slots)
}
Insn::Load { slot, kind } => {
let value = local_get(state, *slot, class, method)?;
let _ = kind;
push(state, wide_push(value))
}
Insn::Store { slot, kind } => {
let slots = usize::from(*kind == SlotKind::Long || *kind == SlotKind::Double) + 1;
let taken = pop_slots(state, slots, class, method, "store")?;
store_local(state, *slot, *kind, taken, class, method)
}
Insn::Iinc { index, .. } => {
let current = local_get(state, *index, class, method)?;
if current != VerificationType::Integer {
return Err(invalid(class, method, "iinc не по int"));
}
Ok(())
}
Insn::ArrayLoad(kind) => {
pop_slots(state, 1, class, method, "индекс массива")?;
let array = pop_slots(state, 1, class, method, "массив")?.remove(0);
push(state, vec![array_element(&array, *kind)])
}
Insn::ArrayStore(kind) => {
let count = match kind {
ArrayKind::Long | ArrayKind::Double => 2,
_ => 1,
};
pop_slots(state, count, class, method, "значение")?;
pop_slots(state, 1, class, method, "индекс массива")?;
pop_slots(state, 1, class, method, "массив")?;
Ok(())
}
Insn::Pop => {
let top = pop_slots(state, 1, class, method, "pop")?.remove(0);
if top == VerificationType::Top {
return Err(invalid(class, method, "pop половины long/double"));
}
Ok(())
}
Insn::Pop2 => {
pop_slots(state, 2, class, method, "pop2")?;
Ok(())
}
Insn::Dup => dup(state, class, method, &[1], &[1, 1]),
Insn::DupX1 => dup(state, class, method, &[2, 1], &[1, 2, 1]),
Insn::DupX2 => dup(state, class, method, &[3, 2, 1], &[1, 3, 2, 1]),
Insn::Dup2 => dup2(state, class, method, false, false),
Insn::Dup2X1 => dup2(state, class, method, true, false),
Insn::Dup2X2 => dup2(state, class, method, true, true),
Insn::Swap => {
let mut taken = pop_slots(state, 2, class, method, "swap")?;
if taken.contains(&VerificationType::Top) {
return Err(invalid(class, method, "swap с половиной"));
}
taken.reverse();
push(state, taken)
}
Insn::Binary { prim, op } => {
let slots = prim_slots(*prim);
if *op == Arith::Neg {
pop_slots(state, slots as usize, class, method, "neg")?;
} else if matches!(op, Arith::Shl | Arith::Shr | Arith::Ushr) {
// Сдвиг берёт (значение, int), а не два значения.
pop_slots(state, slots as usize + 1, class, method, "shift")?;
} else {
pop_slots(state, 2 * slots as usize, class, method, "arith")?;
}
push(state, prim_push(*prim))
}
Insn::Cmp { prim, .. } => {
pop_slots(state, 2 * prim_slots(*prim) as usize, class, method, "cmp")?;
push(state, vec![VerificationType::Integer])
}
Insn::Convert(op) => {
let (src, dst) = convert_shapes(*op);
pop_slots(state, src, class, method, "convert")?;
push(state, prim_push(dst))
}
Insn::Branch(cond, _) => {
let count = match cond {
BranchCond::Zero(..) | BranchCond::Null { .. } => 1,
BranchCond::Int(..) | BranchCond::Ref { .. } => 2,
};
pop_slots(state, count, class, method, "branch")?;
Ok(())
}
Insn::Goto(_) => Ok(()),
Insn::Jsr(_) | Insn::Ret(_) => Err(invalid(
class,
method,
"подпрограммы jsr/ret не поддерживаются",
)),
Insn::TableSwitch { .. } | Insn::LookupSwitch { .. } => {
pop_slots(state, 1, class, method, "switch")?;
Ok(())
}
Insn::Return(prim) => {
if let Some(prim) = prim {
pop_slots(state, prim_slots(*prim) as usize, class, method, "return")?;
}
Ok(())
}
Insn::AReturn => {
pop_slots(state, 1, class, method, "areturn")?;
Ok(())
}
Insn::GetStatic { descriptor, .. } => {
let slots = field_slots(descriptor).map_err(|e| context(class, method, e))?;
push(state, slots)
}
Insn::PutStatic { descriptor, .. } => {
let slots = field_slots(descriptor).map_err(|e| context(class, method, e))?;
pop_slots(state, slots.len(), class, method, "putstatic")?;
Ok(())
}
Insn::GetField { descriptor, .. } => {
pop_slots(state, 1, class, method, "objectref")?;
let slots = field_slots(descriptor).map_err(|e| context(class, method, e))?;
push(state, slots)
}
Insn::PutField { descriptor, .. } => {
let slots = field_slots(descriptor).map_err(|e| context(class, method, e))?;
pop_slots(state, slots.len(), class, method, "putfield")?;
pop_slots(state, 1, class, method, "objectref")?;
Ok(())
}
Insn::Invoke {
kind,
owner,
name,
descriptor,
..
} => invoke(class, method, state, *kind, owner, name, descriptor),
Insn::InvokeDynamic { descriptor, .. } => {
let (params, ret) = method_slots(descriptor).map_err(|e| context(class, method, e))?;
pop_slots(state, count_slots(&params), class, method, "invokedynamic")?;
if let Some(ret) = ret {
push(state, ret)?;
}
Ok(())
}
Insn::New(_) => push(state, vec![VerificationType::Uninitialized(offset)]),
Insn::NewArray(kind) => {
pop_slots(state, 1, class, method, "newarray")?;
let descriptor = match kind {
ArrayKind::Boolean => "[Z",
ArrayKind::Char => "[C",
ArrayKind::Float => "[F",
ArrayKind::Double => "[D",
ArrayKind::Byte => "[B",
ArrayKind::Short => "[S",
ArrayKind::Int => "[I",
ArrayKind::Long => "[J",
ArrayKind::Reference => {
return Err(invalid(class, method, "newarray ссылочного типа"));
}
};
push(
state,
vec![VerificationType::Object(descriptor.to_string())],
)
}
Insn::ANewArray(name) => {
pop_slots(state, 1, class, method, "anewarray")?;
push(state, vec![VerificationType::Object(format!("[L{name};"))])
}
Insn::MultiANewArray { descriptor, dims } => {
pop_slots(state, *dims as usize, class, method, "multianewarray")?;
push(state, vec![VerificationType::Object(descriptor.clone())])
}
Insn::ArrayLength => {
pop_slots(state, 1, class, method, "arraylength")?;
push(state, vec![VerificationType::Integer])
}
Insn::AThrow => {
pop_slots(state, 1, class, method, "athrow")?;
state.stack.clear();
Ok(())
}
Insn::CheckCast(name) => {
pop_slots(state, 1, class, method, "checkcast")?;
push(state, vec![VerificationType::Object(name.clone())])
}
Insn::InstanceOf(_) => {
pop_slots(state, 1, class, method, "instanceof")?;
push(state, vec![VerificationType::Integer])
}
Insn::MonitorEnter | Insn::MonitorExit => {
pop_slots(state, 1, class, method, "monitor")?;
Ok(())
}
}
}

View file

@ -0,0 +1,161 @@
//! Стек и locals: снятие/укладка слотов, `dup*`.
use super::super::{VerificationType, invalid};
use super::State;
use crate::error::ClassfileError;
use crate::insn::SlotKind;
/// Запись в local с учётом ширины (`long`/`double` — парой с `Top`).
pub(super) fn store_local(
state: &mut State,
slot: u16,
kind: SlotKind,
taken: Vec<VerificationType>,
class: &str,
method: &str,
) -> Result<(), ClassfileError> {
let wide = kind == SlotKind::Long || kind == SlotKind::Double;
if taken.len() != usize::from(wide) + 1 {
return Err(invalid(class, method, "ширина store не совпала"));
}
let mut taken = taken;
local_set(state, slot, taken.remove(0), class, method)?;
if wide {
local_set(
state,
slot.checked_add(1)
.ok_or_else(|| invalid(class, method, "слот вне u16"))?,
VerificationType::Top,
class,
method,
)?;
}
Ok(())
}
pub(super) fn push(state: &mut State, slots: Vec<VerificationType>) -> Result<(), ClassfileError> {
state.stack.extend(slots);
Ok(())
}
/// Снять `n` слотов (снизу вверх); нехватка — честная ошибка.
pub(super) fn pop_slots(
state: &mut State,
n: usize,
class: &str,
method: &str,
what: &str,
) -> Result<Vec<VerificationType>, ClassfileError> {
if state.stack.len() < n {
return Err(invalid(
class,
method,
&format!("переполнение стека вниз в {what}"),
));
}
Ok(state.stack.split_off(state.stack.len() - n))
}
pub(super) fn local_get(
state: &State,
slot: u16,
class: &str,
method: &str,
) -> Result<VerificationType, ClassfileError> {
state
.locals
.get(slot as usize)
.cloned()
.ok_or_else(|| invalid(class, method, &format!("чтение слота {slot} вне locals")))
}
pub(super) fn local_set(
state: &mut State,
slot: u16,
value: VerificationType,
class: &str,
method: &str,
) -> Result<(), ClassfileError> {
let cell = state
.locals
.get_mut(slot as usize)
.ok_or_else(|| invalid(class, method, &format!("запись слота {slot} вне locals")))?;
*cell = value;
Ok(())
}
/// Значение local'а целиком: `long`/`double` — парой с `Top`.
pub(super) fn wide_push(value: VerificationType) -> Vec<VerificationType> {
match value {
VerificationType::Long | VerificationType::Double => vec![value, VerificationType::Top],
_ => vec![value],
}
}
/// `dup*` над значениями-категории-1: снять по позициям `take`, вернуть `put`.
pub(super) fn dup(
state: &mut State,
class: &str,
method: &str,
take: &[usize],
put: &[usize],
) -> Result<(), ClassfileError> {
let depth = take.iter().max().copied().unwrap_or(0);
let taken = pop_slots(state, depth, class, method, "dup")?;
if taken.contains(&VerificationType::Top) {
return Err(invalid(class, method, "dup над половиной"));
}
let mut out = Vec::with_capacity(put.len());
for position in put {
out.push(taken[depth - position].clone());
}
push(state, out)
}
/// `dup2*`: форма выбирается по вершине (`Top` — вторая половина `long`).
pub(super) fn dup2(
state: &mut State,
class: &str,
method: &str,
deep: bool,
deeper: bool,
) -> Result<(), ClassfileError> {
let wide = state.stack.last() == Some(&VerificationType::Top);
if wide {
let depth = if deeper {
4
} else if deep {
3
} else {
2
};
let taken = pop_slots(state, depth, class, method, "dup2")?;
if taken[depth - 1] != VerificationType::Top {
return Err(invalid(class, method, "dup2: нет половины"));
}
let value = taken[depth - 2].clone();
if !matches!(value, VerificationType::Long | VerificationType::Double) {
return Err(invalid(class, method, "dup2: половина без long/double"));
}
let mut out = vec![value, VerificationType::Top];
out.extend(taken);
push(state, out)
} else {
let depth = if deeper {
4
} else if deep {
3
} else {
2
};
let taken = pop_slots(state, depth, class, method, "dup2")?;
if taken.contains(&VerificationType::Top) {
return Err(invalid(class, method, "dup2 над половиной"));
}
let (head, tail) = taken.split_at(depth - 2);
let mut out = tail.to_vec();
out.extend_from_slice(head);
out.extend_from_slice(tail);
push(state, out)
}
}