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:
parent
2172206e83
commit
139aea9a8f
96 changed files with 1053 additions and 831 deletions
|
|
@ -1,6 +1,6 @@
|
|||
//! Чтение и запись ZIP-архива JAR.
|
||||
//!
|
||||
//! Единственная задача модуля — переместить записи из/в вектор [`Entry`],
|
||||
//! Единственная задача модуля - переместить записи из/в вектор [`Entry`],
|
||||
//! сохранив порядок и не потеряв дубликатов молча. Всё, что связано с
|
||||
//! содержимым (`Program`, маппинги, подпись), живёт в соседних модулях.
|
||||
|
||||
|
|
@ -30,7 +30,7 @@ pub enum JarError {
|
|||
|
||||
/// Одна запись архива: имя + байты.
|
||||
///
|
||||
/// Порядок в `[`Entry`]` — это порядок в архиве, он сохраняется при
|
||||
/// Порядок в `[`Entry`]` - это порядок в архиве, он сохраняется при
|
||||
/// чтении и записи. Метод сжатия не хранится: всё пишется deflate'ом,
|
||||
/// что детерминированно и одинаково для одинаковых данных.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
|
|
@ -51,7 +51,7 @@ impl Entry {
|
|||
|
||||
/// Читает все не-каталожные записи из `reader`, сохраняя порядок.
|
||||
///
|
||||
/// Повторяющееся имя — ошибка, а не тихое предупреждение: битый архив
|
||||
/// Повторяющееся имя - ошибка, а не тихое предупреждение: битый архив
|
||||
/// с двумя `META-INF/MANIFEST.MF` собрать дороже, чем отказать.
|
||||
pub fn read_entries<R: Read + Seek>(reader: R) -> Result<Vec<Entry>, JarError> {
|
||||
let mut archive = zip::ZipArchive::new(reader)?;
|
||||
|
|
|
|||
|
|
@ -4,13 +4,13 @@
|
|||
//!
|
||||
//! Слой разделён на три несвязанные задачи:
|
||||
//!
|
||||
//! * [`archive`] — чистая работа с ZIP: порядок записей, сжатие, дубликаты;
|
||||
//! * [`signatures`] — срез невалидных подписей `META-INF/*.SF|RSA|DSA|EC`;
|
||||
//! * [`loader`] — перевод архива в [`Program`](noxis_classfile::Program)
|
||||
//! * [`archive`] - чистая работа с ZIP: порядок записей, сжатие, дубликаты;
|
||||
//! * [`signatures`] - срез невалидных подписей `META-INF/*.SF|RSA|DSA|EC`;
|
||||
//! * [`loader`] - перевод архива в [`Program`](noxis_classfile::Program)
|
||||
//! и обратно (классы через [`format`](noxis_classfile::format), остальное
|
||||
//! как ресурсы).
|
||||
//!
|
||||
//! Обфускация ресурсов — пасс `resources` в `noxis-passes`; сюда она не
|
||||
//! Обфускация ресурсов - пасс `resources` в `noxis-passes`; сюда она не
|
||||
//! входит, потому что ей нужен маппинг имён, а не I/O.
|
||||
|
||||
pub mod archive;
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
//! Перевод архива в [`Program`] и обратно.
|
||||
//! Archive to [`Program`] and back.
|
||||
//!
|
||||
//! Правило простое: `*.class` уходит в парсер, всё остальное — в ресурсы.
|
||||
//! Обратно классы сериализуются первыми, затем ресурсы: порядок внутри
|
||||
//! архива на поведение не влияет, а фиксированный порядок даёт
|
||||
//! воспроизводимую запись.
|
||||
//! The rule is simple: `*.class` goes to the parser, everything else -
|
||||
//! to resources. Classes serialize first, then resources: the order inside
|
||||
//! the archive does not affect behaviour, and a fixed order gives
|
||||
//! reproducible writes.
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use noxis_classfile::Program;
|
||||
use noxis_classfile::format;
|
||||
|
|
@ -11,13 +13,22 @@ use noxis_classfile::model::Resource;
|
|||
|
||||
use crate::archive::{Entry, JarError};
|
||||
|
||||
/// Разбирает записи архива: классы — через парсер, остальное — как ресурсы.
|
||||
/// Parses archive entries: classes - through the parser, the rest - as resources.
|
||||
///
|
||||
/// A name that repeats (multi-release jars carry the same class at the root
|
||||
/// and under `META-INF/versions/*`) is kept once, first occurrence wins:
|
||||
/// the flat model cannot hold two classes under one name, and later passes
|
||||
/// (rename maps, jar writing) assume uniqueness.
|
||||
pub fn load(entries: Vec<Entry>) -> Result<Program, JarError> {
|
||||
let mut program = Program::default();
|
||||
let mut seen: BTreeSet<String> = BTreeSet::new();
|
||||
|
||||
for entry in entries {
|
||||
if is_class_entry(&entry.name) {
|
||||
program.classes.push(format::parse(&entry.data)?);
|
||||
let class = format::parse(&entry.data)?;
|
||||
if seen.insert(class.this_class.clone()) {
|
||||
program.classes.push(class);
|
||||
}
|
||||
} else {
|
||||
program.resources.push(Resource {
|
||||
path: entry.name,
|
||||
|
|
@ -29,7 +40,7 @@ pub fn load(entries: Vec<Entry>) -> Result<Program, JarError> {
|
|||
Ok(program)
|
||||
}
|
||||
|
||||
/// Собирает записи архива из [`Program`]: сначала классы, потом ресурсы.
|
||||
/// Assembles archive entries from [`Program`]: classes first, then resources.
|
||||
pub fn save(program: &Program) -> Result<Vec<Entry>, JarError> {
|
||||
let mut entries = Vec::with_capacity(program.classes.len() + program.resources.len());
|
||||
|
||||
|
|
@ -44,7 +55,7 @@ pub fn save(program: &Program) -> Result<Vec<Entry>, JarError> {
|
|||
Ok(entries)
|
||||
}
|
||||
|
||||
/// Классовый файл внутри архива: путь оканчивается на `.class`.
|
||||
/// A class file inside the archive: the path ends in `.class`.
|
||||
fn is_class_entry(name: &str) -> bool {
|
||||
name.len() > ".class".len() && name.ends_with(".class")
|
||||
}
|
||||
|
|
@ -78,7 +89,7 @@ mod tests {
|
|||
assert_eq!(save(&program).expect("save"), source);
|
||||
}
|
||||
|
||||
/// До P1 `format::parse` — заглушка; после P1 ожидается успешный разбор.
|
||||
/// Class entries go through the parser (round-trip keeps them intact).
|
||||
#[test]
|
||||
fn class_entries_go_through_the_parser() {
|
||||
let entries = vec![
|
||||
|
|
@ -97,4 +108,22 @@ mod tests {
|
|||
Err(other) => panic!("unexpected error: {other}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Multi-release twins (`demo/App.class` + `META-INF/versions/9/...`):
|
||||
/// the flat model keeps the first occurrence so no two classes ever
|
||||
/// share one name downstream.
|
||||
#[test]
|
||||
fn duplicate_class_names_keep_first() {
|
||||
use noxis_classfile::ClassBuilder;
|
||||
|
||||
let class = ClassBuilder::new("demo/App", None).build();
|
||||
let bytes = format::write(&class).expect("write");
|
||||
let entries = vec![
|
||||
Entry::new("demo/App.class", bytes.clone()),
|
||||
Entry::new("META-INF/versions/9/demo/App.class", bytes),
|
||||
];
|
||||
let program = load(entries).expect("load");
|
||||
assert_eq!(program.classes.len(), 1);
|
||||
assert_eq!(program.classes[0].this_class, "demo/App");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
//!
|
||||
//! После перепаковки архива `META-INF/*.SF` и `*.RSA|DSA|EC` описывают
|
||||
//! **старые** байты: JVM либо отказывается запускать jar, либо ругается на
|
||||
//! подпись. Вырезать их обязательно — и это делается на уровне записей,
|
||||
//! подпись. Вырезать их обязательно - и это делается на уровне записей,
|
||||
//! потому что `MANIFEST.MF` остаётся, а его секции `Name:` тоже нужно
|
||||
//! выкинуть (ими занимается пасс `resources`).
|
||||
|
||||
|
|
@ -19,10 +19,10 @@ const META_INF: &str = "META-INF/";
|
|||
|
||||
/// Проверяет, является ли запись архива файлом подписи.
|
||||
///
|
||||
/// Совпадение — только для файлов, лежащих **непосредственно** в
|
||||
/// `META-INF/`: вложенный `META-INF/certs/x.SF` — обычный ресурс.
|
||||
/// Совпадение - только для файлов, лежащих **непосредственно** в
|
||||
/// `META-INF/`: вложенный `META-INF/certs/x.SF` - обычный ресурс.
|
||||
///
|
||||
/// Регистр не важен (`meta-inf/x.sf` — тоже подпись), `MANIFEST.MF`
|
||||
/// Регистр не важен (`meta-inf/x.sf` - тоже подпись), `MANIFEST.MF`
|
||||
/// никогда не является подписью.
|
||||
pub fn is_signature_entry(name: &str) -> bool {
|
||||
let Some(relative) = name.strip_prefix(META_INF) else {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue