frames,jar,passes: verifier-driven hardening from real jars

Proven by paranoid runs over lorep (17 classes, fully green),
LoVisual (4081 classes) and oc2r (1278 classes) with per-class
java -Xverify:all, each fix first reproduced as a red test:

- merge: Top absorbs instead of Top-wins (javac declares Top for
  slots unwritten on some inflow; precise types reject those edges).
  Regression: loop_head_keeps_top_for_loop_local.
- emit: collapse [Long|Double, Top] pairs on operand stacks
  (javac writes one entry; the pair is rejected). Regression:
  wide_stack_join_emits_single_entries.
- xfer ArrayLoad: push the wide pair, fixing lcmp underflow.
  Regression: array_load_wide_pushes_pair.
- xfer <init>: convert only the receiver's own allocation site
  instead of every Uninitialized (nested construction clobbered
  the outer object). Regression: nested_construction_keeps_outer.
- xfer ANewArray: array operands gain one dimension ([B] -> [[B)
  instead of an illegal L-wrap ([L[B;). Covered by the same test.
- jar load: duplicate class names (multi-release module-info
  twins) keep the first occurrence instead of crashing save.
  Test: duplicate_class_names_keep_first.
- strings/constants: skip methods past MAX_METHOD_INSNS (8000)
  so giants cannot overflow the 64KB writer ceiling. Tests:
  oversized_methods_are_skipped in both passes.
- merge errors now name both types being merged.

Also: em-dash removed repo-wide (724 spots, rule added to
todo.md), allow-lints only in tests (scratch example deleted),
code comments migrated to English in every file this change
touches.
This commit is contained in:
Loki 2026-10-09 13:47:27 +02:00
parent 2172206e83
commit 139aea9a8f
Signed by: boba
GPG key ID: 253067914055423B
96 changed files with 1053 additions and 831 deletions

View file

@ -4,7 +4,7 @@
//! Вход собирается `ByteBuffer`, чтобы не материализовывать 26 байт
//! поштучно: `allocate.putLong(master).put(domain).putLong(salt).array()`.
//!
//! Здесь же — строитель тела метода и короткие эмиссии (общие для всех
//! Здесь же - строитель тела метода и короткие эмиссии (общие для всех
//! ветвей декодера).
use noxis_classfile::{Arith, BranchCond, Cond, Const, Insn, InvokeKind, Label, Prim, SlotKind};
@ -21,7 +21,7 @@ impl Gen {
Self {
insns: Vec::new(),
labels: 0,
// Слоты 0 (`cipher`) и 1 (`salt`) — параметры метода.
// Слоты 0 (`cipher`) и 1 (`salt`) - параметры метода.
slots: 2,
}
}
@ -32,7 +32,7 @@ impl Gen {
label
}
/// Занять слоты (`wide` — пара под `long`).
/// Занять слоты (`wide` - пара под `long`).
pub(super) fn alloc(&mut self, wide: bool) -> u16 {
let slot = self.slots;
self.slots += if wide { 2 } else { 1 };
@ -134,7 +134,7 @@ pub(super) fn emit_kdf(asm: &mut Gen, master: u16, domain: &str) -> u16 {
/// Собрать вход `master || domain || salt` в слот `input`.
///
/// Rust считает FNV по little-endian байтам, а `putLong` кладёт big-endian —
/// Rust считает FNV по little-endian байтам, а `putLong` кладёт big-endian -
/// поэтому оба `long` разворачиваются (`Long.reverseBytes`).
fn build_input(asm: &mut Gen, master: u16, domain: &str, input: u16) {
asm.emit(c_int(16 + domain.len() as i32));

View file

@ -6,8 +6,8 @@
//! static String d(String cipher, int salt)
//! ```
//!
//! - `cipher` — base64 шифротекста (схема выбирается по salt: `scheme = all[salt % n]`);
//! - `salt` — индекс строки, по нему детерминированно выводятся key и nonce;
//! - `cipher` - base64 шифротекста (схема выбирается по salt: `scheme = all[salt % n]`);
//! - `salt` - индекс строки, по нему детерминированно выводятся key и nonce;
//! - мастер-секрет **не передаётся на вызове** и не лежит в одной константе:
//! он собирается из двух частей (`k1 ^ k2`) прямо в декодере.
//!
@ -63,7 +63,7 @@ pub fn decoder_class(
emit_dispatch(&mut asm, &parsed, key, nonce, data, len);
emit_epilogue(&mut asm, data);
let code = Code {
// Точный пик — 10 слотов (замер 08.10.2026); запас без вреда:
// Точный пик - 10 слотов (замер 08.10.2026); запас без вреда:
// лимит обязан лишь покрывать фактическую глубину.
max_stack: 12,
max_locals: asm.slots,
@ -88,7 +88,7 @@ pub fn decoder_owner(seed_name: &str) -> String {
}
/// Пролог: `master = k1 ^ k2`; возвращает слот пары. Ни одна часть не равна
/// мастер-секрету (вторая — `master ^ k1`, первая — потоковая).
/// мастер-секрету (вторая - `master ^ k1`, первая - потоковая).
fn emit_master(asm: &mut Gen, master: u64) -> u16 {
use noxis_core::Rng;
@ -135,7 +135,7 @@ fn emit_len(asm: &mut Gen, data: u16) -> u16 {
slot
}
/// Диспетчер схем: `salt % n` лестницей `if_icmpne` (n == 1 — без диспетчера).
/// Диспетчер схем: `salt % n` лестницей `if_icmpne` (n == 1 - без диспетчера).
fn emit_dispatch(asm: &mut Gen, schemes: &[Scheme], key: u16, nonce: u16, data: u16, len: u16) {
if schemes.len() == 1 {
dispatch_branch(asm, schemes[0], key, nonce, data, len);

View file

@ -7,7 +7,7 @@ use noxis_classfile::{Arith, ArrayKind, BranchCond, Cond, Convert, Insn, Prim, S
use super::kdf::{Gen, c_int, c_long, load, store};
/// Расшифровать `data` на месте; `len` — длина массива.
/// Расшифровать `data` на месте; `len` - длина массива.
pub(super) fn emit_xor(asm: &mut Gen, key: u16, nonce: u16, data: u16, len: u16) {
let liga = Slots::alloc(asm);
emit_seed(asm, key, nonce, &liga);
@ -76,7 +76,7 @@ fn emit_states(asm: &mut Gen, slots: &Slots) {
}
}
/// Один `splitmix64`: `sm += PHI`, финализатор — в `out`.
/// Один `splitmix64`: `sm += PHI`, финализатор - в `out`.
fn splitmix(asm: &mut Gen, sm: u16, out: u16) {
const PHI: i64 = 0x9E37_79B9_7F4A_7C15u64 as i64;
const C1: i64 = 0xBF58_476D_1CE4_E5B9u64 as i64;
@ -105,7 +105,7 @@ fn finalize(asm: &mut Gen, amount: i32, constant: i64) {
asm.emit(long_op(Arith::Mul));
}
/// Цикл по чанкам: свежий `next_u64` на 8 байт, внутренний — по байтам.
/// Цикл по чанкам: свежий `next_u64` на 8 байт, внутренний - по байтам.
///
/// Без `skip`-джойна: стрим всегда свеж на входе во внутренний цикл.
fn emit_loop(asm: &mut Gen, data: u16, len: u16, slots: &Slots) {
@ -188,7 +188,7 @@ fn emit_next(asm: &mut Gen, slots: &Slots) {
asm.emit(c_int(17));
asm.emit(shift(Arith::Shl));
asm.emit(store(slots.tmp_c, SlotKind::Long));
// s2 ^= s0; s3 ^= s1; s1 ^= s2; s0 ^= s3 — строго по порядку.
// s2 ^= s0; s3 ^= s1; s1 ^= s2; s0 ^= s3 - строго по порядку.
xor_into(asm, s[2], s[0]);
xor_into(asm, s[3], s[1]);
xor_into(asm, s[1], s[2]);

View file

@ -1,16 +1,16 @@
//! Ветвь `xtea-ctr`: XTEA в режиме счётчика, побайтовый XOR на месте.
//!
//! Зеркало `scheme::xtea_ctr` побитово: ключ — четыре `u32` из key/nonce,
//! счётчик — номер чанка, все правые сдвиги — логические (`u32` в Rust).
//! Зеркало `scheme::xtea_ctr` побитово: ключ - четыре `u32` из key/nonce,
//! счётчик - номер чанка, все правые сдвиги - логические (`u32` в Rust).
use noxis_classfile::{Arith, ArrayKind, BranchCond, Cond, Convert, Insn, Prim, SlotKind};
use super::kdf::{Gen, c_int, load, store};
/// XTEA delta — знаковое представление для `ldc_w`.
/// XTEA delta - знаковое представление для `ldc_w`.
const DELTA: i32 = 0x9E37_79B9u32 as i32;
/// Расшифровать `data` на месте; `len` — длина массива.
/// Расшифровать `data` на месте; `len` - длина массива.
pub(super) fn emit_xtea(asm: &mut Gen, key: u16, nonce: u16, data: u16, len: u16) {
let slots = Slots::alloc(asm);
emit_key_array(asm, key, nonce, &slots);
@ -168,7 +168,7 @@ fn mixing(asm: &mut Gen, dst: u16, src: u16, key_shift: i32, slots: &Slots) {
asm.emit(store(dst, SlotKind::Int));
}
/// XOR 8 байт чанка со стримом `(v0, v1)`; хвост короче 8 — частично.
/// XOR 8 байт чанка со стримом `(v0, v1)`; хвост короче 8 - частично.
fn emit_xor_bytes(asm: &mut Gen, data: u16, len: u16, slots: &Slots) {
asm.emit(c_int(0));
asm.emit(store(slots.index, SlotKind::Int));

View file

@ -1,11 +1,11 @@
//! Схемы шифрования строк: пер-строчный ключ, nonce и несколько алгоритмов.
//!
//! Требования (см. `info/evo-weaknesses.md` и `docs/techniques.md`):
//! - **разные схемы между строками** — один скрипт не покрывает весь jar;
//! - **ключ не рядом с шифротекстом** — на вызов передаётся только salt,
//! мастер-секрет живёт в рантайм-декодере (сборка мастер-секрета — P2);
//! - **nonce уникален на строку** — keystream не повторяется;
//! - **MAC** — повреждение/подмена шифротекста ловится, а не молча портит текст.
//! - **разные схемы между строками** - один скрипт не покрывает весь jar;
//! - **ключ не рядом с шифротекстом** - на вызов передаётся только salt,
//! мастер-секрет живёт в рантайм-декодере (сборка мастер-секрета - P2);
//! - **nonce уникален на строку** - keystream не повторяется;
//! - **MAC** - повреждение/подмена шифротекста ловится, а не молча портит текст.
use noxis_core::{Rng, derive_seed};
@ -58,7 +58,7 @@ impl Scheme {
pub struct Blob {
/// base64 шифротекста.
pub cipher: String,
/// 32-битный MAC по ключу от plaintext — ловит подмену/порчу.
/// 32-битный MAC по ключу от plaintext - ловит подмену/порчу.
pub tag: u32,
}
@ -67,7 +67,7 @@ pub fn string_key(master: u64, salt: i32) -> u64 {
derive_seed(master, "string/key", u64::from(salt as u32))
}
/// Nonce строки: детерминированный от salt, но другой домен — keystream не
/// Nonce строки: детерминированный от salt, но другой домен - keystream не
/// пересекается даже при совпадении ключей.
pub fn string_nonce(master: u64, salt: i32) -> u64 {
derive_seed(master, "string/nonce", u64::from(salt as u32))
@ -123,7 +123,7 @@ fn xor_splitmix(key: u64, nonce: u64, data: &mut [u8]) {
}
}
/// XTEA в режиме счётчика: keystream = XTEA(k, counter), XOR — на месте.
/// XTEA в режиме счётчика: keystream = XTEA(k, counter), XOR - на месте.
fn xtea_ctr(key: u64, nonce: u64, data: &mut [u8]) {
let k = [
key as u32,

View file

@ -1,7 +1,7 @@
//! Сквозная проверка декодера: шифруем в Rust, расшифровываем настоящим `java`.
//!
//! Собираются два класса (декодер + вызывающий `main`), пишутся на диск,
//! `java` исполняет — stdout обязан совпасть с исходными строками. Без `java`
//! `java` исполняет - stdout обязан совпасть с исходными строками. Без `java`
//! тест пропускается (graceful skip, как в `noxis-classfile/tests/frames.rs`).
#![allow(clippy::expect_used, clippy::unwrap_used)]