feat(apple): inspect IPS crash reports (#6511)

* feat(apple): inspect IPS crash reports

Add inspect_apple_ips for caller-selected Apple .ips files. Parse the
first-line metadata object and, when bug_type is the string 309, the
crash body: process identity, exception and termination facts, threads,
usedImages, lossless integer registers, and documented classification
rules. Other bug types including 288 stackshots return no crash result.
Opened Mach-O and dSYM UUID joins remain unobserved.

Related to #4297

* fix(apple): keep IPS register literals exact

Use the arm64e fixture integer that matches 0x8527000193ed4944 instead
of an IEEE-754-rounded decimal, and count the lossless JSON example
length from the source text.

* fix(apple): budget IPS parsing and keep typed presence

Parse the IPS metadata and crash body through a projection of the
documented members. Members the report does not interpret are
syntax-checked without being materialized, and thread, frame, image and
the new max_registers budgets run before each row is parsed, so a large
selected file fails with a typed resource error instead of first
expanding every row. Fixed allowances bound JSON nesting, unlisted key
names and termination reasons.

Present values whose JSON type differs from the documented one are no
longer read as absence: they are listed in coverage.type_mismatches and
coverage becomes partial, including wrong-type or missing threads and
usedImages collections. osVersion keeps the producer dictionary or the
string form. Frame roles follow physical frame depth, so the top frame
of a non-faulting thread is current_instruction and inlined rows share
the following frame's role. SIGKILL classes compare the canonical hex
once, so 0xdeadd0cc no longer matches dead10cc. Invalid UTF-8 is refused
instead of being replaced, and JSON strings are decoded by copying
unescaped spans.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ra7LM2NyQ7DumdssxbUq5g

* fix(apple): bound integer text expansion and reconcile coverage

* refactor(apple): retain image metadata at its owning row

* refactor(apple): validate row capacity through reconciled coverage

* test(apple): verify resource recovery at the public error boundary

* docs(apple): record complete IPS task and corpus measurements

* docs(apple): record integrated checks and stdio corpus evidence

* docs(apple): record PE integration and baseline cleanup controls

* fix(native): bound IPS string decoding and preserve invalid identities

* refactor(native): use exact optional IPS source evidence

* docs(native): verify IPS escape capacity and complete-task costs

* test(native): verify IPS through production stdio MCP

* style(tests): format IPS advertised schema bindings

* fix(native): preserve unresolved IPS references and recorded frame semantics

* docs(native): record IPS reference review evidence

* fix(apple-ips): enforce JSON and report consistency

* docs(apple-ips): record final consistency verification

---------

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
This commit is contained in:
Matt Van Horn
2026-10-24 01:02:59 -07:00
committed by GitHub
parent 977c9727e6
commit 781760a195
61 changed files with 4520 additions and 2 deletions
@@ -265,6 +265,14 @@ explicit path or endpoint and do not need it.
build-id or ABI-tag fields. A missing table is observed absence of those
declarations, not proof the image never carried notes. See the
[ELF notes guide](https://github.com/morluto/rea/blob/main/docs/elf-notes.md).
- Apple `.ips` crash reports: `inspect_apple_ips` with the explicit local
file. This built-in target-free inspection requires no Xcode, atos, or
pwntools. Preserve first-line metadata, `bug_type` as a string, process and
exception facts, lossless integer registers, usedImages UUIDs, and
documented classification matches. Only `bug_type` `"309"` yields a crash
body; other types, including `"288"` stackshots, return no crash result.
Opened Mach-O and dSYM UUID joins remain unobserved. See the
[Apple IPS guide](https://github.com/morluto/rea/blob/main/docs/apple-ips.md).
- CPython 3.7–3.13 `.pyc` header, nested marshal code objects and bytecode
identity: `inspect_python_pyc` with the explicit local file. This built-in
target-free inspection requires no Python installation. Preserve magic,
@@ -136,6 +136,18 @@ absence of those declarations, not proof the image never carried notes in
another container. See
[ELF note tables](https://github.com/morluto/rea/blob/main/docs/elf-notes.md).
## Apple IPS crash reports
Use `inspect_apple_ips` for an explicit local Apple `.ips` file when the
question concerns crash metadata, exception and termination facts, threads,
usedImages, lossless registers, or documented crash classes. No `open_binary`,
Xcode, atos, or target execution is required. The CLI equivalent is
`inspect-apple-ips PATH`. Preserve artifact SHA-256, `bug_type` as a string,
uppercase image UUIDs, and integer literals that exceed IEEE-754 range. Only
`bug_type` `"309"` yields a crash body; other DiagnosticReports types return
no crash result. Opened Mach-O and dSYM UUID joins remain unobserved. See
[Apple IPS crash reports](https://github.com/morluto/rea/blob/main/docs/apple-ips.md).
## CPython pyc files
Use `inspect_python_pyc` for an explicit local CPython 3.7–3.13 `.pyc` when the
+1
View File
@@ -194,6 +194,7 @@ Additional tools and host support depend on the target:
| PE certificates | Authenticode WIN_CERTIFICATE entries and PKCS#7 signer/X.509 identity fields | Built-in static PE inspection; [PE certificates](docs/pe-certificates.md) |
| PE imports | Import and delay-load DLL names, ordinals, IAT RVAs and pefile import hash | Built-in static PE inspection; [PE imports](docs/pe-imports.md) |
| ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](docs/elf-notes.md) |
| Apple IPS crashes | .ips metadata, threads, images, registers and documented crash classes | Built-in static JSON inspection; [Apple IPS](docs/apple-ips.md) |
| Erlang BEAM | IFF chunks, atoms, exports, imports, lambdas and the Code header | Built-in static BEAM inspection; [Erlang BEAM](docs/erlang-beam.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) |
+1
View File
@@ -35,6 +35,7 @@ export default defineConfig({
{ text: "PE certificates", link: "/pe-certificates" },
{ text: "PE imports", link: "/pe-imports" },
{ text: "ELF notes", link: "/elf-notes" },
{ text: "Apple IPS crash reports", link: "/apple-ips" },
{ text: "CPython pyc files", link: "/python-pyc" },
{ text: "Erlang BEAM", link: "/erlang-beam" },
{
+105
View File
@@ -0,0 +1,105 @@
# Apple IPS crash reports
`inspect_apple_ips` / `inspect-apple-ips` answers whether one selected local
Apple `.ips` file is a crash report (`bug_type` string `"309"`), which process,
exception, and termination facts that report records, which threads and
`usedImages` rows it lists, and which documented crash classes its strings
match. It reads the file without executing a target and does not require Xcode,
atos, a disassembler, or pwntools.
```bash
rea inspect-apple-ips /absolute/path/to/report.ips --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_apple_ips",
"arguments": { "path": "/artifacts/report.ips" }
}
```
## Observations
The inline Evidence result contains the artifact path, SHA-256, and byte length.
`metadata` is the first-line JSON object. `bug_type` stays a string. The crash
body is parsed only when that string is `"309"`. Other types, including `"288"`
stackshots, return no crash result.
`process`, `exception`, `termination`, `faulting_thread`, `threads`, and
`images` reproduce documented body fields when present. Integers keep their
decimal literals and a hexadecimal form, including arm64e registers above
2^53. Frame roles follow the recorded frame index: row zero of the resolved
faulting thread is `faulting_instruction`, row zero of other threads is
`current_instruction`, and later rows are `return_address`. Each runtime
address uses that row's own image base and offset. The recorded `inline` flag
does not supply a missing offset or authorize borrowing another row's address.
When `faultingThread` is missing or does not resolve to a recorded thread
object, row-zero roles are `unknown`; the recorded lookup remains available.
`process.os_version` keeps the producer's dictionary (`train`, `build`,
`release_type`, `is_embedded`) or the older string form. Image UUIDs are uppercase 8-4-4-4-12; an all-zero UUID
or a missing `arch` is `uuid_unknown`. Image `source` letters keep their
documented meaning (`P` process, `S` shared cache, `C` shared cache library,
`K` kernel, `U` kernel cache, `T` kernel text exec, `A` absolute). Keys the
documentation does not list remain in `unlisted_keys` or `unlisted_body_keys`.
Each frame's `image_index` refers to its entry in `images`. Image architecture,
UUID and `uuid_unknown` are retained once on that image. Resolve the reference
when using those facts; an absent or out-of-range index leaves the image
unknown. This avoids repeating image text for every stack frame.
A present value whose JSON type differs from the documented one is never read
as absent: it is listed in `coverage.type_mismatches` with its path, the
expected type, and the observed type, and `coverage.status` becomes
`partial`. A crash body without `threads` or `usedImages` is also `partial`.
Malformed image UUID strings likewise make coverage partial; their exact spelling
is retained in the mismatch's `recorded_value`, while the canonical image UUID
remains unknown. Both 32 hexadecimal digits and canonical hyphenated UUIDs are accepted.
Unresolved thread/image references also make coverage partial. A rejected
integer lookup keeps its exact decimal literal in `recorded_integer`, including
negative values and values above uint32. Valid uint32 lookup values remain in
`faulting_thread` or `image_index` even when the referenced row is unavailable.
`classification` matches only documented SIGKILL codes (compared by their
canonical hexadecimal form), `EXC_GUARD` subtypes,
pointer-authentication suffixes, and dyld missing-library strings. A report
that matches none of those rules is `unclassified`.
Opened Mach-O and dSYM UUID joins remain unobserved. Symbol names printed
inside the report are recorded claims and are not verified. Linux ELF cores
use the separate [recorded crash](recorded-crashes.md) workflow.
## Limits and failures
The selected file is limited to 1 MiB by default (16 MiB maximum). At most 256
threads, 8192 frames, 1024 `usedImages` rows, and 16384 `threadState`
register slots are examined by default (4096 / 65536 / 4096 / 262144
maximum). Budgets are checked before each row is parsed, and body members the
report does not interpret are syntax-checked without being kept. Fixed
allowances bound JSON nesting (64 levels), unlisted key names per object
(1024), and `termination.reasons` entries (256). Malformed metadata, a
truncated crash body, invalid UTF-8, a NUL in the text, and exhausted budgets
fail without partial success.
`max_integer_text_bytes` (`--max-integer-text-bytes` in the CLI) separately
bounds retained decimal and hexadecimal integer text, including every derived
runtime address and rejected lookup literal. Its default is 8 MiB of UTF-16 text storage and its maximum is
64 MiB. Each appearance counts, even when an object could be shared in memory
but would be serialized more than once. The parser reserves a conservative
upper bound before numeric conversion or address expansion, then releases the
unused reservation after each value. It reuses the parsed source integer when
the same image base contributes to many frames.
Raise this per-call allowance on the next call when more room is needed; no
restart is required. File-size and row budgets do not raise it. This allowance
is separate from JSON wire size, MCP response limits and the client's receive
buffer. An exhausted allowance reports a typed resource error without partial
success or truncated integer facts.
```bash
rea inspect-apple-ips /absolute/path/to/report.ips --max-integer-text-bytes 16777216 --json
```
This workflow does not execute a target, launch atos, or search for dSYMs.
+11
View File
@@ -311,6 +311,17 @@ The workflow reports note owners, types, descriptor hashes, and GNU build-id
or ABI-tag fields when those descriptors match. A missing note table is
observed absence of those declarations. See [ELF note tables](elf-notes.md).
Inspect an Apple `.ips` crash report without executing a target:
```bash
rea inspect-apple-ips /absolute/path/to/report.ips --json
```
The workflow reports first-line metadata, process/exception/termination facts,
threads, usedImages, lossless integer registers, and documented crash classes
when `bug_type` is `"309"`. Other DiagnosticReports types return no crash
result. See [Apple IPS crash reports](apple-ips.md).
Inspect a CPython 3.7–3.13 `.pyc` header and nested marshal code objects
without executing bytecode or installing Python:
+1 -1
View File
@@ -50,7 +50,7 @@ For runtime instrumentation, see the [Frida guide](./frida-instrumentation.md).
| Task | Guides |
| --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Identify a desktop installation | [Runtime candidates, evidence and follow-up files](desktop-applications.md) |
| Inspect native functions, references, or types | [Native binaries](native-investigation.md), [Go metadata](go-binaries.md), [Rust dependencies](rust-binaries.md), [CPython pyc files](python-pyc.md), [PE certificates](pe-certificates.md), [PE imports](pe-imports.md), [ELF notes](elf-notes.md), [Erlang BEAM](erlang-beam.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), [CPython pyc files](python-pyc.md), [PE certificates](pe-certificates.md), [PE imports](pe-imports.md), [ELF notes](elf-notes.md), [Apple IPS](apple-ips.md), [Erlang BEAM](erlang-beam.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), [Kuna](kuna-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), [Electron fuses](electron-fuses.md) |
+1
View File
@@ -159,6 +159,7 @@ npx rea-agents@latest setup
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
| ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](../elf-notes.md) |
| Erlang BEAM | IFF chunks, atoms, exports, and tables | Built-in static BEAM inspection; [Erlang BEAM](../erlang-beam.md) |
| Apple IPS crashes | .ips metadata, threads, images, registers and crash classes | Built-in static JSON inspection; [Apple IPS](../apple-ips.md) |
| أعطال Linux المسجّلة | سجلات note الخام، ومسجّلات المعالج/الإشارات لكل خيط مسجّل، وترشيحات اختيارية لخرائط الذاكرة | pwntools يوفّره المستدعي على Linux x64؛ GDB/pwndbg اختياريان؛ [الأعطال المسجّلة](../recorded-crashes.md) |
| تثبيتات سطح المكتب | عائلات وقت التشغيل، والملفات الداعمة، وعلامات المكونات، والخطوات التالية | فحص محدود للملفات والترويسات;[تطبيقات سطح المكتب](../desktop-applications.md) |
| JavaScript / Electron | الوحدات، وعمليات الاستيراد، وخرائط المصدر، والمسارات، وIPC، وعلاقات إضافات الشيفرة الأصلية | Node.js وnpm؛ [تحليل التطبيقات](../javascript-application-workflows.md); Linux: procps `ps` |
+1
View File
@@ -159,6 +159,7 @@ REA benötigt Node.js 22.x (>=22.23.4), 24.x (>=24.22.1) oder 26+ (>=26.12.1) so
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
| ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](../elf-notes.md) |
| Erlang BEAM | IFF chunks, atoms, exports, and tables | Built-in static BEAM inspection; [Erlang BEAM](../erlang-beam.md) |
| Apple IPS crashes | .ips metadata, threads, images, registers and crash classes | Built-in static JSON inspection; [Apple IPS](../apple-ips.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) |
| Desktop-Installationen | Laufzeitkandidaten, Quelldateien, Komponenten und nächste Schritte | Begrenzte Verzeichnis- und Headerprüfung;[Desktop-Anwendungen](../desktop-applications.md) |
| JavaScript / Electron | Module, Imports, Source Maps, Routen, IPC und Beziehungen zu nativen Add-ons | Node.js und npm; [Anwendungsanalyse](../javascript-application-workflows.md); Linux: procps `ps` |
+1
View File
@@ -158,6 +158,7 @@ npx rea-agents@latest setup
| PE certificates | Authenticode WIN_CERTIFICATE entries and PKCS#7 signer/X.509 identity fields | Built-in static PE inspection; [PE certificates](../pe-certificates.md) |
| PE imports | Import and delay-load DLL names, ordinals, IAT RVAs and pefile import hash | Built-in static PE inspection; [PE imports](../pe-imports.md) |
| ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](../elf-notes.md) |
| Apple IPS crashes | .ips metadata, threads, images, registers and crash classes | Built-in static JSON inspection; [Apple IPS](../apple-ips.md) |
| Erlang BEAM | IFF chunks, atoms, exports, and tables | Built-in static BEAM inspection; [Erlang BEAM](../erlang-beam.md) |
| Αρχείο WASM | Έλεγχο εγκυρότητας, ενότητες, ακριβείς μορφές εισαγωγών/εξαγωγών σε WAT και αποκωδικοποιημένο WAT | WABT 1.0.42 που παρέχει ο χρήστης· `inspect_wasm_artifact` |
| Bytecode EVM | Selectors επιλογής συνάρτησης, μετατοπίσεις σε bytes, εκτιμώμενα ορίσματα και δυνατότητα μεταβολής της κατάστασης | Τοπική είσοδος σε ακατέργαστα bytes ή δεκαεξαδική μορφή σε Linux x64· [οδηγός EVM χωρίς εκτέλεση](../evm-bytecode.md) |
+1
View File
@@ -159,6 +159,7 @@ REA requiere Node.js 22.x (>=22.23.4), 24.x (>=24.22.1) o 26+ (>=26.12.1), adem
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
| ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](../elf-notes.md) |
| Erlang BEAM | IFF chunks, atoms, exports, and tables | Built-in static BEAM inspection; [Erlang BEAM](../erlang-beam.md) |
| Apple IPS crashes | .ips metadata, threads, images, registers and crash classes | Built-in static JSON inspection; [Apple IPS](../apple-ips.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) |
| Instalaciones de escritorio | Posibles entornos de ejecución, archivos de evidencia, componentes y siguientes pasos | Inspección acotada de directorios y cabeceras;[Aplicaciones de escritorio](../desktop-applications.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](../javascript-application-workflows.md); Linux: procps `ps` |
+1
View File
@@ -150,6 +150,7 @@ REA به Node.js 22.x (>=22.23.4)، 24.x (>=24.22.1) یا 26+ (>=26.12.1)، به
| بایت‌کد EVM | selectorهای dispatch، offsetهای بایتی، آرگومان‌های استنباط‌شده و قابلیت تغییر وضعیت | دادهٔ خام/hex محلی روی Linux x64؛ [راهنمای EVM آفلاین](../evm-bytecode.md) |
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
| ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](../elf-notes.md) |
| Apple IPS crashes | .ips metadata, threads, images, registers and crash classes | Built-in static JSON inspection; [Apple IPS](../apple-ips.md) |
| Erlang BEAM | IFF chunks, atoms, exports, and tables | Built-in static BEAM inspection; [Erlang BEAM](../erlang-beam.md) |
| کرش‌های ثبت‌شدهٔ لینوکس | یادداشت‌های خام، رجیسترها/سیگنال‌های تمام threadهای ثبت‌شده و گزینه‌های احتمالی نگاشت | pwntools تأمین‌شده توسط فراخواننده روی Linux x64؛ GDB/pwndbg اختیاری؛ [کرش‌های ثبت‌شده](../recorded-crashes.md) |
| نصب‌های دسکتاپ | نامزدهای زمان اجرا، فایل‌های شاهد، نشانه‌های مؤلفه‌ها و مراحل بعدی | بررسی محدود پوشه‌ها و سرآیندها;[برنامه‌های دسکتاپ](../desktop-applications.md) |
+1
View File
@@ -159,6 +159,7 @@ REA nécessite Node.js 22.x (>=22.23.4), 24.x (>=24.22.1) ou 26+ (>=26.12.1), ai
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
| ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](../elf-notes.md) |
| Erlang BEAM | IFF chunks, atoms, exports, and tables | Built-in static BEAM inspection; [Erlang BEAM](../erlang-beam.md) |
| Apple IPS crashes | .ips metadata, threads, images, registers and crash classes | Built-in static JSON inspection; [Apple IPS](../apple-ips.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) |
| Installations de bureau | Environnements candidats, fichiers justificatifs, composants et étapes suivantes | Inspection bornée des répertoires et en-têtes;[Applications de bureau](../desktop-applications.md) |
| JavaScript / Electron | Modules, imports, source maps, routes, IPC et relations avec les modules natifs | Node.js et npm ; [analyse d'applications](../javascript-application-workflows.md); Linux: procps `ps` |
+1
View File
@@ -161,6 +161,7 @@ Alat tambahan dan dukungan sistem host bergantung pada target yang dianalisis:
| 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) |
| ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](../elf-notes.md) |
| Apple IPS crashes | .ips metadata, threads, images, registers and crash classes | Built-in static JSON inspection; [Apple IPS](../apple-ips.md) |
| Erlang BEAM | IFF chunks, atoms, exports, and tables | Built-in static BEAM inspection; [Erlang BEAM](../erlang-beam.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) |
| Instalasi desktop | Kandidat runtime, berkas pendukung, penanda komponen, dan langkah berikutnya | Pemeriksaan direktori dan header terbatas;[Aplikasi desktop](../desktop-applications.md) |
+1
View File
@@ -159,6 +159,7 @@ REA richiede Node.js 22.x (>=22.23.4), 24.x (>=24.22.1) o 26+ (>=26.12.1), oltre
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
| ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](../elf-notes.md) |
| Erlang BEAM | IFF chunks, atoms, exports, and tables | Built-in static BEAM inspection; [Erlang BEAM](../erlang-beam.md) |
| Apple IPS crashes | .ips metadata, threads, images, registers and crash classes | Built-in static JSON inspection; [Apple IPS](../apple-ips.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) |
| Installazioni desktop | Runtime candidati, file di supporto, componenti e passi successivi | Ispezione limitata di directory e intestazioni;[Applicazioni desktop](../desktop-applications.md) |
| JavaScript / Electron | Moduli, import, source map, route, IPC e relazioni con add-on nativi | Node.js e npm; [analisi delle applicazioni](../javascript-application-workflows.md); Linux: procps `ps` |
+1
View File
@@ -158,6 +158,7 @@ REA には Node.js 22.x(>=22.23.4)、24.x(>=24.22.1)、または 26+ (>=
| EVM バイトコード | ディスパッチセレクター、バイトオフセット、推定された引数と状態変更の可否 | Linux x64 上のローカルの生バイト/16 進数の入力ファイル;[オフライン EVM ガイド](../evm-bytecode.md) |
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
| ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](../elf-notes.md) |
| Apple IPS crashes | .ips metadata, threads, images, registers and crash classes | Built-in static JSON inspection; [Apple IPS](../apple-ips.md) |
| Erlang BEAM | IFF chunks, atoms, exports, and tables | Built-in static BEAM inspection; [Erlang BEAM](../erlang-beam.md) |
| 記録済みの Linux クラッシュ | 生の note レコード、記録された全スレッドのレジスター/シグナル、任意のマッピング候補 | Linux x64 で利用者が用意する pwntools;GDB/pwndbg は任意;[記録済みクラッシュ](../recorded-crashes.md) |
| デスクトップのインストール先 | ランタイム候補、根拠ファイル、コンポーネントの目印、次の調査手順 | 上限付きのディレクトリ・ヘッダー検査;[デスクトップアプリ](../desktop-applications.md) |
+1
View File
@@ -158,6 +158,7 @@ REA에는 Node.js 22.x(>=22.23.4), 24.x(>=24.22.1) 또는 26+ (>=26.12.1)와 npm
| PE certificates | Authenticode WIN_CERTIFICATE entries and PKCS#7 signer/X.509 identity fields | Built-in static PE inspection; [PE certificates](../pe-certificates.md) |
| PE imports | Import and delay-load DLL names, ordinals, IAT RVAs and pefile import hash | Built-in static PE inspection; [PE imports](../pe-imports.md) |
| ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](../elf-notes.md) |
| Apple IPS crashes | .ips metadata, threads, images, registers and crash classes | Built-in static JSON inspection; [Apple IPS](../apple-ips.md) |
| Erlang BEAM | IFF chunks, atoms, exports, and tables | Built-in static BEAM inspection; [Erlang BEAM](../erlang-beam.md) |
| WASM 아티팩트 | 유효성 검사, 섹션, import/export의 정확한 WAT 형식, 디코딩된 WAT | 호출자 제공 WABT 1.0.42; `inspect_wasm_artifact` |
| EVM 바이트코드 | 디스패치 선택자, 바이트 오프셋, 추론한 인자와 상태 변경 가능 여부 | Linux x64의 로컬 원시 바이트/16진수 입력 파일; [오프라인 EVM 가이드](../evm-bytecode.md) |
+1
View File
@@ -162,6 +162,7 @@ REA-യ്ക്ക് Node.js 22.x (>=22.23.4), 24.x (>=24.22.1), അല്ല
| WASM artifact | Validation, sections, കൃത്യമായ import/export WAT രൂപങ്ങൾ, decoded WAT | Caller നൽകുന്ന WABT 1.0.42; `inspect_wasm_artifact` |
| EVM bytecode | Dispatch selectors, byte offsets, അനുമാനിച്ച arguments, mutability | Linux x64-ൽ local raw/hex carrier; [offline EVM guide](../evm-bytecode.md) |
| SQLite databases | Tables, columns, indexes, views, triggers, തിരഞ്ഞെടുത്ത സാധാരണ table rows | Local database/WAL snapshot; native SQLite പരിമിതികളോടെ Node.js 24.x >=24.15 അല്ലെങ്കിൽ 26+ |
| Apple IPS crashes | .ips metadata, threads, images, registers and crash classes | Built-in static JSON inspection; [Apple IPS](../apple-ips.md) |
| Recorded Linux crashes | Raw notes, രേഖപ്പെടുത്തിയ ഓരോ thread-ന്റെയും registers/signals, ഐച്ഛിക mapping candidates | Linux x64-ൽ caller നൽകുന്ന pwntools; ഐച്ഛിക GDB/pwndbg; [recorded crashes](../recorded-crashes.md) |
| ഡെസ്ക്ടോപ്പ് ഇൻസ്റ്റലേഷനുകൾ | റൺടൈം സാധ്യതകൾ, തെളിവ് ഫയലുകൾ, ഘടക സൂചനകൾ, തുടർ നടപടികൾ | പരിധിയുള്ള ഡയറക്ടറി, ഹെഡർ പരിശോധന;[ഡെസ്ക്ടോപ്പ് ആപ്ലിക്കേഷനുകൾ](../desktop-applications.md) |
| JavaScript / Electron | Modules, imports, source maps, routes, IPC, native add-on ബന്ധങ്ങൾ | Node.js, npm; Linux: procps-compatible `ps`; [application analysis](../javascript-application-workflows.md) |
+1
View File
@@ -159,6 +159,7 @@ REA wymaga Node.js 22.x (>=22.23.4), 24.x (>=24.22.1) lub 26+ (>=26.12.1) oraz n
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
| ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](../elf-notes.md) |
| Erlang BEAM | IFF chunks, atoms, exports, and tables | Built-in static BEAM inspection; [Erlang BEAM](../erlang-beam.md) |
| Apple IPS crashes | .ips metadata, threads, images, registers and crash classes | Built-in static JSON inspection; [Apple IPS](../apple-ips.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) |
| Instalacje desktopowe | Kandydaci środowisk uruchomieniowych, pliki źródłowe dowodów, komponenty i dalsze kroki | Ograniczona inspekcja katalogów i nagłówków;[Aplikacje desktopowe](../desktop-applications.md) |
| JavaScript / Electron | Moduły, importy, mapy źródeł, trasy, IPC i powiązania z natywnymi dodatkami | Node.js i npm; [analiza aplikacji](../javascript-application-workflows.md); Linux: procps `ps` |
+1
View File
@@ -159,6 +159,7 @@ O REA requer Node.js 22.x (>=22.23.4), 24.x (>=24.22.1) ou 26+ (>=26.12.1), alé
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
| ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](../elf-notes.md) |
| Erlang BEAM | IFF chunks, atoms, exports, and tables | Built-in static BEAM inspection; [Erlang BEAM](../erlang-beam.md) |
| Apple IPS crashes | .ips metadata, threads, images, registers and crash classes | Built-in static JSON inspection; [Apple IPS](../apple-ips.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) |
| Instalações desktop | Ambientes candidatos, arquivos de evidência, componentes e próximos passos | Inspeção limitada de diretórios e cabeçalhos;[Aplicações desktop](../desktop-applications.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](../javascript-application-workflows.md); Linux: procps `ps` |
+1
View File
@@ -159,6 +159,7 @@ REA требует Node.js 22.x (>=22.23.4), 24.x (>=24.22.1) или 26+ (>=26.1
| PE imports | Import and delay-load DLL names, ordinals, IAT RVAs and pefile import hash | Built-in static PE inspection; [PE imports](../pe-imports.md) |
| ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](../elf-notes.md) |
| Erlang BEAM | IFF chunks, atoms, exports, and tables | Built-in static BEAM inspection; [Erlang BEAM](../erlang-beam.md) |
| Apple IPS crashes | .ips metadata, threads, images, registers and crash classes | Built-in static JSON inspection; [Apple IPS](../apple-ips.md) |
| Записанные сбои Linux | Необработанные записи note, регистры/сигналы каждого записанного потока и необязательные кандидаты сопоставления с памятью | pwntools, предоставленный вызывающей стороной, в Linux x64; необязательные GDB/pwndbg; [записанные сбои](../recorded-crashes.md) |
| Установленные настольные приложения | Возможные среды выполнения, файлы с доказательствами, компоненты и дальнейшие шаги | Ограниченная проверка каталогов и заголовков;[Настольные приложения](../desktop-applications.md) |
| JavaScript / Electron | Модули, импорты, карты исходного кода, маршруты, IPC и связи с нативными дополнениями | Node.js и npm; [анализ приложений](../javascript-application-workflows.md); Linux: procps `ps` |
+1
View File
@@ -159,6 +159,7 @@ REA ต้องใช้ Node.js 22.x (>=22.23.4), 24.x (>=24.22.1) หรื
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
| ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](../elf-notes.md) |
| Erlang BEAM | IFF chunks, atoms, exports, and tables | Built-in static BEAM inspection; [Erlang BEAM](../erlang-beam.md) |
| Apple IPS crashes | .ips metadata, threads, images, registers and crash classes | Built-in static JSON inspection; [Apple IPS](../apple-ips.md) |
| ข้อมูลการแครชของ Linux ที่บันทึกไว้ | ระเบียน note ดิบ รีจิสเตอร์/สัญญาณของทุกเธรดที่บันทึกไว้ และตัวเลือกการแมปเพิ่มเติม | pwntools ที่ผู้ใช้จัดเตรียมบน Linux x64; เลือกใช้ GDB/pwndbg เพิ่มเติมได้; [การแครชที่บันทึกไว้](../recorded-crashes.md) |
| ไดเรกทอรีติดตั้งแอปเดสก์ท็อป | รันไทม์ที่เป็นไปได้ ไฟล์หลักฐาน ตัวบ่งชี้ส่วนประกอบ และขั้นตอนถัดไป | ตรวจสอบไดเรกทอรีและส่วนหัวโดยจำกัดขอบเขต;[แอปเดสก์ท็อป](../desktop-applications.md) |
| JavaScript / Electron | โมดูล การนำเข้า ซอร์สแมป เส้นทาง IPC และความสัมพันธ์กับส่วนเสริมเนทีฟ | Node.js และ npm; [การวิเคราะห์แอปพลิเคชัน](../javascript-application-workflows.md); Linux: procps `ps` |
+1
View File
@@ -156,6 +156,7 @@ REA; Node.js 22.x (>=22.23.4), 24.x (>=24.22.1) veya 26+ (>=26.12.1) ve npm gere
| PE certificates | Authenticode WIN_CERTIFICATE entries and PKCS#7 signer/X.509 identity fields | Built-in static PE inspection; [PE certificates](../pe-certificates.md) |
| PE imports | Import and delay-load DLL names, ordinals, IAT RVAs and pefile import hash | Built-in static PE inspection; [PE imports](../pe-imports.md) |
| ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](../elf-notes.md) |
| Apple IPS crashes | .ips metadata, threads, images, registers and crash classes | Built-in static JSON inspection; [Apple IPS](../apple-ips.md) |
| Erlang BEAM | IFF chunks, atoms, exports, and tables | Built-in static BEAM inspection; [Erlang BEAM](../erlang-beam.md) |
| WASM artefaktı | Doğrulama, bölümler, tam import/export WAT biçimleri ve decode edilmiş WAT | Çağıranın sağladığı WABT 1.0.42; `inspect_wasm_artifact` |
| EVM bayt kodu | Yönlendirme seçicileri, bayt ofsetleri, çıkarımla belirlenen argümanlar ve durum değiştirilebilirliği | Linux x64 üzerinde yerel ham bayt/onaltılık girdi; [çevrimdışı EVM rehberi](../evm-bytecode.md) |
+1
View File
@@ -159,6 +159,7 @@ REA потребує Node.js 22.x (>=22.23.4), 24.x (>=24.22.1) або 26+ (>=26
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
| ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](../elf-notes.md) |
| Erlang BEAM | IFF chunks, atoms, exports, and tables | Built-in static BEAM inspection; [Erlang BEAM](../erlang-beam.md) |
| Apple IPS crashes | .ips metadata, threads, images, registers and crash classes | Built-in static JSON inspection; [Apple IPS](../apple-ips.md) |
| Записані аварійні завершення в Linux | Необроблені записи note, регістри/сигнали кожного записаного потоку й необов'язкові кандидати зіставлення пам'яті | pwntools, наданий стороною, що викликає інструмент, у Linux x64; необов'язкові GDB/pwndbg; [записані аварійні завершення](../recorded-crashes.md) |
| Встановлені настільні програми | Можливі середовища виконання, файли доказів, компоненти та наступні кроки | Обмежена перевірка каталогів і заголовків;[Настільні програми](../desktop-applications.md) |
| JavaScript / Electron | Модулі, імпорти, карти вихідного коду, маршрути, IPC і зв'язки з нативними доповненнями | Node.js і npm; [аналіз застосунків](../javascript-application-workflows.md); Linux: procps `ps` |
+1
View File
@@ -202,6 +202,7 @@ Mazeed tools aur host support target par depend karte hain:
| WASM artifact | Validation, sections, exact import/export WAT forms aur decoded WAT | Aapka diya hua WABT 1.0.42; `inspect_wasm_artifact` |
| EVM bytecode | Dispatch selectors, byte offsets, inferred arguments aur mutability | Linux x64 par local raw/hex carrier; [offline EVM guide](../evm-bytecode.md) |
| SQLite databases | Tables, columns, indexes, views, triggers aur selected ordinary-table rows | Local database/WAL snapshot; native SQLite limits ke saath Node.js 24.x >=24.15 ya 26+ |
| Apple IPS crash reports | Apple .ips metadata, threads, images aur crash classification | Built-in static JSON inspection; [Apple IPS](../apple-ips.md) |
| Recorded Linux crashes | Raw notes, har recorded thread ke registers/signals aur optional mapping candidates | Linux x64 par aapka diya hua pwntools; optional GDB/pwndbg; [recorded crashes](../recorded-crashes.md) |
| JavaScript / Electron | Modules, imports, source maps, routes, IPC aur native add-on relationships | Node.js aur npm; Linux: procps-compatible `ps`; [application analysis](../javascript-application-workflows.md) |
| Websites | Page structure, scripts, network observations aur requested screenshots | Chrome-family browser; [browser analysis](../browser-observation.md) |
+1
View File
@@ -159,6 +159,7 @@ REA yêu cầu Node.js 22.x (>=22.23.4), 24.x (>=24.22.1) hoặc 26+ (>=26.12.1)
| PE imports | Import and delay-load DLL names, ordinals, IAT RVAs and pefile import hash | Built-in static PE inspection; [PE imports](../pe-imports.md) |
| ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](../elf-notes.md) |
| Erlang BEAM | IFF chunks, atoms, exports, and tables | Built-in static BEAM inspection; [Erlang BEAM](../erlang-beam.md) |
| Apple IPS crashes | .ips metadata, threads, images, registers and crash classes | Built-in static JSON inspection; [Apple IPS](../apple-ips.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) |
| Thư mục cài đặt ứng dụng máy tính | Các runtime có khả năng được dùng, tệp bằng chứng, dấu hiệu thành phần và bước tiếp theo | Kiểm tra thư mục và phần đầu tệp có giới hạn;[Ứng dụng máy tính](../desktop-applications.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](../javascript-application-workflows.md); Linux: procps `ps` |
+1
View File
@@ -159,6 +159,7 @@ REA 需要 Node.js 22.x(>=22.23.4)、24.x(>=24.22.1)或 26+ (>=26.12.1)
| PE certificates | Authenticode WIN_CERTIFICATE entries and PKCS#7 signer/X.509 identity fields | Built-in static PE inspection; [PE certificates](../pe-certificates.md) |
| PE imports | Import and delay-load DLL names, ordinals, IAT RVAs and pefile import hash | Built-in static PE inspection; [PE imports](../pe-imports.md) |
| ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](../elf-notes.md) |
| Apple IPS crashes | .ips metadata, threads, images, registers and crash classes | Built-in static JSON inspection; [Apple IPS](../apple-ips.md) |
| 已記錄的 Linux 崩潰 | 原始 note 記錄、每個已記錄執行緒的暫存器/訊號及可選的對映候選項 | Linux x64 上由呼叫方提供的 pwntools;可選的 GDB/pwndbg;[已記錄崩潰](../recorded-crashes.md) |
| 桌面安裝目錄 | 執行階段候選、支持檔案、元件標記與後續操作 | 有界的目錄與檔頭檢查;[桌面應用](../desktop-applications.md) |
| JavaScript / Electron | 模組、匯入、source map、路由、IPC 和原生附加元件關係 | Node.js 和 npm;[應用分析](../javascript-application-workflows.md); Linux: procps `ps` |
+1
View File
@@ -159,6 +159,7 @@ REA 需要 Node.js 22.x(>=22.23.4)、24.x(>=24.22.1)或 26+ (>=26.12.1)
| PE certificates | Authenticode WIN_CERTIFICATE entries and PKCS#7 signer/X.509 identity fields | Built-in static PE inspection; [PE certificates](../pe-certificates.md) |
| PE imports | Import and delay-load DLL names, ordinals, IAT RVAs and pefile import hash | Built-in static PE inspection; [PE imports](../pe-imports.md) |
| ELF notes | PT_NOTE/SHT_NOTE owners, types, GNU build-id and ABI-tag fields | Built-in static ELF inspection; [ELF notes](../elf-notes.md) |
| Apple IPS crashes | .ips metadata, threads, images, registers and crash classes | Built-in static JSON inspection; [Apple IPS](../apple-ips.md) |
| 已记录的 Linux 崩溃 | 原始 note 记录、每个已记录线程的寄存器/信号及可选的映射候选项 | Linux x64 上由调用方提供的 pwntools;可选的 GDB/pwndbg;[已记录崩溃](../recorded-crashes.md) |
| 桌面安装目录 | 运行时候选、支持文件、组件标记和后续操作 | 有界的目录与文件头检查;[桌面应用](../desktop-applications.md) |
| JavaScript / Electron | 模块、导入、source map、路由、IPC 和原生扩展关系 | Node.js 和 npm;[应用分析](../javascript-application-workflows.md); Linux: procps `ps` |
+2 -1
View File
@@ -1,7 +1,8 @@
# Recorded crash evidence
`inspect_recorded_crash` / `rea inspect-recorded-crash` inspect a caller-selected
Linux ELF64 little-endian core independently of an active binary target. Two
Linux ELF64 little-endian core independently of an active binary target. Apple
`.ips` crash reports use the separate [Apple IPS](apple-ips.md) workflow. Two
architecture profiles are interpreted, x86-64 and AArch64; each binds registers
to its own `NT_PRSTATUS` layout, and any other `e_machine` is refused as
`unsupported_target` before note interpretation.
+329
View File
@@ -0,0 +1,329 @@
# Apple IPS static inspection verification — 2026-10-24
## Scope
PR #6511 adds portable `inspect_apple_ips` / `inspect-apple-ips` over one
caller-selected local file. The metadata must identify string `bug_type`
`"309"`; stackshots and other report kinds remain unsupported. No target is
executed, no deep engine starts, and no atos/dSYM lookup is performed. These
checks do not qualify live macOS state, symbolication or Mach-O UUID joins.
The source contract preserves integer literals beyond IEEE-754 precision,
observed type mismatches and partial coverage. The frame address is derived
from its image's base plus its recorded offset, as described in
[Apple's crash JSON documentation](https://developer.apple.com/documentation/xcode/interpreting-the-json-format-of-a-crash-report).
That documentation does not provide a 64-bit bound for every numeric field;
the parser therefore bounds representation cost without truncating values.
## Review corrections and ownership
The contributor's `80ad2a02e9c6cbe248d58d78a7a79753bcc09f79` already projects
documented JSON members and checks thread/frame/image/register allowances
before materializing each row. Unknown members are syntax-checked without
retaining their values. Producer controls include a budget error before an
invalid next item, wrong-type collections, frame-role cases, and the DEAD10CC
decimal near miss. Inline-role assumptions are corrected below. Source inspection alone does not
establish the runtime checks below.
Further negative controls expose three remaining boundaries:
- Two contradictory thread/image coverage reports were accepted by the
service. Coverage now agrees with the retained arrays before Evidence is
issued. Already cancelled requests also preserve the exact caller reason.
- A 20,001-digit image base expanded into 512 derived frame addresses despite
a small input file. The field reader now owns an 8 MiB default / 64 MiB
maximum UTF-16 allowance for recorded decimal/hex fields. Conservative
reservations precede conversion and arithmetic. Source BigInts are reused;
derived sums are not retained in a cache. Repeated appearances count
separately, including a service control using the same object twice.
- A 20,013-character image architecture appeared 513 times in a 512-frame
report. Frames now retain their recorded `image_index`; architecture, UUID
and `uuid_unknown` belong to the image once. The old per-frame copies and
superseded field-access helpers are removed.
Each defect has a failing control before its correction. Raising the integer
allowance to 64 MiB preserves the 512-frame decimal facts exactly. The
allowance is per call, requires no restart, and is separate from file-size,
row, wire and client-buffer limits. Refusal is a typed capacity error without
partial success. A refused task is not treated as a completed-task speedup.
## Public corpus provenance
Two pinned upstream reports are valid two-object JSON and are used without
editing their bytes. Their original generation history has not been verified;
these are public repository corpus files, not a claim of fresh OS capture.
| Report | Repository revision | Bytes | SHA-256 |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------ | -----: | ------------------------------------------------------------------ |
| [LLDB multithread-test.ips](https://github.com/llvm/llvm-project/blob/16dde0db3ce0b9c5eae14178780cc6db1eec7ac8/lldb/test/Shell/ScriptInterpreter/Python/Crashlog/Inputs/interactive_crashlog/multithread-test.ips) | `16dde0db3ce0b9c5eae14178780cc6db1eec7ac8` | 16,093 | `567cadb22af19be798390ab2021ed929fb782c69b2d3b18ff5bb3f39a3741d8c` |
| [pycrashreport Monterey report](https://github.com/doronz88/pycrashreport/blob/634dff127fa6d4d3df948a64a3e254fe208a99dc/tests/user_mode_crash_report_monterey_non_symbolicated.ips) | `634dff127fa6d4d3df948a64a3e254fe208a99dc` | 7,047 | `7de3d3c3515e2ac1bb6ed4c00b70cb12c1ff6ad4317617d258d58346e00264b7` |
Three other LLDB inputs (`a.out.ips`, `a.out.inline.ips`, `no_threadState.ips`)
contain invalid JSON at their pinned revision. They were not repaired or
counted as successful report qualifications. Files remain temporary local
verification inputs and are not committed to this repository.
## Completed-task comparison
Compare `d4930ed57c0b175a632fd8572732b1e88a73cb2f` (original IPS behavior
with main `cc18e9ad` provider/API integration repairs) against
`135cab1234c3663e839f5fbea6bf833737cd140a`. Three alternating pairs of five
public CLI tasks complete, **30 tasks total**. Every task returns complete
coverage. An independent Python JSON/integer oracle checks all ordered images,
threads, registers, frame offsets, derived addresses and the then-selected
physical-depth roles, as well as process facts and artifact digests. The later
source-backed correction below replaces that role assumption; neither public
report contains an inline row, so its role expectations remain unchanged. The two public reports contain
3/1 threads, 8/4 images, 30/4 frames and 36/22 retained registers respectively.
Every Evidence field agrees after excluding the changed Evidence ID, the new
default allowance parameter and the removed per-frame copies. Before excluding
those copies, the oracle verifies each against its owning image. Image facts,
uncertainty, other report fields, provenance, coverage and limitations remain
equal. Artifact digests are unchanged across all runs.
| Complete CLI task | Estimated stdout + invocation tokens, before → after | Elapsed median s | CPU median s | Sampled family RSS median MiB |
| ------------------------------------ | ---------------------------------------------------: | ---------------: | ------------: | ----------------------------: |
| Small synthetic report | 934 → 923 | 2.069 → 1.969 | 2.309 → 2.212 | 368.8 → 369.6 |
| LLDB multithread report | 6,500 → 5,851 | 3.180 → 2.878 | 2.292 → 2.340 | 370.4 → 373.5 |
| Monterey report | 2,377 → 2,309 | 3.330 → 3.029 | 2.230 → 2.208 | 369.6 → 368.8 |
| 20,001-digit base / 64 frames | 402,476 → 401,783 | 2.725 → 2.879 | 2.354 → 2.319 | 375.1 → 371.8 |
| Long image architecture / 512 frames | 1,760,820 → 41,023 | 3.231 → 2.928 | 2.468 → 2.261 | 387.3 → 372.7 |
The long-image task reduces estimated whole-task context by **97.67%** while
preserving joined facts. Ordinary reports save less. No latency speedup is
claimed: elapsed ranges overlap, including the long-image ranges 2.070–3.381 s
before and 2.928–3.633 s after. Small-report ranges reach about 9.2 s in both
lanes during shared-host contention. The wide-integer elapsed median rises
about 0.154 s, while its CPU and sampled RSS medians decrease slightly. Three
pairs do not establish universal performance or maximum-capacity behavior.
Each task uses a fresh Node 24.22.1 CLI/provider process, with CPU affinity 0,1.
OS caches were not flushed; these are fresh-process runs with uncontrolled
warm filesystem caches, not measured cold-disk runs. No result cache or
persistent provider session participates. Warm long-lived process behavior is
unmeasured. Tokenx 1.3.0 estimates the entire Evidence stdout and invocation;
these are estimates, not billed/model token counts. The analyst already knows
the single CLI command, so no discovery, paging or extra lookup call is
required. Image metadata is inline in the same result. MCP catalog costs,
prompts, framing, rendering and client JSON parsing are unmeasured.
CPU is waited-child user plus system time. RSS is a 50 ms recursive
process-family sample; it can double-count shared pages and miss short-lived
or unobserved descendants. Token estimation and the oracle run outside the
timed command. Later main integrations are outside these pinned timings.
Synthetic recipes: `small.ips` has one frame/register; `wide-integer.ips` uses
base `10^20000`, 64 frames with image offset 1; `wide-image-text.ips` uses
architecture `unknown_arch_` plus 20,000 `x` characters and 512 frames. Their
SHA-256 values are respectively
`1178a013b86f8a6c11abef633fdee5fefd23e010c4af92710321cb0f369ed857`,
`b455598694d9953031e52450f1a4a6655e16abd683a2cec59e393899883b6994`, and
`4d3d2f3e4946cbbeed7e14e1f12cd97bd2f119af1b3a33496dd3e8d079c3f999`.
## Final integration checks
Main `cd10ee1f8a8e124be4864e77750a16e76c561809` integration passes **167**
selected domain/service/parser/filesystem/CLI/MCP/shared lifecycle checks in
74.312 s (80.355 s waited-child CPU; 1,902.0 MiB sampled family peak). Full
`npm run check`, `npm run docs:check` and test discovery pass. All 2,324 test
files are discovered exactly once. Earlier source selections overlap these
checks. Two initial public-error assertions placed remediation at the wrong
level; the tests now check the existing top-level error contract. No production
error shape was changed to accommodate them.
Both pinned public reports additionally pass through the actual stdio MCP
server and SDK client, with the advertised input/output JSON Schemas validated
in AJV 2020. The same independent oracle checks their returned facts. The
client explicitly admits a 64 MiB receive buffer for the complete tool catalog;
this does not qualify every client's default buffer. Target execution and
symbolication remain unverified. The 30-task cost table remains pinned to its
recorded revisions, without reusing these integration costs as speed evidence.
## Integration after PE imports
The following integration includes main `4ef916da03d93729041f79cec0ee243cd2d3097d`
and the newly merged PE imports tool. The documentation index retains links to
both static formats. Full static/docs/discovery gates pass, together with
**191** selected checks, including the upstream native-plist cleanup test.
The selection takes 49.939 s, 52.461 s waited-child CPU and 2,079.8 MiB sampled
family RSS. It overlaps earlier selections and is not a performance comparison.
Previous-head CI run `40058145277` had one functional failure in
`nativePlistFileCleanup.test.ts`, repeated by the aggregate lane; seven other
shards were cancelled. Exact main `d662fa54a67bf24bd4800d1867231662019d91c6`
and actual CI merge `440029745397d78103a623fd1fd91cfa09fc6df5` each reproduce
11 passing / 1 failing cases with identical statuses and normalized failure
messages. The failing test and selected producer sources are byte-identical.
The subsequent main integration fixes that closing race, and all 12 cleanup
cases pass within the 191 checks. Cancelled CI coverage was not executed.
## Follow-up string capacity and identity review
Baseline `45a570656cb3b9a7a8bf41302e1f01f8662bc884` and candidate
`6fa8083fc6974bc18c7d227ded33db28eb590f03` isolate three further corrections:
- Retained JSON strings are syntax-checked by the same scanner used for skipped
strings, then escaped literals are decoded by the native JSON string parser.
Unescaped strings retain the slice fast path. The superseded per-escape
fragment array is gone; decoded length is bounded by the selected input's
literal length. Malformed escapes and controls still fail before projection.
- UUID interpretation owns format validation before canonicalization. Both
32 hexadecimal digits and canonical hyphenation are accepted. Malformed
nonempty spellings remain in `coverage.type_mismatches[].recorded_value`,
canonical identity stays unknown and coverage becomes partial. Absent UUIDs
and the documented all-zero unknown sentinel remain distinct from that
malformed-value outcome. The field uses the existing exact-optional contract
convention; it does not admit an explicit JavaScript `undefined` value.
- `metadata.bug_type` is literal `"309"` in the domain and advertised schema.
The service rejects unsupported reports from defective/custom ports too.
Seven new cases failed against the earlier production code: four malformed
UUID spellings, two unsupported port kinds and a public CLI capacity case.
The latter selects a file below the 16 MiB allowance containing 7,500,000
newline escapes. Earlier decoding exhausts a 256 MiB Node old-space limit;
the corrected CLI completes with every character verified by length and
SHA-256. This is an old-space setting, not a 256 MiB total RSS claim.
The first expanded run passed 182 of 183 cases; the advertised-schema MCP case
hit its 30-second test deadline. Its isolated two-case file then passed, and
the expanded schema assertions passed again. After switching to the final
exact-optional representation, both MCP cases passed once more: actual SDK
schemas reject a forged `bug_type: "288"` and admit the partial result with its
exact malformed UUID spelling. `check:fast` passed. The two public crash
reports also passed again through an actual stdio MCP client and Ajv2020.
Twelve further completed CLI tasks used three alternating pairs for the same
public LLVM report and a generated 4,000,063-byte report containing 2,000,000
newline escapes (SHA-256
`5c4a9282da37091973774bf98c6822caeecbc1d74d7c44cd4eda1937ad05e350`).
Every complete Evidence field, including IDs, is identical within each case.
The escape case explicitly selects an 8 MiB read allowance; both revisions
complete under the same default 2 GiB old-space task environment.
| Completed task | Estimated tokens, both revisions | Elapsed seconds, before → after | CPU seconds, before → after | Peak family RSS MiB, before → after |
| ------------------ | -------------------------------- | ------------------------------- | --------------------------- | ----------------------------------- |
| LLVM public report | 5,851 | 2.071 → 1.970 | 2.378 → 2.328 | 365.3 → 366.0 |
| 2,000,000 escapes | 4,000,703 | 2.425 → 2.374 | 2.457 → 2.289 | 459.3 → 372.1 |
Values are medians. Escape-case elapsed ranges overlap: 2.120–2.930 seconds
before and 2.324–2.424 after, so no elapsed-time improvement is established.
Its CPU ranges are 2.318–2.521 versus 2.245–2.297 seconds; sampled peak RSS
ranges are 457.4–462.9 versus 371.5–372.9 MiB. The measured dense-escape task
reduces median CPU by 6.8% and sampled memory by 19.0%; those observations are
specific to this case. LLVM ranges overlap and show no general speed gain.
Complete output and estimated tokens are unchanged.
Tokens use tokenx 1.3.0 estimates for the invocation plus all stdout, not model
billing. These are known CLI tasks with existing inputs, fresh processes and
warm OS caches. Input creation, cold disk, discovery, client parsing, prompts,
schemas, MCP envelopes and retries are outside that comparison. The same
50 ms descendant RSS sampling and waited-child CPU limitations above apply.
Later main integration is not part of these cost measurements.
## Final public MCP workflow and main integration
Main `21a20306b4887ff8e6b614b6ae34a387cfbdf1d8` was integrated in
`6d77f265027d34154145e807b1a5c8fd26a1e567`. Its static/documentation checks
and discovery of all 2,339 test files passed. The combined 183-case selection
again reached the in-memory MCP test's 30-second deadline while its other
182 cases passed. Isolated passes did not establish that the setup was robust
under the combined run.
The MCP test now exercises the compiled production stdio entrypoint through
the existing local-tools fixture, which owns client/transport cleanup and
validates the SDK-advertised schemas. It no longer reconstructs the full source
server/catalog inside the shared test worker. One public workflow retains all
the prior schema, annotation, capacity, register, malformed-UUID and relative-path
assertions; the redundant second connection case was consolidated. No test
deadline was increased. The same combined selection then passed all 182 cases,
including the large escaped-text CLI case and all producer/service regressions.
This is stronger public-path verification, not a claimed runtime speedup from
a before/after test-suite timing comparison.
## Reference and inline review follow-up
Commit `174c7496a2f007a1ef0ab5da0a341b2f801277c3` separates a recorded
uint32 lookup from whether it resolves to an object row. An out-of-range or
malformed `faultingThread` reference preserves the lookup, marks coverage
partial and leaves row-zero roles unknown. Image lookups use the same owner;
unresolved image references preserve `image_index` and leave the derived
address unknown. Negative or oversized integer lookups retain their exact
literal in `coverage.type_mismatches[].recorded_integer`. Reservation happens
before retention, and the service counts these literals against the same
integer-text allowance even for custom ports. No BigInt conversion is needed
for rejected indexes.
The previous comment and guide incorrectly asserted that inline rows share a
subsequent physical row's address. Apple's per-frame `imageOffset` description
and [the pinned LLDB parser](https://github.com/llvm/llvm-project/blob/16dde0db3ce0b9c5eae14178780cc6db1eec7ac8/lldb/examples/python/crashlog.py#L716)
support each row's own base-plus-offset; that parser increments its frame index
for every recorded row. REA therefore keeps its existing address derivation,
leaves a missing offset unknown and corrects roles to use the recorded row
index. The inline flag remains recorded evidence and does not reconstruct a
physical stack. This is source-derived validation; LLDB was not executed.
Nine source controls fail before this correction: two in-range-but-unresolved
faulting indexes, a reference to a malformed thread row, three rejected integer
spellings, the 19-byte rejected-literal allowance, unresolved image coverage and
the corrected row-role oracle. A separate differing/missing-inline-offset case
already passes before the correction and protects the existing address rule;
it is not evidence of a fixed address bug. All **193 selected checks pass**
afterward, including CLI, filesystem, custom-provider allowance checks and
production stdio MCP. The actual SDK validates retained lookup literals against
the advertised schema. `check:fast`, complete formatting, documentation and
cached build also pass. Both pinned public reports pass the real stdio SDK/Ajv
workflow again (13.510 s elapsed, 14.909 s waited CPU, 907.4 MiB sampled family
RSS); the independent integer/JSON oracle checks the CLI comparison below.
Six completed alternating CLI tasks compare the prior actual CI tree
`acb837583d8346016fd8b46d4fcf1fdf09fb0b06` with the corrected commit on the
pinned LLDB multithread report. All full Evidence, including IDs, agrees.
Estimated invocation plus full stdout tokens remain **5,851** (tokenx 1.3.0).
Elapsed medians are 2.071 -> 1.969 s (ranges 2.071–2.271 / 1.919–2.069), CPU
2.295 -> 2.278 s (2.140–2.471 / 2.231–2.283), and sampled process-family RSS
366.8 -> 367.0 MiB (348.2–370.9 / 348.9–368.4). CPU/memory ranges overlap;
this small warm-cache sample does not establish a general speed or memory
improvement. Processes are fresh; the OS file cache is warm. Caller input
creation, schemas/discovery, prompts, client parsing and MCP transport are not
included. The same 50 ms family-RSS limitations apply.
## JSON admission and output consistency follow-up
Three later review findings were reproduced with 19 failing controls before the
correction (49 other selected checks pass). Broad JavaScript trimming admitted
form-feed, vertical-tab, NBSP and BOM around JSON objects; those are not JSON
whitespace. The IPS adapter now passes the original body to the lossless parser,
and the complete-value boundary permits only space, tab, CR and LF after the
value. Twelve controls cover metadata suffixes and body prefixes/suffixes; a
positive control retains JSON whitespace around both objects. Caller bytes and
artifact identity stay unchanged.
The shared service no longer accepts a reported faulting-thread index without
an established retained owner or its corresponding reference mismatch. Unknown
or out-of-range recorded indexes remain available with partial coverage; an
unrelated mismatch cannot justify the unresolved lookup. The old empty service
fixture incorrectly asserted faulting thread zero and now records null. This is
output admission, not reinterpretation of provider source JSON.
Classification is now a named discriminated schema: `classified` requires at
least one match and `unclassified` permits none. The source schema rejects both
contradictions. Production SDK/Ajv checks also test both contradictions and both
valid alternatives against the advertised JSON Schema; an actual stdio call
checks that non-JSON trailing whitespace returns a typed caller-input failure.
These are correctness changes. Prior CLI/performance results retain their exact
pins and are not a fresh measurement of the expanded classification schema or
agent catalog costs. No speed or token reduction is claimed for this follow-up.
Final executed source pin `38b5a3c44824282b55041389b7cbb4d9c261cc8c`
incorporates main `98ec00f4068bab894639d0d1d83ac0bfc428bcbf`. All **216**
selected checks pass after the runtime rebuild: 52.922 s elapsed, 51.098 CPU s,
1,347.9 MiB sampled peak family RSS. Type/lint, complete formatting and generated
documentation checks pass. Both pinned public LLVM/pycrashreport inputs were
repeated through the actual stdio SDK/Ajv: 20.231 s elapsed, 20.602 CPU s,
934.0 MiB sampled family RSS, with complete coverage. Full Evidence and IDs agree
with the saved pre-correction public-corpus outputs for both valid inputs.
This repeat is a fresh client/server process with primed OS/file caches; it is
not a cold-system measurement. Client/schema work is included in runtime costs;
model/catalog token usage was not measured in this repeat. The family sampler's
shared-page and short-lived-child limitations described above still apply.
+7
View File
@@ -274,6 +274,13 @@ export const providerCatalog = (sources) => {
({ name }) => name === "inspect_elf_notes",
),
},
{
identity: sources.artifactProviders.APPLE_IPS_PROVIDER,
contracts:
sources.binaryDiagnosticsContracts.BINARY_DIAGNOSTICS_TOOL_CONTRACTS.filter(
({ name }) => name === "inspect_apple_ips",
),
},
{
identity: sources.artifactProviders.BYTE_ENTROPY_PROVIDER,
contracts:
+1
View File
@@ -450,6 +450,7 @@ const workflowAvailabilityFor = ({
name === "inspect_gpu_code_objects" ||
name === "inspect_rust_binary" ||
name === "inspect_python_pyc" ||
name === "inspect_apple_ips" ||
name === "inspect_erlang_beam" ||
name === "inspect_electron_fuses"
)
@@ -0,0 +1,20 @@
import type {
ExecutionOptions,
ProviderIdentity,
} from "../AnalysisProvider.js";
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
import type {
AppleIps,
InspectAppleIpsInput,
} from "../../domain/native/appleIps.js";
import type { Result } from "../../domain/result.js";
/** Stable read and Apple .ips decoding, independent of a disassembler session. */
export interface AppleIpsPort {
readonly identity: ProviderIdentity;
close?(): Promise<void>;
inspect(
input: InspectAppleIpsInput,
options?: ExecutionOptions,
): Promise<Result<AppleIps, AnalysisError>>;
}
@@ -0,0 +1,275 @@
import { expect, it } from "vitest";
import { AppleIpsService } from "./AppleIpsService.js";
import { APPLE_IPS_PROVIDER } from "../../domain/investigationProviders.js";
import type { AppleIps } from "../../domain/native/appleIps.js";
import { ok } from "../../domain/result.js";
import {
AnalysisCancelledError,
AnalysisInputError,
} from "../../domain/analysisErrorCore.js";
const identity = APPLE_IPS_PROVIDER;
const report: AppleIps = {
artifact: {
path: "/artifacts/report.ips",
sha256: "a".repeat(64),
bytes: 128,
},
metadata: {
bug_type: "309",
timestamp: null,
os_version: null,
bundle_id: null,
incident_id: null,
name: null,
unlisted_keys: [],
},
process: {
name: "rea-crash-fixture",
path: null,
pid: null,
parent: null,
cpu_type: null,
translated: null,
os_version: null,
},
exception: null,
termination: null,
faulting_thread: null,
threads: [],
images: [],
classification: { status: "unclassified", matches: [] },
coverage: {
status: "complete",
reason: null,
threads: 0,
frames: 0,
images: 0,
registers: 0,
type_mismatches: [],
},
unlisted_body_keys: [],
limitations: ["Opened Mach-O and dSYM UUID joins remain unobserved."],
};
it.each([0, 5])(
"rejects an unresolved faulting thread %s without its mismatch",
async (index) => {
const unresolved: AppleIps = { ...report, faulting_thread: index };
const service = new AppleIpsService({
identity,
inspect: () => Promise.resolve(ok(unresolved)),
});
const result = await service.inspect({ path: report.artifact.path });
expect(result.ok).toBe(false);
if (!result.ok) expect(result.error._tag).toBe("AnalysisOutputError");
},
);
it.each(["body.faultingThread", "body.other"])(
"requires the matching reference diagnostic: %s",
async (path) => {
const unresolved: AppleIps = {
...report,
faulting_thread: 5,
coverage: {
...report.coverage,
status: "partial",
reason: "Unresolved reference",
type_mismatches: [
{ path, expected: "index into threads", observed: "out of range" },
],
},
};
const result = await new AppleIpsService({
identity,
inspect: () => Promise.resolve(ok(unresolved)),
}).inspect({ path: report.artifact.path });
expect(result.ok).toBe(path === "body.faultingThread");
},
);
it.each([
{ status: "classified", matches: [] },
{
status: "unclassified",
matches: [
{
rule: "watchdog",
fields: [{ name: "termination.code", value: "2343432205" }],
},
],
},
])(
"rejects contradictory provider classification $status",
async (classification) => {
const malformed = structuredClone(report);
Reflect.set(malformed, "classification", classification);
const result = await new AppleIpsService({
identity,
inspect: () => Promise.resolve(ok(malformed)),
}).inspect({ path: report.artifact.path });
expect(result.ok).toBe(false);
if (!result.ok) expect(result.error._tag).toBe("AnalysisOutputError");
},
);
it.each(["288", "309 "])(
"rejects unsupported crash kind %s from a custom provider",
async (bugType) => {
const malformed = structuredClone(report);
Reflect.set(malformed.metadata, "bug_type", bugType);
const result = await new AppleIpsService({
identity,
inspect: () => Promise.resolve(ok(malformed)),
}).inspect({ path: report.artifact.path });
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Expected unsupported provider crash kind");
expect(result.error._tag).toBe("AnalysisOutputError");
},
);
it.each([null, { caller: "selected IPS request" }])(
"preserves early cancellation cause %j before validation",
async (reason) => {
const result = await new AppleIpsService({
identity,
inspect: () => {
throw new Error(
"An already cancelled request must not reach the provider",
);
},
}).inspect(null, { signal: AbortSignal.abort(reason) });
if (result.ok) throw new Error("Expected early cancellation");
expect(result.error._tag).toBe("AnalysisCancelledError");
expect(result.error.cause).toBe(reason);
},
);
it("rejects a relative path and extra arguments before reading", async () => {
let calls = 0;
const service = new AppleIpsService({
identity,
inspect: () => {
calls += 1;
return Promise.resolve(ok(report));
},
});
const relative = await service.inspect({ path: "report.ips" });
const extra = await service.inspect({
path: report.artifact.path,
extra: true,
});
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 report as a file subject", async () => {
const result = await new AppleIpsService({
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: "file" },
normalized_result: report,
});
});
it.each(["threads", "images"] as const)(
"rejects %s coverage counts that disagree with retained rows",
async (field) => {
const inconsistent: AppleIps = {
...report,
coverage: { ...report.coverage, [field]: 1 },
};
const result = await new AppleIpsService({
identity,
inspect: () => Promise.resolve(ok(inconsistent)),
}).inspect({ path: report.artifact.path });
expect(result.ok).toBe(false);
if (result.ok)
throw new Error("Expected contradictory provider output to fail");
expect(result.error._tag).toBe("AnalysisOutputError");
},
);
it.each([
[43, false],
[44, true],
] as const)(
"checks repeated integer text against %s UTF-16 bytes",
async (maximum, accepted) => {
const value = { decimal: "12345", hex: "0x3039" };
const repeated: AppleIps = {
...report,
process: { ...report.process, pid: value },
termination: {
namespace: null,
code: value,
indicator: null,
reasons: null,
unlisted_keys: [],
},
};
const result = await new AppleIpsService({
identity,
inspect: () => Promise.resolve(ok(repeated)),
}).inspect({ path: report.artifact.path, max_integer_text_bytes: maximum });
expect(result.ok).toBe(accepted);
if (!result.ok) expect(result.error._tag).toBe("AnalysisOutputError");
},
);
it("discards an observation if the caller cancels while the provider completes", async () => {
const controller = new AbortController();
const service = new AppleIpsService({
identity,
inspect: () => {
controller.abort();
return Promise.resolve(ok(report));
},
});
const result = await service.inspect(
{ path: report.artifact.path },
{ signal: controller.signal },
);
expect(result.ok).toBe(false);
if (!result.ok) expect(result.error).toBeInstanceOf(AnalysisCancelledError);
});
it.each([
[19, false],
[20, true],
] as const)(
"charges rejected integer literals from custom providers against %s bytes",
async (maximum, accepted) => {
const partial: AppleIps = {
...report,
coverage: {
...report.coverage,
status: "partial",
reason: "Unresolved index",
type_mismatches: [
{
path: "body.faultingThread",
expected: "unsigned 32-bit integer",
observed: "integer out of range",
recorded_integer: "4294967296",
},
],
},
};
const result = await new AppleIpsService({
identity,
inspect: () => Promise.resolve(ok(partial)),
}).inspect({ path: report.artifact.path, max_integer_text_bytes: maximum });
expect(result.ok).toBe(accepted);
if (!result.ok) expect(result.error._tag).toBe("AnalysisOutputError");
},
);
@@ -0,0 +1,140 @@
import { isAbsolute } from "node:path";
import type { ExecutionOptions } from "../AnalysisProvider.js";
import type { AppleIpsPort } from "./AppleIpsPort.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 {
appleIpsSchema,
appleIpsReportIntegerTextBytes,
inspectAppleIpsInputSchema,
type AppleIps,
} from "../../domain/native/appleIps.js";
import { analysisInputErrorFromIssues } from "../../domain/inputIssueProjection.js";
import { err, type Result } from "../../domain/result.js";
import { completeProviderEvidence } from "../ProviderOperationExecution.js";
const OPERATION = "inspect_apple_ips";
/** Shared caller validation, output parsing and Evidence provenance for CLI/MCP. */
export class AppleIpsService {
constructor(readonly provider: AppleIpsPort) {}
close(): Promise<void> {
return this.provider.close?.() ?? Promise.resolve();
}
async inspect(
rawInput: unknown,
options?: ExecutionOptions,
): Promise<Result<Evidence, AnalysisError>> {
if (options?.signal?.aborted)
return err(
new AnalysisCancelledError(OPERATION, { cause: options.signal.reason }),
);
const input = inspectAppleIpsInputSchema.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 result = await this.provider.inspect(input.data, options);
if (!result.ok) return result;
const report = appleIpsSchema.safeParse(result.value);
if (
!report.success ||
report.data.artifact.path !== input.data.path ||
!validReport(report.data, input.data)
)
return err(
new AnalysisOutputError(
OPERATION,
"Apple IPS provider returned malformed output or changed artifact identity.",
),
);
const value = report.data;
return completeProviderEvidence(
createEvidence(
{
path: value.artifact.path,
format: "file",
sha256: value.artifact.sha256,
},
this.provider.identity,
{
operation: OPERATION,
parameters: input.data,
result: value,
confidence: "observed",
limitations: value.limitations,
locations: [{ kind: "artifact-path", path: value.artifact.path }],
},
),
options?.signal,
);
}
}
const validReport = (
report: AppleIps,
input: InspectAppleIpsInputLike,
): boolean => {
if (
report.artifact.bytes > input.max_file_bytes ||
report.coverage.threads > input.max_threads ||
report.coverage.frames > input.max_frames ||
report.coverage.images > input.max_images ||
report.coverage.registers > input.max_registers ||
report.coverage.threads !== report.threads.length ||
report.coverage.images !== report.images.length
)
return false;
const faultingThread = report.faulting_thread;
const mismatches = report.coverage.type_mismatches;
if (
faultingThread !== null &&
(report.threads[faultingThread]?.index !== faultingThread ||
mismatches.some(
({ path }) => path === `body.threads[${String(faultingThread)}]`,
)) &&
!mismatches.some(({ path }) => path === "body.faultingThread")
)
return false;
let frames = 0;
let registers = 0;
for (const thread of report.threads) {
frames += thread.frames.length;
registers += thread.registers.length;
}
return (
frames === report.coverage.frames &&
registers === report.coverage.registers &&
appleIpsReportIntegerTextBytes(report) <= input.max_integer_text_bytes &&
(report.coverage.status === "complete") ===
(report.coverage.type_mismatches.length === 0)
);
};
type InspectAppleIpsInputLike = {
readonly max_file_bytes: number;
readonly max_threads: number;
readonly max_frames: number;
readonly max_images: number;
readonly max_registers: number;
readonly max_integer_text_bytes: number;
};
+46
View File
@@ -5,6 +5,7 @@ import {
createRecordedCrashService,
createBinaryLayoutService,
createElfNotesService,
createAppleIpsService,
createPeResourcesService,
createPeCertificatesService,
createPeImportsService,
@@ -22,6 +23,7 @@ import { inspectPeCertificatesInputSchema } from "../domain/native/peCertificate
import { inspectPeImportsInputSchema } from "../domain/native/peImports.js";
import { inspectNativeExportsInputSchema } from "../domain/native/nativeExports.js";
import { inspectElfNotesInputSchema } from "../domain/native/elfNotes.js";
import { inspectAppleIpsInputSchema } from "../domain/native/appleIps.js";
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
import { finishAnalysisCommand } from "./finishAnalysisCommand.js";
import { localPathStringSchema } from "../domain/localPath.js";
@@ -72,6 +74,50 @@ export const registerBinaryDiagnosticsCommands = (
);
}),
});
cli.command(CLI_COMMANDS.inspectAppleIps, {
description:
"Inspect an Apple .ips crash report without executing a target",
args: z.object({
path: z.string().describe("Local Apple .ips crash report"),
}),
options: z.object({
maxFileBytes: inspectAppleIpsInputSchema.shape.max_file_bytes,
maxThreads: inspectAppleIpsInputSchema.shape.max_threads,
maxFrames: inspectAppleIpsInputSchema.shape.max_frames,
maxImages: inspectAppleIpsInputSchema.shape.max_images,
maxRegisters: inspectAppleIpsInputSchema.shape.max_registers,
maxIntegerTextBytes:
inspectAppleIpsInputSchema.shape.max_integer_text_bytes,
}),
run: ({ args, options }) =>
withCommandCancellation(async (signal) => {
const ips = createAppleIpsService();
return await logCliCommand(
logger,
CLI_COMMANDS.inspectAppleIps,
async () =>
finishAnalysisCommand(
() =>
ips.inspect(
{
path: absoluteCallerPath(args.path),
max_file_bytes: options.maxFileBytes,
max_threads: options.maxThreads,
max_frames: options.maxFrames,
max_images: options.maxImages,
max_registers: options.maxRegisters,
max_integer_text_bytes: options.maxIntegerTextBytes,
},
{ signal },
),
ips,
"rea-apple-ips",
"inspect_apple_ips",
signal,
),
);
}),
});
const entropy = createByteEntropyService();
cli.command(CLI_COMMANDS.inspectByteEntropy, {
description:
+1
View File
@@ -74,6 +74,7 @@ export const CLI_COMMANDS = Object.freeze({
inspectPeImports: "inspect-pe-imports",
inspectNativeExports: "inspect-native-exports",
inspectElfNotes: "inspect-elf-notes",
inspectAppleIps: "inspect-apple-ips",
inspectAnalysisView: "inspect-analysis-view",
listFridaDevices: "list-frida-devices",
listFridaProcesses: "list-frida-processes",
+6
View File
@@ -12,6 +12,8 @@ import { PeImportsService } from "../application/binaryDiagnostics/PeImportsServ
import { PeImportsProvider } from "../native/pe/PeImportsProvider.js";
import { ElfNotesService } from "../application/binaryDiagnostics/ElfNotesService.js";
import { ElfNotesProvider } from "../native/elf/ElfNotesProvider.js";
import { AppleIpsService } from "../application/binaryDiagnostics/AppleIpsService.js";
import { AppleIpsProvider } from "../native/apple/AppleIpsProvider.js";
import type { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js";
import { NativeExportsService } from "../application/binaryDiagnostics/NativeExportsService.js";
import { NativeExportsProvider } from "../native/exports/NativeExportsProvider.js";
@@ -37,6 +39,10 @@ export const createNativeExportsService = (): NativeExportsService =>
export const createElfNotesService = (): ElfNotesService =>
new ElfNotesService(new ElfNotesProvider());
/** Portable Apple .ips inspection with no engine, Xcode or target acquisition. */
export const createAppleIpsService = (): AppleIpsService =>
new AppleIpsService(new AppleIpsProvider());
/** Compose the offline adapter without import-time I/O or dependency acquisition. */
export const createBinaryLayoutService = (
environment: Readonly<NodeJS.ProcessEnv> = process.env,
@@ -40,6 +40,10 @@ import {
inspectElfNotesInputSchema,
elfNotesSchema,
} from "../../domain/native/elfNotes.js";
import {
inspectAppleIpsInputSchema,
appleIpsSchema,
} from "../../domain/native/appleIps.js";
/** Caller-selected delivery of completed binary layout Evidence. */
export const inspectBinaryLayoutRequestSchema =
@@ -73,6 +77,21 @@ export const BINARY_DIAGNOSTICS_TOOL_CONTRACTS = [
},
],
},
{
name: "inspect_apple_ips",
...toolContractMetadata("inspect_apple_ips"),
kind: "native-provider",
description:
"Inspect one explicit local Apple .ips crash report without executing a target, launching atos, or opening a disassembler. Parses the first-line metadata object and, when bug_type is the string 309, the crash body: process identity, exception and termination facts, threads, usedImages, lossless integer registers, and deterministic classification from documented SIGKILL, EXC_GUARD, pointer-authentication and dyld strings. Other bug_type values, including 288 stackshots, return no crash result. A missing or truncated body fails without partial success. Opened Mach-O and dSYM UUID joins remain unobserved. Portable on supported Node hosts; no Xcode or pwntools required.",
inputSchema: inspectAppleIpsInputSchema,
outputSchema: evidenceResultOf(appleIpsSchema),
examples: [
{
title: "Inspect an Apple .ips crash report",
input: { path: "/artifacts/app.ips" },
},
],
},
{
name: "inspect_byte_entropy",
...toolContractMetadata("inspect_byte_entropy"),
+1
View File
@@ -29,6 +29,7 @@ const EXECUTION_WORDING_EXCEPTIONS: Readonly<Record<string, string>> = {
inspect_pe_resources: "states that it does not execute the image",
inspect_native_exports: "reads the export table without executing it",
inspect_elf_notes: "states that it does not execute the image",
inspect_apple_ips: "states that it does not execute a target",
inspect_desktop_application:
"states that directory triage performs no target execution",
inspect_pe_certificates: "states that it does not execute the image",
+1
View File
@@ -261,6 +261,7 @@ export const TOOL_EFFECTS: Readonly<Record<string, ToolEffects>> = {
inspect_pe_imports: evidence,
inspect_native_exports: evidence,
inspect_elf_notes: evidence,
inspect_apple_ips: evidence,
inspect_analysis_view: evidence,
inspect_firmware_regions: effects({
mutatesSession: true,
+7
View File
@@ -47,6 +47,13 @@ export const ELF_NOTES_PROVIDER = Object.freeze({
version: "1",
} as const);
/** Provider identity for portable, execution-free Apple .ips crash-report decoding. */
export const APPLE_IPS_PROVIDER = Object.freeze({
id: "rea-apple-ips",
name: "REA Apple IPS parser",
version: "1",
} as const);
/** Provider identity for execution-free Shannon byte-entropy profiling. */
export const BYTE_ENTROPY_PROVIDER = Object.freeze({
id: "byte-entropy",
+74
View File
@@ -0,0 +1,74 @@
import { expect, it } from "vitest";
import { appleIpsSchema, inspectAppleIpsInputSchema } from "./appleIps.js";
it.each([
["classified", 0, false],
["classified", 1, true],
["unclassified", 0, true],
["unclassified", 1, false],
] as const)(
"advertises classification %s with %s matches consistently",
(status, count, accepted) => {
const matches = Array.from({ length: count }, () => ({
rule: "watchdog",
fields: [{ name: "termination.code", value: "2343432205" }],
}));
expect(
appleIpsSchema.shape.classification.safeParse({ status, matches })
.success,
).toBe(accepted);
},
);
it("rejects extra caller arguments and admits an empty crash inventory", () => {
expect(
inspectAppleIpsInputSchema.safeParse({
path: "/artifacts/report.ips",
extra: true,
}).success,
).toBe(false);
const report = appleIpsSchema.parse({
artifact: {
path: "/artifacts/report.ips",
sha256: "a".repeat(64),
bytes: 128,
},
metadata: {
bug_type: "309",
timestamp: null,
os_version: null,
bundle_id: null,
incident_id: null,
name: null,
unlisted_keys: [],
},
process: {
name: null,
path: null,
pid: null,
parent: null,
cpu_type: null,
translated: null,
os_version: null,
},
exception: null,
termination: null,
faulting_thread: null,
threads: [],
images: [],
classification: { status: "unclassified", matches: [] },
coverage: {
status: "complete",
reason: null,
threads: 0,
frames: 0,
images: 0,
registers: 0,
type_mismatches: [],
},
unlisted_body_keys: [],
limitations: ["Opened Mach-O and dSYM UUID joins remain unobserved."],
});
expect(report.metadata.bug_type).toBe("309");
expect(report.classification.status).toBe("unclassified");
});
+328
View File
@@ -0,0 +1,328 @@
import { z } from "zod";
import { digestSchema } from "../digests.js";
import { localPathStringSchema } from "../localPath.js";
const uint = z.number().int().min(0).max(0xffffffff);
const recordedIntegerSchema = z.strictObject({
decimal: z.string().regex(/^-?(?:0|[1-9][0-9]*)$/u),
hex: z
.string()
.regex(/^0x[0-9a-f]+$/u)
.nullable(),
});
/** Hard ceiling on the caller-selected input read budget. */
export const MAX_INSPECT_APPLE_IPS_FILE_BYTES = 16777216;
/** Hard ceiling on threads examined in one report. */
export const MAX_INSPECT_APPLE_IPS_THREADS = 4096;
/** Hard ceiling on frames across every thread. */
export const MAX_INSPECT_APPLE_IPS_FRAMES = 65536;
/** Hard ceiling on usedImages rows. */
export const MAX_INSPECT_APPLE_IPS_IMAGES = 4096;
/** Hard ceiling on threadState register slots across every thread. */
export const MAX_INSPECT_APPLE_IPS_REGISTERS = 262144;
/** Conservative retained decimal/hex text capacity, measured as UTF-16 bytes. */
export const DEFAULT_INSPECT_APPLE_IPS_INTEGER_TEXT_BYTES = 8 * 1024 * 1024;
export const MAX_INSPECT_APPLE_IPS_INTEGER_TEXT_BYTES = 64 * 1024 * 1024;
export const APPLE_IPS_IMAGE_SOURCES = [
"P",
"S",
"C",
"K",
"U",
"T",
"A",
] as const;
export type AppleIpsImageSource = (typeof APPLE_IPS_IMAGE_SOURCES)[number];
/** Documented meaning of usedImages source letters. */
export const APPLE_IPS_IMAGE_SOURCE_MEANING: Readonly<
Record<AppleIpsImageSource, string>
> = {
P: "process",
S: "shared cache",
C: "shared cache library",
K: "kernel",
U: "kernel cache",
T: "kernel text exec",
A: "absolute",
};
/** Complete bounded inspection of one Apple .ips crash report. */
export const inspectAppleIpsInputSchema = z.strictObject({
path: localPathStringSchema.describe(
"Absolute filesystem path to an Apple .ips crash report",
),
max_file_bytes: z
.number()
.int()
.min(2)
.max(MAX_INSPECT_APPLE_IPS_FILE_BYTES)
.default(1048576)
.describe("Maximum stable artifact bytes to read (default 1 MiB)"),
max_threads: z
.number()
.int()
.min(1)
.max(MAX_INSPECT_APPLE_IPS_THREADS)
.default(256)
.describe(
"Maximum threads to examine; exceeding the budget fails without partial success",
),
max_frames: z
.number()
.int()
.min(1)
.max(MAX_INSPECT_APPLE_IPS_FRAMES)
.default(8192)
.describe(
"Maximum frames across every thread; exceeding the budget fails without partial success",
),
max_images: z
.number()
.int()
.min(1)
.max(MAX_INSPECT_APPLE_IPS_IMAGES)
.default(1024)
.describe(
"Maximum usedImages rows to examine; exceeding the budget fails without partial success",
),
max_registers: z
.number()
.int()
.min(1)
.max(MAX_INSPECT_APPLE_IPS_REGISTERS)
.default(16384)
.describe(
"Maximum threadState register slots across every thread; exceeding the budget fails without partial success",
),
max_integer_text_bytes: z
.number()
.int()
.min(1)
.max(MAX_INSPECT_APPLE_IPS_INTEGER_TEXT_BYTES)
.default(DEFAULT_INSPECT_APPLE_IPS_INTEGER_TEXT_BYTES)
.describe(
"Conservative UTF-16 byte allowance for all retained decimal/hex integer text, including derived addresses (default 8 MiB, maximum 64 MiB); checked before conversion and allocation",
),
});
export type InspectAppleIpsInput = z.infer<typeof inspectAppleIpsInputSchema>;
const uuidSchema = z
.string()
.regex(/^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$/u);
const metadataSchema = z.strictObject({
bug_type: z.literal("309"),
timestamp: z.string().min(1).nullable(),
os_version: z.string().min(1).nullable(),
bundle_id: z.string().min(1).nullable(),
incident_id: z.string().min(1).nullable(),
name: z.string().min(1).nullable(),
unlisted_keys: z.array(z.string().min(1)),
});
const exceptionSchema = z
.strictObject({
type: z.string().min(1).nullable(),
signal: z.string().min(1).nullable(),
subtype: z.string().min(1).nullable(),
codes: z.string().min(1).nullable(),
message: z.string().min(1).nullable(),
unlisted_keys: z.array(z.string().min(1)),
})
.nullable();
const terminationSchema = z
.strictObject({
namespace: z.string().min(1).nullable(),
code: recordedIntegerSchema.nullable(),
indicator: z.string().min(1).nullable(),
reasons: z.array(z.string().min(1)).nullable(),
unlisted_keys: z.array(z.string().min(1)),
})
.nullable();
const registerSchema = z.strictObject({
name: z.string().min(1),
value: recordedIntegerSchema,
description: z.string().min(1).nullable(),
});
const frameSchema = z.strictObject({
thread_index: uint,
frame_index: uint,
image_index: uint
.nullable()
.describe(
"Recorded index into images; image metadata and its uncertainty belong to that row.",
),
image_offset: recordedIntegerSchema.nullable(),
runtime_address: recordedIntegerSchema.nullable(),
role: z.enum([
"faulting_instruction",
"current_instruction",
"return_address",
"unknown",
]),
inline: z.boolean().nullable(),
recorded_symbol: z.string().min(1).nullable(),
});
const threadSchema = z.strictObject({
index: uint,
id: recordedIntegerSchema.nullable(),
triggered: z.boolean().nullable(),
name: z.string().min(1).nullable(),
queue: z.string().min(1).nullable(),
frames: z.array(frameSchema),
state_flavor: z.string().min(1).nullable(),
registers: z.array(registerSchema),
});
const imageSchema = z.strictObject({
index: uint,
source: z.string().min(1).nullable(),
source_meaning: z.string().min(1).nullable(),
arch: z.string().min(1).nullable(),
base: recordedIntegerSchema.nullable(),
size: recordedIntegerSchema.nullable(),
uuid: uuidSchema.nullable(),
uuid_unknown: z.boolean(),
path: z.string().min(1).nullable(),
});
const classificationMatchSchema = z.strictObject({
rule: z.enum([
"watchdog",
"dead10cc",
"thermal",
"guarded_file_descriptor",
"guarded_mach_port",
"possible_pointer_authentication_failure",
"dyld_missing_library",
]),
fields: z.array(
z.strictObject({
name: z.string().min(1),
value: z.string().min(1),
}),
),
});
const typeMismatchSchema = z.strictObject({
path: z.string().min(1),
expected: z.string().min(1),
observed: z.string().min(1),
recorded_value: z
.string()
.exactOptional()
.describe(
"Exact source spelling when a string fails its documented format",
),
recorded_integer: z
.string()
.regex(/^-?(?:0|[1-9][0-9]*)$/u)
.exactOptional()
.describe(
"Exact decimal literal of a rejected integer lookup; charged to the integer text allowance",
),
});
const coverageSchema = z.strictObject({
status: z.enum(["complete", "partial"]),
reason: z.string().min(1).nullable(),
threads: uint,
frames: uint,
images: uint,
registers: uint,
type_mismatches: z.array(typeMismatchSchema),
});
const osVersionSchema = z
.discriminatedUnion("form", [
z.strictObject({ form: z.literal("text"), text: z.string().min(1) }),
z.strictObject({
form: z.literal("structured"),
train: z.string().min(1).nullable(),
build: z.string().min(1).nullable(),
release_type: z.string().min(1).nullable(),
is_embedded: z.boolean().nullable(),
unlisted_keys: z.array(z.string().min(1)),
}),
])
.nullable();
/** Static Apple .ips crash-report inventory, read without execution. */
export const appleIpsSchema = z.strictObject({
artifact: z.strictObject({
path: z.string().min(1),
sha256: digestSchema,
bytes: uint,
}),
metadata: metadataSchema,
process: z.strictObject({
name: z.string().min(1).nullable(),
path: z.string().min(1).nullable(),
pid: recordedIntegerSchema.nullable(),
parent: z.string().min(1).nullable(),
cpu_type: z.string().min(1).nullable(),
translated: z.boolean().nullable(),
os_version: osVersionSchema,
}),
exception: exceptionSchema,
termination: terminationSchema,
faulting_thread: uint.nullable(),
threads: z.array(threadSchema),
images: z.array(imageSchema),
classification: z.discriminatedUnion("status", [
z.strictObject({
status: z.literal("classified"),
matches: z.array(classificationMatchSchema).min(1),
}),
z.strictObject({
status: z.literal("unclassified"),
matches: z.array(classificationMatchSchema).max(0),
}),
]),
coverage: coverageSchema,
unlisted_body_keys: z.array(z.string().min(1)),
limitations: z.array(z.string().min(1)),
});
export type AppleIps = z.infer<typeof appleIpsSchema>;
export type AppleIpsThread = z.infer<typeof threadSchema>;
export type AppleIpsImage = z.infer<typeof imageSchema>;
export type AppleIpsFrame = z.infer<typeof frameSchema>;
export type AppleIpsTypeMismatch = z.infer<typeof typeMismatchSchema>;
export type RecordedInteger = z.infer<typeof recordedIntegerSchema>;
/** Count each appearance, including repeated objects serialized at different paths. */
export const appleIpsIntegerTextBytes = (
value: RecordedInteger | null,
): number =>
value === null ? 0 : 2 * (value.decimal.length + (value.hex?.length ?? 0));
/** The named report fields own which observed and derived integers are retained. */
export const appleIpsReportIntegerTextBytes = (report: AppleIps): number => {
let bytes =
appleIpsIntegerTextBytes(report.process.pid) +
appleIpsIntegerTextBytes(report.termination?.code ?? null);
for (const image of report.images)
bytes +=
appleIpsIntegerTextBytes(image.base) +
appleIpsIntegerTextBytes(image.size);
for (const thread of report.threads) {
bytes += appleIpsIntegerTextBytes(thread.id);
for (const register of thread.registers)
bytes += appleIpsIntegerTextBytes(register.value);
for (const frame of thread.frames)
bytes +=
appleIpsIntegerTextBytes(frame.image_offset) +
appleIpsIntegerTextBytes(frame.runtime_address);
}
for (const mismatch of report.coverage.type_mismatches)
bytes += 2 * (mismatch.recorded_integer?.length ?? 0);
return bytes;
};
+36
View File
@@ -0,0 +1,36 @@
/** Distinguish corrupt recognized IPS files from unsupported bug types. */
export class AppleIpsFormatFailure extends Error {
constructor(
readonly kind: "malformed" | "unsupported",
message: string,
) {
super(message);
this.name = "AppleIpsFormatFailure";
}
}
/** Caller-selected budgets that bound IPS row expansion. */
export type AppleIpsSelectedBoundary =
| "threads"
| "frames"
| "images"
| "registers"
| "integer_text";
/** Fixed format ceilings that no caller argument raises. */
export type AppleIpsFixedBoundary =
| "termination_reasons"
| "unlisted_keys"
| "nesting";
/** Resource boundary exceeded while walking an IPS crash report. */
export class AppleIpsResourceFailure extends Error {
constructor(
readonly boundary: AppleIpsSelectedBoundary | AppleIpsFixedBoundary,
readonly maximum: number,
message: string,
) {
super(message);
this.name = "AppleIpsResourceFailure";
}
}
@@ -0,0 +1,94 @@
import type { AppleIps } from "../../domain/native/appleIps.js";
type Match = AppleIps["classification"]["matches"][number];
/** SIGKILL codes are compared once, on the canonical hexadecimal form. */
const TERMINATION_CODE_RULES: readonly {
readonly hex: string;
readonly rule: Match["rule"];
}[] = [
{ hex: "0x8badf00d", rule: "watchdog" },
{ hex: "0xdead10cc", rule: "dead10cc" },
{ hex: "0xc00010ff", rule: "thermal" },
];
const GUARD_RULES: readonly {
readonly marker: string;
readonly rule: Match["rule"];
}[] = [
{ marker: "GUARD_TYPE_FD", rule: "guarded_file_descriptor" },
{ marker: "GUARD_TYPE_MACH_PORT", rule: "guarded_mach_port" },
];
const terminationCodeMatches = (
code: NonNullable<AppleIps["termination"]>["code"],
): Match[] =>
code === null
? []
: TERMINATION_CODE_RULES.filter(({ hex }) => code.hex === hex).map(
({ rule, hex }) => ({
rule,
fields: [
{ name: "termination.code", value: code.decimal },
{ name: "termination.code_hex", value: hex },
],
}),
);
const exceptionMatches = (exception: AppleIps["exception"]): Match[] => {
const type = exception?.type ?? null;
const subtype = exception?.subtype ?? null;
if (subtype === null) return [];
const typeField =
type === null ? [] : [{ name: "exception.type", value: type }];
const fields = [...typeField, { name: "exception.subtype", value: subtype }];
const matches: Match[] =
type === "EXC_GUARD"
? GUARD_RULES.filter(({ marker }) => subtype.includes(marker)).map(
({ rule }) => ({ rule, fields }),
)
: [];
if (subtype.endsWith("(possible pointer authentication failure)"))
matches.push({ rule: "possible_pointer_authentication_failure", fields });
return matches;
};
const dyldMatches = (termination: AppleIps["termination"]): Match[] => {
if (termination?.namespace !== "DYLD") return [];
const { indicator } = termination;
const missingLibrary = (termination.reasons ?? []).find((reason) =>
reason.includes("Library not loaded"),
);
if (indicator !== "Library missing" && missingLibrary === undefined)
return [];
return [
{
rule: "dyld_missing_library",
fields: [
{ name: "termination.namespace", value: "DYLD" },
...(indicator === null
? []
: [{ name: "termination.indicator", value: indicator }]),
...(missingLibrary === undefined
? []
: [{ name: "termination.reasons", value: missingLibrary }]),
],
},
];
};
/** Match only documented crash classes against already-parsed report facts. */
export const classify = (
exception: AppleIps["exception"],
termination: AppleIps["termination"],
): AppleIps["classification"] => {
const matches = [
...terminationCodeMatches(termination?.code ?? null),
...exceptionMatches(exception),
...dyldMatches(termination),
];
return {
status: matches.length === 0 ? "unclassified" : "classified",
matches,
};
};
+187
View File
@@ -0,0 +1,187 @@
import {
appleIpsIntegerTextBytes,
type AppleIpsTypeMismatch,
type RecordedInteger,
} from "../../domain/native/appleIps.js";
import { AppleIpsResourceFailure } from "./AppleIps.errors.js";
import type { LosslessJson } from "./losslessJson.js";
export type Members = Readonly<Record<string, LosslessJson>>;
const UINT32_MAX = 0xffffffff;
const observedType = (value: LosslessJson | undefined): string =>
value === undefined
? "absent"
: value.kind === "omitted"
? value.type
: value.kind;
/**
* Typed field access that records every present value whose JSON type differs
* from the documented one, so wrong-type input never reads as observed absence.
* JSON `null` and a missing member both mean "not recorded".
*/
export class FieldReader {
readonly mismatches: AppleIpsTypeMismatch[] = [];
readonly #integers = new WeakMap<RecordedInteger, bigint>();
#integerTextBytes = 0;
constructor(readonly maximumIntegerTextBytes: number) {}
/** Reserve a safe decimal/hex upper bound before BigInt or string expansion. */
#reserve(decimalCharacters: number, hexadecimal: boolean): number {
// log16(10) < 5/6. Include the 0x prefix; signs only enlarge this bound.
const hexCharacters = hexadecimal
? Math.ceil((decimalCharacters * 5) / 6) + 2
: 0;
const bytes = 2 * (decimalCharacters + hexCharacters);
if (bytes > this.maximumIntegerTextBytes - this.#integerTextBytes)
throw new AppleIpsResourceFailure(
"integer_text",
this.maximumIntegerTextBytes,
`IPS integer text reservation exceeds the ${String(this.maximumIntegerTextBytes)}-byte UTF-16 allowance`,
);
this.#integerTextBytes += bytes;
return bytes;
}
#retain(value: bigint, decimal: string, reserved: number): RecordedInteger {
const recorded = {
decimal,
hex: value < 0n ? null : `0x${value.toString(16)}`,
};
this.#integerTextBytes -= reserved - appleIpsIntegerTextBytes(recorded);
return recorded;
}
recordedInteger(decimal: string): RecordedInteger {
const hexadecimal = !decimal.startsWith("-") || decimal === "-0";
const reserved = this.#reserve(decimal.length, hexadecimal);
const value = BigInt(decimal);
const recorded = this.#retain(value, decimal, reserved);
this.#integers.set(recorded, value);
return recorded;
}
add(
left: RecordedInteger | null,
right: RecordedInteger | null,
): RecordedInteger | null {
if (left === null || right === null) return null;
const a = this.#integers.get(left);
const b = this.#integers.get(right);
if (a === undefined || b === undefined)
throw new Error("IPS integer facts do not belong to this field reader");
const reserved = this.#reserve(
Math.max(left.decimal.length, right.decimal.length) + 1,
true,
);
const value = a + b;
return this.#retain(value, value.toString(), reserved);
}
mismatch(path: string, expected: string, value: LosslessJson | undefined) {
this.mismatches.push({ path, expected, observed: observedType(value) });
}
text(value: LosslessJson | undefined, path: string): string | null {
if (value === undefined || value.kind === "null") return null;
if (value.kind === "string")
return value.value.length === 0 ? null : value.value;
this.mismatch(path, "string", value);
return null;
}
integer(
value: LosslessJson | undefined,
path: string,
): RecordedInteger | null {
if (value === undefined || value.kind === "null") return null;
if (value.kind === "integer") return this.recordedInteger(value.decimal);
this.mismatch(path, "integer", value);
return null;
}
index(value: LosslessJson | undefined, path: string): number | null {
if (value?.kind === "integer") {
// Every uint32 fits in ten decimal digits, safely below Number's exact
// integer limit. Oversized source indices need no BigInt allocation.
const index = value.decimal.length <= 10 ? Number(value.decimal) : NaN;
if (Number.isInteger(index) && index >= 0 && index <= UINT32_MAX)
return index === 0 ? 0 : index;
this.#reserve(value.decimal.length, false);
this.mismatches.push({
path,
expected: "unsigned 32-bit integer",
observed: "integer out of range",
recorded_integer: value.decimal,
});
return null;
}
if (value === undefined || value.kind === "null") return null;
this.mismatch(path, "unsigned 32-bit integer", value);
return null;
}
/** Keep a recorded lookup separate from whether it resolves to an object. */
resolveIndex(
index: number | null,
rows: readonly LosslessJson[],
path: string,
): number | null {
if (index === null) return null;
const row = rows[index];
if (row?.kind === "object") return index;
const decimal = String(index);
this.#reserve(decimal.length, false);
this.mismatches.push({
path,
expected: "index of a recorded object row",
observed:
row === undefined
? "integer reference out of range"
: "integer references a non-object row",
recorded_integer: decimal,
});
return null;
}
boolean(value: LosslessJson | undefined, path: string): boolean | null {
if (value === undefined || value.kind === "null") return null;
if (value.kind === "boolean") return value.value;
this.mismatch(path, "boolean", value);
return null;
}
object(
value: LosslessJson | undefined,
path: string,
): { members: Members; unlisted: readonly string[] } | null {
if (value === undefined || value.kind === "null") return null;
if (value.kind === "object") return value;
this.mismatch(path, "object", value);
return null;
}
/** An array element is always recorded, so `null` there is a mismatch too. */
row(value: LosslessJson, path: string): Members {
if (value.kind === "object") return value.members;
this.mismatch(path, "object", value);
return {};
}
array(
value: LosslessJson | undefined,
path: string,
required: boolean,
): readonly LosslessJson[] | null {
if (value?.kind === "array") return value.items;
if (value === undefined || value.kind === "null") {
if (required) this.mismatch(path, "array", value);
return null;
}
this.mismatch(path, "array", value);
return null;
}
}
+232
View File
@@ -0,0 +1,232 @@
import type { InspectAppleIpsInput } from "../../domain/native/appleIps.js";
import {
AppleIpsFormatFailure,
AppleIpsResourceFailure,
type AppleIpsFixedBoundary,
} from "./AppleIps.errors.js";
import {
parseLosslessJson,
LosslessJsonFailure,
type JsonShape,
type LosslessJson,
} from "./losslessJson.js";
/** IPS JSON is at most a handful of levels deep; deeper input is not a crash report shape. */
const MAX_IPS_JSON_DEPTH = 64;
/** Unlisted member names retained per documented object. */
const MAX_UNLISTED_KEYS = 1024;
/** termination.reasons entries; real reports carry a few. */
const MAX_TERMINATION_REASONS = 256;
const SCALAR: JsonShape = { kind: "scalar" };
const scalarMembers = (
...names: readonly string[]
): Record<string, JsonShape> =>
Object.fromEntries(names.map((name) => [name, SCALAR]));
const fixedAllowance =
(boundary: AppleIpsFixedBoundary, maximum: number, noun: string) =>
(count: number) => {
if (count > maximum)
throw new AppleIpsResourceFailure(
boundary,
maximum,
`Crash report lists more than ${String(maximum)} ${noun}`,
);
};
const unlistedKeys = fixedAllowance(
"unlisted_keys",
MAX_UNLISTED_KEYS,
"unlisted keys in one object",
);
/** One running count per selected row collection, checked before each row. */
const selectedAllowances = (input: InspectAppleIpsInput) => {
const counts = { threads: 0, frames: 0, images: 0, registers: 0 };
const take =
(boundary: keyof typeof counts, maximum: number, noun: string) => () => {
counts[boundary] += 1;
if (counts[boundary] > maximum)
throw new AppleIpsResourceFailure(
boundary,
maximum,
`Crash report lists more than ${String(maximum)} ${noun}, above the selected ${String(maximum)}-entry ${boundary} allowance`,
);
};
return {
threads: take("threads", input.max_threads, "threads"),
frames: take("frames", input.max_frames, "frames"),
images: take("images", input.max_images, "usedImages rows"),
registers: take(
"registers",
input.max_registers,
"threadState register slots",
),
};
};
const threadShape = (
take: ReturnType<typeof selectedAllowances>,
): JsonShape => {
const register: JsonShape = {
kind: "object",
members: scalarMembers("value", "description"),
};
return {
kind: "object",
members: {
...scalarMembers("id", "triggered", "name", "queue"),
frames: {
kind: "array",
items: {
kind: "object",
members: scalarMembers(
"imageIndex",
"imageOffset",
"symbol",
"inline",
),
},
beforeItem: take.frames,
},
threadState: {
kind: "object",
members: {
flavor: SCALAR,
x: { kind: "array", items: register, beforeItem: take.registers },
},
rest: register,
beforeRest: take.registers,
},
},
};
};
const terminationShape = (): JsonShape => {
const reasonLimit = fixedAllowance(
"termination_reasons",
MAX_TERMINATION_REASONS,
"termination reasons",
);
let reasons = 0;
return {
kind: "object",
members: {
...scalarMembers("namespace", "code", "indicator"),
reasons: {
kind: "array",
items: SCALAR,
beforeItem: () => {
reasons += 1;
reasonLimit(reasons);
},
},
},
beforeUnlisted: unlistedKeys,
};
};
/**
* Projection of the documented IPS crash body. Budget callbacks fire before
* each row is parsed, so selected allowances bound allocation rather than
* output, and members the report does not interpret are never materialized.
*/
export const ipsBodyShape = (input: InspectAppleIpsInput): JsonShape => {
const take = selectedAllowances(input);
return {
kind: "object",
members: {
...scalarMembers(
"procName",
"procPath",
"pid",
"parentProc",
"cpuType",
"translated",
"faultingThread",
),
osVersion: {
kind: "object",
members: scalarMembers("train", "build", "releaseType", "isEmbedded"),
beforeUnlisted: unlistedKeys,
},
exception: {
kind: "object",
members: scalarMembers("type", "signal", "subtype", "codes", "message"),
beforeUnlisted: unlistedKeys,
},
termination: terminationShape(),
threads: {
kind: "array",
items: threadShape(take),
beforeItem: take.threads,
},
usedImages: {
kind: "array",
items: {
kind: "object",
members: scalarMembers(
"source",
"arch",
"base",
"size",
"uuid",
"path",
),
},
beforeItem: take.images,
},
},
beforeUnlisted: unlistedKeys,
};
};
/** Projection of the first-line IPS metadata object. */
export const IPS_METADATA_SHAPE: JsonShape = {
kind: "object",
members: scalarMembers(
"bug_type",
"timestamp",
"os_version",
"bundleID",
"incident_id",
"name",
),
beforeUnlisted: unlistedKeys,
};
/** Parse one complete JSON value under a projection, mapping failures to IPS errors. */
export const consumeJson = (
text: string,
label: string,
shape: JsonShape,
): LosslessJson => {
try {
const parsed = parseLosslessJson(text, 0, shape, {
maxDepth: MAX_IPS_JSON_DEPTH,
});
if (!/^[\u0020\u0009\u000a\u000d]*$/u.test(text.slice(parsed.end)))
throw new AppleIpsFormatFailure(
"malformed",
`${label} has trailing JSON text after the first complete value`,
);
return parsed.value;
} catch (cause) {
if (
cause instanceof AppleIpsFormatFailure ||
cause instanceof AppleIpsResourceFailure
)
throw cause;
if (cause instanceof LosslessJsonFailure && cause.reason === "depth")
throw new AppleIpsResourceFailure(
"nesting",
cause.maximum ?? MAX_IPS_JSON_DEPTH,
`${label}: ${cause.message}`,
);
const message =
cause instanceof LosslessJsonFailure ? cause.message : String(cause);
throw new AppleIpsFormatFailure("malformed", `${label}: ${message}`);
}
};
+672
View File
@@ -0,0 +1,672 @@
import { createHash } from "node:crypto";
import { expect, it } from "vitest";
import { inspectAppleIpsInputSchema } from "../../domain/native/appleIps.js";
import {
appleIpsBytes,
appleIpsMetadataLine,
appleIpsReport,
} from "../../../tests/fixtures/binaryDiagnostics/appleIps.js";
import {
AppleIpsFormatFailure,
AppleIpsResourceFailure,
} from "./AppleIps.errors.js";
import { parseAppleIps } from "./AppleIpsParser.js";
const input = inspectAppleIpsInputSchema.parse({
path: "/artifacts/report.ips",
});
const artifact = (bytes: Buffer) => ({
path: "/artifacts/report.ips",
sha256: createHash("sha256").update(bytes).digest("hex"),
bytes: bytes.length,
});
it.each(
["\f", "\v", "\u00a0", "\ufeff"].flatMap((character) =>
["metadata suffix", "body prefix", "body suffix"].map((position) => ({
character,
position,
})),
),
)(
"rejects non-JSON whitespace at $position: $character",
({ character, position }) => {
const metadata = appleIpsMetadataLine();
const body = '{"threads":[],"usedImages":[]}';
const bytes = Buffer.from(
position === "metadata suffix"
? `${metadata}${character}\n${body}`
: position === "body prefix"
? `${metadata}\n${character}${body}`
: `${metadata}\n${body}${character}`,
);
expect(() => parseAppleIps(bytes, artifact(bytes), input)).toThrow(
AppleIpsFormatFailure,
);
},
);
it("accepts only JSON whitespace around each object", () => {
const bytes = Buffer.from(
` \t${appleIpsMetadataLine()} \t\r\n \t\r\n{"threads":[],"usedImages":[]} \t\r\n`,
);
expect(parseAppleIps(bytes, artifact(bytes), input).coverage.status).toBe(
"complete",
);
});
it.each([
"not-a-uuid",
"Z".repeat(32),
`A-${"A".repeat(31)}`,
` ${"A".repeat(32)}`,
])("retains malformed UUID spelling %s in partial coverage", (recorded) => {
const parsed = parse(
withBody(
JSON.stringify({
threads: [],
usedImages: [{ uuid: recorded, arch: "arm64" }],
}),
),
);
expect(parsed.images[0]).toMatchObject({ uuid: null, uuid_unknown: true });
expect(parsed.coverage.status).toBe("partial");
expect(parsed.coverage.type_mismatches).toEqual([
{
path: "body.usedImages[0].uuid",
expected: "32 hexadecimal UUID digits with optional canonical hyphens",
observed: "malformed UUID string",
recorded_value: recorded,
},
]);
});
const integerAmplificationBytes = (): Buffer => {
const base = `1${"0".repeat(20000)}`;
const frames = Array.from(
{ length: 512 },
() => '{"imageIndex":0,"imageOffset":1}',
).join(",");
return Buffer.from(
`${appleIpsMetadataLine()}\n{"faultingThread":0,"threads":[{"frames":[${frames}]}],"usedImages":[{"base":${base},"size":4096,"arch":"arm64"}]}`,
);
};
it("retains image metadata once while frames refer to its owner", () => {
const arch = "unknown_arch_" + "x".repeat(20000);
const uuid = "AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAAAAAA";
const frames = Array.from(
{ length: 512 },
() => '{"imageIndex":0,"imageOffset":1}',
).join(",");
const bytes = Buffer.from(
`${appleIpsMetadataLine()}\n{"faultingThread":0,"threads":[{"frames":[${frames}]}],"usedImages":[{"base":4294967296,"size":4096,"arch":${JSON.stringify(arch)},"uuid":${JSON.stringify(uuid)}}]}`,
);
const report = parseAppleIps(bytes, artifact(bytes), input);
expect(report.images[0]).toMatchObject({ arch, uuid });
expect(report.threads[0]?.frames).toHaveLength(512);
expect(report.threads[0]?.frames[0]?.image_index).toBe(0);
const serialized = JSON.stringify(report);
expect(serialized.split(arch)).toHaveLength(2);
expect(serialized.split(uuid)).toHaveLength(2);
});
it("refuses integer text amplification before retaining every derived address", () => {
const bytes = integerAmplificationBytes();
expect(bytes.length).toBeLessThan(input.max_file_bytes);
expect(() => parseAppleIps(bytes, artifact(bytes), input)).toThrow(
expect.objectContaining({
boundary: "integer_text",
maximum: 8 * 1024 * 1024,
}),
);
});
it("honors a larger integer text allowance without changing decimal facts", () => {
const bytes = integerAmplificationBytes();
const options = inspectAppleIpsInputSchema.parse({
path: input.path,
max_integer_text_bytes: 64 * 1024 * 1024,
});
const report = parseAppleIps(bytes, artifact(bytes), options);
expect(report.threads[0]?.frames).toHaveLength(512);
expect(report.images[0]?.base?.decimal).toBe(`1${"0".repeat(20000)}`);
expect(report.threads[0]?.frames[511]?.runtime_address?.decimal).toBe(
`1${"0".repeat(19999)}1`,
);
});
it("reads a 309 crash report, uppercase UUID, and lossless register", () => {
const bytes = appleIpsBytes();
const report = parseAppleIps(bytes, artifact(bytes), input);
expect(report.metadata.bug_type).toBe("309");
expect(report.exception?.type).toBe("EXC_BAD_ACCESS");
expect(report.exception?.signal).toBe("SIGSEGV");
expect(report.images[0]?.uuid).toBe("AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAAAAAA");
expect(report.images[0]?.source_meaning).toBe("process");
expect(report.images[1]?.uuid_unknown).toBe(true);
expect(report.threads[0]?.frames[0]).toMatchObject({
role: "faulting_instruction",
recorded_symbol: "rea_fixture_fault",
});
expect(report.threads[0]?.registers[0]).toMatchObject({
name: "x0",
value: {
decimal: "9594637537912899908",
hex: "0x8527000193ed4944",
},
});
expect(report.classification.status).toBe("unclassified");
expect(report.threads[0]?.frames[0]?.runtime_address?.hex).toBe(
"0x100000338",
);
});
it("refuses a stackshot bug_type without a crash body", () => {
const bytes = appleIpsBytes({ bugType: "288" });
expect(() => parseAppleIps(bytes, artifact(bytes), input)).toThrow(
AppleIpsFormatFailure,
);
try {
parseAppleIps(bytes, artifact(bytes), input);
} catch (cause) {
expect(cause).toMatchObject({
kind: "unsupported",
message: expect.stringContaining("288"),
});
}
});
it("fails closed on a truncated crash body", () => {
const bytes = appleIpsBytes({ truncateBody: true });
expect(() => parseAppleIps(bytes, artifact(bytes), input)).toThrow(
AppleIpsFormatFailure,
);
});
it("matches documented watchdog, guard, PAC and dyld rules", () => {
const watchdog = parseAppleIps(
appleIpsBytes({ terminationCode: "2343432205" }),
artifact(appleIpsBytes({ terminationCode: "2343432205" })),
input,
);
expect(watchdog.classification.matches.map((row) => row.rule)).toEqual([
"watchdog",
]);
const guard = parseAppleIps(
appleIpsBytes({
exceptionType: "EXC_GUARD",
subtype: "GUARD_TYPE_FD",
}),
artifact(
appleIpsBytes({ exceptionType: "EXC_GUARD", subtype: "GUARD_TYPE_FD" }),
),
input,
);
expect(guard.classification.matches[0]?.rule).toBe("guarded_file_descriptor");
const pac = parseAppleIps(
appleIpsBytes({
subtype: "KERN_INVALID_ADDRESS (possible pointer authentication failure)",
}),
artifact(
appleIpsBytes({
subtype:
"KERN_INVALID_ADDRESS (possible pointer authentication failure)",
}),
),
input,
);
expect(pac.classification.matches[0]?.rule).toBe(
"possible_pointer_authentication_failure",
);
const dyld = parseAppleIps(
appleIpsBytes({
terminationNamespace: "DYLD",
terminationIndicator: "Library missing",
terminationReasons: ["Library not loaded: @rpath/libreamissing.dylib"],
}),
artifact(
appleIpsBytes({
terminationNamespace: "DYLD",
terminationIndicator: "Library missing",
terminationReasons: ["Library not loaded: @rpath/libreamissing.dylib"],
}),
),
input,
);
expect(dyld.classification.matches[0]).toMatchObject({
rule: "dyld_missing_library",
});
});
it("treats an empty image path as omitted and accepts CRLF line endings", () => {
const bytes = appleIpsBytes();
const crlf = Buffer.from(appleIpsReport().replaceAll("\n", "\r\n"), "utf8");
const report = parseAppleIps(bytes, artifact(bytes), input);
expect(report.images[1]?.path).toBeNull();
const crlfReport = parseAppleIps(crlf, artifact(crlf), input);
expect(crlfReport.metadata.bug_type).toBe("309");
expect(crlfReport.threads[0]?.frames[0]?.role).toBe("faulting_instruction");
});
it("fails closed when the image budget is exhausted", () => {
const bytes = appleIpsBytes();
expect(() =>
parseAppleIps(
bytes,
artifact(bytes),
inspectAppleIpsInputSchema.parse({
path: "/artifacts/report.ips",
max_images: 1,
}),
),
).toThrow(AppleIpsResourceFailure);
});
const parse = (bytes: Buffer, options: Record<string, number> = {}) =>
parseAppleIps(
bytes,
artifact(bytes),
inspectAppleIpsInputSchema.parse({
path: "/artifacts/report.ips",
...options,
}),
);
const withBody = (body: string): Buffer =>
Buffer.from(`${appleIpsMetadataLine()}\n${body}`, "utf8");
const report = (options: Parameters<typeof appleIpsReport>[0]) =>
parse(appleIpsBytes(options));
it("keeps ReportCrash-shaped producer facts and reports complete coverage", () => {
const parsed = report({});
expect(parsed.process.os_version).toEqual({
form: "structured",
train: "macOS 15.0",
build: "24A335",
release_type: "User",
is_embedded: null,
unlisted_keys: [],
});
expect(
report({ osVersionText: "Version 15.0 (Build 24A335)" }).process.os_version,
).toEqual({
form: "text",
text: "Version 15.0 (Build 24A335)",
});
expect(parsed.threads[0]?.state_flavor).toBe("ARM_THREAD_STATE64");
expect(parsed.threads[0]?.registers.map((row) => row.name)).toEqual([
"x0",
"x1",
"lr",
"esr",
"pc",
"far",
]);
expect(parsed.threads[0]?.registers[3]).toEqual({
name: "esr",
value: { decimal: "2449473606", hex: "0x92000046" },
description: "(Data Abort) byte read Translation fault",
});
expect(parsed.exception?.unlisted_keys).toEqual(["rawCodes"]);
expect(parsed.termination?.unlisted_keys).toEqual([
"flags",
"byProc",
"byPid",
]);
expect(parsed.unlisted_body_keys).toContain("sharedCache");
expect(parsed.unlisted_body_keys).not.toContain("osVersion");
expect(parsed.metadata.unlisted_keys).toContain("slice_uuid");
expect(parsed.coverage).toEqual({
status: "complete",
reason: null,
threads: 2,
frames: 5,
images: 2,
registers: 6,
type_mismatches: [],
});
});
it("assigns frame roles by recorded row without inventing inline stack addresses", () => {
const roles = (parsed: ReturnType<typeof parse>) =>
parsed.threads.map((thread) => thread.frames.map((frame) => frame.role));
expect(roles(report({}))).toEqual([
["faulting_instruction", "return_address"],
["current_instruction", "return_address", "return_address"],
]);
expect(report({}).threads[1]?.frames[0]?.inline).toBe(true);
const unknownFault = parse(
Buffer.from(appleIpsReport().replace('"faultingThread": 0,', ""), "utf8"),
);
expect(unknownFault.faulting_thread).toBeNull();
expect(roles(unknownFault)).toEqual([
["unknown", "return_address"],
["unknown", "return_address", "return_address"],
]);
});
it.each(["5", "4294967295"])(
"preserves unresolved faulting index %s without establishing top-frame roles",
(index) => {
const parsed = parse(
withBody(
`{"faultingThread":${index},"threads":[{"frames":[{},{}]}],"usedImages":[]}`,
),
);
expect(parsed.faulting_thread).toBe(Number(index));
expect(parsed.threads[0]?.frames.map((frame) => frame.role)).toEqual([
"unknown",
"return_address",
]);
expect(parsed.coverage.status).toBe("partial");
expect(parsed.coverage.type_mismatches).toContainEqual(
expect.objectContaining({
path: "body.faultingThread",
recorded_integer: index,
}),
);
},
);
it("does not resolve a faulting reference to a malformed thread row", () => {
const parsed = parse(
withBody(
'{"faultingThread":0,"threads":[null,{"frames":[{}]}],"usedImages":[]}',
),
);
expect(parsed.faulting_thread).toBe(0);
expect(parsed.threads[1]?.frames[0]?.role).toBe("unknown");
expect(parsed.coverage.type_mismatches).toContainEqual(
expect.objectContaining({
path: "body.faultingThread",
recorded_integer: "0",
}),
);
});
it.each(["-1", "4294967296", `1${"0".repeat(1000)}`])(
"preserves rejected index spelling (case %#)",
(index) => {
const parsed = parse(
withBody(
`{"faultingThread":${index},"threads":[{"frames":[{"imageIndex":${index}}]}],"usedImages":[]}`,
),
);
expect(parsed.faulting_thread).toBeNull();
expect(parsed.threads[0]?.frames[0]?.image_index).toBeNull();
expect(
parsed.coverage.type_mismatches.map(
(mismatch) => mismatch.recorded_integer,
),
).toEqual([index, index]);
},
);
it("bounds rejected index text before retaining it", () => {
const bytes = withBody(
'{"faultingThread":4294967296,"threads":[],"usedImages":[]}',
);
expect(() =>
parseAppleIps(bytes, artifact(bytes), {
...input,
max_integer_text_bytes: 19,
}),
).toThrow(expect.objectContaining({ boundary: "integer_text", maximum: 19 }));
const parsed = parseAppleIps(bytes, artifact(bytes), {
...input,
max_integer_text_bytes: 20,
});
expect(parsed.coverage.type_mismatches[0]).toMatchObject({
recorded_integer: "4294967296",
});
});
it("leaves inline addresses unknown when their own offset is absent", () => {
const parsed = parse(
withBody(
'{"faultingThread":0,"threads":[{"frames":[{"imageIndex":0,"inline":true},{"imageIndex":0,"imageOffset":8,"inline":true},{"imageIndex":1,"imageOffset":16}]}],"usedImages":[{"base":4096},{"base":8192}]}',
),
);
expect(
parsed.threads[0]?.frames.map(
(frame) => frame.runtime_address?.decimal ?? null,
),
).toEqual([null, "4104", "8208"]);
expect(
parsed.threads[0]?.frames.map(
(frame) => frame.image_offset?.decimal ?? null,
),
).toEqual([null, "8", "16"]);
});
it("preserves an unresolved image index without claiming complete coverage", () => {
const parsed = parse(
withBody(
'{"threads":[{"frames":[{"imageIndex":7,"imageOffset":8}]}],"usedImages":[]}',
),
);
expect(parsed.threads[0]?.frames[0]).toMatchObject({
image_index: 7,
image_offset: { decimal: "8" },
runtime_address: null,
});
expect(parsed.coverage.status).toBe("partial");
expect(parsed.coverage.type_mismatches[0]).toMatchObject({
path: "body.threads[0].frames[0].imageIndex",
recorded_integer: "7",
});
});
it("classifies SIGKILL codes by canonical hex, not a near-miss decimal", () => {
const rules = (code: string) =>
report({ terminationCode: code }).classification.matches;
expect(rules("3735933132")).toEqual([]);
expect(rules("3735883980")).toEqual([
{
rule: "dead10cc",
fields: [
{ name: "termination.code", value: "3735883980" },
{ name: "termination.code_hex", value: "0xdead10cc" },
],
},
]);
expect(rules("3221229823").map((row) => row.rule)).toEqual(["thermal"]);
});
it("reports wrong-type collections as partial instead of empty and complete", () => {
const parsed = parse(
withBody(
'{"procName":"x","threads":{"0":{}},"usedImages":"none","exception":{"type":"EXC_CRASH"}}',
),
);
expect(parsed.threads).toEqual([]);
expect(parsed.images).toEqual([]);
expect(parsed.exception?.type).toBe("EXC_CRASH");
expect(parsed.coverage.status).toBe("partial");
expect(parsed.coverage.reason).toMatch(/type_mismatches/u);
expect(parsed.coverage.type_mismatches).toEqual([
{ path: "body.usedImages", expected: "array", observed: "string" },
{ path: "body.threads", expected: "array", observed: "object" },
]);
const absent = parse(withBody('{"procName":"x"}'));
expect(absent.coverage.type_mismatches).toEqual([
{ path: "body.usedImages", expected: "array", observed: "absent" },
{ path: "body.threads", expected: "array", observed: "absent" },
]);
const empty = parse(withBody('{"threads":[],"usedImages":[]}'));
expect(empty.coverage.status).toBe("complete");
});
it("records wrong-type rows, scalars and registers with their source path", () => {
const parsed = parse(
withBody(`{
"pid": "4242",
"osVersion": 15,
"faultingThread": -1,
"termination": { "code": 1.5, "reasons": ["ok", 7] },
"threads": [
7,
{
"frames": [{ "imageIndex": 99999999999, "imageOffset": "8" }, []],
"threadState": { "flavor": 1, "x": [1, "two", { "value": null }, {}], "rosetta": { "tmp0": 1 } }
}
],
"usedImages": [null, { "base": 1.0 }]
}`),
);
expect(parsed.process.pid).toBeNull();
expect(parsed.faulting_thread).toBeNull();
expect(parsed.termination?.reasons).toEqual(["ok"]);
expect(parsed.threads).toHaveLength(2);
expect(parsed.threads[1]?.registers).toEqual([
{ name: "x0", value: { decimal: "1", hex: "0x1" }, description: null },
]);
expect(parsed.images).toHaveLength(2);
expect(parsed.coverage.status).toBe("partial");
expect(parsed.coverage.type_mismatches).toEqual([
{ path: "body.usedImages[0]", expected: "object", observed: "null" },
{
path: "body.usedImages[1].base",
expected: "integer",
observed: "number",
},
{
path: "body.faultingThread",
expected: "unsigned 32-bit integer",
observed: "integer out of range",
recorded_integer: "-1",
},
{ path: "body.threads[0]", expected: "object", observed: "integer" },
{
path: "body.threads[1].frames[0].imageIndex",
expected: "unsigned 32-bit integer",
observed: "integer out of range",
recorded_integer: "99999999999",
},
{
path: "body.threads[1].frames[0].imageOffset",
expected: "integer",
observed: "string",
},
{
path: "body.threads[1].frames[1]",
expected: "object",
observed: "array",
},
{
path: "body.threads[1].threadState.x[1]",
expected: "integer or object with integer value",
observed: "string",
},
{
path: "body.threads[1].threadState.x[3].value",
expected: "integer",
observed: "absent",
},
{
path: "body.threads[1].threadState.rosetta.value",
expected: "integer",
observed: "absent",
},
{
path: "body.threads[1].threadState.flavor",
expected: "string",
observed: "integer",
},
{
path: "body.termination.code",
expected: "integer",
observed: "number",
},
{
path: "body.termination.reasons[1]",
expected: "string",
observed: "integer",
},
{ path: "body.pid", expected: "integer", observed: "string" },
{
path: "body.osVersion",
expected: "string or object",
observed: "integer",
},
]);
});
it("refuses invalid UTF-8 instead of replacing bytes", () => {
const valid = appleIpsBytes({ procName: "rea-é" });
const offset = valid.indexOf(Buffer.from("é", "utf8"));
const corrupt = Buffer.from(valid);
corrupt[offset] = 0xff;
expect(parse(valid).process.name).toBe("rea-é");
expect(() => parse(corrupt)).toThrow(
expect.objectContaining({
kind: "malformed",
message: expect.stringMatching(/UTF-8/u),
}),
);
});
const registerFlood = (count: number): Buffer =>
withBody(
`{"threads":[{"threadState":{"x":[${Array.from({ length: count }, () => "0").join(",")}]}}],"usedImages":[]}`,
);
it("budgets register slots before expanding threadState rows", () => {
const flood = registerFlood(150_000);
expect(flood.length).toBeLessThan(400_000);
expect(() => parse(flood)).toThrow(
expect.objectContaining({ boundary: "registers", maximum: 16384 }),
);
expect(parse(registerFlood(4), { max_registers: 4 }).coverage.registers).toBe(
4,
);
expect(() => parse(registerFlood(5), { max_registers: 4 })).toThrow(
AppleIpsResourceFailure,
);
});
it("refuses an over-budget collection before parsing the rows past the allowance", () => {
// Rows after the allowance are invalid JSON; the budget must refuse first.
const threads = withBody(`{"threads":[{},{},{},?],"usedImages":[]}`);
expect(() => parse(threads, { max_threads: 3 })).toThrow(
expect.objectContaining({ boundary: "threads", maximum: 3 }),
);
const frames = withBody(
`{"threads":[{"frames":[{},{}]},{"frames":[{},?]}],"usedImages":[]}`,
);
expect(() => parse(frames, { max_frames: 3 })).toThrow(
expect.objectContaining({ boundary: "frames", maximum: 3 }),
);
const images = withBody(`{"threads":[],"usedImages":[{},?]}`);
expect(() => parse(images, { max_images: 1 })).toThrow(
expect.objectContaining({ boundary: "images", maximum: 1 }),
);
});
it("skips unconsumed producer members and bounds fixed structural allowances", () => {
const skipped = parse(
withBody(
`{"threads":[],"usedImages":[],"lastExceptionBacktrace":[${Array.from({ length: 100_000 }, () => "{}").join(",")}]}`,
),
);
expect(skipped.coverage.status).toBe("complete");
expect(skipped.unlisted_body_keys).toEqual(["lastExceptionBacktrace"]);
const keys = Array.from(
{ length: 1025 },
(_, index) => `"k${String(index)}":0`,
).join(",");
expect(() => parse(withBody(`{${keys}}`))).toThrow(
expect.objectContaining({ boundary: "unlisted_keys", maximum: 1024 }),
);
const deep = `${"[".repeat(100)}${"]".repeat(100)}`;
expect(() => parse(withBody(`{"vmSummary":${deep}}`))).toThrow(
expect.objectContaining({ boundary: "nesting", maximum: 64 }),
);
const reasons = JSON.stringify(Array.from({ length: 257 }, () => "r"));
expect(() =>
parse(withBody(`{"termination":{"reasons":${reasons}}}`)),
).toThrow(expect.objectContaining({ boundary: "termination_reasons" }));
});
+434
View File
@@ -0,0 +1,434 @@
import {
APPLE_IPS_IMAGE_SOURCE_MEANING,
APPLE_IPS_IMAGE_SOURCES,
type AppleIps,
type AppleIpsFrame,
type AppleIpsImage,
type AppleIpsImageSource,
type AppleIpsThread,
type InspectAppleIpsInput,
} from "../../domain/native/appleIps.js";
import { AppleIpsFormatFailure } from "./AppleIps.errors.js";
import { classify } from "./AppleIpsClassification.js";
import { FieldReader } from "./AppleIpsFieldReader.js";
import {
consumeJson,
ipsBodyShape,
IPS_METADATA_SHAPE,
} from "./AppleIpsJsonShape.js";
import type { LosslessJson } from "./losslessJson.js";
type Artifact = AppleIps["artifact"];
const ZERO_UUID = "00000000000000000000000000000000";
const recordedUuidPattern =
/^(?:[0-9a-fA-F]{32}|[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$/u;
const IPS_LIMITATIONS = [
"Rows reproduce the selected .ips JSON; they do not establish that the process still exists or that a live kernel agrees with the recorded state.",
"Opened Mach-O and dSYM UUID joins remain unobserved.",
"Symbol names printed inside the report are recorded claims and are not verified.",
];
const utf8 = new TextDecoder("utf-8", { fatal: true });
const canonicalUuid = (
raw: string | null,
): { uuid: string | null; unknown: boolean; malformed: boolean } => {
if (raw === null) return { uuid: null, unknown: true, malformed: false };
if (!recordedUuidPattern.test(raw))
return { uuid: null, unknown: true, malformed: true };
const hex = raw.replaceAll("-", "").toUpperCase();
if (hex === ZERO_UUID) return { uuid: null, unknown: true, malformed: false };
return {
uuid: `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`,
unknown: false,
malformed: false,
};
};
const sourceMeaning = (source: string | null): string | null => {
if (source === null) return null;
return (APPLE_IPS_IMAGE_SOURCES as readonly string[]).includes(source)
? APPLE_IPS_IMAGE_SOURCE_MEANING[source as AppleIpsImageSource]
: null;
};
const exceptionOf = (
value: LosslessJson | undefined,
read: FieldReader,
): AppleIps["exception"] => {
const object = read.object(value, "body.exception");
if (object === null) return null;
const { members } = object;
return {
type: read.text(members.type, "body.exception.type"),
signal: read.text(members.signal, "body.exception.signal"),
subtype: read.text(members.subtype, "body.exception.subtype"),
codes: read.text(members.codes, "body.exception.codes"),
message: read.text(members.message, "body.exception.message"),
unlisted_keys: [...object.unlisted],
};
};
const terminationOf = (
value: LosslessJson | undefined,
read: FieldReader,
): AppleIps["termination"] => {
const object = read.object(value, "body.termination");
if (object === null) return null;
const { members } = object;
const reasonItems = read.array(
members.reasons,
"body.termination.reasons",
false,
);
return {
namespace: read.text(members.namespace, "body.termination.namespace"),
code: read.integer(members.code, "body.termination.code"),
indicator: read.text(members.indicator, "body.termination.indicator"),
reasons:
reasonItems === null
? null
: reasonItems.flatMap((item, index) => {
const reason = read.text(
item,
`body.termination.reasons[${String(index)}]`,
);
return reason === null ? [] : [reason];
}),
unlisted_keys: [...object.unlisted],
};
};
const osVersionOf = (
value: LosslessJson | undefined,
read: FieldReader,
): AppleIps["process"]["os_version"] => {
if (value === undefined || value.kind === "null") return null;
if (value.kind === "string")
return value.value.length === 0
? null
: { form: "text", text: value.value };
if (value.kind !== "object") {
read.mismatch("body.osVersion", "string or object", value);
return null;
}
const { members } = value;
return {
form: "structured",
train: read.text(members.train, "body.osVersion.train"),
build: read.text(members.build, "body.osVersion.build"),
release_type: read.text(members.releaseType, "body.osVersion.releaseType"),
is_embedded: read.boolean(members.isEmbedded, "body.osVersion.isEmbedded"),
unlisted_keys: [...value.unlisted],
};
};
const registerValue = (
name: string,
value: LosslessJson,
path: string,
read: FieldReader,
): AppleIpsThread["registers"][number] | null => {
if (value.kind === "null") return null;
if (value.kind === "integer")
return {
name,
value: read.recordedInteger(value.decimal),
description: null,
};
if (value.kind !== "object") {
read.mismatch(path, "integer or object with integer value", value);
return null;
}
const integer = read.integer(value.members.value, `${path}.value`);
if (integer === null) {
if (value.members.value === undefined)
read.mismatch(`${path}.value`, "integer", undefined);
return null;
}
return {
name,
value: integer,
description: read.text(value.members.description, `${path}.description`),
};
};
const threadStateOf = (
value: LosslessJson | undefined,
path: string,
read: FieldReader,
): Pick<AppleIpsThread, "state_flavor" | "registers"> => {
const object = read.object(value, path);
if (object === null) return { state_flavor: null, registers: [] };
const { members } = object;
const registers: AppleIpsThread["registers"] = [];
const vector = read.array(members.x, `${path}.x`, false);
vector?.forEach((entry, index) => {
const register = registerValue(
`x${String(index)}`,
entry,
`${path}.x[${String(index)}]`,
read,
);
if (register !== null) registers.push(register);
});
for (const [key, entry] of Object.entries(members)) {
if (key === "x" || key === "flavor") continue;
const register = registerValue(key, entry, `${path}.${key}`, read);
if (register !== null) registers.push(register);
}
return {
state_flavor: read.text(members.flavor, `${path}.flavor`),
registers,
};
};
const imagesOf = (
rows: readonly LosslessJson[],
read: FieldReader,
): AppleIpsImage[] =>
rows.map((row, index) => {
const path = `body.usedImages[${String(index)}]`;
const members = read.row(row, path);
const source = read.text(members.source, `${path}.source`);
const recordedUuid = read.text(members.uuid, `${path}.uuid`);
const uuid = canonicalUuid(recordedUuid);
if (uuid.malformed && recordedUuid !== null)
read.mismatches.push({
path: `${path}.uuid`,
expected: "32 hexadecimal UUID digits with optional canonical hyphens",
observed: "malformed UUID string",
recorded_value: recordedUuid,
});
const arch = read.text(members.arch, `${path}.arch`);
return {
index,
source,
source_meaning: sourceMeaning(source),
arch,
base: read.integer(members.base, `${path}.base`),
size: read.integer(members.size, `${path}.size`),
uuid: uuid.uuid,
uuid_unknown: uuid.unknown || arch === null,
path: read.text(members.path, `${path}.path`),
};
});
/**
* Frame 0 is the thread's sampled program counter; later frames are unwound
* return addresses. Keep each row's own recorded address, including inline
* rows: the inline flag does not establish a replacement address or row index.
*/
const frameRole = (
frameIndex: number,
threadIndex: number,
faultingThread: number | null,
): AppleIpsFrame["role"] => {
if (frameIndex > 0) return "return_address";
if (faultingThread === null) return "unknown";
return threadIndex === faultingThread
? "faulting_instruction"
: "current_instruction";
};
const threadsOf = (
rows: readonly LosslessJson[],
imageRows: readonly LosslessJson[],
images: readonly AppleIpsImage[],
faultingThread: number | null,
read: FieldReader,
): AppleIpsThread[] =>
rows.map((row, threadIndex) => {
const path = `body.threads[${String(threadIndex)}]`;
const members = read.row(row, path);
const frames = (
read.array(members.frames, `${path}.frames`, false) ?? []
).map((frame, frameIndex): AppleIpsFrame => {
const framePath = `${path}.frames[${String(frameIndex)}]`;
const frameMembers = read.row(frame, framePath);
const imageIndex = read.index(
frameMembers.imageIndex,
`${framePath}.imageIndex`,
);
const resolvedImage = read.resolveIndex(
imageIndex,
imageRows,
`${framePath}.imageIndex`,
);
const image = resolvedImage === null ? undefined : images[resolvedImage];
const imageOffset = read.integer(
frameMembers.imageOffset,
`${framePath}.imageOffset`,
);
const inline = read.boolean(frameMembers.inline, `${framePath}.inline`);
const role = frameRole(frameIndex, threadIndex, faultingThread);
return {
thread_index: threadIndex,
frame_index: frameIndex,
image_index: imageIndex,
image_offset: imageOffset,
runtime_address: read.add(image?.base ?? null, imageOffset),
role,
inline,
recorded_symbol: read.text(frameMembers.symbol, `${framePath}.symbol`),
};
});
return {
index: threadIndex,
id: read.integer(members.id, `${path}.id`),
triggered: read.boolean(members.triggered, `${path}.triggered`),
name: read.text(members.name, `${path}.name`),
queue: read.text(members.queue, `${path}.queue`),
frames,
...threadStateOf(members.threadState, `${path}.threadState`, read),
};
});
/** Split the UTF-8 text into the first-line metadata object and the crash body. */
const crashText = (bytes: Buffer): { metadata: string; body: string } => {
let text: string;
try {
text = utf8.decode(bytes);
} catch {
throw new AppleIpsFormatFailure(
"malformed",
"IPS crash reports are UTF-8 JSON text; the selected bytes are not valid UTF-8",
);
}
if (text.includes("\u0000"))
throw new AppleIpsFormatFailure(
"malformed",
"IPS crash reports are UTF-8 JSON text and cannot contain NUL",
);
const newline = text.search(/\r\n|\n|\r/u);
if (newline <= 0)
throw new AppleIpsFormatFailure(
"malformed",
"IPS crash report must start with a metadata JSON object on the first line",
);
const lineEnd = text.startsWith("\r\n", newline) ? newline + 2 : newline + 1;
return { metadata: text.slice(0, newline), body: text.slice(lineEnd) };
};
const metadataOf = (text: string, read: FieldReader): AppleIps["metadata"] => {
const json = consumeJson(text, "IPS metadata", IPS_METADATA_SHAPE);
if (json.kind !== "object")
throw new AppleIpsFormatFailure(
"malformed",
"IPS metadata is not a JSON object",
);
const { members } = json;
const bugType = members.bug_type;
if (bugType?.kind !== "string" || bugType.value.length === 0)
throw new AppleIpsFormatFailure(
"malformed",
"IPS metadata bug_type is missing or not a string",
);
if (bugType.value !== "309")
throw new AppleIpsFormatFailure(
"unsupported",
`IPS bug_type ${JSON.stringify(bugType.value)} is not a crash report (309); this operation does not decode other DiagnosticReports types`,
);
return {
bug_type: bugType.value,
timestamp: read.text(members.timestamp, "metadata.timestamp"),
os_version: read.text(members.os_version, "metadata.os_version"),
bundle_id: read.text(members.bundleID, "metadata.bundleID"),
incident_id: read.text(members.incident_id, "metadata.incident_id"),
name: read.text(members.name, "metadata.name"),
unlisted_keys: [...json.unlisted],
};
};
const coverageOf = (
threads: readonly AppleIpsThread[],
images: readonly AppleIpsImage[],
mismatches: AppleIps["coverage"]["type_mismatches"],
): AppleIps["coverage"] => {
let frames = 0;
let registers = 0;
for (const thread of threads) {
frames += thread.frames.length;
registers += thread.registers.length;
}
return {
status: mismatches.length === 0 ? "complete" : "partial",
reason:
mismatches.length === 0
? null
: `${String(mismatches.length)} recorded value(s) did not satisfy the documented type, format or reference and were not interpreted; see type_mismatches`,
threads: threads.length,
frames,
images: images.length,
registers,
type_mismatches: mismatches,
};
};
/** Read one Apple .ips crash report without executing a target. */
export const parseAppleIps = (
bytes: Buffer,
artifact: Artifact,
input: InspectAppleIpsInput,
): AppleIps => {
const text = crashText(bytes);
const read = new FieldReader(input.max_integer_text_bytes);
const metadata = metadataOf(text.metadata, read);
if (text.body.length === 0)
throw new AppleIpsFormatFailure(
"malformed",
"IPS crash report body is missing after metadata line",
);
const bodyJson = consumeJson(
text.body,
"IPS crash body",
ipsBodyShape(input),
);
if (bodyJson.kind !== "object")
throw new AppleIpsFormatFailure(
"malformed",
"IPS crash body is not a JSON object",
);
const body = bodyJson.members;
const imageRows = read.array(body.usedImages, "body.usedImages", true) ?? [];
const images = imagesOf(imageRows, read);
const threadRows = read.array(body.threads, "body.threads", true) ?? [];
const faultingThread = read.index(body.faultingThread, "body.faultingThread");
const resolvedFaultingThread = read.resolveIndex(
faultingThread,
threadRows,
"body.faultingThread",
);
const threads = threadsOf(
threadRows,
imageRows,
images,
resolvedFaultingThread,
read,
);
const exception = exceptionOf(body.exception, read);
const termination = terminationOf(body.termination, read);
return {
artifact,
metadata,
process: {
name: read.text(body.procName, "body.procName"),
path: read.text(body.procPath, "body.procPath"),
pid: read.integer(body.pid, "body.pid"),
parent: read.text(body.parentProc, "body.parentProc"),
cpu_type: read.text(body.cpuType, "body.cpuType"),
translated: read.boolean(body.translated, "body.translated"),
os_version: osVersionOf(body.osVersion, read),
},
exception,
termination,
faulting_thread: faultingThread,
threads,
images,
classification: classify(exception, termination),
coverage: coverageOf(threads, images, read.mismatches),
unlisted_body_keys: [...bodyJson.unlisted],
limitations: IPS_LIMITATIONS,
};
};
+292
View File
@@ -0,0 +1,292 @@
import { isSelectedRegularFileError } from "../../filesystem/RegularFile.js";
import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js";
import { readStableArtifact } from "../../artifacts/readStableArtifact.js";
import {
artifactFilePrimaryFacts,
type ArtifactFilePrimaryFacts,
assertArtifactReadActive,
bindArtifactFileCleanup,
artifactFileCancellationCause,
} from "../../artifacts/ArtifactFileOperation.js";
import { ArtifactResourceScope } from "../../artifacts/ArtifactResourceScope.js";
import { analysisErrorWithCleanupFailure } from "../../domain/analysisErrorCleanup.js";
import type { AppleIpsPort } from "../../application/binaryDiagnostics/AppleIpsPort.js";
import type { ExecutionOptions } from "../../application/AnalysisProvider.js";
import { AnalysisError } from "../../domain/analysisErrorBase.js";
import {
AnalysisAccessDeniedError,
AnalysisArtifactChangedError,
AnalysisCancelledError,
AnalysisInputError,
AnalysisResourceConstraintError,
AnalysisUnsupportedTargetError,
} from "../../domain/analysisErrorCore.js";
import {
MAX_INSPECT_APPLE_IPS_FILE_BYTES,
MAX_INSPECT_APPLE_IPS_FRAMES,
MAX_INSPECT_APPLE_IPS_IMAGES,
MAX_INSPECT_APPLE_IPS_REGISTERS,
MAX_INSPECT_APPLE_IPS_INTEGER_TEXT_BYTES,
DEFAULT_INSPECT_APPLE_IPS_INTEGER_TEXT_BYTES,
MAX_INSPECT_APPLE_IPS_THREADS,
type InspectAppleIpsInput,
} from "../../domain/native/appleIps.js";
import { fixedAllowanceOptions } from "../../domain/fixedAllowanceRemediation.js";
import { ProviderAdapterError } from "../../domain/providerAdapterError.js";
import { ProviderCleanupError } from "../../domain/providerCleanupError.js";
import { err, ok } from "../../domain/result.js";
import { APPLE_IPS_PROVIDER } from "../../domain/investigationProviders.js";
import {
AppleIpsFormatFailure,
AppleIpsResourceFailure,
type AppleIpsSelectedBoundary,
} from "./AppleIps.errors.js";
import { parseAppleIps } from "./AppleIpsParser.js";
const OPERATION = "inspect_apple_ips";
/** Portable .ips reader over one bounded, identity-checked regular file. */
export class AppleIpsProvider implements AppleIpsPort {
readonly identity = APPLE_IPS_PROVIDER;
readonly #resources = new ArtifactResourceScope();
async close(): Promise<void> {
try {
await this.#resources.close();
} catch (cause: unknown) {
if (
!(cause instanceof ArtifactReaderFailure) ||
cause.cleanup === undefined
)
throw cause;
const error = new ProviderCleanupError(
APPLE_IPS_PROVIDER.id,
cause.cleanup.resources,
{ reason: cause.cleanup.reason },
{ cause, operation: OPERATION },
);
bindArtifactFileCleanup(cause, error);
throw error;
}
}
async inspect(input: InspectAppleIpsInput, options?: ExecutionOptions) {
try {
return await this.#resources.run(async () => {
const snapshot = await readStableArtifact(
input.path,
input.max_file_bytes,
options?.signal,
undefined,
this.#resources,
);
assertArtifactReadActive(options?.signal);
const report = parseAppleIps(
snapshot.bytes,
{
path: input.path,
sha256: snapshot.sha256,
bytes: snapshot.bytes.length,
},
input,
);
assertArtifactReadActive(options?.signal);
return ok(report);
});
} catch (cause) {
return err(appleIpsFailure(cause, input));
}
}
}
const fileSizeRemediation = (input: InspectAppleIpsInput): string =>
input.max_file_bytes >= MAX_INSPECT_APPLE_IPS_FILE_BYTES
? `The report exceeds the ${String(MAX_INSPECT_APPLE_IPS_FILE_BYTES)}-byte input ceiling this operation accepts. This release has no runtime override. Select a smaller artifact.`
: `Raise max_file_bytes above ${String(input.max_file_bytes)} bytes or select a smaller artifact; the limit is the caller-selected input read budget.`;
const SELECTED_BUDGETS = {
threads: {
argument: "max_threads",
maximum: MAX_INSPECT_APPLE_IPS_THREADS,
unit: "entries",
},
frames: {
argument: "max_frames",
maximum: MAX_INSPECT_APPLE_IPS_FRAMES,
unit: "entries",
},
images: {
argument: "max_images",
maximum: MAX_INSPECT_APPLE_IPS_IMAGES,
unit: "entries",
},
registers: {
argument: "max_registers",
maximum: MAX_INSPECT_APPLE_IPS_REGISTERS,
unit: "entries",
},
integer_text: {
argument: "max_integer_text_bytes",
maximum: MAX_INSPECT_APPLE_IPS_INTEGER_TEXT_BYTES,
unit: "UTF-16 bytes",
},
} as const satisfies Record<
AppleIpsSelectedBoundary,
{
readonly argument: keyof InspectAppleIpsInput;
readonly maximum: number;
readonly unit: string;
}
>;
const isSelectedBoundary = (
boundary: AppleIpsResourceFailure["boundary"],
): boundary is AppleIpsSelectedBoundary =>
Object.hasOwn(SELECTED_BUDGETS, boundary);
const budgetRemediation = (
input: InspectAppleIpsInput,
boundary: AppleIpsSelectedBoundary,
): string => {
const { argument, maximum, unit } = SELECTED_BUDGETS[boundary];
const selected = input[argument];
const recovery =
selected >= maximum
? `This release has no override above the ${String(maximum)} ${unit} ceiling; select a smaller report.`
: `Raise ${argument} on the next call, up to ${String(maximum)} ${unit}, or select a smaller report; no restart is required.`;
const explanation =
boundary === "integer_text"
? ` The default is ${String(DEFAULT_INSPECT_APPLE_IPS_INTEGER_TEXT_BYTES)} UTF-16 bytes. Conservative reservation precedes numeric conversion and address expansion; max_file_bytes and row budgets do not raise it.`
: "";
return `Effective ${argument} is ${String(selected)} ${unit}. ${recovery}${explanation}`;
};
const resourceRecovery = (
input: InspectAppleIpsInput,
cause: AppleIpsResourceFailure,
) => {
const { boundary } = cause;
return isSelectedBoundary(boundary)
? {
retryable: false,
remediationAction: budgetRemediation(input, boundary),
}
: fixedAllowanceOptions(
`The IPS ${boundary.replaceAll("_", " ")} allowance of ${String(cause.maximum)}`,
"parser",
);
};
const appleIpsFailurePrimary = (
facts: ArtifactFilePrimaryFacts,
input: InspectAppleIpsInput,
): AnalysisError => {
const { cause, origin } = facts;
if (origin === "owned-or-classified" && cause instanceof AnalysisError)
return cause;
if (
origin === "owned-or-classified" &&
cause instanceof ArtifactReaderFailure &&
cause.reason === "integrity"
)
return new AnalysisArtifactChangedError(
OPERATION,
input.path,
cause.message,
{ cause },
);
if (
origin === "owned-or-classified" &&
cause instanceof ArtifactReaderFailure &&
cause.reason === "cancelled"
)
return new AnalysisCancelledError(OPERATION, {
cause: artifactFileCancellationCause(cause),
});
if (cause instanceof AppleIpsResourceFailure)
return new AnalysisResourceConstraintError(
OPERATION,
"capacity",
`Selected artifact ${input.path}: ${cause.message}`,
{ boundary: cause.boundary, maximum: cause.maximum },
{ cause, ...resourceRecovery(input, cause) },
);
if (cause instanceof AppleIpsFormatFailure)
return cause.kind === "unsupported"
? new AnalysisUnsupportedTargetError(
OPERATION,
input.path,
cause.message,
{ cause },
)
: new AnalysisInputError(OPERATION, { cause }, [
{
path: ["path"],
reason: "invalid_format",
message: `Selected artifact ${input.path}: ${cause.message}`,
},
]);
if (
origin === "owned-or-classified" &&
cause instanceof ArtifactReaderFailure
) {
if (cause.reason === "limit")
return new AnalysisResourceConstraintError(
OPERATION,
"file-size",
cause.message,
{ max_file_bytes: input.max_file_bytes },
{
cause,
retryable: false,
remediationAction: fileSizeRemediation(input),
},
);
if (cause.reason === "path" || cause.reason === "format")
return new AnalysisInputError(OPERATION, { cause }, [
{ path: ["path"], reason: "invalid_format", message: cause.message },
]);
}
if (cause instanceof Error && "code" in cause) {
if (cause.code === "EACCES" || cause.code === "EPERM")
return new AnalysisAccessDeniedError(OPERATION, input.path, cause.code, {
cause,
});
if (isSelectedRegularFileError(cause))
return new AnalysisInputError(OPERATION, { cause }, [
{
path: ["path"],
reason: "invalid_value",
message: `Selected report could not be read (${String(cause.code)}): ${input.path}.`,
},
]);
}
return new ProviderAdapterError(APPLE_IPS_PROVIDER.id, OPERATION, {
cause,
diagnostics: {
path: input.path,
reason: cause instanceof Error ? cause.message : String(cause),
},
});
};
const appleIpsFailure = (
cause: unknown,
input: InspectAppleIpsInput,
): AnalysisError => {
const primary = appleIpsFailurePrimary(
artifactFilePrimaryFacts(cause),
input,
);
if (!(cause instanceof ArtifactReaderFailure) || cause.cleanup === undefined)
return primary;
const cleanup = new ProviderCleanupError(
APPLE_IPS_PROVIDER.id,
cause.cleanup.resources,
{ reason: cause.cleanup.reason },
{ operation: OPERATION, cause },
);
const error = analysisErrorWithCleanupFailure(primary, cleanup, OPERATION);
bindArtifactFileCleanup(cause, error);
return error;
};
+128
View File
@@ -0,0 +1,128 @@
import { expect, it } from "vitest";
import {
LosslessJsonFailure,
parseLosslessJson,
type JsonShape,
} from "./losslessJson.js";
it("keeps integers above 2^53 as decimal text", () => {
const { value, end } = parseLosslessJson('{"far":9587749362346101060}', 0);
expect(end).toBe(27);
expect(value).toEqual({
kind: "object",
members: { far: { kind: "integer", decimal: "9587749362346101060" } },
unlisted: [],
});
});
it("rejects duplicate object keys", () => {
expect(() => parseLosslessJson('{"a":1,"a":2}')).toThrow(/Duplicate/u);
});
it("decodes long and escaped strings exactly as JSON.parse does", () => {
const plain = "a".repeat(1_040_000);
const escaped = `${"b".repeat(5000)}\\n\\u00e9\\"${"c".repeat(5000)}\\\\`;
for (const literal of [`"${plain}"`, `"${escaped}"`, '""', '"\\u2028"']) {
const { value } = parseLosslessJson(literal);
expect(value).toEqual({ kind: "string", value: JSON.parse(literal) });
}
expect(() => parseLosslessJson('"a\u0001"')).toThrow(/control character/u);
expect(() => parseLosslessJson('"abc')).toThrow(/Unterminated/u);
expect(() => parseLosslessJson('"\\x"')).toThrow(/escape/u);
});
it("keeps only projected members and names the unlisted ones", () => {
const shape: JsonShape = {
kind: "object",
members: { keep: { kind: "scalar" } },
beforeUnlisted: () => undefined,
};
const { value } = parseLosslessJson(
'{"drop":[1,{"deep":"x"}],"keep":"y","other":"z"}',
0,
shape,
);
expect(value).toEqual({
kind: "object",
members: { keep: { kind: "string", value: "y" } },
unlisted: ["drop", "other"],
});
});
it("marks a container of the wrong type as omitted without materializing it", () => {
const shape: JsonShape = {
kind: "object",
members: {
rows: { kind: "array", items: { kind: "scalar" } },
name: { kind: "scalar" },
},
};
const { value } = parseLosslessJson(
'{"rows":{"a":1},"name":[1,2]}',
0,
shape,
);
expect(value).toEqual({
kind: "object",
members: {
rows: { kind: "omitted", type: "object" },
name: { kind: "omitted", type: "array" },
},
unlisted: [],
});
});
it("runs item budgets before parsing each item", () => {
let parsed = 0;
const shape: JsonShape = {
kind: "array",
items: { kind: "scalar" },
beforeItem: () => {
parsed += 1;
if (parsed > 3) throw new RangeError("budget");
},
};
// The fourth item is syntactically invalid; the budget must refuse it first.
expect(() => parseLosslessJson("[1,2,3,?]", 0, shape)).toThrow(RangeError);
expect(parsed).toBe(4);
});
it("does not resolve inherited names as projected members", () => {
const shape: JsonShape = { kind: "object", members: {} };
const { value } = parseLosslessJson(
'{"constructor":1,"__proto__":{"x":1},"toString":[]}',
0,
shape,
);
expect(value).toEqual({ kind: "object", members: {}, unlisted: [] });
});
it("still checks syntax inside skipped values", () => {
const shape: JsonShape = { kind: "object", members: {} };
for (const text of [
'{"skip":[1,]}',
'{"skip":{"a" 1}}',
'{"skip":"\\q"}',
'{"skip":tru}',
'{"skip":01}',
])
expect(() => parseLosslessJson(text, 0, shape)).toThrow(
LosslessJsonFailure,
);
});
it("refuses nesting beyond the selected depth in kept and skipped values", () => {
const deep = `${"[".repeat(20)}${"]".repeat(20)}`;
expect(() => parseLosslessJson(deep, 0, undefined, { maxDepth: 10 })).toThrow(
expect.objectContaining({ reason: "depth", maximum: 10 }),
);
expect(() =>
parseLosslessJson(
`{"skip":${deep}}`,
0,
{ kind: "object", members: {} },
{ maxDepth: 10 },
),
).toThrow(expect.objectContaining({ reason: "depth" }));
expect(parseLosslessJson(deep, 0, undefined, { maxDepth: 20 }).end).toBe(40);
});
+427
View File
@@ -0,0 +1,427 @@
/** JSON container type a projection declined to materialize. */
export type JsonContainerType = "array" | "object";
/** A JSON value that keeps integer literals as decimal text. */
export type LosslessJson =
| { readonly kind: "null" }
| { readonly kind: "boolean"; readonly value: boolean }
| { readonly kind: "string"; readonly value: string }
| { readonly kind: "integer"; readonly decimal: string }
| { readonly kind: "number"; readonly recorded: string }
| { readonly kind: "array"; readonly items: readonly LosslessJson[] }
| {
readonly kind: "object";
readonly members: Readonly<Record<string, LosslessJson>>;
/** Names of members the projection did not select, in source order. */
readonly unlisted: readonly string[];
}
/** A container whose type did not match the projection; syntax was checked, content was not kept. */
| { readonly kind: "omitted"; readonly type: JsonContainerType };
/**
* Projection that selects which parts of a JSON value are materialized.
*
* Values outside the projection are syntax-checked without being retained, so
* callers can bound allocation by the facts they actually consume. Budget
* callbacks run before the corresponding item is parsed and may throw.
*/
export type JsonShape =
| { readonly kind: "any" }
| { readonly kind: "scalar" }
| {
readonly kind: "array";
readonly items: JsonShape;
readonly beforeItem?: () => void;
}
| {
readonly kind: "object";
readonly members: Readonly<Record<string, JsonShape>>;
/** Shape for members not listed in `members`; absent means they are skipped. */
readonly rest?: JsonShape;
readonly beforeRest?: () => void;
/** When present, names of skipped members are kept; called with the new count first. */
readonly beforeUnlisted?: (count: number) => void;
};
/** JSON text could not be parsed as a single complete value. */
export class LosslessJsonFailure extends Error {
constructor(
message: string,
readonly reason: "syntax" | "depth" = "syntax",
readonly maximum: number | null = null,
) {
super(message);
this.name = "LosslessJsonFailure";
}
}
const ANY: JsonShape = { kind: "any" };
const OMITTED_ARRAY: LosslessJson = Object.freeze({
kind: "omitted",
type: "array",
});
const OMITTED_OBJECT: LosslessJson = Object.freeze({
kind: "omitted",
type: "object",
});
const DEFAULT_MAX_DEPTH = 512;
const QUOTE = 0x22;
const BACKSLASH = 0x5c;
const isDigit = (code: number): boolean => code >= 48 && code <= 57;
/** Parse one JSON value, preserving integer literals that exceed IEEE-754 range. */
export const parseLosslessJson = (
text: string,
start = 0,
shape: JsonShape = ANY,
options: { readonly maxDepth?: number } = {},
): { readonly value: LosslessJson; readonly end: number } => {
const parser = new Parser(text, start, options.maxDepth ?? DEFAULT_MAX_DEPTH);
const value = parser.parseValue(shape, 0);
return { value, end: parser.position };
};
class Parser {
position: number;
constructor(
readonly text: string,
start: number,
readonly maxDepth: number,
) {
this.position = start;
}
parseValue(shape: JsonShape, depth: number): LosslessJson {
this.skipWhitespace();
const ch = this.text[this.position];
if (ch === "{") {
if (shape.kind === "object" || shape.kind === "any")
return this.parseObject(shape, depth + 1);
this.skipValue(depth);
return OMITTED_OBJECT;
}
if (ch === "[") {
if (shape.kind === "array" || shape.kind === "any")
return this.parseArray(shape, depth + 1);
this.skipValue(depth);
return OMITTED_ARRAY;
}
return this.parseScalar();
}
private enter(depth: number): void {
if (depth > this.maxDepth)
throw new LosslessJsonFailure(
`JSON nesting exceeds ${String(this.maxDepth)} levels at offset ${String(this.position)}`,
"depth",
this.maxDepth,
);
}
private parseScalar(): LosslessJson {
const ch = this.text[this.position];
if (ch === undefined)
throw new LosslessJsonFailure("Unexpected end of JSON input");
if (ch === '"') return { kind: "string", value: this.parseString() };
if (ch === "t")
return this.literal("true", { kind: "boolean", value: true });
if (ch === "f")
return this.literal("false", { kind: "boolean", value: false });
if (ch === "n") return this.literal("null", { kind: "null" });
if (ch === "-" || isDigit(ch.charCodeAt(0))) {
const start = this.position;
const fractional = this.scanNumber();
const recorded = this.text.slice(start, this.position);
return fractional
? { kind: "number", recorded }
: { kind: "integer", decimal: recorded };
}
throw new LosslessJsonFailure(
`Unexpected ${JSON.stringify(ch)} at JSON offset ${String(this.position)}`,
);
}
private parseObject(
shape: Extract<JsonShape, { kind: "object" | "any" }>,
depth: number,
): LosslessJson {
this.enter(depth);
this.position += 1;
const members: Record<string, LosslessJson> = Object.create(null) as Record<
string,
LosslessJson
>;
const unlisted: string[] = [];
const unlistedSeen = new Set<string>();
this.skipWhitespace();
if (this.text[this.position] === "}") {
this.position += 1;
return { kind: "object", members, unlisted };
}
for (;;) {
this.skipWhitespace();
if (this.text[this.position] !== '"')
throw new LosslessJsonFailure(
`Expected a JSON object key at offset ${String(this.position)}`,
);
const key = this.parseString();
this.skipWhitespace();
this.expect(":");
const listed = shape.kind === "any" || Object.hasOwn(shape.members, key);
const memberShape =
shape.kind === "any" ? ANY : listed ? shape.members[key] : shape.rest;
if (memberShape === undefined) {
if (shape.kind === "object" && shape.beforeUnlisted !== undefined) {
if (unlistedSeen.has(key))
throw new LosslessJsonFailure(
`Duplicate JSON object key ${JSON.stringify(key)}`,
);
shape.beforeUnlisted(unlisted.length + 1);
unlistedSeen.add(key);
unlisted.push(key);
}
this.skipValue(depth);
} else {
if (Object.hasOwn(members, key))
throw new LosslessJsonFailure(
`Duplicate JSON object key ${JSON.stringify(key)}`,
);
if (!listed && shape.kind === "object") shape.beforeRest?.();
members[key] = this.parseValue(memberShape, depth);
}
this.skipWhitespace();
const next = this.text[this.position];
if (next === "}") {
this.position += 1;
return { kind: "object", members, unlisted };
}
if (next !== ",")
throw new LosslessJsonFailure(
`Expected ',' or '}' in JSON object at offset ${String(this.position)}`,
);
this.position += 1;
}
}
private parseArray(
shape: Extract<JsonShape, { kind: "array" | "any" }>,
depth: number,
): LosslessJson {
this.enter(depth);
this.position += 1;
const items: LosslessJson[] = [];
const itemShape = shape.kind === "any" ? ANY : shape.items;
const beforeItem = shape.kind === "array" ? shape.beforeItem : undefined;
this.skipWhitespace();
if (this.text[this.position] === "]") {
this.position += 1;
return { kind: "array", items };
}
for (;;) {
beforeItem?.();
items.push(this.parseValue(itemShape, depth));
this.skipWhitespace();
const next = this.text[this.position];
if (next === "]") {
this.position += 1;
return { kind: "array", items };
}
if (next !== ",")
throw new LosslessJsonFailure(
`Expected ',' or ']' in JSON array at offset ${String(this.position)}`,
);
this.position += 1;
}
}
/** Validate one value's syntax without retaining it. */
private skipValue(depth: number): void {
this.skipWhitespace();
const ch = this.text[this.position];
if (ch === "{") {
this.enter(depth + 1);
this.position += 1;
this.skipWhitespace();
if (this.text[this.position] === "}") {
this.position += 1;
return;
}
for (;;) {
this.skipWhitespace();
if (this.text[this.position] !== '"')
throw new LosslessJsonFailure(
`Expected a JSON object key at offset ${String(this.position)}`,
);
this.skipString();
this.skipWhitespace();
this.expect(":");
this.skipValue(depth + 1);
this.skipWhitespace();
const next = this.text[this.position];
if (next === "}") {
this.position += 1;
return;
}
if (next !== ",")
throw new LosslessJsonFailure(
`Expected ',' or '}' in JSON object at offset ${String(this.position)}`,
);
this.position += 1;
}
}
if (ch === "[") {
this.enter(depth + 1);
this.position += 1;
this.skipWhitespace();
if (this.text[this.position] === "]") {
this.position += 1;
return;
}
for (;;) {
this.skipValue(depth + 1);
this.skipWhitespace();
const next = this.text[this.position];
if (next === "]") {
this.position += 1;
return;
}
if (next !== ",")
throw new LosslessJsonFailure(
`Expected ',' or ']' in JSON array at offset ${String(this.position)}`,
);
this.position += 1;
}
}
if (ch === '"') {
this.skipString();
return;
}
if (ch === "-" || (ch !== undefined && isDigit(ch.charCodeAt(0)))) {
this.scanNumber();
return;
}
this.parseScalar();
}
/** Validate once, then let V8 decode escapes without a per-escape fragment array. */
private parseString(): string {
const start = this.position;
const escaped = this.skipString();
if (!escaped) return this.text.slice(start + 1, this.position - 1);
// A decoded JSON string cannot contain more code units than its literal.
// The input's byte allowance bounds this allocation, including dense escapes.
const decoded: unknown = JSON.parse(this.text.slice(start, this.position));
if (typeof decoded !== "string")
throw new LosslessJsonFailure("Expected a JSON string");
return decoded;
}
private skipString(): boolean {
const text = this.text;
let escaped = false;
this.position += 1;
while (this.position < text.length) {
const code = text.charCodeAt(this.position);
if (code === QUOTE) {
this.position += 1;
return escaped;
}
if (code === BACKSLASH) {
escaped = true;
this.position += 1;
this.escape();
continue;
}
if (code < 0x20)
throw new LosslessJsonFailure(
"Unescaped control character in JSON string",
);
this.position += 1;
}
throw new LosslessJsonFailure("Unterminated JSON string");
}
private escape(): string {
const ch = this.text[this.position];
this.position += 1;
switch (ch) {
case '"':
case "\\":
case "/":
return ch;
case "b":
return "\b";
case "f":
return "\f";
case "n":
return "\n";
case "r":
return "\r";
case "t":
return "\t";
case "u": {
const hex = this.text.slice(this.position, this.position + 4);
if (!/^[0-9a-fA-F]{4}$/u.test(hex))
throw new LosslessJsonFailure("Invalid JSON Unicode escape");
this.position += 4;
return String.fromCharCode(Number.parseInt(hex, 16));
}
default:
throw new LosslessJsonFailure("Invalid JSON string escape");
}
}
/** Advance over one number; returns whether it has a fraction or exponent. */
private scanNumber(): boolean {
if (this.text[this.position] === "-") this.position += 1;
if (this.text[this.position] === "0") this.position += 1;
else {
if (!isDigit(this.text.charCodeAt(this.position)))
throw new LosslessJsonFailure("Invalid JSON number");
while (isDigit(this.text.charCodeAt(this.position))) this.position += 1;
}
let fractional = false;
if (this.text[this.position] === ".") {
fractional = true;
this.position += 1;
if (!isDigit(this.text.charCodeAt(this.position)))
throw new LosslessJsonFailure("Invalid JSON number fraction");
while (isDigit(this.text.charCodeAt(this.position))) this.position += 1;
}
if (this.text[this.position] === "e" || this.text[this.position] === "E") {
fractional = true;
this.position += 1;
if (this.text[this.position] === "+" || this.text[this.position] === "-")
this.position += 1;
if (!isDigit(this.text.charCodeAt(this.position)))
throw new LosslessJsonFailure("Invalid JSON number exponent");
while (isDigit(this.text.charCodeAt(this.position))) this.position += 1;
}
return fractional;
}
private literal<T extends LosslessJson>(token: string, value: T): T {
if (!this.text.startsWith(token, this.position))
throw new LosslessJsonFailure(`Expected ${token} in JSON`);
this.position += token.length;
return value;
}
private expect(token: string): void {
if (!this.text.startsWith(token, this.position))
throw new LosslessJsonFailure(
`Expected ${JSON.stringify(token)} at JSON offset ${String(this.position)}`,
);
this.position += token.length;
}
private skipWhitespace(): void {
while (this.position < this.text.length) {
const ch = this.text[this.position];
if (ch !== " " && ch !== "\t" && ch !== "\n" && ch !== "\r") return;
this.position += 1;
}
}
}
+15
View File
@@ -24,17 +24,20 @@ import { snapshotEnvironment } from "../process/snapshotEnvironment.js";
import { captureProviderEnvironment } from "./providerEnvironment.js";
import {
createElfNotesService,
createAppleIpsService,
createPeCertificatesService,
createPeImportsService,
createNativeExportsService,
createPeResourcesService,
} from "../composition/binaryDiagnostics.js";
import type { ElfNotesService } from "../application/binaryDiagnostics/ElfNotesService.js";
import type { AppleIpsService } from "../application/binaryDiagnostics/AppleIpsService.js";
import type { PeCertificatesService } from "../application/binaryDiagnostics/PeCertificatesService.js";
import type { PeImportsService } from "../application/binaryDiagnostics/PeImportsService.js";
import type { PeResourcesService } from "../application/binaryDiagnostics/PeResourcesService.js";
import type { NativeExportsService } from "../application/binaryDiagnostics/NativeExportsService.js";
import { registerElfNotesTool } from "./registerElfNotesTool.js";
import { registerAppleIpsTool } from "./registerAppleIpsTool.js";
import { registerPeCertificatesTool } from "./registerPeCertificatesTool.js";
import { registerPeImportsTool } from "./registerPeImportsTool.js";
import { registerPeResourcesTool } from "./registerPeResourcesTool.js";
@@ -470,6 +473,7 @@ export const createServer = (
const peResources = createPeResourcesService();
const nativeExports = createNativeExportsService();
const elfNotes = createElfNotesService();
const appleIps = createAppleIpsService();
const peCertificates = createPeCertificatesService();
const peImports = createPeImportsService();
const electronFuses = options.electronFuses ?? createElectronFuseService();
@@ -582,6 +586,7 @@ export const createServer = (
peResources.close(),
nativeExports.close(),
elfNotes.close(),
appleIps.close(),
peCertificates.close(),
peImports.close(),
electronFuses.close(),
@@ -651,6 +656,7 @@ export const createServer = (
peResources,
nativeExports,
elfNotes,
appleIps,
peCertificates,
peImports,
electronFuses,
@@ -889,6 +895,7 @@ const registerConfiguredAnalysisTools = (
peResources,
nativeExports,
elfNotes,
appleIps,
peCertificates,
peImports,
pythonPyc,
@@ -957,6 +964,13 @@ const registerConfiguredAnalysisTools = (
recordEvidence,
withAdmittedAnalysis,
);
registerAppleIpsTool(
server,
appleIps,
toolLogger,
recordEvidence,
withAdmittedAnalysis,
);
registerPeCertificatesTool(
server,
peCertificates,
@@ -1127,6 +1141,7 @@ interface ServerToolContext extends ReturnType<typeof createSessionRecorders> {
readonly peResources: PeResourcesService;
readonly nativeExports: NativeExportsService;
readonly elfNotes: ElfNotesService;
readonly appleIps: AppleIpsService;
readonly peCertificates: PeCertificatesService;
readonly peImports: PeImportsService;
readonly electronFuses: ElectronFuseService;
+31
View File
@@ -0,0 +1,31 @@
import type { EvidenceMcpServer } from "./EvidenceMcpServer.js";
import type { AppleIpsService } from "../application/binaryDiagnostics/AppleIpsService.js";
import type { EvidenceWriter } from "../application/investigation/InvestigationRecordPort.js";
import { toolContract } from "../contracts/toolContracts.js";
import type { Logger } from "pino";
import { createEvidenceToolHandler } from "./contractToolHandler.js";
import type { WithAdmittedAnalysis } from "./analysisAdmission.js";
import { toolRegistrationOptions } from "./toolRegistrationOptions.js";
/** Register the explicit-file Apple .ips workflow and retain its complete Evidence. */
export const registerAppleIpsTool = (
server: EvidenceMcpServer,
service: AppleIpsService,
logger: Logger,
recordEvidence?: EvidenceWriter["recordEvidence"],
withAdmittedAnalysis?: WithAdmittedAnalysis,
): void => {
const contract = toolContract("inspect_apple_ips");
const handler = createEvidenceToolHandler(
server,
(input, options) => service.inspect(input, options),
logger,
recordEvidence,
withAdmittedAnalysis,
);
server.registerTool(
contract.name,
toolRegistrationOptions(contract),
handler(contract),
);
};
+117
View File
@@ -0,0 +1,117 @@
import { writeFile } from "node:fs/promises";
import { createHash } from "node:crypto";
import { join } from "node:path";
import { expect } from "vitest";
import { toolContract } from "../../src/contracts/toolContracts.js";
import { appleIpsBytes } from "../fixtures/binaryDiagnostics/appleIps.js";
import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js";
import { cliTest } from "../support/cli/cliFixture.js";
cliTest(
"decodes escape-dense IPS text within the selected Node heap",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-apple-ips-escaped-");
const path = join(root, "escaped.ips");
const name = "\n".repeat(7_500_000);
await writeFile(
path,
`{"bug_type":"309"}\n${JSON.stringify({ procName: name, threads: [], usedImages: [] })}`,
);
const result = await cli.run({
arguments: [
"inspect-apple-ips",
path,
"--max-file-bytes",
"16777216",
"--json",
],
environment: { NODE_OPTIONS: "--max-old-space-size=256" },
timeoutMs: 30000,
});
expect(result.exitCode, result.stderr).toBe(0);
const evidence = toolContract("inspect_apple_ips").outputSchema.parse(
result.json,
);
const actual = evidence.normalized_result.process.name;
expect(actual?.length).toBe(name.length);
expect(
createHash("sha256")
.update(actual ?? "")
.digest("hex"),
).toBe(createHash("sha256").update(name).digest("hex"));
expect(evidence.normalized_result.coverage.status).toBe("complete");
},
45000,
);
cliTest(
"reports Apple IPS crash facts through the public CLI",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-apple-ips-cli-");
const path = join(root, "report.ips");
await writeFile(path, appleIpsBytes());
const constrained = await cli.run({
arguments: [
"inspect-apple-ips",
path,
"--max-integer-text-bytes",
"1",
"--json",
],
});
expect(constrained.exitCode, constrained.stderr).toBe(1);
expect(constrained.json).toMatchObject({
code: "resource_constraint",
remediation: {
action: expect.stringContaining("max_integer_text_bytes"),
},
details: {
reported_limits: { boundary: "integer_text", maximum: 1 },
},
});
const result = await cli.run({
arguments: ["inspect-apple-ips", path, "--json"],
});
expect(result.exitCode, result.stderr).toBe(0);
const evidence = toolContract("inspect_apple_ips").outputSchema.parse(
result.json,
);
expect(evidence.normalized_result.metadata.bug_type).toBe("309");
expect(evidence.normalized_result.exception?.type).toBe("EXC_BAD_ACCESS");
expect(evidence.normalized_result.images[0]?.uuid).toBe(
"AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAAAAAA",
);
},
);
cliTest(
"resolves a relative IPS path against the working directory",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-apple-ips-cli-rel-");
await writeFile(join(root, "report.ips"), appleIpsBytes());
const result = await cli.run({
arguments: ["inspect-apple-ips", "report.ips", "--json"],
cwd: root,
});
expect(result.exitCode, result.stderr).toBe(0);
const evidence = toolContract("inspect_apple_ips").outputSchema.parse(
result.json,
);
expect(evidence.normalized_result.process.name).toBe("rea-crash-fixture");
expect(evidence.subject?.local_path).toBe(join(root, "report.ips"));
},
);
cliTest("reports a truncated IPS as invalid input", async ({ cli }) => {
const root = await createTestTempDirectory("rea-apple-ips-cli-bad-");
const path = join(root, "broken.ips");
await writeFile(path, appleIpsBytes({ truncateBody: true }));
const result = await cli.run({
arguments: ["inspect-apple-ips", path, "--json"],
});
expect(result.exitCode).toBe(1);
expect(result.json).toMatchObject({
code: "invalid_request",
details: { operation: "inspect_apple_ips" },
});
});
@@ -222,6 +222,7 @@ const pathRequests: readonly [string, Record<string, unknown>][] = [
["inspect_pe_imports", { path: nul }],
["inspect_native_exports", { path: nul }],
["inspect_elf_notes", { path: nul }],
["inspect_apple_ips", { path: nul }],
["inspect_python_pyc", { path: nul }],
["inspect_erlang_beam", { path: nul }],
["inspect_binary_layout", { path: nul }],
+113
View File
@@ -0,0 +1,113 @@
import { symlink, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
import { createAppleIpsService } from "../../../src/composition/binaryDiagnostics.js";
import {
appleIpsBytes,
appleIpsMetadataLine,
} from "../../fixtures/binaryDiagnostics/appleIps.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
describe("Apple IPS stable filesystem boundary", () => {
it("reports non-file and malformed IPS selections as caller input errors", async () => {
const root = await createTestTempDirectory("rea-apple-ips-files-");
const path = join(root, "report.ips");
const alias = join(root, "alias.ips");
const malformed = join(root, "malformed.ips");
await writeFile(path, appleIpsBytes());
await writeFile(malformed, Buffer.from("not an ips report"));
await symlink(path, alias);
const service = createAppleIpsService();
for (const [candidate, reason] of [
[root, "invalid_format"],
[alias, "invalid_format"],
[malformed, "invalid_format"],
[join(root, "missing.ips"), "invalid_value"],
[join(path, "child.ips"), "invalid_value"],
] as const) {
const result = await service.inspect({ path: candidate });
if (result.ok) throw new Error("Expected file acquisition failure");
expect(result.error).toMatchObject({
_tag: "AnalysisInputError",
operation: "inspect_apple_ips",
issues: [
{
path: ["path"],
reason,
message: expect.any(String),
},
],
});
}
});
it("returns typed resource and cancellation failures", async () => {
const root = await createTestTempDirectory("rea-apple-ips-budget-");
const path = join(root, "report.ips");
await writeFile(path, appleIpsBytes());
const service = createAppleIpsService();
const tooSmall = await service.inspect({ path, max_file_bytes: 64 });
if (tooSmall.ok) throw new Error("Expected resource budget failure");
expect(tooSmall.error._tag).toBe("AnalysisResourceConstraintError");
const cancelled = await service.inspect(
{ path },
{ signal: AbortSignal.abort() },
);
if (cancelled.ok) throw new Error("Expected cancellation");
expect(cancelled.error._tag).toBe("AnalysisCancelledError");
});
it("maps register, fixed structural and UTF-8 refusals to typed errors", async () => {
const root = await createTestTempDirectory("rea-apple-ips-shape-");
const report = join(root, "report.ips");
const deep = join(root, "deep.ips");
const corrupt = join(root, "corrupt.ips");
await writeFile(report, appleIpsBytes());
await writeFile(
deep,
`${appleIpsMetadataLine()}\n{"vmSummary":${"[".repeat(100)}${"]".repeat(100)}}`,
);
const bytes = appleIpsBytes();
bytes[bytes.indexOf(Buffer.from("rea-crash-fixture"))] = 0xc3;
await writeFile(corrupt, bytes);
const service = createAppleIpsService();
const registers = await service.inspect({ path: report, max_registers: 2 });
if (registers.ok) throw new Error("Expected register budget failure");
expect(registers.error).toMatchObject({
_tag: "AnalysisResourceConstraintError",
resource: "capacity",
reportedLimits: { boundary: "registers", maximum: 2 },
retryable: false,
remediationAction: expect.stringContaining("max_registers"),
});
const nesting = await service.inspect({ path: deep });
if (nesting.ok) throw new Error("Expected nesting allowance failure");
expect(nesting.error).toMatchObject({
_tag: "AnalysisResourceConstraintError",
reportedLimits: { boundary: "nesting", maximum: 64 },
retryable: false,
fixedAllowance: { kind: "parser" },
});
const invalid = await service.inspect({ path: corrupt });
if (invalid.ok) throw new Error("Expected invalid UTF-8 failure");
expect(invalid.error).toMatchObject({
_tag: "AnalysisInputError",
issues: [
{
path: ["path"],
reason: "invalid_format",
message: expect.stringContaining("UTF-8"),
},
],
});
const complete = await service.inspect({ path: report });
if (!complete.ok) throw complete.error;
expect(complete.value.normalized_result).toMatchObject({
coverage: { status: "complete", registers: 6, type_mismatches: [] },
});
});
});
+148
View File
@@ -0,0 +1,148 @@
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { Ajv2020 } from "ajv/dist/2020.js";
import { expect, it } from "vitest";
import { appleIpsSchema } from "../../../src/domain/native/appleIps.js";
import { parseEvidence } from "../../../src/domain/evidence.js";
import { connectLocalToolsMcp } from "../../fixtures/localToolsMcp.js";
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
import { appleIpsBytes } from "../../fixtures/binaryDiagnostics/appleIps.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
it("advertises and validates IPS facts and failures through production stdio MCP", async () => {
const { client, call } = await connectLocalToolsMcp();
const advertised = (await client.listTools()).tools.find(
(tool) => tool.name === "inspect_apple_ips",
);
if (advertised?.outputSchema === undefined)
throw new Error("Missing advertised IPS output schema");
const inputSchema: Record<string, unknown> = advertised.inputSchema;
const outputSchema: Record<string, unknown> = advertised.outputSchema;
const ajv = new Ajv2020({ strict: false, validateFormats: false });
expect(advertised?.annotations).toMatchObject({
readOnlyHint: false,
idempotentHint: true,
});
const root = await createTestTempDirectory("rea-apple-ips-mcp-");
const path = join(root, "report.ips");
await writeFile(path, appleIpsBytes());
expect(ajv.validate(inputSchema, { path, max_integer_text_bytes: 1 })).toBe(
true,
);
const constrained = await call("inspect_apple_ips", {
path,
max_integer_text_bytes: 1,
});
expect(constrained.isError).toBe(true);
expect(parseMcpToolError(constrained).error).toMatchObject({
code: "resource_constraint",
remediation: {
action: expect.stringContaining(
"Raise max_integer_text_bytes on the next call",
),
},
details: {
reported_limits: { boundary: "integer_text", maximum: 1 },
},
});
const response = await call("inspect_apple_ips", { path });
expect(response.isError, JSON.stringify(response.content)).not.toBe(true);
expect(
ajv.validate(outputSchema, response.structuredContent),
JSON.stringify(ajv.errors),
).toBe(true);
const evidence = parseEvidence(response.structuredContent);
const report = appleIpsSchema.parse(evidence.normalized_result);
expect(evidence.operation).toBe("inspect_apple_ips");
expect(report.metadata.bug_type).toBe("309");
expect(report.threads[0]?.registers[0]).toMatchObject({
name: "x0",
value: {
decimal: "9594637537912899908",
hex: "0x8527000193ed4944",
},
});
expect(report.classification.status).toBe("unclassified");
const unsupported = {
...evidence,
normalized_result: {
...report,
metadata: { ...report.metadata, bug_type: "288" },
},
};
expect(ajv.validate(outputSchema, unsupported)).toBe(false);
const match = {
rule: "watchdog",
fields: [{ name: "termination.code", value: "2343432205" }],
};
for (const [status, matches, accepted] of [
["classified", [], false],
["classified", [match], true],
["unclassified", [match], false],
["unclassified", [], true],
] as const) {
expect(
ajv.validate(outputSchema, {
...evidence,
normalized_result: { ...report, classification: { status, matches } },
}),
).toBe(accepted);
}
const whitespacePath = join(root, "non-json-whitespace.ips");
await writeFile(
whitespacePath,
Buffer.concat([appleIpsBytes(), Buffer.from("\f")]),
);
const whitespace = await call("inspect_apple_ips", { path: whitespacePath });
expect(whitespace.isError).toBe(true);
expect(parseMcpToolError(whitespace).error.code).toBe("invalid_request");
const malformedPath = join(root, "malformed-uuid.ips");
await writeFile(malformedPath, appleIpsBytes({ uuid: "malformed/report" }));
const malformed = await call("inspect_apple_ips", { path: malformedPath });
expect(malformed.isError, JSON.stringify(malformed.content)).not.toBe(true);
expect(
ajv.validate(outputSchema, malformed.structuredContent),
JSON.stringify(ajv.errors),
).toBe(true);
const partial = appleIpsSchema.parse(
parseEvidence(malformed.structuredContent).normalized_result,
);
expect(partial.coverage.status).toBe("partial");
expect(partial.coverage.type_mismatches).toEqual([
{
path: "body.usedImages[0].uuid",
expected: "32 hexadecimal UUID digits with optional canonical hyphens",
observed: "malformed UUID string",
recorded_value: "malformed/report",
},
]);
const indexPath = join(root, "unresolved-indexes.ips");
await writeFile(
indexPath,
'{"bug_type":"309"}\n{"faultingThread":5,"threads":[{"frames":[{"imageIndex":-1},{"imageIndex":4294967296}]}],"usedImages":[]}',
);
const indexes = await call("inspect_apple_ips", { path: indexPath });
expect(indexes.isError, JSON.stringify(indexes.content)).not.toBe(true);
expect(
ajv.validate(outputSchema, indexes.structuredContent),
JSON.stringify(ajv.errors),
).toBe(true);
const unresolved = appleIpsSchema.parse(
parseEvidence(indexes.structuredContent).normalized_result,
);
expect(unresolved.faulting_thread).toBe(5);
expect(unresolved.threads[0]?.frames[0]?.role).toBe("unknown");
expect(unresolved.coverage.status).toBe("partial");
expect(
unresolved.coverage.type_mismatches.map(
(mismatch) => mismatch.recorded_integer,
),
).toEqual(["5", "-1", "4294967296"]);
const relative = await call("inspect_apple_ips", { path: "report.ips" });
expect(relative.isError).toBe(true);
expect(parseMcpToolError(relative).error).toMatchObject({
code: "invalid_request",
details: { operation: "inspect_apple_ips" },
});
});
+168
View File
@@ -0,0 +1,168 @@
export interface AppleIpsFixtureOptions {
readonly bugType?: string;
readonly procName?: string;
readonly uuid?: string;
readonly arch?: string | null;
readonly imageOffset?: number;
readonly base?: number;
readonly exceptionType?: string;
readonly signal?: string;
readonly subtype?: string;
readonly terminationNamespace?: string;
readonly terminationCode?: string;
readonly terminationIndicator?: string;
readonly terminationReasons?: readonly string[];
readonly registerValue?: string;
/** Emit the older string `osVersion` form instead of the producer dictionary. */
readonly osVersionText?: string;
readonly extraBody?: string;
readonly truncateBody?: boolean;
}
const defaultUuid = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa";
/** First-line metadata object as macOS ReportCrash writes it. */
export const appleIpsMetadataLine = (
options: Pick<AppleIpsFixtureOptions, "bugType" | "procName"> = {},
): string =>
JSON.stringify({
app_name: options.procName ?? "rea-crash-fixture",
timestamp: "2026-01-01 00:00:00.000 +0000",
app_version: "",
slice_uuid: defaultUuid,
build_version: "",
platform: 1,
share_with_app_devs: 0,
is_first_party: 1,
bug_type: options.bugType ?? "309",
os_version: "macOS 15.0 (24A335)",
roots_installed: 0,
bundleID: "test.rea.fixture",
incident_id: "AAAA-BBBB",
name: options.procName ?? "rea-crash-fixture",
});
/**
* Two-object Apple .ips crash report with a 309 metadata line, shaped like
* ReportCrash output: dictionary `osVersion`, `threadState.flavor`, register
* descriptions, a non-faulting thread with an inlined top frame, and unlisted
* producer keys.
*/
export const appleIpsReport = (
options: AppleIpsFixtureOptions = {},
): string => {
const metadata = appleIpsMetadataLine(options);
const uuid = options.uuid ?? defaultUuid;
const arch = options.arch === undefined ? "arm64" : options.arch;
const base = options.base ?? 4294967296;
const imageOffset = options.imageOffset ?? 824;
const register = options.registerValue ?? "9594637537912899908";
const reasons =
options.terminationReasons === undefined
? ""
: `,"reasons":${JSON.stringify([...options.terminationReasons])}`;
const extra = options.extraBody ?? "";
const archJson = arch === null ? "null" : JSON.stringify(arch);
const osVersion =
options.osVersionText === undefined
? `{ "train": "macOS 15.0", "build": "24A335", "releaseType": "User" }`
: JSON.stringify(options.osVersionText);
const body = `{
"uptime": 1000,
"procRole": "Foreground",
"version": 2,
"userID": 501,
"deployVersion": 210,
"modelCode": "Mac14,2",
"osVersion": ${osVersion},
"captureTime": "2026-01-01 00:00:00.0000 +0000",
"incident": "AAAA-BBBB",
"pid": 4242,
"translated": false,
"cpuType": "ARM-64",
"procName": ${JSON.stringify(options.procName ?? "rea-crash-fixture")},
"procPath": "/tmp/rea-crash-fixture",
"parentProc": "zsh",
"parentPid": 100,
"coalitionName": "com.apple.Terminal",
"instructionByteStream": { "beforePC": "AAAA", "atPC": "AAAA" },
"sip": "enabled",
"vmRegionInfo": "0x10 is not in any region.",
"exception": {
"codes": "0x0000000000000001, 0x0000000000000010",
"rawCodes": [1, 16],
"type": ${JSON.stringify(options.exceptionType ?? "EXC_BAD_ACCESS")},
"signal": ${JSON.stringify(options.signal ?? "SIGSEGV")},
"subtype": ${JSON.stringify(options.subtype ?? "KERN_INVALID_ADDRESS at 0x0000000000000010")}
},
"termination": {
"flags": 0,
"code": ${options.terminationCode ?? "11"},
"namespace": ${JSON.stringify(options.terminationNamespace ?? "SIGNAL")},
"indicator": ${JSON.stringify(options.terminationIndicator ?? "Segmentation fault: 11")},
"byProc": "exc handler",
"byPid": 4242${reasons}
},
"extMods": { "caller": { "thread_create": 0 }, "warnings": 0 },
"faultingThread": 0,
"threads": [
{
"triggered": true,
"id": 1,
"threadState": {
"x": [
{ "value": ${register} },
{ "value": 16 }
],
"flavor": "ARM_THREAD_STATE64",
"lr": { "value": ${String(base + 900)} },
"esr": { "value": 2449473606, "description": "(Data Abort) byte read Translation fault" },
"pc": { "value": ${String(base + imageOffset)}, "matchesCrashFrame": 1 },
"far": { "value": 16 }
},
"queue": "com.apple.main-thread",
"frames": [
{ "imageOffset": ${String(imageOffset)}, "symbol": "rea_fixture_fault", "symbolLocation": 12, "imageIndex": 0 },
{ "imageOffset": 900, "symbol": "main", "symbolLocation": 40, "imageIndex": 0 }
]
},
{
"id": 2,
"name": "worker",
"frames": [
{ "imageOffset": 1000, "symbol": "rea_fixture_inlined", "symbolLocation": 0, "imageIndex": 0, "inline": true },
{ "imageOffset": 1000, "symbol": "rea_fixture_wait", "symbolLocation": 8, "imageIndex": 0 },
{ "imageOffset": 1100, "symbol": "rea_fixture_worker", "symbolLocation": 20, "imageIndex": 0 }
]
}
],
"usedImages": [
{
"source": "P",
"arch": ${archJson},
"base": ${String(base)},
"size": 16384,
"uuid": ${JSON.stringify(uuid)},
"path": "/tmp/rea-crash-fixture",
"name": "rea-crash-fixture"
},
{
"source": "A",
"arch": null,
"base": 0,
"size": 0,
"uuid": "00000000-0000-0000-0000-000000000000",
"path": ""
}
],
"sharedCache": { "base": 6442450944, "size": 4294967296, "uuid": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb" },
"vmSummary": "ReadOnly portion of Libraries: Total=1.0G",
"legacyInfo": { "threadTriggered": { "queue": "com.apple.main-thread" } },
"trialInfo": { "rollouts": [], "experiments": [] }${extra}
}`;
if (options.truncateBody === true) return `${metadata}\n${body.slice(0, 40)}`;
return `${metadata}\n${body}`;
};
export const appleIpsBytes = (options: AppleIpsFixtureOptions = {}): Buffer =>
Buffer.from(appleIpsReport(options), "utf8");
@@ -96,11 +96,13 @@ import { inspectPythonPycInputSchema } from "../../src/domain/python/pythonPyc.j
import { PeResourcesProvider } from "../../src/native/pe/PeResourcesProvider.js";
import { Il2cppMetadataProvider } from "../../src/il2cpp/Il2cppMetadataProvider.js";
import { ElfNotesProvider } from "../../src/native/elf/ElfNotesProvider.js";
import { AppleIpsProvider } from "../../src/native/apple/AppleIpsProvider.js";
import { PeCertificatesProvider } from "../../src/native/pe/PeCertificatesProvider.js";
import { PeImportsProvider } from "../../src/native/pe/PeImportsProvider.js";
import { NativeExportsProvider } from "../../src/native/exports/NativeExportsProvider.js";
import { BeamProvider } from "../../src/erlang/BeamProvider.js";
import { inspectElfNotesInputSchema } from "../../src/domain/native/elfNotes.js";
import { inspectAppleIpsInputSchema } from "../../src/domain/native/appleIps.js";
import { inspectPeCertificatesInputSchema } from "../../src/domain/native/peCertificates.js";
import { inspectPeImportsInputSchema } from "../../src/domain/native/peImports.js";
import { inspectNativeExportsInputSchema } from "../../src/domain/native/nativeExports.js";
@@ -856,6 +858,7 @@ it.each([
const stableArtifactProviders = [
...staticProviders,
["ELF notes", () => new ElfNotesProvider()],
["Apple IPS", () => new AppleIpsProvider()],
["PE certificates", () => new PeCertificatesProvider()],
["PE imports", () => new PeImportsProvider()],
["native exports", () => new NativeExportsProvider()],
@@ -883,6 +886,11 @@ const inspectFileFailure = (
inspectElfNotesInputSchema.parse({ path }),
options,
);
if (provider instanceof AppleIpsProvider)
return provider.inspect(
inspectAppleIpsInputSchema.parse({ path }),
options,
);
if (provider instanceof PeCertificatesProvider)
return provider.inspect(
inspectPeCertificatesInputSchema.parse({ path }),