feat(native): read embedded cargo-auditable Rust dependency lists (#2456)

* feat(native): read embedded cargo-auditable Rust dependency lists

* fix(native): trim PE section padding and compute dependency cycles iteratively

* fix(native): ignore SHT_NOBITS sections and validate dependency edges linearly

* fix(native): bound compiler path scanning and retained hashes

* fix(rust): pass package arrays as named parameterized test cases

* fix(rust): admit graph expansion and preserve resource units

* fix(rust): reject malformed packages, false cycles, and hidden compiler paths

Fail the first malformed cargo-auditable package before Zod collects an
issue per field. Require cyclic dependency indices to match the cycles in
package dependencies. Keep read-only ELF ranges when a section name does
not fit the bounded name window, so a rustc path in that range is still
reported.

* fix(ci): repair integrated checks and Hopper test baseline

* fix(go): verify Mach-O sections against segments

---------

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
This commit is contained in:
authored and GitHub committed 2026-10-14 02:50:30 +08:00
1 parent c7ceaadb70
commit 0e6eca4085
59 files changed
+2767 -12

No files matched your search

@@ -122,6 +122,14 @@ explicit path or endpoint and do not need it.
absent metadata does not prove a binary is not Go, and unparsed text records
remain explicit. See the
[Go binary guide](https://github.com/morluto/rea/blob/main/docs/go-binaries.md).
- Embedded cargo-auditable Rust dependency metadata: `inspect_rust_binary`
with the explicit local ELF, PE, or thin Mach-O file. This built-in
target-free inspection requires no Rust installation or disassembler. Preserve
package order, dependency indices, and the section file range. A missing
`.dep-v0` section is null metadata, not proof the binary has no dependencies.
A rustc commit hash is an inference from a read-only `/rustc/<40-hex>/` path.
See the
[Rust binary guide](https://github.com/morluto/rea/blob/main/docs/rust-binaries.md).
- Explicit local SQLite database snapshot: `inspect_sqlite_database`. Inspect
with Node.js 24.x >=24.15 or 26+ providing native SQLite limits. Read schema
@@ -11,6 +11,18 @@ unparsed module-text line. A missing record is an unknown, not proof of a
non-Go binary; observed metadata does not authenticate its claims. Function,
source and type recovery remain a separate native-analysis question.
## Rust dependency metadata
Use `inspect_rust_binary` for an explicit local ELF, PE, or thin Mach-O when
the question concerns the cargo-auditable crate list embedded in `.dep-v0`
(Mach-O: `__DATA,.dep-v0`). No `open_binary`, Rust installation, or target
execution is required. The CLI equivalent is `inspect-rust-binary PATH`.
Preserve artifact SHA-256, package order, dependency indices, and the section
file range. A missing section is null metadata, not proof the binary has no
dependencies. An omitted package kind stays null. A rustc commit hash is
inferred from a read-only `/rustc/<40-hex>/` path and does not by itself
identify the compiler. See [Rust binaries](https://github.com/morluto/rea/blob/main/docs/rust-binaries.md).
## SQLite snapshots
Use `inspect_sqlite_database` with an explicit local database snapshot for its
+1
View File
@@ -155,6 +155,7 @@ Additional tools and host support depend on the target:
| Native binaries | Pseudocode, assembly, strings, symbols, calls and references | Hopper, Ghidra or IDA; [native analysis](https://rea.tools/guides/native/) |
| Offline ELF layout | Sections, segments, original symbols/relocations and static mitigation candidates | Caller-supplied pwntools on Linux x64; [binary diagnostics](docs/binary-diagnostics.md) |
| Go binaries | Embedded compiler version, modules, replacements and build settings | Built-in static ELF/PE/thin Mach-O inspection; no Go installation required |
| Rust binaries | Embedded cargo-auditable crate names, versions, sources and dependency edges | Built-in static ELF/PE/thin Mach-O inspection; [Rust binaries](docs/rust-binaries.md) |
| WASM artifact | Validation, sections, exact import/export WAT forms and decoded WAT | Caller-supplied WABT 1.0.42; `inspect_wasm_artifact` |
| EVM bytecode | Dispatch selectors, byte offsets, inferred arguments and mutability | Local raw/hex carrier on Linux x64; [offline EVM guide](docs/evm-bytecode.md) |
| SQLite databases | Tables, columns, indexes, views, triggers and selected ordinary-table rows | Local database/WAL snapshot; Node.js 24.x >=24.15 or 26+ with native SQLite limits |
+1
View File
@@ -29,6 +29,7 @@ export default defineConfig({
items: [
{ text: "Native binaries", link: "/native-investigation" },
{ text: "Go build metadata", link: "/go-binaries" },
{ text: "Rust dependencies", link: "/rust-binaries" },
{
text: "JavaScript and Electron",
link: "/javascript-artifact-reconstruction",
+12
View File
@@ -83,6 +83,18 @@ The shared CLI/MCP workflow reports compiler version, modules, replacements,
build settings, original bytes and SHA-256. See [Go binaries](go-binaries.md)
for supported containers and absent or malformed metadata.
Inspect an embedded cargo-auditable Rust dependency list without installing
Rust or configuring a disassembler:
```bash
rea inspect-rust-binary /absolute/path/to/program --json
```
The same workflow reports package names, versions, sources, the root package,
dependency edges, and the section's file range. A missing `.dep-v0` section is
null metadata, not proof that the binary has no dependencies. See
[Rust binaries](rust-binaries.md).
Configure [Hopper or Ghidra](installation.md#hopper), or the
[IDA adapter](ida-provider.md), before analyzing a native target. Substitute
your target, search text and function name or address in these examples:
+1 -1
View File
@@ -49,7 +49,7 @@ For runtime instrumentation, see the [Frida guide](./frida-instrumentation.md).
| Task | Guides |
| --------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Inspect native functions, references, or types | [Native binaries](native-investigation.md), [Go metadata](go-binaries.md), [offline ELF layout](binary-diagnostics.md), [recorded crashes](recorded-crashes.md) |
| Inspect native functions, references, or types | [Native binaries](native-investigation.md), [Go metadata](go-binaries.md), [Rust dependencies](rust-binaries.md), [offline ELF layout](binary-diagnostics.md), [recorded crashes](recorded-crashes.md) |
| Select a native provider or import format | [Ghidra semantics](ghidra-provider.md), [Windows](windows-ghidra-p0.md), [DOS](ghidra-dos.md), [raw bytes](raw-binary.md), [MIPS](ghidra-mips.md), [PSP](ghidra-psp.md), [NativeAOT](ghidra-nativeaot.md), [IDA](ida-provider.md), [JEB](jeb-analysis.md) |
| Analyze JavaScript or Electron artifacts | [Static analysis](javascript-artifact-reconstruction.md), [tracing and comparisons](javascript-application-workflows.md), [application graph](javascript-application-graph.md), [source recovery](javascript-recovery.md) |
| Compare static JavaScript with runtime observations | [Runtime attribution](javascript-runtime-observation.md), [reconciliation](javascript-runtime-reconciliation.md), [Electron observation](electron-observation.md) |
+1
View File
@@ -135,6 +135,7 @@ npx rea-agents@latest setup
| الملفات الثنائية للشيفرة الأصلية | شيفرة شبه برمجية، وتعليمات تجميع، وسلاسل نصية، ورموز، واستدعاءات، ومراجع | Hopper أو Ghidra أو IDA؛ [تحليل الشيفرة الأصلية](https://rea.tools/ar/guides/native/) |
| بنية ELF دون تشغيل | الأقسام والمقاطع والرموز ومعلومات إعادة التموضع الأصلية، وآليات الحماية المحتملة المستدلّ عليها بالفحص الثابت | pwntools يوفّره المستدعي على Linux x64؛ [تشخيص الملفات الثنائية](../binary-diagnostics.md) |
| شيفرة EVM البايتية | محددات التوجيه، وإزاحات البايتات، والمعاملات المستنتجة، وقابلية تغيير الحالة | مدخل محلي يحمل بايتات خامًا أو تمثيلًا سداسيًا عشريًا على Linux x64؛ [دليل EVM دون تشغيل](../evm-bytecode.md) |
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
| أعطال Linux المسجّلة | سجلات note الخام، ومسجّلات المعالج/الإشارات لكل خيط مسجّل، وترشيحات اختيارية لخرائط الذاكرة | pwntools يوفّره المستدعي على Linux x64؛ GDB/pwndbg اختياريان؛ [الأعطال المسجّلة](../recorded-crashes.md) |
| JavaScript / Electron | الوحدات، وعمليات الاستيراد، وخرائط المصدر، والمسارات، وIPC، وعلاقات إضافات الشيفرة الأصلية | Node.js وnpm؛ [تحليل التطبيقات](https://rea.tools/ar/guides/javascript/) |
| المواقع | بنية الصفحة، والسكربتات، وملاحظات الشبكة، ولقطات الشاشة المطلوبة | متصفح من عائلة Chrome؛ [تحليل المتصفح](https://rea.tools/ar/guides/browser/) |
+1
View File
@@ -135,6 +135,7 @@ REA benötigt Node.js 22.x (>=22.19), 24.x (>=24.11) oder 26+ sowie npm. Zusätz
| Native Binärdateien | Pseudocode, Assemblercode, Zeichenketten, Symbole, Aufrufe und Verweise | Hopper, Ghidra oder IDA; [native Analyse](https://rea.tools/de/guides/native/) |
| Offline-ELF-Struktur | Sektionen, Segmente, ursprüngliche Symbole/Relokationen und mögliche Schutzmaßnahmen aus statischer Analyse | Vom Aufrufer bereitgestelltes pwntools unter Linux x64; [Binärdiagnostik](../binary-diagnostics.md) |
| EVM-Bytecode | Dispatch-Selektoren, Byte-Offsets, abgeleitete Argumente und Veränderbarkeit des Zustands | Lokale Eingabe mit Rohbytes oder Hexdarstellung unter Linux x64; [Offline-EVM-Leitfaden](../evm-bytecode.md) |
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
| Aufgezeichnete Linux-Abstürze | Unverarbeitete Note-Einträge, Register/Signale aller aufgezeichneten Threads und optionale Zuordnungskandidaten | Vom Aufrufer bereitgestelltes pwntools unter Linux x64; optional GDB/pwndbg; [aufgezeichnete Abstürze](../recorded-crashes.md) |
| JavaScript / Electron | Module, Imports, Source Maps, Routen, IPC und Beziehungen zu nativen Add-ons | Node.js und npm; [Anwendungsanalyse](https://rea.tools/de/guides/javascript/) |
| Websites | Seitenstruktur, Skripte, Netzwerkbeobachtungen und angeforderte Screenshots | Ein Browser aus der Chrome-Familie; [Browseranalyse](https://rea.tools/de/guides/browser/) |
+1
View File
@@ -135,6 +135,7 @@ REA requiere Node.js 22.x (>=22.19), 24.x (>=24.11) o 26+, además de npm. Las h
| Binarios nativos | Pseudocódigo, ensamblador, cadenas, símbolos, llamadas y referencias | Hopper, Ghidra o IDA; [análisis nativo](https://rea.tools/es/guides/native/) |
| Estructura ELF sin ejecutar | Secciones, segmentos, símbolos/reubicaciones originales y posibles medidas de mitigación detectadas mediante análisis estático | pwntools proporcionado por quien llama, en Linux x64; [diagnóstico de binarios](../binary-diagnostics.md) |
| Bytecode EVM | Selectores de despacho, desplazamientos en bytes, argumentos inferidos y mutabilidad | Entrada local con bytes sin procesar o representación hexadecimal, en Linux x64; [guía de EVM sin ejecutar](../evm-bytecode.md) |
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
| Fallos de Linux registrados | Registros note sin procesar, registros/señales de cada hilo registrado y posibles correspondencias de memoria opcionales | pwntools proporcionado por quien llama, en Linux x64; GDB/pwndbg opcionales; [fallos registrados](../recorded-crashes.md) |
| JavaScript / Electron | Módulos, importaciones, mapas de código fuente, rutas, IPC y relaciones con complementos nativos | Node.js y npm; [análisis de aplicaciones](https://rea.tools/es/guides/javascript/) |
| Sitios web | Estructura de la página, scripts, observaciones de red y capturas de pantalla solicitadas | Un navegador de la familia Chrome; [análisis del navegador](https://rea.tools/es/guides/browser/) |
+1
View File
@@ -127,6 +127,7 @@ REA به Node.js 22.x (>=22.19)، 24.x (>=24.11) یا 26+، به‌همراه np
| باینری‌های بومی | شبه‌کد، اسمبلی، رشته‌ها، نمادها، فراخوانی‌ها و ارجاعات | Hopper، Ghidra یا IDA؛ [تحلیل بومی](https://rea.tools/fa/guides/native/) |
| ساختار ELF آفلاین | بخش‌ها، سگمنت‌ها، نمادها و relocationهای اصلی و گزینه‌های احتمالی کاهش آسیب‌پذیری ایستا | pwntools تأمین‌شده توسط فراخواننده روی Linux x64؛ [تشخیص باینری](../binary-diagnostics.md) |
| بایت‌کد EVM | selectorهای dispatch، offsetهای بایتی، آرگومان‌های استنباط‌شده و قابلیت تغییر وضعیت | دادهٔ خام/hex محلی روی Linux x64؛ [راهنمای EVM آفلاین](../evm-bytecode.md) |
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
| کرش‌های ثبت‌شدهٔ لینوکس | یادداشت‌های خام، رجیسترها/سیگنال‌های تمام threadهای ثبت‌شده و گزینه‌های احتمالی نگاشت | pwntools تأمین‌شده توسط فراخواننده روی Linux x64؛ GDB/pwndbg اختیاری؛ [کرش‌های ثبت‌شده](../recorded-crashes.md) |
| JavaScript / Electron | ماژول‌ها، importها، source mapها، مسیرها، IPC و ارتباط افزونه‌های بومی | Node.js و npm؛ [تحلیل برنامه](https://rea.tools/fa/guides/javascript/) |
| وب‌سایت‌ها | ساختار صفحه، اسکریپت‌ها، مشاهدات شبکه و اسکرین‌شات‌های درخواستی | مرورگر مبتنی بر Chrome؛ [تحلیل مرورگر](https://rea.tools/fa/guides/browser/) |
+1
View File
@@ -139,6 +139,7 @@ REA nécessite Node.js 22.x (>=22.19), 24.x (>=24.11) ou 26+, ainsi que npm. Les
| Structure ELF hors ligne | Sections, segments, symboles et relocations d'origine, et protections statiques candidates | pwntools fourni par l'utilisateur sous Linux x64 ; [diagnostics binaires](../binary-diagnostics.md) |
| Artefact WASM | Validation, sections, formes WAT exactes d'import/export et WAT décodé | WABT 1.0.42 fourni par l'utilisateur ; `inspect_wasm_artifact` |
| Bytecode EVM | Sélecteurs de dispatch, offsets en octets, arguments inférés et mutabilité | Fichier local brut ou hexadécimal sous Linux x64 ; [guide EVM hors ligne](../evm-bytecode.md) |
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
| Crashs Linux enregistrés | Notes brutes, registres et signaux de chaque thread enregistré, et correspondances de mappings facultatives | pwntools fourni par l'utilisateur sous Linux x64 ; GDB/pwndbg en option ; [crashs enregistrés](../recorded-crashes.md) |
| JavaScript / Electron | Modules, imports, source maps, routes, IPC et relations avec les modules natifs | Node.js et npm ; [analyse d'applications](https://rea.tools/fr/guides/javascript/) |
| Sites web | Structure des pages, scripts, observations réseau et captures d'écran demandées | Un navigateur de la famille Chrome ; [analyse du navigateur](https://rea.tools/fr/guides/browser/) |
+1
View File
@@ -142,6 +142,7 @@ Alat tambahan dan dukungan sistem host bergantung pada target yang dianalisis:
| Tata letak ELF offline | Bagian, segmen, simbol/relokasi asli, dan kandidat mitigasi statis | pwntools yang disediakan pemanggil pada Linux x64; [diagnostik biner](../binary-diagnostics.md) |
| Artefak WASM | Validasi, bagian, bentuk WAT import/ekspor yang tepat, dan WAT yang didekode | WABT 1.0.42 yang disediakan pemanggil; `inspect_wasm_artifact` |
| Bytecode EVM | Selector dispatch, offset byte, argumen yang disimpulkan, dan sifat mutabilitas | Pembawa data raw/hex lokal; [panduan EVM offline](../evm-bytecode.md) |
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
| Crash Linux yang direkam | Catatan mentah, register/sinyal setiap thread yang direkam, dan kandidat pemetaan opsional | pwntools yang disediakan pemanggil; GDB/pwndbg opsional; [crash yang direkam](../recorded-crashes.md) |
| JavaScript / Electron | Modul, impor, source map, rute, IPC, dan hubungan add-on native | Node.js dan npm; [analisis aplikasi](https://rea.tools/id/guides/javascript/) |
| Situs web | Struktur halaman, skrip, pengamatan jaringan, dan tangkapan layar yang diminta | Peramban berbasis Chrome; [analisis peramban](https://rea.tools/id/guides/browser/) |
+1
View File
@@ -139,6 +139,7 @@ REA richiede Node.js 22.x (>=22.19), 24.x (>=24.11) o 26+, oltre a npm. Strument
| Layout ELF offline | Sezioni, segmenti, simboli/relocazioni originali e candidati mitigazioni statiche | pwntools fornito dal chiamante su Linux x64; [diagnostica binaria](../binary-diagnostics.md) |
| Artefatto WASM | Convalida, sezioni, forme WAT di import/export esatte e WAT decodificato | WABT 1.0.42 fornito dal chiamante; `inspect_wasm_artifact` |
| Bytecode EVM | Selettori di dispatch, offset dei byte, argomenti inferiti e mutabilità | Carrier raw/hex locale; [guida EVM offline](../evm-bytecode.md) |
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
| Crash Linux registrati | Note grezze, registri/segnai di ogni thread registrato e candidati mapping opzionali | pwntools fornito dal chiamante; GDB/pwndbg opzionale; [crash registrati](../recorded-crashes.md) |
| JavaScript / Electron | Moduli, import, source map, route, IPC e relazioni con add-on nativi | Node.js e npm; [analisi delle applicazioni](https://rea.tools/it/guides/javascript/) |
| Siti web | Struttura della pagina, script, osservazioni di rete e screenshot richiesti | Un browser della famiglia Chrome; [analisi browser](https://rea.tools/it/guides/browser/) |
+1
View File
@@ -135,6 +135,7 @@ REA には Node.js 22.x(>=22.19)、24.x(>=24.11)、または 26+ と npm
| ネイティブバイナリ | 疑似コード、アセンブリ、文字列、シンボル、呼び出し、参照 | Hopper、Ghidra、または IDA;[ネイティブ分析](https://rea.tools/ja/guides/native/) |
| オフライン ELF レイアウト | セクション、セグメント、元のシンボル/再配置情報、静的解析によるセキュリティ対策の候補 | Linux x64 で利用者が用意する pwntools;[バイナリ診断](../binary-diagnostics.md) |
| EVM バイトコード | ディスパッチセレクター、バイトオフセット、推定された引数と状態変更の可否 | Linux x64 上のローカルの生バイト/16 進数の入力ファイル;[オフライン EVM ガイド](../evm-bytecode.md) |
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
| 記録済みの Linux クラッシュ | 生の note レコード、記録された全スレッドのレジスター/シグナル、任意のマッピング候補 | Linux x64 で利用者が用意する pwntools;GDB/pwndbg は任意;[記録済みクラッシュ](../recorded-crashes.md) |
| JavaScript / Electron | モジュール、インポート、ソースマップ、ルート、IPC、ネイティブアドオンの関係 | Node.js と npm;[アプリケーション分析](https://rea.tools/ja/guides/javascript/) |
| ウェブサイト | ページ構造、スクリプト、ネットワークの観測結果、依頼されたスクリーンショット | Chrome 系ブラウザー;[ブラウザー分析](https://rea.tools/ja/guides/browser/) |
+1
View File
@@ -135,6 +135,7 @@ REA에는 Node.js 22.x(>=22.19), 24.x(>=24.11) 또는 26+와 npm이 필요합니
| 네이티브 바이너리 | 의사 코드, 어셈블리, 문자열, 심볼, 호출, 참조 | Hopper, Ghidra 또는 IDA; [네이티브 분석](https://rea.tools/ko/guides/native/) |
| 오프라인 ELF 레이아웃 | 섹션, 세그먼트, 원본 심볼/재배치 정보, 정적 분석으로 얻은 보안 완화 기법 후보 | Linux x64에서 호출자가 제공하는 pwntools; [바이너리 진단](../binary-diagnostics.md) |
| EVM 바이트코드 | 디스패치 선택자, 바이트 오프셋, 추론한 인자와 상태 변경 가능 여부 | Linux x64의 로컬 원시 바이트/16진수 입력 파일; [오프라인 EVM 가이드](../evm-bytecode.md) |
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
| 기록된 Linux 크래시 | 원시 note 레코드, 기록된 모든 스레드의 레지스터/시그널, 선택적 매핑 후보 | Linux x64에서 호출자가 제공하는 pwntools; GDB/pwndbg는 선택 사항; [기록된 크래시](../recorded-crashes.md) |
| JavaScript / Electron | 모듈, 임포트, 소스 맵, 라우트, IPC, 네이티브 애드온 관계 | Node.js와 npm; [애플리케이션 분석](https://rea.tools/ko/guides/javascript/) |
| 웹사이트 | 페이지 구조, 스크립트, 네트워크 관찰 결과, 요청한 스크린샷 | Chrome 계열 브라우저; [브라우저 분석](https://rea.tools/ko/guides/browser/) |
+1
View File
@@ -135,6 +135,7 @@ REA wymaga Node.js 22.x (>=22.19), 24.x (>=24.11) lub 26+ oraz npm. Dodatkowe na
| Natywne pliki binarne | Pseudokod, kod asemblera, ciągi znaków, symbole, wywołania i odwołania | Hopper, Ghidra lub IDA; [analiza kodu natywnego](https://rea.tools/pl/guides/native/) |
| Układ pliku ELF w trybie offline | Sekcje, segmenty, oryginalne symbole/relokacje i potencjalne mechanizmy zabezpieczeń wskazane przez analizę statyczną | pwntools dostarczone przez użytkownika w systemie Linux x64; [diagnostyka plików binarnych](../binary-diagnostics.md) |
| Kod bajtowy EVM | Selektory wywołań, przesunięcia bajtowe, wywnioskowane argumenty i mutowalność | Lokalny plik z surowymi bajtami lub zapisem szesnastkowym w systemie Linux x64; [przewodnik EVM offline](../evm-bytecode.md) |
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
| Zarejestrowane awarie systemu Linux | Surowe rekordy note, rejestry/sygnały każdego zarejestrowanego wątku i opcjonalni kandydaci na mapowania | pwntools dostarczone przez użytkownika w systemie Linux x64; opcjonalnie GDB/pwndbg; [zarejestrowane awarie](../recorded-crashes.md) |
| JavaScript / Electron | Moduły, importy, mapy źródeł, trasy, IPC i powiązania z natywnymi dodatkami | Node.js i npm; [analiza aplikacji](https://rea.tools/pl/guides/javascript/) |
| Strony internetowe | Struktura strony, skrypty, obserwacje sieciowe i zamówione zrzuty ekranu | Przeglądarka z rodziny Chrome; [analiza w przeglądarce](https://rea.tools/pl/guides/browser/) |
+1
View File
@@ -135,6 +135,7 @@ O REA requer Node.js 22.x (>=22.19), 24.x (>=24.11) ou 26+, além do npm. As fer
| Binários nativos | Pseudocódigo, assembly, strings, símbolos, chamadas e referências | Hopper, Ghidra ou IDA; [análise nativa](https://rea.tools/pt-BR/guides/native/) |
| Estrutura ELF sem execução | Seções, segmentos, símbolos/relocações originais e possíveis mecanismos de mitigação identificados por análise estática | pwntools fornecido pelo chamador no Linux x64; [diagnóstico de binários](../binary-diagnostics.md) |
| Bytecode EVM | Seletores de despacho, offsets de bytes, argumentos inferidos e mutabilidade | Entrada local com bytes brutos ou representação hexadecimal no Linux x64; [guia de EVM sem execução](../evm-bytecode.md) |
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
| Falhas do Linux registradas | Registros note brutos, registradores/sinais de cada thread registrada e candidatos opcionais a mapeamentos | pwntools fornecido pelo chamador no Linux x64; GDB/pwndbg opcionais; [falhas registradas](../recorded-crashes.md) |
| JavaScript / Electron | Módulos, importações, source maps, rotas, IPC e relações com complementos nativos | Node.js e npm; [análise de aplicativos](https://rea.tools/pt-BR/guides/javascript/) |
| Sites | Estrutura da página, scripts, observações de rede e capturas de tela solicitadas | Um navegador da família Chrome; [análise do navegador](https://rea.tools/pt-BR/guides/browser/) |
+1
View File
@@ -135,6 +135,7 @@ REA требует Node.js 22.x (>=22.19), 24.x (>=24.11) или 26+, а так
| Нативные бинарные файлы | Псевдокод, ассемблер, строки, символы, вызовы и ссылки | Hopper, Ghidra или IDA; [нативный анализ](https://rea.tools/ru/guides/native/) |
| Структура ELF без запуска | Секции, сегменты, исходные символы/релокации и возможные механизмы защиты по данным статического анализа | pwntools, предоставленный вызывающей стороной, в Linux x64; [диагностика бинарных файлов](../binary-diagnostics.md) |
| Байт-код EVM | Селекторы диспетчеризации, смещения байтов, предполагаемые аргументы и изменяемость состояния | Локальные необработанные байты или шестнадцатеричное представление в Linux x64; [руководство по EVM без запуска](../evm-bytecode.md) |
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
| Записанные сбои Linux | Необработанные записи note, регистры/сигналы каждого записанного потока и необязательные кандидаты сопоставления с памятью | pwntools, предоставленный вызывающей стороной, в Linux x64; необязательные GDB/pwndbg; [записанные сбои](../recorded-crashes.md) |
| JavaScript / Electron | Модули, импорты, карты исходного кода, маршруты, IPC и связи с нативными дополнениями | Node.js и npm; [анализ приложений](https://rea.tools/ru/guides/javascript/) |
| Сайты | Структура страницы, скрипты, наблюдения за сетью и запрошенные снимки экрана | Браузер семейства Chrome; [анализ браузера](https://rea.tools/ru/guides/browser/) |
+1
View File
@@ -135,6 +135,7 @@ REA ต้องใช้ Node.js 22.x (>=22.19), 24.x (>=24.11) หรือ 26
| ไบนารีเนทีฟ | โค้ดเทียม แอสเซมบลี สตริง สัญลักษณ์ การเรียก และการอ้างอิง | Hopper, Ghidra หรือ IDA; [การวิเคราะห์เนทีฟ](https://rea.tools/th/guides/native/) |
| โครงสร้าง ELF แบบออฟไลน์ | เซกชัน เซกเมนต์ สัญลักษณ์/รีโลเคชันต้นฉบับ และตัวเลือกกลไกป้องกันที่พบจากการวิเคราะห์แบบสถิต | pwntools ที่ผู้ใช้จัดเตรียมบน Linux x64; [การวินิจฉัยไบนารี](../binary-diagnostics.md) |
| ไบต์โค้ด EVM | ตัวเลือกฟังก์ชันสำหรับการเรียก ออฟเซ็ตไบต์ อาร์กิวเมนต์และความสามารถในการเปลี่ยนสถานะที่อนุมานได้ | ไฟล์บนเครื่องที่มีไบต์ดิบหรือข้อมูลเลขฐานสิบหกบน Linux x64; [คู่มือ EVM แบบออฟไลน์](../evm-bytecode.md) |
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
| ข้อมูลการแครชของ Linux ที่บันทึกไว้ | ระเบียน note ดิบ รีจิสเตอร์/สัญญาณของทุกเธรดที่บันทึกไว้ และตัวเลือกการแมปเพิ่มเติม | pwntools ที่ผู้ใช้จัดเตรียมบน Linux x64; เลือกใช้ GDB/pwndbg เพิ่มเติมได้; [การแครชที่บันทึกไว้](../recorded-crashes.md) |
| JavaScript / Electron | โมดูล การนำเข้า ซอร์สแมป เส้นทาง IPC และความสัมพันธ์กับส่วนเสริมเนทีฟ | Node.js และ npm; [การวิเคราะห์แอปพลิเคชัน](https://rea.tools/th/guides/javascript/) |
| เว็บไซต์ | โครงสร้างหน้า สคริปต์ ข้อมูลเครือข่ายที่สังเกตได้ และภาพหน้าจอที่ร้องขอ | เบราว์เซอร์ในตระกูล Chrome; [การวิเคราะห์ผ่านเบราว์เซอร์](https://rea.tools/th/guides/browser/) |
+1
View File
@@ -135,6 +135,7 @@ REA потребує Node.js 22.x (>=22.19), 24.x (>=24.11) або 26+, а та
| Нативні бінарні файли | Псевдокод, асемблерний код, рядки, символи, виклики й посилання | Hopper, Ghidra або IDA; [нативний аналіз](https://rea.tools/uk/guides/native/) |
| Структура ELF без запуску | Секції, сегменти, початкові символи/релокації та можливі захисні механізми за даними статичного аналізу | pwntools, наданий стороною, що викликає інструмент, у Linux x64; [діагностика бінарних файлів](../binary-diagnostics.md) |
| Байт-код EVM | Селектори диспетчеризації, зміщення байтів, виведені припущення про аргументи та змінюваність стану | Локальні необроблені байти або шістнадцяткове представлення у Linux x64; [посібник EVM без запуску](../evm-bytecode.md) |
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
| Записані аварійні завершення в Linux | Необроблені записи note, регістри/сигнали кожного записаного потоку й необов'язкові кандидати зіставлення пам'яті | pwntools, наданий стороною, що викликає інструмент, у Linux x64; необов'язкові GDB/pwndbg; [записані аварійні завершення](../recorded-crashes.md) |
| JavaScript / Electron | Модулі, імпорти, карти вихідного коду, маршрути, IPC і зв'язки з нативними доповненнями | Node.js і npm; [аналіз застосунків](https://rea.tools/uk/guides/javascript/) |
| Сайти | Структура сторінки, скрипти, мережеві спостереження й запитані знімки екрана | Браузер сімейства Chrome; [аналіз браузера](https://rea.tools/uk/guides/browser/) |
+1
View File
@@ -135,6 +135,7 @@ REA yêu cầu Node.js 22.x (>=22.19), 24.x (>=24.11) hoặc 26+, cùng với np
| Tệp nhị phân mã máy | Mã giả, hợp ngữ, chuỗi, ký hiệu, lời gọi và tham chiếu | Hopper, Ghidra hoặc IDA; [phân tích mã máy](https://rea.tools/vi/guides/native/) |
| Bố cục ELF ngoại tuyến | Các section, segment, ký hiệu/thông tin tái định vị gốc và các biện pháp giảm thiểu có thể có từ phân tích tĩnh | pwntools do bên gọi cung cấp trên Linux x64; [chẩn đoán tệp nhị phân](../binary-diagnostics.md) |
| Bytecode EVM | Bộ chọn điều phối, độ lệch byte, tham số suy luận và khả năng thay đổi trạng thái | Đầu vào cục bộ chứa byte thô hoặc dạng thập lục phân trên Linux x64; [hướng dẫn EVM ngoại tuyến](../evm-bytecode.md) |
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
| Sự cố Linux đã ghi lại | Bản ghi note thô, thanh ghi/tín hiệu của mọi luồng được ghi lại và các ứng viên ánh xạ tùy chọn | pwntools do bên gọi cung cấp trên Linux x64; GDB/pwndbg tùy chọn; [sự cố đã ghi lại](../recorded-crashes.md) |
| JavaScript / Electron | Mô-đun, các khai báo import, source map, tuyến, IPC và quan hệ với tiện ích mở rộng mã máy | Node.js và npm; [phân tích ứng dụng](https://rea.tools/vi/guides/javascript/) |
| Trang web | Cấu trúc trang, script, quan sát mạng và ảnh chụp màn hình được yêu cầu | Trình duyệt thuộc họ Chrome; [phân tích trình duyệt](https://rea.tools/vi/guides/browser/) |
+1
View File
@@ -139,6 +139,7 @@ REA 需要 Node.js 22.x(>=22.19)、24.x(>=24.11)或 26+,以及 npm。
| Go 二進位檔案 | 內嵌編譯器版本、模組、替換項和建置設定 | 內建靜態 ELF/PE/thin Mach-O 檢查;無需安裝 Go |
| WASM 工件 | 驗證、節、精確的匯入/匯出 WAT 形式及解碼後的 WAT | 呼叫方提供的 WABT 1.0.42;`inspect_wasm_artifact` |
| EVM 位元組碼 | 分派選擇器、位元組偏移、推斷的參數和狀態可變性 | Linux x64 上的本地原始位元組/十六進位輸入載體;[離線 EVM 指南](../evm-bytecode.md) |
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
| 已記錄的 Linux 崩潰 | 原始 note 記錄、每個已記錄執行緒的暫存器/訊號及可選的對映候選項 | Linux x64 上由呼叫方提供的 pwntools;可選的 GDB/pwndbg;[已記錄崩潰](../recorded-crashes.md) |
| JavaScript / Electron | 模組、匯入、source map、路由、IPC 和原生附加元件關係 | Node.js 和 npm;[應用分析](https://rea.tools/zh-TW/guides/javascript/) |
| 網站 | 頁面結構、腳本、網路觀察結果和按請求獲取的螢幕擷取畫面 | Chrome 系瀏覽器;[瀏覽器分析](https://rea.tools/zh-TW/guides/browser/) |
+1
View File
@@ -139,6 +139,7 @@ REA 需要 Node.js 22.x(>=22.19)、24.x(>=24.11)或 26+,以及 npm。
| Go 二进制文件 | 内嵌编译器版本、模块、替换项和构建设置 | 内置静态 ELF/PE/thin Mach-O 检查;无需安装 Go |
| WASM 工件 | 验证、节、精确的导入/导出 WAT 形式及解码后的 WAT | 调用方提供的 WABT 1.0.42;`inspect_wasm_artifact` |
| EVM 字节码 | 分派选择器、字节偏移、推断的参数和状态可变性 | Linux x64 上的本地原始字节/十六进制输入载体;[离线 EVM 指南](../evm-bytecode.md) |
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
| 已记录的 Linux 崩溃 | 原始 note 记录、每个已记录线程的寄存器/信号及可选的映射候选项 | Linux x64 上由调用方提供的 pwntools;可选的 GDB/pwndbg;[已记录崩溃](../recorded-crashes.md) |
| JavaScript / Electron | 模块、导入、source map、路由、IPC 和原生扩展关系 | Node.js 和 npm;[应用分析](https://rea.tools/zh-CN/guides/javascript/) |
| 网站 | 页面结构、脚本、网络观察结果和按请求获取的截图 | Chrome 系浏览器;[浏览器分析](https://rea.tools/zh-CN/guides/browser/) |
+67
View File
@@ -0,0 +1,67 @@
# Rust binary dependency metadata
`inspect_rust_binary` / `inspect-rust-binary` answers which crate names,
versions, sources, and dependency edges are embedded by cargo-auditable in one
selected local executable or library. It reads the file without running it and
requires no Rust installation, active binary target, Hopper, Ghidra, or IDA.
```bash
rea inspect-rust-binary /absolute/path/to/application --json
```
The CLI resolves relative paths against the caller's working directory. MCP
requires an absolute filesystem path and preserves that exact selection:
```json
{
"name": "inspect_rust_binary",
"arguments": { "path": "/artifacts/application" }
}
```
## Observations
The inline Evidence result contains the artifact path, SHA-256, byte length,
container format, architecture, word size, and byte order. When a `.dep-v0`
section is present, `auditable_metadata` contains:
- `section_name`, `.dep-v0` for ELF and PE or `__DATA,.dep-v0` for thin Mach-O;
- `file_offset` and `section_bytes`, the exact file range of the compressed section;
- `compressed_bytes` and `uncompressed_bytes`;
- `format_version`, the embedded integer, or `null` when the field is omitted;
- `packages`, in the section's original order, with `name`, `version`, `source`,
`kind`, `dependencies`, and `root`;
- `root_package_index`, the single package marked `root`, or `null` when none is marked;
- `dependency_edges`, pairs of package indices in package-then-dependency order;
- `cyclic_dependency_indices`, sorted unique indices that participate in a cycle.
An omitted `kind` stays `null`. The embedded format treats that omission as
runtime, and that default is not stored in the section. An omitted dependency
list is an empty array. An omitted `root` flag is false.
`rustc_commit` is present only when a `/rustc/<40-hex>/` path is found in
read-only data. Its `evidence_kind` is `inferred`. The hash is not a claim that
that compiler produced the binary. Several distinct hashes leave a single
commit unknown.
## Limits and failures
The selected file is limited to 256 MiB. A dependency section is limited to 1
MiB compressed. Inflated JSON is limited to the smaller of 8 MiB and 64 times
the compressed size. The complete report is limited to 8 MiB. Empty sections,
truncated or trailing zlib bytes, invalid JSON, unknown fields, dependency
indices outside the package list, more than one root package, and compressed
dependency sections are typed failures. A cycle is reported and is not walked.
A missing section returns success with `auditable_metadata: null` and a
limitation. Absence of `.dep-v0` does not mean the binary has no dependencies.
Embedded names and versions are file content. They do not authenticate the
packages or the compiler.
Universal Mach-O containers are unsupported. Select a thin slice first. ELF,
PE, and thin Mach-O are the supported containers.
Package and dependency counts are checked against the minimum required JSON
representation before graph allocation or schema cloning. Oversized graphs
report the output byte budget; the compiler-path capacity reports
`maximum_rustc_hashes` in hashes, independently of byte limits.
+5
View File
@@ -28,6 +28,7 @@ export const toolFamilyCatalog = (sources) => {
...sources.artifactContracts.ARTIFACT_TOOL_CONTRACTS,
...sources.evmContracts.EVM_TOOL_CONTRACTS,
...sources.goContracts.GO_TOOL_CONTRACTS,
...sources.rustContracts.RUST_TOOL_CONTRACTS,
...sources.wasmContracts.WASM_TOOL_CONTRACTS,
...sources.sqliteContracts.SQLITE_TOOL_CONTRACTS,
...sources.captureContracts.CAPTURE_TOOL_CONTRACTS,
@@ -225,6 +226,10 @@ export const providerCatalog = (sources) => {
identity: sources.goProvider.GO_BINARY_PROVIDER_IDENTITY,
contracts: sources.goContracts.GO_TOOL_CONTRACTS,
},
{
identity: sources.rustProvider.RUST_BINARY_PROVIDER_IDENTITY,
contracts: sources.rustContracts.RUST_TOOL_CONTRACTS,
},
{
identity: sources.sqliteDomain.SQLITE_PROVIDER_IDENTITY,
contracts: sources.sqliteContracts.SQLITE_TOOL_CONTRACTS,
+2
View File
@@ -59,6 +59,8 @@ const SOURCE_PATHS = {
evmProvider: "dist/evm/EvmoleRelease.js",
goContracts: "dist/contracts/go/goToolContracts.js",
goProvider: "dist/go/GoBinaryProvider.js",
rustContracts: "dist/contracts/rust/rustToolContracts.js",
rustProvider: "dist/rust/RustBinaryProvider.js",
artifactContracts: "dist/contracts/artifactToolContracts.js",
managedContracts: "dist/contracts/managed/managedToolContracts.js",
firmwareContracts: "dist/contracts/firmware/firmwareToolContracts.js",
+1
View File
@@ -8,6 +8,7 @@ const MIGRATED_PROVIDER_ROOTS = new Set([
"sqlite",
"evm",
"go",
"rust",
"wasm",
"captures",
"signals",
+1 -1
View File
@@ -343,7 +343,7 @@ const workflowAvailabilityFor = ({
kind,
policy,
}: AvailabilityContext): Availability | null => {
if (name === "inspect_go_binary")
if (name === "inspect_go_binary" || name === "inspect_rust_binary")
return { reason: "available", remediation: null };
if (name === "inspect_sqlite_database") {
const capability = sqliteDatabaseCapability(process.versions.node);
+19
View File
@@ -0,0 +1,19 @@
import type {
ExecutionOptions,
ProviderIdentity,
} from "../AnalysisProvider.js";
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
import type {
InspectRustBinaryInput,
RustBinary,
} from "../../domain/rust/rustBinary.js";
import type { Result } from "../../domain/result.js";
/** Replaceable offline dependency reader with no target-execution authority. */
export interface RustBinaryPort {
readonly identity: ProviderIdentity;
inspect(
input: InspectRustBinaryInput,
options?: ExecutionOptions,
): Promise<Result<RustBinary, AnalysisError>>;
}
@@ -0,0 +1,132 @@
import { expect, it } from "vitest";
import {
AnalysisInputError,
AnalysisOutputError,
} from "../../domain/analysisErrorCore.js";
import type { RustBinary } from "../../domain/rust/rustBinary.js";
import { ok } from "../../domain/result.js";
import { RustBinaryService } from "./RustBinaryService.js";
const identity = {
id: "fixture-rust",
name: "Source-owned Rust reader seam",
version: "1",
};
const report: RustBinary = {
artifact: { path: "/artifacts/program", sha256: "a".repeat(64), bytes: 128 },
format: "elf",
architecture: "amd64",
bits: 64,
byte_order: "little",
auditable_metadata: {
section_name: ".dep-v0",
file_offset: 16,
section_bytes: 8,
compressed_bytes: 8,
uncompressed_bytes: 20,
format_version: 1,
packages: [
{
name: "sample",
version: "0.1.0",
source: "local",
kind: null,
dependencies: [],
root: true,
},
],
root_package_index: 0,
dependency_edges: [],
cyclic_dependency_indices: [],
},
rustc_commit: null,
limitations: [
"No /rustc/<40-hex>/ path was found in read-only data, so a rustc commit hash is unknown.",
],
};
it("rejects a relative path and extra arguments before reading", async () => {
let calls = 0;
const service = new RustBinaryService({
identity,
inspect: () => {
calls += 1;
return Promise.resolve(ok(report));
},
});
const relative = await service.inspect({ path: "program" });
const extra = await service.inspect({
path: report.artifact.path,
detail: "complete",
});
expect(relative.ok).toBe(false);
expect(extra.ok).toBe(false);
if (!relative.ok) expect(relative.error).toBeInstanceOf(AnalysisInputError);
if (!extra.ok) expect(extra.error).toBeInstanceOf(AnalysisInputError);
expect(calls).toBe(0);
});
it("records the selected artifact and the dependency section range", async () => {
const result = await new RustBinaryService({
identity,
inspect: () => Promise.resolve(ok(report)),
}).inspect({ path: report.artifact.path });
expect(result.ok).toBe(true);
if (!result.ok) throw result.error;
expect(result.value).toMatchObject({
confidence: "observed",
subject: { format: "elf", architecture: "x86_64" },
normalized_result: report,
locations: [
{ kind: "artifact-path", path: report.artifact.path },
{ kind: "file-offset-range", start: 16, end: 24 },
],
});
const mismatched: RustBinary = {
...report,
artifact: { ...report.artifact, path: "/artifacts/other" },
};
const invalid = await new RustBinaryService({
identity,
inspect: () => Promise.resolve(ok(mismatched)),
}).inspect({ path: report.artifact.path });
expect(invalid.ok).toBe(false);
if (!invalid.ok) expect(invalid.error).toBeInstanceOf(AnalysisOutputError);
});
it("rejects a cycle claim the package dependencies do not contain", async () => {
if (report.auditable_metadata === null) throw new Error("expected metadata");
const invalidReport: RustBinary = {
...report,
auditable_metadata: {
...report.auditable_metadata,
cyclic_dependency_indices: [0],
},
};
const result = await new RustBinaryService({
identity,
inspect: () => Promise.resolve(ok(invalidReport)),
}).inspect({ path: report.artifact.path });
expect(result.ok).toBe(false);
if (!result.ok) expect(result.error).toBeInstanceOf(AnalysisOutputError);
});
it.each([
{ from_index: 1, to_index: 0 },
{ from_index: 0, to_index: 1 },
])("rejects provider edges referencing missing packages: %j", async (edge) => {
if (report.auditable_metadata === null) throw new Error("expected metadata");
const invalidReport: RustBinary = {
...report,
auditable_metadata: {
...report.auditable_metadata,
dependency_edges: [edge],
},
};
const result = await new RustBinaryService({
identity,
inspect: () => Promise.resolve(ok(invalidReport)),
}).inspect({ path: report.artifact.path });
expect(result.ok).toBe(false);
if (!result.ok) expect(result.error).toBeInstanceOf(AnalysisOutputError);
});
+113
View File
@@ -0,0 +1,113 @@
import { isAbsolute } from "node:path";
import type { ExecutionOptions } from "../AnalysisProvider.js";
import type { RustBinaryPort } from "./RustBinaryPort.js";
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
import {
AnalysisCancelledError,
AnalysisInputError,
AnalysisOutputError,
} from "../../domain/analysisErrorCore.js";
import { createEvidence, type Evidence } from "../../domain/evidence.js";
import { BINARY_ARCHITECTURES } from "../../domain/binaryTargetTypes.js";
import {
inspectRustBinaryInputSchema,
rustBinarySchema,
} from "../../domain/rust/rustBinary.js";
import { analysisInputErrorFromIssues } from "../../domain/inputIssueProjection.js";
import { err, ok, type Result } from "../../domain/result.js";
const OPERATION = "inspect_rust_binary";
const architectureForSubject = (architecture: string) => {
switch (architecture) {
case "386":
return "x86";
case "amd64":
return "x86_64";
default:
return BINARY_ARCHITECTURES.find(
(supported) => supported === architecture,
);
}
};
/** Shared exact input validation and artifact-bound Evidence for CLI and MCP. */
export class RustBinaryService {
constructor(readonly provider: RustBinaryPort) {}
/** Inspect one explicitly selected file without acquiring a binary session. */
async inspect(
rawInput: unknown,
options?: ExecutionOptions,
): Promise<Result<Evidence, AnalysisError>> {
if (options?.signal?.aborted)
return err(new AnalysisCancelledError(OPERATION));
const input = inspectRustBinaryInputSchema.safeParse(rawInput);
if (!input.success)
return err(
analysisInputErrorFromIssues(OPERATION, input.error.issues, rawInput, {
cause: input.error,
}),
);
if (!isAbsolute(input.data.path))
return err(
new AnalysisInputError(OPERATION, undefined, [
{
path: ["path"],
reason: "invalid_format",
message: "Expected an absolute filesystem path on this host.",
},
]),
);
const inspected = await this.provider.inspect(input.data, options);
if (!inspected.ok) return inspected;
if (options?.signal?.aborted)
return err(new AnalysisCancelledError(OPERATION));
const report = rustBinarySchema.safeParse(inspected.value);
if (!report.success || report.data.artifact.path !== input.data.path)
return err(
new AnalysisOutputError(
OPERATION,
"Rust dependency reader returned malformed metadata or changed the selected artifact identity.",
),
);
return ok(evidenceFor(this.provider.identity, input.data, report.data));
}
}
const evidenceFor = (
provider: RustBinaryPort["identity"],
parameters: { readonly path: string },
value: ReturnType<typeof rustBinarySchema.parse>,
): Evidence => {
const architecture = architectureForSubject(value.architecture);
const section = value.auditable_metadata;
return createEvidence(
{
path: value.artifact.path,
sha256: value.artifact.sha256,
format: value.format === "macho" ? "mach-o" : value.format,
...(architecture === undefined ? {} : { architecture }),
},
provider,
{
operation: OPERATION,
parameters,
result: value,
confidence: "observed",
limitations: value.limitations,
locations: [
{ kind: "artifact-path", path: value.artifact.path },
...(section === null
? []
: [
{
kind: "file-offset-range" as const,
start: section.file_offset,
end: section.file_offset + section.section_bytes,
},
]),
],
},
);
};
+2
View File
@@ -5,6 +5,7 @@ import { snapshotEnvironment } from "./process/snapshotEnvironment.js";
import { createDirectAnalysis } from "./composition/directAnalysis.js";
import { registerEvmCommands } from "./cli/evmCommands.js";
import { registerGoCommands } from "./cli/goCommands.js";
import { registerRustCommands } from "./cli/rustCommands.js";
import { registerSqliteCommands } from "./cli/sqliteCommands.js";
import { registerAnalysisViewCommands } from "./cli/analysisViewCommands.js";
import { registerBinaryDiagnosticsCommands } from "./cli/binaryDiagnosticsCommands.js";
@@ -101,6 +102,7 @@ export const createCli = (
registerSignalCommands(cli, logger, environment);
registerEvmCommands(cli, logger, environment);
registerGoCommands(cli, logger);
registerRustCommands(cli, logger);
registerSqliteCommands(cli, logger, environment);
registerArtifactCommands(cli, logger, analysis.runProviderAnalysis);
registerManagedCommands(cli, logger, analysis.runProviderAnalysis);
+34
View File
@@ -0,0 +1,34 @@
import { z } from "incur";
import { resolve } from "node:path";
import { createRustBinaryService } from "../composition/rust.js";
import { CLI_COMMANDS } from "../cliCommandNames.js";
import { logCliCommand } from "../cliLogging.js";
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
import type { Logger } from "pino";
import type { CliInstance } from "./types.js";
import { withCommandCancellation } from "./commandCancellation.js";
/** Resolve caller paths and inspect embedded Rust dependencies through the shared workflow. */
export const registerRustCommands = (
cli: CliInstance,
logger: Logger,
): void => {
const service = createRustBinaryService();
cli.command(CLI_COMMANDS.inspectRustBinary, {
description:
"Inspect an embedded cargo-auditable Rust dependency list without target execution",
args: z.object({
path: z.string().describe("Local ELF, PE or thin Mach-O image"),
}),
run: ({ args }) =>
withCommandCancellation((signal) =>
logCliCommand(logger, CLI_COMMANDS.inspectRustBinary, async () => {
const result = await service.inspect(
{ path: resolve(args.path) },
{ signal },
);
return result.ok ? result.value : projectAnalysisError(result.error);
}),
),
});
};
+1
View File
@@ -50,6 +50,7 @@ export const CLI_COMMANDS = Object.freeze({
inspectRecordedCrash: "inspect-recorded-crash",
inspectBinaryLayout: "inspect-binary-layout",
inspectGoBinary: "inspect-go-binary",
inspectRustBinary: "inspect-rust-binary",
inspectPeResources: "inspect-pe-resources",
inspectAnalysisView: "inspect-analysis-view",
listFridaDevices: "list-frida-devices",
+6
View File
@@ -0,0 +1,6 @@
import { RustBinaryService } from "../application/rust/RustBinaryService.js";
import { RustBinaryProvider } from "../rust/RustBinaryProvider.js";
/** Compose bundled offline inspection without a Rust toolchain or any child process. */
export const createRustBinaryService = (): RustBinaryService =>
new RustBinaryService(new RustBinaryProvider());
+26
View File
@@ -0,0 +1,26 @@
import {
inspectRustBinaryInputSchema,
rustBinarySchema,
} from "../../domain/rust/rustBinary.js";
import type { ToolContract } from "../toolContractTypes.js";
import { toolContractMetadata } from "../toolEffects.js";
import { evidenceResultOf } from "../toolOutputSchemaPrimitives.js";
/** Static Rust dependency metadata is independent of active native-analysis providers. */
export const RUST_TOOL_CONTRACTS = [
{
name: "inspect_rust_binary",
...toolContractMetadata("inspect_rust_binary"),
kind: "artifact-provider",
description:
"Inspect the cargo-auditable crate dependency list embedded in an explicit local ELF, PE or thin Mach-O image. Returns the artifact SHA-256, the .dep-v0 section file offset and size, compressed and uncompressed sizes, format version, packages in their original order, the root package index, and dependency edges by index. Reads zlib-compressed JSON without installing Rust, opening a disassembler, executing the target, writing temporary files or accessing the network. A missing section is null metadata with a limitation and is not proof that the binary has no dependencies. A rustc commit hash is reported only as an inference when a /rustc/<40-hex>/ path is present in read-only data. Resource guards bound files to 256 MiB, dependency sections to 1 MiB, inflated JSON to the smaller of 8 MiB and 64 times the compressed size, and the complete report to 8 MiB. Malformed, unsupported and oversized inputs receive typed failures rather than partial success.",
inputSchema: inspectRustBinaryInputSchema,
outputSchema: evidenceResultOf(rustBinarySchema),
examples: [
{
title: "Inspect an embedded Rust dependency list",
input: { path: "/artifacts/application" },
},
],
},
] as const satisfies readonly ToolContract[];
+2
View File
@@ -3,6 +3,7 @@ import { SIGNAL_TOOL_CONTRACTS } from "./signals/signalToolContracts.js";
import { WASM_TOOL_CONTRACTS } from "./wasm/wasmToolContracts.js";
import { EVM_TOOL_CONTRACTS } from "./evm/evmToolContracts.js";
import { GO_TOOL_CONTRACTS } from "./go/goToolContracts.js";
import { RUST_TOOL_CONTRACTS } from "./rust/rustToolContracts.js";
import { SQLITE_TOOL_CONTRACTS } from "./sqlite/sqliteToolContracts.js";
import { ANALYSIS_VIEW_TOOL_CONTRACTS } from "./analysisViewToolContracts.js";
import { BINARY_DIAGNOSTICS_TOOL_CONTRACTS } from "./native/binaryDiagnosticsToolContracts.js";
@@ -40,6 +41,7 @@ export const TOOL_CONTRACTS = [
...ARTIFACT_TOOL_CONTRACTS,
...EVM_TOOL_CONTRACTS,
...GO_TOOL_CONTRACTS,
...RUST_TOOL_CONTRACTS,
...WASM_TOOL_CONTRACTS,
...SQLITE_TOOL_CONTRACTS,
...CAPTURE_TOOL_CONTRACTS,
+2
View File
@@ -19,6 +19,8 @@ const EXECUTION_WORDING_EXCEPTIONS: Readonly<Record<string, string>> = {
resolve_native_call_targets: "states that it does not execute code",
inspect_pe_resources: "states that it does not execute the image",
inspect_go_binary: "reads embedded metadata without executing the target",
inspect_rust_binary:
"reads an embedded dependency list without executing the target",
inspect_sqlite_database:
"uses a trusted inspector worker without executing application code or caller SQL",
inspect_binary_layout:
+1
View File
@@ -54,6 +54,7 @@ const sessionEvidence = effects({ mutatesSession: true });
/** Explicit effect audit for every public tool. */
export const TOOL_EFFECTS: Readonly<Record<string, ToolEffects>> = {
inspect_go_binary: evidence,
inspect_rust_binary: evidence,
inspect_sqlite_database: effects({
mutatesSession: true,
writesFilesystem: true,
+294
View File
@@ -0,0 +1,294 @@
import { z } from "zod";
import { digestSchema } from "../digests.js";
import { localPathStringSchema } from "../localPath.js";
export const RUST_AUDITABLE_FORMAT_VERSION = 1;
const index = z.number().int().nonnegative();
const rustcHash = z.string().regex(/^[0-9a-f]{40}$/u);
/** One embedded package, with omitted kind and root preserved as null and false. */
export const rustPackageSchema = z.strictObject({
name: z.string().min(1),
version: z.string().min(1),
source: z.string().min(1),
kind: z.enum(["runtime", "build"]).nullable(),
dependencies: z.array(index),
root: z.boolean(),
});
const dependencyEdgeSchema = z.strictObject({
from_index: index,
to_index: index,
});
/** zlib JSON from one `.dep-v0` section, in the section's package order. */
export const rustAuditableMetadataSchema = z.strictObject({
section_name: z.enum([".dep-v0", "__DATA,.dep-v0"]),
file_offset: index,
section_bytes: z.number().int().positive(),
compressed_bytes: z.number().int().positive(),
uncompressed_bytes: z.number().int().positive(),
format_version: z.literal(RUST_AUDITABLE_FORMAT_VERSION).nullable(),
packages: z.array(rustPackageSchema),
root_package_index: index.nullable(),
dependency_edges: z.array(dependencyEdgeSchema),
cyclic_dependency_indices: z.array(index),
});
/** Inferred compiler id from read-only `/rustc/<40-hex>/` paths. */
export const rustcCommitSchema = z.strictObject({
evidence_kind: z.literal("inferred"),
hashes: z
.array(
z.strictObject({
hash: rustcHash,
file_offset: index,
}),
)
.min(1),
});
/** One explicit local executable or library, independent of a disassembler target. */
export const inspectRustBinaryInputSchema = z.strictObject({
path: localPathStringSchema.describe(
"Absolute filesystem path to the selected executable or library",
),
});
/** Bounded cargo-auditable observation. A missing section is null, not an empty dependency list. */
export const rustBinarySchema = z
.strictObject({
artifact: z.strictObject({
path: localPathStringSchema,
sha256: digestSchema,
bytes: index,
}),
format: z.enum(["elf", "pe", "macho"]),
architecture: z.string(),
bits: z.union([z.literal(32), z.literal(64)]),
byte_order: z.enum(["little", "big"]),
auditable_metadata: rustAuditableMetadataSchema.nullable(),
rustc_commit: rustcCommitSchema.nullable(),
limitations: z.array(z.string()),
})
.superRefine((report, context) => {
const metadata = report.auditable_metadata;
if (metadata !== null) {
checkSection(report.artifact.bytes, metadata, context);
checkPackages(metadata, context);
}
checkRustc(report.artifact.bytes, report.rustc_commit, context);
});
export type RustPackage = z.infer<typeof rustPackageSchema>;
export type RustAuditableMetadata = z.infer<typeof rustAuditableMetadataSchema>;
export type RustBinary = z.infer<typeof rustBinarySchema>;
export type InspectRustBinaryInput = z.infer<
typeof inspectRustBinaryInputSchema
>;
const issue = (
context: z.RefinementCtx,
path: (string | number)[],
message: string,
): void => {
context.addIssue({ code: "custom", path, message });
};
const inside = (fileBytes: number, offset: number, size: number): boolean =>
offset <= fileBytes && size <= fileBytes - offset;
const checkSection = (
fileBytes: number,
metadata: RustAuditableMetadata,
context: z.RefinementCtx,
): void => {
if (
!inside(fileBytes, metadata.file_offset, metadata.section_bytes) ||
metadata.compressed_bytes !== metadata.section_bytes
)
issue(
context,
["auditable_metadata", "file_offset"],
"Dependency section range is outside the selected artifact or disagrees with its compressed size.",
);
};
const checkPackages = (
metadata: RustAuditableMetadata,
context: z.RefinementCtx,
): void => {
const roots: number[] = [];
const edgesByPackage = new Map<
number,
z.infer<typeof dependencyEdgeSchema>[]
>();
for (const [position, edge] of metadata.dependency_edges.entries()) {
if (
edge.from_index >= metadata.packages.length ||
edge.to_index >= metadata.packages.length
) {
issue(
context,
["auditable_metadata", "dependency_edges", position],
"Dependency edge index is outside the package list.",
);
continue;
}
const grouped = edgesByPackage.get(edge.from_index);
if (grouped === undefined) edgesByPackage.set(edge.from_index, [edge]);
else grouped.push(edge);
}
for (const [packageIndex, pkg] of metadata.packages.entries()) {
if (pkg.root) roots.push(packageIndex);
const edges = edgesByPackage.get(packageIndex) ?? [];
if (
edges.length !== pkg.dependencies.length ||
edges.some((edge, index) => edge.to_index !== pkg.dependencies[index])
)
issue(
context,
["auditable_metadata", "dependency_edges"],
"Dependency edges do not match package dependency indices.",
);
if (pkg.dependencies.some((target) => target >= metadata.packages.length))
issue(
context,
["auditable_metadata", "packages", packageIndex, "dependencies"],
"Dependency index is outside the package list.",
);
}
const root = metadata.root_package_index;
if (
root === null ? roots.length !== 0 : roots.length !== 1 || roots[0] !== root
)
issue(
context,
["auditable_metadata", "root_package_index"],
"Root package index does not match packages marked root.",
);
const cycles = cyclicDependencyIndices(metadata.packages);
if (
cycles.length !== metadata.cyclic_dependency_indices.length ||
cycles.some(
(index, position) =>
metadata.cyclic_dependency_indices[position] !== index,
)
)
issue(
context,
["auditable_metadata", "cyclic_dependency_indices"],
"Cyclic dependency indices do not match the cycles in package dependencies.",
);
};
/** Package indices that participate in a dependency cycle, in ascending order. */
export const cyclicDependencyIndices = (
packages: readonly { readonly dependencies: readonly number[] }[],
): number[] => {
const edges = packages.map((pkg) => pkg.dependencies);
const order = dependencyPostorder(edges);
const reversed = reversedDependencyEdges(edges);
const seen = new Uint8Array(edges.length);
const cyclic = new Set<number>();
for (let cursor = order.length - 1; cursor >= 0; cursor -= 1) {
const start = order[cursor];
if (start === undefined || seen[start] === 1) continue;
const component = dependencyComponent(start, reversed, seen);
if (dependencyComponentInCycle(component, edges))
for (const index of component) cyclic.add(index);
}
return [...cyclic].sort((left, right) => left - right);
};
interface DependencyWalkFrame {
readonly index: number;
edge: number;
}
const dependencyPostorder = (
edges: readonly (readonly number[])[],
): number[] => {
const seen = new Uint8Array(edges.length);
const order: number[] = [];
for (let start = 0; start < edges.length; start += 1) {
if (seen[start] === 1) continue;
const stack: DependencyWalkFrame[] = [{ index: start, edge: 0 }];
seen[start] = 1;
while (stack.length > 0) {
const frame = stack[stack.length - 1];
if (frame === undefined) break;
const next = edges[frame.index]?.[frame.edge];
if (next === undefined) {
stack.pop();
order.push(frame.index);
continue;
}
frame.edge += 1;
if (seen[next] === 0) {
seen[next] = 1;
stack.push({ index: next, edge: 0 });
}
}
}
return order;
};
const reversedDependencyEdges = (
edges: readonly (readonly number[])[],
): number[][] => {
const reversed = Array.from({ length: edges.length }, () => [] as number[]);
for (let from = 0; from < edges.length; from += 1)
for (const to of edges[from] ?? []) reversed[to]?.push(from);
return reversed;
};
const dependencyComponent = (
start: number,
reversed: readonly (readonly number[])[],
seen: Uint8Array,
): number[] => {
const component: number[] = [];
const stack = [start];
seen[start] = 1;
while (stack.length > 0) {
const index = stack.pop();
if (index === undefined) break;
component.push(index);
for (const next of reversed[index] ?? []) {
if (seen[next] === 0) {
seen[next] = 1;
stack.push(next);
}
}
}
return component;
};
const dependencyComponentInCycle = (
component: readonly number[],
edges: readonly (readonly number[])[],
): boolean => {
if (component.length > 1) return true;
const only = component[0];
return only !== undefined && (edges[only]?.includes(only) ?? false);
};
const checkRustc = (
fileBytes: number,
commit: RustBinary["rustc_commit"],
context: z.RefinementCtx,
): void => {
if (commit === null) return;
const seen = new Set<string>();
for (const [position, entry] of commit.hashes.entries()) {
if (seen.has(entry.hash) || !inside(fileBytes, entry.file_offset, 48))
issue(
context,
["rustc_commit", "hashes", position],
"rustc path hash is duplicated or its file offset is outside the artifact.",
);
seen.add(entry.hash);
}
};
+37
View File
@@ -34,6 +34,16 @@ export interface GoZeroFillMapping {
readonly size: bigint;
}
/** One named file section. Invalid declared ranges stay visible with `file_backed: false`. */
export interface GoImageSection {
readonly name: string;
readonly offset: number;
readonly size: number;
readonly file_backed: boolean;
readonly read_only: boolean;
readonly compressed: boolean;
}
/** Native container facts and its producer-defined build-info search area. */
export interface GoBinaryContainer {
readonly format: "elf" | "pe" | "macho";
@@ -42,6 +52,7 @@ export interface GoBinaryContainer {
readonly byte_order: "little" | "big";
readonly mappings: readonly GoFileMapping[];
readonly zero_fills: readonly GoZeroFillMapping[];
readonly sections: readonly GoImageSection[];
readonly search: GoFileMapping | null;
}
@@ -110,6 +121,32 @@ export class GoBinaryReader {
}
}
/** Admit a declared file span, or mark it unbacked when it leaves the file. */
export const declaredFileRange = (
fileBytes: number,
offset: bigint,
size: bigint,
): {
readonly offset: number;
readonly size: number;
readonly file_backed: boolean;
} => {
if (
offset < 0n ||
size < 0n ||
offset > BigInt(Number.MAX_SAFE_INTEGER) ||
size > BigInt(Number.MAX_SAFE_INTEGER) ||
offset > BigInt(fileBytes) ||
size > BigInt(fileBytes) - offset
)
return { offset: 0, size: 0, file_backed: false };
return {
offset: Number(offset),
size: Number(size),
file_backed: size > 0n,
};
};
/** Map a virtual span without losing precision or selecting an ambiguous alias. */
export const mappedFileOffset = (
mappings: readonly GoFileMapping[],
+3 -2
View File
@@ -55,7 +55,7 @@ export interface GoBinaryImage {
/** Inspect bounded executable bytes using native mappings, without loading or executing code. */
export const readGoBinaryImage = (bytes: Buffer): GoBinaryImage => {
const container = readContainer(bytes);
const container = readGoBinaryContainer(bytes);
const { format, architecture, bits, byte_order } = container;
const search = container.search;
const header = findHeader(bytes, search);
@@ -184,7 +184,8 @@ interface LocatedString {
readonly location: GoStringLocation;
}
const readContainer = (bytes: Buffer): GoBinaryContainer => {
/** Identify an ELF, PE, or thin Mach-O image and its named file sections. */
export const readGoBinaryContainer = (bytes: Buffer): GoBinaryContainer => {
if (bytes.subarray(0, 4).equals(Buffer.from([0x7f, 0x45, 0x4c, 0x46])))
return readGoElfImage(bytes);
if (bytes.subarray(0, 2).equals(Buffer.from("MZ")))
+107
View File
@@ -1,9 +1,11 @@
import {
declaredFileRange,
GoBinaryFormatFailure,
GoBinaryReader,
mappedFileOffset,
type GoBinaryContainer,
type GoFileMapping,
type GoImageSection,
type GoZeroFillMapping,
} from "./GoBinaryContainer.js";
import { readElfFixedHeader } from "../domain/elfFixedHeader.js";
@@ -100,6 +102,7 @@ export const readGoElfImage = (bytes: Buffer): GoBinaryContainer => {
);
const mappings: GoFileMapping[] = [];
const zeroFills: GoZeroFillMapping[] = [];
const sections: GoImageSection[] = [];
let fallback: GoFileMapping | null = null;
for (let index = 0; index < phCount; index++) {
const offset = phStart + index * phWidth;
@@ -158,6 +161,15 @@ export const readGoElfImage = (bytes: Buffer): GoBinaryContainer => {
"malformed",
"ELF section name offset is outside its string table",
);
recordElfSection(sections, {
bytes,
names,
nameOffset,
reader,
bits,
wide,
header: offset,
});
// Comparing the complete fixed name avoids quadratic scans through malicious string tables.
const expectedName = Buffer.from(".go.buildinfo\0");
if (
@@ -213,10 +225,105 @@ export const readGoElfImage = (bytes: Buffer): GoBinaryContainer => {
byte_order: reader.little ? "little" : "big",
mappings,
zero_fills: zeroFills,
sections,
search: search ?? fallback,
};
};
const recordElfSection = (
sections: GoImageSection[],
input: {
readonly bytes: Buffer;
readonly names: { readonly offset: number; readonly size: number };
readonly nameOffset: number;
readonly reader: GoBinaryReader;
readonly bits: 32 | 64;
readonly wide: boolean;
readonly header: number;
},
): void => {
const decoded = elfSectionName(input.bytes, input.names, input.nameOffset);
const header = {
reader: input.reader,
bits: input.bits,
wide: input.wide,
header: input.header,
};
switch (decoded.status) {
case "named":
sections.push(elfImageSection({ ...header, name: decoded.name }));
return;
case "truncated": {
// Names past this window stay out of .dep-v0 matching. Read-only
// bytes can still hold the only compiler path.
const section = elfImageSection({ ...header, name: "" });
if (section.read_only) sections.push(section);
return;
}
case "unnamed":
return;
default: {
const unreachable: never = decoded;
return unreachable;
}
}
};
const elfSectionName = (
bytes: Buffer,
names: { readonly offset: number; readonly size: number },
nameOffset: number,
):
| { readonly status: "named"; readonly name: string }
| { readonly status: "unnamed" }
| { readonly status: "truncated" } => {
const start = names.offset + nameOffset;
const limit = Math.min(start + 64, names.offset + names.size);
const nul = bytes.subarray(start, limit).indexOf(0);
if (nul === 0) return { status: "unnamed" };
if (nul < 0) return { status: "truncated" };
return {
status: "named",
name: bytes.toString("latin1", start, start + nul),
};
};
const elfImageSection = ({
reader,
bits,
wide,
header,
name,
}: {
readonly reader: GoBinaryReader;
readonly bits: 32 | 64;
readonly wide: boolean;
readonly header: number;
readonly name: string;
}): GoImageSection => {
const type = reader.u32(header + 4);
const flags = reader.word(header + 8, bits);
const declared = declaredFileRange(
reader.bytes.length,
reader.word(header + (wide ? 24 : 16), bits),
reader.word(header + (wide ? 32 : 20), bits),
);
// SHT_NOBITS offsets and sizes do not describe stored bytes.
const range =
type === SHT_NOBITS ? { ...declared, file_backed: false } : declared;
const compressed = (flags & 0x800n) !== 0n;
const readOnly =
type === 1 &&
range.file_backed &&
!compressed &&
(flags & 1n) === 0n &&
(flags & 4n) === 0n &&
(flags & 2n) !== 0n;
return { name, ...range, read_only: readOnly, compressed };
};
const SHT_NOBITS = 8;
const elfArchitecture = (machine: number): string => {
const names = new Map([
[3, "386"],
+86 -5
View File
@@ -1,9 +1,11 @@
import {
declaredFileRange,
GoBinaryFormatFailure,
GoBinaryReader,
mappedFileOffset,
type GoBinaryContainer,
type GoFileMapping,
type GoImageSection,
type GoZeroFillMapping,
} from "./GoBinaryContainer.js";
@@ -30,6 +32,7 @@ export const readGoMachoImage = (bytes: Buffer): GoBinaryContainer => {
);
const mappings: GoFileMapping[] = [];
const zeroFills: GoZeroFillMapping[] = [];
const sections: GoImageSection[] = [];
const named: GoFileMapping[] = [];
let fallback: GoFileMapping | null = null;
let offset = commandStart;
@@ -105,11 +108,25 @@ export const readGoMachoImage = (bytes: Buffer): GoBinaryContainer => {
fallback = mapping;
for (let sectionIndex = 0; sectionIndex < sectionCount; sectionIndex++) {
const section = offset + segmentHeader + sectionIndex * sectionWidth;
if (
fixedName(reader.range(section, 16, "Mach-O section name")) !==
"__go_buildinfo"
)
continue;
const sectionName = fixedName(
reader.range(section, 16, "Mach-O section name"),
);
sections.push(
machoImageSection({
reader,
bits,
wide,
header: section,
name: `${fixedName(reader.range(section + 16, 16, "Mach-O section segment"))},${sectionName}`,
protection,
unmapped: unmapped || mapping === undefined,
segmentAddress: address,
segmentMemorySize: memorySize,
segmentFileOffset: fileOffset,
segmentFileSize: fileSize,
}),
);
if (sectionName !== "__go_buildinfo") continue;
if (unmapped)
throw new GoBinaryFormatFailure(
"malformed",
@@ -175,10 +192,74 @@ export const readGoMachoImage = (bytes: Buffer): GoBinaryContainer => {
byte_order: little ? "little" : "big",
mappings,
zero_fills: zeroFills,
sections,
search: named[0] ?? fallback,
};
};
const machoImageSection = ({
reader,
bits,
wide,
header,
name,
protection,
unmapped,
segmentAddress,
segmentMemorySize,
segmentFileOffset,
segmentFileSize,
}: {
readonly reader: GoBinaryReader;
readonly bits: 32 | 64;
readonly wide: boolean;
readonly header: number;
readonly name: string;
readonly protection: number;
readonly unmapped: boolean;
readonly segmentAddress: bigint;
readonly segmentMemorySize: bigint;
readonly segmentFileOffset: bigint;
readonly segmentFileSize: bigint;
}): GoImageSection => {
const flags = reader.u32(header + (wide ? 64 : 56));
const zeroFill = [1, 0xc, 0x12].includes(flags & 0xff);
const sectionAddress = reader.word(header + 32, bits);
const sectionSize = reader.word(header + (wide ? 40 : 36), bits);
const sectionOffset = BigInt(reader.u32(header + (wide ? 48 : 40)));
const addressDelta = sectionAddress - segmentAddress;
const fileDelta = sectionOffset - segmentFileOffset;
const inSegmentMemory =
addressDelta >= 0n &&
addressDelta <= segmentMemorySize &&
sectionSize <= segmentMemorySize - addressDelta;
const inSegmentFile =
fileDelta >= 0n &&
fileDelta <= segmentFileSize &&
sectionSize <= segmentFileSize - fileDelta;
const range = unmapped
? { offset: 0, size: 0, file_backed: false }
: declaredFileRange(
reader.bytes.length,
BigInt(reader.u32(header + (wide ? 48 : 40))),
reader.word(header + (wide ? 40 : 36), bits),
);
const fileBacked =
range.file_backed && !zeroFill && inSegmentMemory && inSegmentFile;
const readOnly =
fileBacked &&
(protection & 1) !== 0 &&
(protection & 2) === 0 &&
(flags & 0x80000400) === 0;
return {
name,
...range,
file_backed: fileBacked,
read_only: readOnly,
compressed: false,
};
};
const fixedName = (bytes: Buffer): string => {
const nul = bytes.indexOf(0);
return bytes.subarray(0, nul < 0 ? bytes.length : nul).toString("latin1");
+51
View File
@@ -1,9 +1,11 @@
import { observeMzWindowsHeader, parseDosMzHeader } from "../domain/dosMz.js";
import {
declaredFileRange,
GoBinaryFormatFailure,
GoBinaryReader,
type GoBinaryContainer,
type GoFileMapping,
type GoImageSection,
type GoZeroFillMapping,
} from "./GoBinaryContainer.js";
@@ -86,14 +88,30 @@ export const readGoPeImage = (bytes: Buffer): GoBinaryContainer => {
);
const mappings: GoFileMapping[] = [];
const zeroFills: GoZeroFillMapping[] = [];
const sections: GoImageSection[] = [];
let search: GoFileMapping | null = null;
for (let index = 0; index < count; index++) {
const entry = start + index * 40;
const sectionName = peSectionName(
reader.range(entry, 8, "PE section name"),
);
const virtualSize = reader.u32(entry + 8);
const virtualAddress = reader.u32(entry + 12);
const size = reader.u32(entry + 16);
const offset = reader.u32(entry + 20);
const characteristics = reader.u32(entry + 36);
// SizeOfRawData includes file-alignment padding. VirtualSize is the content.
const contentSize = virtualSize === 0 ? size : Math.min(virtualSize, size);
if (sectionName.length > 0)
sections.push(
peImageSection({
reader,
name: sectionName,
size: contentSize,
offset,
characteristics,
}),
);
const address = imageBase + BigInt(virtualAddress);
if (address + BigInt(Math.max(size, virtualSize)) > 1n << BigInt(bits))
throw new GoBinaryFormatFailure(
@@ -138,6 +156,39 @@ export const readGoPeImage = (bytes: Buffer): GoBinaryContainer => {
byte_order: "little",
mappings,
zero_fills: zeroFills,
sections,
search,
};
};
const peSectionName = (bytes: Buffer): string => {
const nul = bytes.indexOf(0);
return bytes.toString("latin1", 0, nul < 0 ? bytes.length : nul);
};
const peImageSection = ({
reader,
name,
size,
offset,
characteristics,
}: {
readonly reader: GoBinaryReader;
readonly name: string;
readonly size: number;
readonly offset: number;
readonly characteristics: number;
}): GoImageSection => {
const range = declaredFileRange(
reader.bytes.length,
BigInt(offset),
BigInt(size),
);
const readOnly =
range.file_backed &&
(characteristics & 0x40000000) !== 0 &&
(characteristics & 0x80000000) === 0 &&
(characteristics & 0x20000000) === 0 &&
(characteristics & 0x40) !== 0;
return { name, ...range, read_only: readOnly, compressed: false };
};
+221
View File
@@ -0,0 +1,221 @@
import { deflateSync } from "node:zlib";
import { expect, it } from "vitest";
import {
RUST_SECTION_BYTES,
readAuditableSection,
RustAuditableFormatFailure,
RustAuditableResourceFailure,
} from "./RustAuditableReader.js";
const section = (document: unknown) =>
readAuditableSection(
".dep-v0",
0,
deflateSync(Buffer.from(JSON.stringify(document))),
);
it("preserves package order, omitted kind, and the root index", () => {
const parsed = section({
packages: [
{ name: "itoa", version: "1.0.18", source: "crates.io" },
{
name: "sample",
version: "0.1.0",
source: "local",
dependencies: [0],
root: true,
kind: "runtime",
},
],
format: 1,
});
expect(parsed).toMatchObject({
section_name: ".dep-v0",
format_version: 1,
root_package_index: 1,
compressed_bytes: parsed.section_bytes,
packages: [
{
name: "itoa",
version: "1.0.18",
source: "crates.io",
kind: null,
dependencies: [],
root: false,
},
{
name: "sample",
version: "0.1.0",
kind: "runtime",
dependencies: [0],
root: true,
},
],
dependency_edges: [{ from_index: 1, to_index: 0 }],
cyclic_dependency_indices: [],
});
expect(parsed.uncompressed_bytes).toBeGreaterThan(parsed.compressed_bytes);
});
it("reports dependency cycles and keeps an omitted format version unknown", () => {
const parsed = section({
packages: [
{ name: "a", version: "1", source: "local", dependencies: [1] },
{ name: "b", version: "1", source: "local", dependencies: [0] },
],
});
expect(parsed.cyclic_dependency_indices).toEqual([0, 1]);
expect(parsed.format_version).toBeNull();
expect(parsed.root_package_index).toBeNull();
});
it("includes every package that participates in a dependency cycle", () => {
const parsed = section({
packages: [
{ name: "a", version: "1", source: "local", dependencies: [1, 2] },
{ name: "b", version: "1", source: "local", dependencies: [0] },
{ name: "c", version: "1", source: "local", dependencies: [1] },
],
});
expect(parsed.cyclic_dependency_indices).toEqual([0, 1, 2]);
const self = section({
packages: [{ name: "a", version: "1", source: "local", dependencies: [0] }],
});
expect(self.cyclic_dependency_indices).toEqual([0]);
});
it("finishes a long acyclic dependency chain", () => {
const count = 20_000;
const parsed = section({
packages: Array.from({ length: count }, (_, index) => ({
name: `p${index.toString(36)}`,
version: "1",
source: "local",
...(index + 1 < count ? { dependencies: [index + 1] } : {}),
})),
});
expect(parsed.packages).toHaveLength(count);
expect(parsed.cyclic_dependency_indices).toEqual([]);
});
it("rejects out-of-range indices, multiple roots, and schema drift", () => {
expect(() =>
section({
packages: [
{ name: "a", version: "1", source: "local", dependencies: [2] },
],
}),
).toThrow(RustAuditableFormatFailure);
expect(() =>
section({
packages: [
{ name: "a", version: "1", source: "local", root: true },
{ name: "b", version: "1", source: "local", root: true },
],
}),
).toThrow(/more than one root/);
expect(() =>
section({
packages: [{ name: "a", version: "1", source: "local" }],
extra: true,
}),
).toThrow(/package schema/);
});
it("rejects invalid JSON, trailing zlib bytes, and an empty section", () => {
expect(() =>
readAuditableSection(".dep-v0", 0, deflateSync(Buffer.from("{"))),
).toThrow(/JSON is invalid/);
const trailed = Buffer.concat([
deflateSync(Buffer.from('{"packages":[]}')),
Buffer.from([0]),
]);
expect(() => readAuditableSection(".dep-v0", 0, trailed)).toThrow(
/zlib stream/,
);
expect(() => readAuditableSection(".dep-v0", 0, Buffer.alloc(0))).toThrow(
/empty/,
);
expect(() =>
readAuditableSection(".dep-v0", 0, Buffer.from([1, 2, 3])),
).toThrow(RustAuditableFormatFailure);
});
it("stops an inflate that exceeds the section ratio", () => {
const raw = Buffer.alloc(8000);
const compressed = deflateSync(raw);
expect(compressed.length * 64).toBeLessThan(raw.length);
expect(() => readAuditableSection(".dep-v0", 0, compressed)).toThrow(
RustAuditableResourceFailure,
);
expect(() =>
readAuditableSection(".dep-v0", 0, Buffer.alloc(RUST_SECTION_BYTES + 1)),
).toThrow(/1 MiB/);
});
it.each([{ packages: [] }, { packages: [{ future_package_field: true }] }])(
"rejects an unknown format before interpreting its packages: %j",
({ packages }) => {
expect(() => section({ format: 99, packages })).toThrow(
expect.objectContaining({
name: "RustAuditableFormatFailure",
kind: "unsupported",
message: "Unsupported cargo-auditable format version 99",
}),
);
},
);
it("rejects the first malformed package before schema issues accumulate", () => {
const stored = (document: unknown) =>
deflateSync(Buffer.from(JSON.stringify(document)), { level: 0 });
const emptyPackages = stored({
format: 1,
packages: Array.from({ length: 80_000 }, () => ({})),
});
expect(emptyPackages.length).toBeLessThan(RUST_SECTION_BYTES);
expect(() => readAuditableSection(".dep-v0", 0, emptyPackages)).toThrow(
/Dependency package 0 does not match the package schema/,
);
const invalidDependencies = stored({
format: 1,
packages: [
{
name: "a",
version: "1",
source: "local",
dependencies: Array.from({ length: 1_000 }, () => null),
},
],
});
expect(() => readAuditableSection(".dep-v0", 0, invalidDependencies)).toThrow(
/Dependency package 0 does not match the package schema/,
);
});
it("admits dependency expansion before allocating normalized or reverse edges", () => {
const document = {
packages: [
{
name: "a",
version: "1",
source: "local",
dependencies: Array<number>(400_000).fill(0),
},
],
};
// An uncompressed zlib stream clears the ratio guard while remaining below
// the one-MiB section limit, isolating the graph representation admission.
const payload = deflateSync(Buffer.from(JSON.stringify(document)), {
level: 0,
});
expect(payload.length).toBeLessThan(RUST_SECTION_BYTES);
expect(() => readAuditableSection(".dep-v0", 0, payload)).toThrow(
expect.objectContaining({
constraint: {
resource: "transport",
reported_limits: { maximum_output_bytes: 8 * 1024 * 1024 },
},
}),
);
});
+440
View File
@@ -0,0 +1,440 @@
import { deflateSync, inflateSync } from "node:zlib";
import { z } from "zod";
import {
RUST_AUDITABLE_FORMAT_VERSION,
cyclicDependencyIndices,
type RustAuditableMetadata,
type RustPackage,
} from "../domain/rust/rustBinary.js";
/** Selected-file admission limit shared with the stable artifact reader. */
export const RUST_BINARY_INPUT_BYTES = 256 * 1024 * 1024;
/** Compressed `.dep-v0` section limit. */
export const RUST_SECTION_BYTES = 1024 * 1024;
/** Inflated JSON may grow by at most this multiple of the section size. */
export const RUST_INFLATE_RATIO = 64;
/** Absolute inflated JSON limit, applied together with the ratio. */
export const RUST_UNCOMPRESSED_BYTES = 8 * 1024 * 1024;
/** Complete normalized report limit. */
export const RUST_OUTPUT_BYTES = 8 * 1024 * 1024;
const JSON_DEPTH = 32;
const packageSchema = z.strictObject({
name: z.string().min(1),
version: z.string().min(1),
source: z.string().min(1),
kind: z.enum(["runtime", "build"]).optional(),
dependencies: z.array(z.number().int().nonnegative()).optional(),
root: z.boolean().optional(),
});
const documentVersionSchema = z.object({
format: z.number().int().nonnegative().optional(),
});
const documentSchema = documentVersionSchema
.extend({
format: z.literal(RUST_AUDITABLE_FORMAT_VERSION).optional(),
packages: z.array(packageSchema),
})
.strict();
/** Malformed or unsupported dependency-section bytes. */
export class RustAuditableFormatFailure extends Error {
constructor(
readonly kind: "malformed" | "unsupported",
message: string,
options?: ErrorOptions,
) {
super(message, options);
this.name = "RustAuditableFormatFailure";
}
}
/** A resource admission limit with explicit semantic units. */
export type RustAuditableConstraint =
| {
readonly resource: "memory";
readonly reported_limits: { readonly maximum_bytes: number };
}
| {
readonly resource: "transport";
readonly reported_limits: { readonly maximum_output_bytes: number };
}
| {
readonly resource: "capacity";
readonly reported_limits: { readonly maximum_rustc_hashes: number };
};
/** Admission refused before retained output or graph expansion. */
export class RustAuditableResourceFailure extends Error {
constructor(
readonly constraint: RustAuditableConstraint,
message: string,
options?: ErrorOptions,
) {
super(message, options);
this.name = "RustAuditableResourceFailure";
}
}
/** Parsed section payload. Cycles are listed and are not walked. */
export const readAuditableSection = (
sectionName: RustAuditableMetadata["section_name"],
fileOffset: number,
bytes: Buffer,
): RustAuditableMetadata => {
if (bytes.length === 0)
throw new RustAuditableFormatFailure(
"malformed",
"Dependency section is empty",
);
if (bytes.length > RUST_SECTION_BYTES)
throw new RustAuditableResourceFailure(
{
resource: "memory",
reported_limits: { maximum_bytes: RUST_SECTION_BYTES },
},
"Dependency section exceeds the 1 MiB compressed limit",
);
const inflated = inflateSection(bytes);
const document = parseDocument(inflated);
const packages = normalizePackages(document.packages);
const root = rootIndex(packages);
return {
section_name: sectionName,
file_offset: fileOffset,
section_bytes: bytes.length,
compressed_bytes: bytes.length,
uncompressed_bytes: inflated.length,
format_version: document.format ?? null,
packages,
root_package_index: root,
dependency_edges: packages.flatMap((pkg, fromIndex) =>
pkg.dependencies.map((toIndex) => ({
from_index: fromIndex,
to_index: toIndex,
})),
),
cyclic_dependency_indices: cyclicDependencyIndices(packages),
};
};
const inflateSection = (bytes: Buffer): Buffer => {
const maximum = Math.min(
RUST_UNCOMPRESSED_BYTES,
bytes.length * RUST_INFLATE_RATIO,
);
try {
const inflated = inflateSync(bytes, {
maxOutputLength: maximum,
...(rejectsTrailingJunk() ? { rejectGarbageAfterEnd: true } : {}),
});
if (
!rejectsTrailingJunk() &&
shortestPrefix(bytes, inflated, maximum) !== bytes.length
)
throw new RustAuditableFormatFailure(
"malformed",
"Dependency section has bytes after the zlib stream",
);
return inflated;
} catch (cause) {
if (cause instanceof RustAuditableFormatFailure) throw cause;
throw inflateFailure(cause, maximum);
}
};
let trailingJunkRejected: boolean | undefined;
const rejectsTrailingJunk = (): boolean => {
if (trailingJunkRejected !== undefined) return trailingJunkRejected;
const trailed = Buffer.concat([
deflateSync(Buffer.from("a")),
Buffer.from([0]),
]);
try {
inflateSync(trailed, { rejectGarbageAfterEnd: true });
trailingJunkRejected = false;
} catch (cause) {
trailingJunkRejected =
cause instanceof Error &&
"code" in cause &&
cause.code === "ERR_TRAILING_JUNK_AFTER_STREAM_END";
}
return trailingJunkRejected;
};
const shortestPrefix = (
input: Buffer,
output: Buffer,
maximum: number,
): number => {
let low = 0;
let high = input.length;
while (low < high) {
const mid = Math.floor((low + high) / 2);
if (prefixMatches(input.subarray(0, mid), output, maximum)) high = mid;
else low = mid + 1;
}
return high;
};
const prefixMatches = (
prefix: Buffer,
output: Buffer,
maximum: number,
): boolean => {
try {
return inflateSync(prefix, { maxOutputLength: maximum }).equals(output);
} catch {
return false;
}
};
const inflateFailure = (cause: unknown, maximum: number): Error => {
const code =
cause instanceof Error && "code" in cause ? String(cause.code) : "";
if (code === "ERR_BUFFER_TOO_LARGE")
return new RustAuditableResourceFailure(
{ resource: "memory", reported_limits: { maximum_bytes: maximum } },
"Inflated dependency JSON exceeds the size limit",
);
if (
code === "ERR_TRAILING_JUNK_AFTER_STREAM_END" ||
code.startsWith("Z_") ||
code === "ERR_INVALID_ARG_TYPE"
)
return new RustAuditableFormatFailure(
"malformed",
"Dependency section is not a complete zlib stream",
);
return cause instanceof Error ? cause : new Error(String(cause));
};
const parseDocument = (bytes: Buffer): z.infer<typeof documentSchema> => {
let text: string;
try {
text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
} catch (cause) {
throw new RustAuditableFormatFailure(
"malformed",
"Dependency JSON is not valid UTF-8",
{ cause },
);
}
assertDepth(text);
let value: unknown;
try {
value = JSON.parse(text);
} catch (cause) {
throw new RustAuditableFormatFailure(
"malformed",
"Dependency section JSON is invalid",
{ cause },
);
}
const version = documentVersionSchema.safeParse(value);
if (!version.success)
throw new RustAuditableFormatFailure(
"malformed",
"Dependency JSON does not match the format schema",
{ cause: version.error },
);
if (
version.data.format !== undefined &&
version.data.format !== RUST_AUDITABLE_FORMAT_VERSION
)
throw new RustAuditableFormatFailure(
"unsupported",
`Unsupported cargo-auditable format version ${version.data.format}`,
);
admitGraphExpansion(value);
// One malformed package must fail before Zod retains an issue for every field.
rejectMalformedPackages(value);
const parsed = documentSchema.safeParse(value);
if (!parsed.success)
throw new RustAuditableFormatFailure(
"malformed",
"Dependency JSON does not match the package schema",
{ cause: parsed.error },
);
return parsed.data;
};
// These are lower bounds on the required normalized representation, not
// guessed per-object heap sizes. Count source arrays before Zod clones them,
// normalization builds edge objects, or SCC analysis creates reverse edges.
const MIN_PACKAGE_OUTPUT_BYTES = JSON.stringify({
name: "a",
version: "a",
source: "a",
kind: null,
dependencies: [],
root: true,
}).length;
const MIN_EDGE_OUTPUT_BYTES = JSON.stringify({
from_index: 0,
to_index: 0,
}).length;
const admitGraphExpansion = (value: unknown): void => {
if (
typeof value !== "object" ||
value === null ||
!("packages" in value) ||
!Array.isArray(value.packages)
)
return;
const packages: readonly unknown[] = value.packages;
let minimumBytes =
packages.length * MIN_PACKAGE_OUTPUT_BYTES +
Math.max(0, packages.length - 1);
let edges = 0;
const admit = () => {
const edgeBytes = edges * MIN_EDGE_OUTPUT_BYTES + Math.max(0, edges - 1);
if (minimumBytes + edgeBytes > RUST_OUTPUT_BYTES)
throw new RustAuditableResourceFailure(
{
resource: "transport",
reported_limits: { maximum_output_bytes: RUST_OUTPUT_BYTES },
},
"Dependency graph exceeds the complete report byte budget before expansion",
);
};
admit();
for (const pkg of packages) {
// Shape and index validity remain owned by documentSchema/rootIndex.
if (
typeof pkg !== "object" ||
pkg === null ||
!("dependencies" in pkg) ||
!Array.isArray(pkg.dependencies)
)
continue;
const count = pkg.dependencies.length;
edges += count;
minimumBytes += Math.max(0, count * 2 - 1);
admit();
}
};
const PACKAGE_FIELDS = new Set([
"name",
"version",
"source",
"kind",
"dependencies",
"root",
]);
const rejectMalformedPackages = (value: unknown): void => {
if (
typeof value !== "object" ||
value === null ||
!("packages" in value) ||
!Array.isArray(value.packages)
)
return;
for (const [index, pkg] of value.packages.entries()) {
if (!packageRecord(pkg))
throw new RustAuditableFormatFailure(
"malformed",
`Dependency package ${String(index)} does not match the package schema`,
);
}
};
const packageRecord = (
pkg: unknown,
): pkg is {
readonly dependencies?: readonly unknown[];
} => {
if (typeof pkg !== "object" || pkg === null || Array.isArray(pkg))
return false;
const record = pkg as Record<string, unknown>;
for (const key of Object.keys(record)) {
if (!PACKAGE_FIELDS.has(key)) return false;
}
if (
!nonEmptyString(record.name) ||
!nonEmptyString(record.version) ||
!nonEmptyString(record.source)
)
return false;
if (
record.kind !== undefined &&
record.kind !== "runtime" &&
record.kind !== "build"
)
return false;
if (record.root !== undefined && typeof record.root !== "boolean")
return false;
const dependencies = record.dependencies;
if (dependencies === undefined) return true;
return (
Array.isArray(dependencies) &&
dependencies.every(
(entry) =>
typeof entry === "number" && Number.isInteger(entry) && entry >= 0,
)
);
};
const nonEmptyString = (value: unknown): value is string =>
typeof value === "string" && value.length > 0;
const assertDepth = (text: string): void => {
let depth = 0;
let quoted = false;
let escaped = false;
for (const char of text) {
if (quoted) {
if (escaped) escaped = false;
else if (char === "\\") escaped = true;
else if (char === '"') quoted = false;
continue;
}
if (char === '"') {
quoted = true;
continue;
}
if (char === "{" || char === "[") {
depth += 1;
if (depth > JSON_DEPTH)
throw new RustAuditableFormatFailure(
"malformed",
"Dependency JSON nesting exceeds 32 levels",
);
} else if ((char === "}" || char === "]") && depth > 0) depth -= 1;
}
};
const normalizePackages = (
packages: z.infer<typeof documentSchema>["packages"],
): RustPackage[] =>
packages.map((pkg) => ({
name: pkg.name,
version: pkg.version,
source: pkg.source,
kind: pkg.kind ?? null,
dependencies: pkg.dependencies ?? [],
root: pkg.root ?? false,
}));
const rootIndex = (packages: readonly RustPackage[]): number | null => {
const roots = packages.flatMap((pkg, index) => (pkg.root ? [index] : []));
if (roots.length > 1)
throw new RustAuditableFormatFailure(
"malformed",
"Dependency JSON declares more than one root package",
);
for (const [index, pkg] of packages.entries()) {
const outside = pkg.dependencies.find(
(target) => target >= packages.length,
);
if (outside !== undefined)
throw new RustAuditableFormatFailure(
"malformed",
`Dependency index ${String(outside)} from package ${String(index)} is outside the package list`,
);
}
return roots[0] ?? null;
};
+181
View File
@@ -0,0 +1,181 @@
import {
GoBinaryFormatFailure,
GoBinaryResourceFailure,
type GoImageSection,
} from "../go/GoBinaryContainer.js";
import { readGoBinaryContainer } from "../go/GoBinaryImage.js";
import type {
RustAuditableMetadata,
RustBinary,
} from "../domain/rust/rustBinary.js";
import {
readAuditableSection,
RustAuditableFormatFailure,
RustAuditableResourceFailure,
} from "./RustAuditableReader.js";
const RUSTC_PREFIX = Buffer.from("/rustc/");
const HASH_BYTES = 40;
/** One toolchain normally leaves one hash; a small cap bounds retained output. */
const MAX_RUSTC_HASHES = 64;
/** Container identity plus optional dependency metadata and inferred rustc commits. */
export interface RustBinaryImage {
readonly format: RustBinary["format"];
readonly architecture: string;
readonly bits: 32 | 64;
readonly byte_order: RustBinary["byte_order"];
readonly auditable_metadata: RustAuditableMetadata | null;
readonly rustc_commit: RustBinary["rustc_commit"];
}
/** Read dependency metadata from container sections without executing the image. */
export const readRustBinaryImage = (bytes: Buffer): RustBinaryImage => {
const container = readContainer(bytes);
const dependency = dependencySection(container.format, container.sections);
return {
format: container.format,
architecture: container.architecture,
bits: container.bits,
byte_order: container.byte_order,
auditable_metadata:
dependency === null
? null
: readDependency(bytes, container.format, dependency),
rustc_commit: rustcCommit(bytes, container.sections),
};
};
const readContainer = (bytes: Buffer) => {
try {
return readGoBinaryContainer(bytes);
} catch (cause) {
if (cause instanceof GoBinaryFormatFailure)
throw new RustAuditableFormatFailure(cause.kind, cause.message, {
cause,
});
if (cause instanceof GoBinaryResourceFailure)
throw new RustAuditableResourceFailure(
{
resource: "memory",
reported_limits: { maximum_bytes: cause.maximum_bytes },
},
cause.message,
{
cause,
},
);
throw cause;
}
};
const dependencySection = (
format: RustBinaryImage["format"],
sections: readonly GoImageSection[],
): GoImageSection | null => {
const expected = format === "macho" ? "__DATA,.dep-v0" : ".dep-v0";
const matches = sections.filter((section) => section.name === expected);
if (matches.length > 1)
throw new RustAuditableFormatFailure(
"malformed",
"Image contains more than one dependency section",
);
const section = matches[0];
if (section === undefined) return null;
if (section.compressed)
throw new RustAuditableFormatFailure(
"unsupported",
"Compressed dependency sections are unsupported",
);
if (!section.file_backed)
throw new RustAuditableFormatFailure(
"malformed",
"Dependency section is not file-backed",
);
return section;
};
const readDependency = (
bytes: Buffer,
format: RustBinaryImage["format"],
section: GoImageSection,
): RustAuditableMetadata =>
readAuditableSection(
format === "macho" ? "__DATA,.dep-v0" : ".dep-v0",
section.offset,
bytes.subarray(section.offset, section.offset + section.size),
);
const rustcCommit = (
bytes: Buffer,
sections: readonly GoImageSection[],
): RustBinaryImage["rustc_commit"] => {
const hashes: { hash: string; file_offset: number }[] = [];
const seen = new Set<string>();
// Overlapping declared sections are scanned once, so total work stays
// bounded by the file size however many sections reference a range.
for (const range of mergedReadOnlyRanges(sections))
collectHashes(bytes, range, seen, hashes);
return hashes.length === 0 ? null : { evidence_kind: "inferred", hashes };
};
const mergedReadOnlyRanges = (
sections: readonly GoImageSection[],
): { readonly offset: number; readonly end: number }[] => {
const ranges = sections
.filter((section) => section.read_only && section.size > 0)
.map((section) => ({
offset: section.offset,
end: section.offset + section.size,
}))
.sort((left, right) => left.offset - right.offset);
const merged: { offset: number; end: number }[] = [];
for (const range of ranges) {
const last = merged.at(-1);
if (last !== undefined && range.offset <= last.end)
last.end = Math.max(last.end, range.end);
else merged.push({ ...range });
}
return merged;
};
const collectHashes = (
bytes: Buffer,
range: { readonly offset: number; readonly end: number },
seen: Set<string>,
hashes: { hash: string; file_offset: number }[],
): void => {
const region = bytes.subarray(range.offset, range.end);
let cursor = 0;
while (cursor < region.length) {
const at = region.indexOf(RUSTC_PREFIX, cursor);
if (at < 0) return;
const start = at + RUSTC_PREFIX.length;
const hash = hashAt(region, start);
if (hash !== null && !seen.has(hash)) {
if (seen.size >= MAX_RUSTC_HASHES)
throw new RustAuditableResourceFailure(
{
resource: "capacity",
reported_limits: { maximum_rustc_hashes: MAX_RUSTC_HASHES },
},
`Read-only data contains more than ${MAX_RUSTC_HASHES} distinct rustc commit paths.`,
);
seen.add(hash);
hashes.push({ hash, file_offset: range.offset + at });
}
cursor = at + 1;
}
};
const hashAt = (region: Buffer, start: number): string | null => {
const end = start + HASH_BYTES;
if (end >= region.length || region[end] !== 0x2f) return null;
for (let index = start; index < end; index += 1) {
const byte = region[index] ?? 0;
const hex =
(byte >= 0x30 && byte <= 0x39) || (byte >= 0x61 && byte <= 0x66);
if (!hex) return null;
}
return region.toString("latin1", start, end);
};
+216
View File
@@ -0,0 +1,216 @@
import { readStableArtifact } from "../artifacts/readStableArtifact.js";
import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js";
import type { ExecutionOptions } from "../application/AnalysisProvider.js";
import type { RustBinaryPort } from "../application/rust/RustBinaryPort.js";
import { AnalysisError } from "../domain/analysisErrorBase.js";
import {
AnalysisAccessDeniedError,
AnalysisArtifactChangedError,
AnalysisCancelledError,
AnalysisInputError,
AnalysisOutputError,
AnalysisResourceConstraintError,
AnalysisUnsupportedTargetError,
} from "../domain/analysisErrorCore.js";
import {
rustBinarySchema,
type InspectRustBinaryInput,
type RustBinary,
} from "../domain/rust/rustBinary.js";
import { ProviderAdapterError } from "../domain/providerAdapterError.js";
import { err, ok, type Result } from "../domain/result.js";
import { readRustBinaryImage } from "./RustBinaryImage.js";
import {
RUST_BINARY_INPUT_BYTES,
RUST_OUTPUT_BYTES,
RustAuditableFormatFailure,
RustAuditableResourceFailure,
} from "./RustAuditableReader.js";
/** Bundled reader identity. Embedded package versions are separate observed facts. */
export const RUST_BINARY_PROVIDER_IDENTITY = {
id: "rust-auditable",
name: "REA Rust dependency reader",
version: "1",
} as const;
const OPERATION = "inspect_rust_binary";
/** Stable in-process dependency inspection without subprocesses, temporary files, or a Rust toolchain. */
export class RustBinaryProvider implements RustBinaryPort {
readonly identity = RUST_BINARY_PROVIDER_IDENTITY;
/** Read one dependency section and any inferred rustc commit from the selected file. */
async inspect(
input: InspectRustBinaryInput,
options?: ExecutionOptions,
): Promise<Result<RustBinary, AnalysisError>> {
try {
const snapshot = await readStableArtifact(
input.path,
RUST_BINARY_INPUT_BYTES,
options?.signal,
);
options?.signal?.throwIfAborted();
const image = readRustBinaryImage(snapshot.bytes);
const report = {
...image,
artifact: {
path: input.path,
sha256: snapshot.sha256,
bytes: snapshot.bytes.length,
},
limitations: limitationsFor(image),
};
if (Buffer.byteLength(JSON.stringify(report), "utf8") > RUST_OUTPUT_BYTES)
throw outputLimit(input.path);
const parsed = rustBinarySchema.safeParse(report);
if (!parsed.success)
throw new AnalysisOutputError(
OPERATION,
"Rust reader produced an invalid dependency report.",
{ cause: parsed.error },
);
options?.signal?.throwIfAborted();
return ok(parsed.data);
} catch (cause: unknown) {
return err(failureFor(cause, input.path, options?.signal));
}
}
}
const outputLimit = (path: string) =>
new AnalysisResourceConstraintError(
OPERATION,
"transport",
`Complete Rust metadata for ${path} exceeds the inline output budget; no partial success is returned.`,
{ maximum_output_bytes: RUST_OUTPUT_BYTES },
);
const limitationsFor = (
image: ReturnType<typeof readRustBinaryImage>,
): string[] => [
"Embedded dependency metadata is file content. It does not authenticate package names, versions, sources, or the compiler that produced the binary.",
"Only the .dep-v0 dependency section and read-only compiler path strings are inspected. Functions, types, and runtime behavior are not recovered.",
"Resource guards bound the selected file to 256 MiB, a dependency section to 1 MiB, inflated JSON to the smaller of 8 MiB and 64 times the section size, and the complete report to 8 MiB. Package and dependency counts are admitted against that output budget before schema cloning, graph objects, or reverse-edge allocation.",
...(image.auditable_metadata === null
? [
"No .dep-v0 section was found. Absence of .dep-v0 does not mean the binary has no dependencies.",
]
: []),
...(image.auditable_metadata?.packages.some((pkg) => pkg.kind === null)
? [
"An omitted package kind is left null. The embedded format treats an omitted kind as runtime, and that default was not stored in the section.",
]
: []),
...(image.auditable_metadata !== null &&
image.auditable_metadata.cyclic_dependency_indices.length > 0
? [
"Dependency indices contain a cycle. The cycle is reported and was not traversed.",
]
: []),
...rustcLimitation(image.rustc_commit),
];
const rustcLimitation = (
commit: ReturnType<typeof readRustBinaryImage>["rustc_commit"],
): string[] => {
if (commit === null)
return [
"No /rustc/<40-hex>/ path was found in read-only data, so a rustc commit hash is unknown.",
];
if (commit.hashes.length === 1)
return [
"The rustc commit hash is inferred from a /rustc/<40-hex>/ path in read-only data. It does not by itself prove which compiler produced the binary.",
];
return [
"Read-only data contains more than one /rustc/<40-hex>/ path, so a single rustc commit is unknown.",
];
};
const failureFor = (
cause: unknown,
path: string,
signal?: AbortSignal,
): AnalysisError => {
if (cause instanceof AnalysisError) return cause;
if (signal?.aborted) return new AnalysisCancelledError(OPERATION);
if (cause instanceof RustAuditableResourceFailure)
return new AnalysisResourceConstraintError(
OPERATION,
cause.constraint.resource,
`Selected artifact ${path}: ${cause.message}`,
cause.constraint.reported_limits,
{ cause },
);
if (cause instanceof RustAuditableFormatFailure)
return cause.kind === "unsupported"
? new AnalysisUnsupportedTargetError(OPERATION, path, cause.message, {
cause,
})
: new AnalysisInputError(OPERATION, { cause }, [
{
path: ["path"],
reason: "invalid_format",
message: `Selected artifact ${path}: ${cause.message}`,
},
]);
return artifactFailure(cause, path);
};
const artifactFailure = (cause: unknown, path: string): AnalysisError => {
if (cause instanceof ArtifactReaderFailure)
return artifactReaderFailure(cause, path);
if (cause instanceof Error && "code" in cause) {
if (cause.code === "EACCES" || cause.code === "EPERM")
return new AnalysisAccessDeniedError(OPERATION, path, cause.code, {
cause,
});
if (
["ENOENT", "ENOTDIR", "ELOOP", "ENAMETOOLONG"].includes(
String(cause.code),
)
)
return new AnalysisInputError(OPERATION, { cause }, [
{
path: ["path"],
reason: "invalid_value",
message: `Selected artifact could not be read (${String(cause.code)}): ${path}.`,
},
]);
}
return new ProviderAdapterError(RUST_BINARY_PROVIDER_IDENTITY.id, OPERATION, {
cause,
diagnostics: {
path,
reason: cause instanceof Error ? cause.message : String(cause),
},
});
};
const artifactReaderFailure = (
cause: ArtifactReaderFailure,
path: string,
): AnalysisError => {
if (cause.reason === "integrity")
return new AnalysisArtifactChangedError(OPERATION, path, cause.message, {
cause,
});
if (cause.reason === "cancelled")
return new AnalysisCancelledError(OPERATION);
if (cause.reason === "limit")
return new AnalysisResourceConstraintError(
OPERATION,
"file-size",
cause.message,
{ maximum_input_bytes: RUST_BINARY_INPUT_BYTES },
{ cause },
);
if (cause.reason === "path")
return new AnalysisInputError(OPERATION, { cause }, [
{ path: ["path"], reason: "invalid_format", message: cause.message },
]);
return new ProviderAdapterError(RUST_BINARY_PROVIDER_IDENTITY.id, OPERATION, {
cause,
diagnostics: { path, reason: cause.message },
});
};
+11
View File
@@ -22,6 +22,9 @@ import { createRecordedCrashService } from "../composition/binaryDiagnostics.js"
import { createGoBinaryService } from "../composition/go.js";
import type { GoBinaryService } from "../application/go/GoBinaryService.js";
import { registerGoTools } from "./registerGoTools.js";
import { createRustBinaryService } from "../composition/rust.js";
import type { RustBinaryService } from "../application/rust/RustBinaryService.js";
import { registerRustTools } from "./registerRustTools.js";
import type { RecordedCrashService } from "../application/binaryDiagnostics/RecordedCrashService.js";
import { registerAnalysisViewTool } from "./registerAnalysisViewTool.js";
import { registerBinaryDiagnosticsTools } from "./registerBinaryDiagnosticsTools.js";
@@ -141,6 +144,7 @@ export interface CreateServerOptions {
readonly logger?: Logger;
readonly binaryLayout?: BinaryLayoutService;
readonly goBinary?: GoBinaryService;
readonly rustBinary?: RustBinaryService;
readonly recordedCrash?: RecordedCrashService;
readonly firmwareAnalysis?: FirmwareAnalysisPort;
readonly javascriptRecovery?: JavaScriptRecoveryPort;
@@ -582,6 +586,13 @@ const registerConfiguredAnalysisTools = (
recordEvidence,
withAdmittedAnalysis,
);
registerRustTools(
server,
options.rustBinary ?? createRustBinaryService(),
toolLogger,
recordEvidence,
withAdmittedAnalysis,
);
registerWasmTools(
server,
wasmArtifact,
+31
View File
@@ -0,0 +1,31 @@
import type { EvidenceMcpServer } from "./EvidenceMcpServer.js";
import type { RustBinaryService } from "../application/rust/RustBinaryService.js";
import type { EvidenceWriter } from "../application/investigation/InvestigationRecordPort.js";
import { toolContract } from "../contracts/toolContracts.js";
import type { Logger } from "pino";
import type { WithAdmittedAnalysis } from "./analysisAdmission.js";
import { createEvidenceToolHandler } from "./contractToolHandler.js";
import { toolRegistrationOptions } from "./toolRegistrationOptions.js";
/** Bind the exact Rust contract to its shared workflow and owning session Evidence ledger. */
export const registerRustTools = (
server: EvidenceMcpServer,
service: RustBinaryService,
logger: Logger,
recordEvidence?: EvidenceWriter["recordEvidence"],
withAdmittedAnalysis?: WithAdmittedAnalysis,
): void => {
const contract = toolContract("inspect_rust_binary");
const handler = createEvidenceToolHandler(
server,
(input, options) => service.inspect(input, options),
logger,
recordEvidence,
withAdmittedAnalysis,
);
server.registerTool(
contract.name,
toolRegistrationOptions(contract),
handler(contract),
);
};
+71
View File
@@ -0,0 +1,71 @@
import { deflateSync } from "node:zlib";
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { expect } from "vitest";
import { toolContract } from "../../src/contracts/toolContracts.js";
import { createRustImage } from "../fixtures/rust/image.js";
import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js";
import { cliTest } from "../support/cli/cliFixture.js";
cliTest(
"reports cargo-auditable packages through the public CLI",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-rust-cli-");
const path = join(root, "sample");
await writeFile(path, createRustImage({ format: "macho" }));
const result = await cli.run({
arguments: ["inspect-rust-binary", path, "--json"],
});
expect(result.exitCode, result.stderr).toBe(0);
const evidence = toolContract("inspect_rust_binary").outputSchema.parse(
result.json,
);
expect(evidence.normalized_result.auditable_metadata).toMatchObject({
section_name: "__DATA,.dep-v0",
format_version: 1,
root_package_index: 1,
packages: [
{ name: "itoa", version: "1.0.18" },
{ name: "sample", version: "0.1.0", root: true },
],
});
},
);
cliTest("returns null metadata when .dep-v0 is absent", async ({ cli }) => {
const root = await createTestTempDirectory("rea-rust-cli-missing-");
const path = join(root, "plain");
await writeFile(path, createRustImage({ format: "elf", omitSection: true }));
const result = await cli.run({
arguments: ["inspect-rust-binary", path, "--json"],
});
expect(result.exitCode, result.stderr).toBe(0);
const evidence = toolContract("inspect_rust_binary").outputSchema.parse(
result.json,
);
expect(evidence.normalized_result.auditable_metadata).toBeNull();
expect(evidence.normalized_result.limitations.join("\n")).toContain(
"does not mean the binary has no dependencies",
);
});
cliTest("reports malformed zlib as invalid input", async ({ cli }) => {
const root = await createTestTempDirectory("rea-rust-cli-zlib-");
const path = join(root, "broken.elf");
await writeFile(
path,
createRustImage({
format: "elf",
payload: deflateSync(Buffer.from("{")),
includeRustc: false,
}),
);
const result = await cli.run({
arguments: ["inspect-rust-binary", path, "--json"],
});
expect(result.exitCode).toBe(1);
expect(result.json).toMatchObject({
code: "invalid_request",
details: { operation: "inspect_rust_binary" },
});
});
+21
View File
@@ -6,6 +6,7 @@ import {
machoImage,
} from "../../../src/artifacts/apple/MachoImage.fixture.js";
import { readGoBinaryImage } from "../../../src/go/GoBinaryImage.js";
import { readGoMachoImage } from "../../../src/go/GoMachoImage.js";
import {
createGoBinaryFixture,
GO_MODULE_TEXT,
@@ -519,6 +520,26 @@ it("rejects build-info declared inside an unmapped Mach-O segment", () => {
expect(() => readGoBinaryImage(fixture.bytes)).toThrowError(/unmapped/i);
});
it("does not mark Mach-O section bytes outside their owning segment as read-only", () => {
const fixture = createGoBinaryFixture({ format: "macho" });
const command = 32;
const section = command + 72;
fixture.bytes.write("__TEXT", command + 8, "ascii");
fixture.bytes.writeBigUInt64LE(1024n, command + 48);
fixture.bytes.writeUInt32LE(1, command + 56);
fixture.bytes.writeUInt32LE(1, command + 60);
fixture.bytes.write("__debug_info", section, "ascii");
fixture.bytes.write("__TEXT", section + 16, "ascii");
fixture.bytes.writeBigUInt64LE(0x10c00n, section + 32);
fixture.bytes.writeBigUInt64LE(128n, section + 40);
fixture.bytes.writeUInt32LE(3072, section + 48);
const image = readGoMachoImage(fixture.bytes);
expect(image.sections[0]).toMatchObject({
file_backed: false,
read_only: false,
});
});
it.each(["elf", "pe", "macho"] as const)(
"refuses file aliases overlapping only part of a zero-filled %s module header",
(format) => {
@@ -1,6 +1,6 @@
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
import { expect, it } from "vitest";
import { writeFile } from "node:fs/promises";
import { mkdir, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { nativeAotPeFixture } from "../../fixtures/nativeaotPe.js";
@@ -13,7 +13,6 @@ import {
sessionEvidence,
} from "./ghidraMcpHarness.js";
import { spawn } from "node:child_process";
import { mkdir } from "node:fs/promises";
import { fileURLToPath } from "node:url";
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import { GhidraClient } from "../../../src/ghidra/GhidraClient.js";
+122
View File
@@ -0,0 +1,122 @@
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { expect } from "vitest";
import { toolContract } from "../../../src/contracts/toolContracts.js";
import { createServer } from "../../../src/server/createServer.js";
import { createTestBinarySession } from "../../fixtures/binarySession.js";
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
import {
createRustImage,
createRustHashOverflowImage,
} from "../../fixtures/rust/image.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { mcpTest, type TestMcp } from "../../support/mcp/mcpFixture.js";
const connect = async (mcp: TestMcp) => {
const session = createTestBinarySession(() => {
throw new Error("Rust metadata must not start a deep provider");
});
const client = await mcp.connect(createServer({ kind: "session", session }));
return { client, session };
};
mcpTest(
"advertises inspect_rust_binary and reads an embedded dependency list",
async ({ mcp }) => {
const { client, session } = await connect(mcp);
try {
const advertised = (await client.listTools()).tools.find(
(tool) => tool.name === "inspect_rust_binary",
);
expect(advertised?.annotations).toMatchObject({
readOnlyHint: false,
idempotentHint: true,
});
const root = await createTestTempDirectory("rea-rust-mcp-");
const path = join(root, "sample.elf");
await writeFile(path, createRustImage({ format: "elf" }));
const response = await client.callTool({
name: "inspect_rust_binary",
arguments: { path },
});
expect(response.isError, JSON.stringify(response.content)).not.toBe(true);
const evidence = toolContract("inspect_rust_binary").outputSchema.parse(
response.structuredContent,
);
expect(evidence.normalized_result.auditable_metadata?.packages).toEqual([
expect.objectContaining({ name: "itoa", version: "1.0.18" }),
expect.objectContaining({
name: "sample",
version: "0.1.0",
root: true,
}),
]);
const missing = join(root, "plain.elf");
await writeFile(
missing,
createRustImage({ format: "pe", omitSection: true }),
);
const absent = await client.callTool({
name: "inspect_rust_binary",
arguments: { path: missing },
});
expect(absent.isError).not.toBe(true);
const absentEvidence = toolContract(
"inspect_rust_binary",
).outputSchema.parse(absent.structuredContent);
expect(absentEvidence.normalized_result.auditable_metadata).toBeNull();
} finally {
await session.close();
}
},
);
mcpTest(
"requires an absolute path for inspect_rust_binary",
async ({ mcp }) => {
const { client, session } = await connect(mcp);
try {
const response = await client.callTool({
name: "inspect_rust_binary",
arguments: { path: "sample" },
});
expect(response.isError).toBe(true);
expect(parseMcpToolError(response).error).toMatchObject({
code: "invalid_request",
details: { operation: "inspect_rust_binary" },
});
} finally {
await session.close();
}
},
);
mcpTest(
"preserves rustc hash capacity units through the public error contract",
async ({ mcp }) => {
const { client, session } = await connect(mcp);
try {
const root = await createTestTempDirectory("rea-rust-mcp-");
const path = join(root, "many-hashes.elf");
await writeFile(path, createRustHashOverflowImage());
const response = await client.callTool({
name: "inspect_rust_binary",
arguments: { path },
});
expect(response.isError).toBe(true);
expect(parseMcpToolError(response).error).toMatchObject({
code: "resource_constraint",
details: {
operation: "inspect_rust_binary",
resource: "capacity",
reported_limits: { maximum_rustc_hashes: 64 },
},
});
expect(parseMcpToolError(response).error.details).not.toHaveProperty(
"reported_limits.maximum_bytes",
);
} finally {
await session.close();
}
},
);
+141
View File
@@ -0,0 +1,141 @@
import { expect, it } from "vitest";
import { fat } from "../../../src/domain/binaryTarget.fixture.js";
import {
createRustImage,
createRustHashOverflowImage,
RUSTC_HASH,
rustDependencyPayload,
} from "../../fixtures/rust/image.js";
import { readRustBinaryImage } from "../../../src/rust/RustBinaryImage.js";
import { RustAuditableFormatFailure } from "../../../src/rust/RustAuditableReader.js";
const packages = [
{
name: "itoa",
version: "1.0.18",
source: "crates.io",
kind: null,
dependencies: [],
root: false,
},
{
name: "sample",
version: "0.1.0",
source: "local",
kind: null,
dependencies: [0],
root: true,
},
];
it.each(["elf", "pe"] as const)(
"reads a .dep-v0 section and an inferred rustc hash from %s",
(format) => {
const image = readRustBinaryImage(createRustImage({ format }));
expect(image.format).toBe(format);
expect(image.auditable_metadata).toMatchObject({
section_name: ".dep-v0",
file_offset: 1024,
section_bytes: rustDependencyPayload().length,
format_version: 1,
root_package_index: 1,
packages,
dependency_edges: [{ from_index: 1, to_index: 0 }],
});
expect(image.rustc_commit).toEqual({
evidence_kind: "inferred",
hashes: [{ hash: RUSTC_HASH, file_offset: 2048 }],
});
},
);
it("reads a Mach-O __DATA,.dep-v0 section", () => {
const image = readRustBinaryImage(createRustImage({ format: "macho" }));
expect(image).toMatchObject({
format: "macho",
architecture: "arm64",
auditable_metadata: {
section_name: "__DATA,.dep-v0",
file_offset: 1024,
root_package_index: 1,
packages,
},
rustc_commit: {
evidence_kind: "inferred",
hashes: [{ hash: RUSTC_HASH, file_offset: 2048 }],
},
});
});
it("ignores PE file-alignment padding after the dependency content", () => {
const bytes = createRustImage({ format: "pe", includeRustc: false });
const payloadBytes = rustDependencyPayload().length;
const entry = 264;
expect(bytes.readUInt32LE(entry + 8)).toBe(payloadBytes);
bytes.writeUInt32LE(512, entry + 16);
const image = readRustBinaryImage(bytes);
expect(image.auditable_metadata).toMatchObject({
section_name: ".dep-v0",
section_bytes: payloadBytes,
packages,
});
});
it("returns null metadata when the dependency section is absent", () => {
const image = readRustBinaryImage(
createRustImage({ format: "elf", omitSection: true }),
);
expect(image.auditable_metadata).toBeNull();
expect(image.rustc_commit?.hashes).toEqual([
{ hash: RUSTC_HASH, file_offset: 2048 },
]);
});
it("refuses a universal Mach-O container", () => {
expect(() => readRustBinaryImage(fat([0x0100000c]))).toThrow(
RustAuditableFormatFailure,
);
expect(() => readRustBinaryImage(fat([0x0100000c]))).toThrow(/thin slice/);
});
it("does not read SHT_NOBITS dependency sections as stored bytes", () => {
const bytes = createRustImage({ format: "elf" });
// The fixture writes the .dep-v0 section header at byte 384.
bytes.writeUInt32LE(8, 384 + 4);
let metadata: unknown;
try {
metadata = readRustBinaryImage(bytes).auditable_metadata;
} catch (cause: unknown) {
expect(cause).toBeInstanceOf(RustAuditableFormatFailure);
return;
}
expect(metadata).toBeNull();
});
it("reads a rustc path from a read-only ELF section whose name exceeds 64 bytes", () => {
const bytes = createRustImage({ format: "elf", omitSection: true });
// omitSection places the rustc section header at 384 and shstrtab at 320.
const nameOffset = bytes.readUInt32LE(384);
const tableOffset = Number(bytes.readBigUInt64LE(320 + 24));
const longName = Buffer.concat([Buffer.alloc(65, 0x61), Buffer.from([0])]);
longName.copy(bytes, tableOffset + nameOffset);
bytes.writeBigUInt64LE(BigInt(nameOffset + longName.length), 320 + 32);
const image = readRustBinaryImage(bytes);
expect(image.auditable_metadata).toBeNull();
expect(image.rustc_commit).toEqual({
evidence_kind: "inferred",
hashes: [{ hash: RUSTC_HASH, file_offset: 2048 }],
});
});
it("refuses read-only data with more distinct rustc paths than it retains", () => {
const bytes = createRustHashOverflowImage();
expect(() => readRustBinaryImage(bytes)).toThrow(
expect.objectContaining({
constraint: {
resource: "capacity",
reported_limits: { maximum_rustc_hashes: 64 },
},
}),
);
});
+263
View File
@@ -0,0 +1,263 @@
import { deflateSync } from "node:zlib";
/** zlib payload matching the embedded dependency JSON shape. */
export const rustDependencyPayload = (
document: unknown = {
packages: [
{ name: "itoa", version: "1.0.18", source: "crates.io" },
{
name: "sample",
version: "0.1.0",
source: "local",
dependencies: [0],
root: true,
},
],
format: 1,
},
): Buffer => deflateSync(Buffer.from(JSON.stringify(document)));
export const RUSTC_HASH = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
const RUSTC_PATH = `/rustc/${RUSTC_HASH}/library/core`;
/** Synthetic ELF, PE, or thin Mach-O with an optional dependency section. */
export const createRustImage = (options: {
readonly format: "elf" | "pe" | "macho";
readonly payload?: Buffer;
readonly omitSection?: boolean;
readonly includeRustc?: boolean;
}): Buffer => {
const payload = options.payload ?? rustDependencyPayload();
const rustc = options.includeRustc === false ? null : Buffer.from(RUSTC_PATH);
const bytes = Buffer.alloc(4096);
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
const little = (offset: number, value: number, width: 16 | 32 | 64) => {
if (width === 16) view.setUint16(offset, value, true);
else if (width === 32) view.setUint32(offset, value, true);
else view.setBigUint64(offset, BigInt(value), true);
};
const image = {
bytes,
little,
payload,
omitSection: options.omitSection === true,
rustc,
};
if (options.format === "elf") writeElf(image);
else if (options.format === "pe") writePe(image);
else writeMacho(image);
return bytes;
};
interface RustImageWrite {
readonly bytes: Buffer;
readonly little: (offset: number, value: number, width: 16 | 32 | 64) => void;
readonly payload: Buffer;
readonly omitSection: boolean;
readonly rustc: Buffer | null;
}
const writeElf = ({
bytes,
little,
payload,
omitSection,
rustc,
}: RustImageWrite): void => {
bytes.set([0x7f, 0x45, 0x4c, 0x46, 2, 1, 1]);
little(16, 2, 16);
little(18, 62, 16);
little(20, 1, 32);
little(32, 128, 64);
little(40, 256, 64);
little(52, 64, 16);
little(54, 56, 16);
little(56, 1, 16);
little(58, 64, 16);
const names = omitSection
? Buffer.from("\0.shstrtab\0.rodata\0")
: Buffer.from("\0.shstrtab\0.dep-v0\0.rodata\0");
little(60, rustc === null && omitSection ? 0 : omitSection ? 3 : 4, 16);
little(62, rustc === null && omitSection ? 0 : 1, 16);
little(128, 1, 32);
little(132, 6, 32);
little(144, 0x400000, 64);
little(160, bytes.length, 64);
little(168, bytes.length, 64);
if (rustc === null && omitSection) return;
names.copy(bytes, 768);
const shstr = 320;
little(shstr, 1, 32);
little(shstr + 4, 3, 32);
little(shstr + 24, 768, 64);
little(shstr + 32, names.length, 64);
let next = 384;
if (!omitSection) {
little(next, 11, 32);
little(next + 4, 1, 32);
little(next + 24, 1024, 64);
little(next + 32, payload.length, 64);
payload.copy(bytes, 1024);
next += 64;
}
if (rustc !== null) {
const name = omitSection ? 11 : 19;
little(next, name, 32);
little(next + 4, 1, 32);
little(next + 8, 2, 64);
little(next + 16, 0x400000 + 2048, 64);
little(next + 24, 2048, 64);
little(next + 32, rustc.length, 64);
rustc.copy(bytes, 2048);
}
};
const writePe = ({
bytes,
little,
payload,
omitSection,
rustc,
}: RustImageWrite): void => {
bytes.write("MZ", 0, "ascii");
little(60, 128, 32);
bytes.write("PE\0\0", 128, "ascii");
little(132, 0x8664, 16);
const count = Number(!omitSection) + Number(rustc !== null);
little(134, count, 16);
little(148, 112, 16);
little(152, 0x20b, 16);
little(176, 0x140000000, 64);
const writeSection = (section: {
readonly entry: number;
readonly name: string;
readonly offset: number;
readonly body: Buffer;
readonly address: number;
}) => {
bytes.write(section.name, section.entry, "ascii");
little(section.entry + 8, section.body.length, 32);
little(section.entry + 12, section.address, 32);
little(section.entry + 16, section.body.length, 32);
little(section.entry + 20, section.offset, 32);
little(section.entry + 36, 0x40000040, 32);
section.body.copy(bytes, section.offset);
};
let entry = 264;
if (!omitSection) {
writeSection({
entry,
name: ".dep-v0",
offset: 1024,
body: payload,
address: 0x1000,
});
entry += 40;
}
if (rustc !== null)
writeSection({
entry,
name: ".rdata",
offset: 2048,
body: rustc,
address: 0x2000,
});
};
const writeMacho = ({
bytes,
little,
payload,
omitSection,
rustc,
}: RustImageWrite): void => {
const commands = Number(rustc !== null) + Number(!omitSection);
little(0, 0xfeedfacf, 32);
little(4, 0x0100000c, 32);
little(12, 2, 32);
little(16, commands, 32);
little(20, commands * 152, 32);
let command = 32;
if (rustc !== null) {
writeMachoSegment({
bytes,
little,
command,
segment: "__TEXT",
section: "__cstring",
body: rustc,
fileOffset: 2048,
protection: 5,
flags: 2,
});
command += 152;
}
if (!omitSection)
writeMachoSegment({
bytes,
little,
command,
segment: "__DATA",
section: ".dep-v0",
body: payload,
fileOffset: 1024,
protection: 3,
flags: 0,
});
};
const writeMachoSegment = ({
bytes,
little,
command,
segment,
section,
body,
fileOffset,
protection,
flags,
}: {
readonly bytes: Buffer;
readonly little: RustImageWrite["little"];
readonly command: number;
readonly segment: string;
readonly section: string;
readonly body: Buffer;
readonly fileOffset: number;
readonly protection: number;
readonly flags: number;
}): void => {
little(command, 0x19, 32);
little(command + 4, 152, 32);
bytes.write(segment, command + 8, "ascii");
little(command + 24, 0x100000000, 64);
little(command + 32, 0x2000, 64);
little(command + 40, fileOffset, 64);
little(command + 48, body.length, 64);
little(command + 56, protection, 32);
little(command + 60, protection, 32);
little(command + 64, 1, 32);
const header = command + 72;
bytes.write(section, header, "ascii");
bytes.write(segment, header + 16, "ascii");
little(header + 32, 0x100000000, 64);
little(header + 40, body.length, 64);
little(header + 48, fileOffset, 32);
little(header + 64, flags, 32);
body.copy(bytes, fileOffset);
};
/** Read-only ELF data exceeding the distinct rustc-hash retention capacity. */
export const createRustHashOverflowImage = (): Buffer => {
const bytes = createRustImage({ format: "elf", omitSection: true });
// Without .dep-v0 the read-only section header is the second one, at 384.
// Section names end before byte 800, so the hash block starts there.
bytes.writeBigUInt64LE(800n, 384 + 24);
bytes.writeBigUInt64LE(3296n, 384 + 32);
bytes.fill(0, 800);
for (let index = 0; index < 65; index += 1) {
const hash = index.toString(16).padStart(40, "0");
bytes.write(`/rustc/${hash}/`, 800 + index * 48, "latin1");
}
return bytes;
};
@@ -41,6 +41,9 @@ it
}
// A rejection must come from the fixture we are about to launch. An
// existing Xvfb would otherwise mask a broken executable-identity check.
// The copied launcher has a unique file identity and has not run yet.
// The synthetic proc root isolates the positive identity check from
// unrelated restricted host processes while retaining display-name checks.
await assertCleanLinuxHopperRuntime(
alternateMode ? [launcher] : [],
procRoot,
+1 -1
View File
@@ -51,7 +51,7 @@ const TEST_PROJECTS = [
name: "adapters",
include: [
"src/*.test.ts",
"src/{apktool,jeb,adb,artifacts,browser,dotnet,evm,firmware,flutter,pcap,ghidra,go,hopper,ida,inspector,javascript,native,process,reference,server,wasm,windows}/**/*.test.ts",
"src/{apktool,jeb,adb,artifacts,browser,dotnet,evm,firmware,flutter,pcap,ghidra,go,hopper,ida,inspector,javascript,native,process,reference,rust,server,wasm,windows}/**/*.test.ts",
],
pool: "forks" as const,
maxWorkers: MAX_TEST_WORKERS,