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.
8.2 KiB
8.2 KiB
Архитектура
Слои
┌──────────────────────────┐
CLI (noxis) │ clap → конфиг → отчёт │
└────────────┬─────────────┘
│
┌─────────────────────┼─────────────────────┐
▼ ▼ ▼
┌─────────────┐ ┌──────────────┐ ┌──────────────┐
│ noxis-jar │ │noxis-passes │ │noxis-verify │
│ ZIP, ресурсы│ │ трансформации│ │ red-паттерны │
└──────┬──────┘ └──────┬───────┘ └──────┬───────┘
│ │ │
│ ┌──────┴───────┐ │
│ │ noxis-core │ │
│ │ конфиг, seed │ │
│ │ маппинги, Pass│ │
│ └──────┬───────┘ │
│ │ │
▼ ▼ ▼
┌────────────────────────────────────────────────────────┐
│ noxis-classfile (IR) │
│ Program → ClassFile → Code → Insn (метки, не смещения)│
│ model/ format/ insn error │
└────────────────────────────────────────────────────────┘
▲
┌──────┴──────┐ ┌───────────────┐ ┌──────────────────┐
│noxis- │ │noxis-runtime │ │noxis-structure- │
│analysis │ │ шифры, ABI │ │check (build-time)│
└─────────────┘ └───────────────┘ └──────────────────┘
Зависимости направлены только вниз. Циклов нет: сборка конвейера живёт в
noxis-passes, потому что noxis-core знает только контракт Pass.
Крейты
| крейт | ответственность |
|---|---|
noxis-classfile |
собственный IR классовых файлов, чтение/запись, StackMapTable |
noxis-core |
конфигурация, детерминированный seed, маппинги, контекст и контракт Pass |
noxis-analysis |
иерархия наследования, entrypoint'ы, рефлексия по имени, keep-правила |
noxis-passes |
сами трансформации + сборка стандартного конвейера |
noxis-runtime |
схемы шифрования строк и ABI класса-декодера |
noxis-jar |
ZIP: чтение, перепаковка, вырезание подписей, патч ресурсов |
noxis-verify |
red-паттерны, java -Xverify:all, метрики |
noxis |
CLI |
noxis-structure-check |
автогейт структурных правил (см. README.md) |
Собственный IR
Пассы не работают с API стороннего парсера - они работают с моделью:
Program все классы + ресурсы архива
├─ ClassFile версия, флаги, this/super, поля, методы, атрибуты
│ ├─ Field
│ └─ Method
│ └─ Code max_stack/max_locals, insns, exception_table, frames
└─ Resource путь + байты внутри архива
Ключевое решение - Insn::Mark(Label) вместо байтовых смещений:
- любой пасс, меняющий порядок или длину инструкций, не пересчитывает jump-офсеты;
- пересчёт делается ровно один раз - в writer'е;
- в IR нет состояния, в котором «смещение уехало, а метка осталась».
Атрибуты, которые мы не понимаем, хранятся как Attribute::Raw { name, data }
и возвращаются на место без изменений - round-trip без потерь.
Контракт пасса
pub trait Pass: Send + Sync {
fn id(&self) -> PassId;
fn apply(&self, ctx: &mut Context, program: &mut Program)
-> Result<PassReport, CoreError>;
}
Context- всё состояние прогона: конфиг, мастер-сид, маппинги, заметки. Из него берётся RNG:ctx.rng("rename/classes")- доменное разделение, чтобы случайности разных пассов не были статистически коррелированы.PassReport- что трансформировано, что сознательно пропущено и почему.Report- собирает отчёты всех пассов + счётчики, сериализуется в JSON.
Детерминированность - свойство, а не пожелание: одинаковый seed на одинаковой версии даёт байт-в-байт одинаковый выход. Это проверяется тестами.
Конвейер
Порядок пассов - не деталь:
Rename → Resources → Strings → Constants → Flow → Noise → DebugInfo
почему именно так:
- Rename первым - все остальные техники порождают новые ссылки
(
LDC-шум, вызовы декодера, junk-классы), и они уже должны быть в обфусцированных именах. - Resources сразу после Rename - патч
fabric.mod.json,*.mixins.json, refmap и access widener должен видеть итоговые имена классов. - Strings и Constants - работают по телам методов, ещё не искажённым control-flow'ем.
- Flow - искажает CFG. Если поставить его раньше, Strings/Constants перестанут находить свои шаблоны в инструкциях.
- Noise - junk-классы и аннотации добавляются в самом конце, когда целевой граф уже финален.
- DebugInfo последним - strip/spoof отладочной информации после всех
трансформаций, чтобы не породить свежие
LineNumberTable.
Статус
| этап | что |
|---|---|
| готово | IR, модель, конфиг, seed/PRNG, маппинги, анализ, keep, гейт правил, format::parse/write поверх ристретто, вывод кадров StackMapTable |
| готово | пассы rename/resources/strings/constants/noise/debug, генератор декодера, вывод мастер-секрета в рантайме |
| готово | noxis-verify (red-паттерны, java -Xverify:all, метрики), CLI (obfuscate/analyze/patterns) |
| готово | пасс flow (4 схемы + ротация + мешанина, приёмка настоящим java) |
| план | фикстуры и regression-тесты |
Детали выбора - stack.md, план - ../todo.md.