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,152 @@
//! Spike, часть 2: доступна ли **генерация** кадров `StackMapTable`.
//!
//! Ключевой вопрос `docs/stack.md` §3: считает ли `ristretto_classfile` кадры
//! сам и отдаёт ли их наружу. Если да — наша работа сводится к записи; если
//! нет — кадры придётся считать (или вендорить их `verifiers::bytecode`).
#![allow(clippy::expect_used, clippy::unwrap_used)]
use ristretto_classfile::attributes::{Attribute, Instruction, StackFrame};
use ristretto_classfile::verifiers::bytecode::{
FallbackStrategy, VerificationPath, VerifierConfig, verify_class,
};
use ristretto_classfile::verifiers::context::VerificationContext;
use ristretto_classfile::verifiers::error::Result as VerifyResult;
use ristretto_classfile::{
ClassAccessFlags, ClassFile, Constant, ConstantPool, MethodAccessFlags, Version,
};
/// Разрешающий контекст: в spike'е нас интересует только сам вывод типов.
#[derive(Debug)]
struct Permissive;
impl VerificationContext for Permissive {
fn is_subclass(&self, _subclass: &str, _superclass: &str) -> VerifyResult<bool> {
Ok(true)
}
fn is_assignable(&self, _target: &str, _source: &str) -> VerifyResult<bool> {
Ok(true)
}
fn common_superclass(&self, _class1: &str, _class2: &str) -> VerifyResult<String> {
Ok("java/lang/Object".to_string())
}
}
/// `void run()` с одной ветвью: на сдвиге 3 (`Ifeq`-target) кадр обязателен.
fn branch_class() -> ClassFile<'static> {
let mut constant_pool = ConstantPool::default();
let utf8_name = constant_pool
.add(Constant::utf8("demo/Branch"))
.expect("utf8");
let this_class = constant_pool
.add(Constant::Class(utf8_name))
.expect("class");
let run = constant_pool.add(Constant::utf8("run")).expect("utf8");
let descriptor = constant_pool.add(Constant::utf8("()V")).expect("utf8");
let code = constant_pool.add(Constant::utf8("Code")).expect("utf8");
ClassFile {
version: Version::Java8 { minor: 0 },
constant_pool,
access_flags: ClassAccessFlags::PUBLIC,
this_class,
super_class: 0,
interfaces: Vec::new(),
fields: Vec::new(),
methods: vec![ristretto_classfile::Method {
access_flags: MethodAccessFlags::PUBLIC | MethodAccessFlags::STATIC,
name_index: run,
descriptor_index: descriptor,
attributes: vec![Attribute::Code {
name_index: code,
max_stack: 1,
max_locals: 0,
code: vec![
Instruction::Iconst_1,
Instruction::Ifeq(3),
Instruction::Return,
Instruction::Return,
],
exception_table: Vec::new(),
attributes: Vec::new(),
}],
}],
attributes: Vec::new(),
code_source_url: None,
}
}
fn inference_config() -> VerifierConfig {
VerifierConfig::default().with_fallback_strategy(FallbackStrategy::AlwaysInference)
}
/// Без `StackMapTable` inference обязан пройти сам — значит, движок кадров у
/// крейта есть, просто его результат не отдаётся наружу.
#[test]
fn inference_path_verifies_without_stack_map_table() {
let class_file = branch_class();
let results = verify_class(&class_file, &Permissive, &inference_config()).expect("verify");
assert_eq!(results.len(), 1);
assert!(results[0].success, "ошибка: {:?}", results[0].error);
assert_eq!(results[0].path_used, VerificationPath::Inference);
}
/// Строгий режим без кадров обязан упасть — иначе требование `StackMapTable`
/// для классов ≥ 50 не проверяется вовсе.
#[test]
fn strict_mode_rejects_missing_stack_map_table() {
let class_file = branch_class();
let config = VerifierConfig::default().with_fallback_strategy(FallbackStrategy::Strict);
let outcome = verify_class(&class_file, &Permissive, &config);
// Строгий режим падает либо `Err`, либо `success == false` — оба варианта
// приемлемы, важен сам отказ.
let rejected = match &outcome {
Err(_) => true,
Ok(results) => results.iter().any(|r| !r.success),
};
assert!(rejected, "строгий режим обязан отвергнуть класс без кадров");
}
/// Негативный результат spike'а: кадр не на том сдвиге не отдаёт диагностику.
///
/// Ждали `Ok(vec![result])` с заполненным `diagnostic.expected` — тогда
/// генерация кадров шла бы итеративным путём через публичный API. Фактически
/// `verify_class` в строгом режиме возвращает `Err(VerifyError)`, и ожидаемый
/// кадр до нас не доходит: из крейта достаётся только место ошибки
/// («merge point at offset N»), но не её содержимое.
///
/// Вывод для `docs/stack.md` §3: проверять кадры крейт умеет, отдавать — нет.
#[test]
fn diagnostic_exposes_expected_frame_on_failure() {
let mut class_file = branch_class();
let name_index = class_file
.constant_pool
.add_utf8("StackMapTable")
.expect("utf8");
// Кадр на сдвиге 0, а настоящий якорь — на сдвиге 3.
class_file.attributes.push(Attribute::StackMapTable {
name_index,
frames: vec![StackFrame::SameFrame { frame_type: 0 }],
});
// Кладём атрибут в метод, а не в класс: `StackMapTable` живёт в `Code`.
let stack_map = class_file.attributes.pop().expect("frame");
match &mut class_file.methods[0].attributes[0] {
Attribute::Code { attributes, .. } => attributes.push(stack_map),
other => panic!("ожидался Code, получено {other:?}"),
}
let strict = VerifierConfig::default().with_fallback_strategy(FallbackStrategy::Strict);
let outcome = verify_class(&class_file, &Permissive, &strict);
// Наблюдаемое поведение: `Err`, а не `Ok` с `diagnostic`.
let err = outcome.expect_err("битые кадры обязаны быть отвергнуты");
let message = err.to_string();
assert!(
message.contains("StackMapTable"),
"ожидалась ошибка про кадры, получено: {message}"
);
}