From efab044f303ae3d957d977730bb1628063223236 Mon Sep 17 00:00:00 2001 From: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com> Date: Wed, 21 Oct 2026 19:06:44 +0800 Subject: [PATCH] feat(gpu): inspect static CUDA and HIP code objects (#5813) --- .../skills/reverse-engineer-anything/SKILL.md | 2 + README.md | 1 + docs/gpu-code-objects.md | 166 ++ docs/readme/README_ar.md | 1 + docs/readme/README_de.md | 1 + docs/readme/README_el.md | 1 + docs/readme/README_es.md | 1 + docs/readme/README_fa.md | 1 + docs/readme/README_fr.md | 1 + docs/readme/README_id.md | 1 + docs/readme/README_it.md | 1 + docs/readme/README_ja.md | 1 + docs/readme/README_ko.md | 1 + docs/readme/README_ml.md | 1 + docs/readme/README_pl.md | 1 + docs/readme/README_pt-BR.md | 1 + docs/readme/README_ru.md | 1 + docs/readme/README_th.md | 1 + docs/readme/README_tr.md | 1 + docs/readme/README_uk.md | 1 + docs/readme/README_vi.md | 1 + docs/readme/README_zh-TW.md | 1 + docs/readme/README_zh.md | 1 + .../gpu-code-objects-2026-10-21.json | 1906 +++++++++++++++++ .../gpu-code-objects-2026-10-21.md | 286 +++ licenses/npm-dependencies.txt | 7 +- licenses/npm-runtime-notices.txt | 16 +- package-lock.json | 10 + package.json | 1 + scripts/lib/catalog-builders.mjs | 5 + scripts/lib/catalog-core.mjs | 2 + scripts/lib/module-boundaries.mjs | 1 + scripts/lib/test-projects.mjs | 2 +- src/application/CapabilityInventory.ts | 1 + src/application/gpu/GpuCodeObjectsEvidence.ts | 57 + src/application/gpu/GpuCodeObjectsPort.ts | 20 + src/application/gpu/GpuCodeObjectsService.ts | 97 + src/cli.ts | 2 + src/cli/gpuCommands.ts | 132 ++ src/cliCommandNames.ts | 1 + src/composition/gpu.ts | 4 + src/contracts/analysisViewToolContracts.ts | 2 +- src/contracts/gpu/gpuToolContracts.ts | 40 + src/contracts/toolContracts.ts | 2 + src/contracts/toolEffects.test.ts | 2 + src/contracts/toolEffects.ts | 1 + src/domain/analysisView/analysisView.ts | 28 + src/domain/analysisView/gpuCodeObjectsView.ts | 421 ++++ src/domain/elfFixedHeader.test.ts | 14 + src/domain/elfFixedHeader.ts | 8 +- src/domain/gpu/gpuCodeObjects.test.ts | 34 + src/domain/gpu/gpuCodeObjects.ts | 391 ++++ src/gpu/AmdGpuMetadata.test.ts | 44 + src/gpu/AmdGpuMetadata.ts | 187 ++ src/gpu/ClangOffloadBundle.ts | 115 + src/gpu/CudaFatbinary.ts | 81 + src/gpu/CudaInfo.test.ts | 29 + src/gpu/CudaInfo.ts | 142 ++ src/gpu/CudaTarget.ts | 46 + src/gpu/GpuCodeObjectsProvider.ts | 334 +++ src/gpu/GpuElfKernels.ts | 491 +++++ src/gpu/GpuInventory.test.ts | 57 + src/gpu/GpuInventory.ts | 575 +++++ src/gpu/GpuObject.ts | 52 + src/gpu/GpuPayloadCodec.test.ts | 21 + src/gpu/GpuPayloadCodec.ts | 55 + src/gpu/GpuSource.ts | 145 ++ src/gpu/Lz4Block.test.ts | 21 + src/gpu/Lz4Block.ts | 75 + src/gpu/PtxKernels.test.ts | 96 + src/gpu/PtxKernels.ts | 397 ++++ src/native/metadata/ElfImage.ts | 264 +-- src/native/metadata/ElfSymbol.ts | 36 + src/native/metadata/NativeSectionDirectory.ts | 336 +++ src/native/metadata/PeImage.ts | 25 +- src/server/createServer.ts | 12 + src/server/registerGpuCodeObjectsTools.ts | 34 + tests/acceptance/gpuCodeObjectsCli.test.ts | 356 +++ tests/boundary/gpu/codeObjects.test.ts | 529 +++++ tests/fixtures/gpu/codeObjects.ts | 301 +++ tests/process-global/gpuFileLifetime.test.ts | 311 +++ 81 files changed, 8598 insertions(+), 251 deletions(-) create mode 100644 docs/gpu-code-objects.md create mode 100644 docs/verification/gpu-code-objects-2026-10-21.json create mode 100644 docs/verification/gpu-code-objects-2026-10-21.md create mode 100644 src/application/gpu/GpuCodeObjectsEvidence.ts create mode 100644 src/application/gpu/GpuCodeObjectsPort.ts create mode 100644 src/application/gpu/GpuCodeObjectsService.ts create mode 100644 src/cli/gpuCommands.ts create mode 100644 src/composition/gpu.ts create mode 100644 src/contracts/gpu/gpuToolContracts.ts create mode 100644 src/domain/analysisView/gpuCodeObjectsView.ts create mode 100644 src/domain/gpu/gpuCodeObjects.test.ts create mode 100644 src/domain/gpu/gpuCodeObjects.ts create mode 100644 src/gpu/AmdGpuMetadata.test.ts create mode 100644 src/gpu/AmdGpuMetadata.ts create mode 100644 src/gpu/ClangOffloadBundle.ts create mode 100644 src/gpu/CudaFatbinary.ts create mode 100644 src/gpu/CudaInfo.test.ts create mode 100644 src/gpu/CudaInfo.ts create mode 100644 src/gpu/CudaTarget.ts create mode 100644 src/gpu/GpuCodeObjectsProvider.ts create mode 100644 src/gpu/GpuElfKernels.ts create mode 100644 src/gpu/GpuInventory.test.ts create mode 100644 src/gpu/GpuInventory.ts create mode 100644 src/gpu/GpuObject.ts create mode 100644 src/gpu/GpuPayloadCodec.test.ts create mode 100644 src/gpu/GpuPayloadCodec.ts create mode 100644 src/gpu/GpuSource.ts create mode 100644 src/gpu/Lz4Block.test.ts create mode 100644 src/gpu/Lz4Block.ts create mode 100644 src/gpu/PtxKernels.test.ts create mode 100644 src/gpu/PtxKernels.ts create mode 100644 src/native/metadata/ElfSymbol.ts create mode 100644 src/native/metadata/NativeSectionDirectory.ts create mode 100644 src/server/registerGpuCodeObjectsTools.ts create mode 100644 tests/acceptance/gpuCodeObjectsCli.test.ts create mode 100644 tests/boundary/gpu/codeObjects.test.ts create mode 100644 tests/fixtures/gpu/codeObjects.ts create mode 100644 tests/process-global/gpuFileLifetime.test.ts diff --git a/.agents/skills/reverse-engineer-anything/SKILL.md b/.agents/skills/reverse-engineer-anything/SKILL.md index 250503e471..6583fb15a2 100644 --- a/.agents/skills/reverse-engineer-anything/SKILL.md +++ b/.agents/skills/reverse-engineer-anything/SKILL.md @@ -181,6 +181,8 @@ explicit path or endpoint and do not need it. absent metadata does not prove a binary is not Go, and unparsed text records remain explicit. See the [Go binary guide](https://github.com/morluto/rea/blob/main/docs/go-binaries.md). +- Embedded CUDA/HIP code objects: `inspect_gpu_code_objects`; inventory targets + first, then decode selected payloads and query retained GPU Evidence. - Bundled GGUF or safetensors model declarations: `inspect_model_artifact` with the explicit local file. Default header hashing leaves full-file identity unknown; `hash: "full"` additionally reads all weights for SHA-256. MCP returns diff --git a/README.md b/README.md index ff4777a55b..c47b6399cb 100644 --- a/README.md +++ b/README.md @@ -180,6 +180,7 @@ Additional tools and host support depend on the target: | Native binaries | Pseudocode, assembly, strings, symbols, calls and references | Hopper, Ghidra or IDA; [native analysis](docs/native-investigation.md) | | Offline ELF layout | Sections, segments, original symbols/relocations and static mitigation candidates | Caller-supplied pwntools on Linux x64; [binary diagnostics](docs/binary-diagnostics.md) | | Go binaries | Embedded compiler version, modules, replacements and build settings | Built-in static ELF/PE/thin Mach-O inspection; no Go installation required | +| GPU code objects | CUDA/HIP targets, selected kernel parameters and resources, retained PTX text | Built-in static ELF/PE/fatbin/CUBIN/PTX inspection; [GPU code objects](docs/gpu-code-objects.md) | | Model artifacts | GGUF / safetensors metadata, tensor declarations and derived totals | Built-in header inspection; optional full-file hash; [model files](docs/model-artifacts.md) | | IL2CPP metadata | Unity global-metadata.dat assembly images, type and method names and index ranges | Built-in static decoding of versions 24, 27, 29, 31 and 39; no Unity required | | Godot PCK | Archive inventory and extraction, original header/directory facts and selected content identities | Built-in static formats 2, 3 and 4; no Godot required | diff --git a/docs/gpu-code-objects.md b/docs/gpu-code-objects.md new file mode 100644 index 0000000000..ead60041c9 --- /dev/null +++ b/docs/gpu-code-objects.md @@ -0,0 +1,166 @@ +# Static CUDA and HIP code objects + +`inspect_gpu_code_objects` answers which GPU targets and kernel declarations a +local artifact carries. It reads ELF/PE sections, standalone CUDA fatbins, +CUBIN/PTX and Clang HIP bundles, including CCOB v1/v2/v3. It neither executes the +artifact nor invokes CUDA/ROCm tools. GPU execution, SASS disassembly and host +launch correlation remain outside coverage. + +Start with a target inventory: + +```json +{ "path": "/artifacts/library.so", "view": { "collection": "targets" } } +``` + +Target rows aggregate inventoried payloads and distinguish an unrequested kernel +analysis from a complete or partial one. `objects` pages expose exact object +IDs, compression declarations and encoded/header locations. Fields, exact +filters, batch IDs and stable pagination select the facts delivered to the +caller. IDs are positional within the recorded artifact SHA-256. Selecting `object_ids` +in a new inspection requires `expected_sha256` from that inventory; a changed +file is rejected before parsing selected payloads. + +`target` is the inventory declaration used for selection; `payload_target` is +the independently observed inner declaration when available. Object pages keep +both values, their kind, `target_consistency` and inspection coverage even with +selected fields. +Conflicting base architectures retain both values, mark object/report/target +coverage partial and annotate the affected kernels. CUBIN flags establish a base +architecture: `base-only` explicitly leaves feature-profile agreement unknown, +including `a` variants. A standalone declaration is `single-source`, and +unexamined payloads remain `unexamined`. An exact AMDGPU target is normalized at +its note owner for comparison with a Clang bundle declaration; raw values remain +intact. Unresolved feature comparisons stay `unknown`. + +`cuda_target_header` retains the OS ABI, normalized raw `e_flags` and exact +header location. CUDA OS ABI `0x33` and the observed `0x41` flags layout use +different architecture fields; they are decoded at one ABI-aware owner. +Unrecognized layouts keep the header and an unknown target instead of +inventing an SM label. Header provenance is validated against its CUBIN object. + +Target filters select inventory declarations. An unselected payload's inner +target remains unexamined; an empty filtered result does not establish that +no enclosed code declares that target. Discovering every inner declaration +requires decoding those payloads. + +HIP IDs with one or two separators before the architecture retain the declared +target. If a recognized AMDGPU bundle ID does not expose a target, the same +bounded note-discovery owner used for standalone AMDGPU files establishes it +before filtering. Those directory/note reads share the inspection work budget; +successive queries can select the target returned by inventory. + +Decode selected targets and return a kernel page in the same call: + +```json +{ + "path": "/artifacts/library.so", + "analysis": "kernels", + "targets": ["sm_100"], + "view": { + "collection": "kernels", + "names": ["kernel_name"], + "fields": [ + "name", + "parameter_count", + "parameter_bytes", + "parameters", + "registers", + "shared_bytes" + ] + } +} +``` + +Only selected payloads are decoded. Other inventory entries remain present with +`not-selected` states, and overall payload coverage remains partial. An empty +kernel page proves no matching retained fact; it does not establish that an +unexamined payload contains no kernel. Missing fields remain null. + +Kernel representations have different meanings: + +- PTX reports parameter declarations and virtual registers. Shared bytes cover + supported static declarations inside that entry; they do not establish module + shared memory, dynamic allocation or physical device resources. + Scalar register declarations count each bare or percent-prefixed name and + each parameterized range. Unsupported declaration forms or overflowing totals + retain an unknown count and partial decoding coverage. Unsupported parameter + layouts and unknown shared-memory declarations likewise retain their known + facts, explicit limitations and partial coverage through retained pages. +- CUBIN identifies CUDA entry symbols and reads numeric `nv.info` records and + native section declarations. Global register declarations also cover compiler + versions that leave the older text-section register field empty. +- AMDGPU reads v3+ MessagePack notes. Argument counts include hidden ABI arguments + when present; SGPR/VGPR counts and fixed group/private segment sizes retain + their declared meanings. Legacy YAML metadata remains unsupported. + +Each selected PTX payload becomes a retained UTF-8 text artifact with a SHA-256, +byte length and source coordinates. It is omitted from compact replies. Read it +with `inspect_analysis_view` and the parent Evidence ID returned by the compact +receipt: + +```json +{ + "source": { "kind": "retained-evidence", "evidence_id": "ev_..." }, + "view": { + "kind": "gpu-code-objects", + "collection": "ptx", + "object_id": "...", + "offset": 0, + "limit": 4096 + } +} +``` + +Text offsets use UTF-16 code units and must fall on Unicode scalar boundaries. +Complete Evidence retains the selected text; `export_evidence_bundle` provides +its portable source record. Historical pages never reopen the input file. + +A location with `container_id: null` uses original file offsets. Other locations +use that inventoried object's decoded byte coordinates. Follow its parent, +encoded location and digest to trace decompressed metadata back to a native +section and the original file. Decoded offsets are never presented as physical +file offsets. + +The CLI uses the same service and contracts: + +```sh +rea inspect-gpu-code-objects /artifacts/library.so --analysis kernels \ + --targets '["sm_100"]' --detail summary \ + --view '{"collection":"kernels","names":["kernel_name"]}' \ + --evidence_output /analysis/library-gpu.evidence.json --json +``` + +A full-file SHA-256 always streams every byte. Inventory then reads bounded +native directories and bundle headers; CCOB requires decompression to discover +its inner directory. Kernel analysis reads selected payloads. `bytes_read` +includes hashing and subsequent descriptor reads, so this is not a header-only +hashing workflow. + +CUDA raw LZ4 blocks and Zstd, and CCOB zlib/Zstd are supported. Unknown versions +or codecs retain declarations with explicit limitations. Lengths are checked +before decoding, and CCOB's truncated MD5 is verified against decoded bytes. +Resource limits are one active inspection, 128 MiB per input/output buffer, +512 MiB of payload input, decoded output and AMD note-read work per inspection, +and 16 MiB retained facts including selected PTX. Payload work is reserved before +reading or allocating; a capacity refusal does not reread an aliased payload. +Native tables and decoded section names each have a 16 MiB structural budget; +AMDGPU ELF note discovery admits at most 8 MiB of source note bytes, with +bounded depth and container/string allocations. Standalone AMD targets are read +from those notes before target selection; selected kernel analysis reuses the +same parsed declarations. Pages have a +1 MiB response budget. Resource refusals and interrupted observations preserve +known facts and explicit uncertainty. + +CUDA ELF parsing additionally shares a 262,144 record/symbol visit limit, +128 MiB of cumulative metadata section reads, and a 16 MiB serialized-size +accounting budget for temporary index facts. These limits are checked before +index growth; they are not claims about JavaScript heap size. Equivalent scoped +metadata byte ranges and repeated equal register declarations are coalesced; +conflicting values remain unknown. Parameter sections are parsed once per ELF. + +Missing CUBIN symbol tables or unresolved section names produce `partial` +object, report and target coverage even when some kernel facts survive. AMD note +failures preserve earlier successfully parsed declarations and the original +failure reason. Standalone PTX recognition tries a small header first, then +continues within the existing 16 MiB text budget when leading comments or +whitespace obscure its directives; admitted full-file probe bytes are reused. diff --git a/docs/readme/README_ar.md b/docs/readme/README_ar.md index 86d7943131..435ed3e7b5 100644 --- a/docs/readme/README_ar.md +++ b/docs/readme/README_ar.md @@ -267,6 +267,7 @@ npx rea-agents@latest setup - [التثبيت والإعداد](../installation.md): تسجيل الوكلاء، وتهيئة الموفّرين، والتحديثات، وإلغاء التثبيت. - [الجاهزية واستكشاف الأخطاء](../installation.md#check-readiness-for-your-task): تشخيص وكيل أو محرك تحليل محدد. +- [CUDA / HIP](../gpu-code-objects.md) - [GGUF / safetensors](../model-artifacts.md) - [CLI وEvidence](../cli.md): الأوامر، واختيار الموفّر، واللقطات، والاستيراد/التصدير، وحالات الخروج. - [عقود MCP](../mcp-contracts.md) و[تعليمات الوكلاء](../mcp-prompts.md): نتائج الأدوات، والجلسات، والاستقصاءات الموجّهة. diff --git a/docs/readme/README_de.md b/docs/readme/README_de.md index 13c5a56592..5fddd80ff3 100644 --- a/docs/readme/README_de.md +++ b/docs/readme/README_de.md @@ -265,6 +265,7 @@ Beginne mit den [praxisnahen Anleitungen](https://rea.tools/de/guides/) auf der - [Installation und Einrichtung](../installation.md): Agentenregistrierung, Provider-Konfiguration, Updates und Deinstallation. - [Bereitschaft und Fehlerbehebung](../installation.md#check-readiness-for-your-task): Diagnose eines bestimmten Agenten oder einer Analyse-Engine. +- [CUDA / HIP](../gpu-code-objects.md) - [GGUF / safetensors](../model-artifacts.md) - [CLI und Evidence](../cli.md): Befehle, Provider-Auswahl, Snapshots, Import/Export und Exit-Status. - [MCP-Kontrakte](../mcp-contracts.md) und [Agenten-Prompts](../mcp-prompts.md): Werkzeugergebnisse, Sitzungen und geführte Untersuchungen. diff --git a/docs/readme/README_el.md b/docs/readme/README_el.md index b7e9e19f7b..93ef1517db 100644 --- a/docs/readme/README_el.md +++ b/docs/readme/README_el.md @@ -278,6 +278,7 @@ npx rea-agents@latest setup - [Εγκατάσταση και ρύθμιση](../installation.md): καταχώριση agents, ρύθμιση παρόχων ανάλυσης, ενημερώσεις και απεγκατάσταση. - [Έλεγχος ετοιμότητας και αντιμετώπιση προβλημάτων](../installation.md#check-readiness-for-your-task): διάγνωση ενός agent ή μιας μηχανής ανάλυσης. +- [CUDA / HIP](../gpu-code-objects.md) - [GGUF / safetensors](../model-artifacts.md) - [CLI και Evidence](../cli.md): εντολές, επιλογή παρόχου, στιγμιότυπα, εισαγωγή/εξαγωγή και κωδικοί εξόδου. - [Προδιαγραφές MCP](../mcp-contracts.md) και [prompts για agents](../mcp-prompts.md): αποτελέσματα εργαλείων, συνεδρίες και καθοδηγούμενες διερευνήσεις. diff --git a/docs/readme/README_es.md b/docs/readme/README_es.md index 26f197ff62..02dc7cda3e 100644 --- a/docs/readme/README_es.md +++ b/docs/readme/README_es.md @@ -265,6 +265,7 @@ Empieza con las [guías prácticas](https://rea.tools/es/guides/) del sitio web. - [Instalación y configuración](../installation.md): registro de agentes, configuración de proveedores, actualizaciones y desinstalación. - [Preparación y resolución de problemas](../installation.md#check-readiness-for-your-task): diagnóstico de un agente o motor de análisis concreto. +- [CUDA / HIP](../gpu-code-objects.md) - [GGUF / safetensors](../model-artifacts.md) - [CLI y Evidence](../cli.md): comandos, selección de proveedores, instantáneas, importación/exportación y estados de salida. - [Contratos de MCP](../mcp-contracts.md) y [prompts para agentes](../mcp-prompts.md): resultados de herramientas, sesiones e investigaciones guiadas. diff --git a/docs/readme/README_fa.md b/docs/readme/README_fa.md index fa23c15949..37a3355d96 100644 --- a/docs/readme/README_fa.md +++ b/docs/readme/README_fa.md @@ -250,6 +250,7 @@ npx rea-agents@latest setup - [نصب و راه‌اندازی](../installation.md): ثبت ایجنت، پیکربندی ارائه‌دهنده، به‌روزرسانی و حذف نصب. - [آمادگی و رفع اشکال](../installation.md#check-readiness-for-your-task): عیب‌یابی یک ایجنت یا موتور تحلیل. +- [CUDA / HIP](../gpu-code-objects.md) - [GGUF / safetensors](../model-artifacts.md) - [CLI و شواهد](../cli.md): دستورات، انتخاب ارائه‌دهنده، snapshotها، ورود/خروج داده و وضعیت‌های خروج. - [قراردادهای MCP](../mcp-contracts.md) و [پرامپت‌های ایجنت](../mcp-prompts.md): نتایج ابزارها، نشست‌ها و بررسی‌های هدایت‌شده. diff --git a/docs/readme/README_fr.md b/docs/readme/README_fr.md index db2a7051bb..40083bd489 100644 --- a/docs/readme/README_fr.md +++ b/docs/readme/README_fr.md @@ -269,6 +269,7 @@ Commencez par les [guides pas à pas](https://rea.tools/fr/guides/) du site web. - [Installation et configuration](../installation.md) : enregistrement des agents, configuration des fournisseurs, mises à jour et désinstallation. - [Vérification et dépannage](../installation.md#check-readiness-for-your-task) : diagnostiquer un agent ou un moteur d'analyse donné. +- [CUDA / HIP](../gpu-code-objects.md) - [GGUF / safetensors](../model-artifacts.md) - [CLI et Evidence](../cli.md) : commandes, choix du fournisseur, snapshots, import/export et codes de sortie. - [Contrats MCP](../mcp-contracts.md) et [prompts pour agents](../mcp-prompts.md) : résultats des outils, sessions et investigations guidées. diff --git a/docs/readme/README_id.md b/docs/readme/README_id.md index d824477757..b8906ba2b3 100644 --- a/docs/readme/README_id.md +++ b/docs/readme/README_id.md @@ -290,6 +290,7 @@ Untuk mengetahui opsi, prasyarat, dan kontrak hasil secara tepat: - [Instalasi dan penyiapan](../installation.md): pendaftaran agen, konfigurasi penyedia, pembaruan, dan penghapusan instalasi. - [Kesiapan dan pemecahan masalah](../installation.md#check-readiness-for-your-task): mendiagnosis satu agen atau mesin analisis. +- [CUDA / HIP](../gpu-code-objects.md) - [GGUF / safetensors](../model-artifacts.md) - [CLI dan Bukti](../cli.md): perintah, pemilihan penyedia, snapshot, impor/ekspor, dan status keluar. - [Kontrak MCP](../mcp-contracts.md) dan [prompt agen](../mcp-prompts.md): hasil alat, sesi, dan investigasi terpandu. diff --git a/docs/readme/README_it.md b/docs/readme/README_it.md index 7f951545a2..6831919be8 100644 --- a/docs/readme/README_it.md +++ b/docs/readme/README_it.md @@ -269,6 +269,7 @@ Inizia dalle [guide pratiche](https://rea.tools/it/guides/) del sito web. Per op - [Installazione e configurazione](../installation.md): registrazione agente, configurazione provider, aggiornamenti e disinstallazione. - [Pronto all'uso e risoluzione dei problemi](../installation.md#check-readiness-for-your-task): diagnostica un agente o un motore di analisi. +- [CUDA / HIP](../gpu-code-objects.md) - [GGUF / safetensors](../model-artifacts.md) - [CLI ed Evidence](../cli.md): comandi, selezione provider, snapshot, import/export e stati di uscita. - [Contratti MCP](../mcp-contracts.md) e [prompt agente](../mcp-prompts.md): risultati degli strumenti, sessioni e indagini guidate. diff --git a/docs/readme/README_ja.md b/docs/readme/README_ja.md index f4595b6824..325287fa4c 100644 --- a/docs/readme/README_ja.md +++ b/docs/readme/README_ja.md @@ -267,6 +267,7 @@ npx rea-agents@latest setup - [インストールとセットアップ](../installation.md):エージェントの登録、プロバイダーの設定、更新、アンインストール。 - [準備状況の確認とトラブルシューティング](../installation.md#check-readiness-for-your-task):特定のエージェントや分析エンジンを診断。 +- [CUDA / HIP](../gpu-code-objects.md) - [GGUF / safetensors](../model-artifacts.md) - [CLI と Evidence](../cli.md):コマンド、プロバイダーの選択、スナップショット、インポート/エクスポート、終了ステータス。 - [MCP 契約](../mcp-contracts.md)と[エージェント向けプロンプト](../mcp-prompts.md):ツールの結果、セッション、ガイド付き調査。 diff --git a/docs/readme/README_ko.md b/docs/readme/README_ko.md index 3fa5f2569a..462fd08e21 100644 --- a/docs/readme/README_ko.md +++ b/docs/readme/README_ko.md @@ -280,6 +280,7 @@ npx rea-agents@latest setup - [설치 및 설정](../installation.md): 에이전트 등록, 제공자 설정, 업데이트, 제거. - [준비 상태 확인 및 문제 해결](../installation.md#check-readiness-for-your-task): 특정 에이전트 또는 분석 엔진 진단. +- [CUDA / HIP](../gpu-code-objects.md) - [GGUF / safetensors](../model-artifacts.md) - [CLI 및 Evidence](../cli.md): 명령, 제공자 선택, 스냅샷, 가져오기/내보내기, 종료 상태. - [MCP 계약](../mcp-contracts.md) 및 [에이전트 프롬프트](../mcp-prompts.md): 도구 결과, 세션, 안내에 따른 조사. diff --git a/docs/readme/README_ml.md b/docs/readme/README_ml.md index 5e07f3663f..6dd281e900 100644 --- a/docs/readme/README_ml.md +++ b/docs/readme/README_ml.md @@ -283,6 +283,7 @@ npx rea-agents@latest setup - [Installation and setup](../installation.md): agent registration, provider configuration, updates, uninstall. - [Readiness and troubleshooting](../installation.md#check-readiness-for-your-task): ഒരു agent-ന്റെയോ analysis engine-ന്റെയോ പ്രശ്നങ്ങൾ കണ്ടെത്തുക. +- [CUDA / HIP](../gpu-code-objects.md) - [GGUF / safetensors](../model-artifacts.md) - [CLI and Evidence](../cli.md): commands, provider തിരഞ്ഞെടുക്കൽ, snapshots, import/export, exit statuses. - [MCP contracts](../mcp-contracts.md), [agent prompts](../mcp-prompts.md): tool results, sessions, guided investigations. diff --git a/docs/readme/README_pl.md b/docs/readme/README_pl.md index 4c692d2364..1ae828eb10 100644 --- a/docs/readme/README_pl.md +++ b/docs/readme/README_pl.md @@ -265,6 +265,7 @@ Zacznij od [przewodników z przykładami](https://rea.tools/pl/guides/) na stron - [Instalacja i konfiguracja](../installation.md): rejestracja agentów, konfiguracja dostawców analizy, aktualizacje i odinstalowanie. - [Gotowość i rozwiązywanie problemów](../installation.md#check-readiness-for-your-task): diagnostyka jednego agenta lub silnika analizy. +- [CUDA / HIP](../gpu-code-objects.md) - [GGUF / safetensors](../model-artifacts.md) - [CLI i Evidence](../cli.md): polecenia, wybór dostawcy, migawki, import/eksport i kody zakończenia. - [Kontrakty MCP](../mcp-contracts.md) i [prompty dla agentów](../mcp-prompts.md): wyniki narzędzi, sesje i prowadzone analizy. diff --git a/docs/readme/README_pt-BR.md b/docs/readme/README_pt-BR.md index 019bba1129..1390281366 100644 --- a/docs/readme/README_pt-BR.md +++ b/docs/readme/README_pt-BR.md @@ -265,6 +265,7 @@ Comece pelos [guias práticos](https://rea.tools/pt-BR/guides/) do site. Para op - [Instalação e configuração](../installation.md): registro de agentes, configuração de provedores, atualizações e desinstalação. - [Prontidão e solução de problemas](../installation.md#check-readiness-for-your-task): diagnóstico de um agente ou motor de análise específico. +- [CUDA / HIP](../gpu-code-objects.md) - [GGUF / safetensors](../model-artifacts.md) - [CLI e Evidence](../cli.md): comandos, seleção de provedores, snapshots, importação/exportação e status de saída. - [Contratos MCP](../mcp-contracts.md) e [prompts para agentes](../mcp-prompts.md): resultados das ferramentas, sessões e investigações guiadas. diff --git a/docs/readme/README_ru.md b/docs/readme/README_ru.md index 22ff426b8a..42234a8c06 100644 --- a/docs/readme/README_ru.md +++ b/docs/readme/README_ru.md @@ -265,6 +265,7 @@ npx rea-agents@latest setup - [Установка и настройка](../installation.md): регистрация агентов, настройка провайдеров, обновление и удаление. - [Готовность и устранение неполадок](../installation.md#check-readiness-for-your-task): диагностика конкретного агента или движка анализа. +- [CUDA / HIP](../gpu-code-objects.md) - [GGUF / safetensors](../model-artifacts.md) - [CLI и Evidence](../cli.md): команды, выбор провайдера, снимки, импорт/экспорт и коды завершения. - [Контракты MCP](../mcp-contracts.md) и [промпты агентов](../mcp-prompts.md): результаты инструментов, сеансы и исследования с подсказками. diff --git a/docs/readme/README_th.md b/docs/readme/README_th.md index fb8c3ff604..f8203910e9 100644 --- a/docs/readme/README_th.md +++ b/docs/readme/README_th.md @@ -265,6 +265,7 @@ npx rea-agents@latest setup - [การติดตั้งและตั้งค่า](../installation.md): การลงทะเบียนเอเจนต์ การตั้งค่าผู้ให้บริการ การอัปเดต และการถอนการติดตั้ง - [ความพร้อมใช้งานและการแก้ไขปัญหา](../installation.md#check-readiness-for-your-task): ตรวจวินิจฉัยเอเจนต์หรือเอนจินวิเคราะห์หนึ่งตัว +- [CUDA / HIP](../gpu-code-objects.md) - [GGUF / safetensors](../model-artifacts.md) - [CLI และ Evidence](../cli.md): คำสั่ง การเลือกผู้ให้บริการ สแนปช็อต การนำเข้า/ส่งออก และรหัสการสิ้นสุด - [สัญญา MCP](../mcp-contracts.md) และ[พรอมป์ต์สำหรับเอเจนต์](../mcp-prompts.md): ผลลัพธ์เครื่องมือ เซสชัน และการตรวจสอบตามคำแนะนำ diff --git a/docs/readme/README_tr.md b/docs/readme/README_tr.md index 2cb89a5496..6758279259 100644 --- a/docs/readme/README_tr.md +++ b/docs/readme/README_tr.md @@ -270,6 +270,7 @@ Web sitesindeki [uygulamalı rehberlerden](https://rea.tools/tr/guides/) başlay - [Yükleme ve kurulum](../installation.md): ajan kaydı, sağlayıcı yapılandırması, güncellemeler ve kaldırma. - [Hazırlık ve sorun giderme](../installation.md#check-readiness-for-your-task): belirli bir ajanı veya analiz motorunu tanılama. +- [CUDA / HIP](../gpu-code-objects.md) - [GGUF / safetensors](../model-artifacts.md) - [CLI ve Evidence](../cli.md): komutlar, sağlayıcı seçimi, anlık görüntüler, içe/dışa aktarma ve çıkış durumları. - [MCP sözleşmeleri](../mcp-contracts.md) ve [ajan istemleri](../mcp-prompts.md): araç sonuçları, oturumlar ve yönlendirmeli araştırmalar. diff --git a/docs/readme/README_uk.md b/docs/readme/README_uk.md index eac65f273e..59c29dbe16 100644 --- a/docs/readme/README_uk.md +++ b/docs/readme/README_uk.md @@ -265,6 +265,7 @@ npx rea-agents@latest setup - [Встановлення й налаштування](../installation.md): реєстрація агентів, конфігурація провайдерів, оновлення та видалення. - [Готовність та усунення неполадок](../installation.md#check-readiness-for-your-task): діагностика конкретного агента або рушія аналізу. +- [CUDA / HIP](../gpu-code-objects.md) - [GGUF / safetensors](../model-artifacts.md) - [CLI та Evidence](../cli.md): команди, вибір провайдера, знімки, імпорт/експорт і статуси завершення. - [Контракти MCP](../mcp-contracts.md) і [промпти агентів](../mcp-prompts.md): результати інструментів, сеанси й керовані дослідження. diff --git a/docs/readme/README_vi.md b/docs/readme/README_vi.md index cfba9f3c47..72f66b8bdc 100644 --- a/docs/readme/README_vi.md +++ b/docs/readme/README_vi.md @@ -265,6 +265,7 @@ Bắt đầu với [hướng dẫn thực hành](https://rea.tools/vi/guides/) t - [Cài đặt và thiết lập](../installation.md): đăng ký tác nhân, cấu hình trình phân tích, cập nhật và gỡ cài đặt. - [Kiểm tra mức sẵn sàng và khắc phục sự cố](../installation.md#check-readiness-for-your-task): chẩn đoán một tác nhân hoặc bộ máy phân tích cụ thể. +- [CUDA / HIP](../gpu-code-objects.md) - [GGUF / safetensors](../model-artifacts.md) - [CLI và Evidence](../cli.md): lệnh, lựa chọn trình phân tích, ảnh chụp trạng thái, nhập/xuất và trạng thái thoát. - [Hợp đồng MCP](../mcp-contracts.md) và [prompt cho tác nhân](../mcp-prompts.md): kết quả công cụ, phiên làm việc và điều tra có hướng dẫn. diff --git a/docs/readme/README_zh-TW.md b/docs/readme/README_zh-TW.md index 635bcbc9af..777dc0d58d 100644 --- a/docs/readme/README_zh-TW.md +++ b/docs/readme/README_zh-TW.md @@ -271,6 +271,7 @@ npx rea-agents@latest setup - [安裝與設定](../installation.md):AI 代理註冊、提供者組態、更新和卸載。 - [就緒檢查與故障排查](../installation.md#check-readiness-for-your-task):診斷特定 AI 代理或分析引擎。 +- [CUDA / HIP](../gpu-code-objects.md) - [GGUF / safetensors](../model-artifacts.md) - [CLI 與 Evidence](../cli.md):命令、提供者選擇、快照、匯入/匯出和退出狀態。 - [MCP 契約](../mcp-contracts.md)和[AI 代理提示詞](../mcp-prompts.md):工具結果、會話和引導式調查。 diff --git a/docs/readme/README_zh.md b/docs/readme/README_zh.md index 313262ae53..e7fe365fba 100644 --- a/docs/readme/README_zh.md +++ b/docs/readme/README_zh.md @@ -271,6 +271,7 @@ npx rea-agents@latest setup - [安装与设置](../installation.md):智能体注册、提供方配置、更新和卸载。 - [就绪检查与故障排查](../installation.md#check-readiness-for-your-task):诊断特定智能体或分析引擎。 +- [CUDA / HIP](../gpu-code-objects.md) - [GGUF / safetensors](../model-artifacts.md) - [CLI 与 Evidence](../cli.md):命令、提供方选择、快照、导入/导出和退出状态。 - [MCP 契约](../mcp-contracts.md)和[智能体提示词](../mcp-prompts.md):工具结果、会话和引导式调查。 diff --git a/docs/verification/gpu-code-objects-2026-10-21.json b/docs/verification/gpu-code-objects-2026-10-21.json new file mode 100644 index 0000000000..ad310ee0fa --- /dev/null +++ b/docs/verification/gpu-code-objects-2026-10-21.json @@ -0,0 +1,1906 @@ +{ + "date": "2026-10-21", + "runtime_source_revision": "c29b98da653658e5e8b2d8c601fb3ed890ed74f8", + "runtime_source_tree": "50940bbf829fd9a757fc1fd5c64296b29bd246a9", + "equivalent_rewritten_revision": "58cccca5123250104033dfde4efcbee89af57f63", + "node": "24.18.0", + "npm": "11.16.0", + "host": "Linux-6.1.0-54-cloud-amd64-x86_64-with-glibc2.36", + "cpu_affinity": [0, 1], + "heap_limit_mib": 3072, + "guard": { + "family_rss_bytes": 8589934592, + "wall_s": 420, + "sample_ms": 20, + "accounting": "Sum payload descendants through all task child lists; RUSAGE_CHILDREN delta for CPU. Observer excluded; sampled peak is approximate." + }, + "published_artifacts": { + "nvjpeg-receipt": { + "url": "https://developer.download.nvidia.com/compute/cuda/redist/libnvjpeg/linux-x86_64/libnvjpeg-linux-x86_64-12.4.0.76-archive.tar.xz", + "manifest": { + "relative_path": "libnvjpeg/linux-x86_64/libnvjpeg-linux-x86_64-12.4.0.76-archive.tar.xz", + "sha256": "ddd8245b2803f5b55211261d7e5d7abf803c05f3b032238d0feaa6e09ea9401d", + "md5": "b9f7806fb279c9cdd0b4a40e4171b87b", + "size": "6378624" + } + }, + "nvjpeg-windows-receipt": { + "url": "https://developer.download.nvidia.com/compute/cuda/redist/libnvjpeg/windows-x86_64/libnvjpeg-windows-x86_64-12.4.0.76-archive.zip", + "archive": { + "relative_path": "libnvjpeg/windows-x86_64/libnvjpeg-windows-x86_64-12.4.0.76-archive.zip", + "sha256": "b253241fc88bf30947b8ee068101aca8930960f113d8ee4a9583de021a79ffa1", + "md5": "d19d13f9bd3dbc9c9361a3c1fee99dd5", + "size": "4667529" + }, + "files": [ + { + "member": "libnvjpeg-windows-x86_64-12.4.0.76-archive/bin/nvjpeg64_12.dll", + "path": "/home/yann/yann/rea-5345-lab/fixtures/nvjpeg64_12.dll", + "size": 7664128, + "sha256": "26559f5319f6b947912daded08680ab248d5af7867529402af050b38b53193ff" + } + ] + }, + "toolchain-receipts": [ + { + "component": "cuda_nvcc", + "url": "https://developer.download.nvidia.com/compute/cuda/redist/cuda_nvcc/linux-x86_64/cuda_nvcc-linux-x86_64-12.9.86-archive.tar.xz", + "sha256": "7a1a5b652e5ef85c82b721d10672fc9a2dbaab44e9bd3c65a69517bf53998c35", + "archive_bytes": 81100244 + }, + { + "component": "cuda_cudart", + "url": "https://developer.download.nvidia.com/compute/cuda/redist/cuda_cudart/linux-x86_64/cuda_cudart-linux-x86_64-12.9.79-archive.tar.xz", + "sha256": "1f6ad42d4f530b24bfa35894ccf6b7209d2354f59101fd62ec4a6192a184ce99", + "archive_bytes": 1514676 + }, + { + "component": "cuda_cuobjdump", + "url": "https://developer.download.nvidia.com/compute/cuda/redist/cuda_cuobjdump/linux-x86_64/cuda_cuobjdump-linux-x86_64-12.9.82-archive.tar.xz", + "sha256": "ee0de40e8c18068bfcc53e73510e7e7a1a80555205347940df67fa525d24452f", + "archive_bytes": 214896 + } + ], + "linux_library": { + "sha256": "27e1eb1834b20db64f99deba379746d8ec46b92975ccb4cdfa06a84d77e4c11e", + "bytes": 9352784, + "prior_full_oracle": { + "cubins": 165, + "ptx": 10, + "native_kernel_resource_tuples": 3720, + "ptx_kernel_declarations": 248, + "result": "All decoded hashes, PTX bytes and native resource tuples matched cuobjdump; final MCP rechecked the 248 sm_100 kernel facts." + } + } + }, + "completed_tasks": { + "final-full": { + "resource_receipt": { + "command": [ + "taskset", + "-c", + "0,1", + "node", + "/home/yann/yann/rea-5345-lab/bench.mjs", + "/home/yann/yann/rea-5345-gpu-code-objects", + "final-full", + "full" + ], + "exit_code": 0, + "wall_s": 15.492352435998328, + "cpu_s": 18.220271, + "cpu_accounting": "payload family wait rusage delta; inherited shell child usage excluded", + "peak_family_rss_bytes": 1139601408, + "sample_ms": 20, + "guard_stop": null + }, + "task_receipt": { + "runtime": "/home/yann/yann/rea-5345-gpu-code-objects", + "label": "final-full", + "mode": "full", + "target": "/home/yann/yann/rea-5345-lab/fixtures/libnvjpeg.so.12.4.0.76", + "task": "Read sm_100 parameter layout, physical registers and shared memory for the named kernel, retain source Evidence and repeat the historical query three times", + "answer": [ + { + "name": "_ZN6nvjpeg33dctQuantInvJpegKernelMultiChannelI7ushort2Li1ELi32ELi8EEEvNS_24InvDctParamsMultiChannelE", + "n": 1, + "bytes": 168, + "parameters": [ + { + "ordinal": 0, + "name": null, + "offset": 0, + "size": 168, + "type": null, + "hidden": null + } + ], + "registers": { + "physical": 31, + "sgpr": null, + "vgpr": null, + "agpr": null, + "virtual": [] + }, + "shared": 9216 + } + ], + "objects": 176, + "kernels": 248, + "coverage": { + "discovery": "complete", + "discovery_scope": "supported-section-names-and-standalone-formats", + "examined_sections": 33, + "payloads": "partial", + "unknown_section_names": 0 + }, + "used_input_declarations_plus_primary_estimated_tokens": 406862, + "all_input_declarations_plus_primary_estimated_tokens": 635381, + "catalog_and_all_calls_estimated_tokens": 1475258, + "token_accounting": "tokenx estimates of advertised declarations, arguments and full SDK replies; initialize and JSON-RPC envelopes omitted; not actual model usage", + "task_wall_s": 14.997796184999999, + "call_times_ms": [ + { + "name": "inspect_gpu_code_objects", + "wall_ms": 8572.237158 + }, + { + "name": "inspect_analysis_view", + "wall_ms": 410.12681299999895 + }, + { + "name": "export_evidence_bundle", + "wall_ms": 38.289639000000534 + }, + { + "name": "inspect_analysis_view", + "wall_ms": 10.732239999999365 + }, + { + "name": "inspect_analysis_view", + "wall_ms": 10.636399999999412 + }, + { + "name": "inspect_analysis_view", + "wall_ms": 9.782919999999649 + } + ] + } + }, + "final-selected": { + "resource_receipt": { + "command": [ + "taskset", + "-c", + "0,1", + "node", + "/home/yann/yann/rea-5345-lab/bench.mjs", + "/home/yann/yann/rea-5345-gpu-code-objects", + "final-selected", + "selected" + ], + "exit_code": 0, + "wall_s": 15.442506011997466, + "cpu_s": 18.074447000000003, + "cpu_accounting": "payload family wait rusage delta; inherited shell child usage excluded", + "peak_family_rss_bytes": 1321566208, + "sample_ms": 20, + "guard_stop": null + }, + "task_receipt": { + "runtime": "/home/yann/yann/rea-5345-gpu-code-objects", + "label": "final-selected", + "mode": "selected", + "target": "/home/yann/yann/rea-5345-lab/fixtures/libnvjpeg.so.12.4.0.76", + "task": "Read sm_100 parameter layout, physical registers and shared memory for the named kernel, retain source Evidence and repeat the historical query three times", + "answer": [ + { + "name": "_ZN6nvjpeg33dctQuantInvJpegKernelMultiChannelI7ushort2Li1ELi32ELi8EEEvNS_24InvDctParamsMultiChannelE", + "n": 1, + "bytes": 168, + "parameters": [ + { + "ordinal": 0, + "name": null, + "offset": 0, + "size": 168, + "type": null, + "hidden": null + } + ], + "registers": { + "physical": 31, + "sgpr": null, + "vgpr": null, + "agpr": null, + "virtual": [] + }, + "shared": 9216 + } + ], + "objects": 176, + "kernels": 248, + "coverage": { + "discovery": "complete", + "discovery_scope": "supported-section-names-and-standalone-formats", + "examined_sections": 33, + "payloads": "partial", + "unknown_section_names": 0 + }, + "used_input_declarations_plus_primary_estimated_tokens": 26106, + "all_input_declarations_plus_primary_estimated_tokens": 254625, + "catalog_and_all_calls_estimated_tokens": 1094502, + "token_accounting": "tokenx estimates of advertised declarations, arguments and full SDK replies; initialize and JSON-RPC envelopes omitted; not actual model usage", + "task_wall_s": 14.924958990999999, + "call_times_ms": [ + { + "name": "inspect_gpu_code_objects", + "wall_ms": 8809.613411 + }, + { + "name": "inspect_analysis_view", + "wall_ms": 435.95342300000084 + }, + { + "name": "export_evidence_bundle", + "wall_ms": 32.59329000000071 + }, + { + "name": "inspect_analysis_view", + "wall_ms": 11.959560000001147 + }, + { + "name": "inspect_analysis_view", + "wall_ms": 11.40408000000025 + }, + { + "name": "inspect_analysis_view", + "wall_ms": 10.63430000000153 + } + ] + } + }, + "final-windows-cli": { + "resource_receipt": { + "command": [ + "taskset", + "-c", + "0,1", + "node", + "scripts/rea.mjs", + "inspect-gpu-code-objects", + "/home/yann/yann/rea-5345-lab/fixtures/nvjpeg64_12.dll", + "--analysis", + "kernels", + "--detail", + "summary", + "--view", + "{\"collection\":\"targets\"}", + "--evidence_output", + "/home/yann/yann/rea-5345-lab/final-windows-evidence.json", + "--json" + ], + "exit_code": 0, + "wall_s": 4.06407550899894, + "cpu_s": 4.908962, + "cpu_accounting": "payload family wait rusage delta; inherited shell child usage excluded", + "peak_family_rss_bytes": 503459840, + "sample_ms": 20, + "guard_stop": null + } + }, + "final-producer-fixtures": { + "resource_receipt": { + "command": [ + "taskset", + "-c", + "0,1", + "node", + "--import", + "tsx", + "/home/yann/yann/rea-5345-lab/probe.mts" + ], + "exit_code": 0, + "wall_s": 0.8731043400002818, + "cpu_s": 1.1189369999999998, + "cpu_accounting": "payload family wait rusage delta; inherited shell child usage excluded", + "peak_family_rss_bytes": 123265024, + "sample_ms": 20, + "guard_stop": null + } + }, + "final-focused": { + "resource_receipt": { + "command": [ + "taskset", + "-c", + "0,1", + "npm", + "run", + "test:focused", + "--", + "tests/acceptance/gpuCodeObjectsCli.test.ts", + "tests/boundary/gpu/codeObjects.test.ts", + "tests/process-global/gpuFileLifetime.test.ts", + "src/domain/gpu/gpuCodeObjects.test.ts", + "src/gpu/AmdGpuMetadata.test.ts", + "src/gpu/CudaInfo.test.ts", + "src/gpu/Lz4Block.test.ts", + "src/gpu/PtxKernels.test.ts", + "src/gpu/GpuInventory.test.ts", + "src/gpu/GpuPayloadCodec.test.ts", + "src/domain/elfFixedHeader.test.ts", + "src/native/metadata/PeImage.test.ts", + "src/native/metadata/Il2cppContainer.test.ts", + "tests/boundary/go/goBinaryMappings.test.ts", + "tests/boundary/go/goBinaryImage.test.ts", + "tests/boundary/rust/rustBinaryImage.test.ts", + "src/rust/RustAuditableReader.test.ts", + "tests/boundary/il2cpp/il2cppBinaryFile.test.ts", + "tests/boundary/mcp/outputSchemaProfile.test.ts" + ], + "exit_code": 0, + "wall_s": 75.88229032499657, + "cpu_s": 136.819571, + "cpu_accounting": "payload family wait rusage delta; inherited shell child usage excluded", + "peak_family_rss_bytes": 3480600576, + "sample_ms": 20, + "guard_stop": null + } + }, + "final-check": { + "resource_receipt": { + "command": ["taskset", "-c", "0,1", "npm", "run", "check"], + "exit_code": 0, + "wall_s": 147.5841734400001, + "cpu_s": 292.92577, + "cpu_accounting": "payload family wait rusage delta; inherited shell child usage excluded", + "peak_family_rss_bytes": 6866296832, + "sample_ms": 20, + "guard_stop": null + } + }, + "qualified-docs-4": { + "resource_receipt": { + "command": ["taskset", "-c", "0,1", "npm", "run", "docs:check"], + "exit_code": 0, + "wall_s": 9.546646034999867, + "cpu_s": 11.63225, + "cpu_accounting": "payload family wait rusage delta; inherited shell child usage excluded", + "peak_family_rss_bytes": 752164864, + "sample_ms": 20, + "guard_stop": null + } + }, + "schema-isolated": { + "resource_receipt": { + "command": [ + "taskset", + "-c", + "0,1", + "npm", + "run", + "test:focused", + "--", + "tests/boundary/mcp/toolSchemaValidity.test.ts" + ], + "exit_code": 0, + "wall_s": 111.93744548900031, + "cpu_s": 140.419096, + "cpu_accounting": "payload family wait rusage delta; inherited shell child usage excluded", + "peak_family_rss_bytes": 3463942144, + "sample_ms": 20, + "guard_stop": null + } + } + }, + "oracle_results": { + "final-linux-oracle": [ + { + "mode": "final-full", + "kernel_facts": 248, + "result": "All previously oracle-verified fields match; new representation is cuda-elf-metadata; selected decoded hashes and source locations match" + }, + { + "mode": "final-selected", + "kernel_facts": 248, + "result": "All previously oracle-verified fields match; new representation is cuda-elf-metadata; selected decoded hashes and source locations match" + } + ], + "final-windows-oracle": { + "input_sha256": "26559f5319f6b947912daded08680ab248d5af7867529402af050b38b53193ff", + "kernels": 3968, + "cubin_hashes": 165, + "ptx_byte_comparisons": 10, + "native_kernel_resource_tuples": 3720, + "result": "Final public CLI retained facts exactly equal previously independently verified producer-oracle facts", + "oracle_scope": "LLVM PE section coordinates; cuobjdump on extracted fatbin, no direct cuobjdump DLL or Windows runtime verification" + }, + "windows-oracle-receipt": { + "cubin_hashes": 165, + "ptx_byte_comparisons": 10, + "native_kernel_resource_tuples": 3720, + "result": "all matched", + "scope": "PE section coordinates from LLVM; NVIDIA oracle consumes extracted fatbin, not DLL directly" + } + }, + "limits": [ + "No GPU execution or Windows runtime verification.", + "CCOB v2 Zstd has a real LLVM 19 producer oracle; v1/v3 and zlib have portable boundary fixtures. Legacy AMDGPU YAML is unsupported.", + "Fresh MCP servers and disabled Node compile cache; OS caches and unrelated host load uncontrolled. One final pair is not a latency distribution.", + "Tokenx estimates, not model billing. Full SDK replies include text and structured representations; initialization and JSON-RPC envelopes omitted.", + "Earlier all-target complete MCP response exceeded the existing transport guard and did not complete. It is not a baseline. Both completed comparisons select sm_100.", + "A prior combined focused run timed out two schema cases at 30 seconds on two CPUs; the isolated schema suite passed 15/15 without changing timeouts." + ], + "review_followup": { + "source_revision": "09c6062cbd70692e6eb4c7cf53b7a10ba886aeb6", + "source_tree": "8f1db7308f83065ce8415f7ffc5d298b4879ec22", + "before": { + "source_revision": "36b87ed45912d0d53191ae3611b258f140019ab9", + "real_gfx900_matching_selection": { + "exit_code": 0, + "target": null, + "kernel_count": 0 + }, + "malformed_targets_cli_issue_path": ["view"] + }, + "after": { + "real_cli": { + "calls": [ + { + "kind": "inventory", + "command": [ + "node", + "scripts/rea.mjs", + "inspect-gpu-code-objects", + "/home/yann/yann/rea-5345-lab/fixtures/add-gfx900.hsaco", + "--detail", + "complete", + "--json" + ], + "exit_code": 0, + "sha256": "0b1c25b301a7ded9fcac09968f5e140940db16df395867496845623e3c38a672", + "target": "amdgcn-amd-amdhsa--gfx900", + "kernel_count": 0 + }, + { + "kind": "selected", + "command": [ + "node", + "scripts/rea.mjs", + "inspect-gpu-code-objects", + "/home/yann/yann/rea-5345-lab/fixtures/add-gfx900.hsaco", + "--detail", + "complete", + "--json", + "--analysis", + "kernels", + "--targets", + "[\"amdgcn-amd-amdhsa--gfx900\"]" + ], + "exit_code": 0, + "sha256": "0b1c25b301a7ded9fcac09968f5e140940db16df395867496845623e3c38a672", + "target": "amdgcn-amd-amdhsa--gfx900", + "kernel_count": 1 + }, + { + "kind": "unselected", + "command": [ + "node", + "scripts/rea.mjs", + "inspect-gpu-code-objects", + "/home/yann/yann/rea-5345-lab/fixtures/add-gfx900.hsaco", + "--detail", + "complete", + "--json", + "--analysis", + "kernels", + "--targets", + "[\"amdgcn-amd-amdhsa--gfx1100\"]" + ], + "exit_code": 0, + "sha256": "0b1c25b301a7ded9fcac09968f5e140940db16df395867496845623e3c38a672", + "target": "amdgcn-amd-amdhsa--gfx900", + "kernel_count": 0 + } + ], + "oracle": "Clang 14 producer source and independent llvm-readelf metadata output; no GPU execution" + }, + "resource_receipt": { + "command": [ + "taskset", + "-c", + "0,1", + "python3", + "/home/yann/yann/rea-5345-lab/review-amd-cli.py" + ], + "exit_code": 0, + "wall_s": 5.9736752989992965, + "cpu_s": 6.950947, + "cpu_accounting": "payload family wait rusage delta; inherited shell child usage excluded", + "peak_family_rss_bytes": 383623168, + "sample_ms": 20, + "guard_stop": null + }, + "focused_resource_receipt": { + "command": [ + "taskset", + "-c", + "0,1", + "npm", + "run", + "test:focused", + "--", + "tests/acceptance/gpuCodeObjectsCli.test.ts", + "tests/boundary/gpu/codeObjects.test.ts", + "tests/process-global/gpuFileLifetime.test.ts", + "src/gpu/AmdGpuMetadata.test.ts" + ], + "exit_code": 0, + "wall_s": 83.7720508450002, + "cpu_s": 138.813059, + "cpu_accounting": "payload family wait rusage delta; inherited shell child usage excluded", + "peak_family_rss_bytes": 3452530688, + "sample_ms": 20, + "guard_stop": null + }, + "focused_tests": { + "files": 4, + "passed": 35 + }, + "catalog_resource_receipt": { + "command": [ + "taskset", + "-c", + "0,1", + "npm", + "run", + "test:focused", + "--", + "tests/boundary/filesystem/productCatalog.test.ts" + ], + "exit_code": 0, + "wall_s": 74.92947246800031, + "cpu_s": 126.30404600000001, + "cpu_accounting": "payload family wait rusage delta; inherited shell child usage excluded", + "peak_family_rss_bytes": 3469959168, + "sample_ms": 20, + "guard_stop": null + }, + "catalog_tests": { + "files": 1, + "passed": 10 + }, + "nine_real_producer_artifacts": "complete payload coverage; rerun after review fixes", + "producer_resource_receipt": { + "command": [ + "taskset", + "-c", + "0,1", + "node", + "--import", + "tsx", + "/home/yann/yann/rea-5345-lab/probe.mts" + ], + "exit_code": 0, + "wall_s": 0.5241675279976334, + "cpu_s": 0.6436419999999999, + "cpu_accounting": "payload family wait rusage delta; inherited shell child usage excluded", + "peak_family_rss_bytes": 124952576, + "sample_ms": 20, + "guard_stop": null + } + }, + "ownership": "Bounded AMD ELF note parser produces target plus parsed kernel declarations once; inventory discovers target before filtering and selected projection reuses prepared notes. The 8 MiB budget applies to aggregate encoded source-note bytes, before their reads; parsed JS representation expands. Prepared notes live only for one payload inspection. CLI parses each named JSON option and preserves the full parsed input for schema diagnostics.", + "performance_scope": "The paired nvJPEG MCP measurements above remain pinned to their original qualified runtime; this follow-up does not claim a new CUDA speed/memory result." + }, + "bounded_metadata_review": { + "source_revision": "ad1e17add9e9daacf0cb60c33d5bf359cf631eab", + "source_tree": "2d4536b06abab51837cf88bc196d415fe4194832", + "base_revision": "52415975942a4b9c53a817915b313ccc404f280e", + "focused_tests": { + "files": 9, + "passed": 70 + }, + "sdk_schema_tests": { + "files": 1, + "passed": 15 + }, + "tasks": { + "rebased-review-focused": { + "resource_receipt": { + "command": [ + "taskset", + "-c", + "0,1", + "npm", + "run", + "test:focused", + "--", + "tests/acceptance/gpuCodeObjectsCli.test.ts", + "tests/boundary/gpu/codeObjects.test.ts", + "tests/process-global/gpuFileLifetime.test.ts", + "src/gpu/CudaInfo.test.ts", + "src/gpu/PtxKernels.test.ts", + "src/domain/gpu/gpuCodeObjects.test.ts", + "tests/boundary/filesystem/productCatalog.test.ts", + "src/domain/analysisView/rustBinaryView.test.ts", + "tests/acceptance/rustDependencyViews.test.ts" + ], + "exit_code": 0, + "wall_s": 167.9911166489983, + "cpu_s": 316.988263, + "cpu_accounting": "payload family wait rusage delta; inherited shell child usage excluded", + "peak_family_rss_bytes": 3486601216, + "sample_ms": 20, + "guard_stop": null + } + }, + "revised-schema-isolated": { + "resource_receipt": { + "command": [ + "taskset", + "-c", + "0,1", + "npm", + "run", + "test:focused", + "--", + "tests/boundary/mcp/toolSchemaValidity.test.ts" + ], + "exit_code": 0, + "wall_s": 43.76464435999878, + "cpu_s": 50.095241, + "cpu_accounting": "payload family wait rusage delta; inherited shell child usage excluded", + "peak_family_rss_bytes": 1928617984, + "sample_ms": 20, + "guard_stop": null + } + }, + "revised-full": { + "resource_receipt": { + "command": [ + "taskset", + "-c", + "0,1", + "node", + "/home/yann/yann/rea-5345-lab/bench.mjs", + "/home/yann/yann/rea-5345-gpu-code-objects", + "revised-full", + "full" + ], + "exit_code": 0, + "wall_s": 15.4867707600024, + "cpu_s": 18.404025999999998, + "cpu_accounting": "payload family wait rusage delta; inherited shell child usage excluded", + "peak_family_rss_bytes": 1251254272, + "sample_ms": 20, + "guard_stop": null + }, + "task_receipt": { + "runtime": "/home/yann/yann/rea-5345-gpu-code-objects", + "label": "revised-full", + "mode": "full", + "target": "/home/yann/yann/rea-5345-lab/fixtures/libnvjpeg.so.12.4.0.76", + "task": "Read sm_100 parameter layout, physical registers and shared memory for the named kernel, retain source Evidence and repeat the historical query three times", + "answer": [ + { + "name": "_ZN6nvjpeg33dctQuantInvJpegKernelMultiChannelI7ushort2Li1ELi32ELi8EEEvNS_24InvDctParamsMultiChannelE", + "n": 1, + "bytes": 168, + "parameters": [ + { + "ordinal": 0, + "name": null, + "offset": 0, + "size": 168, + "type": null, + "hidden": null + } + ], + "registers": { + "physical": 31, + "sgpr": null, + "vgpr": null, + "agpr": null, + "virtual": [] + }, + "shared": 9216 + } + ], + "objects": 176, + "kernels": 248, + "coverage": { + "discovery": "complete", + "discovery_scope": "supported-section-names-and-standalone-formats", + "examined_sections": 33, + "payloads": "partial", + "unknown_section_names": 0 + }, + "used_input_declarations_plus_primary_estimated_tokens": 407368, + "all_input_declarations_plus_primary_estimated_tokens": 636495, + "catalog_and_all_calls_estimated_tokens": 1480698, + "token_accounting": "tokenx estimates of advertised declarations, arguments and full SDK replies; initialize and JSON-RPC envelopes omitted; not actual model usage", + "task_wall_s": 14.930120244, + "call_times_ms": [ + { + "name": "inspect_gpu_code_objects", + "wall_ms": 8648.842552000002 + }, + { + "name": "inspect_analysis_view", + "wall_ms": 392.55837599999904 + }, + { + "name": "export_evidence_bundle", + "wall_ms": 32.70938000000024 + }, + { + "name": "inspect_analysis_view", + "wall_ms": 12.31524999999965 + }, + { + "name": "inspect_analysis_view", + "wall_ms": 11.467388999999457 + }, + { + "name": "inspect_analysis_view", + "wall_ms": 11.990239999999176 + } + ] + } + }, + "revised-selected": { + "resource_receipt": { + "command": [ + "taskset", + "-c", + "0,1", + "node", + "/home/yann/yann/rea-5345-lab/bench.mjs", + "/home/yann/yann/rea-5345-gpu-code-objects", + "revised-selected", + "selected" + ], + "exit_code": 0, + "wall_s": 15.923008769001171, + "cpu_s": 18.760102, + "cpu_accounting": "payload family wait rusage delta; inherited shell child usage excluded", + "peak_family_rss_bytes": 1201774592, + "sample_ms": 20, + "guard_stop": null + }, + "task_receipt": { + "runtime": "/home/yann/yann/rea-5345-gpu-code-objects", + "label": "revised-selected", + "mode": "selected", + "target": "/home/yann/yann/rea-5345-lab/fixtures/libnvjpeg.so.12.4.0.76", + "task": "Read sm_100 parameter layout, physical registers and shared memory for the named kernel, retain source Evidence and repeat the historical query three times", + "answer": [ + { + "name": "_ZN6nvjpeg33dctQuantInvJpegKernelMultiChannelI7ushort2Li1ELi32ELi8EEEvNS_24InvDctParamsMultiChannelE", + "n": 1, + "bytes": 168, + "parameters": [ + { + "ordinal": 0, + "name": null, + "offset": 0, + "size": 168, + "type": null, + "hidden": null + } + ], + "registers": { + "physical": 31, + "sgpr": null, + "vgpr": null, + "agpr": null, + "virtual": [] + }, + "shared": 9216 + } + ], + "objects": 176, + "kernels": 248, + "coverage": { + "discovery": "complete", + "discovery_scope": "supported-section-names-and-standalone-formats", + "examined_sections": 33, + "payloads": "partial", + "unknown_section_names": 0 + }, + "used_input_declarations_plus_primary_estimated_tokens": 26612, + "all_input_declarations_plus_primary_estimated_tokens": 255739, + "catalog_and_all_calls_estimated_tokens": 1099942, + "token_accounting": "tokenx estimates of advertised declarations, arguments and full SDK replies; initialize and JSON-RPC envelopes omitted; not actual model usage", + "task_wall_s": 15.387531861, + "call_times_ms": [ + { + "name": "inspect_gpu_code_objects", + "wall_ms": 8730.058778999999 + }, + { + "name": "inspect_analysis_view", + "wall_ms": 402.6264579999988 + }, + { + "name": "export_evidence_bundle", + "wall_ms": 43.726230000000214 + }, + { + "name": "inspect_analysis_view", + "wall_ms": 10.474229000001287 + }, + { + "name": "inspect_analysis_view", + "wall_ms": 10.128700999999637 + }, + { + "name": "inspect_analysis_view", + "wall_ms": 10.954589000000851 + } + ] + } + }, + "revised-windows-cli": { + "resource_receipt": { + "command": [ + "taskset", + "-c", + "0,1", + "node", + "scripts/rea.mjs", + "inspect-gpu-code-objects", + "/home/yann/yann/rea-5345-lab/fixtures/nvjpeg64_12.dll", + "--analysis", + "kernels", + "--detail", + "summary", + "--view", + "{\"collection\":\"targets\"}", + "--evidence_output", + "/home/yann/yann/rea-5345-lab/revised-windows-evidence.json", + "--json" + ], + "exit_code": 0, + "wall_s": 4.078310482000234, + "cpu_s": 4.879858, + "cpu_accounting": "payload family wait rusage delta; inherited shell child usage excluded", + "peak_family_rss_bytes": 498380800, + "sample_ms": 20, + "guard_stop": null + } + }, + "revised-edge-cli": { + "resource_receipt": { + "command": [ + "taskset", + "-c", + "0,1", + "python3", + "/home/yann/yann/rea-5345-lab/verify-review-edges.py" + ], + "exit_code": 0, + "wall_s": 5.132059294999635, + "cpu_s": 6.221724999999999, + "cpu_accounting": "payload family wait rusage delta; inherited shell child usage excluded", + "peak_family_rss_bytes": 405835776, + "sample_ms": 20, + "guard_stop": null + } + }, + "revised-producer-fixtures": { + "resource_receipt": { + "command": [ + "taskset", + "-c", + "0,1", + "node", + "--import", + "tsx", + "/home/yann/yann/rea-5345-lab/probe.mts" + ], + "exit_code": 0, + "wall_s": 0.501328217000264, + "cpu_s": 0.621808999999999, + "cpu_accounting": "payload family wait rusage delta; inherited shell child usage excluded", + "peak_family_rss_bytes": 125714432, + "sample_ms": 20, + "guard_stop": null + } + } + }, + "oracle": { + "source": "ad1e17add9e9daacf0cb60c33d5bf359cf631eab", + "windows_cubin_hashes": 165, + "windows_ptx_byte_comparisons": 10, + "unchanged_native_kernel_facts": 3720, + "independently_counted_ptx_register_declarations": 248, + "corrected_ptx_kernel_register_totals": 20, + "virtual_oracle_scope": "Independent Python declaration counts from cuobjdump-extracted PTX, not physical allocation or GPU execution", + "linux_sm100_kernel_facts_per_mcp_task": 248, + "nine_producer_artifacts": "complete payload coverage", + "edge_workflows": "three public CLI cases completed; see review-edge-results.json" + }, + "edge_inputs": [ + { + "path": "/home/yann/yann/rea-5345-lab/fixtures/aliased-cubin", + "bytes": 861896, + "sha256": "dc363e8192d11dcb2d9718f1467b636ab707c10a07e5919a990d9c1db0ff40c9" + }, + { + "path": "/home/yann/yann/rea-5345-lab/fixtures/partial-amdgpu", + "bytes": 608, + "sha256": "f403ac1c38758d4226111daaa4ec9aeb4ea03ff73fad9efe5eb2d1808093ff20" + }, + { + "path": "/home/yann/yann/rea-5345-lab/fixtures/long-header-ptx", + "bytes": 71946, + "sha256": "567068fe43816d4ad7ec6c2de47f3536f5041da974c8fc686a518e3f10445159" + } + ], + "edge_results": [ + { + "case": "aliased-cubin", + "command": [ + "node", + "scripts/rea.mjs", + "inspect-gpu-code-objects", + "/home/yann/yann/rea-5345-lab/fixtures/aliased-cubin", + "--analysis", + "kernels", + "--detail", + "complete", + "--json" + ], + "exit_code": 0, + "sha256": "dc363e8192d11dcb2d9718f1467b636ab707c10a07e5919a990d9c1db0ff40c9", + "inspection": "complete", + "coverage": { + "discovery_scope": "supported-section-names-and-standalone-formats", + "discovery": "complete", + "payloads": "complete", + "examined_sections": 0, + "unknown_section_names": 0 + }, + "kernel": { + "name": "add_one", + "parameter_count": 2, + "parameter_bytes": 12, + "registers": { + "physical": 10, + "sgpr": null, + "vgpr": null, + "agpr": null, + "virtual": [] + } + } + }, + { + "case": "partial-amdgpu", + "command": [ + "node", + "scripts/rea.mjs", + "inspect-gpu-code-objects", + "/home/yann/yann/rea-5345-lab/fixtures/partial-amdgpu", + "--analysis", + "kernels", + "--detail", + "complete", + "--json" + ], + "exit_code": 0, + "sha256": "f403ac1c38758d4226111daaa4ec9aeb4ea03ff73fad9efe5eb2d1808093ff20", + "inspection": "malformed", + "coverage": { + "discovery_scope": "supported-section-names-and-standalone-formats", + "discovery": "complete", + "payloads": "partial", + "examined_sections": 0, + "unknown_section_names": 0 + }, + "kernel": { + "name": "add_one", + "parameter_count": 2, + "parameter_bytes": 12, + "registers": { + "physical": null, + "sgpr": 6, + "vgpr": 2, + "agpr": null, + "virtual": [] + } + } + }, + { + "case": "long-header-ptx", + "command": [ + "node", + "scripts/rea.mjs", + "inspect-gpu-code-objects", + "/home/yann/yann/rea-5345-lab/fixtures/long-header-ptx", + "--analysis", + "kernels", + "--detail", + "complete", + "--json" + ], + "exit_code": 0, + "sha256": "567068fe43816d4ad7ec6c2de47f3536f5041da974c8fc686a518e3f10445159", + "inspection": "complete", + "coverage": { + "discovery_scope": "supported-section-names-and-standalone-formats", + "discovery": "complete", + "payloads": "complete", + "examined_sections": 0, + "unknown_section_names": 0 + }, + "kernel": { + "name": "add_one", + "parameter_count": 2, + "parameter_bytes": 12, + "registers": { + "physical": null, + "sgpr": null, + "vgpr": null, + "agpr": null, + "virtual": [ + { + "type": ".b32", + "count": 9 + }, + { + "type": ".b64", + "count": 5 + } + ] + } + } + } + ], + "limitations": [ + "The prior adversarial expansion is source-derived; the unsafe 67-million-entry old path was not executed.", + "Aliased metadata coalesces identical scoped byte declarations, preserving first source coordinates and distinct conflicting values.", + "Index accounting bounds serialized fact size, aggregate visits and reads; it is not a byte-accurate JS heap limit.", + "PTX register totals are source declarations, independently counted from cuobjdump-extracted text; physical allocation remains outside this oracle.", + "Main CI browser termination and wasm optional-commandTimeoutMs assertions failed identically without the GPU changes; fail-fast cancelled shards remain unverified." + ] + }, + "target_consistency_and_cuda_abi": { + "source_revision": "f5be46f9b3b01d04feb778bdafe288b666bd283c", + "source_tree": "8243a36743a332f3f4f37a9fb46a04b547872d4e", + "measurement_revision": "21eaa0d8f0195f182c06a6d85378cbdf99fb43bb", + "measurement_tree": "fcf82a374a9d268b5a34a9b62f07c7722a568c46", + "base_revision": "f801b2fbfd28f4e3713adefddc7bd73db44c284c", + "qualification": { + "shared_header_gpu_native_focused_passed": 251, + "latest_unknown_summary_and_gpu_boundary_passed": 46, + "sdk_schema_passed": 15 + }, + "oracle": { + "source_revision": "21eaa0d8f0195f182c06a6d85378cbdf99fb43bb", + "windows_kernel_facts_equal_prior_corrected_oracle": 3968, + "independent_cubin_target_and_header_checks": 165, + "os_abi_counts": { + "65": 55, + "51": 110 + }, + "false_conflicts": 0, + "ptx_byte_identities": 10, + "independent_virtual_register_totals": "248 PTX kernels verified at prior corrected stage; PTX facts unchanged", + "nine_real_producer_artifacts": "complete payload coverage" + }, + "hip_controls": { + "oracle": "Same real Clang 14 device ELF, independently checked with llvm-readelf; portable alternate wrappers, not a real legacy producer claim", + "cases": [ + { + "wrapper": "double", + "sha256": "34d7fce938cbaa2bdc55300d522698ccf6b4d71694573045378e5e4f39766f16", + "selected_target": "gfx900", + "payload_target": "amdgcn-amd-amdhsa--gfx900", + "target_consistency": "consistent", + "kernel": { + "name": "add_one", + "parameter_count": 2, + "parameter_bytes": 12, + "registers": { + "physical": null, + "sgpr": 6, + "vgpr": 2, + "agpr": null, + "virtual": [] + } + } + }, + { + "wrapper": "single", + "sha256": "a46f62836fcc7f9ed93a0ea1e4b35305dc69839a5b1bf65028dd86bba1df444b", + "selected_target": "gfx900", + "payload_target": "amdgcn-amd-amdhsa--gfx900", + "target_consistency": "consistent", + "kernel": { + "name": "add_one", + "parameter_count": 2, + "parameter_bytes": 12, + "registers": { + "physical": null, + "sgpr": 6, + "vgpr": 2, + "agpr": null, + "virtual": [] + } + } + }, + { + "wrapper": "opaque", + "sha256": "12c7e35d38844f8129991fc87e99f43458173dbcb4a4830d68c296ce6f4027c2", + "selected_target": "amdgcn-amd-amdhsa--gfx900", + "payload_target": "amdgcn-amd-amdhsa--gfx900", + "target_consistency": "single-source", + "kernel": { + "name": "add_one", + "parameter_count": 2, + "parameter_bytes": 12, + "registers": { + "physical": null, + "sgpr": 6, + "vgpr": 2, + "agpr": null, + "virtual": [] + } + } + } + ] + }, + "tasks": { + "cuda-abi-focused": { + "resource_receipt": { + "command": [ + "taskset", + "-c", + "0,1", + "npm", + "run", + "test:focused", + "--", + "tests/acceptance/gpuCodeObjectsCli.test.ts", + "tests/boundary/gpu/codeObjects.test.ts", + "tests/process-global/gpuFileLifetime.test.ts", + "src/domain/elfFixedHeader.test.ts", + "tests/boundary/go/goBinaryMappings.test.ts", + "tests/boundary/go/goBinaryImage.test.ts", + "tests/boundary/rust/rustBinaryImage.test.ts", + "src/rust/RustAuditableReader.test.ts", + "tests/boundary/il2cpp/il2cppBinaryFile.test.ts" + ], + "exit_code": 0, + "wall_s": 111.51468113699957, + "cpu_s": 182.05457199999998, + "cpu_accounting": "payload family wait rusage delta; inherited shell child usage excluded", + "peak_family_rss_bytes": 3498504192, + "sample_ms": 20, + "guard_stop": null + } + }, + "unknown-target-focused": { + "resource_receipt": { + "command": [ + "taskset", + "-c", + "0,1", + "npm", + "run", + "test:focused", + "--", + "tests/acceptance/gpuCodeObjectsCli.test.ts", + "tests/boundary/gpu/codeObjects.test.ts" + ], + "exit_code": 0, + "wall_s": 131.12109861799763, + "cpu_s": 216.183547, + "cpu_accounting": "payload family wait rusage delta; inherited shell child usage excluded", + "peak_family_rss_bytes": 3476627456, + "sample_ms": 20, + "guard_stop": null + } + }, + "final-abi-schema": { + "resource_receipt": { + "command": [ + "taskset", + "-c", + "0,1", + "npm", + "run", + "test:focused", + "--", + "tests/boundary/mcp/toolSchemaValidity.test.ts" + ], + "exit_code": 0, + "wall_s": 114.20525343700137, + "cpu_s": 175.515544, + "cpu_accounting": "payload family wait rusage delta; inherited shell child usage excluded", + "peak_family_rss_bytes": 3491016704, + "sample_ms": 20, + "guard_stop": null + } + }, + "final-abi-windows": { + "resource_receipt": { + "command": [ + "taskset", + "-c", + "0,1", + "node", + "scripts/rea.mjs", + "inspect-gpu-code-objects", + "/home/yann/yann/rea-5345-lab/fixtures/nvjpeg64_12.dll", + "--analysis", + "kernels", + "--detail", + "summary", + "--view", + "{\"collection\":\"targets\"}", + "--evidence_output", + "/home/yann/yann/rea-5345-lab/final-abi-windows-evidence.json", + "--json" + ], + "exit_code": 0, + "wall_s": 5.44585134999943, + "cpu_s": 5.915798000000001, + "cpu_accounting": "payload family wait rusage delta; inherited shell child usage excluded", + "peak_family_rss_bytes": 498192384, + "sample_ms": 20, + "guard_stop": null + } + }, + "final-abi-full": { + "resource_receipt": { + "command": [ + "taskset", + "-c", + "0,1", + "node", + "/home/yann/yann/rea-5345-lab/bench.mjs", + "/home/yann/yann/rea-5345-gpu-code-objects", + "final-abi-full", + "full" + ], + "exit_code": 0, + "wall_s": 21.306950419002533, + "cpu_s": 23.537641, + "cpu_accounting": "payload family wait rusage delta; inherited shell child usage excluded", + "peak_family_rss_bytes": 1146519552, + "sample_ms": 20, + "guard_stop": null + }, + "task_receipt": { + "runtime": "/home/yann/yann/rea-5345-gpu-code-objects", + "label": "final-abi-full", + "mode": "full", + "target": "/home/yann/yann/rea-5345-lab/fixtures/libnvjpeg.so.12.4.0.76", + "task": "Read sm_100 parameter layout, physical registers and shared memory for the named kernel, retain source Evidence and repeat the historical query three times", + "answer": [ + { + "name": "_ZN6nvjpeg33dctQuantInvJpegKernelMultiChannelI7ushort2Li1ELi32ELi8EEEvNS_24InvDctParamsMultiChannelE", + "n": 1, + "bytes": 168, + "parameters": [ + { + "ordinal": 0, + "name": null, + "offset": 0, + "size": 168, + "type": null, + "hidden": null + } + ], + "registers": { + "physical": 31, + "sgpr": null, + "vgpr": null, + "agpr": null, + "virtual": [] + }, + "shared": 9216 + } + ], + "objects": 176, + "kernels": 248, + "coverage": { + "discovery": "complete", + "discovery_scope": "supported-section-names-and-standalone-formats", + "examined_sections": 33, + "payloads": "partial", + "unknown_section_names": 0 + }, + "used_input_declarations_plus_primary_estimated_tokens": 418967, + "all_input_declarations_plus_primary_estimated_tokens": 648242, + "catalog_and_all_calls_estimated_tokens": 1493041, + "token_accounting": "tokenx estimates of advertised declarations, arguments and full SDK replies; initialize and JSON-RPC envelopes omitted; not actual model usage", + "task_wall_s": 20.560803566, + "call_times_ms": [ + { + "name": "inspect_gpu_code_objects", + "wall_ms": 11339.654018000001 + }, + { + "name": "inspect_analysis_view", + "wall_ms": 638.0042919999978 + }, + { + "name": "export_evidence_bundle", + "wall_ms": 74.49857999999949 + }, + { + "name": "inspect_analysis_view", + "wall_ms": 27.067176999997173 + }, + { + "name": "inspect_analysis_view", + "wall_ms": 23.736125999999786 + }, + { + "name": "inspect_analysis_view", + "wall_ms": 23.241156000000046 + } + ] + } + }, + "final-abi-selected": { + "resource_receipt": { + "command": [ + "taskset", + "-c", + "0,1", + "node", + "/home/yann/yann/rea-5345-lab/bench.mjs", + "/home/yann/yann/rea-5345-gpu-code-objects", + "final-abi-selected", + "selected" + ], + "exit_code": 0, + "wall_s": 24.60369418600021, + "cpu_s": 26.454144, + "cpu_accounting": "payload family wait rusage delta; inherited shell child usage excluded", + "peak_family_rss_bytes": 1341333504, + "sample_ms": 20, + "guard_stop": null + }, + "task_receipt": { + "runtime": "/home/yann/yann/rea-5345-gpu-code-objects", + "label": "final-abi-selected", + "mode": "selected", + "target": "/home/yann/yann/rea-5345-lab/fixtures/libnvjpeg.so.12.4.0.76", + "task": "Read sm_100 parameter layout, physical registers and shared memory for the named kernel, retain source Evidence and repeat the historical query three times", + "answer": [ + { + "name": "_ZN6nvjpeg33dctQuantInvJpegKernelMultiChannelI7ushort2Li1ELi32ELi8EEEvNS_24InvDctParamsMultiChannelE", + "n": 1, + "bytes": 168, + "parameters": [ + { + "ordinal": 0, + "name": null, + "offset": 0, + "size": 168, + "type": null, + "hidden": null + } + ], + "registers": { + "physical": 31, + "sgpr": null, + "vgpr": null, + "agpr": null, + "virtual": [] + }, + "shared": 9216 + } + ], + "objects": 176, + "kernels": 248, + "coverage": { + "discovery": "complete", + "discovery_scope": "supported-section-names-and-standalone-formats", + "examined_sections": 33, + "payloads": "partial", + "unknown_section_names": 0 + }, + "used_input_declarations_plus_primary_estimated_tokens": 26653, + "all_input_declarations_plus_primary_estimated_tokens": 255928, + "catalog_and_all_calls_estimated_tokens": 1100727, + "token_accounting": "tokenx estimates of advertised declarations, arguments and full SDK replies; initialize and JSON-RPC envelopes omitted; not actual model usage", + "task_wall_s": 23.815860039, + "call_times_ms": [ + { + "name": "inspect_gpu_code_objects", + "wall_ms": 13581.298396999999 + }, + { + "name": "inspect_analysis_view", + "wall_ms": 586.4171619999979 + }, + { + "name": "export_evidence_bundle", + "wall_ms": 146.93752499999755 + }, + { + "name": "inspect_analysis_view", + "wall_ms": 21.25048599999718 + }, + { + "name": "inspect_analysis_view", + "wall_ms": 20.08134400000199 + }, + { + "name": "inspect_analysis_view", + "wall_ms": 18.104333999999653 + } + ] + } + }, + "final-abi-hip": { + "resource_receipt": { + "command": [ + "taskset", + "-c", + "0,1", + "python3", + "/home/yann/yann/rea-5345-lab/hip-roundtrip.py" + ], + "exit_code": 0, + "wall_s": 15.695726638998167, + "cpu_s": 16.426953, + "cpu_accounting": "payload family wait rusage delta; inherited shell child usage excluded", + "peak_family_rss_bytes": 382410752, + "sample_ms": 20, + "guard_stop": null + } + }, + "final-abi-producers": { + "resource_receipt": { + "command": [ + "taskset", + "-c", + "0,1", + "node", + "--import", + "tsx", + "/home/yann/yann/rea-5345-lab/probe.mts" + ], + "exit_code": 0, + "wall_s": 0.7437617239993415, + "cpu_s": 0.6657989999999927, + "cpu_accounting": "payload family wait rusage delta; inherited shell child usage excluded", + "peak_family_rss_bytes": 125800448, + "sample_ms": 20, + "guard_stop": null + } + } + }, + "superseded_diagnostics": { + "revision": "6dd8f0dad26e677118f08dfe407c8d01c3dced34", + "outcome": "Process tasks completed, but 55 modern CUDA objects received false target-conflict facts. These runs are not correctness-qualified performance evidence. The ABI-qualified producer replaces the low-byte-only interpretation.", + "same_code_timing_variability": "The selected diagnostic run used 33.803 CPU s, while its repeat used 18.043 CPU s. No causal attribution or general speedup is established.", + "receipts": { + "target-full": { + "command": [ + "taskset", + "-c", + "0,1", + "node", + "/home/yann/yann/rea-5345-lab/bench.mjs", + "/home/yann/yann/rea-5345-gpu-code-objects", + "target-full", + "full" + ], + "exit_code": 0, + "wall_s": 30.42589916200086, + "cpu_s": 33.988588, + "cpu_accounting": "payload family wait rusage delta; inherited shell child usage excluded", + "peak_family_rss_bytes": 1337733120, + "sample_ms": 20, + "guard_stop": null + }, + "target-selected": { + "command": [ + "taskset", + "-c", + "0,1", + "node", + "/home/yann/yann/rea-5345-lab/bench.mjs", + "/home/yann/yann/rea-5345-gpu-code-objects", + "target-selected", + "selected" + ], + "exit_code": 0, + "wall_s": 29.977477751999686, + "cpu_s": 33.803096, + "cpu_accounting": "payload family wait rusage delta; inherited shell child usage excluded", + "peak_family_rss_bytes": 1163767808, + "sample_ms": 20, + "guard_stop": null + }, + "control-before-selected": { + "command": [ + "taskset", + "-c", + "0,1", + "node", + "/home/yann/yann/rea-5345-lab/bench.mjs", + "/home/yann/yann/rea-5345-perf-before", + "control-before-selected", + "selected" + ], + "exit_code": 0, + "wall_s": 16.23110394300238, + "cpu_s": 18.834853000000003, + "cpu_accounting": "payload family wait rusage delta; inherited shell child usage excluded", + "peak_family_rss_bytes": 1337622528, + "sample_ms": 20, + "guard_stop": null + }, + "control-after-selected": { + "command": [ + "taskset", + "-c", + "0,1", + "node", + "/home/yann/yann/rea-5345-lab/bench.mjs", + "/home/yann/yann/rea-5345-gpu-code-objects", + "control-after-selected", + "selected" + ], + "exit_code": 0, + "wall_s": 15.375846307000756, + "cpu_s": 18.043155, + "cpu_accounting": "payload family wait rusage delta; inherited shell child usage excluded", + "peak_family_rss_bytes": 1186308096, + "sample_ms": 20, + "guard_stop": null + } + } + }, + "limitations": [ + "Raw OS ABI and flags plus header provenance remain retained. Recognized ABI/layout predicates produce base architecture declarations; unsupported patterns remain unknown/partial. No hardware compatibility or feature-profile proof is claimed.", + "Alternate HIP wrappers contain the same real Clang ELF; they do not establish behavior of a legacy compiler producer.", + "Final normal-workflow measurements precede the last guard that exposes failed target discovery as partial; this guard only changes unresolved observations.", + "Baseline browser and WASM CLI-schema CI failures remain separately reproduced on unmodified main." + ] + }, + "payload_admission_and_ptx_coverage": { + "source_revision": "893e04d22f2316d1c2794048161176d0e307d10a", + "source_tree": "c2cc1a45c911a34de187b6c6220c608604aedc5c", + "qualification": { + "focused_passed": 67, + "additional_source_admission_passed": 3, + "source_run_overlaps_one_existing_focused_case": true + }, + "ownership": "PTX parsing emits kernel facts with typed coverage; inventory forwards it rather than reinterpreting nullable fields. Shared work admission reserves uncompressed input once, compressed input plus output atomically, before payload reads/allocations; AMD note reads use the same owner.", + "sparse_hip_control": { + "aliases": 12, + "payload_size_bytes": 134217728, + "full_payload_reads_observed": 4, + "capacity_without_payload_read": 8, + "retained_kernels": 4, + "before_behavior": "Source-derived: old ordering would read rejected alias payloads before consulting the cumulative allowance; it was not run to exhaustion." + }, + "compiler_control": { + "command": [ + "/home/yann/yann/rea-5345-lab/toolchains/cuda_nvcc-unpacked/cuda_nvcc-linux-x86_64-12.9.86-archive/bin/ptxas", + "-arch=sm_80", + "/home/yann/yann/rea-5345-lab/pointer-parameter.ptx", + "-o", + "/home/yann/yann/rea-5345-lab/pointer-parameter.cubin" + ], + "exit_code": 0, + "stdout": "", + "stderr": "" + }, + "public_cli_oracle": { + "pointer": { + "command": [ + "node", + "scripts/rea.mjs", + "inspect-gpu-code-objects", + "/home/yann/yann/rea-5345-lab/pointer-parameter.ptx", + "--analysis", + "kernels", + "--detail", + "summary", + "--view", + "{\"collection\":\"targets\"}", + "--evidence_output", + "/home/yann/yann/rea-5345-lab/ptx-pointer-validated-evidence.json", + "--json" + ], + "artifact": { + "bytes_read": 302, + "file_size": 135, + "path": "/home/yann/yann/rea-5345-lab/pointer-parameter.ptx", + "sha256": "fcabf9ca3bea80be8a9a2e55f1f2235423705609c63fecc7c04924ab468d1dcb" + }, + "coverage": { + "discovery": "complete", + "discovery_scope": "supported-section-names-and-standalone-formats", + "examined_sections": 0, + "payloads": "partial", + "unknown_section_names": 0 + }, + "kernel": { + "id": "file@0/entry@35", + "limitations": [ + "PTX registers are virtual declarations, not physical allocations. Parameter offsets/sizes and entry-local static shared bytes are derived from supported declarations; module shared bytes, dynamic allocation, device ABI and runtime allocation were not verified.", + "Unsupported PTX parameter declarations or overflowing layouts leave parameter sizes/offsets and the total byte count unknown." + ], + "location": { + "container_id": "file@0", + "offset": 53, + "section_index": null, + "section_name": null, + "size": 81 + }, + "name": "pointer_parameter", + "object_id": "file@0", + "parameter_bytes": null, + "parameter_count": 2, + "parameters": [ + { + "hidden": null, + "name": null, + "offset": null, + "ordinal": 0, + "size": null, + "type": ".u64" + }, + { + "hidden": null, + "name": "n", + "offset": null, + "ordinal": 1, + "size": 4, + "type": ".u32" + } + ], + "private_bytes": null, + "registers": { + "agpr": null, + "physical": null, + "sgpr": null, + "vgpr": null, + "virtual": [] + }, + "representation": "ptx-declarations", + "shared_bytes": 0, + "sources": { + "parameters": { + "container_id": "file@0", + "offset": 53, + "section_index": null, + "section_name": null, + "size": 81 + }, + "private": null, + "registers": null, + "shared": null + }, + "symbol": null + } + }, + "windows": { + "command": [ + "node", + "scripts/rea.mjs", + "inspect-gpu-code-objects", + "/home/yann/yann/rea-5345-lab/fixtures/nvjpeg64_12.dll", + "--analysis", + "kernels", + "--detail", + "summary", + "--view", + "{\"collection\":\"targets\"}", + "--evidence_output", + "/home/yann/yann/rea-5345-lab/final-review-windows-validated-evidence.json", + "--json" + ], + "kernel_count": 3968, + "object_count": 176, + "prior_factual_fields_and_ptx_bytes_exact": true, + "new_partial_ptx_objects": 8, + "new_explicit_parameter_limitations": 59, + "coverage": { + "discovery": "complete", + "discovery_scope": "supported-section-names-and-standalone-formats", + "examined_sections": 8, + "payloads": "partial", + "unknown_section_names": 0 + } + } + }, + "real_producers": { + "count": 9, + "all_payload_coverage": "complete" + }, + "tasks": { + "ptx-coverage-focused": { + "command": [ + "taskset", + "-c", + "0,1", + "npm", + "run", + "test:focused", + "--", + "src/gpu/PtxKernels.test.ts", + "tests/boundary/gpu/codeObjects.test.ts", + "tests/acceptance/gpuCodeObjectsCli.test.ts" + ], + "exit_code": 0, + "wall_s": 106.22117376599999, + "cpu_s": 171.538923, + "cpu_accounting": "payload family wait rusage delta; inherited shell child usage excluded", + "peak_family_rss_bytes": 3492151296, + "sample_ms": 20, + "guard_stop": null + }, + "payload-budget-focused": { + "command": [ + "taskset", + "-c", + "0,1", + "npm", + "run", + "test:focused", + "--", + "src/gpu/PtxKernels.test.ts", + "src/gpu/GpuInventory.test.ts", + "tests/boundary/gpu/codeObjects.test.ts", + "tests/acceptance/gpuCodeObjectsCli.test.ts", + "tests/process-global/gpuFileLifetime.test.ts" + ], + "exit_code": 0, + "wall_s": 142.44056975799685, + "cpu_s": 211.52848500000002, + "cpu_accounting": "payload family wait rusage delta; inherited shell child usage excluded", + "peak_family_rss_bytes": 3478200320, + "sample_ms": 20, + "guard_stop": null + }, + "compressed-admission-tests": { + "command": [ + "taskset", + "-c", + "0,1", + "npm", + "run", + "test:local", + "--", + "src/gpu/GpuInventory.test.ts" + ], + "exit_code": 0, + "wall_s": 3.3343061989980924, + "cpu_s": 3.755217, + "cpu_accounting": "payload family wait rusage delta; inherited shell child usage excluded", + "peak_family_rss_bytes": 477700096, + "sample_ms": 20, + "guard_stop": null + }, + "final-review-producers": { + "command": [ + "taskset", + "-c", + "0,1", + "node", + "--import", + "tsx", + "/home/yann/yann/rea-5345-lab/probe.mts" + ], + "exit_code": 0, + "wall_s": 1.1484615720000875, + "cpu_s": 1.099774, + "cpu_accounting": "payload family wait rusage delta; inherited shell child usage excluded", + "peak_family_rss_bytes": 122732544, + "sample_ms": 20, + "guard_stop": null + }, + "final-review-real-cli": { + "command": [ + "taskset", + "-c", + "0,1", + "python3", + "/home/yann/yann/rea-5345-lab/verify-final-review.py" + ], + "exit_code": 0, + "wall_s": 10.10232881200136, + "cpu_s": 11.301614, + "cpu_accounting": "payload family wait rusage delta; inherited shell child usage excluded", + "peak_family_rss_bytes": 512335872, + "sample_ms": 20, + "guard_stop": null + }, + "final-review-check": { + "command": ["taskset", "-c", "0,1", "npm", "run", "check"], + "exit_code": 0, + "wall_s": 135.34464124299848, + "cpu_s": 266.56255, + "cpu_accounting": "payload family wait rusage delta; inherited shell child usage excluded", + "peak_family_rss_bytes": 5371879424, + "sample_ms": 20, + "guard_stop": null + }, + "final-review-docs": { + "command": ["taskset", "-c", "0,1", "npm", "run", "docs:check"], + "exit_code": 0, + "wall_s": 13.528166624997539, + "cpu_s": 17.260022, + "cpu_accounting": "payload family wait rusage delta; inherited shell child usage excluded", + "peak_family_rss_bytes": 804327424, + "sample_ms": 20, + "guard_stop": null + } + }, + "failed_validation_attempts": [ + { + "reason": "Verifier treated exported Evidence bundle as a bare record; fixed to read records[0].", + "task": { + "command": [ + "taskset", + "-c", + "0,1", + "python3", + "/home/yann/yann/rea-5345-lab/verify-final-review.py" + ], + "exit_code": 1, + "wall_s": 4.254547918000753, + "cpu_s": 4.326556, + "cpu_accounting": "payload family wait rusage delta; inherited shell child usage excluded", + "peak_family_rss_bytes": 382095360, + "sample_ms": 20, + "guard_stop": null + } + }, + { + "reason": "Retry attempted to reuse an existing Evidence export path; CLI refused overwrite. Existing export preserved; new paths used.", + "task": { + "command": [ + "taskset", + "-c", + "0,1", + "python3", + "/home/yann/yann/rea-5345-lab/verify-final-review.py" + ], + "exit_code": 1, + "wall_s": 5.362011795001308, + "cpu_s": 5.3208850000000005, + "cpu_accounting": "payload family wait rusage delta; inherited shell child usage excluded", + "peak_family_rss_bytes": 379301888, + "sample_ms": 20, + "guard_stop": null + } + }, + { + "reason": "Prior snapshot comparison wrongly required complete PTX coverage to remain unchanged. All factual fields were equal; 8 PTX objects/59 kernels now correctly expose existing unknown parameter layouts. Updated oracle checks exact facts plus these intended uncertainty changes.", + "task": { + "command": [ + "taskset", + "-c", + "0,1", + "python3", + "/home/yann/yann/rea-5345-lab/verify-final-review.py" + ], + "exit_code": 1, + "wall_s": 18.017960425997444, + "cpu_s": 16.76444, + "cpu_accounting": "payload family wait rusage delta; inherited shell child usage excluded", + "peak_family_rss_bytes": 507072512, + "sample_ms": 20, + "guard_stop": null + } + } + ], + "performance_scope": "The complete/selected MCP timing and token comparisons above remain pinned to their measured revision. This later change strengthens work admission and uncertainty propagation; no new general latency/CPU/memory improvement is inferred." + } +} diff --git a/docs/verification/gpu-code-objects-2026-10-21.md b/docs/verification/gpu-code-objects-2026-10-21.md new file mode 100644 index 0000000000..d79f85ccc4 --- /dev/null +++ b/docs/verification/gpu-code-objects-2026-10-21.md @@ -0,0 +1,286 @@ +# Static GPU code-object verification + +The [receipts](gpu-code-objects-2026-10-21.json) pin the measured runtime tree, +publisher URLs/digests, actual commands, completed answers and resource costs. +Downloaded binaries, toolchains and complete Evidence bundles remain outside Git. + +## Correctness + +NVIDIA CUDA 12.9.1 redistribution archives were verified against their publisher +manifest. nvcc 12.9.86 compiled a small owned kernel for sm_80 and sm_90a with +uncompressed, size-compressed and speed-compressed fatbins. cuobjdump 12.9.82 +independently confirmed two parameters at offsets 0/8 with sizes 8/4, 12 parameter +bytes, 10 physical registers and architecture-dependent shared memory. Separate +PTX/CUBIN and a host ELF object exercised the same parser boundaries. + +Clang 14 produced a gfx900 HIP bundle and AMDGPU ELF object; llvm-readelf supplied +the MessagePack metadata oracle. An LLVM 19 offload bundler produced a real CCOB +v2 Zstd bundle. All nine producer artifacts were reread on the final runtime +source with complete payload coverage. CCOB v1/v3 and zlib have portable boundary +fixtures, rather than real producer verification. + +Released Linux and Windows nvJPEG 12.4.0.76 artifacts each contain 165 CUBIN and +10 PTX payloads, with 3,968 kernel declarations across 15 target declarations. +Independent extraction matched all 165 decoded CUBIN hashes and all 10 PTX byte +sequences; cuobjdump resource output matched all 3,720 native kernel +name/register/shared-memory tuples. Pre-review MCP workflows rechecked the Linux +sm_100 subset's 248 complete kernel facts against those prior oracle facts. +The pre-review public CLI analyzed the entire Windows DLL, exported complete +Evidence, and reproduced all previously verified facts exactly. + +Windows validation ran on Linux: LLVM independently identified the PE section +coordinates, and cuobjdump consumed its extracted fatbin. cuobjdump did not +recognize the DLL directly. This does not establish Windows runtime behavior, +GPU execution, host launch correlation or legacy AMDGPU YAML support. + +## Initial completed MCP workflows + +Both fresh stdio MCP sessions analyzed only `sm_100` in the same Linux library, +retained 176 inventory objects and 248 kernel facts, queried the same named +kernel, exported Evidence, and repeated the historical query three times. +The complete variant returned the selected-target report; the selected variant +returned a same-call kernel page. Both preserved the exact answer: one parameter +of 168 bytes, 31 physical registers and 9,216 shared bytes. + +| Cost | Complete report | Selected page | +| --------------------------------------------------------------- | ----------------: | -----------------: | +| Used input declarations + three primary calls, estimated tokens | 406,862 | 26,106 | +| All input declarations + primary calls, estimated tokens | 635,381 | 254,625 | +| Full advertised catalog + all six calls, estimated tokens | 1,475,258 | 1,094,502 | +| Process-family elapsed seconds | 15.492 | 15.443 | +| Process-family CPU seconds | 18.220 | 18.074 | +| Sampled peak family RSS bytes | 1,139,601,408 | 1,321,566,208 | +| First inspection milliseconds | 8,572 | 8,810 | +| Three warm retained-page calls, milliseconds | 10.7 / 10.6 / 9.8 | 12.0 / 11.4 / 10.6 | + +Tokenx estimates include advertised schemas, arguments and full SDK replies; +they are not actual model usage. Text and structured reply representations are +both counted. Initialization and JSON-RPC envelopes are unmeasured. Complete +catalog discovery substantially reduces the overall percentage saving. + +These are workflow variants on the same implementation, not a before/after +parser speed benchmark. Node compile caching was disabled, but OS caches and +unrelated host load were uncontrolled. No general latency, CPU or memory +improvement is established. The selected workflow used more peak memory in this +pair. An earlier all-target complete reply exceeded the existing transport guard; +that incomplete attempt is excluded from the completed comparison. + +The full Windows DLL CLI task took 4.064 seconds / 4.909 CPU seconds / +503,459,840 peak family RSS bytes. All profiled completed tasks stayed within +the 8 GiB and 420 second guards, using two CPU affinities. Peak RSS sums payload +descendants every 20 ms and is approximate; the observer is excluded. + +## Regression checks + +- The initial focused run passed 265 tests in 19 files: GPU domain/codec/parser + cases, public CLI/MCP, retained views, output profiles, cancellation and cleanup, + plus shared ELF/PE Go, Rust and IL2CPP consumers. +- Full static checks passed before the catalog/docs-only follow-up; its receipt + records 6,866,296,832 peak family RSS bytes. The final CI run qualifies the PR. +- Documentation/catalog/license checks passed after fixing missing catalog + ownership, translated README links and the new MessagePack dependency notices. +- An earlier combined schema run hit two 30 second timeouts on two CPUs. The + isolated canonical schema suite passed all 15 cases without increasing limits. + This is recorded as a successful isolation, not a proven baseline failure. + +Retention caches are scoped to immutable Evidence through a WeakMap; historical +pages do not reopen files. Live inspections always stream a fresh full-file hash +and revalidate the descriptor/path. No live-state freshness cache is claimed. + +## Review follow-up + +At `36b87ed`, the public CLI reproduced both review findings: selecting +`amdgcn-amd-amdhsa--gfx900` in the real Clang-produced standalone object returned +zero kernels with an unknown target, and malformed `--targets` JSON was reported +at the `view` issue path. + +At `09c6062`, AMD ELF note parsing owns target discovery and prepared kernel +declarations. Inventory reads those bounded notes before selection; selected +analysis reuses them without decoding MessagePack twice. Aggregate encoded note +bytes are limited to 8 MiB before reads, with existing per-value/depth limits; +the parsed JavaScript representation expands beyond that source-byte count. +The prepared declarations live only for the current payload inspection. Named +CLI option parsing also supplies the actual parsed input to schema diagnostics. + +The real public CLI now inventories the gfx900 target, returns the producer's +one kernel/two parameters/12-byte parameter area/6 SGPR/2 VGPR when selected, +and preserves an unselected object without kernel facts for a different target. +Those three complete CLI tasks together took 5.974 seconds / 6.951 CPU seconds / +383,623,168 sampled peak family RSS bytes, including the Python task driver. +All nine producer artifacts were reread with complete payload coverage. + +The review regression run passed 35 cases in four files, including public CLI, +MCP, metadata capacity and cleanup. A further 10 product-catalog cases passed +after adding the missing CLI `analysis` option description identified by CI. +The earlier paired nvJPEG measurements remain pinned to their original runtime; +these follow-ups do not establish a new CUDA performance result. + +## Bounded metadata and partial-fact review + +The latest qualification at `ad1e17a` rebases onto main `5241597`, preserving +both Rust and GPU retained views. All 70 focused cases in nine files passed, +including both public CLI/MCP workflows; the separate SDK-converted canonical +schema suite passed all 15 cases. + +CUDA parsing shares an aggregate 262,144 record/symbol visit budget, 128 MiB +section-read budget and 16 MiB serialized-size accounting budget for index facts. +Equal scoped byte ranges and duplicate register values are coalesced before +index growth; conflicting values survive. Parsed parameter metadata is reused. +The public CLI completed an 861,896-byte CUBIN containing 1,024 headers aliasing +65,536 records and retained the correct physical count. The old 67-million-entry +expansion is source-derived; it was not run to exhaustion. + +Decoder coverage now reaches object, report and target pages: absent CUBIN +symbol tables and unknown section names remain partial. AMD note parsing uses +a tagged Result that retains earlier parsed notes on later malformed, capacity, +unsupported or interrupted reads; retained kernel facts are emitted before the +original failure is propagated. Standalone PTX recognition continues beyond the +64 KiB fast path up to its actual 16 MiB text budget and reuses complete probe +bytes. Unsupported/overflowing register counts stay null instead of low totals. + +The public CLI verified an AMD valid-note/truncated-note input and a PTX file +with a 70 KiB leading comment, in addition to the alias case. Those three tasks +together took 5.132 seconds / 6.222 CPU seconds / 405,835,776 peak family RSS +bytes, including the Python driver. Regression cases additionally cover invalid +MessagePack and unsupported extensions in standalone and bundled AMD objects. + +The revised Windows DLL workflow preserved all 165 decoded CUBIN hashes, all +10 PTX byte identities and all 3,720 native kernel facts. Independently counting +register declarations in cuobjdump-extracted PTX verified all 248 PTX kernels; +20 kernel totals now correctly include previously missed declarations. This +is a virtual-declaration oracle, not physical allocation or GPU execution. +The Linux sm_100 workflows still match all 248 independently checked native +kernel facts. All nine real producer artifacts retain complete payload coverage. + +The latest complete/selected MCP pair performed the same six calls and answered +the same question as the initial pair. Receipts retain both measurement stages. + +| Cost | Complete report | Selected page | +| --------------------------------------------------------- | --------------: | ------------: | +| Used input declarations + primary calls, estimated tokens | 407,368 | 26,612 | +| All input declarations + primary calls, estimated tokens | 636,495 | 255,739 | +| Full catalog + all six calls, estimated tokens | 1,480,698 | 1,099,942 | +| Process-family elapsed seconds | 15.487 | 15.923 | +| Process-family CPU seconds | 18.404 | 18.760 | +| Peak family RSS bytes | 1,251,254,272 | 1,201,774,592 | + +These remain tokenizer estimates and same-code workflow variants. The single +latest pair establishes no general latency/CPU/memory improvement; OS caches +and host contention remain uncontrolled. The Windows CLI rerun completed in +4.078 seconds / 4.880 CPU seconds / 498,380,800 peak family RSS bytes. + +Main CI independently reproduced the browser termination and WASM optional-input +assertion failures without GPU changes. Cancelled fail-fast shards are not +counted as passing. Those baseline failures are outside this feature's fixes. + +## Target declaration and CUDA ABI qualification + +The latest code `f5be46f` is based on main `f801b2f`. Inventory and payload +target declarations remain separate, with typed consistency and inspection +coverage preserved in object pages even with selected fields. A mismatched +outer/inner base target remains partial and annotates affected kernels; CUDA +base-only agreement does not establish agreement on a feature profile. Target +filters select inventory declarations, and unexamined inner targets stay unknown. + +Standalone and unknown-ID bundled AMDGPU files use the same bounded target +discovery owner before filtering. Portable controls wrap the same real Clang +ELF with two-separator, one-separator and opaque HIP IDs. All three inventory +then exact-target CLI workflows retain the verified kernel. They are alternate +wrapper controls, not verification of an actual legacy compiler producer. + +The shared fixed ELF header owner now retains OS ABI. NVIDIA's released CUBINs +include distinct `0x33` and `0x41` flag layouts. The earlier low-byte-only +interpretation falsely produced `sm_2` for 55 modern payloads; that attempted +target-check implementation and its timings are superseded diagnostic evidence, +not a correctness-qualified benchmark. The ABI-qualified decoder retains raw +flags and exact fixed-header coordinates, and leaves unrecognized patterns unknown. +Header ownership is checked when importing portable Evidence. + +All 165 released Windows CUBIN targets and header values were checked against +cuobjdump-extracted files: 110 use OS ABI `0x33`, 55 use `0x41`; no false conflicts +remain. All 3,968 corrected kernel facts and 10 PTX byte identities match the +prior independent oracle. The nine real producer artifacts retain complete +payload coverage. Unsupported layout controls retain their headers and make +unknown target discovery partial in the default MCP summary. + +After the shared-header change, 251 cases in nine files passed, including +Go/Rust/IL2CPP consumers, GPU CLI/MCP and provenance forgery. The last uncertainty +propagation check passed 46 public/boundary cases. The separate SDK-converted +canonical schema suite passed all 15 cases on the current base. + +The latest qualified complete/selected pair retains the same sm_100 question, +answer and six-call workflow. It runs at `21eaa0d`, before the final unresolved- +discovery-only guard. + +| Cost | Complete report | Selected page | +| --------------------------------------------------------- | --------------: | ------------: | +| Used input declarations + primary calls, estimated tokens | 418,967 | 26,653 | +| All input declarations + primary calls, estimated tokens | 648,242 | 255,928 | +| Full catalog + all six calls, estimated tokens | 1,493,041 | 1,100,727 | +| Process-family elapsed seconds | 21.307 | 24.604 | +| Process-family CPU seconds | 23.538 | 26.454 | +| Peak family RSS bytes | 1,146,519,552 | 1,341,333,504 | + +Selected replies save context but do not establish faster analysis. The latest +pair used more elapsed/CPU time and peak memory for selected delivery; variability +is visible across the preserved trials. Fresh process lifetime is controlled; +OS caches, frequency and unrelated load are not. A same-code diagnostic timing +repeat varied substantially, and no cause or persistent speedup is inferred. +The tokenizer/client scope and omitted initialization costs remain as above. + +The final Windows CLI task took 5.446 seconds / 5.916 CPU seconds / 498,192,384 +peak family RSS bytes. The six HIP wrapper CLI tasks took 15.696 seconds / +16.427 CPU seconds / 382,410,752 peak family RSS bytes. All completed tasks stayed +within the 8 GiB family guard. + +## Payload admission and PTX uncertainty follow-up + +At `893e04d`, PTX parsing owns typed per-kernel coverage and limitations for +unknown parameter layouts, shared declarations and virtual register counts. +Inventory forwards that coverage to objects, reports and historical pages. +The old register-only interpretation in inventory was removed. + +A 67-case focused run passed across five files, including parser, public CLI/MCP, +provider boundary and lifecycle tests. Three source admission cases also passed +(one overlaps the focused run). NVIDIA `ptxas 12.9.86` accepted a pointer-parameter +PTX control; the public CLI retained its unknown layout and reported partial +coverage. A released nvJPEG example independently extracted by cuobjdump uses +`.param .u64 .ptr .align 1`; this remains outside supported layout interpretation. + +The revised Windows DLL report retains all 176 object factual fields, all 3,968 +kernel factual fields and all 10 exact PTX byte identities. Eight PTX objects and +59 kernels now expose their pre-existing unknown parameter layouts with partial +coverage and added limitations. The earlier complete payload-coverage claim for +those PTX objects is superseded; decoded numerical facts are unchanged. Nine real +producer artifacts still retain complete payload coverage. + +The shared work owner now reserves uncompressed payload input once, and compressed +input plus decoded output atomically, before reading/allocating. CCOB uses the +same admission owner as payload analysis and AMD note reads. The allowance is +512 MiB of that work, with 128 MiB per input/output buffer. Admitted failed work +remains charged. A real sparse HIP file contains twelve aliases of one 128 MiB +ELF payload: file-read instrumentation observed four full payload reads and eight +capacity refusals with no payload read or decoded digest; the earlier four kernel +facts survive. CUDA and CCOB oversized-output controls likewise prove refusal +before the compressed input read. Old rejected-alias rereads are source-derived; +they were not run to exhaustion. + +The nine-producer pass took 1.148 seconds / 1.100 CPU seconds / 122,732,544 peak +family RSS bytes. The pointer-PTX and released-DLL public CLI tasks together took +10.102 seconds / 11.302 CPU seconds / 512,335,872 peak family RSS bytes. The focused +run took 142.441 seconds / 211.528 CPU seconds / 3,478,200,320 peak family RSS bytes, +including the required artifact build. No resource guard stopped these tasks. +The earlier MCP cost pairs remain pinned to their measured revisions; no new +speedup is inferred from this follow-up. + +Three verifier attempts are retained as unsuccessful in the JSON record: an +incorrect bare-record assumption for the Evidence bundle, refusal to overwrite +an existing export, and a stale expectation that the now-corrected PTX coverage +would stay complete. The successful comparison requires all factual values and +PTX bytes to match while checking the intended uncertainty changes explicitly. + +`npm run check` passed at this source revision, including type/lint/format, module +boundaries and architecture guards: 135.345 seconds / 266.563 CPU seconds / +5,371,879,424 sampled peak family RSS bytes; no resource guard stop. + +`npm run docs:check` also passed, including all five README conformance cases. diff --git a/licenses/npm-dependencies.txt b/licenses/npm-dependencies.txt index c2c2280df1..8225eedadf 100644 --- a/licenses/npm-dependencies.txt +++ b/licenses/npm-dependencies.txt @@ -2,7 +2,7 @@ REA npm dependency inventory Prepared from the root package-lock.json; refresh this inventory when locked dependencies change. License identifiers are upstream lockfile declarations, not a legal assessment. Runtime includes transitive and optional platform packages; development includes build, test and website tooling. -Locked package entries: 751; runtime: 179. +Locked package entries: 752; runtime: 180. Full runtime copyright/license/NOTICE texts: npm-runtime-notices.txt. @algolia/abtesting@1.25.0 | development | MIT @@ -690,6 +690,11 @@ commander@13.1.0 | runtime | MIT Package: https://www.npmjs.com/package/@modelcontextprotocol/server/v/2.3.1 Archive: https://registry.npmjs.org/@modelcontextprotocol/server/-/server-2.3.1.tgz Integrity: sha512-e59MfWuSssj6DoQ96JCTqdW1AXT5/XtnTl0WWd6MU1Ka434fFUGGosCLJbGNySDhHGvxQWoRWK137FXYFkYNWw== +@msgpack/msgpack@3.1.3 | runtime | ISC + Lockfile path: node_modules/@msgpack/msgpack + Package: https://www.npmjs.com/package/@msgpack/msgpack/v/3.1.3 + Archive: https://registry.npmjs.org/@msgpack/msgpack/-/msgpack-3.1.3.tgz + Integrity: sha512-47XIizs9XZXvuJgoaJUIE2lFoID8ugvc0jzSHP+Ptfk8nTbnR8g788wv48N03Kx0UkAv559HWRQ3yzOgzlRNUA== @napi-rs/lzma-linux-x64-gnu@1.5.1 | development, optional | MIT Lockfile path: node_modules/@napi-rs/lzma-linux-x64-gnu Package: https://www.npmjs.com/package/@napi-rs/lzma-linux-x64-gnu/v/1.5.1 diff --git a/licenses/npm-runtime-notices.txt b/licenses/npm-runtime-notices.txt index 0fc82a1312..177220131c 100644 --- a/licenses/npm-runtime-notices.txt +++ b/licenses/npm-runtime-notices.txt @@ -1,7 +1,7 @@ REA third-party npm runtime copyright, license and NOTICE texts Collected from integrity-verified locked npm archives. Release-specific supplements and upstream notice gaps are identified in their entries. External engines are separately obtained; see THIRD_PARTY_NOTICES.md. -Runtime dependency fingerprint (SHA-256): 4125777159e02cb749e17c959e77f59528cfffe3f7a5220b8a21ce3c49ad3061 +Runtime dependency fingerprint (SHA-256): d0a0286bb8c158a670b019cdb56ee120542cb072bbecf3741c172b5f06b2301b ===== @babel/helper-string-parser@8.0.6 ===== Declared license: MIT @@ -1822,6 +1822,20 @@ CC-BY-4.0. See https://creativecommons.org/licenses/by/4.0/legalcode for the full license text. +===== @msgpack/msgpack@3.1.3 ===== +Declared license: ISC +Archive: https://registry.npmjs.org/@msgpack/msgpack/-/msgpack-3.1.3.tgz +Integrity: sha512-47XIizs9XZXvuJgoaJUIE2lFoID8ugvc0jzSHP+Ptfk8nTbnR8g788wv48N03Kx0UkAv559HWRQ3yzOgzlRNUA== +Scope: npm runtime dependency, including optional platform packages. + +--- package/LICENSE --- + +Copyright 2019 The MessagePack Community. + +Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + ===== @pinojs/redact@0.4.0 ===== Declared license: MIT Archive: https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz diff --git a/package-lock.json b/package-lock.json index 67a74129f6..a8d20bd23d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "@lydell/node-pty": "1.1.0", "@modelcontextprotocol/client": "2.3.1", "@modelcontextprotocol/server": "2.3.1", + "@msgpack/msgpack": "3.1.3", "@xmldom/xmldom": "0.9.12", "@xterm/addon-serialize": "0.14.0", "@xterm/headless": "6.0.0", @@ -2239,6 +2240,15 @@ "node": ">=20" } }, + "node_modules/@msgpack/msgpack": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@msgpack/msgpack/-/msgpack-3.1.3.tgz", + "integrity": "sha512-47XIizs9XZXvuJgoaJUIE2lFoID8ugvc0jzSHP+Ptfk8nTbnR8g788wv48N03Kx0UkAv559HWRQ3yzOgzlRNUA==", + "license": "ISC", + "engines": { + "node": ">= 18" + } + }, "node_modules/@napi-rs/lzma-linux-x64-gnu": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", diff --git a/package.json b/package.json index 1a752edc22..8aab2719b9 100644 --- a/package.json +++ b/package.json @@ -336,6 +336,7 @@ "@lydell/node-pty": "1.1.0", "@modelcontextprotocol/client": "2.3.1", "@modelcontextprotocol/server": "2.3.1", + "@msgpack/msgpack": "3.1.3", "@xmldom/xmldom": "0.9.12", "@xterm/addon-serialize": "0.14.0", "@xterm/headless": "6.0.0", diff --git a/scripts/lib/catalog-builders.mjs b/scripts/lib/catalog-builders.mjs index d33c72d10a..cedce74583 100644 --- a/scripts/lib/catalog-builders.mjs +++ b/scripts/lib/catalog-builders.mjs @@ -34,6 +34,7 @@ export const toolFamilyCatalog = (sources) => { ...sources.desktopContracts.DESKTOP_APPLICATION_TOOL_CONTRACTS, ...sources.evmContracts.EVM_TOOL_CONTRACTS, ...sources.goContracts.GO_TOOL_CONTRACTS, + ...sources.gpuContracts.GPU_TOOL_CONTRACTS, ...sources.il2cppContracts.IL2CPP_TOOL_CONTRACTS, ...sources.modelContracts.MODEL_TOOL_CONTRACTS, ...sources.rustContracts.RUST_TOOL_CONTRACTS, @@ -268,6 +269,10 @@ export const providerCatalog = (sources) => { identity: sources.goProvider.GO_BINARY_PROVIDER_IDENTITY, contracts: sources.goContracts.GO_TOOL_CONTRACTS, }, + { + identity: sources.gpuProvider.GPU_CODE_OBJECTS_PROVIDER_IDENTITY, + contracts: sources.gpuContracts.GPU_TOOL_CONTRACTS, + }, { identity: sources.il2cppProvider.IL2CPP_METADATA_PROVIDER_IDENTITY, contracts: sources.il2cppContracts.IL2CPP_TOOL_CONTRACTS, diff --git a/scripts/lib/catalog-core.mjs b/scripts/lib/catalog-core.mjs index ceac794fec..569ffd2b80 100644 --- a/scripts/lib/catalog-core.mjs +++ b/scripts/lib/catalog-core.mjs @@ -69,6 +69,8 @@ const SOURCE_PATHS = { evmProvider: "dist/evm/EvmoleRelease.js", goContracts: "dist/contracts/go/goToolContracts.js", goProvider: "dist/go/GoBinaryProvider.js", + gpuContracts: "dist/contracts/gpu/gpuToolContracts.js", + gpuProvider: "dist/gpu/GpuCodeObjectsProvider.js", modelContracts: "dist/contracts/model/modelToolContracts.js", modelProvider: "dist/model/ModelArtifactProvider.js", il2cppContracts: "dist/contracts/il2cpp/il2cppToolContracts.js", diff --git a/scripts/lib/module-boundaries.mjs b/scripts/lib/module-boundaries.mjs index 41b12f48d3..7577e953f5 100644 --- a/scripts/lib/module-boundaries.mjs +++ b/scripts/lib/module-boundaries.mjs @@ -10,6 +10,7 @@ const MIGRATED_PROVIDER_ROOTS = new Set([ "go", "il2cpp", "model", + "gpu", "rust", "wasm", "captures", diff --git a/scripts/lib/test-projects.mjs b/scripts/lib/test-projects.mjs index 2be464ec47..9d73487cee 100644 --- a/scripts/lib/test-projects.mjs +++ b/scripts/lib/test-projects.mjs @@ -17,7 +17,7 @@ export const SOURCE_TEST_PROJECTS = [ group: "source", include: [ "src/*.test.ts", - "src/{apktool,jeb,adb,artifacts,browser,dotnet,electron,evm,firmware,flutter,pcap,ghidra,go,hopper,ida,inspector,javascript,native,process,reference,rust,server,sqlite,wasm,windows}/**/*.test.ts", + "src/{apktool,jeb,adb,artifacts,browser,dotnet,electron,evm,firmware,flutter,pcap,ghidra,go,gpu,hopper,ida,inspector,javascript,native,process,reference,rust,server,sqlite,wasm,windows}/**/*.test.ts", ], }, { diff --git a/src/application/CapabilityInventory.ts b/src/application/CapabilityInventory.ts index 87e2dc711a..d0eabbb0d9 100644 --- a/src/application/CapabilityInventory.ts +++ b/src/application/CapabilityInventory.ts @@ -415,6 +415,7 @@ const workflowAvailabilityFor = ({ name === "inspect_go_binary" || name === "inspect_il2cpp_metadata" || name === "inspect_model_artifact" || + name === "inspect_gpu_code_objects" || name === "inspect_rust_binary" || name === "inspect_electron_fuses" ) diff --git a/src/application/gpu/GpuCodeObjectsEvidence.ts b/src/application/gpu/GpuCodeObjectsEvidence.ts new file mode 100644 index 0000000000..3f95fcb13a --- /dev/null +++ b/src/application/gpu/GpuCodeObjectsEvidence.ts @@ -0,0 +1,57 @@ +import type { ProviderIdentity } from "../AnalysisProvider.js"; +import type { + GpuCodeObjects, + GpuCodeObjectsInput, + GpuCodeObjectsPartial, +} from "../../domain/gpu/gpuCodeObjects.js"; +import { createEvidence, type Evidence } from "../../domain/evidence.js"; + +const gpuParameters = (parameters: GpuCodeObjectsInput) => ({ + path: parameters.path, + analysis: parameters.analysis, + ...(parameters.object_ids === undefined + ? {} + : { object_ids: parameters.object_ids }), + ...(parameters.targets === undefined ? {} : { targets: parameters.targets }), + ...(parameters.expected_sha256 === undefined + ? {} + : { expected_sha256: parameters.expected_sha256 }), +}); +/** One Evidence interpretation for success and observations retained by file finalization failures. */ +export const createGpuCodeObjectsEvidence = ( + report: GpuCodeObjects, + provider: ProviderIdentity, + parameters: GpuCodeObjectsInput, +): Evidence => + createEvidence( + { + path: report.artifact.path, + sha256: report.artifact.sha256, + format: "file", + }, + provider, + { + operation: "inspect_gpu_code_objects", + parameters: gpuParameters(parameters), + result: report, + confidence: "observed", + locations: [{ kind: "artifact-path", path: report.artifact.path }], + limitations: report.limitations, + }, + ); + +export const createGpuCodeObjectsPartialEvidence = ( + report: GpuCodeObjectsPartial, + provider: ProviderIdentity, + parameters: GpuCodeObjectsInput, +): Evidence => + createEvidence(undefined, provider, { + operation: "inspect_gpu_code_objects", + parameters: gpuParameters(parameters), + result: report, + confidence: "observed", + subjectUnavailableReason: + "The interrupted inspection did not complete final file identity verification; root digest and completeness are unknown.", + locations: [{ kind: "artifact-path", path: report.artifact.path }], + limitations: report.limitations, + }); diff --git a/src/application/gpu/GpuCodeObjectsPort.ts b/src/application/gpu/GpuCodeObjectsPort.ts new file mode 100644 index 0000000000..6efcbd35d8 --- /dev/null +++ b/src/application/gpu/GpuCodeObjectsPort.ts @@ -0,0 +1,20 @@ +import type { + ExecutionOptions, + ProviderIdentity, +} from "../AnalysisProvider.js"; +import type { AnalysisError } from "../../domain/analysisErrorBase.js"; +import type { + GpuCodeObjects, + GpuCodeObjectsInput, +} from "../../domain/gpu/gpuCodeObjects.js"; +import type { Result } from "../../domain/result.js"; + +/** Read-only, local GPU declarations; no GPU runtime, target execution or downloads. */ +export interface GpuCodeObjectsPort { + readonly identity: ProviderIdentity; + inspect( + input: GpuCodeObjectsInput, + options?: ExecutionOptions, + ): Promise>; + close(): Promise; +} diff --git a/src/application/gpu/GpuCodeObjectsService.ts b/src/application/gpu/GpuCodeObjectsService.ts new file mode 100644 index 0000000000..cd9d3178a3 --- /dev/null +++ b/src/application/gpu/GpuCodeObjectsService.ts @@ -0,0 +1,97 @@ +import { inspectGpuCodeObjectsInputSchema } from "../../domain/gpu/gpuCodeObjects.js"; +import type { GpuCodeObjectsPort } from "./GpuCodeObjectsPort.js"; +import type { ExecutionOptions } from "../AnalysisProvider.js"; +import { gpuCodeObjectsSchema } from "../../domain/gpu/gpuCodeObjects.js"; +import { analysisInputErrorFromIssues } from "../../domain/inputIssueProjection.js"; +import type { AnalysisError } from "../../domain/analysisErrorBase.js"; +import { + AnalysisCancelledError, + AnalysisOutputError, +} from "../../domain/analysisErrorCore.js"; +import type { Evidence } from "../../domain/evidence.js"; +import { createGpuCodeObjectsEvidence } from "./GpuCodeObjectsEvidence.js"; +import { err, ok, type Result } from "../../domain/result.js"; +import { + requireAnalysisViewRetention, + projectRetainedAnalysis, + type AnalysisViewRetention, +} from "../analysisView/AnalysisViewService.js"; + +/** Shared input, Evidence and retained delivery boundary for CLI, MCP and SDK callers. */ +export class GpuCodeObjectsService { + constructor(readonly provider: GpuCodeObjectsPort) {} + close(): Promise { + return this.provider.close(); + } + async inspect( + raw: unknown, + options?: ExecutionOptions, + retention: AnalysisViewRetention = { + evidenceById: undefined, + recordEvidence: undefined, + }, + ): Promise> { + const input = inspectGpuCodeObjectsInputSchema.safeParse(raw); + if (!input.success) + return err( + analysisInputErrorFromIssues( + "inspect_gpu_code_objects", + input.error.issues, + raw, + { cause: input.error }, + ), + ); + if (options?.signal?.aborted) + return err( + new AnalysisCancelledError("inspect_gpu_code_objects", { + cause: options.signal.reason, + }), + ); + const { detail, view, ...parameters } = input.data; + if (detail === "summary") { + const ready = requireAnalysisViewRetention( + "inspect_gpu_code_objects", + retention, + ); + if (!ready.ok) return ready; + } + const inspected = await this.provider.inspect(parameters, options); + if (!inspected.ok) return inspected; + const checked = gpuCodeObjectsSchema.safeParse(inspected.value); + if ( + !checked.success || + checked.data.artifact.path !== parameters.path || + checked.data.analysis !== parameters.analysis || + (parameters.expected_sha256 !== undefined && + checked.data.artifact.sha256 !== parameters.expected_sha256) + ) + return err( + new AnalysisOutputError( + "inspect_gpu_code_objects", + "GPU reader changed the selected input or returned invalid facts", + ), + ); + const report = checked.data; + const evidence = createGpuCodeObjectsEvidence( + report, + this.provider.identity, + parameters, + ); + if (options?.signal?.aborted) + return err( + new AnalysisCancelledError("inspect_gpu_code_objects", { + cause: options.signal.reason, + partialObservation: evidence, + }), + ); + return detail === "summary" + ? projectRetainedAnalysis( + evidence, + retention, + view === undefined + ? { kind: "summary" } + : { kind: "gpu-code-objects", ...view }, + ) + : ok(evidence); + } +} diff --git a/src/cli.ts b/src/cli.ts index 586942bcd5..07b7c1b7f4 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,3 +1,4 @@ +import { registerGpuCommands } from "./cli/gpuCommands.js"; import { registerNativeFunctionMatchingCommands } from "./cli/nativeFunctionMatchingCommands.js"; import { registerCaptureCommands } from "./cli/captureCommands.js"; import { registerSignalCommands } from "./cli/signalCommands.js"; @@ -130,6 +131,7 @@ export const createCli = ( registerGoCommands(cli, logger); registerIl2cppCommands(cli, logger); registerModelCommands(cli, logger); + registerGpuCommands(cli, logger); registerRustCommands(cli, logger); registerSqliteCommands(cli, logger, environment); registerArtifactCommands(cli, logger, analysis.runProviderAnalysis); diff --git a/src/cli/gpuCommands.ts b/src/cli/gpuCommands.ts new file mode 100644 index 0000000000..93f18a84c7 --- /dev/null +++ b/src/cli/gpuCommands.ts @@ -0,0 +1,132 @@ +import { inspectGpuCodeObjectsInputSchema } from "../domain/gpu/gpuCodeObjects.js"; +import { analysisInputErrorFromIssues } from "../domain/inputIssueProjection.js"; +import { AnalysisInputError } from "../domain/analysisErrorCore.js"; +import { projectAnalysisError } from "../domain/analysisErrorProjection.js"; +import { z } from "incur"; +import type { Logger } from "pino"; +import type { CliInstance } from "./types.js"; +import { absoluteCallerPath } from "../filesystem/callerPath.js"; +import { createGpuCodeObjectsService } from "../composition/gpu.js"; +import { CLI_COMMANDS } from "../cliCommandNames.js"; +import { logCliCommand } from "../cliLogging.js"; +import { withCommandCancellation } from "./commandCancellation.js"; +import { + analysisEvidenceDeliveryOptions, + deliverCliAnalysisEvidence, +} from "./analysisEvidenceDelivery.js"; + +/** One-shot summaries export the same complete observations used by MCP retained pages. */ +export const registerGpuCommands = (cli: CliInstance, logger: Logger): void => { + cli.command(CLI_COMMANDS.inspectGpuCodeObjects, { + description: + "Inspect embedded CUDA/HIP targets and selected static kernel metadata", + args: z.object({ + path: z + .string() + .describe("Local ELF/PE, CUDA fatbin, CUBIN, PTX or HIP bundle"), + }), + options: z.object({ + ...analysisEvidenceDeliveryOptions, + view: z + .string() + .optional() + .describe( + "JSON GPU object/kernel/PTX page selection; requires --detail summary and --evidence_output", + ), + analysis: z + .enum(["inventory", "kernels"]) + .default("inventory") + .describe("Inventory GPU targets or decode selected kernel metadata"), + expected_sha256: z + .string() + .optional() + .describe("Inventory SHA-256; required with --object_ids"), + object_ids: z + .string() + .optional() + .describe("JSON array of exact object IDs to decode"), + targets: z + .string() + .optional() + .describe("JSON array of exact target declarations to decode"), + }), + run: ({ args, options }) => + withCommandCancellation((signal) => + logCliCommand(logger, CLI_COMMANDS.inspectGpuCodeObjects, async () => { + const selectors: Partial< + Record<"object_ids" | "targets" | "view", unknown> + > = {}; + for (const name of ["object_ids", "targets", "view"] as const) { + const value = options[name]; + if (value === undefined) continue; + try { + selectors[name] = JSON.parse(value); + } catch (cause) { + return projectAnalysisError( + new AnalysisInputError("inspect_gpu_code_objects", { cause }, [ + { + path: [name], + reason: "invalid_format", + message: `${name} must be valid JSON`, + }, + ]), + ); + } + } + const rawInput = { + path: absoluteCallerPath(args.path), + analysis: options.analysis, + ...(options.expected_sha256 === undefined + ? {} + : { expected_sha256: options.expected_sha256 }), + ...selectors, + detail: options.detail, + }; + const selected = inspectGpuCodeObjectsInputSchema.safeParse(rawInput); + if (!selected.success) + return projectAnalysisError( + analysisInputErrorFromIssues( + "inspect_gpu_code_objects", + selected.error.issues, + rawInput, + ), + ); + const service = createGpuCodeObjectsService(); + return deliverCliAnalysisEvidence({ + run: () => + service.inspect( + { + path: absoluteCallerPath(args.path), + analysis: selected.data.analysis, + ...(selected.data.expected_sha256 === undefined + ? {} + : { expected_sha256: selected.data.expected_sha256 }), + ...(selected.data.object_ids === undefined + ? {} + : { object_ids: selected.data.object_ids }), + ...(selected.data.targets === undefined + ? {} + : { targets: selected.data.targets }), + detail: "complete", + }, + { signal }, + ), + service, + providerId: "gpu-code-objects", + operation: "inspect_gpu_code_objects", + signal, + detail: options.detail, + evidenceOutput: options.evidence_output, + ...(selected.data.view === undefined + ? {} + : { + view: { + kind: "gpu-code-objects" as const, + ...selected.data.view, + }, + }), + }); + }), + ), + }); +}; diff --git a/src/cliCommandNames.ts b/src/cliCommandNames.ts index ce6c6bf837..91ff0fb0eb 100644 --- a/src/cliCommandNames.ts +++ b/src/cliCommandNames.ts @@ -62,6 +62,7 @@ export const CLI_COMMANDS = Object.freeze({ inspectGoBinary: "inspect-go-binary", inspectIl2cppMetadata: "inspect-il2cpp-metadata", inspectModelArtifact: "inspect-model-artifact", + inspectGpuCodeObjects: "inspect-gpu-code-objects", inspectRustBinary: "inspect-rust-binary", inspectPeResources: "inspect-pe-resources", inspectAnalysisView: "inspect-analysis-view", diff --git a/src/composition/gpu.ts b/src/composition/gpu.ts new file mode 100644 index 0000000000..1bd11a0b1d --- /dev/null +++ b/src/composition/gpu.ts @@ -0,0 +1,4 @@ +import { GpuCodeObjectsService } from "../application/gpu/GpuCodeObjectsService.js"; +import { GpuCodeObjectsProvider } from "../gpu/GpuCodeObjectsProvider.js"; +export const createGpuCodeObjectsService = (): GpuCodeObjectsService => + new GpuCodeObjectsService(new GpuCodeObjectsProvider()); diff --git a/src/contracts/analysisViewToolContracts.ts b/src/contracts/analysisViewToolContracts.ts index e6a79eb61c..fc073abde9 100644 --- a/src/contracts/analysisViewToolContracts.ts +++ b/src/contracts/analysisViewToolContracts.ts @@ -17,7 +17,7 @@ export const ANALYSIS_VIEW_TOOL_CONTRACTS = [ ...toolContractMetadata("inspect_analysis_view"), kind: "application", description: - 'Inspect selected views of completed Frida session status, GDB/objdump command observations, Go build metadata, Rust embedded dependencies, binary-layout, JavaScript application, native analyze_function, inspect_artifact, inventory_artifact, inspect_wasm_artifact, inspect_macho, decode_interface_builder, inspect_evm_interface, inspect_managed_artifact, inspect_managed_members, inspect_android_package, inspect_dart_aot, inspect_il2cpp_metadata, inspect_model_artifact or inspect_web_network_capture Evidence without repeating analysis. Command summary views preserve ordered GDB records or opaque objdump stdout/stderr once, source parameters, completion facts and parent identity. They do not establish current target state. Historical capture summaries return identity and counts without bodies or archive inventories. Capture views page retained records: view {kind: "capture", detail: "preview" | "complete", offset: 0, limit: 20}; preview selects recorded headline fields and binary descriptors, complete returns exact records with all sidecars. Offsets follow the selected parent record order, preserving original producer ordinals. Paging never reopens the source or reruns the decoder. Go metadata uses kind go, collection dependencies/settings, exact paths/keys or indices, fields and pagination. Row index refers to the original collection; empty selections return no rows, and dependency coverage remains unknown. Rust metadata uses kind rust, collection packages/edges, exact names/versions or original indices, selected fields, edge from_indices/to_indices and matching-row pagination. Embedded-record coverage is separate from unknown runtime dependencies. For inspect_pcap_capture parents, kind pcap pages endpoints, dns-queries, tls-sni, protocols, http-uris or unattributed-addresses. Select an exact query for dns-addresses, or a tshark/capinfos stream for UTF-16 diagnostics text. Page totals count retained facts; source coverage remains explicit. Qlog summaries keep profile/scope and counts; kind qlog pages traces, streams, loss-declarations, incomplete-records, contributions, ranges or final-sizes. Except traces, select trace_index; stream-specific facets also require exact stream_id and direction. Facts retain all-recorded-events-before-selection scope, and interval overlap or endpoint loss declarations do not prove retransmission or network loss. The api-surface view groups retained HAR requests by method, origin and format-rule path template; templated segments are inferences. It reads no headers or bodies and reports no query values. The environment-manifest view projects the same HAR requests on the primary origin (most JSON-like records) into a replayable tool surface: view {kind: "environment-manifest", template_rules: ["after-uid-literal", "opaque-token", "sibling-variance"], include_query_parameter_names: true}. Each tool has a deterministic name, a path template and parameters from the four format rules plus the selected opt-in rules, observed query parameter names, a value-free body shape for POST/PUT/PATCH, status and content-type sets and record ordinals; records are pointers into the parent, excluded groups carry a reason and three fixed unknowns are always stated. Templates, parameters and names are inferences; the view copies no header, cookie or body values and refuses parents inspected with sensitive values. A capture_browser_scenario parent that selected response bodies or header values projects the same way, one record per network transaction in request order; it relies on capture-time redaction of declared secrets instead of that refusal, and a transaction whose URL was redacted is left unrepresented. The environment-state view extracts, from the same parents, the initial state the recorded reads observed: view {kind: "environment-state", template_rules: [...]} with the rules of the manifest it accompanies. It reads response bodies recorded before the first related write and reports entity types with an identity field (proved by a path parameter or inferred from a field name), entities in collection order with per-view values where views differ, documents for non-parameterized non-entity paths written by POST/PUT/PATCH (item-shaped and delete-only paths stay recorded), and a render plan per read tool: item and list views reproduce their latest recordings from the state, and every other read tool is a recorded view with a reason. Types and identity are inferences; values are recorded data, so it refuses parents inspected with sensitive values, and a scenario parent must have selected response bodies; its values keep the capture redaction markers, and a write whose URL it redacted ends the state of every type. For a retained record use source: {kind: "retained-evidence", evidence_id: ""}. Use normalized_result.parent_evidence_id from a summary receipt. Source kind is required. Portable source uses {kind: "inline", evidence: }. A completed Evidence JSON file on this host uses {kind: "evidence-file", path: ""}: REA reads and authenticates it, then projects it like retained Evidence, so a large capture is bounded by the file reader rather than by inline-input accounting. For layout, JavaScript, inventory, WASM, Mach-O, Interface Builder, EVM, managed artifact, Android package and Dart AOT sources, request a summary with view: {kind: "summary"}; summary is a view kind and has no facet. The WASM summary gives validation, counts, WAT digest/length, tool profile and execution/network states without reading WAT text. Select a layout mitigations/linkage facet, one section/symbol/module, or a stable page. Module pages include assets, bundled and source modules; use node_id for ambiguous or unavailable paths. Typed module references use view {kind: "javascript", collection: "modules", projection: "references", where: {path: "main.js", kind: "javascript-module"}, offset: 0, limit: 64}; where matches exact retained strings (null selects unavailable paths). Rows include canonical identity, retained source-range count and observation support states. Source coverage and selection remain explicit; exhausting matching rows does not prove runtime loading or complete analysis. For exact file/native facts and processing gaps, use view {kind: "javascript-facts", where: {paths: ["package.json"]}, property_keys: ["name", "operation", "analysis_status"]}. Rows page original observations with canonical identities and Evidence; missing properties remain omitted. Paths match recorded artifact-path/source-range locations; optional kinds filter nodes. To discover field names without values, opt into include_available_property_keys: true with property_keys: []. Canonical identity and Evidence are separate from properties; keep each node_id/observation_id pair together when composing results. Inventory uses view: {kind: "inventory", collection: "occurrences", logical_paths: ["package.json"], offset: 0, limit: 64}. Inventory views page nodes, occurrences, edges or integrity contradictions. Occurrences accept exact logical_paths and occurrence_ids; nodes accept artifact_ids. Batch filters intersect (omitted means all available, [] means none); fields selects columns while retaining identity links. source_complete describes retained graph membership; source_coverage describes producer coverage, including unknown encrypted PCK directories. Page exhaustion does not prove absence in a partial source. WASM views use kind wasm and facet sections/imports/exports/candidates/glue/wat/headers/details/tool_profile; text facets use UTF-16 code units and source forms remain exact. Mach-O views use kind macho and facet entrypoints/architectures/build_metadata/load_commands/dependencies/imports/exports/segments/provenance; source_exhaustive false means unreturned rows may exist. Interface Builder views use kind interface-builder and collection documents/nodes/edges/coverage; nodes and edges accept an exact document relative_path. EVM summary returns all inferred interface candidates, carrier/decoded identities and explicit unknowns without hex or duplicate stdout. EVM views use kind evm and facet functions/bytecode_hex/stdout/stderr; function pages preserve the original candidate rows, and text offsets/limits count UTF-16 code units (hex has two characters per decoded byte). Exhausting candidate pages does not establish global interface completeness. Managed artifact summaries preserve complete identity/classification, metadata status, module/MVID, frameworks, coverage and limitations without unrelated inventories. Managed pages use kind managed and collection references/resources/attributes, retaining source coverage and build-local token scope; page exhaustion does not establish complete metadata. Managed member summaries keep identity, selection with unmatched names, coverage and per-collection counts; managed-members pages use collection types/exported_types/fields/methods/member_refs/call_edges/field_accesses/user_strings and carry the source selection and coverage. Android package summaries retain all declarations, engine identity and counts without decoded manifest text. Android manifest pages use kind android, facet manifest, offset and limit in UTF-16 code units; source_status preserves partial decoded coverage even when the retained text page is exhausted. Dart AOT summary preserves target/ABI/library identity, snapshot headers and pool lower bounds. String pages use kind dart-aot, collection package_uris/dart_uris/class_like_tokens; source coverage remains partial when the producer says partial. Protobuf views use kind protobuf, collection bodies/frames/fields, with record_ordinals, body_pointers and fields; field pages also select frame_indices, field_ids, field_numbers or schema_names. Byte identity, offset basis and body status remain present. Text and schema_value are opt-in page columns. Model views use kind model-artifact and collection metadata (keys), tensors (names/types) or types (derived totals), with indices, fields and paging. Header SHA-256 scopes declarations; full-file identity remains unknown unless full hashing was selected. Arrays retain typed eight-element previews with explicit omissions. IL2CPP metadata views use kind il2cpp-metadata and collection sections/images/types/methods/fields/properties/events/parameters/string_literals/code_gen_modules/method_addresses/return_types/field_types/parameter_types. Select indices, fields and pages; named rows accept names, types also accept image_indices/namespaces/declaring_types, and methods/members accept declaring_types. Parameters and parameter_types accept method_indices; code_gen_modules accept image_indices; method_addresses accept addresses/states, preserving every folded alternative. Row index is scoped to the metadata digest; native pages add binary_source. Uncollected native tables have null counts and remain unavailable. Desktop directory views use kind desktop-application and collection files/omissions/candidates/components/next_probes, with fields and pagination; files and omissions accept exact paths or their stable IDs, and candidates accept families. Source coverage and stable row IDs remain available. Native inventory Evidence from list_names, list/search_procedures or list/search_strings uses view: {kind: "native-inventory", offset: 0, limit: 64, fields: ["value"]}. Rows keep source_index and address even with selected fields. Summary counts describe retained rows; provider discovery coverage remains unknown. Native function Evidence requires kind native: view: {kind: "native", facet: "procedure"}. Facets select procedure, pseudocode, assembly, basic blocks, comments, callers/callees, references, unresolved calls, referenced strings/names, native API and high-pcode. Use nonnegative offset and a positive caller-selected limit (defaults 0 and 64); pseudocode uses UTF-16 code units, other native pages use rows. Returns artifact identity, parent Evidence ID, a distinct view digest, coverage, limitations and unknowns. Actual serialized size determines MCP transport admission; choose a smaller page or export the complete retained Evidence if needed.', + 'Inspect selected views of completed Frida session status, GDB/objdump command observations, Go build metadata, Rust embedded dependencies, binary-layout, JavaScript application, native analyze_function, inspect_artifact, inventory_artifact, inspect_wasm_artifact, inspect_macho, decode_interface_builder, inspect_evm_interface, inspect_managed_artifact, inspect_managed_members, inspect_android_package, inspect_dart_aot, inspect_il2cpp_metadata, inspect_model_artifact, inspect_gpu_code_objects or inspect_web_network_capture Evidence without repeating analysis. Command summary views preserve ordered GDB records or opaque objdump stdout/stderr once, source parameters, completion facts and parent identity. They do not establish current target state. Historical capture summaries return identity and counts without bodies or archive inventories. Capture views page retained records: view {kind: "capture", detail: "preview" | "complete", offset: 0, limit: 20}; preview selects recorded headline fields and binary descriptors, complete returns exact records with all sidecars. Offsets follow the selected parent record order, preserving original producer ordinals. Paging never reopens the source or reruns the decoder. Go metadata uses kind go, collection dependencies/settings, exact paths/keys or indices, fields and pagination. Row index refers to the original collection; empty selections return no rows, and dependency coverage remains unknown. Rust metadata uses kind rust, collection packages/edges, exact names/versions or original indices, selected fields, edge from_indices/to_indices and matching-row pagination. Embedded-record coverage is separate from unknown runtime dependencies. For inspect_pcap_capture parents, kind pcap pages endpoints, dns-queries, tls-sni, protocols, http-uris or unattributed-addresses. Select an exact query for dns-addresses, or a tshark/capinfos stream for UTF-16 diagnostics text. Page totals count retained facts; source coverage remains explicit. Qlog summaries keep profile/scope and counts; kind qlog pages traces, streams, loss-declarations, incomplete-records, contributions, ranges or final-sizes. Except traces, select trace_index; stream-specific facets also require exact stream_id and direction. Facts retain all-recorded-events-before-selection scope, and interval overlap or endpoint loss declarations do not prove retransmission or network loss. The api-surface view groups retained HAR requests by method, origin and format-rule path template; templated segments are inferences. It reads no headers or bodies and reports no query values. The environment-manifest view projects the same HAR requests on the primary origin (most JSON-like records) into a replayable tool surface: view {kind: "environment-manifest", template_rules: ["after-uid-literal", "opaque-token", "sibling-variance"], include_query_parameter_names: true}. Each tool has a deterministic name, a path template and parameters from the four format rules plus the selected opt-in rules, observed query parameter names, a value-free body shape for POST/PUT/PATCH, status and content-type sets and record ordinals; records are pointers into the parent, excluded groups carry a reason and three fixed unknowns are always stated. Templates, parameters and names are inferences; the view copies no header, cookie or body values and refuses parents inspected with sensitive values. A capture_browser_scenario parent that selected response bodies or header values projects the same way, one record per network transaction in request order; it relies on capture-time redaction of declared secrets instead of that refusal, and a transaction whose URL was redacted is left unrepresented. The environment-state view extracts, from the same parents, the initial state the recorded reads observed: view {kind: "environment-state", template_rules: [...]} with the rules of the manifest it accompanies. It reads response bodies recorded before the first related write and reports entity types with an identity field (proved by a path parameter or inferred from a field name), entities in collection order with per-view values where views differ, documents for non-parameterized non-entity paths written by POST/PUT/PATCH (item-shaped and delete-only paths stay recorded), and a render plan per read tool: item and list views reproduce their latest recordings from the state, and every other read tool is a recorded view with a reason. Types and identity are inferences; values are recorded data, so it refuses parents inspected with sensitive values, and a scenario parent must have selected response bodies; its values keep the capture redaction markers, and a write whose URL it redacted ends the state of every type. For a retained record use source: {kind: "retained-evidence", evidence_id: ""}. Use normalized_result.parent_evidence_id from a summary receipt. Source kind is required. Portable source uses {kind: "inline", evidence: }. A completed Evidence JSON file on this host uses {kind: "evidence-file", path: ""}: REA reads and authenticates it, then projects it like retained Evidence, so a large capture is bounded by the file reader rather than by inline-input accounting. For layout, JavaScript, inventory, WASM, Mach-O, Interface Builder, EVM, managed artifact, Android package and Dart AOT sources, request a summary with view: {kind: "summary"}; summary is a view kind and has no facet. The WASM summary gives validation, counts, WAT digest/length, tool profile and execution/network states without reading WAT text. Select a layout mitigations/linkage facet, one section/symbol/module, or a stable page. Module pages include assets, bundled and source modules; use node_id for ambiguous or unavailable paths. Typed module references use view {kind: "javascript", collection: "modules", projection: "references", where: {path: "main.js", kind: "javascript-module"}, offset: 0, limit: 64}; where matches exact retained strings (null selects unavailable paths). Rows include canonical identity, retained source-range count and observation support states. Source coverage and selection remain explicit; exhausting matching rows does not prove runtime loading or complete analysis. For exact file/native facts and processing gaps, use view {kind: "javascript-facts", where: {paths: ["package.json"]}, property_keys: ["name", "operation", "analysis_status"]}. Rows page original observations with canonical identities and Evidence; missing properties remain omitted. Paths match recorded artifact-path/source-range locations; optional kinds filter nodes. To discover field names without values, opt into include_available_property_keys: true with property_keys: []. Canonical identity and Evidence are separate from properties; keep each node_id/observation_id pair together when composing results. Inventory uses view: {kind: "inventory", collection: "occurrences", logical_paths: ["package.json"], offset: 0, limit: 64}. Inventory views page nodes, occurrences, edges or integrity contradictions. Occurrences accept exact logical_paths and occurrence_ids; nodes accept artifact_ids. Batch filters intersect (omitted means all available, [] means none); fields selects columns while retaining identity links. source_complete describes retained graph membership; source_coverage describes producer coverage, including unknown encrypted PCK directories. Page exhaustion does not prove absence in a partial source. WASM views use kind wasm and facet sections/imports/exports/candidates/glue/wat/headers/details/tool_profile; text facets use UTF-16 code units and source forms remain exact. Mach-O views use kind macho and facet entrypoints/architectures/build_metadata/load_commands/dependencies/imports/exports/segments/provenance; source_exhaustive false means unreturned rows may exist. Interface Builder views use kind interface-builder and collection documents/nodes/edges/coverage; nodes and edges accept an exact document relative_path. EVM summary returns all inferred interface candidates, carrier/decoded identities and explicit unknowns without hex or duplicate stdout. EVM views use kind evm and facet functions/bytecode_hex/stdout/stderr; function pages preserve the original candidate rows, and text offsets/limits count UTF-16 code units (hex has two characters per decoded byte). Exhausting candidate pages does not establish global interface completeness. Managed artifact summaries preserve complete identity/classification, metadata status, module/MVID, frameworks, coverage and limitations without unrelated inventories. Managed pages use kind managed and collection references/resources/attributes, retaining source coverage and build-local token scope; page exhaustion does not establish complete metadata. Managed member summaries keep identity, selection with unmatched names, coverage and per-collection counts; managed-members pages use collection types/exported_types/fields/methods/member_refs/call_edges/field_accesses/user_strings and carry the source selection and coverage. Android package summaries retain all declarations, engine identity and counts without decoded manifest text. Android manifest pages use kind android, facet manifest, offset and limit in UTF-16 code units; source_status preserves partial decoded coverage even when the retained text page is exhausted. Dart AOT summary preserves target/ABI/library identity, snapshot headers and pool lower bounds. String pages use kind dart-aot, collection package_uris/dart_uris/class_like_tokens; source coverage remains partial when the producer says partial. Protobuf views use kind protobuf, collection bodies/frames/fields, with record_ordinals, body_pointers and fields; field pages also select frame_indices, field_ids, field_numbers or schema_names. Byte identity, offset basis and body status remain present. Text and schema_value are opt-in page columns. Model views use kind model-artifact and collection metadata (keys), tensors (names/types) or types (derived totals), with indices, fields and paging. Header SHA-256 scopes declarations; full-file identity remains unknown unless full hashing was selected. Arrays retain typed eight-element previews with explicit omissions. IL2CPP metadata views use kind il2cpp-metadata and collection sections/images/types/methods/fields/properties/events/parameters/string_literals/code_gen_modules/method_addresses/return_types/field_types/parameter_types. Select indices, fields and pages; named rows accept names, types also accept image_indices/namespaces/declaring_types, and methods/members accept declaring_types. Parameters and parameter_types accept method_indices; code_gen_modules accept image_indices; method_addresses accept addresses/states, preserving every folded alternative. Row index is scoped to the metadata digest; native pages add binary_source. Uncollected native tables have null counts and remain unavailable. Desktop directory views use kind desktop-application and collection files/omissions/candidates/components/next_probes, with fields and pagination; files and omissions accept exact paths or their stable IDs, and candidates accept families. Source coverage and stable row IDs remain available. Native inventory Evidence from list_names, list/search_procedures or list/search_strings uses view: {kind: "native-inventory", offset: 0, limit: 64, fields: ["value"]}. Rows keep source_index and address even with selected fields. Summary counts describe retained rows; provider discovery coverage remains unknown. Native function Evidence requires kind native: view: {kind: "native", facet: "procedure"}. Facets select procedure, pseudocode, assembly, basic blocks, comments, callers/callees, references, unresolved calls, referenced strings/names, native API and high-pcode. Use nonnegative offset and a positive caller-selected limit (defaults 0 and 64); pseudocode uses UTF-16 code units, other native pages use rows. Returns artifact identity, parent Evidence ID, a distinct view digest, coverage, limitations and unknowns. Actual serialized size determines MCP transport admission; choose a smaller page or export the complete retained Evidence if needed.', inputSchema: inspectAnalysisViewInputSchema, outputSchema: evidenceResultOf(analysisViewResultSchema), examples: [ diff --git a/src/contracts/gpu/gpuToolContracts.ts b/src/contracts/gpu/gpuToolContracts.ts new file mode 100644 index 0000000000..8c6d01233d --- /dev/null +++ b/src/contracts/gpu/gpuToolContracts.ts @@ -0,0 +1,40 @@ +import { z } from "zod"; +import { + inspectGpuCodeObjectsInputSchema, + gpuCodeObjectsSchema, +} from "../../domain/gpu/gpuCodeObjects.js"; +import { + gpuCodeObjectsSummaryViewResultSchema, + gpuCodeObjectsPageViewResultSchema, +} from "../../domain/analysisView/analysisView.js"; +import type { ToolContract } from "../toolContractTypes.js"; +import { toolContractMetadata } from "../toolEffects.js"; +import { evidenceResultOf } from "../toolOutputSchemaPrimitives.js"; +export const GPU_TOOL_CONTRACTS = [ + { + name: "inspect_gpu_code_objects", + ...toolContractMetadata("inspect_gpu_code_objects"), + kind: "artifact-provider", + description: + "Inspect static CUDA/HIP GPU code objects in local ELF/PE hosts, CUDA fatbins, CUBIN, PTX and Clang HIP bundles (CUDA LZ4/Zstd and CCOB v1/v2/v3 zlib/Zstd). Default inventory reads directories; full-file SHA-256 always streams every byte. analysis kernels decodes only selected object_ids/targets (object_ids require expected_sha256 from their inventory), retaining all inventory rows and explicit partial/unknown states. Reports PTX virtual registers, CUBIN physical registers/nv.info parameters/shared memory and AMDGPU MessagePack kernel declarations separately. No GPU tools, target execution, network, SASS disassembly or host launch correlation. Unsupported CUDA compression retains encoded locations. Default summary retains complete Evidence; optional view returns a selected targets/objects/kernels page or PTX text page in this call. inspect_analysis_view kind gpu-code-objects reuses retained facts without reopening files; PTX text uses UTF-16 code-unit pages. Object IDs are positional within artifact.sha256. One active inspection; 128 MiB/object, 512 MiB decoded work, 16 MiB retained facts. Full Evidence includes selected PTX text artifacts with SHA-256 and encoded/decoded provenance.", + inputSchema: inspectGpuCodeObjectsInputSchema, + outputSchema: evidenceResultOf( + z.union([ + gpuCodeObjectsSchema, + gpuCodeObjectsSummaryViewResultSchema, + gpuCodeObjectsPageViewResultSchema, + ]), + ), + examples: [ + { + title: "List embedded GPU targets before selecting payloads", + input: { + path: "/artifacts/application.so", + analysis: "inventory", + detail: "summary", + view: { collection: "targets" }, + }, + }, + ], + }, +] as const satisfies readonly ToolContract[]; diff --git a/src/contracts/toolContracts.ts b/src/contracts/toolContracts.ts index 73be894f49..1e2f1be484 100644 --- a/src/contracts/toolContracts.ts +++ b/src/contracts/toolContracts.ts @@ -1,3 +1,4 @@ +import { GPU_TOOL_CONTRACTS } from "./gpu/gpuToolContracts.js"; import { CAPTURE_TOOL_CONTRACTS } from "./captures/captureToolContracts.js"; import { SIGNAL_TOOL_CONTRACTS } from "./signals/signalToolContracts.js"; import { WASM_TOOL_CONTRACTS } from "./wasm/wasmToolContracts.js"; @@ -51,6 +52,7 @@ export const TOOL_CONTRACTS = [ ...GO_TOOL_CONTRACTS, ...IL2CPP_TOOL_CONTRACTS, ...MODEL_TOOL_CONTRACTS, + ...GPU_TOOL_CONTRACTS, ...RUST_TOOL_CONTRACTS, ...WASM_TOOL_CONTRACTS, ...SQLITE_TOOL_CONTRACTS, diff --git a/src/contracts/toolEffects.test.ts b/src/contracts/toolEffects.test.ts index 2df469c121..29cd1a04e6 100644 --- a/src/contracts/toolEffects.test.ts +++ b/src/contracts/toolEffects.test.ts @@ -66,6 +66,8 @@ const EXECUTION_WORDING_EXCEPTIONS: Readonly> = { "inspects the operator-selected adb client; never starts its server", list_adb_devices: "may start the local adb server, which REA does not own", inspect_firmware_regions: "states there is no target execution", + inspect_gpu_code_objects: + "reads static declarations without executing GPU code", inspect_model_artifact: "states that model code and inference are never executed", extract_firmware: "states that extracted code is never executed", diff --git a/src/contracts/toolEffects.ts b/src/contracts/toolEffects.ts index 0e11470c0a..b10480a7cb 100644 --- a/src/contracts/toolEffects.ts +++ b/src/contracts/toolEffects.ts @@ -129,6 +129,7 @@ export const TOOL_EFFECTS: Readonly> = { inspect_go_binary: evidence, inspect_il2cpp_metadata: evidence, inspect_model_artifact: evidence, + inspect_gpu_code_objects: evidence, inspect_rust_binary: evidence, inspect_sqlite_database: effects({ mutatesSession: true, diff --git a/src/domain/analysisView/analysisView.ts b/src/domain/analysisView/analysisView.ts index 0020f4aec4..bcf6f93c39 100644 --- a/src/domain/analysisView/analysisView.ts +++ b/src/domain/analysisView/analysisView.ts @@ -1,3 +1,11 @@ +import { + gpuCodeObjectsViewRequestSchema, + gpuCodeObjectsSummarySchema, + gpuCodeObjectsItemsSchema, + gpuCodeObjectsPtxPageSchema, + projectGpuCodeObjectsView, +} from "./gpuCodeObjectsView.js"; +import { gpuCodeObjectsObjectSchema } from "../gpu/gpuCodeObjects.js"; import { projectModelArtifactView, modelArtifactSummarySchema, @@ -311,6 +319,7 @@ export const analysisViewRequestSchema = z.discriminatedUnion("kind", [ dartAotViewRequestSchema, il2cppMetadataViewRequestSchema, modelArtifactViewRequestSchema, + gpuCodeObjectsViewRequestSchema, ]); // The application owns admission and authenticated Evidence parsing. SDK and @@ -420,6 +429,7 @@ const viewResultBase = { "inspect_dart_aot", "inspect_il2cpp_metadata", "inspect_model_artifact", + "inspect_gpu_code_objects", ...NATIVE_INVENTORY_OPERATIONS, "match_native_functions", ]), @@ -605,6 +615,21 @@ export const dartAotSummaryViewResultSchema = z.strictObject({ summary: dartAotSummarySchema, }); +export const gpuCodeObjectsSummaryViewResultSchema = z.strictObject({ + ...summaryViewResultBase, + parent_operation: z.literal("inspect_gpu_code_objects"), + summary: gpuCodeObjectsSummarySchema, +}); +export const gpuCodeObjectsPageViewResultSchema = z.strictObject({ + kind: z.literal("gpu-code-objects"), + view: gpuCodeObjectsViewRequestSchema, + items: gpuCodeObjectsItemsSchema, + ptx: gpuCodeObjectsPtxPageSchema.nullable(), + source_coverage: gpuCodeObjectsObjectSchema.shape.coverage, + ...viewResultBase, + parent_operation: z.literal("inspect_gpu_code_objects"), +}); + export const modelArtifactSummaryViewResultSchema = z.strictObject({ ...summaryViewResultBase, parent_operation: z.literal("inspect_model_artifact"), @@ -708,6 +733,7 @@ export const analysisViewResultSchema = z.discriminatedUnion("kind", [ dartAotSummaryViewResultSchema, il2cppMetadataSummaryViewResultSchema, modelArtifactSummaryViewResultSchema, + gpuCodeObjectsSummaryViewResultSchema, nativeInventorySummaryViewResultSchema, nativeFunctionMatchingSummaryViewResultSchema, ]), @@ -857,6 +883,7 @@ export const analysisViewResultSchema = z.discriminatedUnion("kind", [ parent_operation: z.literal("inspect_dart_aot"), }), modelArtifactPageViewResultSchema, + gpuCodeObjectsPageViewResultSchema, z.strictObject({ kind: z.literal("il2cpp-metadata"), view: il2cppMetadataViewRequestSchema, @@ -997,6 +1024,7 @@ const retainedViewProjectors = new Map([ ["inspect_dart_aot", projectDartAotView], ["inspect_il2cpp_metadata", projectIl2cppMetadataView], ["inspect_model_artifact", projectModelArtifactView], + ["inspect_gpu_code_objects", projectGpuCodeObjectsView], ["match_native_functions", projectNativeFunctionMatchingView], ["inspect_artifact", projectArtifactInventoryView], ["inventory_artifact", projectArtifactInventoryView], diff --git a/src/domain/analysisView/gpuCodeObjectsView.ts b/src/domain/analysisView/gpuCodeObjectsView.ts new file mode 100644 index 0000000000..1b256b756e --- /dev/null +++ b/src/domain/analysisView/gpuCodeObjectsView.ts @@ -0,0 +1,421 @@ +import { isImmutableJsonSnapshot } from "../immutableJson.js"; +import { digestSchema } from "../digests.js"; +import { z } from "zod"; +import { + gpuCodeObjectsSchema, + gpuCodeObjectsObjectSchema, + gpuCodeObjectsSelectionSchema, + gpuObjectSchema, + gpuKernelSchema, + gpuTargetSchema, + type GpuTarget, + type GpuCodeObjects, +} from "../gpu/gpuCodeObjects.js"; +import type { AnalysisError } from "../analysisErrorBase.js"; +import type { + AnalysisViewParent, + AnalysisViewRequest, + UnsignedAnalysisView, +} from "./analysisView.js"; +import { analysisViewInputError } from "./analysisViewErrors.js"; +import { parseAnalysisViewResult } from "./analysisViewParent.js"; +import { + boundedViewMessages, + completeWithinViewCoverage, + pageViewCoverage, +} from "./analysisViewCoverage.js"; +import { err, ok, type Result } from "../result.js"; +import type { JsonValue } from "../jsonValue.js"; +import { jsonValueByteLength } from "../jsonValueByteLength.js"; +import { AnalysisResourceConstraintError } from "../analysisErrorCore.js"; +export const gpuCodeObjectsViewRequestSchema = z.discriminatedUnion( + "collection", + [ + gpuCodeObjectsSelectionSchema.options[0].extend({ + kind: z.literal("gpu-code-objects"), + }), + gpuCodeObjectsSelectionSchema.options[1].extend({ + kind: z.literal("gpu-code-objects"), + }), + gpuCodeObjectsSelectionSchema.options[2].extend({ + kind: z.literal("gpu-code-objects"), + }), + gpuCodeObjectsSelectionSchema.options[3].extend({ + kind: z.literal("gpu-code-objects"), + }), + ], +); +export const gpuCodeObjectsSummarySchema = gpuCodeObjectsObjectSchema + .omit({ objects: true, kernels: true, ptx_artifacts: true }) + .extend({ + counts: z.strictObject({ + objects: z.number().int().nonnegative(), + kernels: z.number().int().nonnegative(), + ptx_artifacts: z.number().int().nonnegative(), + targets: z.number().int().nonnegative(), + }), + target_preview: z.array(z.string()), + }); +export const gpuCodeObjectsItemsSchema = z.union([ + z.array( + gpuObjectSchema.partial().required({ + id: true, + kind: true, + target: true, + location: true, + metadata_location: true, + payload_target: true, + target_consistency: true, + inspection: true, + }), + ), + z.array( + gpuKernelSchema.partial().required({ + id: true, + object_id: true, + location: true, + sources: true, + representation: true, + }), + ), + z.array( + gpuTargetSchema.partial().required({ + id: true, + target: true, + confidence: true, + kernel_coverage: true, + }), + ), +]); +export const gpuCodeObjectsPtxPageSchema = z.strictObject({ + artifact_id: z.string(), + object_id: z.string(), + sha256: digestSchema, + byte_length: z.number().int().nonnegative(), + text: z.string(), + offset: z.number().int().nonnegative(), + offset_unit: z.literal("utf16-code-unit"), +}); +const summarizeGpuTargets = (report: GpuCodeObjects): GpuTarget[] => { + const groups = new Map< + string | null, + { row: GpuTarget; encoded: bigint; decoded: bigint | null } + >(); + const kernelCounts = new Map(); + for (const kernel of report.kernels) + kernelCounts.set( + kernel.object_id, + (kernelCounts.get(kernel.object_id) ?? 0) + 1, + ); + for (const object of report.objects) { + if ( + object.kind === "cuda-fatbin" || + object.kind === "hip-bundle" || + object.kind === "native-section" || + object.kind === "host" + ) + continue; + let group = groups.get(object.target); + if (group === undefined) { + group = { + encoded: 0n, + decoded: 0n, + row: { + id: `target:${JSON.stringify(object.target)}`, + target: object.target, + confidence: "derived", + object_count: 0, + kernel_count: 0, + encoded_bytes: "0", + decoded_bytes: "0", + kernel_coverage: + report.analysis === "inventory" ? "not-requested" : "complete", + by_kind: { ptx: 0, cubin: 0, amdgpu: 0, unknown: 0 }, + }, + }; + groups.set(object.target, group); + } + group.row.object_count++; + group.row.by_kind[object.kind]++; + group.row.kernel_count += kernelCounts.get(object.id) ?? 0; + group.encoded += BigInt(object.encoded_size); + group.decoded = + group.decoded === null || object.decoded_size === null + ? null + : group.decoded + BigInt(object.decoded_size); + if (report.analysis === "kernels" && object.inspection !== "complete") + group.row.kernel_coverage = "partial"; + } + return [...groups.values()].map(({ row, encoded, decoded }) => ({ + ...row, + encoded_bytes: encoded.toString(), + decoded_bytes: decoded?.toString() ?? null, + })); +}; +type CheckedGpu = { + report: z.infer; + byId: Map>; + summary: z.infer; + targets: GpuTarget[]; +}; +// Evidence owns this immutable snapshot. Collection eviction releases the indexes as well. +const checkedGpu = new WeakMap(); +const readGpuFacts = ( + parent: AnalysisViewParent, +): Result => { + const key = parent.normalizedResult; + const reusable = + parent.sealedResult === true && + key !== null && + typeof key === "object" && + isImmutableJsonSnapshot(key); + let checked = reusable ? checkedGpu.get(key) : undefined; + if (checked === undefined) { + const parsed = parseAnalysisViewResult(gpuCodeObjectsSchema, parent); + if (!parsed.ok) return parsed; + const report = reusable + ? (key as z.infer) + : parsed.value; + const { objects, kernels, ptx_artifacts, ...context } = report; + const targets = new Set( + objects.flatMap((row) => (row.target === null ? [] : [row.target])), + ); + checked = { + report, + targets: summarizeGpuTargets(report), + byId: new Map(objects.map((row) => [row.id, row])), + summary: { + ...context, + counts: { + objects: objects.length, + kernels: kernels.length, + ptx_artifacts: ptx_artifacts.length, + targets: targets.size, + }, + target_preview: [...targets] + .slice(0, 8) + .map((target) => target.slice(0, 256)), + }, + }; + if (reusable) checkedGpu.set(key, checked); + } + if ( + parent.subject != null && + (parent.subject.local_path !== checked.report.artifact.path || + parent.subject.digest.sha256 !== checked.report.artifact.sha256) + ) + return err( + analysisViewInputError([ + { + path: ["source", "subject"], + reason: "invalid_value", + message: "GPU Evidence subject does not match its recorded artifact", + }, + ]), + ); + return ok(checked); +}; +export const projectGpuCodeObjectsView = ( + parent: AnalysisViewParent, + view: AnalysisViewRequest, +): Result => { + if (view.kind !== "summary" && view.kind !== "gpu-code-objects") + return err( + analysisViewInputError([ + { + path: ["view"], + reason: "invalid_value", + message: "GPU Evidence supports summary and gpu-code-objects pages", + }, + ]), + ); + const parsed = readGpuFacts(parent); + if (!parsed.ok) return parsed; + const { report, byId, summary, targets: targetRows } = parsed.value; + const shared = { + parent_evidence_id: parent.evidenceId, + parent_operation: "inspect_gpu_code_objects" as const, + parent_digest: parent.evidenceId.slice(3), + artifact: { path: report.artifact.path, sha256: report.artifact.sha256 }, + limitations: boundedViewMessages( + [...parent.limitations, ...report.limitations], + parent.evidenceId, + "limitations", + ), + unknowns: [ + "GPU execution, runtime compatibility and host launch sites were not inspected.", + ...(report.coverage.payloads !== "complete" + ? [ + "Kernel facts are incomplete outside successfully decoded selected payloads; consult object inspection states.", + ] + : []), + ], + }; + if (view.kind === "summary") { + return ok({ + kind: "summary", + view, + ...shared, + summary: { ...summary, limitations: shared.limitations }, + coverage: completeWithinViewCoverage( + report.objects.length, + report.objects.length, + ), + }); + } + if (view.collection === "ptx") { + const artifact = report.ptx_artifacts.find( + (row) => row.object_id === view.object_id, + ); + if (artifact === undefined) + return err( + analysisViewInputError([ + { + path: ["view", "object_id"], + reason: "invalid_value", + message: + "No retained PTX artifact exists for this object; inspect a selected PTX payload with analysis kernels", + }, + ]), + ); + const end = Math.min(artifact.text.length, view.offset + view.limit); + const split = (offset: number): boolean => + offset > 0 && + offset < artifact.text.length && + /[\uD800-\uDBFF]/u.test(artifact.text[offset - 1]!) && + /[\uDC00-\uDFFF]/u.test(artifact.text[offset]!); + if (split(view.offset) || split(end)) + return err( + analysisViewInputError([ + { + path: ["view"], + reason: "invalid_value", + message: + "PTX text page must end and start on Unicode scalar boundaries; offsets use UTF-16 code units", + }, + ]), + ); + const text = artifact.text.slice(view.offset, end); + if (jsonValueByteLength(text, 1024 * 1024) > 1024 * 1024) + return err( + new AnalysisResourceConstraintError( + "inspect_analysis_view", + "memory", + "PTX page exceeds the 1 MiB response budget; lower limit", + { maximum_page_bytes: 1024 * 1024 }, + ), + ); + return ok({ + kind: "gpu-code-objects", + view, + ...shared, + source_coverage: report.coverage, + items: [], + ptx: { + artifact_id: artifact.id, + object_id: artifact.object_id, + sha256: artifact.sha256, + byte_length: artifact.byte_length, + text, + offset: view.offset, + offset_unit: "utf16-code-unit", + }, + coverage: pageViewCoverage( + view.offset, + text.length, + artifact.text.length, + ), + }); + } + const targets = + view.targets === undefined ? undefined : new Set(view.targets); + const ids = view.ids === undefined ? undefined : new Set(view.ids); + const objectIds = + view.collection === "kernels" && view.object_ids !== undefined + ? new Set(view.object_ids) + : undefined; + const names = + view.collection === "kernels" && view.names !== undefined + ? new Set(view.names) + : undefined; + const rows = + view.collection === "objects" + ? report.objects + : view.collection === "targets" + ? targetRows + : report.kernels; + const matches = rows.filter( + (row) => + (ids === undefined || ids.has(row.id)) && + (objectIds === undefined || + ("object_id" in row && objectIds.has(row.object_id))) && + (names === undefined || ("name" in row && names.has(row.name))) && + (targets === undefined || + targets.has( + ("target" in row ? row.target : byId.get(row.object_id)?.target) ?? + "", + )), + ); + const defaults = + view.collection === "kernels" + ? new Set([ + "id", + "object_id", + "name", + "parameter_count", + "parameter_bytes", + "registers", + "shared_bytes", + "private_bytes", + "location", + "limitations", + ]) + : undefined; + const fields = + view.fields === undefined ? defaults : new Set(view.fields); + const items = matches + .slice(view.offset, view.offset + view.limit) + .map((row) => { + const projected: Record = {}; + for (const [key, value] of Object.entries(row)) + if ( + key === "id" || + key === "object_id" || + key === "location" || + key === "metadata_location" || + key === "kind" || + key === "target" || + key === "payload_target" || + key === "target_consistency" || + key === "inspection" || + key === "sources" || + key === "representation" || + (view.collection === "targets" && + (key === "target" || + key === "confidence" || + key === "kernel_coverage")) || + fields === undefined || + fields.has(key) + ) + projected[key] = value; + return projected; + }); + if (jsonValueByteLength(items, 1024 * 1024) > 1024 * 1024) + return err( + new AnalysisResourceConstraintError( + "inspect_analysis_view", + "memory", + "GPU page exceeds the 1 MiB response budget; select fewer fields or rows", + { maximum_page_bytes: 1024 * 1024 }, + ), + ); + return ok({ + kind: "gpu-code-objects", + view, + ...shared, + source_coverage: report.coverage, + items: gpuCodeObjectsItemsSchema.parse(items), + ptx: null, + coverage: pageViewCoverage(view.offset, items.length, matches.length), + }); +}; diff --git a/src/domain/elfFixedHeader.test.ts b/src/domain/elfFixedHeader.test.ts index dedc77ce21..9b1a303d02 100644 --- a/src/domain/elfFixedHeader.test.ts +++ b/src/domain/elfFixedHeader.test.ts @@ -18,6 +18,7 @@ it.each([ byteOrder: data === 1 ? "little" : "big", headerSize: cls === 1 ? 52 : 64, machine: cls === 1 ? 3 : 62, + osAbi: 0, architecture: cls === 1 ? "x86" : "x86_64", }, }); @@ -68,3 +69,16 @@ it("preserves an unknown ELF machine as a fact without rejecting the header", () value: { machine: 0xfffe, architecture: null }, }); }); + +it("accepts NVIDIA's vendor ELF version only for the CUDA machine and OS ABI", () => { + const bytes = elf(2, 1, 190); + bytes.writeUInt32LE(0x81, 20); + expect(readElfFixedHeader(bytes).ok).toBe(false); + bytes[7] = 0x33; + expect(readElfFixedHeader(bytes)).toMatchObject({ + ok: true, + value: { machine: 190 }, + }); + bytes.writeUInt16LE(62, 18); + expect(readElfFixedHeader(bytes).ok).toBe(false); +}); diff --git a/src/domain/elfFixedHeader.ts b/src/domain/elfFixedHeader.ts index c91eeb0ce1..38ae7c01da 100644 --- a/src/domain/elfFixedHeader.ts +++ b/src/domain/elfFixedHeader.ts @@ -9,6 +9,7 @@ export interface ElfFixedHeader { readonly byteOrder: "little" | "big"; readonly headerSize: number; readonly machine: number; + readonly osAbi: number; readonly architecture: Exclude< BinaryArchitecture, RawOnlyArchitecture @@ -52,16 +53,19 @@ export const readElfFixedHeader = ( littleEndian ? bytes.readUInt16LE(offset) : bytes.readUInt16BE(offset); const readU32 = (offset: number): number => littleEndian ? bytes.readUInt32LE(offset) : bytes.readUInt32BE(offset); - if (readU32(20) !== 1) return err("unsupported ELF header version"); + const machine = readU16(18); + // NVIDIA CUBIN uses ELFOSABI_CUDA and a vendor ABI version in e_version. + if (readU32(20) !== 1 && !(machine === 190 && bytes[7] === 0x33)) + return err("unsupported ELF header version"); const headerSize = readU16(elfClass === 32 ? 40 : 52); if (headerSize < minimumSize || headerSize > totalBytes) return err("invalid ELF header size"); - const machine = readU16(18); return ok({ elfClass, byteOrder: littleEndian ? "little" : "big", headerSize, machine, + osAbi: bytes[7]!, architecture: elfArchitecture(machine) ?? null, }); }; diff --git a/src/domain/gpu/gpuCodeObjects.test.ts b/src/domain/gpu/gpuCodeObjects.test.ts new file mode 100644 index 0000000000..4ad9eca74d --- /dev/null +++ b/src/domain/gpu/gpuCodeObjects.test.ts @@ -0,0 +1,34 @@ +import { createHash } from "node:crypto"; +import { expect, it } from "vitest"; +import { gpuPtxArtifactSchema } from "./gpuCodeObjects.js"; +it("binds retained PTX identity to its actual UTF-8 bytes and source extent", () => { + const text = "\uFEFF// 雪🧪\n.version 8.8\n", + size = Buffer.byteLength(text); + const artifact = { + id: "ptx", + object_id: "object", + sha256: createHash("sha256").update(text).digest("hex"), + media_type: "text/x-ptx", + encoding: "utf-8", + byte_length: size, + text, + location: { + container_id: "object", + offset: 0, + size, + section_index: null, + section_name: null, + }, + }; + expect(gpuPtxArtifactSchema.safeParse(artifact).success).toBe(true); + for (const patch of [ + { sha256: "0".repeat(64) }, + { text: text + "changed" }, + { byte_length: size + 1 }, + { location: { ...artifact.location, size: size + 1 } }, + { text: "\ud800" }, + ]) + expect( + gpuPtxArtifactSchema.safeParse({ ...artifact, ...patch }).success, + ).toBe(false); +}); diff --git a/src/domain/gpu/gpuCodeObjects.ts b/src/domain/gpu/gpuCodeObjects.ts new file mode 100644 index 0000000000..af216fa0a9 --- /dev/null +++ b/src/domain/gpu/gpuCodeObjects.ts @@ -0,0 +1,391 @@ +import { z } from "zod"; +import { createHash } from "node:crypto"; +import { digestSchema } from "../digests.js"; +import { + absoluteLocalPathStringSchema, + localPathStringSchema, +} from "../localPath.js"; +const count = z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER); +export const gpuLocationSchema = z.strictObject({ + container_id: z.string().nullable(), + offset: count, + size: count, + section_index: count.nullable(), + section_name: z.string().nullable(), +}); +export const gpuObjectSchema = z.strictObject({ + id: z.string(), + parent_id: z.string().nullable(), + kind: z.enum([ + "cuda-fatbin", + "hip-bundle", + "native-section", + "ptx", + "cubin", + "amdgpu", + "host", + "unknown", + ]), + target: z.string().nullable(), + payload_target: z.string().nullable(), + cuda_target_header: z + .strictObject({ + os_abi: z.number().int().min(0).max(255), + flags: z.string().regex(/^0x[0-9a-f]{8}$/u), + location: gpuLocationSchema, + }) + .nullable(), + target_consistency: z.enum([ + "unexamined", + "single-source", + "consistent", + "base-only", + "conflict", + "unknown", + ]), + bundle_id: z.string().nullable(), + compression: z.enum(["none", "zstd", "zlib", "lz4", "unsupported"]), + compression_id: z.string().nullable(), + encoded_size: count, + decoded_size: count.nullable(), + sha256: digestSchema.nullable(), + raw_flags: z.string().nullable(), + location: gpuLocationSchema, + metadata_location: gpuLocationSchema, + inspection: z.enum([ + "inventory", + "complete", + "partial", + "not-selected", + "unsupported", + "malformed", + "capacity", + ]), + limitations: z.array(z.string()), +}); +export const gpuParameterSchema = z.strictObject({ + ordinal: count, + name: z.string().nullable(), + offset: count.nullable(), + size: count.nullable(), + type: z.string().nullable(), + hidden: z.boolean().nullable(), +}); +export const gpuKernelSchema = z.strictObject({ + id: z.string(), + object_id: z.string(), + representation: z.enum([ + "ptx-declarations", + "cuda-elf-metadata", + "amdgpu-metadata", + ]), + name: z.string(), + symbol: z.string().nullable(), + parameter_count: count.nullable(), + parameter_bytes: count.nullable(), + parameters: z.array(gpuParameterSchema), + registers: z.strictObject({ + physical: count.nullable(), + sgpr: count.nullable(), + vgpr: count.nullable(), + agpr: count.nullable(), + virtual: z.array( + z.strictObject({ type: z.string(), count: count.nullable() }), + ), + }), + shared_bytes: count + .nullable() + .describe( + "PTX: entry-local declared static bytes; CUDA ELF: allocated section bytes; AMDGPU: group_segment_fixed_size. Runtime dynamic/module shared allocations are not established by PTX.", + ), + private_bytes: count.nullable(), + location: gpuLocationSchema, + sources: z.strictObject({ + parameters: gpuLocationSchema.nullable(), + registers: gpuLocationSchema.nullable(), + shared: gpuLocationSchema.nullable(), + private: gpuLocationSchema.nullable(), + }), + limitations: z.array(z.string()), +}); +export const gpuPtxArtifactSchema = z + .strictObject({ + id: z.string(), + object_id: z.string(), + sha256: digestSchema, + media_type: z.literal("text/x-ptx"), + encoding: z.literal("utf-8"), + byte_length: count, + text: z.string(), + location: gpuLocationSchema, + }) + .superRefine((artifact, context) => { + if ( + /[\uD800-\uDFFF]/u.test(artifact.text) || + Buffer.byteLength(artifact.text, "utf8") !== artifact.byte_length || + artifact.location.size !== artifact.byte_length + ) + context.addIssue({ + code: "custom", + path: ["byte_length"], + message: "PTX text and its UTF-8 source extent disagree", + }); + if ( + createHash("sha256").update(artifact.text, "utf8").digest("hex") !== + artifact.sha256 + ) + context.addIssue({ + code: "custom", + path: ["sha256"], + message: "PTX digest does not match retained UTF-8 text", + }); + }); +export const gpuCodeObjectsObjectSchema = z.strictObject({ + artifact: z.strictObject({ + path: localPathStringSchema, + file_size: count, + sha256: digestSchema, + bytes_read: count, + }), + format: z.enum(["elf", "pe", "cuda-fatbin", "hip-bundle", "ptx"]), + analysis: z.enum(["inventory", "kernels"]), + objects: z.array(gpuObjectSchema), + kernels: z.array(gpuKernelSchema), + ptx_artifacts: z.array(gpuPtxArtifactSchema), + coverage: z.strictObject({ + discovery_scope: z.literal( + "supported-section-names-and-standalone-formats", + ), + discovery: z.enum(["complete", "partial"]), + payloads: z.enum(["not-requested", "complete", "partial"]), + examined_sections: count, + unknown_section_names: count, + }), + limitations: z.array(z.string()), +}); +/** Validate local identities and coordinate spaces once before consumers build lookup indexes. */ +export const gpuCodeObjectsSchema = gpuCodeObjectsObjectSchema.superRefine( + (report, context) => { + const objects = new Map(); + const issue = (path: (string | number)[], message: string): void => + context.addIssue({ code: "custom", path, message }); + for (let index = 0; index < report.objects.length; index++) { + const row = report.objects[index]!; + if (objects.has(row.id)) + issue(["objects", index, "id"], "GPU object IDs must be unique"); + if (row.parent_id !== null && !objects.has(row.parent_id)) + issue( + ["objects", index, "parent_id"], + "GPU parents must precede their children", + ); + if ( + row.location.container_id !== row.parent_id || + row.metadata_location.container_id !== row.parent_id + ) + issue( + ["objects", index, "location"], + "GPU object ranges must use their parent's coordinate space", + ); + objects.set(row.id, row); + } + const range = (value: GpuLocation, path: (string | number)[]): void => { + const size = + value.container_id === null + ? report.artifact.file_size + : objects.get(value.container_id)?.decoded_size; + if ( + size === undefined || + (size !== null && + (value.offset > size || value.size > size - value.offset)) + ) + issue( + path, + "GPU source range is outside its declared coordinate space", + ); + }; + for (let index = 0; index < report.objects.length; index++) { + const row = report.objects[index]!; + range(row.location, ["objects", index, "location"]); + range(row.metadata_location, ["objects", index, "metadata_location"]); + const header = row.cuda_target_header; + if (header !== null) { + range(header.location, [ + "objects", + index, + "cuda_target_header", + "location", + ]); + if ( + row.kind !== "cubin" || + ![52, 64].includes(header.location.size) || + (header.location.container_id === row.id + ? header.location.offset !== 0 + : header.location.container_id !== row.parent_id || + header.location.offset !== row.location.offset) + ) + issue( + ["objects", index, "cuda_target_header"], + "CUDA header provenance must start at its CUBIN object", + ); + } + } + const kernelIds = new Set(); + for (let index = 0; index < report.kernels.length; index++) { + const row = report.kernels[index]!; + if (kernelIds.has(row.id)) + issue(["kernels", index, "id"], "GPU kernel IDs must be unique"); + kernelIds.add(row.id); + if ( + !objects.has(row.object_id) || + row.location.container_id !== row.object_id + ) + issue( + ["kernels", index, "object_id"], + "GPU kernels require an inventoried object and its coordinate space", + ); + range(row.location, ["kernels", index, "location"]); + for (const [field, source] of Object.entries(row.sources)) + if (source !== null) + range(source, ["kernels", index, "sources", field]); + } + const artifactIds = new Set(); + for (let index = 0; index < report.ptx_artifacts.length; index++) { + const row = report.ptx_artifacts[index]!; + if (artifactIds.has(row.id)) + issue( + ["ptx_artifacts", index, "id"], + "PTX artifact IDs must be unique", + ); + artifactIds.add(row.id); + if ( + objects.get(row.object_id)?.kind !== "ptx" || + row.location.container_id !== row.object_id + ) + issue( + ["ptx_artifacts", index, "object_id"], + "PTX artifacts require an inventoried PTX object", + ); + range(row.location, ["ptx_artifacts", index, "location"]); + } + }, +); +/** Interrupted observations retain facts without claiming final source identity or completeness. */ +export const gpuCodeObjectsPartialSchema = z.strictObject({ + kind: z.literal("gpu-code-objects-partial"), + artifact: z.strictObject({ + path: localPathStringSchema, + observed_file_size: count, + sha256: z.null(), + file_identity: z.literal("unverified"), + }), + analysis: z.enum(["inventory", "kernels"]), + objects: z.array(gpuObjectSchema), + kernels: z.array(gpuKernelSchema), + ptx_artifacts: z.array(gpuPtxArtifactSchema), + coverage: z.strictObject({ + discovery: z.literal("partial"), + payloads: z.literal("partial"), + }), + limitations: z.array(z.string()), +}); +export type GpuCodeObjectsPartial = z.infer; +export const gpuTargetSchema = z.strictObject({ + id: z.string(), + target: z.string().nullable(), + confidence: z.literal("derived"), + object_count: count, + kernel_count: count, + encoded_bytes: z.string().regex(/^(?:0|[1-9][0-9]*)$/u), + decoded_bytes: z + .string() + .regex(/^(?:0|[1-9][0-9]*)$/u) + .nullable(), + kernel_coverage: z.enum(["not-requested", "complete", "partial"]), + by_kind: z.strictObject({ + ptx: count, + cubin: count, + amdgpu: count, + unknown: count, + }), +}); +export type GpuTarget = z.infer; +const selection = { + ids: z.array(z.string()).optional(), + targets: z.array(z.string()).optional(), + offset: count.default(0), + limit: z.number().int().positive().default(32), +}; +export const gpuCodeObjectsSelectionSchema = z.discriminatedUnion( + "collection", + [ + z.strictObject({ + ...selection, + collection: z.literal("objects"), + fields: z.array(gpuObjectSchema.keyof()).optional(), + }), + z.strictObject({ + ...selection, + collection: z.literal("kernels"), + object_ids: z.array(z.string()).optional(), + names: z.array(z.string()).optional(), + fields: z.array(gpuKernelSchema.keyof()).optional(), + }), + z.strictObject({ + collection: z.literal("ptx"), + object_id: z.string(), + offset: count.default(0), + limit: z.number().int().positive().default(4096), + }), + z.strictObject({ + ...selection, + collection: z.literal("targets"), + fields: z.array(gpuTargetSchema.keyof()).optional(), + }), + ], +); +export const inspectGpuCodeObjectsInputSchema = z + .strictObject({ + path: absoluteLocalPathStringSchema, + analysis: z.enum(["inventory", "kernels"]).default("inventory"), + expected_sha256: digestSchema + .optional() + .describe( + "Require the current full-file digest; mandatory when selecting inventory object_ids", + ), + object_ids: z + .array(z.string()) + .optional() + .describe( + "Decode only these inventory IDs; IDs are scoped to the artifact SHA-256", + ), + targets: z + .array(z.string()) + .optional() + .describe( + "Decode only exact target declarations, preserving all inventory rows", + ), + detail: z.enum(["summary", "complete"]).default("summary"), + view: gpuCodeObjectsSelectionSchema.optional(), + }) + .superRefine((input, context) => { + if (input.object_ids !== undefined && input.expected_sha256 === undefined) + context.addIssue({ + code: "custom", + path: ["expected_sha256"], + message: "object_ids require the artifact SHA-256 from their inventory", + }); + if (input.view !== undefined && input.detail === "complete") + context.addIssue({ + code: "custom", + path: ["view"], + message: "view requires summary detail", + }); + }); +export type GpuCodeObjects = z.infer; +export type GpuObject = z.infer; +export type GpuKernel = z.infer; +export type GpuLocation = z.infer; +export type GpuCodeObjectsInput = Pick< + z.infer, + "path" | "analysis" | "object_ids" | "targets" | "expected_sha256" +>; diff --git a/src/gpu/AmdGpuMetadata.test.ts b/src/gpu/AmdGpuMetadata.test.ts new file mode 100644 index 0000000000..b1d4740530 --- /dev/null +++ b/src/gpu/AmdGpuMetadata.test.ts @@ -0,0 +1,44 @@ +import { expect, it } from "vitest"; +import { encode } from "@msgpack/msgpack"; +import { readAmdMetadata } from "./AmdGpuMetadata.js"; +it.each(["\uFEFFkernel", "\uFEFF" + "kernel".repeat(100)])( + "preserves leading BOMs in MessagePack strings %s", + (name) => { + expect(readAmdMetadata(Buffer.from(encode({ name })))).toEqual({ name }); + }, +); +it("preserves raw string tags separately from binary values", () => { + const binary = Uint8Array.from([0x61, 0x62]); + const read = readAmdMetadata( + Buffer.from(encode({ text: "ab", binary })), + ) as Record; + expect(read.text).toBe("ab"); + expect(read.binary).toBeInstanceOf(Uint8Array); + expect(Array.from(read.binary as Uint8Array)).toEqual(Array.from(binary)); +}); +it("rejects duplicate map keys and malformed UTF-8 before semantic decoding", () => { + expect(() => readAmdMetadata(Buffer.from("82a16101a16102", "hex"))).toThrow( + /duplicate/u, + ); + expect(() => readAmdMetadata(Buffer.from("81a161a1ff", "hex"))).toThrow( + /UTF-8/u, + ); +}); +it("refuses declared container expansion before allocation", () => { + expect(() => readAmdMetadata(Buffer.from("ddffffffff", "hex"))).toThrow( + /allocation/u, + ); +}); +it("preserves long leading-BOM map keys through the public decoder extension point", () => { + const key = "\uFEFF" + "field".repeat(60), + object = { [key]: 123 }; + expect(readAmdMetadata(Buffer.from(encode(object)))).toEqual(object); +}); +it("distinguishes an unsupported extension tag from a forbidden MessagePack token", () => { + expect(() => readAmdMetadata(Buffer.from("d40000", "hex"))).toThrow( + expect.objectContaining({ kind: "unsupported" }), + ); + expect(() => readAmdMetadata(Buffer.from("c1", "hex"))).toThrow( + expect.objectContaining({ kind: "malformed" }), + ); +}); diff --git a/src/gpu/AmdGpuMetadata.ts b/src/gpu/AmdGpuMetadata.ts new file mode 100644 index 0000000000..6baeb6fcd8 --- /dev/null +++ b/src/gpu/AmdGpuMetadata.ts @@ -0,0 +1,187 @@ +import { decode } from "@msgpack/msgpack"; +import { + BinaryResourceFailure, + BinaryFormatFailure, +} from "../native/metadata/BinaryContainer.js"; +import { rejectGpu } from "./GpuSource.js"; + +export const AMD_METADATA_BYTES = 8 * 1024 * 1024; + +/** Validate allocation and nesting costs before the standard decoder allocates containers. */ +const utf8 = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }); +const preflight = (bytes: Buffer): Set => { + const strings = new Set(); + let at = 0, + values = 0; + const take = (size: number): number => { + const start = at; + if (size > bytes.length - at) + rejectGpu("Truncated AMDGPU MessagePack metadata"); + at += size; + return start; + }; + const value = (depth: number): string | undefined => { + if (depth > 32 || ++values > 262144) + throw new BinaryResourceFailure( + "structure", + 262144, + "AMDGPU metadata exceeds depth 32 or 262144 decoded values", + ); + const code = bytes[take(1)]!; + const string = + (code & 0xe0) === 0xa0 || code === 0xd9 || code === 0xda || code === 0xdb; + const map = (code & 0xf0) === 0x80 || code === 0xde || code === 0xdf; + let count = 0, + payload = 0; + if ( + code <= 0x7f || + code >= 0xe0 || + code === 0xc0 || + code === 0xc2 || + code === 0xc3 + ) + return; + if ((code & 0xe0) === 0xa0) payload = code & 31; + else if ((code & 0xf0) === 0x90) count = code & 15; + else if ((code & 0xf0) === 0x80) count = (code & 15) * 2; + else if (code === 0xdc || code === 0xde) + count = bytes.readUInt16BE(take(2)) * (code === 0xde ? 2 : 1); + else if (code === 0xdd || code === 0xdf) + count = bytes.readUInt32BE(take(4)) * (code === 0xdf ? 2 : 1); + else if (code === 0xd9 || code === 0xc4) payload = bytes[take(1)]!; + else if (code === 0xda || code === 0xc5) + payload = bytes.readUInt16BE(take(2)); + else if (code === 0xdb || code === 0xc6) + payload = bytes.readUInt32BE(take(4)); + else { + const widths: Readonly> = { + 0xca: 4, + 0xcb: 8, + 0xcc: 1, + 0xcd: 2, + 0xce: 4, + 0xcf: 8, + 0xd0: 1, + 0xd1: 2, + 0xd2: 4, + 0xd3: 8, + }; + const width = widths[code]; + if (width === undefined) { + if ([0xc7, 0xc8, 0xc9, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8].includes(code)) + throw new BinaryFormatFailure( + "unsupported", + "AMDGPU MessagePack extensions are outside metadata coverage", + ); + rejectGpu("Reserved AMDGPU MessagePack token"); + } + payload = width!; + } + if (count > 262144 - values || payload > 65536) + throw new BinaryResourceFailure( + "structure", + 262144, + "AMDGPU metadata container/string exceeds its allocation budget", + ); + const start = take(payload); + if (string) { + strings.add(`${bytes.byteOffset + start}:${payload}`); + try { + return utf8.decode(bytes.subarray(start, start + payload)); + } catch { + return rejectGpu("AMDGPU metadata contains invalid UTF-8"); + } + } + if (map) { + const keys = new Set(); + for (let index = 0; index < count; index += 2) { + const key = value(depth + 1); + if (key === undefined || keys.has(key)) + rejectGpu("AMDGPU metadata map has non-string or duplicate keys"); + keys.add(key!); + value(depth + 1); + } + } else for (let index = 0; index < count; index++) value(depth + 1); + return undefined; + }; + value(0); + if (at !== bytes.length) rejectGpu("Trailing AMDGPU MessagePack values"); + return strings; +}; +export const readAmdMetadata = (bytes: Buffer): unknown => { + if (bytes.length > AMD_METADATA_BYTES) + throw new BinaryResourceFailure( + "structure", + AMD_METADATA_BYTES, + "AMDGPU metadata exceeds 8 MiB", + ); + const strings = preflight(bytes); + const stringValue = (value: unknown): string => { + if ( + !(value instanceof Uint8Array) || + value.buffer !== bytes.buffer || + !strings.has(`${value.byteOffset}:${value.byteLength}`) + ) + return rejectGpu("AMDGPU metadata map key is not a string"); + return utf8.decode(value); + }; + // rawStrings avoids the standard decoder's native UTF-8 path stripping a + // leading BOM. The preflight retains original type tags, so binary values + // cannot accidentally become strings during this bounded in-place walk. + const normalize = (value: unknown): unknown => { + if (value instanceof Uint8Array) + return value.buffer === bytes.buffer && + strings.has(`${value.byteOffset}:${value.byteLength}`) + ? stringValue(value) + : value; + if (Array.isArray(value)) { + for (let index = 0; index < value.length; index++) + value[index] = normalize(value[index]); + } else if (value !== null && typeof value === "object") + for (const key of Object.keys(value)) { + const record = value as Record; + record[key] = normalize(record[key]); + } + return value; + }; + try { + return normalize( + decode(bytes, { + rawStrings: true, + keyDecoder: { + canBeCached: () => true, + decode: (source, offset, length) => + utf8.decode(source.subarray(offset, offset + length)), + }, + mapKeyConverter: (key) => { + if (typeof key !== "string") + return rejectGpu("AMDGPU metadata map key is not a string"); + return key; + }, + useBigInt64: true, + maxStrLength: 65536, + maxBinLength: 65536, + maxArrayLength: 65536, + maxMapLength: 65536, + maxExtLength: 0, + }), + ); + } catch { + return rejectGpu("Malformed AMDGPU MessagePack metadata"); + } +}; +export const amdRecord = (value: unknown): Record | null => + typeof value === "object" && value !== null && !Array.isArray(value) + ? (value as Record) + : null; +export const amdCount = (value: unknown): number | null => { + if (typeof value === "bigint") + return value >= 0n && value <= BigInt(Number.MAX_SAFE_INTEGER) + ? Number(value) + : null; + return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 + ? value + : null; +}; +export const amdString = (value: unknown): string | null => + typeof value === "string" ? value : null; diff --git a/src/gpu/ClangOffloadBundle.ts b/src/gpu/ClangOffloadBundle.ts new file mode 100644 index 0000000000..fdac9390da --- /dev/null +++ b/src/gpu/ClangOffloadBundle.ts @@ -0,0 +1,115 @@ +import { scheduler } from "node:timers/promises"; +import type { GpuObject } from "../domain/gpu/gpuCodeObjects.js"; +import { + BinaryFormatFailure, + BinaryResourceFailure, +} from "../native/metadata/BinaryContainer.js"; +import { assertArtifactReadActive } from "../artifacts/ArtifactFileOperation.js"; +import { + gpuCount, + GPU_REPORT_BYTES, + rejectGpu, + type GpuSource, +} from "./GpuSource.js"; +import { gpuObject } from "./GpuObject.js"; +export const CLANG_BUNDLE_MAGIC = Buffer.from("__CLANG_OFFLOAD_BUNDLE__"); +export const readClangBundleDirectory = async ( + source: GpuSource, + base: number, + signal?: AbortSignal, +): Promise<{ objects: GpuObject[]; end: number }> => { + const header = await source.read(base, 32), + count = gpuCount(header.readBigUInt64LE(24)); + if (!header.subarray(0, 24).equals(CLANG_BUNDLE_MAGIC)) + rejectGpu("Invalid Clang offload bundle magic"); + if (count > 65536) + throw new BinaryResourceFailure( + "structure", + 65536, + "HIP bundle entry count exceeds the inventory budget", + ); + const objects: GpuObject[] = []; + let position = base + 32; + for (let index = 0; index < count; index++) { + if (index % 256 === 0) { + await scheduler.yield(); + assertArtifactReadActive(signal); + } + const h = await source.read(position, 24), + offset = gpuCount(BigInt(base) + h.readBigUInt64LE(0)), + size = gpuCount(h.readBigUInt64LE(8)), + nameSize = gpuCount(h.readBigUInt64LE(16)); + if (nameSize > 65536 || position - base + 24 + nameSize > GPU_REPORT_BYTES) + throw new BinaryResourceFailure( + "structure", + GPU_REPORT_BYTES, + "HIP directory exceeds 16 MiB or its bundle ID exceeds 65536 bytes", + ); + let name: string; + try { + name = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode( + await source.read(position + 24, nameSize), + ); + } catch (cause) { + if (cause instanceof TypeError) + return rejectGpu("HIP bundle ID is not valid UTF-8"); + throw cause; + } + if (offset > source.size || size > source.size - offset) + rejectGpu("HIP entry range is outside its bundle"); + const row = gpuObject(source, { + offset, + size, + kind: name.startsWith("host-") + ? "host" + : /^(?:hip|hipv4)-amdgcn-/u.test(name) + ? "amdgpu" + : "unknown", + metadata: { offset: position, size: 24 + nameSize }, + }); + row.bundle_id = name; + row.target = + /-(?:-)?(gfx[a-zA-Z0-9]+(?::[^\s]+)?)$/u.exec(name)?.[1] ?? null; + objects.push(row); + position += 24 + nameSize; + } + let end = position; + for (const row of objects) { + if (row.encoded_size === 0) continue; + if (row.location.offset < position) + rejectGpu("HIP entry overlaps its bundle directory"); + end = Math.max(end, row.location.offset + row.encoded_size); + } + return { objects, end }; +}; +export const readCcobHeader = (bytes: Buffer, remaining: number) => { + if (bytes.length < 20) rejectGpu("CCOB header is truncated"); + const version = bytes.readUInt16LE(4), + method = bytes.readUInt16LE(6); + let width: number, total: number, decoded: number, hash: bigint; + if (version === 1) { + width = 20; + total = remaining; + decoded = bytes.readUInt32LE(8); + hash = bytes.readBigUInt64LE(12); + } else if (version === 2) { + width = 24; + if (bytes.length < width) rejectGpu("CCOB v2 header is truncated"); + total = bytes.readUInt32LE(8); + decoded = bytes.readUInt32LE(12); + hash = bytes.readBigUInt64LE(16); + } else if (version === 3) { + width = 32; + if (bytes.length < width) rejectGpu("CCOB v3 header is truncated"); + total = gpuCount(bytes.readBigUInt64LE(8)); + decoded = gpuCount(bytes.readBigUInt64LE(16)); + hash = bytes.readBigUInt64LE(24); + } else + throw new BinaryFormatFailure( + "unsupported", + `Unsupported CCOB version ${version}`, + ); + if (total < width || total > remaining) + rejectGpu("CCOB declared extent is invalid"); + return { version, method, width, total, decoded, hash }; +}; diff --git a/src/gpu/CudaFatbinary.ts b/src/gpu/CudaFatbinary.ts new file mode 100644 index 0000000000..f64d6f6569 --- /dev/null +++ b/src/gpu/CudaFatbinary.ts @@ -0,0 +1,81 @@ +import type { GpuObject } from "../domain/gpu/gpuCodeObjects.js"; +import { assertArtifactReadActive } from "../artifacts/ArtifactFileOperation.js"; +import { + gpuCount, + nextGpuContainerOffset, + rejectGpu, + type GpuSource, +} from "./GpuSource.js"; +import { gpuObject } from "./GpuObject.js"; +export async function* readCudaFatbinary( + source: GpuSource, + signal?: AbortSignal, +): AsyncGenerator { + let base = 0; + while (base < source.size) { + const header = await source.read(base, 16); + if (header.readUInt32LE(0) !== 0xba55ed50) + rejectGpu("Unexpected bytes between CUDA fatbinary records"); + if (header.readUInt16LE(4) !== 1 || header.readUInt16LE(6) < 16) + rejectGpu("Unsupported CUDA fatbinary header"); + const start = base + header.readUInt16LE(6), + length = gpuCount(header.readBigUInt64LE(8)); + if (start > source.size || length > source.size - start) + rejectGpu("CUDA fatbinary extent is truncated"); + const end = start + length; + for (let position = start; position < end;) { + assertArtifactReadActive(signal); + if (end - position < 64) rejectGpu("CUDA entry header is truncated"); + const h = await source.read(position, 64); + const kind = h.readUInt16LE(0), + version = h.readUInt16LE(2), + width = h.readUInt32LE(4); + const padded = gpuCount(h.readBigUInt64LE(8)), + encoded = h.readUInt32LE(16), + flags = h.readBigUInt64LE(40); + if ( + width < 64 || + width > end - position || + padded > end - position - width || + encoded > padded + ) + rejectGpu("CUDA entry extent is invalid"); + const compressed = (flags & 0xa000n) !== 0n; + const row = gpuObject(source, { + offset: position + width, + size: compressed ? encoded : padded, + kind: kind === 1 ? "ptx" : kind === 2 ? "cubin" : "unknown", + metadata: { offset: position, size: width }, + }); + row.target = `sm_${h.readUInt32LE(28)}${(flags & 0x100000n) !== 0n ? "a" : ""}`; + row.raw_flags = `0x${flags.toString(16)}`; + row.compression = + (flags & 0x8000n) !== 0n && (flags & 0x2000n) === 0n + ? "zstd" + : (flags & 0x2000n) !== 0n && (flags & 0x8000n) === 0n + ? "lz4" + : compressed + ? "unsupported" + : "none"; + row.compression_id = compressed ? `cuda-flags:${row.raw_flags}` : null; + row.decoded_size = compressed ? gpuCount(h.readBigUInt64LE(56)) : padded; + if (compressed && (encoded === 0 || row.decoded_size === 0)) + rejectGpu("Compressed CUDA entry has an empty declared extent"); + if (version !== 0x101) { + row.compression = "unsupported"; + row.target = null; + row.decoded_size = null; + row.limitations.push( + `CUDA entry version 0x${version.toString(16)} is not decoded`, + ); + } + if (row.compression === "unsupported") + row.limitations.push( + "This CUDA compression/version is not supported; original encoded bytes and target declarations remain located", + ); + yield row; + position += width + padded; + } + base = await nextGpuContainerOffset(source, end); + } +} diff --git a/src/gpu/CudaInfo.test.ts b/src/gpu/CudaInfo.test.ts new file mode 100644 index 0000000000..55c402110d --- /dev/null +++ b/src/gpu/CudaInfo.test.ts @@ -0,0 +1,29 @@ +import { expect, it } from "vitest"; +import { readCudaInfo, readCudaParameters } from "./CudaInfo.js"; +it("retains conflicting parameter-area declarations as an unknown aggregate", () => { + const read = readCudaParameters(Buffer.from("03190c0003191000", "hex"), true); + expect(read.parameter_bytes).toBeNull(); + expect(read.parameter_count).toBeNull(); + expect(read.limitations.join(" ")).toContain("12, 16"); +}); +it("retains observed argument ranges while reporting inconsistent area declarations", () => { + const read = readCudaParameters( + Buffer.from("0319040004170c00000000000000000000f02100", "hex"), + true, + ); + expect(read).toMatchObject({ + parameter_count: 1, + parameter_bytes: 4, + parameters: [{ offset: 0, size: 8 }], + }); + expect(read.limitations.join(" ")).toContain("disagree"); +}); +it("bounds dense numeric record streams without retaining a record array", () => { + const bytes = Buffer.alloc(4 * 262145); + for (let offset = 0; offset < bytes.length; offset += 4) bytes[offset] = 1; + expect(() => { + for (const record of readCudaInfo(bytes, true)) { + if (record.format !== 1) throw new Error("unexpected source format"); + } + }).toThrow(/aggregate record\/symbol visits/u); +}); diff --git a/src/gpu/CudaInfo.ts b/src/gpu/CudaInfo.ts new file mode 100644 index 0000000000..74a3128b8e --- /dev/null +++ b/src/gpu/CudaInfo.ts @@ -0,0 +1,142 @@ +import { + BinaryReader, + BinaryResourceFailure, +} from "../native/metadata/BinaryContainer.js"; +import { GPU_OBJECT_BYTES, GPU_REPORT_BYTES, rejectGpu } from "./GpuSource.js"; +import { jsonValueByteLength } from "../domain/jsonValueByteLength.js"; +import type { JsonValue } from "../domain/jsonValue.js"; + +/** Shared by every metadata record, symbol and temporary index in one CUDA ELF. */ +export class CudaMetadataBudget { + #visits = 0; + #readBytes = 0; + #indexBytes = 0; + visit(): void { + if (++this.#visits > 262144) + throw new BinaryResourceFailure( + "structure", + 262144, + "CUDA metadata exceeds 262144 aggregate record/symbol visits", + ); + } + read(size: number): void { + if (size > GPU_OBJECT_BYTES - this.#readBytes) + throw new BinaryResourceFailure( + "structure", + GPU_OBJECT_BYTES, + "CUDA metadata exceeds 128 MiB aggregate section reads", + ); + this.#readBytes += size; + } + retain(value: JsonValue): void { + const size = jsonValueByteLength(value, GPU_REPORT_BYTES) + 2; + if (size > GPU_REPORT_BYTES - this.#indexBytes) + throw new BinaryResourceFailure( + "structure", + GPU_REPORT_BYTES, + "CUDA metadata exceeds the 16 MiB temporary index budget", + ); + this.#indexBytes += size; + } +} +export interface CudaInfoRecord { + readonly format: number; + readonly attribute: number; + readonly value: number; + readonly offset: number; + readonly length: number; + readonly data: Buffer; +} +/** CUDA nv.info is a numeric record format, not a string-keyed metadata map. */ +export function* readCudaInfo( + bytes: Buffer, + little: boolean, + budget = new CudaMetadataBudget(), +): Generator { + const reader = new BinaryReader(bytes, little); + for (let offset = 0; offset < bytes.length;) { + budget.visit(); + const head = reader.range(offset, 4, "CUDA nv.info record"); + const format = head[0]!, + attribute = head[1]!, + value = reader.u16(offset + 2); + if (format < 1 || format > 4) + rejectGpu("Unknown CUDA nv.info record format"); + const length = format === 4 ? value : 0; + yield { + format, + attribute, + value, + offset, + length: 4 + length, + data: reader.range(offset + 4, length, "CUDA nv.info value"), + }; + offset += 4 + length; + } +} + +/** Kernel argument interpretation owns duplicate and range semantics at the numeric-record boundary. */ +export const readCudaParameters = ( + bytes: Buffer, + little: boolean, + budget = new CudaMetadataBudget(), +) => { + const parameters: import("../domain/gpu/gpuCodeObjects.js").GpuKernel["parameters"] = + []; + const byteDeclarations = new Set(); + const limitations: string[] = []; + let complete = true; + for (const record of readCudaInfo(bytes, little, budget)) { + if (record.format === 3 && record.attribute === 0x19) + byteDeclarations.add(record.value); + if (record.format !== 4 || record.attribute !== 0x17) continue; + if (record.data.length !== 12) { + complete = false; + continue; + } + const r = new BinaryReader(record.data, little); + if (r.u32(0) !== 0) complete = false; + const parameter = { + ordinal: r.u16(4), + name: null, + offset: r.u16(6), + size: r.u32(8) >>> 18, + type: null, + hidden: null, + }; + budget.retain(parameter); + parameters.push(parameter); + } + parameters.sort((a, b) => a.ordinal - b.ordinal); + if (parameters.some((row, index) => row.ordinal !== index)) complete = false; + const parameter_bytes = + byteDeclarations.size === 1 ? [...byteDeclarations][0]! : null; + if (byteDeclarations.size > 1) + limitations.push( + `Conflicting CUDA argument-area byte declarations: ${[...byteDeclarations].join(", ")}; parameter_bytes remains unknown`, + ); + if ( + parameter_bytes !== null && + parameters.some( + (row) => + row.offset! > parameter_bytes || + row.size! > parameter_bytes - row.offset!, + ) + ) + limitations.push( + "CUDA parameter ranges disagree with the declared argument-area length; declarations do not establish a valid ABI layout", + ); + if (!complete) + limitations.push( + "CUDA parameter records have unsupported indices/sizes, duplicate ordinals or gaps; parameter count is unknown", + ); + return { + parameter_count: + complete && (parameters.length > 0 || parameter_bytes === 0) + ? parameters.length + : null, + parameter_bytes, + parameters, + limitations, + }; +}; diff --git a/src/gpu/CudaTarget.ts b/src/gpu/CudaTarget.ts new file mode 100644 index 0000000000..b8f24d231e --- /dev/null +++ b/src/gpu/CudaTarget.ts @@ -0,0 +1,46 @@ +import type { NativeSectionDirectory } from "../native/metadata/NativeSectionDirectory.js"; +import { gpuLocation, type GpuPayloadTarget } from "./GpuObject.js"; +import { rejectGpu, type GpuSource } from "./GpuSource.js"; + +/** ABI-qualified e_flags layouts, checked against NVIDIA producer/cuobjdump oracles. */ +export const readCudaTarget = ( + source: GpuSource, + directory: NativeSectionDirectory, +): GpuPayloadTarget => { + if ( + directory.format !== "elf" || + directory.machine !== 190 || + directory.elf_os_abi === null + ) + return rejectGpu("CUDA target declaration requires a CUDA ELF header"); + const flags = directory.flags; + const legacy = directory.elf_os_abi === 0x33; + const newer = + directory.elf_os_abi === 0x41 && + directory.bits === 64 && + directory.little && + (flags & 0x00ff00ff) === 2; + const architecture = legacy + ? flags & 0xff + : newer + ? (flags >>> 8) & 0xff + : null; + const value = + architecture !== null && architecture >= 10 ? `sm_${architecture}` : null; + const cudaHeader = { + os_abi: directory.elf_os_abi, + flags: `0x${flags.toString(16).padStart(8, "0")}`, + location: gpuLocation(source, 0, directory.bits === 64 ? 64 : 52), + }; + return { + value, + key: value, + scope: "cuda-base", + cudaHeader, + ...(value === null + ? { + limitation: `CUDA ELF target encoding is unknown for OS ABI 0x${directory.elf_os_abi?.toString(16)} and flags ${cudaHeader.flags}; the raw header remains available.`, + } + : {}), + }; +}; diff --git a/src/gpu/GpuCodeObjectsProvider.ts b/src/gpu/GpuCodeObjectsProvider.ts new file mode 100644 index 0000000000..b2107a281a --- /dev/null +++ b/src/gpu/GpuCodeObjectsProvider.ts @@ -0,0 +1,334 @@ +import { lstat, type FileHandle } from "node:fs/promises"; +import type { GpuCodeObjectsPort } from "../application/gpu/GpuCodeObjectsPort.js"; +import { + createGpuCodeObjectsEvidence, + createGpuCodeObjectsPartialEvidence, +} from "../application/gpu/GpuCodeObjectsEvidence.js"; +import type { ExecutionOptions } from "../application/AnalysisProvider.js"; +import type { + GpuCodeObjects, + GpuCodeObjectsInput, +} from "../domain/gpu/gpuCodeObjects.js"; +import { + gpuCodeObjectsSchema, + gpuCodeObjectsPartialSchema, +} from "../domain/gpu/gpuCodeObjects.js"; +import { AnalysisError } from "../domain/analysisErrorBase.js"; +import { + AnalysisArtifactChangedError, + AnalysisAccessDeniedError, + AnalysisCancelledError, + AnalysisResourceConstraintError, + AnalysisUnsupportedTargetError, +} from "../domain/analysisErrorCore.js"; +import { BinaryTargetError } from "../domain/configurationErrors.js"; +import { ProviderAdapterError } from "../domain/providerAdapterError.js"; +import { err, ok, type Result } from "../domain/result.js"; +import { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js"; +import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js"; +import { + withArtifactRegularFile, + bindArtifactFileCleanup, + artifactFilePrimaryFailure, + artifactFileCancellationCause, + ArtifactFileCancelled, + ArtifactFileCleanupFailure, +} from "../artifacts/ArtifactFileOperation.js"; +import { analysisErrorWithCleanupFailure } from "../domain/analysisErrorCleanup.js"; +import { + BinaryFormatFailure, + BinaryResourceFailure, +} from "../native/metadata/BinaryContainer.js"; +import { + GpuFileSource, + gpuCount, + GPU_OBJECT_BYTES, + GPU_REPORT_BYTES, +} from "./GpuSource.js"; +import { GpuInventory } from "./GpuInventory.js"; +import { ProviderCleanupError } from "../domain/providerCleanupError.js"; +import { sameRegularFileState } from "../filesystem/RegularFile.js"; + +export const GPU_CODE_OBJECTS_PROVIDER_IDENTITY = { + id: "gpu-code-objects", + name: "REA static GPU code-object reader", + version: "1", +} as const; +const OPERATION = "inspect_gpu_code_objects"; + +/** One admitted descriptor and bounded report at a time; retained cleanup blocks new work. */ +export class GpuCodeObjectsProvider implements GpuCodeObjectsPort { + readonly identity = GPU_CODE_OBJECTS_PROVIDER_IDENTITY; + readonly #resources = new ArtifactResourceScope(); + #busy = false; + + async close(): Promise { + try { + await this.#resources.close(); + } catch (cause) { + const error = new ProviderCleanupError( + this.identity.id, + this.#resources.cleanupResources, + {}, + { cause, operation: OPERATION }, + ); + bindArtifactFileCleanup(cause, error); + throw error; + } + } + + async inspect( + input: GpuCodeObjectsInput, + options?: ExecutionOptions, + ): Promise> { + if (this.#busy) + return err( + new AnalysisResourceConstraintError( + OPERATION, + "capacity", + "One GPU inspection is already active", + { concurrent_inspections: 1 }, + { retryable: true }, + ), + ); + this.#busy = true; + let progress: { inventory: GpuInventory; size: number } | undefined; + try { + return ok( + await this.#resources.run(() => + withArtifactRegularFile( + input.path, + this.#resources, + options?.signal, + (file) => + this.#read(file, input, options?.signal, (value) => { + progress = value; + }), + ), + ), + ); + } catch (cause) { + const primary = artifactFilePrimaryFailure(cause); + let error: AnalysisError; + if (primary instanceof AnalysisError) error = primary; + else if (primary instanceof BinaryResourceFailure) + error = new AnalysisResourceConstraintError( + OPERATION, + "memory", + primary.message, + { + maximum_object_bytes: GPU_OBJECT_BYTES, + maximum_report_bytes: GPU_REPORT_BYTES, + }, + { cause, retryable: false }, + ); + else if (primary instanceof BinaryFormatFailure) { + error = + primary.kind === "unsupported" + ? new AnalysisUnsupportedTargetError( + OPERATION, + input.path, + primary.message, + { cause }, + ) + : new BinaryTargetError(input.path, primary.message, { + cause, + constraint: "file_format_rejected", + }); + } else if ( + primary instanceof ArtifactReaderFailure && + primary.reason === "cancelled" + ) + error = new AnalysisCancelledError(OPERATION, { + cause: artifactFileCancellationCause(primary), + }); + else if (primary instanceof ArtifactFileCancelled) + error = new AnalysisCancelledError(OPERATION, { + cause: primary.fileCancellation.reason, + }); + else if ( + primary instanceof ArtifactReaderFailure && + primary.reason === "integrity" + ) + error = new AnalysisArtifactChangedError( + OPERATION, + input.path, + primary.message, + { cause }, + ); + else if ( + primary instanceof Error && + "code" in primary && + (primary.code === "EACCES" || primary.code === "EPERM") + ) + error = new AnalysisAccessDeniedError( + OPERATION, + input.path, + primary.code, + { cause: primary }, + ); + else if ( + primary instanceof ArtifactReaderFailure && + primary.reason === "limit" + ) + error = new AnalysisResourceConstraintError( + OPERATION, + "memory", + primary.message, + {}, + { cause: primary }, + ); + else if (primary instanceof Error && "code" in primary) + error = new BinaryTargetError(input.path, primary.message, { cause }); + else + error = new ProviderAdapterError(this.identity.id, OPERATION, { + cause, + diagnostics: { + path: input.path, + reason: + primary instanceof Error ? primary.message : String(primary), + }, + }); + if ( + cause instanceof ArtifactReaderFailure && + cause.cleanup !== undefined + ) { + const cleanup = new ProviderCleanupError( + this.identity.id, + cause.cleanup.resources, + { reason: cause.cleanup.reason }, + { cause, operation: OPERATION }, + ); + error = analysisErrorWithCleanupFailure(error, cleanup, OPERATION); + } + const completed = + cause instanceof ArtifactFileCleanupFailure && + cause.fileCleanupFailure.outcome.kind === "completed" + ? cause.fileCleanupFailure.outcome.value + : cause instanceof ArtifactFileCancelled + ? cause.fileCancellation.outcome.value + : undefined; + if (completed !== undefined) { + // The descriptor owner retains observations even when its final close fails. + const report = gpuCodeObjectsSchema.safeParse(completed); + if (report.success) + error.retainPartialObservation( + createGpuCodeObjectsEvidence(report.data, this.identity, input), + ); + } + if ( + error.partialObservation === undefined && + progress !== undefined && + progress.inventory.objects.length > 0 + ) { + const partial = gpuCodeObjectsPartialSchema.parse({ + kind: "gpu-code-objects-partial", + artifact: { + path: input.path, + observed_file_size: progress.size, + sha256: null, + file_identity: "unverified", + }, + analysis: input.analysis, + objects: progress.inventory.objects, + kernels: progress.inventory.kernels, + ptx_artifacts: progress.inventory.ptx_artifacts, + coverage: { discovery: "partial", payloads: "partial" }, + limitations: [ + ...progress.inventory.limitations, + "Only completed declarations observed before interruption are retained. Root file identity and completeness were not verified; positional IDs are scoped to this interrupted observation.", + ], + }); + error.retainPartialObservation( + createGpuCodeObjectsPartialEvidence(partial, this.identity, input), + ); + } + bindArtifactFileCleanup(cause, error); + return err(error); + } finally { + this.#busy = false; + } + } + + async #read( + file: FileHandle, + input: GpuCodeObjectsInput, + signal?: AbortSignal, + retainProgress?: (progress: { + inventory: GpuInventory; + size: number; + }) => void, + ): Promise { + const initial = await file.stat({ bigint: true }); + const size = gpuCount(initial.size); + const source = new GpuFileSource(file, size, signal); + const sha256 = await source.digest(); + if (input.expected_sha256 !== undefined && sha256 !== input.expected_sha256) + throw new AnalysisArtifactChangedError( + OPERATION, + input.path, + "GPU source does not match the inventory's selected SHA-256", + ); + const inventory = new GpuInventory(input, signal); + retainProgress?.({ inventory, size }); + const format = await inventory.inspect(source); + const after = await file.stat({ bigint: true }); + const named = await lstat(input.path, { bigint: true }).catch( + (cause: unknown) => { + if ( + cause instanceof Error && + "code" in cause && + ["ENOENT", "ENOTDIR", "ELOOP"].includes(String(cause.code)) + ) + throw new ArtifactReaderFailure( + "integrity", + "Selected GPU path changed during inspection", + { cause }, + ); + throw cause; + }, + ); + if ([after, named].some((state) => !sameRegularFileState(initial, state))) + throw new ArtifactReaderFailure( + "integrity", + "GPU source changed during inspection", + ); + const leaves = inventory.objects.filter( + (row) => + row.kind !== "cuda-fatbin" && + row.kind !== "hip-bundle" && + row.kind !== "native-section" && + row.kind !== "host", + ); + return gpuCodeObjectsSchema.parse({ + artifact: { + path: input.path, + file_size: size, + sha256, + bytes_read: source.bytesRead, + }, + format, + analysis: input.analysis, + objects: inventory.objects, + kernels: inventory.kernels, + ptx_artifacts: inventory.ptx_artifacts, + coverage: { + discovery_scope: "supported-section-names-and-standalone-formats", + discovery: inventory.discovery, + payloads: + input.analysis === "inventory" + ? "not-requested" + : leaves.every((row) => row.inspection === "complete") && + inventory.discovery === "complete" + ? "complete" + : "partial", + examined_sections: inventory.examined_sections, + unknown_section_names: inventory.unknown_section_names, + }, + limitations: [ + ...inventory.limitations, + "Full-file SHA-256 streams every file byte; section and selected payload reads add to artifact.bytes_read. Retained Evidence is historical and never reopens the target.", + ], + }); + } +} diff --git a/src/gpu/GpuElfKernels.ts b/src/gpu/GpuElfKernels.ts new file mode 100644 index 0000000000..6b1caacbcb --- /dev/null +++ b/src/gpu/GpuElfKernels.ts @@ -0,0 +1,491 @@ +import { readCudaTarget } from "./CudaTarget.js"; +import type { GpuPayloadTarget } from "./GpuObject.js"; +import { err, ok, type Result } from "../domain/result.js"; +import { + CudaMetadataBudget, + readCudaInfo, + readCudaParameters, +} from "./CudaInfo.js"; +import type { GpuKernel, GpuLocation } from "../domain/gpu/gpuCodeObjects.js"; +import { + BinaryReader, + BinaryFormatFailure, + BinaryResourceFailure, +} from "../native/metadata/BinaryContainer.js"; +import { + readElfSectionDirectory, + type NativeSection, + type NativeSectionDirectory, +} from "../native/metadata/NativeSectionDirectory.js"; +import { readElfString, readElfSymbol } from "../native/metadata/ElfSymbol.js"; +import { + gpuCount, + readGpuNativeDirectory, + rejectGpu, + type GpuSource, +} from "./GpuSource.js"; +import { + AMD_METADATA_BYTES, + amdCount, + amdRecord, + amdString, + readAmdMetadata, +} from "./AmdGpuMetadata.js"; +const sectionBytes = ( + source: GpuSource, + section: NativeSection, + budget?: CudaMetadataBudget, +): Promise => { + if (!section.file_backed || (section.flags & 0x800n) !== 0n) + rejectGpu("GPU metadata section has no supported uncompressed file range"); + budget?.read(gpuCount(section.size)); + return source.read(gpuCount(section.offset), gpuCount(section.size)); +}; +const site = ( + source: GpuSource, + section: NativeSection, + offset = 0, + size = gpuCount(section.size), +): GpuLocation => ({ + container_id: source.containerId, + offset: gpuCount(section.offset) + offset, + size, + section_index: section.index, + section_name: section.name, +}); +const blankKernel = ( + source: GpuSource, + id: string, + name: string, + location: GpuLocation, +): GpuKernel => ({ + id: `${source.containerId!}/${id}`, + object_id: source.containerId!, + representation: "cuda-elf-metadata", + name, + symbol: name, + parameter_count: null, + parameter_bytes: null, + parameters: [], + registers: { + physical: null, + sgpr: null, + vgpr: null, + agpr: null, + virtual: [], + }, + shared_bytes: null, + private_bytes: null, + location, + sources: { parameters: null, registers: null, shared: null, private: null }, + limitations: [], +}); +export const readGpuElfKernels = async ( + source: GpuSource, + kind: "cubin" | "amdgpu", + onKernel: (kernel: GpuKernel) => void, + preparedAmd?: AmdElfMetadata, + onTarget?: (target: GpuPayloadTarget) => void, +): Promise<{ + limitations: string[]; + coverage: "complete" | "partial"; +}> => { + if (kind === "amdgpu" && preparedAmd !== undefined) + return amdKernels(source, preparedAmd, onKernel, onTarget); + const directory = await readGpuNativeDirectory( + source, + readElfSectionDirectory(source.size), + ); + if (directory.machine !== (kind === "cubin" ? 190 : 224)) + rejectGpu("GPU entry kind disagrees with ELF machine"); + if (kind === "cubin") { + onTarget?.(readCudaTarget(source, directory)); + const coverage = await cudaKernels(source, directory, onKernel); + return { + coverage, + limitations: [ + ...(directory.sections.some((s) => s.name === null) + ? [ + "Some ELF section names are unknown; named metadata coverage is partial", + ] + : []), + ...(directory.sections.some((s) => s.type === 2 || s.type === 11) + ? [] + : [ + "No CUBIN symbol table was available; kernel declaration coverage is unknown", + ]), + ], + }; + } + const metadata = await readAmdElfMetadata(source, directory); + const parsed = amdKernels( + source, + metadata.ok ? metadata.value : metadata.error.partial, + onKernel, + onTarget, + ); + if (!metadata.ok) throw metadata.error.cause; + return parsed; +}; +const indexCudaMetadata = async ( + source: GpuSource, + directory: NativeSectionDirectory, +) => { + const infosByText = new Map(), + sharedByText = new Map(); + const registerFacts = new Map>(); + const budget = new CudaMetadataBudget(), + seenMetadata = new Set(); + for (const row of directory.sections) { + const map = + row.type === 8 && row.name?.startsWith(".nv.shared.") === true + ? sharedByText + : row.type === 0x70000000 && row.name?.startsWith(".nv.info.") === true + ? infosByText + : undefined; + const global = row.type === 0x70000000 && row.name === ".nv.info"; + if (map === undefined && !global) continue; + // The same scoped byte declaration may be aliased by many ELF headers. + const key = `${row.type}:${row.flags}:${row.name}:${row.offset}:${row.size}:${row.link}:${row.info}`; + if (seenMetadata.has(key)) continue; + budget.retain(key); + seenMetadata.add(key); + if (map !== undefined) { + budget.retain({ text: row.info, section: row.index }); + const values = map.get(row.info) ?? []; + values.push(row); + map.set(row.info, values); + } + if (global) { + for (const record of readCudaInfo( + await sectionBytes(source, row, budget), + directory.little, + budget, + )) { + if (record.format !== 4 || record.attribute !== 0x2f) continue; + if (record.data.length !== 8) + rejectGpu("CUDA register count record has an unsupported extent"); + const r = new BinaryReader(record.data, directory.little), + key = `${row.link}:${r.u32(0)}`; + const values = registerFacts.get(key) ?? new Map(); + const value = r.u32(4); + if (!values.has(value)) { + const location = site(source, row, record.offset, record.length); + budget.retain({ symbol: key, value, location }); + values.set(value, location); + registerFacts.set(key, values); + } + } + } + } + const allNamesKnown = directory.sections.every((row) => row.name !== null); + return { + source, + directory, + infosByText, + sharedByText, + registerFacts, + allNamesKnown, + budget, + parameters: new Map>(), + }; +}; +type CudaMetadataIndex = Awaited>; + +const attachCudaResources = ( + kernel: GpuKernel, + text: NativeSection, + index: CudaMetadataIndex, + symbolKey: string, +): void => { + const { source, directory } = index; + const registerRows = index.registerFacts.get(symbolKey); + const registerValues = new Set(registerRows?.keys()); + const sectionRegisters = text.info >>> 24; + kernel.registers.physical = + registerValues.size === 1 + ? [...registerValues][0]! + : registerValues.size === 0 && sectionRegisters !== 0 + ? sectionRegisters + : null; + if (registerValues.size === 1) + kernel.sources.registers = registerRows!.values().next().value!; + else if (sectionRegisters !== 0) + kernel.sources.registers = { + container_id: source.containerId, + offset: text.header_offset, + size: directory.bits === 64 ? 64 : 40, + section_index: text.index, + section_name: text.name, + }; + if ( + registerValues.size > 1 || + (kernel.registers.physical !== null && + sectionRegisters !== 0 && + kernel.registers.physical !== sectionRegisters) + ) { + kernel.registers.physical = null; + kernel.limitations.push( + "Conflicting CUDA register declarations; physical count remains unknown", + ); + } + const shared = index.sharedByText.get(text.index) ?? []; + kernel.shared_bytes = + shared.length === 1 + ? gpuCount(shared[0]!.size) + : shared.length === 0 && index.allNamesKnown + ? 0 + : null; + if (shared.length === 1) + kernel.sources.shared = { + container_id: source.containerId, + offset: shared[0]!.header_offset, + size: directory.bits === 64 ? 64 : 40, + section_index: shared[0]!.index, + section_name: shared[0]!.name, + }; +}; + +const cudaKernels = async ( + source: GpuSource, + directory: NativeSectionDirectory, + onKernel: (kernel: GpuKernel) => void, +): Promise<"complete" | "partial"> => { + const index = await indexCudaMetadata(source, directory); + let complete = index.allNamesKnown; + let symbolTables = 0; + for (const section of directory.sections) { + if (section.type !== 2 && section.type !== 11) continue; + symbolTables++; + const width = gpuCount(section.entry_size), + minimum = directory.bits === 64 ? 24 : 16; + if (width < minimum || section.size % BigInt(width) !== 0n) + rejectGpu("CUBIN symbol table width is invalid"); + const namesSection = directory.sections[section.link]; + if (namesSection === undefined || namesSection.type !== 3) + rejectGpu("CUBIN symbol string table is invalid"); + const names = await sectionBytes(source, namesSection!, index.budget); + const bytes = await sectionBytes(source, section, index.budget); + for (let offset = 0; offset < bytes.length; offset += width) { + index.budget.visit(); + const symbol = readElfSymbol( + bytes.subarray(offset, offset + width), + directory.bits, + directory.little, + ); + // STO_CUDA_ENTRY distinguishes launchable kernels from ordinary device functions. + if (symbol.type !== 2 || (symbol.other & 0x10) === 0) continue; + const text = directory.sections[symbol.section_index]; + if (text === undefined || text.type !== 1 || (text.flags & 4n) === 0n) + rejectGpu("CUBIN kernel symbol has no executable section"); + const kernel = blankKernel( + source, + `sym:${section.index}:${offset / width}`, + readElfString(names, symbol.name_offset), + site(source, section, offset, width), + ); + attachCudaResources( + kernel, + text!, + index, + `${section.index}:${offset / width}`, + ); + const infos = index.infosByText.get(text!.index) ?? []; + if (infos.length === 1) { + const info = infos[0]!; + let decoded = index.parameters.get(info.index); + if (decoded === undefined) { + decoded = readCudaParameters( + await sectionBytes(source, info, index.budget), + directory.little, + index.budget, + ); + index.budget.retain({ parameter_section: info.index }); + index.parameters.set(info.index, decoded); + } + if (decoded.limitations.length > 0) complete = false; + kernel.sources.parameters = site(source, infos[0]!); + kernel.parameter_bytes = decoded.parameter_bytes; + kernel.parameter_count = decoded.parameter_count; + kernel.parameters = decoded.parameters; + kernel.limitations.push(...decoded.limitations); + } else { + complete = false; + kernel.limitations.push( + "Exactly one matching CUDA nv.info parameter section was not available", + ); + } + kernel.limitations.push( + "CUDA metadata fields are declarations from ELF symbols, section headers and nv.info; SASS and runtime behavior were not inspected", + ); + onKernel(kernel); + } + } + return complete && symbolTables > 0 ? "complete" : "partial"; +}; +export interface AmdElfMetadata { + readonly target: string | null; + readonly target_key: string | null; + readonly limitations: string[]; + readonly notes: { + section: NativeSection; + noteOffset: number; + desc: number; + size: number; + kernels: unknown[]; + }[]; +} +/** One bounded parsed owner for target discovery and later selected kernel projection. */ +export const readAmdElfMetadata = async ( + source: GpuSource, + directory: NativeSectionDirectory, +): Promise< + Result< + AmdElfMetadata, + { kind: "incomplete-amd-metadata"; cause: unknown; partial: AmdElfMetadata } + > +> => { + const targets = new Set(), + limitations: string[] = []; + const notes: AmdElfMetadata["notes"] = []; + let retainedBytes = 0; + const observed = (): AmdElfMetadata => { + const target = targets.size === 1 ? [...targets][0]! : null; + const target_key = + target === null + ? null + : (/^amdgcn-amd-amdhsa--(gfx[a-zA-Z0-9]+(?::[^\s]+)?)$/u.exec( + target, + )?.[1] ?? null); + return { target, target_key, limitations, notes }; + }; + try { + if (directory.machine !== 224) + rejectGpu("AMDGPU metadata requires an AMDGPU ELF machine"); + for (const section of directory.sections) { + if (section.type !== 7) continue; + const sectionSize = gpuCount(section.size); + if (sectionSize > AMD_METADATA_BYTES - retainedBytes) + throw new BinaryResourceFailure( + "structure", + AMD_METADATA_BYTES, + "AMDGPU ELF note discovery exceeds the 8 MiB retained metadata budget", + ); + retainedBytes += sectionSize; + const bytes = await sectionBytes(source, section), + r = new BinaryReader(bytes, directory.little); + for (let p = 0; p < bytes.length;) { + if (bytes.length - p < 12) rejectGpu("Truncated ELF note header"); + const namesize = r.u32(p), + size = r.u32(p + 4), + type = r.u32(p + 8); + const nameOffset = p + 12, + desc = nameOffset + Math.ceil(namesize / 4) * 4, + end = desc + Math.ceil(size / 4) * 4; + r.range(nameOffset, namesize, "ELF note vendor"); + r.range(desc, size, "ELF note descriptor"); + if (end > bytes.length) + rejectGpu("ELF note alignment extends outside its section"); + const vendor = bytes + .subarray(nameOffset, nameOffset + namesize) + .toString("latin1") + .replace(/\0$/u, ""); + if (vendor === "AMDGPU" && type === 32) { + const metadata = amdRecord( + readAmdMetadata(bytes.subarray(desc, desc + size)), + ); + const kernels = metadata?.["amdhsa.kernels"]; + if (!Array.isArray(kernels)) + return rejectGpu("AMDGPU metadata has no valid kernel array"); + const target = amdString(metadata!["amdhsa.target"]); + if (target !== null) targets.add(target); + notes.push({ section, noteOffset: p, desc, size, kernels }); + } else if (vendor === "AMD" && (type === 10 || type === 11)) + limitations.push( + "Legacy AMDGPU YAML metadata is outside MessagePack coverage", + ); + p = end; + } + } + if (notes.length === 0) + throw new BinaryFormatFailure( + "unsupported", + "No AMDGPU v3+ MessagePack kernel metadata was available", + ); + return ok(observed()); + } catch (cause) { + return err({ kind: "incomplete-amd-metadata", cause, partial: observed() }); + } +}; + +const amdKernels = ( + source: GpuSource, + prepared: AmdElfMetadata, + onKernel: (kernel: GpuKernel) => void, + onTarget?: (target: GpuPayloadTarget) => void, +): { limitations: string[]; coverage: "complete" } => { + onTarget?.({ + value: prepared.target, + key: prepared.target_key, + scope: "exact", + }); + for (const { + section, + noteOffset, + desc, + size, + kernels: rows, + } of prepared.notes) { + for (let index = 0; index < rows.length; index++) { + const value = amdRecord(rows[index]); + const name = value === null ? null : amdString(value[".name"]); + if (value === null || name === null) + rejectGpu("AMDGPU kernel metadata has no name"); + const kernel = blankKernel( + source, + `note:${section.index}:${noteOffset}:${index}`, + name!, + site(source, section, desc, size), + ); + kernel.representation = "amdgpu-metadata"; + kernel.sources = { + parameters: kernel.location, + registers: kernel.location, + shared: kernel.location, + private: kernel.location, + }; + kernel.symbol = amdString(value![".symbol"]); + kernel.parameter_bytes = amdCount(value![".kernarg_segment_size"]); + kernel.shared_bytes = amdCount(value![".group_segment_fixed_size"]); + kernel.private_bytes = amdCount(value![".private_segment_fixed_size"]); + kernel.registers.sgpr = amdCount(value![".sgpr_count"]); + kernel.registers.vgpr = amdCount(value![".vgpr_count"]); + kernel.registers.agpr = amdCount(value![".agpr_count"]); + const args = value![".args"]; + if (Array.isArray(args)) { + kernel.parameter_count = args.length; + kernel.parameters = args.map((argument, ordinal) => { + const row = amdRecord(argument); + if (row === null) rejectGpu("Malformed AMDGPU argument metadata"); + const kind = amdString(row![".value_kind"]); + return { + ordinal, + name: amdString(row![".name"]), + offset: amdCount(row![".offset"]), + size: amdCount(row![".size"]), + type: amdString(row![".type_name"]), + hidden: kind === null ? null : kind.startsWith("hidden_"), + }; + }); + } + kernel.limitations.push( + "AMDGPU parameter count includes hidden ABI arguments when present; missing fields remain unknown. Kernel metadata is a declaration, not runtime verification.", + ); + onKernel(kernel); + } + } + return { + limitations: prepared.limitations, + coverage: "complete", + }; +}; diff --git a/src/gpu/GpuInventory.test.ts b/src/gpu/GpuInventory.test.ts new file mode 100644 index 0000000000..e98be43042 --- /dev/null +++ b/src/gpu/GpuInventory.test.ts @@ -0,0 +1,57 @@ +import { expect, it } from "vitest"; +import { GpuInventory } from "./GpuInventory.js"; +import { GpuBufferSource } from "./GpuSource.js"; +import { + ccobFixture, + cubinFixture, + cudaFixture, +} from "../../tests/fixtures/gpu/codeObjects.js"; + +it.each(["cuda", "ccob"] as const)( + "refuses oversized %s output before reading compressed input", + async (kind) => { + const bytes = + kind === "cuda" + ? cudaFixture("zstd", [{ kind: 2, target: 80, bytes: cubinFixture() }]) + : ccobFixture(3); + const payloadOffset = kind === "cuda" ? 80 : 32; + bytes.writeBigUInt64LE( + BigInt(128 * 1024 * 1024 + 1), + kind === "cuda" ? 72 : 16, + ); + const source = new GpuBufferSource(bytes, "fixture"), + reads: number[] = [], + inventory = new GpuInventory({ path: "/fixture", analysis: "kernels" }); + await inventory.inspect({ + size: source.size, + containerId: source.containerId, + read: async (offset, size) => { + reads.push(offset); + return source.read(offset, size); + }, + }); + expect(reads).not.toContain(payloadOffset); + expect(inventory.objects[0]).toMatchObject({ + inspection: "capacity", + sha256: null, + }); + expect(inventory.kernels).toEqual([]); + }, +); +it("observes cancellation while walking flat empty bundles in a decoded buffer", async () => { + const empty = Buffer.alloc(32); + empty.write("__CLANG_OFFLOAD_BUNDLE__"); + const bytes = Buffer.concat(Array.from({ length: 4096 }, () => empty)); + const controller = new AbortController(), + reason = { selected: "cancel-buffer-walk" }; + const inventory = new GpuInventory( + { path: "/fixture", analysis: "inventory" }, + controller.signal, + ); + const result = inventory.inspect(new GpuBufferSource(bytes, "fixture")); + setImmediate(() => controller.abort(reason)); + await expect(result).rejects.toMatchObject({ + reason: "cancelled", + cause: reason, + }); +}); diff --git a/src/gpu/GpuInventory.ts b/src/gpu/GpuInventory.ts new file mode 100644 index 0000000000..cebc7a06c9 --- /dev/null +++ b/src/gpu/GpuInventory.ts @@ -0,0 +1,575 @@ +import { + CLANG_BUNDLE_MAGIC, + readClangBundleDirectory, + readCcobHeader, +} from "./ClangOffloadBundle.js"; +import { gpuObject, gpuLocation, type GpuPayloadTarget } from "./GpuObject.js"; +import { readCudaTarget } from "./CudaTarget.js"; +import { readCudaFatbinary } from "./CudaFatbinary.js"; +import { decodeGpuPayload } from "./GpuPayloadCodec.js"; +import { createHash } from "node:crypto"; +import { scheduler } from "node:timers/promises"; +import type { + GpuCodeObjectsInput, + GpuCodeObjects, + GpuObject, + GpuKernel, +} from "../domain/gpu/gpuCodeObjects.js"; +import { + BinaryFormatFailure, + BinaryResourceFailure, +} from "../native/metadata/BinaryContainer.js"; +import { + readElfSectionDirectory, + readPeSectionDirectory, + type NativeSectionDirectory, +} from "../native/metadata/NativeSectionDirectory.js"; +import { assertArtifactReadActive } from "../artifacts/ArtifactFileOperation.js"; +import { jsonValueByteLength } from "../domain/jsonValueByteLength.js"; +import type { JsonValue } from "../domain/jsonValue.js"; +import { + GpuBufferSource, + checkGpuRange, + GPU_OBJECT_BYTES, + GPU_REPORT_BYTES, + GpuReportCapacityFailure, + gpuCount, + nextGpuContainerOffset, + gpuSha256, + readGpuNativeDirectory, + rejectGpu, + type GpuSource, +} from "./GpuSource.js"; +import { + readGpuElfKernels, + readAmdElfMetadata, + type AmdElfMetadata, +} from "./GpuElfKernels.js"; +import { readPtxKernels, readPtxTarget } from "./PtxKernels.js"; + +/** File declarations only. Every child range stays in its named parent coordinate space. */ +export class GpuInventory { + readonly objects: GpuObject[] = []; + readonly kernels: GpuKernel[] = []; + readonly ptx_artifacts: GpuCodeObjects["ptx_artifacts"] = []; + readonly limitations = [ + "Static declarations only; no GPU execution, SASS disassembly, host launch correlation or runtime compatibility was verified.", + "Object IDs are positional identities scoped to artifact.sha256. Locations inside decoded objects name their containing object; they are not file offsets.", + ]; + discovery: "complete" | "partial" = "complete"; + examined_sections = 0; + unknown_section_names = 0; + #factBytes = 0; + #workBytes = 0; + readonly #outerTargets = new WeakMap(); + readonly #selectedObjects: ReadonlySet | undefined; + readonly #selectedTargets: ReadonlySet | undefined; + constructor( + readonly input: GpuCodeObjectsInput, + readonly signal?: AbortSignal, + ) { + this.#selectedObjects = + input.object_ids === undefined ? undefined : new Set(input.object_ids); + this.#selectedTargets = + input.targets === undefined ? undefined : new Set(input.targets); + this.#retain({ path: input.path, limitations: this.limitations }); + this.#factBytes += 1024; + } + #retain(value: JsonValue): void { + const bytes = jsonValueByteLength(value, GPU_REPORT_BYTES); + if (bytes + 2 > GPU_REPORT_BYTES - this.#factBytes) + throw new GpuReportCapacityFailure(); + this.#factBytes += bytes + 2; + } + #add(row: GpuObject): void { + if (row.raw_flags !== null || row.bundle_id !== null) + this.#outerTargets.set(row, row.target); + this.#retain(row); + this.#retain(" ".repeat(32)); // Reserve all later inspection-state changes. + this.objects.push(row); + } + #decoded(row: GpuObject, sha256: string, size: number): void { + this.#retain({ sha256, decoded_size: size }); + row.sha256 = sha256; + row.decoded_size = size; + } + #objectLimitations(row: GpuObject, messages: string[]): void { + this.#retain(messages); + row.limitations.push(...messages); + } + /** Reserve input/output work atomically, before any payload allocation or read. */ + #budgetWork(...sizes: number[]): void { + const total = sizes.reduce((sum, size) => sum + size, 0); + if ( + sizes.some((size) => size > GPU_OBJECT_BYTES) || + total > 512 * 1024 * 1024 - this.#workBytes + ) + throw new BinaryResourceFailure( + "structure", + GPU_OBJECT_BYTES, + "GPU reads/decoding exceed 128 MiB per buffer or 512 MiB per inspection", + ); + this.#workBytes += total; + } + #selected(row: GpuObject): boolean { + return ( + (this.#selectedObjects === undefined || + this.#selectedObjects.has(row.id)) && + (this.#selectedTargets === undefined || + (row.target !== null && this.#selectedTargets.has(row.target))) + ); + } + async inspect( + source: GpuSource, + depth = 0, + ): Promise { + assertArtifactReadActive(this.signal); + if (depth > 8) + throw new BinaryResourceFailure( + "structure", + 8, + "GPU container nesting exceeds eight levels", + ); + const prefix = await source.read(0, Math.min(32, source.size)); + if (prefix.subarray(0, 4).equals(Buffer.from([0x50, 0xed, 0x55, 0xba]))) { + await this.#cuda(source); + return "cuda-fatbin"; + } + if ( + prefix.subarray(0, CLANG_BUNDLE_MAGIC.length).equals(CLANG_BUNDLE_MAGIC) + ) { + await this.#hip(source, depth); + return "hip-bundle"; + } + if (prefix.subarray(0, 4).toString("ascii") === "CCOB") { + await this.#hip(source, depth); + return "hip-bundle"; + } + if ( + prefix.subarray(0, 4).equals(Buffer.from([0x7f, 0x45, 0x4c, 0x46])) || + prefix.subarray(0, 2).toString("ascii") === "MZ" + ) { + const elf = prefix[0] === 0x7f; + const directory = await readGpuNativeDirectory( + source, + elf + ? readElfSectionDirectory(source.size) + : readPeSectionDirectory(source.size), + ); + if (elf && (directory.machine === 190 || directory.machine === 224)) { + const row = gpuObject(source, { + offset: 0, + size: source.size, + kind: directory.machine === 190 ? "cubin" : "amdgpu", + }); + this.#add(row); + if (directory.machine === 190) { + this.#target(row, readCudaTarget(source, directory)); + } + if (directory.machine === 224) + await this.#amdPayload(source, row, directory); + else await this.#payload(source, row); + } else { + for (const section of directory.sections) { + this.examined_sections++; + if (section.name === null) { + this.unknown_section_names++; + this.discovery = "partial"; + continue; + } + if (!isGpuSection(section.name)) continue; + if ( + !section.file_backed || + (directory.format === "elf" && (section.flags & 0x800n) !== 0n) + ) { + this.discovery = "partial"; + const message = `GPU section ${section.index} has no supported uncompressed file range`; + this.#retain(message); + this.limitations.push(message); + continue; + } + const offset = gpuCount(section.offset), + size = gpuCount(section.size); + const borrowed: GpuSource = { + size, + containerId: source.containerId, + read: (start, length) => source.read(offset + start, length), + }; + // A section is a coordinate space with an explicit physical backing, including PE short names. + const row = gpuObject(source, { + offset: offset, + size: size, + kind: "native-section", + metadata: { + offset: section.header_offset, + size: + directory.format === "elf" + ? directory.bits === 64 + ? 64 + : 40 + : 40, + }, + }); + row.location.section_index = section.index; + row.location.section_name = section.name; + this.#add(row); + const sectionSource: GpuSource = { ...borrowed, containerId: row.id }; + try { + await this.inspect(sectionSource, depth + 1); + row.inspection = "complete"; + } catch (cause) { + this.#retainFailure(row, cause); + this.discovery = "partial"; + } + } + } + return directory.format; + } + // PTX is positively identified by its directives, never by extension or arbitrary embedded text. + let probe = await source.read(0, Math.min(source.size, 64 * 1024)); + let target = readPtxTarget(probe.toString("utf8")); + if ( + target === null && + probe.length < Math.min(source.size, GPU_REPORT_BYTES) + ) { + probe = await source.read(0, Math.min(source.size, GPU_REPORT_BYTES)); + target = readPtxTarget(probe.toString("utf8")); + } + if (target !== null) { + const row = gpuObject(source, { + offset: 0, + size: source.size, + kind: "ptx", + }); + row.target = target; + this.#add(row); + await this.#payload( + probe.length === source.size + ? new GpuBufferSource(probe, source.containerId) + : source, + row, + ); + return "ptx"; + } + if (probe.length < source.size) + throw new BinaryResourceFailure( + "structure", + GPU_REPORT_BYTES, + "Standalone text recognition exceeds the 16 MiB PTX budget before a target was established", + ); + throw new BinaryFormatFailure( + "unsupported", + "No supported CUDA/HIP container or PTX declarations were identified", + ); + } + #retainFailure(row: GpuObject, cause: unknown): void { + if (cause instanceof GpuReportCapacityFailure) throw cause; + if ( + !(cause instanceof BinaryFormatFailure) && + !(cause instanceof BinaryResourceFailure) + ) + throw cause; + row.inspection = + cause instanceof BinaryResourceFailure + ? "capacity" + : cause.kind === "unsupported" + ? "unsupported" + : "malformed"; + this.#objectLimitations(row, [cause.message]); + } + async #payload( + source: GpuSource, + row: GpuObject, + preparedAmd?: AmdElfMetadata, + ): Promise { + if (this.input.analysis !== "kernels") return; + if (!this.#selected(row)) { + row.inspection = "not-selected"; + return; + } + if (row.compression === "unsupported") { + row.inspection = "unsupported"; + return; + } + try { + let coverage: "complete" | "partial" = "complete"; + if ( + row.kind === "ptx" && + (row.decoded_size === null || row.decoded_size > GPU_REPORT_BYTES) + ) + throw new BinaryResourceFailure( + "structure", + GPU_REPORT_BYTES, + "PTX text exceeds the 16 MiB retained report budget", + ); + const compression = row.compression; + const compressed = + compression === "zstd" || + compression === "zlib" || + compression === "lz4"; + if (compressed && row.decoded_size === null) + rejectGpu("Compressed GPU object has no decoded length"); + this.#budgetWork( + row.location.size, + ...(compressed ? [row.decoded_size!] : []), + ); + let bytes = await source.read(row.location.offset, row.location.size); + if (compressed) { + try { + bytes = await decodeGpuPayload( + bytes, + compression, + row.decoded_size!, + this.signal, + ); + } catch (cause) { + if (cause instanceof BinaryResourceFailure) throw cause; + assertArtifactReadActive(this.signal); + throw new BinaryFormatFailure( + "malformed", + "GPU payload decompression failed", + ); + } + } + this.#decoded(row, gpuSha256(bytes), bytes.length); + const decoded = new GpuBufferSource(bytes, row.id); + if (row.kind === "ptx") { + // NVIDIA appends NUL alignment padding to an uncompressed PTX entry. + let length = bytes.length; + if (row.raw_flags !== null) + while (length > 0 && bytes[length - 1] === 0) length--; + const content = bytes.subarray(0, length); + let text: string; + try { + text = new TextDecoder("utf-8", { + fatal: true, + ignoreBOM: true, + }).decode(content); + } catch { + return rejectGpu("PTX is not valid UTF-8 text"); + } + const target = readPtxTarget(text); + const conflict = this.#target(row, { + value: target, + key: target, + scope: "exact", + }); + if (conflict !== undefined) coverage = "partial"; + const parsed = readPtxKernels(text, row.id); + const artifact = { + id: `${row.id}/ptx`, + object_id: row.id, + sha256: gpuSha256(content), + media_type: "text/x-ptx" as const, + encoding: "utf-8" as const, + byte_length: content.length, + text, + location: gpuLocation(decoded, 0, content.length), + }; + this.#retain(artifact); + this.ptx_artifacts.push(artifact); + for (const { kernel, coverage: kernelCoverage } of parsed) { + if (conflict !== undefined) kernel.limitations.push(conflict); + if (kernelCoverage === "partial") coverage = "partial"; + this.#retain(kernel); + this.kernels.push(kernel); + } + } else if (row.kind === "cubin" || row.kind === "amdgpu") { + coverage = await this.#elfFacts(decoded, row, row.kind, preparedAmd); + } else { + row.inspection = "unsupported"; + this.#objectLimitations(row, [ + "Payload kind is outside kernel metadata coverage", + ]); + return; + } + row.inspection = coverage; + } catch (cause) { + this.#retainFailure(row, cause); + } + } + #target(row: GpuObject, payload: GpuPayloadTarget): string | undefined { + const selectedTarget = row.target; + const outer = this.#outerTargets.get(row) ?? null; + const hasOuter = outer !== null; + let state: GpuObject["target_consistency"] = "unknown"; + if (payload.value !== null && !hasOuter) state = "single-source"; + else if (outer !== null && payload.key !== null) { + if (payload.scope === "cuda-base") { + const base = /^sm_([0-9]+)[a-z]*$/u.exec(outer)?.[1]; + const inner = /^sm_([0-9]+)$/u.exec(payload.key)?.[1]; + if (base !== undefined && inner !== undefined) + state = base === inner ? "base-only" : "conflict"; + } else if (outer === payload.key) state = "consistent"; + else if ( + row.kind === "amdgpu" && + outer.split(":")[0] === payload.key.split(":")[0] + ) + state = "unknown"; + else state = "conflict"; + } + this.#retain({ + payload_target: payload.value, + cuda_target_header: payload.cudaHeader ?? null, + target_consistency: state, + target: selectedTarget ?? payload.value, + }); + row.payload_target = payload.value; + row.cuda_target_header = payload.cudaHeader ?? null; + row.target_consistency = state; + if (selectedTarget === null) row.target = payload.value; + if (row.target === null) this.discovery = "partial"; + if (state !== "conflict" && payload.limitation === undefined) return; + const message = + state === "conflict" + ? `Conflicting target declarations: inventory ${outer}; payload ${payload.value}. Target agreement is not established.` + : payload.limitation!; + this.#objectLimitations(row, [message]); + return message; + } + async #amdPayload( + source: GpuSource, + row: GpuObject, + directory?: NativeSectionDirectory, + ): Promise { + const child: GpuSource = { + size: row.location.size, + containerId: row.id, + read: (offset, size) => { + checkGpuRange(row.location.size, offset, size); + this.#budgetWork(size); + return source.read(row.location.offset + offset, size); + }, + }; + try { + const native = + directory ?? + (await readGpuNativeDirectory( + child, + readElfSectionDirectory(child.size), + )); + const inspected = await readAmdElfMetadata(child, native); + const prepared = inspected.ok ? inspected.value : inspected.error.partial; + this.#target(row, { + value: prepared.target, + key: prepared.target_key, + scope: "exact", + }); + if (!inspected.ok) { + this.discovery = "partial"; + if (this.input.analysis === "kernels" && this.#selected(row)) + await this.#elfFacts(child, row, "amdgpu", prepared); + this.#retainFailure(row, inspected.error.cause); + return; + } + await this.#payload(source, row, prepared); + } catch (cause) { + this.discovery = "partial"; + this.#retainFailure(row, cause); + } + } + async #elfFacts( + source: GpuSource, + row: GpuObject, + kind: "cubin" | "amdgpu", + preparedAmd?: AmdElfMetadata, + ): Promise<"complete" | "partial"> { + let conflict: string | undefined; + const parsed = await readGpuElfKernels( + source, + kind, + (kernel) => { + if (conflict !== undefined) kernel.limitations.push(conflict); + this.#retain(kernel); + this.kernels.push(kernel); + }, + preparedAmd, + (target) => { + conflict = this.#target(row, target); + }, + ); + this.#objectLimitations(row, parsed.limitations); + return conflict !== undefined ? "partial" : parsed.coverage; + } + async #cuda(source: GpuSource): Promise { + for await (const row of readCudaFatbinary(source, this.signal)) { + this.#add(row); + await this.#payload(source, row); + } + } + async #hip(source: GpuSource, depth: number): Promise { + let base = 0; + let bundles = 0; + while (base < source.size) { + if (++bundles % 128 === 0) { + await scheduler.yield(); + assertArtifactReadActive(this.signal); + } + const prefix = await source.read(base, Math.min(32, source.size - base)); + if (prefix.subarray(0, 24).equals(CLANG_BUNDLE_MAGIC)) { + const directory = await readClangBundleDirectory( + source, + base, + this.signal, + ); + for (const row of directory.objects) { + this.#add(row); + if (row.kind === "host") row.inspection = "complete"; + else if (row.kind === "amdgpu" && row.target === null) + await this.#amdPayload(source, row); + else await this.#payload(source, row); + } + base = await nextGpuContainerOffset(source, directory.end); + continue; + } + if (prefix.subarray(0, 4).toString("ascii") !== "CCOB") + rejectGpu("Unexpected bytes between Clang offload bundles"); + const header = readCcobHeader(prefix, source.size - base), + { width, total, decoded, hash, version, method } = header; + const row = gpuObject(source, { + offset: base + width, + size: total - width, + kind: "hip-bundle", + metadata: { offset: base, size: width }, + }); + row.compression = + method === 0 ? "zlib" : method === 1 ? "zstd" : "unsupported"; + row.compression_id = `ccob:${version}:${method}`; + row.decoded_size = decoded; + this.#add(row); + if (row.compression === "unsupported") { + row.inspection = "unsupported"; + this.#objectLimitations(row, ["Unsupported CCOB compression method"]); + this.discovery = "partial"; + } else + try { + this.#budgetWork(total - width, decoded); + const data = await decodeGpuPayload( + await source.read(base + width, total - width), + row.compression, + decoded, + this.signal, + ); + if ( + createHash("md5").update(data).digest().readBigUInt64LE(0) !== hash + ) + rejectGpu("CCOB truncated MD5 does not match decoded bytes"); + this.#decoded(row, gpuSha256(data), data.length); + await this.inspect(new GpuBufferSource(data, row.id), depth + 1); + row.inspection = "complete"; + } catch (cause) { + this.#retainFailure(row, cause); + this.discovery = "partial"; + } + base = await nextGpuContainerOffset(source, base + total); + } + } +} +const isGpuSection = (name: string): boolean => + [ + ".nv_fatbin", + ".nv_fatb", + ".hip_fatbin", + ".hip_fat", + ".llvm.offloading", + ].includes(name); diff --git a/src/gpu/GpuObject.ts b/src/gpu/GpuObject.ts new file mode 100644 index 0000000000..01ddf28ac9 --- /dev/null +++ b/src/gpu/GpuObject.ts @@ -0,0 +1,52 @@ +import type { GpuLocation, GpuObject } from "../domain/gpu/gpuCodeObjects.js"; +import type { GpuSource } from "./GpuSource.js"; +export interface GpuPayloadTarget { + readonly value: string | null; + readonly key: string | null; + readonly scope: "exact" | "cuda-base"; + readonly cudaHeader?: GpuObject["cuda_target_header"]; + readonly limitation?: string; +} +export const gpuLocation = ( + source: GpuSource, + offset: number, + size: number, +): GpuLocation => ({ + container_id: source.containerId, + offset, + size, + section_index: null, + section_name: null, +}); +/** Row identity and both encoded/header locations belong to the same coordinate space. */ +export const gpuObject = ( + source: GpuSource, + input: { + offset: number; + size: number; + kind: GpuObject["kind"]; + metadata?: { offset: number; size: number }; + }, +): GpuObject => { + const { offset, size, kind, metadata = { offset, size: 0 } } = input; + return { + id: `${source.containerId ?? "file"}@${metadata.offset.toString(16)}`, + parent_id: source.containerId, + kind, + target: null, + payload_target: null, + cuda_target_header: null, + target_consistency: "unexamined", + bundle_id: null, + compression: "none", + compression_id: null, + encoded_size: size, + decoded_size: size, + sha256: null, + raw_flags: null, + location: gpuLocation(source, offset, size), + metadata_location: gpuLocation(source, metadata.offset, metadata.size), + inspection: "inventory", + limitations: [], + }; +}; diff --git a/src/gpu/GpuPayloadCodec.test.ts b/src/gpu/GpuPayloadCodec.test.ts new file mode 100644 index 0000000000..97fc039ee8 --- /dev/null +++ b/src/gpu/GpuPayloadCodec.test.ts @@ -0,0 +1,21 @@ +import { expect, it } from "vitest"; +import { deflateSync, zstdCompressSync } from "node:zlib"; +import { decodeGpuPayload } from "./GpuPayloadCodec.js"; +it.each(["zlib", "zstd"] as const)( + "requires all declared %s input bytes to belong to the codec stream", + async (codec) => { + const source = Buffer.from("kernel"), + encode = codec === "zstd" ? zstdCompressSync : deflateSync, + compressed = encode(source); + expect(await decodeGpuPayload(compressed, codec, source.length)).toEqual( + source, + ); + await expect( + decodeGpuPayload( + Buffer.concat([compressed, Buffer.from("junk")]), + codec, + source.length, + ), + ).rejects.toMatchObject({ kind: "malformed" }); + }, +); diff --git a/src/gpu/GpuPayloadCodec.ts b/src/gpu/GpuPayloadCodec.ts new file mode 100644 index 0000000000..e3bdb6596a --- /dev/null +++ b/src/gpu/GpuPayloadCodec.ts @@ -0,0 +1,55 @@ +import { constants, inflate, zstdDecompress } from "node:zlib"; +import { promisify } from "node:util"; +import { assertArtifactReadActive } from "../artifacts/ArtifactFileOperation.js"; +import { decodeLz4Block } from "./Lz4Block.js"; +import { rejectGpu } from "./GpuSource.js"; +const unzip = promisify(inflate); +const unzstd = promisify(zstdDecompress); +export const decodeGpuPayload = async ( + bytes: Buffer, + compression: "zlib" | "zstd" | "lz4", + expected: number, + signal?: AbortSignal, +): Promise => { + assertArtifactReadActive(signal); + if (compression === "lz4") return decodeLz4Block(bytes, expected, signal); + let result: unknown; + try { + const maxOutputLength = Math.max(1, expected); + result = await (compression === "zstd" + ? unzstd(bytes, { + maxOutputLength, + info: true, + params: { [constants.ZSTD_d_windowLogMax]: 27 }, + }) + : unzip(bytes, { maxOutputLength, info: true })); + } catch { + assertArtifactReadActive(signal); + return rejectGpu( + "GPU payload decompression failed or exceeded its declared length", + ); + } + assertArtifactReadActive(signal); + if ( + typeof result !== "object" || + result === null || + !("buffer" in result) || + !Buffer.isBuffer(result.buffer) || + !("engine" in result) || + typeof result.engine !== "object" || + result.engine === null || + !("bytesWritten" in result.engine) || + typeof result.engine.bytesWritten !== "number" + ) + throw new Error( + "Built-in GPU decoder omitted its buffer or consumed-input accounting", + ); + const decoded = result.buffer; + if (result.engine.bytesWritten !== bytes.length) + rejectGpu( + "GPU compressed payload contains bytes outside its complete codec stream", + ); + if (decoded.length !== expected) + rejectGpu("GPU decompressed length disagrees with its declaration"); + return decoded; +}; diff --git a/src/gpu/GpuSource.ts b/src/gpu/GpuSource.ts new file mode 100644 index 0000000000..f2e43d839d --- /dev/null +++ b/src/gpu/GpuSource.ts @@ -0,0 +1,145 @@ +import { createHash } from "node:crypto"; +import type { FileHandle } from "node:fs/promises"; +import { assertArtifactReadActive } from "../artifacts/ArtifactFileOperation.js"; +import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js"; +import { + BinaryFormatFailure, + BinaryResourceFailure, +} from "../native/metadata/BinaryContainer.js"; +import type { + NativeReadRequest, + NativeSectionDirectory, +} from "../native/metadata/NativeSectionDirectory.js"; +export const GPU_OBJECT_BYTES = 128 * 1024 * 1024; +export const GPU_REPORT_BYTES = 16 * 1024 * 1024; +/** A retained-facts refusal aborts discovery; it is never downgraded to one opaque payload. */ +export class GpuReportCapacityFailure extends BinaryResourceFailure { + constructor() { + super( + "structure", + GPU_REPORT_BYTES, + "GPU facts exceed the 16 MiB retained report budget", + ); + } +} +export const rejectGpu = (message: string): never => { + throw new BinaryFormatFailure("malformed", message); +}; +export const gpuCount = (value: bigint): number => { + if (value < 0n || value > BigInt(Number.MAX_SAFE_INTEGER)) + rejectGpu("GPU range exceeds exact host offset precision"); + return Number(value); +}; +export interface GpuSource { + readonly size: number; + readonly containerId: string | null; + read(offset: number, size: number): Promise; +} +export const checkGpuRange = ( + length: number, + offset: number, + size: number, +): void => { + if ( + !Number.isSafeInteger(offset) || + !Number.isSafeInteger(size) || + offset < 0 || + size < 0 || + offset > length || + size > length - offset + ) + rejectGpu("GPU byte range is outside its containing object"); + if (size > GPU_OBJECT_BYTES) + throw new BinaryResourceFailure( + "structure", + GPU_OBJECT_BYTES, + "GPU object exceeds the 128 MiB decoding budget", + ); +}; +export class GpuFileSource implements GpuSource { + readonly containerId = null; + bytesRead = 0; + constructor( + readonly file: FileHandle, + readonly size: number, + readonly signal?: AbortSignal, + ) {} + async read(offset: number, size: number): Promise { + checkGpuRange(this.size, offset, size); + const bytes = Buffer.alloc(size); + let position = 0; + while (position < size) { + assertArtifactReadActive(this.signal); + const result = await this.file.read( + bytes, + position, + size - position, + offset + position, + ); + if (result.bytesRead === 0) + throw new ArtifactReaderFailure( + "integrity", + "GPU source was truncated during inspection", + ); + position += result.bytesRead; + this.bytesRead += result.bytesRead; + } + return bytes; + } + async digest(): Promise { + const hash = createHash("sha256"); + for (let position = 0; position < this.size;) { + const bytes = await this.read( + position, + Math.min(1024 * 1024, this.size - position), + ); + hash.update(bytes); + position += bytes.length; + } + return hash.digest("hex"); + } +} +export class GpuBufferSource implements GpuSource { + readonly size: number; + constructor( + readonly bytes: Buffer, + readonly containerId: string | null, + ) { + this.size = bytes.length; + } + async read(offset: number, size: number): Promise { + checkGpuRange(this.size, offset, size); + return this.bytes.subarray(offset, offset + size); + } +} +export const readGpuNativeDirectory = async ( + source: GpuSource, + steps: Generator, +): Promise => { + let next = steps.next(); + while (!next.done) { + const { offset, size } = next.value; + next = steps.next(await source.read(offset, size)); + } + return next.value; +}; +export const gpuSha256 = (bytes: Buffer): string => + createHash("sha256").update(bytes).digest("hex"); + +export const nextGpuContainerOffset = async ( + source: GpuSource, + start: number, +): Promise => { + let width = 64; + for (let position = start; position < source.size;) { + const bytes = await source.read( + position, + Math.min(width, source.size - position), + ); + const index = bytes.findIndex((byte) => byte !== 0); + if (index >= 0) return position + index; + position += bytes.length; + width = 65536; + } + return source.size; +}; diff --git a/src/gpu/Lz4Block.test.ts b/src/gpu/Lz4Block.test.ts new file mode 100644 index 0000000000..59db865f46 --- /dev/null +++ b/src/gpu/Lz4Block.test.ts @@ -0,0 +1,21 @@ +import { expect, it } from "vitest"; +import { decodeLz4Block } from "./Lz4Block.js"; +it("decodes overlapping matches by repeating the newly produced pattern", async () => { + // The initial match is the same overlap shape observed in nvcc 12.9 speed-mode PTX. + const input = Buffer.from("3c0a2f2f0300504142434445", "hex"); + expect((await decodeLz4Block(input, 24)).toString()).toBe( + "\n//".repeat(6) + "\nABCDE", + ); +}); +it.each(["10000000", "10000100", "f0ff", "f001", "00"])( + "rejects malformed or mismatched block %s", + async (hex) => { + await expect(decodeLz4Block(Buffer.from(hex, "hex"), 8)).rejects.toThrow(); + }, +); +it("preserves caller cancellation before allocation work continues", async () => { + const signal = AbortSignal.abort("caller stopped"); + await expect( + decodeLz4Block(Buffer.from("10aa", "hex"), 1, signal), + ).rejects.toMatchObject({ reason: "cancelled" }); +}); diff --git a/src/gpu/Lz4Block.ts b/src/gpu/Lz4Block.ts new file mode 100644 index 0000000000..2cfab3e948 --- /dev/null +++ b/src/gpu/Lz4Block.ts @@ -0,0 +1,75 @@ +import { scheduler } from "node:timers/promises"; +import { assertArtifactReadActive } from "../artifacts/ArtifactFileOperation.js"; +import { GPU_OBJECT_BYTES, rejectGpu } from "./GpuSource.js"; +import { BinaryResourceFailure } from "../native/metadata/BinaryContainer.js"; +/** Raw LZ4 blocks used by CUDA's 0x2000 mode; not an LZ4 frame decoder. */ +export const decodeLz4Block = async ( + input: Buffer, + expected: number, + signal?: AbortSignal, +): Promise => { + if ( + !Number.isSafeInteger(expected) || + expected < 0 || + expected > GPU_OBJECT_BYTES + ) + throw new BinaryResourceFailure( + "structure", + GPU_OBJECT_BYTES, + "LZ4 decoded length exceeds the object budget", + ); + const output = Buffer.alloc(expected); + let read = 0, + write = 0, + checkpoint = 0; + const byte = (): number => { + if (read >= input.length) return rejectGpu("Truncated LZ4 block"); + return input[read++]!; + }; + const length = (nibble: number, remaining: number): number => { + let total = nibble; + if (nibble === 15) { + let extra: number; + do { + extra = byte(); + if (extra > remaining - total) + rejectGpu("LZ4 length exceeds its declared output"); + total += extra; + } while (extra === 255); + } + if (total > remaining) rejectGpu("LZ4 length exceeds its declared output"); + return total; + }; + while (read < input.length) { + if (write - checkpoint >= 4 * 1024 * 1024) { + await scheduler.yield(); + checkpoint = write; + } + assertArtifactReadActive(signal); + const token = byte(); + const literals = length(token >>> 4, expected - write); + if (literals > input.length - read) + rejectGpu("LZ4 literals exceed their encoded input"); + input.copy(output, write, read, read + literals); + read += literals; + write += literals; + if (read === input.length) break; + const distance = byte() | (byte() << 8); + if (distance === 0 || distance > write) + rejectGpu("LZ4 match has an invalid backward distance"); + const match = length(token & 15, expected - write - 4) + 4; + let copied = Math.min(distance, match); + output.copy(output, write, write - distance, write - distance + copied); + // Grow the already decoded pattern; Buffer.copy alone has memmove rather than LZ4 overlap semantics. + while (copied < match) { + const count = Math.min(copied, match - copied); + output.copy(output, write + copied, write, write + count); + copied += count; + } + write += match; + } + if (write !== expected) + rejectGpu("LZ4 decoded length disagrees with its declaration"); + assertArtifactReadActive(signal); + return output; +}; diff --git a/src/gpu/PtxKernels.test.ts b/src/gpu/PtxKernels.test.ts new file mode 100644 index 0000000000..3b811fcc93 --- /dev/null +++ b/src/gpu/PtxKernels.test.ts @@ -0,0 +1,96 @@ +import { expect, it } from "vitest"; +import { readPtxKernels, readPtxTarget } from "./PtxKernels.js"; +it("recognizes directives outside comments and strings and preserves UTF-8 byte coordinates", () => { + expect(readPtxTarget('".version 8.8\n.target sm_80\n"')).toBeNull(); + const prefix = "// 雪 .entry fake() {}\n.version 8.8\n.target sm_80\n"; + const text = + prefix + ".visible .entry real(.param .u64 x) { .reg .b32 %r<3>; ret; }"; + expect(readPtxTarget(text)).toBe("sm_80"); + const rows = [...readPtxKernels(text, "object")]; + expect(rows).toHaveLength(1); + expect(rows[0]!.coverage).toBe("complete"); + expect(rows[0]!.kernel.location.offset).toBe( + Buffer.byteLength(prefix + ".visible "), + ); + expect(rows[0]!.kernel.registers.virtual).toEqual([ + { type: ".b32", count: 3 }, + ]); +}); +it("derives vector/array parameter declarations and leaves unknown qualifiers unresolved", () => { + const rows = [ + ...readPtxKernels( + ".entry k(.param .v2 .u32 x, .param .align 16 .b8 a[32], .param .u64 .ptr .global y) {ret;}", + "object", + ), + ]; + expect( + rows[0]!.kernel.parameters.map(({ offset, size }) => [offset, size]), + ).toEqual([ + [0, 8], + [16, 32], + [null, null], + ]); + expect(rows[0]!.kernel.parameter_bytes).toBeNull(); + expect(rows[0]!.coverage).toBe("partial"); + expect(rows[0]!.kernel.limitations.join(" ")).toContain( + "parameter declarations", + ); + expect(rows[0]!.kernel.parameter_count).toBe(3); +}); +it("keeps dynamic and compound shared declarations unknown", () => { + const [row] = [ + ...readPtxKernels( + ".entry k() {.shared .u32 a,b; .shared .b8 dynamic[]; ret;}", + "object", + ), + ]; + expect(row!.kernel.shared_bytes).toBeNull(); + expect(row!.coverage).toBe("partial"); + expect(row!.kernel.representation).toBe("ptx-declarations"); +}); +it("sums bare names and every register range while preserving unsupported totals as unknown", () => { + const [row] = [ + ...readPtxKernels( + ".entry k() { .reg .b32 r0, r1; .reg .b32 %r<4>, %s<8>; .reg .u32 %x; .reg .u32 unsupported[2]; .reg .u32 %y; .reg .pred p, %p<2>; ret; }", + "object", + ), + ]; + expect(row!.kernel.registers.virtual).toEqual([ + { type: ".b32", count: 14 }, + { type: ".u32", count: null }, + { type: ".pred", count: 3 }, + ]); + expect(row!.kernel.limitations.join(" ")).toContain("count unknown"); +}); +it("does not round overflowing register totals or accept truncated ranges", () => { + for (const declaration of [ + "%r<9007199254740991>, %s", + "%r<4", + "%r<0>", + "%r,", + ]) { + const [row] = [ + ...readPtxKernels( + `.entry k() { .reg .b32 ${declaration}; ret; }`, + "object", + ), + ]; + expect(row!.kernel.registers.virtual).toEqual([ + { type: ".b32", count: null }, + ]); + } +}); +it.each([ + "/* unfinished", + ".entry broken(", + ".entry k() {", + ".entry k() {.reg .u32 %x}", + ".entry k() {.reg %x;}", +])("rejects incomplete PTX %s", (text) => { + expect(() => [...readPtxKernels(text, "object")]).toThrow(); +}); + +it("does not label unrelated malformed quoted text as a recognized PTX artifact", () => { + expect(readPtxTarget('unrelated "unterminated')).toBeNull(); + expect(readPtxTarget("/* unrelated unterminated")).toBeNull(); +}); diff --git a/src/gpu/PtxKernels.ts b/src/gpu/PtxKernels.ts new file mode 100644 index 0000000000..fcdcc01c8b --- /dev/null +++ b/src/gpu/PtxKernels.ts @@ -0,0 +1,397 @@ +import { BinaryFormatFailure } from "../native/metadata/BinaryContainer.js"; +import type { GpuKernel } from "../domain/gpu/gpuCodeObjects.js"; +import { BinaryResourceFailure } from "../native/metadata/BinaryContainer.js"; +import { GPU_REPORT_BYTES, rejectGpu } from "./GpuSource.js"; +interface Token { + readonly value: string; + readonly start: number; + readonly end: number; +} +/** Byte coordinates are accumulated once, including non-ASCII comments and quoted strings. */ +function* tokens(text: string): Generator { + let at = 0, + byte = 0; + const advance = (): void => { + const code = text.codePointAt(at)!; + at += code > 0xffff ? 2 : 1; + byte += code < 0x80 ? 1 : code < 0x800 ? 2 : code < 0x10000 ? 3 : 4; + }; + while (at < text.length) { + if (/\s/u.test(text[at]!)) { + advance(); + continue; + } + if (text.startsWith("//", at)) { + while (at < text.length && text[at] !== "\n") advance(); + continue; + } + if (text.startsWith("/*", at)) { + advance(); + advance(); + while (at < text.length && !text.startsWith("*/", at)) advance(); + if (at === text.length) rejectGpu("Unterminated PTX comment"); + advance(); + advance(); + continue; + } + const start = byte, + position = at; + if (text[at] === '"') { + advance(); + let closed = false; + while (at < text.length) { + if (text[at] === '"') { + advance(); + closed = true; + break; + } + if (text[at] === "\\") { + advance(); + if (at < text.length) advance(); + } else advance(); + } + if (!closed) rejectGpu("Unterminated PTX string"); + yield { value: '""', start, end: byte }; + continue; + } + if (/[.\w$%@]/u.test(text[at]!)) { + while (at < text.length && /[.\w$%@]/u.test(text[at]!)) advance(); + } else advance(); + if (at - position > 65536) + throw new BinaryResourceFailure( + "structure", + 65536, + "PTX token exceeds the string budget", + ); + yield { value: text.slice(position, at), start, end: byte }; + } +} +const integer = (token: string | undefined): number | null => { + if (token === undefined || !/^(?:0|[1-9][0-9]*)$/u.test(token)) return null; + const value = Number(token); + return Number.isSafeInteger(value) ? value : null; +}; +const sizeOf = (type: string | undefined): number | null => { + const match = /^\.(?:b|u|s|f)(8|16|32|64|128)$/u.exec(type ?? ""); + return match === null ? null : Number(match[1]) / 8; +}; +/** Parse every declared register name/range; unsupported forms poison the type total. */ +const registerDeclaration = ( + values: string[], +): { type: string; count: number | null } => { + const identifier = /^%?[A-Za-z_$][\w$@.]*$/u; + const firstName = values.findIndex((value) => identifier.test(value)); + if (firstName <= 0) + return rejectGpu("PTX register declaration has no type or name"); + const type = values.slice(0, firstName).join(" "); + if (firstName !== 1 || !/^\.(?:pred|[busf](?:8|16|32|64|128))$/u.test(type)) + return { type, count: null }; + let at = firstName, + count = 0; + while (at < values.length) { + if (!identifier.test(values[at++]!)) return { type, count: null }; + let declared = 1; + if (values[at] === "<") { + const range = integer(values[at + 1]); + if (range === null || range === 0 || values[at + 2] !== ">") + return { type, count: null }; + declared = range; + at += 3; + } + if (!Number.isSafeInteger(count + declared)) return { type, count: null }; + count += declared; + if (at === values.length) return { type, count }; + if (values[at++] !== "," || at === values.length) + return { type, count: null }; + } + return { type, count: null }; +}; +const declaration = ( + source: string[], +): { + name: string | null; + type: string | null; + size: number | null; + align: number | null; +} => { + const values = source[0] === ".param" ? source.slice(1) : source; + let at = 0; + const explicit = values[at] === ".align"; + const alignment = explicit ? integer(values[++at]) : null; + if (explicit) at++; + const vector = values[at] === ".v2" ? 2 : values[at] === ".v4" ? 4 : 1; + if (vector !== 1) at++; + const scalar = values[at++], + unit = sizeOf(scalar), + candidate = values[at++]; + const name = + candidate !== undefined && /^[A-Za-z_$][\w$@.]*$/u.test(candidate) + ? candidate + : null; + let size = unit === null ? null : unit * vector; + const type = + scalar === undefined + ? null + : vector === 1 + ? scalar + : `.v${vector} ${scalar}`; + const align = explicit ? alignment : size; + if (values[at] === "[") { + const count = integer(values[at + 1]); + size = + count === null || + size === null || + values[at + 2] !== "]" || + !Number.isSafeInteger(count * size) + ? null + : count * size; + at += 3; + } + if ( + at !== values.length || + name === null || + align === null || + align <= 0 || + !Number.isInteger(Math.log2(align)) + ) + return { name, type, size: null, align: null }; + return { name, type, size, align }; +}; + +/** Entry declarations and virtual resources; no instruction semantics or physical register claims. */ +export function* readPtxKernels( + text: string, + objectId: string, +): Generator<{ kernel: GpuKernel; coverage: "complete" | "partial" }> { + if (Buffer.byteLength(text) > GPU_REPORT_BYTES) + throw new BinaryResourceFailure( + "structure", + GPU_REPORT_BYTES, + "PTX text exceeds the 16 MiB retained report budget", + ); + const iterator = tokens(text); + const next = (): Token => { + const value = iterator.next(); + if (value.done) return rejectGpu("Truncated PTX entry declaration"); + return value.value; + }; + let kernelCount = 0; + let outerDepth = 0; + for (let step = iterator.next(); !step.done; step = iterator.next()) { + const token = step.value; + if (token.value === "{") { + outerDepth++; + continue; + } + if (token.value === "}") { + if (outerDepth === 0) rejectGpu("Unbalanced PTX body"); + outerDepth--; + continue; + } + if (outerDepth !== 0 || token.value !== ".entry") continue; + const name = next(); + if (!/^[A-Za-z_$][\w$@.]*$/u.test(name.value) || next().value !== "(") + rejectGpu("Malformed PTX entry name or parameters"); + const parameters: GpuKernel["parameters"] = []; + let part: string[] = [], + parameterBytes: number | null = 0; + const flush = (): void => { + if (part.length === 0) return; + if (part[0] !== ".param") rejectGpu("Unknown PTX parameter declaration"); + const value = declaration(part); + let offset: number | null = null; + if ( + parameterBytes !== null && + value.size !== null && + value.align !== null && + value.align > 0 && + Number.isSafeInteger(value.align) + ) { + offset = Math.ceil(parameterBytes / value.align) * value.align; + parameterBytes = Number.isSafeInteger(offset + value.size) + ? offset + value.size + : null; + } else parameterBytes = null; + parameters.push({ + ordinal: parameters.length, + name: value.name, + offset, + size: value.size, + type: value.type, + hidden: null, + }); + part = []; + }; + for (let t = next(); t.value !== ")"; t = next()) { + if (t.value === ",") flush(); + else part.push(t.value); + if (part.length > 128 || parameters.length > 65536) + throw new BinaryResourceFailure( + "structure", + 65536, + "PTX parameter declarations exceed the facts budget", + ); + } + flush(); + let t = next(); + // Entry directives can occur between the parameter list and body. + let headerTokens = 0; + while (t.value !== "{" && t.value !== ";") { + if (++headerTokens > 65536) rejectGpu("PTX entry header is too large"); + t = next(); + } + let end = t.end, + shared: number | null = t.value === "{" ? 0 : null; + const virtual = new Map(); + if (t.value === "{") { + let depth = 1; + while (depth > 0) { + t = next(); + end = t.end; + if (t.value === "{") { + depth++; + continue; + } + if (t.value === "}") { + depth--; + continue; + } + if (t.value !== ".reg" && t.value !== ".shared") continue; + const kind = t.value; + const declarationTokens: string[] = []; + for (t = next(); t.value !== ";"; t = next()) { + if (t.value === "}" || declarationTokens.length > 128) + rejectGpu("Malformed PTX resource declaration"); + declarationTokens.push(t.value); + } + if (kind === ".reg") { + const { type, count } = registerDeclaration(declarationTokens); + const previous = virtual.get(type); + const total = + previous === null || count === null + ? null + : (previous ?? 0) + count; + virtual.set( + type, + total === null || !Number.isSafeInteger(total) ? null : total, + ); + } else { + const value = declaration(declarationTokens); + if ( + shared === null || + value.size === null || + value.align === null || + value.align === 0 + ) + shared = null; + else { + const total = + Math.ceil(shared / value.align) * value.align + value.size; + shared = Number.isSafeInteger(total) ? total : null; + } + } + } + } + const kernel: GpuKernel = { + id: `${objectId}/entry@${token.start.toString(16)}`, + object_id: objectId, + representation: "ptx-declarations", + name: name.value, + symbol: null, + parameter_count: parameters.length, + parameter_bytes: parameterBytes, + parameters, + registers: { + physical: null, + sgpr: null, + vgpr: null, + agpr: null, + virtual: [...virtual].map(([type, count]) => ({ type, count })), + }, + shared_bytes: shared, + private_bytes: null, + location: { + container_id: objectId, + offset: token.start, + size: end - token.start, + section_index: null, + section_name: null, + }, + sources: { + parameters: { + container_id: objectId, + offset: token.start, + size: end - token.start, + section_index: null, + section_name: null, + }, + registers: null, + shared: null, + private: null, + }, + limitations: [ + "PTX registers are virtual declarations, not physical allocations. Parameter offsets/sizes and entry-local static shared bytes are derived from supported declarations; module shared bytes, dynamic allocation, device ABI and runtime allocation were not verified.", + ], + }; + let coverage: "complete" | "partial" = "complete"; + if (parameterBytes === null) { + coverage = "partial"; + kernel.limitations.push( + "Unsupported PTX parameter declarations or overflowing layouts leave parameter sizes/offsets and the total byte count unknown.", + ); + } + if (shared === null) { + coverage = "partial"; + kernel.limitations.push( + "Unsupported, dynamic or overflowing PTX shared declarations, or an absent entry body, leave entry-local static shared bytes unknown.", + ); + } + if (kernel.registers.virtual.some((row) => row.count === null)) { + coverage = "partial"; + kernel.limitations.push( + "Unsupported PTX register declarations or overflowing totals leave the affected virtual register count unknown.", + ); + } + if (++kernelCount > 65536) + throw new BinaryResourceFailure( + "structure", + 65536, + "PTX kernel count exceeds the facts budget", + ); + yield { kernel, coverage }; + } + if (outerDepth !== 0) rejectGpu("Unbalanced PTX body"); +} + +/** Positive PTX header recognition uses the same lexer as kernel extraction. */ +export const readPtxTarget = (text: string): string | null => { + const stream = tokens(text); + let version = false, + target: string | null = null; + try { + for (const token of stream) { + if ( + token.value === ".entry" || + token.value === ".func" || + token.value === "{" + ) + break; + if (token.value === ".version") { + const value = stream.next(); + version = !value.done && /^[0-9]+\.[0-9]+$/u.test(value.value.value); + } + if (token.value === ".target") { + const value = stream.next(); + target = + !value.done && /^sm_[0-9]+[a-z]?$/u.test(value.value.value) + ? value.value.value + : null; + } + if (version && target !== null) return target; + } + } catch (cause) { + if (!(cause instanceof BinaryFormatFailure)) throw cause; + // A malformed quote/comment before both directives does not establish PTX identity. + } + return null; +}; diff --git a/src/native/metadata/ElfImage.ts b/src/native/metadata/ElfImage.ts index 71e9f99f3c..86a5251c80 100644 --- a/src/native/metadata/ElfImage.ts +++ b/src/native/metadata/ElfImage.ts @@ -8,89 +8,19 @@ import { type LoadRegion, type ZeroFillMapping, } from "./BinaryContainer.js"; -import { readElfFixedHeader } from "../../domain/elfFixedHeader.js"; +import { readNativeDirectoryFromBuffer } from "./NativeSectionDirectory.js"; /** Read ELF sections and file-backed program mappings without interpreting language metadata. */ export const readElfImage = (bytes: Buffer): BinaryContainer => { - const fixedHeader = readElfFixedHeader(bytes); - if (!fixedHeader.ok) - throw new BinaryFormatFailure("malformed", fixedHeader.error); - const { elfClass: bits, headerSize } = fixedHeader.value; - const reader = new BinaryReader( - bytes, - fixedHeader.value.byteOrder === "little", - ); + const directory = readNativeDirectoryFromBuffer(bytes, "elf"); + const { + bits, + program_offset: phOffset, + program_count: phCount, + program_width: phWidth, + } = directory; + const reader = new BinaryReader(bytes, directory.little); const wide = bits === 64; - const phOffset = reader.word(wide ? 32 : 28, bits); - const shOffset = reader.word(wide ? 40 : 32, bits); - const phWidth = reader.u16(wide ? 54 : 42); - const shWidth = reader.u16(wide ? 58 : 46); - let phCount = reader.u16(wide ? 56 : 44); - let shCount = reader.u16(wide ? 60 : 48); - let namesIndex = reader.u16(wide ? 62 : 50); - if ( - (phCount !== 0 && phOffset < BigInt(headerSize)) || - (shOffset !== 0n && shOffset < BigInt(headerSize)) - ) - throw new BinaryFormatFailure( - "malformed", - "ELF header table overlaps the native header", - ); - if ( - shOffset !== 0n && - (shCount === 0 || phCount === 0xffff || namesIndex === 0xffff) - ) { - if (shWidth < (wide ? 64 : 40)) - throw new BinaryFormatFailure( - "malformed", - "ELF extended section header width is invalid", - ); - const first = reader.fileRange( - shOffset, - BigInt(shWidth), - "ELF section zero", - ).offset; - if (reader.u32(first + 4) !== 0) - throw new BinaryFormatFailure( - "malformed", - "ELF initial section for extended numbering must have SHT_NULL type", - ); - if (shCount === 0) { - const count = reader.word(first + (wide ? 32 : 20), bits); - if (count < 0xff00n || count > BigInt(Number.MAX_SAFE_INTEGER)) - throw new BinaryFormatFailure( - "malformed", - "ELF extended section count is invalid", - ); - shCount = Number(count); - } - if (phCount === 0xffff) { - phCount = reader.u32(first + (wide ? 44 : 28)); - if (phCount < 0xffff) - throw new BinaryFormatFailure( - "malformed", - "ELF extended program count is below its reserved range", - ); - } - if (namesIndex === 0xffff) { - namesIndex = reader.u32(first + (wide ? 40 : 24)); - if (namesIndex < 0xff00) - throw new BinaryFormatFailure( - "malformed", - "ELF extended section name index is below its reserved range", - ); - } - } else if ( - shOffset === 0n && - (shCount !== 0 || namesIndex !== 0 || phCount === 0xffff) - ) { - throw new BinaryFormatFailure("malformed", "ELF section table is missing"); - } - if (phCount !== 0 && phWidth < (wide ? 56 : 32)) - throw new BinaryFormatFailure( - "malformed", - "ELF program header width is invalid", - ); const phStart = reader.table( phOffset, phCount, @@ -135,53 +65,34 @@ export const readElfImage = (bytes: Buffer): BinaryContainer => { if (memorySize > size) zeroFills.push({ address: address + size, size: memorySize - size }); } - if (shCount !== 0) { - if (shWidth < (wide ? 64 : 40) || namesIndex >= shCount) - throw new BinaryFormatFailure( - "malformed", - "ELF section width or name-table index is invalid", - ); - const shStart = reader.table( - shOffset, - shCount, - shWidth, - "ELF section headers", - ); - if (namesIndex !== 0) { - const namesHeader = shStart + namesIndex * shWidth; - if (reader.u32(namesHeader + 4) !== 3) - throw new BinaryFormatFailure( - "malformed", - "ELF section name table is not a string table", - ); - const names = reader.fileRange( - reader.word(namesHeader + (wide ? 24 : 16), bits), - reader.word(namesHeader + (wide ? 32 : 20), bits), - "ELF section names", - ); - for (let index = 0; index < shCount; index++) { - const offset = shStart + index * shWidth; - const nameOffset = reader.u32(offset); - if (nameOffset >= names.size) - throw new BinaryFormatFailure( - "malformed", - "ELF section name offset is outside its string table", - ); - recordElfSection(sections, { - bytes, - names, - nameOffset, - reader, - bits, - wide, - header: offset, - }); - } - } + for (const section of directory.sections) { + if (section.name === "" || section.name_state === "unavailable") continue; + const range = declaredFileRange(bytes.length, section.offset, section.size); + const file_backed = section.type !== 8 && range.file_backed; + const compressed = (section.flags & 0x800n) !== 0n; + const read_only = + section.type === 1 && + file_backed && + !compressed && + (section.flags & 1n) === 0n && + (section.flags & 4n) === 0n && + (section.flags & 2n) !== 0n; + if (section.name === null && !read_only) continue; + sections.push({ + format: "elf", + name: section.name ?? "", + type: section.type, + address: section.address, + declared_range: { offset: section.offset, size: section.size }, + ...range, + file_backed, + compressed, + read_only, + }); } return { format: "elf", - architecture: architectureForElfMachine(fixedHeader.value.machine), + architecture: architectureForElfMachine(directory.machine), bits, byte_order: reader.little ? "little" : "big", mappings, @@ -192,113 +103,6 @@ export const readElfImage = (bytes: Buffer): BinaryContainer => { }; }; -const recordElfSection = ( - sections: ImageSection[], - input: { - readonly bytes: Buffer; - readonly names: { readonly offset: number; readonly size: number }; - readonly nameOffset: number; - readonly reader: BinaryReader; - readonly bits: 32 | 64; - readonly wide: boolean; - readonly header: number; - }, -): void => { - const decoded = elfSectionName(input.bytes, input.names, input.nameOffset); - const header = { - reader: input.reader, - bits: input.bits, - wide: input.wide, - header: input.header, - }; - switch (decoded.status) { - case "named": - sections.push(elfImageSection({ ...header, name: decoded.name })); - return; - case "truncated": { - // Names past this window stay out of .dep-v0 matching. Read-only - // bytes can still hold the only compiler path. - const section = elfImageSection({ ...header, name: "" }); - if (section.read_only) sections.push(section); - return; - } - case "unnamed": - return; - default: { - const unreachable: never = decoded; - return unreachable; - } - } -}; - -const elfSectionName = ( - bytes: Buffer, - names: { readonly offset: number; readonly size: number }, - nameOffset: number, -): - | { readonly status: "named"; readonly name: string } - | { readonly status: "unnamed" } - | { readonly status: "truncated" } => { - const start = names.offset + nameOffset; - const limit = Math.min(start + 64, names.offset + names.size); - const nul = bytes.subarray(start, limit).indexOf(0); - if (nul === 0) return { status: "unnamed" }; - if (nul < 0) return { status: "truncated" }; - return { - status: "named", - name: bytes.toString("latin1", start, start + nul), - }; -}; - -const elfImageSection = ({ - reader, - bits, - wide, - header, - name, -}: { - readonly reader: BinaryReader; - readonly bits: 32 | 64; - readonly wide: boolean; - readonly header: number; - readonly name: string; -}): ImageSection => { - const type = reader.u32(header + 4); - const flags = reader.word(header + 8, bits); - const declared_range = { - offset: reader.word(header + (wide ? 24 : 16), bits), - size: reader.word(header + (wide ? 32 : 20), bits), - }; - const declared = declaredFileRange( - reader.bytes.length, - declared_range.offset, - declared_range.size, - ); - // SHT_NOBITS offsets and sizes do not describe stored bytes. - const range = - type === SHT_NOBITS ? { ...declared, file_backed: false } : declared; - const compressed = (flags & 0x800n) !== 0n; - const readOnly = - type === 1 && - range.file_backed && - !compressed && - (flags & 1n) === 0n && - (flags & 4n) === 0n && - (flags & 2n) !== 0n; - return { - format: "elf", - name, - type, - address: reader.word(header + (wide ? 16 : 12), bits), - declared_range, - ...range, - read_only: readOnly, - compressed, - }; -}; - -const SHT_NOBITS = 8; - /** Map ELF machine identifiers to stable architecture names. */ const architectureForElfMachine = (machine: number): string => { const names = new Map([ diff --git a/src/native/metadata/ElfSymbol.ts b/src/native/metadata/ElfSymbol.ts new file mode 100644 index 0000000000..2729380921 --- /dev/null +++ b/src/native/metadata/ElfSymbol.ts @@ -0,0 +1,36 @@ +import { BinaryReader, BinaryFormatFailure } from "./BinaryContainer.js"; +/** Raw ELF symbol fields; GPU and other consumers own the meaning of processor-specific flags. */ +export const readElfSymbol = ( + bytes: Buffer, + bits: 32 | 64, + little: boolean, +) => { + const r = new BinaryReader(bytes, little); + const wide = bits === 64; + const info = r.range(wide ? 4 : 12, 1, "ELF symbol info")[0]!; + return { + name_offset: r.u32(0), + binding: info >>> 4, + type: info & 15, + other: r.range(wide ? 5 : 13, 1, "ELF symbol other")[0]!, + section_index: r.u16(wide ? 6 : 14), + value: r.word(wide ? 8 : 4, bits), + size: r.word(wide ? 16 : 8, bits), + }; +}; +export const readElfString = (bytes: Buffer, offset: number): string => { + if (offset >= bytes.length) + throw new BinaryFormatFailure( + "malformed", + "ELF string offset is outside its string table", + ); + const end = bytes + .subarray(offset, Math.min(bytes.length, offset + 65536)) + .indexOf(0); + if (end < 0) + throw new BinaryFormatFailure( + "malformed", + "ELF string is unterminated or exceeds the string budget", + ); + return bytes.toString("latin1", offset, offset + end); +}; diff --git a/src/native/metadata/NativeSectionDirectory.ts b/src/native/metadata/NativeSectionDirectory.ts new file mode 100644 index 0000000000..aa1c890acb --- /dev/null +++ b/src/native/metadata/NativeSectionDirectory.ts @@ -0,0 +1,336 @@ +import { readElfFixedHeader } from "../../domain/elfFixedHeader.js"; +import { + BinaryFormatFailure, + BinaryReader, + BinaryResourceFailure, + declaredFileRange, +} from "./BinaryContainer.js"; + +/** A borrowed positional source. Drivers own admission, cancellation and file identity. */ +export interface NativeReadRequest { + readonly offset: number; + readonly size: number; +} +export interface NativeSection { + readonly index: number; + readonly name: string | null; + readonly name_state: "known" | "unavailable" | "truncated"; + readonly header_offset: number; + readonly offset: bigint; + readonly size: bigint; + readonly address: bigint; + readonly type: number; + readonly flags: bigint; + readonly link: number; + readonly info: number; + readonly entry_size: bigint; + readonly file_backed: boolean; +} +export interface NativeSectionDirectory { + readonly format: "elf" | "pe"; + readonly bits: 32 | 64; + readonly little: boolean; + readonly machine: number; + readonly elf_os_abi: number | null; + readonly flags: number; + readonly program_offset: bigint; + readonly program_count: number; + readonly program_width: number; + readonly sections: readonly NativeSection[]; +} +const STRUCTURE_BYTES = 16 * 1024 * 1024; +const reject = (message: string): never => { + throw new BinaryFormatFailure("malformed", message); +}; +const exact = (value: bigint): number => { + if (value < 0n || value > BigInt(Number.MAX_SAFE_INTEGER)) + reject("Native offset exceeds exact host precision"); + return Number(value); +}; +const request = ( + fileSize: number, + offset: bigint, + size: bigint, +): NativeReadRequest => { + if ( + offset < 0n || + size < 0n || + offset > BigInt(fileSize) || + size > BigInt(fileSize) - offset + ) + reject("Native directory range is truncated or outside the file"); + if (size > BigInt(STRUCTURE_BYTES)) + throw new BinaryResourceFailure( + "structure", + STRUCTURE_BYTES, + "Native directory exceeds the 16 MiB structural decoding budget", + ); + return { offset: exact(offset), size: exact(size) }; +}; + +/** One ELF interpretation for in-memory images and bounded descriptor-backed inspections. */ +export function* readElfSectionDirectory( + fileSize: number, +): Generator { + const prefix = yield request(fileSize, 0n, 16n); + const headerSize = prefix[4] === 1 ? 52 : 64; + const bytes = yield request(fileSize, 0n, BigInt(headerSize)); + const parsed = readElfFixedHeader(bytes, fileSize); + if (!parsed.ok) return reject(parsed.error); + const { elfClass: bits, byteOrder, machine } = parsed.value; + const little = byteOrder === "little"; + const wide = bits === 64; + const r = new BinaryReader(bytes, little); + const program_offset = r.word(wide ? 32 : 28, bits); + const sectionOffset = r.word(wide ? 40 : 32, bits); + const program_width = r.u16(wide ? 54 : 42); + const sectionWidth = r.u16(wide ? 58 : 46); + let program_count = r.u16(wide ? 56 : 44); + let count = r.u16(wide ? 60 : 48); + let namesIndex = r.u16(wide ? 62 : 50); + if ( + (program_count !== 0 && program_offset < BigInt(headerSize)) || + (sectionOffset !== 0n && sectionOffset < BigInt(headerSize)) + ) + reject("ELF header table overlaps the native header"); + if ( + sectionOffset === 0n && + (count !== 0 || namesIndex !== 0 || program_count === 0xffff) + ) + reject("ELF section table is missing"); + if ( + sectionOffset !== 0n && + (count === 0 || namesIndex === 0xffff || program_count === 0xffff) + ) { + if (sectionWidth < (wide ? 64 : 40)) + reject("ELF extended section header width is invalid"); + const first = new BinaryReader( + yield request(fileSize, sectionOffset, BigInt(sectionWidth)), + little, + ); + if (first.u32(4) !== 0) + reject( + "ELF initial section for extended numbering must have SHT_NULL type", + ); + if (count === 0) { + const declared = first.word(wide ? 32 : 20, bits); + if (declared < 0xff00n) reject("ELF extended section count is invalid"); + count = exact(declared); + } + if (namesIndex === 0xffff) { + namesIndex = first.u32(wide ? 40 : 24); + if (namesIndex < 0xff00) + reject("ELF extended section name index is below its reserved range"); + } + if (program_count === 0xffff) { + program_count = first.u32(wide ? 44 : 28); + if (program_count < 0xffff) + reject("ELF extended program count is below its reserved range"); + } + } + if (program_count !== 0) { + if (program_width < (wide ? 56 : 32)) + reject("ELF program header width is invalid"); + request( + fileSize, + program_offset, + BigInt(program_count) * BigInt(program_width), + ); + } + const sections: NativeSection[] = []; + if (count !== 0) { + if (sectionWidth < (wide ? 64 : 40) || namesIndex >= count) + reject("ELF section width or name-table index is invalid"); + const table = new BinaryReader( + yield request( + fileSize, + sectionOffset, + BigInt(count) * BigInt(sectionWidth), + ), + little, + ); + let names: Buffer | null = null; + if (namesIndex !== 0) { + const h = namesIndex * sectionWidth; + if (table.u32(h + 4) !== 3) + reject("ELF section name table is not a string table"); + names = yield request( + fileSize, + table.word(h + (wide ? 24 : 16), bits), + table.word(h + (wide ? 32 : 20), bits), + ); + } + const decodedNames = new Map< + number, + { name: string | null; state: NativeSection["name_state"] } + >(); + let nameBytes = 0; + for (let index = 0; index < count; index++) { + const h = index * sectionWidth; + const nameOffset = table.u32(h); + let name: string | null = null; + let name_state: NativeSection["name_state"] = "unavailable"; + if (names !== null) { + if (nameOffset >= names.length) + reject("ELF section name offset is outside its string table"); + let decoded = decodedNames.get(nameOffset); + if (decoded === undefined) { + const window = names.subarray( + nameOffset, + Math.min(nameOffset + 65536, names.length), + ); + const end = window.indexOf(0); + nameBytes += end < 0 ? window.length : end + 1; + if (nameBytes > STRUCTURE_BYTES) + throw new BinaryResourceFailure( + "structure", + STRUCTURE_BYTES, + "Decoded ELF section names exceed the 16 MiB structural budget", + ); + decoded = { + name: end < 0 ? null : window.toString("latin1", 0, end), + state: end < 0 ? "truncated" : "known", + }; + decodedNames.set(nameOffset, decoded); + } + name_state = decoded.state; + name = decoded.name; + } + const type = table.u32(h + 4); + const offset = table.word(h + (wide ? 24 : 16), bits); + const size = table.word(h + (wide ? 32 : 20), bits); + sections.push({ + index, + name, + name_state, + header_offset: exact(sectionOffset) + h, + offset, + size, + address: table.word(h + (wide ? 16 : 12), bits), + type, + flags: table.word(h + 8, bits), + link: table.u32(h + (wide ? 40 : 24)), + info: table.u32(h + (wide ? 44 : 28)), + entry_size: table.word(h + (wide ? 56 : 36), bits), + file_backed: + type !== 8 && declaredFileRange(fileSize, offset, size).file_backed, + }); + } + } + return { + format: "elf", + elf_os_abi: parsed.value.osAbi, + bits, + little, + machine, + flags: r.u32(wide ? 48 : 36), + program_offset, + program_count, + program_width, + sections, + }; +} + +/** Shared COFF section fields; content extent excludes file-alignment padding. */ +export const readPeSectionHeader = (bytes: Buffer) => { + const r = new BinaryReader(bytes, true); + const name = r.range(0, 8, "PE section name"); + const nul = name.indexOf(0); + const virtual_size = r.u32(8); + const raw_size = r.u32(16); + return { + name: name.toString("latin1", 0, nul < 0 ? name.length : nul), + virtual_size, + virtual_address: r.u32(12), + raw_size, + offset: r.u32(20), + characteristics: r.u32(36), + size: virtual_size === 0 ? raw_size : Math.min(virtual_size, raw_size), + }; +}; + +export function* readPeSectionDirectory( + fileSize: number, +): Generator { + const dos = new BinaryReader(yield request(fileSize, 0n, 64n), true); + if (dos.u16(0) !== 0x5a4d) reject("Missing DOS MZ header"); + const pe = dos.u32(60); + if (pe < 64) reject("PE signature overlaps the DOS header"); + const coff = new BinaryReader(yield request(fileSize, BigInt(pe), 24n), true); + if (coff.u32(0) !== 0x4550) reject("PE signature is invalid"); + const optionalSize = coff.u16(20); + const optional = new BinaryReader( + yield request(fileSize, BigInt(pe + 24), BigInt(optionalSize)), + true, + ); + const magic = optional.u16(0); + const bits = magic === 0x10b ? 32 : magic === 0x20b ? 64 : null; + if (bits === null) + throw new BinaryFormatFailure( + "unsupported", + "Only PE32/PE32+ images are supported", + ); + const minimum = bits === 64 ? 112 : 96; + if ( + optionalSize < minimum || + optional.u32(minimum - 4) * 8 > optionalSize - minimum + ) + reject("PE optional header or data directories are truncated"); + const count = coff.u16(6); + const start = pe + 24 + optionalSize; + const table = yield request(fileSize, BigInt(start), BigInt(count) * 40n); + const sections: NativeSection[] = []; + for (let index = 0; index < count; index++) { + const row = readPeSectionHeader( + table.subarray(index * 40, (index + 1) * 40), + ); + sections.push({ + index, + name: row.name, + name_state: "known", + header_offset: start + index * 40, + offset: BigInt(row.offset), + size: BigInt(row.size), + address: BigInt(row.virtual_address), + type: 0, + flags: BigInt(row.characteristics), + link: 0, + info: 0, + entry_size: 0n, + file_backed: declaredFileRange( + fileSize, + BigInt(row.offset), + BigInt(row.size), + ).file_backed, + }); + } + return { + format: "pe", + elf_os_abi: null, + bits, + little: true, + machine: coff.u16(4), + flags: coff.u16(22), + program_offset: 0n, + program_count: 0, + program_width: 0, + sections, + }; +} + +/** Synchronous driver borrows bytes; file drivers satisfy the identical requests without loading payloads. */ +export const readNativeDirectoryFromBuffer = ( + bytes: Buffer, + format: "elf" | "pe", +): NativeSectionDirectory => { + const steps = + format === "elf" + ? readElfSectionDirectory(bytes.length) + : readPeSectionDirectory(bytes.length); + let next = steps.next(); + while (!next.done) { + const { offset, size } = next.value; + next = steps.next(bytes.subarray(offset, offset + size)); + } + return next.value; +}; diff --git a/src/native/metadata/PeImage.ts b/src/native/metadata/PeImage.ts index f694550c71..b68b1f6eb7 100644 --- a/src/native/metadata/PeImage.ts +++ b/src/native/metadata/PeImage.ts @@ -1,3 +1,4 @@ +import { readPeSectionHeader } from "./NativeSectionDirectory.js"; import { observeMzWindowsHeader, parseDosMzHeader, @@ -95,16 +96,15 @@ export const readPeImage = (bytes: Buffer): BinaryContainer => { const sections: ImageSection[] = []; for (let index = 0; index < count; index++) { const entry = start + index * 40; - const sectionName = peSectionName( - reader.range(entry, 8, "PE section name"), - ); - const virtualSize = reader.u32(entry + 8); - const virtualAddress = reader.u32(entry + 12); - const size = reader.u32(entry + 16); - const offset = reader.u32(entry + 20); - const characteristics = reader.u32(entry + 36); - // SizeOfRawData includes file-alignment padding. VirtualSize is the content. - const contentSize = virtualSize === 0 ? size : Math.min(virtualSize, size); + const { + name: sectionName, + virtual_size: virtualSize, + virtual_address: virtualAddress, + raw_size: size, + offset, + characteristics, + size: contentSize, + } = readPeSectionHeader(reader.range(entry, 40, "PE section header")); sections.push( peImageSection({ reader, @@ -162,11 +162,6 @@ export const readPeImage = (bytes: Buffer): BinaryContainer => { }; }; -const peSectionName = (bytes: Buffer): string => { - const nul = bytes.indexOf(0); - return bytes.toString("latin1", 0, nul < 0 ? bytes.length : nul); -}; - const peImageSection = ({ reader, name, diff --git a/src/server/createServer.ts b/src/server/createServer.ts index 4d01bb3b7a..c2768c5f90 100644 --- a/src/server/createServer.ts +++ b/src/server/createServer.ts @@ -1,3 +1,5 @@ +import { createGpuCodeObjectsService } from "../composition/gpu.js"; +import { registerGpuCodeObjectsTools } from "./registerGpuCodeObjectsTools.js"; import type { PacketCaptureService } from "../application/captures/PacketCaptureService.js"; import { UNTRUSTED_CONTENT_INSTRUCTIONS } from "./toolResultEncoding.js"; import { createPacketCaptureService } from "../composition/captures.js"; @@ -332,6 +334,7 @@ export const createServer = ( const goBinary = options.goBinary ?? createGoBinaryService(); const rustBinary = options.rustBinary ?? createRustBinaryService(); const modelArtifact = createModelArtifactService(); + const gpuCodeObjects = createGpuCodeObjectsService(); const il2cppMetadata = options.il2cppMetadata ?? createIl2cppMetadataService(); const peResources = createPeResourcesService(); @@ -420,6 +423,7 @@ export const createServer = ( rustBinary.close(), il2cppMetadata.close(), modelArtifact.close(), + gpuCodeObjects.close(), peResources.close(), electronFuses.close(), webNetworkCapture.close(), @@ -503,6 +507,14 @@ export const createServer = ( analysisAdmission, evidenceById, ); + registerGpuCodeObjectsTools( + server, + gpuCodeObjects, + toolLogger, + recordEvidence, + analysisAdmission, + evidenceById, + ); registerBinaryAnalysisTools(toolContext); registerReverseEngineeringTools( server, diff --git a/src/server/registerGpuCodeObjectsTools.ts b/src/server/registerGpuCodeObjectsTools.ts new file mode 100644 index 0000000000..08d82ba8d0 --- /dev/null +++ b/src/server/registerGpuCodeObjectsTools.ts @@ -0,0 +1,34 @@ +import type { EvidenceMcpServer } from "./EvidenceMcpServer.js"; +import type { EvidenceLookup } from "../application/EvidenceInputResolver.js"; +import type { GpuCodeObjectsService } from "../application/gpu/GpuCodeObjectsService.js"; +import type { EvidenceWriter } from "../application/investigation/InvestigationRecordPort.js"; +import type { Logger } from "pino"; +import type { WithAdmittedAnalysis } from "./analysisAdmission.js"; +import { toolContract } from "../contracts/toolContracts.js"; +import { createEvidenceToolHandler } from "./contractToolHandler.js"; +import { toolRegistrationOptions } from "./toolRegistrationOptions.js"; + +/** Bind the named GPU contract to the shared inspection and session Evidence owners. */ +export const registerGpuCodeObjectsTools = ( + server: EvidenceMcpServer, + service: GpuCodeObjectsService, + logger: Logger, + recordEvidence?: EvidenceWriter["recordEvidence"], + withAdmittedAnalysis?: WithAdmittedAnalysis, + evidenceById?: EvidenceLookup, +): void => { + const contract = toolContract("inspect_gpu_code_objects"); + const handler = createEvidenceToolHandler( + server, + (input, options) => + service.inspect(input, options, { recordEvidence, evidenceById }), + logger, + recordEvidence, + withAdmittedAnalysis, + ); + server.registerTool( + contract.name, + toolRegistrationOptions(contract), + handler(contract), + ); +}; diff --git a/tests/acceptance/gpuCodeObjectsCli.test.ts b/tests/acceptance/gpuCodeObjectsCli.test.ts new file mode 100644 index 0000000000..088a683ff4 --- /dev/null +++ b/tests/acceptance/gpuCodeObjectsCli.test.ts @@ -0,0 +1,356 @@ +import { expect, it } from "vitest"; +import { writeFile, unlink } from "node:fs/promises"; +import { join } from "node:path"; +import { createHash } from "node:crypto"; +import { toolContract } from "../../src/contracts/toolContracts.js"; +import { gpuCodeObjectsSchema } from "../../src/domain/gpu/gpuCodeObjects.js"; +import { + gpuCodeObjectsPageViewResultSchema, + gpuCodeObjectsSummaryViewResultSchema, +} from "../../src/domain/analysisView/analysisView.js"; +import { parseEvidence, createEvidence } from "../../src/domain/evidence.js"; +import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js"; +import { connectLocalToolsMcp } from "../fixtures/localToolsMcp.js"; +import { parseMcpToolError } from "../fixtures/mcpToolError.js"; +import { cliTest } from "../support/cli/cliFixture.js"; +import { + cudaFixture, + ccobFixture, + ptxFixture, + amdFixture, + cubinFixture, +} from "../fixtures/gpu/codeObjects.js"; + +cliTest( + "reads CUDA/PTX and compressed HIP through the public CLI", + async ({ cli }) => { + const root = await createTestTempDirectory("rea-gpu-cli-"); + for (const [name, bytes] of [ + ["cuda", cudaFixture("zstd")], + ["hip", ccobFixture()], + ["amd", amdFixture()], + ] as const) { + const path = join(root, name); + await writeFile(path, bytes); + const response = await cli.run({ + arguments: [ + "inspect-gpu-code-objects", + path, + "--analysis", + "kernels", + "--detail", + "complete", + ...(name === "amd" + ? ["--targets", '["amdgcn-amd-amdhsa--gfx900"]'] + : []), + "--json", + ], + }); + expect(response.exitCode, response.stderr).toBe(0); + const evidence = parseEvidence( + toolContract("inspect_gpu_code_objects").outputSchema.parse( + response.json, + ), + ); + const report = gpuCodeObjectsSchema.parse(evidence.normalized_result); + expect(report.artifact.sha256).toBe( + createHash("sha256").update(bytes).digest("hex"), + ); + expect(report.kernels).toHaveLength(name === "cuda" ? 2 : 1); + expect(report.kernels[0]!.parameter_bytes).toBe(12); + expect(report.coverage.payloads).toBe("complete"); + } + }, +); + +cliTest( + "attributes malformed selector JSON to its actual CLI option", + async ({ cli }) => { + for (const name of ["object_ids", "targets", "view"]) { + const response = await cli.run({ + arguments: [ + "inspect-gpu-code-objects", + "/not-opened-for-invalid-selectors", + `--${name}`, + "[", + "--json", + ], + }); + expect(response.exitCode).not.toBe(0); + expect(response.json).toMatchObject({ + code: "invalid_request", + details: { + operation: "inspect_gpu_code_objects", + issues: [ + { + path: [name], + reason: "invalid_format", + message: `${name} must be valid JSON`, + }, + ], + }, + }); + } + }, +); + +it("preserves incomplete decoder coverage through MCP target and object pages", async () => { + const root = await createTestTempDirectory("rea-gpu-partial-"), + { call } = await connectLocalToolsMcp(); + for (const missing of [ + "section-names", + "symbols", + "later-amd-note", + "target-conflict", + "ptx-parameter", + "ptx-shared", + ]) { + const bytes = + missing === "later-amd-note" + ? amdFixture(false, Buffer.alloc(3)) + : missing === "target-conflict" + ? cudaFixture("none", [ + { + kind: 2, + target: 80, + bytes: cubinFixture({ architecture: 90 }), + }, + ]) + : missing === "ptx-parameter" + ? Buffer.from( + ptxFixture.replace(".u64 output", ".u64 .ptr .global output"), + ) + : missing === "ptx-shared" + ? Buffer.from(ptxFixture.replace("scratch[32]", "scratch[]")) + : cubinFixture(), + path = join(root, missing); + if (missing === "section-names") bytes.writeUInt16LE(0, 62); + else if (missing === "symbols") + bytes.writeUInt32LE(1, Number(bytes.readBigUInt64LE(40)) + 3 * 64 + 4); + await writeFile(path, bytes); + const reply = await call("inspect_gpu_code_objects", { + path, + analysis: "kernels", + view: { collection: "targets" }, + }); + expect(reply.isError, JSON.stringify(reply)).not.toBe(true); + const targets = gpuCodeObjectsPageViewResultSchema.parse( + parseEvidence(reply.structuredContent).normalized_result, + ); + expect(targets.source_coverage.payloads).toBe("partial"); + expect(targets.items).toEqual([ + expect.objectContaining({ + target: + missing === "later-amd-note" ? "amdgcn-amd-amdhsa--gfx900" : "sm_80", + kernel_count: missing === "symbols" ? 0 : 1, + kernel_coverage: "partial", + }), + ]); + const objectReply = await call("inspect_analysis_view", { + source: { + kind: "retained-evidence", + evidence_id: targets.parent_evidence_id, + }, + view: { + kind: "gpu-code-objects", + collection: "objects", + fields: ["kind"], + }, + }); + expect(objectReply.isError, JSON.stringify(objectReply)).not.toBe(true); + const objects = gpuCodeObjectsPageViewResultSchema.parse( + parseEvidence(objectReply.structuredContent).normalized_result, + ); + expect(objects.items).toEqual([ + expect.objectContaining({ + inspection: missing === "later-amd-note" ? "malformed" : "partial", + }), + ]); + if (missing === "target-conflict") + expect(objects.items[0]).toMatchObject({ + target: "sm_80", + payload_target: "sm_90", + target_consistency: "conflict", + }); + } +}); + +it("keeps unknown target discovery visible in the default MCP summary", async () => { + const root = await createTestTempDirectory("rea-gpu-unknown-target-"), + path = join(root, "cuda"); + await writeFile(path, cubinFixture({ osAbi: 0x41, flags: 0x06016402 })); + const { call } = await connectLocalToolsMcp(); + const reply = await call("inspect_gpu_code_objects", { path }); + expect(reply.isError, JSON.stringify(reply)).not.toBe(true); + const result = gpuCodeObjectsSummaryViewResultSchema.parse( + parseEvidence(reply.structuredContent).normalized_result, + ); + expect(result.summary.coverage.discovery).toBe("partial"); + expect(result.summary.counts).toMatchObject({ objects: 1, targets: 0 }); +}); + +it("retains selected kernel facts and complete PTX for historical MCP pages after source removal", async () => { + const root = await createTestTempDirectory("rea-gpu-mcp-"), + path = join(root, "application"); + await writeFile(path, cudaFixture("zstd")); + const { call } = await connectLocalToolsMcp(); + const inventoryReply = await call("inspect_gpu_code_objects", { + path, + view: { collection: "objects", fields: ["kind", "target"] }, + }); + expect(inventoryReply.isError, JSON.stringify(inventoryReply)).not.toBe(true); + const inventory = gpuCodeObjectsPageViewResultSchema.parse( + parseEvidence(inventoryReply.structuredContent).normalized_result, + ); + expect(inventory.items).toHaveLength(2); + expect(inventory.items[0]).not.toHaveProperty("sha256"); + expect(inventory.items[0]).toHaveProperty("metadata_location"); + const targetsReply = await call("inspect_analysis_view", { + source: { + kind: "retained-evidence", + evidence_id: inventory.parent_evidence_id, + }, + view: { kind: "gpu-code-objects", collection: "targets" }, + }); + expect(targetsReply.isError, JSON.stringify(targetsReply)).not.toBe(true); + const targets = gpuCodeObjectsPageViewResultSchema.parse( + parseEvidence(targetsReply.structuredContent).normalized_result, + ); + expect(targets.items).toHaveLength(2); + expect(targets.items).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + target: "sm_80", + object_count: 1, + kernel_count: 0, + kernel_coverage: "not-requested", + }), + expect.objectContaining({ + target: "sm_90", + object_count: 1, + kernel_count: 0, + kernel_coverage: "not-requested", + }), + ]), + ); + const inspectedReply = await call("inspect_gpu_code_objects", { + path, + analysis: "kernels", + view: { + collection: "kernels", + names: ["add_one"], + fields: ["parameter_count", "registers"], + }, + }); + expect(inspectedReply.isError, JSON.stringify(inspectedReply)).not.toBe(true); + const inspected = gpuCodeObjectsPageViewResultSchema.parse( + parseEvidence(inspectedReply.structuredContent).normalized_result, + ); + expect(inspected.items).toHaveLength(2); + expect(inspected.items[0]).not.toHaveProperty("parameters"); + expect(inspected.items[0]).toHaveProperty("sources"); + const objectId = + "object_id" in inspected.items[0]! + ? inspected.items[0]!.object_id + : undefined; + expect(objectId).toBeTypeOf("string"); + await unlink(path); + const source = { + kind: "retained-evidence", + evidence_id: inspected.parent_evidence_id, + }; + const textReply = await call("inspect_analysis_view", { + source, + view: { + kind: "gpu-code-objects", + collection: "ptx", + object_id: objectId, + offset: 0, + limit: 4096, + }, + }); + expect(textReply.isError, JSON.stringify(textReply)).not.toBe(true); + const text = gpuCodeObjectsPageViewResultSchema.parse( + parseEvidence(textReply.structuredContent).normalized_result, + ); + expect(text.ptx!.text).toBe(ptxFixture); + expect(text.ptx!.sha256).toBe( + createHash("sha256").update(ptxFixture).digest("hex"), + ); + const summaryReply = await call("inspect_analysis_view", { + source, + view: { kind: "summary" }, + }); + expect(summaryReply.isError, JSON.stringify(summaryReply)).not.toBe(true); + const summary = gpuCodeObjectsSummaryViewResultSchema.parse( + parseEvidence(summaryReply.structuredContent).normalized_result, + ); + expect(summary.summary.counts).toMatchObject({ + objects: 2, + kernels: 2, + ptx_artifacts: 1, + }); + expect(summary.summary).not.toHaveProperty("ptx_artifacts"); +}); + +it("rejects forged duplicate object IDs and mismatched Evidence subjects through MCP", async () => { + const root = await createTestTempDirectory("rea-gpu-import-"), + path = join(root, "cuda"); + await writeFile(path, cudaFixture()); + const { call } = await connectLocalToolsMcp(); + const reply = await call("inspect_gpu_code_objects", { + path, + analysis: "kernels", + detail: "complete", + }); + expect(reply.isError, JSON.stringify(reply)).not.toBe(true); + const parent = parseEvidence(reply.structuredContent), + report = gpuCodeObjectsSchema.parse(parent.normalized_result); + const forgedHeaderReport = gpuCodeObjectsSchema.parse( + parent.normalized_result, + ); + forgedHeaderReport.objects[1]!.cuda_target_header!.location.container_id = + forgedHeaderReport.objects[0]!.id; + const forgedHeader = createEvidence(undefined, parent.provider, { + operation: parent.operation, + parameters: parent.parameters, + result: forgedHeaderReport, + }); + expect( + parseMcpToolError( + await call("inspect_analysis_view", { + source: { kind: "inline", evidence: forgedHeader }, + view: { kind: "gpu-code-objects", collection: "objects" }, + }), + ).error, + ).toMatchObject({ code: "invalid_request" }); + report.objects[1]!.id = report.objects[0]!.id; + const forged = createEvidence(undefined, parent.provider, { + operation: "inspect_gpu_code_objects", + parameters: parent.parameters, + result: report, + }); + const failure = parseMcpToolError( + await call("inspect_analysis_view", { + source: { kind: "inline", evidence: forged }, + view: { kind: "gpu-code-objects", collection: "objects" }, + }), + ); + expect(failure.error).toMatchObject({ code: "invalid_request" }); + const mismatched = createEvidence( + { path: join(root, "other-file"), sha256: "f".repeat(64), format: "file" }, + parent.provider, + { + operation: parent.operation, + parameters: parent.parameters, + result: parent.normalized_result, + }, + ); + const identityFailure = parseMcpToolError( + await call("inspect_analysis_view", { + source: { kind: "inline", evidence: mismatched }, + view: { kind: "summary" }, + }), + ); + expect(identityFailure.error).toMatchObject({ code: "invalid_request" }); +}); diff --git a/tests/boundary/gpu/codeObjects.test.ts b/tests/boundary/gpu/codeObjects.test.ts new file mode 100644 index 0000000000..e464fa599b --- /dev/null +++ b/tests/boundary/gpu/codeObjects.test.ts @@ -0,0 +1,529 @@ +import { expect, it, onTestFinished } from "vitest"; +import { writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { parseEvidence } from "../../../src/domain/evidence.js"; +import { gpuCodeObjectsPartialSchema } from "../../../src/domain/gpu/gpuCodeObjects.js"; +import { GpuCodeObjectsProvider } from "../../../src/gpu/GpuCodeObjectsProvider.js"; +import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; +import { + cudaFixture, + cubinFixture, + ccobFixture, + hipFixture, + peGpuFixture, + elfFixture, + ptxFixture, + amdFixture, +} from "../../fixtures/gpu/codeObjects.js"; +const inspect = async ( + bytes: Buffer, + options: { + analysis?: "inventory" | "kernels"; + targets?: string[]; + object_ids?: string[]; + expected_sha256?: string; + } = {}, +) => { + const root = await createTestTempDirectory("rea-gpu-"), + path = join(root, "no-format-extension"); + await writeFile(path, bytes); + const provider = new GpuCodeObjectsProvider(); + onTestFinished(() => provider.close()); + return provider.inspect({ path, analysis: "kernels", ...options }); +}; +it.each(["none", "zstd"] as const)( + "reads selected CUDA metadata and PTX with %s compression", + async (compression) => { + const result = await inspect(cudaFixture(compression)); + if (!result.ok) throw result.error; + expect(result.value.coverage).toMatchObject({ + discovery: "complete", + payloads: "complete", + }); + expect( + result.value.kernels.map((row) => [ + row.name, + row.parameter_count, + row.parameter_bytes, + row.shared_bytes, + row.registers.physical, + ]), + ).toEqual([ + ["add_one", 2, 12, 32, null], + ["add_one", 2, 12, 1056, 10], + ]); + expect(result.value.ptx_artifacts[0]!.text).toBe(ptxFixture); + expect( + result.value.kernels[1]!.parameters.map((row) => [ + row.ordinal, + row.offset, + row.size, + ]), + ).toEqual([ + [0, 0, 8], + [1, 8, 4], + ]); + }, +); +it.each([1, 2, 3] as const)( + "reads CCOB v%d zlib/Zstd and retains exact decoded provenance", + async (version) => { + for (const method of ["zlib", "zstd"] as const) { + const result = await inspect(ccobFixture(version, method)); + if (!result.ok) throw result.error; + expect(result.value.kernels[0]).toMatchObject({ + name: "add_one", + parameter_count: 2, + parameter_bytes: 12, + registers: { sgpr: 6, vgpr: 2 }, + shared_bytes: 0, + }); + expect(result.value.objects[2]!.location.container_id).toBe( + result.value.objects[0]!.id, + ); + } + }, +); +it("does not decode unselected payloads and keeps stable positional identities", async () => { + const bytes = cudaFixture("zstd"); + const inventory = await inspect(bytes, { analysis: "inventory" }), + selected = await inspect(bytes, { targets: ["sm_90"] }); + if (!inventory.ok) throw inventory.error; + if (!selected.ok) throw selected.error; + expect(inventory.value.kernels).toEqual([]); + expect(selected.value.objects.map((row) => row.id)).toEqual( + inventory.value.objects.map((row) => row.id), + ); + expect(selected.value.ptx_artifacts).toEqual([]); + expect(selected.value.kernels).toHaveLength(1); + expect(selected.value.coverage.payloads).toBe("partial"); +}); +it.each(["cubin", "ptx"] as const)( + "retains conflicting container and %s target declarations as partial facts", + async (kind) => { + const bytes = cudaFixture("none", [ + { + kind: kind === "cubin" ? 2 : 1, + target: 80, + bytes: + kind === "cubin" + ? cubinFixture({ architecture: 90 }) + : Buffer.from(ptxFixture.replace("sm_80", "sm_90")), + }, + ]); + const result = await inspect(bytes, { targets: ["sm_80"] }); + if (!result.ok) throw result.error; + expect(result.value.objects[0]).toMatchObject({ + target: "sm_80", + payload_target: "sm_90", + target_consistency: "conflict", + inspection: "partial", + }); + expect(result.value.coverage.payloads).toBe("partial"); + expect(result.value.kernels).toHaveLength(1); + expect(result.value.kernels[0]!.limitations.join(" ")).toContain( + "inventory sm_80; payload sm_90", + ); + const unexamined = await inspect(bytes, { targets: ["sm_90"] }); + if (!unexamined.ok) throw unexamined.error; + expect(unexamined.value.objects[0]).toMatchObject({ + target: "sm_80", + payload_target: null, + target_consistency: "unexamined", + inspection: "not-selected", + }); + expect(unexamined.value.coverage.payloads).toBe("partial"); + }, +); +it("keeps CUBIN base-only agreement distinct from verified feature-profile agreement", async () => { + const bytes = cudaFixture("none", [ + { kind: 2, target: 90, bytes: cubinFixture({ architecture: 90 }) }, + ]); + bytes.writeBigUInt64LE(bytes.readBigUInt64LE(16 + 40) | 0x100000n, 16 + 40); + const result = await inspect(bytes, { targets: ["sm_90a"] }); + if (!result.ok) throw result.error; + expect(result.value.objects[0]).toMatchObject({ + target: "sm_90a", + payload_target: "sm_90", + target_consistency: "base-only", + inspection: "complete", + }); +}); +it.each([ + [100, 0x06006402], + [101, 0x06006502], + [103, 0x05006702], + [120, 0x06007802], + [121, 0x06007902], +])( + "reads ABI-qualified CUDA target sm_%d from released NVIDIA header flags", + async (architecture, flags) => { + const result = await inspect(cubinFixture({ osAbi: 0x41, flags })); + if (!result.ok) throw result.error; + expect(result.value.objects[0]).toMatchObject({ + target: `sm_${architecture}`, + payload_target: `sm_${architecture}`, + target_consistency: "single-source", + inspection: "complete", + cuda_target_header: { + os_abi: 0x41, + flags: `0x${flags.toString(16).padStart(8, "0")}`, + }, + }); + }, +); +it("keeps unsupported CUDA flag layouts unknown while retaining raw header evidence", async () => { + const result = await inspect( + cubinFixture({ osAbi: 0x41, flags: 0x06016402 }), + ); + if (!result.ok) throw result.error; + expect(result.value.objects[0]).toMatchObject({ + target: null, + payload_target: null, + target_consistency: "unknown", + inspection: "partial", + cuda_target_header: { os_abi: 0x41, flags: "0x06016402" }, + }); + expect(result.value.kernels).toHaveLength(1); + expect(result.value.kernels[0]!.limitations.join(" ")).toContain( + "target encoding is unknown", + ); + const inventory = await inspect( + cubinFixture({ osAbi: 0x41, flags: 0x06016402 }), + { analysis: "inventory" }, + ); + if (!inventory.ok) throw inventory.error; + expect(inventory.value.coverage.discovery).toBe("partial"); +}); +it("discovers standalone AMD targets before exact selection", async () => { + const bytes = amdFixture(), + target = "amdgcn-amd-amdhsa--gfx900"; + const inventory = await inspect(bytes, { analysis: "inventory" }); + const selected = await inspect(bytes, { targets: [target] }); + const skipped = await inspect(bytes, { + targets: ["amdgcn-amd-amdhsa--gfx1100"], + }); + if (!inventory.ok) throw inventory.error; + if (!selected.ok) throw selected.error; + if (!skipped.ok) throw skipped.error; + expect(inventory.value.objects[0]).toMatchObject({ + target, + inspection: "inventory", + sha256: null, + }); + expect(inventory.value.kernels).toEqual([]); + expect(selected.value.objects[0]).toMatchObject({ + target, + inspection: "complete", + id: inventory.value.objects[0]!.id, + }); + expect(selected.value.kernels).toHaveLength(1); + expect(selected.value.kernels[0]).toMatchObject({ + representation: "amdgpu-metadata", + parameter_bytes: 12, + }); + expect(skipped.value.objects[0]).toMatchObject({ + target, + inspection: "not-selected", + sha256: null, + }); + expect(skipped.value.kernels).toEqual([]); + expect(skipped.value.coverage.payloads).toBe("partial"); +}); +it.each([ + "hipv4-amdgcn-amd-amdhsa--gfx900", + "hipv4-amdgcn-amd-amdhsa-gfx900", + "hipv4-amdgcn-amd-amdhsa-opaque", +])("keeps discovered HIP targets selectable for %s", async (targetId) => { + const bytes = hipFixture(amdFixture(), targetId); + const inventory = await inspect(bytes, { analysis: "inventory" }); + if (!inventory.ok) throw inventory.error; + const row = inventory.value.objects.find((value) => value.kind === "amdgpu")!; + expect(row.target).not.toBeNull(); + const selected = await inspect(bytes, { targets: [row.target!] }); + if (!selected.ok) throw selected.error; + expect(selected.value.kernels).toHaveLength(1); + expect( + selected.value.objects.find((value) => value.kind === "amdgpu"), + ).toMatchObject({ + target: row.target, + payload_target: "amdgcn-amd-amdhsa--gfx900", + target_consistency: targetId.endsWith("opaque") + ? "single-source" + : "consistent", + inspection: "complete", + }); +}); +it("bounds standalone AMD note discovery before retaining metadata", async () => { + const result = await inspect( + elfFixture(224, [ + { name: ".note", type: 7, bytes: Buffer.alloc(8 * 1024 * 1024 + 1) }, + ]), + { analysis: "inventory" }, + ); + if (!result.ok) throw result.error; + expect(result.value.objects[0]).toMatchObject({ + target: null, + inspection: "capacity", + }); + expect(result.value.objects[0]!.limitations).toContain( + "AMDGPU ELF note discovery exceeds the 8 MiB retained metadata budget", + ); + expect(result.value.kernels).toEqual([]); +}); +it("reads PE raw-size fallback and ELF-host section locations", async () => { + for (const bytes of [ + peGpuFixture(), + elfFixture(62, [{ name: ".nv_fatbin", bytes: cudaFixture() }]), + ]) { + const result = await inspect(bytes); + if (!result.ok) throw result.error; + expect(result.value.kernels).toHaveLength(2); + expect(result.value.objects[0]!.location.section_index).not.toBeNull(); + expect(result.value.objects[1]!.location.container_id).toBe( + result.value.objects[0]!.id, + ); + } +}); +it("coalesces aliased CUDA metadata before expanding the temporary index", async () => { + const bytes = cubinFixture({ globalRecords: 65536, globalAliases: 1024 }); + expect(bytes.length).toBeLessThan(1024 * 1024); + const result = await inspect(bytes); + if (!result.ok) throw result.error; + expect(result.value.objects[0]!.inspection).toBe("complete"); + expect(result.value.kernels).toHaveLength(1); + expect(result.value.kernels[0]!.registers.physical).toBe(10); +}); +it("retains unknown PTX virtual counts and partial payload coverage", async () => { + const result = await inspect( + Buffer.from(ptxFixture.replace("%r<9>", "r0, r1, %r<4>, %s<8>")), + ); + if (!result.ok) throw result.error; + expect(result.value.kernels[0]!.registers.virtual[0]).toEqual({ + type: ".b32", + count: 14, + }); + const unsupported = await inspect( + Buffer.from(ptxFixture.replace("%r<9>", "%r[4]")), + ); + if (!unsupported.ok) throw unsupported.error; + expect(unsupported.value.objects[0]!.inspection).toBe("partial"); + expect(unsupported.value.coverage.payloads).toBe("partial"); + expect(unsupported.value.kernels[0]!.registers.virtual[0]).toEqual({ + type: ".b32", + count: null, + }); +}); +it.each([ + ["parameter", ".u64 output", ".u64 .ptr .global output"], + ["shared", "scratch[32]", "scratch[]"], +] as const)( + "marks unknown PTX %s declarations partial while retaining known facts", + async (kind, from, to) => { + const bytes = Buffer.from(ptxFixture.replace(from, to)); + const result = await inspect(bytes); + if (!result.ok) throw result.error; + expect(result.value.objects[0]!.inspection).toBe("partial"); + expect(result.value.coverage.payloads).toBe("partial"); + expect(result.value.ptx_artifacts[0]!.text).toBe(bytes.toString()); + const kernel = result.value.kernels[0]!; + expect(kernel.registers.virtual[0]).toEqual({ type: ".b32", count: 9 }); + expect(kernel.limitations.join(" ")).toContain(`PTX ${kind} declarations`); + if (kind === "parameter") { + expect(kernel.parameter_count).toBe(2); + expect(kernel.parameter_bytes).toBeNull(); + expect( + kernel.parameters.map(({ size, offset }) => [size, offset]), + ).toEqual([ + [null, null], + [4, null], + ]); + expect(kernel.shared_bytes).toBe(32); + } else { + expect(kernel.parameter_bytes).toBe(12); + expect(kernel.shared_bytes).toBeNull(); + } + }, +); +it.each(["whitespace", "comment"] as const)( + "recognizes standalone PTX after more than 64 KiB of leading %s", + async (kind) => { + const prefix = + kind === "whitespace" + ? " ".repeat(70 * 1024) + : `/*${"x".repeat(70 * 1024)}*/\n`; + const bytes = Buffer.from(prefix + ptxFixture); + const inventory = await inspect(bytes, { analysis: "inventory" }); + const selected = await inspect(bytes, { targets: ["sm_80"] }); + if (!inventory.ok) throw inventory.error; + if (!selected.ok) throw selected.error; + expect(inventory.value.objects[0]!.target).toBe("sm_80"); + expect(selected.value.kernels).toHaveLength(1); + expect(selected.value.kernels[0]!.location.offset).toBeGreaterThan( + 64 * 1024, + ); + }, +); +it.each(["truncated", "invalid", "extension"] as const)( + "retains earlier AMD kernel declarations after a later %s note", + async (kind) => { + const tail = Buffer.alloc(kind === "truncated" ? 3 : 24); + if (kind !== "truncated") { + tail.writeUInt32LE(7, 0); + tail.writeUInt32LE(kind === "invalid" ? 1 : 3, 4); + tail.writeUInt32LE(32, 8); + tail.write("AMDGPU\0", 12); + tail[20] = kind === "invalid" ? 0xc1 : 0xd4; + } + for (const bytes of [ + amdFixture(false, tail), + hipFixture(amdFixture(false, tail)), + ]) { + const result = await inspect(bytes); + if (!result.ok) throw result.error; + expect(result.value.kernels).toHaveLength(1); + expect(result.value.kernels[0]).toMatchObject({ + name: "add_one", + parameter_bytes: 12, + registers: { sgpr: 6, vgpr: 2 }, + }); + expect( + result.value.objects.find((row) => row.kind === "amdgpu")!.inspection, + ).toBe(kind === "extension" ? "unsupported" : "malformed"); + expect(result.value.coverage.payloads).toBe("partial"); + } + }, +); +it("bounds cumulative CUDA records across separate in-range metadata sections", async () => { + const record = Buffer.from("042f0800010000000a000000", "hex"), + records = Buffer.alloc(record.length * 131073); + for (let offset = 0; offset < records.length; offset += record.length) + record.copy(records, offset); + const result = await inspect( + elfFixture(190, [ + { name: ".nv.info", type: 0x70000000, bytes: records }, + { name: ".nv.info", type: 0x70000000, bytes: records }, + ]), + ); + if (!result.ok) throw result.error; + expect(result.value.objects[0]!.inspection).toBe("capacity"); + expect(result.value.objects[0]!.limitations.join(" ")).toContain( + "aggregate record/symbol visits", + ); +}); +it.each(["section-names", "symbols"] as const)( + "retains partial CUBIN coverage when %s are unavailable", + async (missing) => { + const bytes = cubinFixture(); + if (missing === "section-names") bytes.writeUInt16LE(0, 62); + else bytes.writeUInt32LE(1, Number(bytes.readBigUInt64LE(40)) + 3 * 64 + 4); + const result = await inspect(bytes); + if (!result.ok) throw result.error; + expect(result.value.objects[0]!.inspection).toBe("partial"); + expect(result.value.coverage.payloads).toBe("partial"); + expect(result.value.kernels).toHaveLength(missing === "symbols" ? 0 : 1); + expect(result.value.objects[0]!.limitations.join(" ")).toContain( + missing === "symbols" + ? "kernel declaration coverage is unknown" + : "named metadata coverage is partial", + ); + }, +); +it("preserves parameters for long mangled names and rejects conflicting register counts", async () => { + const name = "kernel_" + "argument".repeat(64); + const result = await inspect( + cubinFixture({ name, textRegisters: 9, globalRegisters: 10 }), + ); + if (!result.ok) throw result.error; + expect(result.value.kernels[0]).toMatchObject({ + name, + parameter_count: 2, + registers: { physical: null }, + }); + expect(result.value.kernels[0]!.limitations.join(" ")).toContain( + "Conflicting", + ); +}); +it("iterates flat concatenated fatbins beyond the genuine container nesting limit", async () => { + const result = await inspect( + Buffer.concat(Array.from({ length: 20 }, () => cudaFixture())), + ); + if (!result.ok) throw result.error; + expect(result.value.objects).toHaveLength(40); + expect(result.value.kernels).toHaveLength(40); + expect(result.value.coverage.discovery).toBe("complete"); +}); +it("reports checksum corruption and unknown compression as partial, retaining known entries", async () => { + const corrupt = ccobFixture(); + corrupt[16] = corrupt[16]! ^ 1; + const result = await inspect(corrupt); + if (!result.ok) throw result.error; + expect(result.value.coverage.discovery).toBe("partial"); + expect(result.value.objects[0]!.inspection).toBe("malformed"); + const unknown = ccobFixture(); + unknown.writeUInt16LE(99, 6); + const unsupported = await inspect(unknown); + if (!unsupported.ok) throw unsupported.error; + expect(unsupported.value.objects[0]!.inspection).toBe("unsupported"); +}); +it("rejects out-of-range bundle entries without guessing a payload", async () => { + const bytes = hipFixture(); + bytes.writeBigUInt64LE(BigInt(bytes.length + 1), 32); + const result = await inspect(bytes); + expect(result).toMatchObject({ + ok: false, + error: { _tag: "BinaryTargetError" }, + }); +}); + +it("preserves standalone PTX BOM and trailing NUL bytes in the retained text artifact", async () => { + const bytes = Buffer.from("\uFEFF" + ptxFixture + "\0"); + const result = await inspect(bytes); + if (!result.ok) throw result.error; + expect(Buffer.from(result.value.ptx_artifacts[0]!.text)).toEqual(bytes); + expect(result.value.ptx_artifacts[0]!.byte_length).toBe(bytes.length); +}); +it("rejects a changed full-file identity before applying positional object selection", async () => { + const bytes = cudaFixture(); + const result = await inspect(bytes, { + expected_sha256: "0".repeat(64), + object_ids: ["file@10"], + }); + expect(result).toMatchObject({ + ok: false, + error: { _tag: "AnalysisArtifactChangedError" }, + }); +}); +it("iterates mixed plain/CCOB bundles without counting flat records as nesting", async () => { + const records = Array.from({ length: 20 }, (_, index) => + index % 2 === 0 ? hipFixture() : ccobFixture(), + ); + const result = await inspect(Buffer.concat(records)); + if (!result.ok) throw result.error; + expect(result.value.kernels).toHaveLength(20); + expect(result.value.coverage.discovery).toBe("complete"); +}); +it("continues at the declared CCOB boundary after an unsupported codec", async () => { + const unknown = ccobFixture(); + unknown.writeUInt16LE(99, 6); + const result = await inspect(Buffer.concat([unknown, ccobFixture()])); + if (!result.ok) throw result.error; + expect(result.value.kernels).toHaveLength(1); + expect(result.value.coverage.discovery).toBe("partial"); +}); + +it("refuses escaped PTX report expansion while preserving bounded interrupted facts", async () => { + const text = ptxFixture + "\n//" + "\u0001".repeat(3 * 1024 * 1024); + const result = await inspect(Buffer.from(text)); + expect(result).toMatchObject({ + ok: false, + error: { _tag: "AnalysisResourceConstraintError" }, + }); + if (result.ok) throw new Error("expected representation capacity refusal"); + const partial = gpuCodeObjectsPartialSchema.parse( + parseEvidence(result.error.partialObservation).normalized_result, + ); + expect(partial.ptx_artifacts).toEqual([]); + expect(partial.objects).toHaveLength(1); + expect(partial.artifact.sha256).toBeNull(); +}); diff --git a/tests/fixtures/gpu/codeObjects.ts b/tests/fixtures/gpu/codeObjects.ts new file mode 100644 index 0000000000..5eb8165065 --- /dev/null +++ b/tests/fixtures/gpu/codeObjects.ts @@ -0,0 +1,301 @@ +import { createHash } from "node:crypto"; +import { deflateSync, zstdCompressSync } from "node:zlib"; +import { encode } from "@msgpack/msgpack"; +export const ptxFixture = `// Producer-shaped static PTX; portable tests do not invoke a GPU compiler. +.version 8.8 +.target sm_80 +.address_size 64 +.visible .entry add_one(.param .u64 output, .param .u32 value) { +.reg .b32 %r<9>; +.reg .b64 %rd<5>; +.shared .align 4 .b8 scratch[32]; +ret; +} +`; +export interface ElfFixtureSection { + name: string; + type?: number; + flags?: bigint; + bytes?: Buffer; + size?: number; + info?: number; + link?: number; + entrySize?: number; +} +/** Format boundary fixtures, including numeric records checked against nvcc 12.9 cuobjdump. */ +export const elfFixture = ( + machine: number, + sections: ElfFixtureSection[], + version = 1, +): Buffer => { + const names = Buffer.from( + `\0.shstrtab\0${sections.map((row) => row.name).join("\0")}\0`, + ); + const rows = [ + { name: "", type: 0, bytes: Buffer.alloc(0) }, + { name: ".shstrtab", type: 3, bytes: names }, + ...sections, + ]; + let position = 64; + const entries = rows.map((row) => { + position = Math.ceil(position / 8) * 8; + const offset = position; + if (row.type !== 8) position += row.bytes?.length ?? 0; + return { row, offset }; + }); + const table = Math.ceil(position / 8) * 8; + const result = Buffer.alloc(table + rows.length * 64); + result.set([ + 0x7f, + 0x45, + 0x4c, + 0x46, + 2, + 1, + 1, + machine === 190 ? 0x33 : machine === 224 ? 0x40 : 0, + ]); + result.writeUInt16LE(2, 16); + result.writeUInt16LE(machine, 18); + result.writeUInt32LE(version, 20); + result.writeBigUInt64LE(BigInt(table), 40); + result.writeUInt32LE(machine === 190 ? 0x500550 : 0x12c, 48); + result.writeUInt16LE(64, 52); + result.writeUInt16LE(64, 58); + result.writeUInt16LE(rows.length, 60); + result.writeUInt16LE(1, 62); + for (let index = 1; index < entries.length; index++) { + const { row, offset } = entries[index]!, + h = table + index * 64; + result.writeUInt32LE(names.indexOf(Buffer.from(`${row.name}\0`)), h); + result.writeUInt32LE(row.type ?? 1, h + 4); + result.writeBigUInt64LE(row.flags ?? 0n, h + 8); + result.writeBigUInt64LE(BigInt(offset), h + 24); + result.writeBigUInt64LE(BigInt(row.size ?? row.bytes?.length ?? 0), h + 32); + result.writeUInt32LE(row.link ?? 0, h + 40); + result.writeUInt32LE(row.info ?? 0, h + 44); + result.writeBigUInt64LE(BigInt(row.entrySize ?? 0), h + 56); + if (row.type !== 8) row.bytes?.copy(result, offset); + } + return result; +}; +export const cubinFixture = ( + options: { + shared?: number; + textRegisters?: number; + globalRegisters?: number; + name?: string; + globalRecords?: number; + globalAliases?: number; + architecture?: number; + osAbi?: number; + flags?: number; + } = {}, +): Buffer => { + const name = options.name ?? "add_one"; + const symbols = Buffer.alloc(48); + symbols.writeUInt32LE(1, 24); + symbols[28] = 0x12; + symbols[29] = 0x10; + symbols.writeUInt16LE(4, 30); + symbols.writeBigUInt64LE(16n, 40); + const record = Buffer.from("042f0800010000000a000000", "hex"); + record.writeUInt32LE(options.globalRegisters ?? 10, 8); + const global = Buffer.alloc(record.length * (options.globalRecords ?? 1)); + for (let offset = 0; offset < global.length; offset += record.length) + record.copy(global, offset); + const info = Buffer.from( + "03190c0004170c00000000000100080000f0110004170c00000000000000000000f02100", + "hex", + ); + const result = elfFixture( + 190, + [ + { name: ".strtab", type: 3, bytes: Buffer.from(`\0${name}\0`) }, + { name: ".symtab", type: 2, link: 2, entrySize: 24, bytes: symbols }, + { + name: `.text.${name}`, + flags: 0x100006n, + info: ((options.textRegisters ?? 0) << 24) | 1, + bytes: Buffer.alloc(16), + }, + { name: ".nv.info", type: 0x70000000, link: 3, bytes: global }, + { + name: `.nv.info.${name}`, + type: 0x70000000, + link: 3, + info: 4, + bytes: info, + }, + { + name: `.nv.shared.${name}`, + type: 8, + info: 4, + size: options.shared ?? 1056, + }, + ...Array.from({ length: (options.globalAliases ?? 1) - 1 }, () => ({ + name: ".nv.info", + type: 0x70000000, + link: 3, + bytes: Buffer.alloc(0), + })), + ], + 0x81, + ); + if (options.architecture !== undefined) + result.writeUInt32LE( + (result.readUInt32LE(48) & ~0xff) | options.architecture, + 48, + ); + const table = Number(result.readBigUInt64LE(40)); + if (options.osAbi !== undefined) { + result[7] = options.osAbi; + result.writeUInt32LE(1, 20); + } + if (options.flags !== undefined) result.writeUInt32LE(options.flags, 48); + for (let index = 8; index < result.readUInt16LE(60); index++) { + result.writeBigUInt64LE( + result.readBigUInt64LE(table + 5 * 64 + 24), + table + index * 64 + 24, + ); + result.writeBigUInt64LE(BigInt(global.length), table + index * 64 + 32); + } + return result; +}; +export const amdFixture = (hidden = false, trailingNote?: Buffer): Buffer => { + const args = [ + { ".offset": 0, ".size": 8, ".value_kind": "global_buffer" }, + { + ".offset": 8, + ".size": 4, + ".value_kind": hidden ? "hidden_global_offset_x" : "by_value", + }, + ]; + const metadata = Buffer.from( + encode({ + "amdhsa.target": "amdgcn-amd-amdhsa--gfx900", + "amdhsa.version": [1, 1], + "amdhsa.kernels": [ + { + ".name": "add_one", + ".symbol": "add_one.kd", + ".args": args, + ".kernarg_segment_size": 12, + ".group_segment_fixed_size": 0, + ".private_segment_fixed_size": 0, + ".sgpr_count": 6, + ".vgpr_count": 2, + }, + ], + }), + ); + const note = Buffer.alloc(20 + Math.ceil(metadata.length / 4) * 4); + note.writeUInt32LE(7, 0); + note.writeUInt32LE(metadata.length, 4); + note.writeUInt32LE(32, 8); + note.write("AMDGPU\0", 12); + metadata.copy(note, 20); + return elfFixture(224, [ + { + name: ".note", + type: 7, + bytes: + trailingNote === undefined ? note : Buffer.concat([note, trailingNote]), + }, + ]); +}; +export const cudaFixture = ( + compression: "none" | "zstd" = "none", + entries = [ + { kind: 1, target: 80, bytes: Buffer.from(ptxFixture) }, + { kind: 2, target: 90, bytes: cubinFixture({ architecture: 90 }) }, + ], +): Buffer => { + const records = entries.map(({ kind, target, bytes }) => { + const compressed = compression === "zstd", + data = compressed ? zstdCompressSync(bytes) : bytes; + const result = Buffer.alloc(64 + Math.ceil(data.length / 8) * 8); + result.writeUInt16LE(kind, 0); + result.writeUInt16LE(0x101, 2); + result.writeUInt32LE(64, 4); + result.writeBigUInt64LE(BigInt(result.length - 64), 8); + result.writeUInt32LE(compressed ? data.length : 0, 16); + result.writeUInt32LE(target, 28); + result.writeBigUInt64LE(compressed ? 0x8011n : 0x11n, 40); + result.writeBigUInt64LE(compressed ? BigInt(bytes.length) : 0n, 56); + data.copy(result, 64); + return result; + }); + const header = Buffer.alloc(16); + header.writeUInt32LE(0xba55ed50); + header.writeUInt16LE(1, 4); + header.writeUInt16LE(16, 6); + header.writeBigUInt64LE( + BigInt(records.reduce((n, row) => n + row.length, 0)), + 8, + ); + return Buffer.concat([header, ...records]); +}; +export const hipFixture = ( + payload = amdFixture(), + targetId = "hipv4-amdgcn-amd-amdhsa--gfx900", +): Buffer => { + const ids = ["host-x86_64-unknown-linux", targetId]; + const headers = ids.map((id) => Buffer.alloc(24 + Buffer.byteLength(id))); + const start = 32 + headers.reduce((total, row) => total + row.length, 0); + for (let index = 0; index < headers.length; index++) { + const row = headers[index]!; + row.writeBigUInt64LE(BigInt(start)); + row.writeBigUInt64LE(index === 0 ? 0n : BigInt(payload.length), 8); + row.writeBigUInt64LE(BigInt(Buffer.byteLength(ids[index]!)), 16); + row.write(ids[index]!, 24); + } + const head = Buffer.alloc(32); + head.write("__CLANG_OFFLOAD_BUNDLE__"); + head.writeBigUInt64LE(2n, 24); + return Buffer.concat([head, ...headers, payload]); +}; +export const ccobFixture = ( + version: 1 | 2 | 3 = 2, + method: "zlib" | "zstd" = "zstd", + bundle = hipFixture(), +): Buffer => { + const compressed = + method === "zstd" ? zstdCompressSync(bundle) : deflateSync(bundle); + const width = version === 1 ? 20 : version === 2 ? 24 : 32, + head = Buffer.alloc(width); + head.write("CCOB"); + head.writeUInt16LE(version, 4); + head.writeUInt16LE(method === "zstd" ? 1 : 0, 6); + if (version === 3) { + head.writeBigUInt64LE(BigInt(width + compressed.length), 8); + head.writeBigUInt64LE(BigInt(bundle.length), 16); + } else if (version === 2) { + head.writeUInt32LE(width + compressed.length, 8); + head.writeUInt32LE(bundle.length, 12); + } else head.writeUInt32LE(bundle.length, 8); + createHash("md5") + .update(bundle) + .digest() + .copy(head, width - 8, 0, 8); + return Buffer.concat([head, compressed]); +}; +export const peGpuFixture = (payload = cudaFixture()): Buffer => { + const result = Buffer.alloc(512 + payload.length), + pe = 64, + optionalSize = 240, + section = pe + 24 + optionalSize; + result.write("MZ"); + result.writeUInt32LE(pe, 60); + result.write("PE\0\0", pe); + result.writeUInt16LE(0x8664, pe + 4); + result.writeUInt16LE(1, pe + 6); + result.writeUInt16LE(optionalSize, pe + 20); + result.writeUInt16LE(0x20b, pe + 24); + result.write(".nv_fatb", section); + result.writeUInt32LE(payload.length, section + 8); + result.writeUInt32LE(payload.length, section + 16); + result.writeUInt32LE(512, section + 20); + payload.copy(result, 512); + return result; +}; diff --git a/tests/process-global/gpuFileLifetime.test.ts b/tests/process-global/gpuFileLifetime.test.ts new file mode 100644 index 0000000000..3122e74e54 --- /dev/null +++ b/tests/process-global/gpuFileLifetime.test.ts @@ -0,0 +1,311 @@ +import { expect, it, vi } from "vitest"; +import { open, writeFile, unlink, truncate } from "node:fs/promises"; +import type { FileHandle } from "node:fs/promises"; +import { join } from "node:path"; +import { GpuCodeObjectsProvider } from "../../src/gpu/GpuCodeObjectsProvider.js"; +import { parseEvidence } from "../../src/domain/evidence.js"; +import { + gpuCodeObjectsPartialSchema, + gpuCodeObjectsSchema, +} from "../../src/domain/gpu/gpuCodeObjects.js"; +import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js"; +import { amdFixture, cudaFixture } from "../fixtures/gpu/codeObjects.js"; +type ReadFile = ( + buffer: Buffer, + offset: number, + length: number, + position: number, +) => Promise<{ bytesRead: number; buffer: Buffer }>; +const fixture = async () => { + const root = await createTestTempDirectory("rea-gpu-lifetime-"), + path = join(root, "cuda"), + bytes = cudaFixture(); + await writeFile(path, bytes); + const handle = await open(path, "r"); + const prototype = Object.getPrototypeOf(handle) as { read: ReadFile }; + await handle.close(); + return { path, bytes, prototype, provider: new GpuCodeObjectsProvider() }; +}; +const interceptClose = ( + prototype: { read: ReadFile }, + override: (close: () => Promise) => Promise, +): (() => void) => { + const read = prototype.read; + let attached = false, + restoreHandle: (() => void) | undefined; + const spy = vi.spyOn(prototype, "read").mockImplementation(async function ( + this: FileHandle, + ...args: Parameters + ) { + if (!attached) { + attached = true; + const close = this.close.bind(this); + const closeSpy = vi + .spyOn(this, "close") + .mockImplementation(() => override(close)); + restoreHandle = () => closeSpy.mockRestore(); + } + return read.apply(this, args); + }); + return () => { + spy.mockRestore(); + restoreHandle?.(); + }; +}; +it("reserves the inspection work budget before reading aliased HIP payloads", async () => { + const root = await createTestTempDirectory("rea-gpu-aliased-hip-"), + path = join(root, "aliased-hip"), + payload = amdFixture(), + payloadSize = 128 * 1024 * 1024, + id = Buffer.from("hipv4-amdgcn-amd-amdhsa--gfx900"), + count = 12, + start = 32 + count * (24 + id.length), + directory = Buffer.alloc(start); + directory.write("__CLANG_OFFLOAD_BUNDLE__"); + directory.writeBigUInt64LE(BigInt(count), 24); + for (let index = 0; index < count; index++) { + const at = 32 + index * (24 + id.length); + directory.writeBigUInt64LE(BigInt(start), at); + directory.writeBigUInt64LE(BigInt(payloadSize), at + 8); + directory.writeBigUInt64LE(BigInt(id.length), at + 16); + id.copy(directory, at + 24); + } + const file = await open(path, "w+"); + try { + await file.write(directory, 0, directory.length, 0); + await file.write(payload, 0, payload.length, start); + await file.truncate(start + payloadSize); + } finally { + await file.close(); + } + const handle = await open(path, "r"), + prototype = Object.getPrototypeOf(handle) as { read: ReadFile }; + await handle.close(); + const read = prototype.read; + let payloadReads = 0; + const spy = vi.spyOn(prototype, "read").mockImplementation(async function ( + this: FileHandle, + ...args: Parameters + ) { + if (args[3] === start && args[2] === payloadSize) payloadReads++; + return read.apply(this, args); + }); + const provider = new GpuCodeObjectsProvider(); + try { + const result = await provider.inspect({ path, analysis: "kernels" }); + if (!result.ok) throw result.error; + expect(payloadReads).toBe(4); + expect(result.value.objects.map((row) => row.inspection)).toEqual([ + ...Array(4).fill("complete"), + ...Array(8).fill("capacity"), + ]); + expect(result.value.coverage.payloads).toBe("partial"); + expect(result.value.kernels).toHaveLength(4); + expect(result.value.objects[4]!.sha256).toBeNull(); + expect(result.value.objects[4]!.limitations.join(" ")).toContain( + "512 MiB per inspection", + ); + } finally { + spy.mockRestore(); + await provider.close(); + } +}); +it("retains completed PTX facts and unknown root identity when a later payload is cancelled", async () => { + const { path, bytes, prototype, provider } = await fixture(); + const controller = new AbortController(), + reason = { selected: "caller-cancel" }; + const read = prototype.read; + const secondHeader = 16 + 64 + Number(bytes.readBigUInt64LE(24)); + const spy = vi.spyOn(prototype, "read").mockImplementation(async function ( + this: FileHandle, + ...args: Parameters + ) { + const result = await read.apply(this, args); + if (args[3] === secondHeader && args[2] === 64) controller.abort(reason); + return result; + }); + try { + const result = await provider.inspect( + { path, analysis: "kernels" }, + { signal: controller.signal }, + ); + expect(result).toMatchObject({ + ok: false, + error: { _tag: "AnalysisCancelledError", cause: reason }, + }); + if (result.ok) throw new Error("expected cancellation"); + const evidence = parseEvidence(result.error.partialObservation); + const report = gpuCodeObjectsPartialSchema.parse( + evidence.normalized_result, + ); + expect(report.artifact).toMatchObject({ + sha256: null, + file_identity: "unverified", + }); + expect(report.kernels[0]!.name).toBe("add_one"); + expect(report.ptx_artifacts).toHaveLength(1); + } finally { + spy.mockRestore(); + await provider.close(); + } +}); +it("retains a failed descriptor close, blocks admission until retry and clears the exact cleanup marker", async () => { + const { path, prototype, provider } = await fixture(); + let failures = 2; + const restore = interceptClose(prototype, async (close) => { + if (failures-- > 0) throw new Error("owned close failure"); + await close(); + }); + try { + const result = await provider.inspect({ path, analysis: "kernels" }); + if (result.ok) throw new Error("expected cleanup failure"); + expect(result.error.cleanupIncomplete).toBe(true); + expect( + gpuCodeObjectsSchema.parse( + parseEvidence(result.error.partialObservation).normalized_result, + ).kernels, + ).toHaveLength(2); + const blocked = await provider.inspect({ path, analysis: "inventory" }); + expect(blocked.ok).toBe(false); + const reopened = await provider.inspect({ path, analysis: "inventory" }); + expect(reopened.ok).toBe(true); + expect(result.error.cleanupIncomplete).toBe(false); + } finally { + restore(); + await provider.close(); + } +}); +it("preserves completed observations when caller cancellation arrives during final close", async () => { + const { path, prototype, provider } = await fixture(); + const controller = new AbortController(); + const reason = { selected: "final-close-cancel" }; + const restore = interceptClose(prototype, async (close) => { + await close(); + controller.abort(reason); + }); + try { + const result = await provider.inspect( + { path, analysis: "kernels" }, + { signal: controller.signal }, + ); + if (result.ok) throw new Error("expected final-close cancellation"); + expect(result.error).toMatchObject({ + _tag: "AnalysisCancelledError", + cause: reason, + }); + expect( + gpuCodeObjectsSchema.parse( + parseEvidence(result.error.partialObservation).normalized_result, + ).kernels, + ).toHaveLength(2); + expect(result.error.cleanupIncomplete).toBe(false); + } finally { + restore(); + await provider.close(); + } +}); + +it("joins an admitted file read during shutdown and rejects overlapping work", async () => { + const { path, prototype, provider } = await fixture(); + let entered!: () => void, release!: () => void; + const admitted = new Promise((resolve) => { + entered = resolve; + }), + barrier = new Promise((resolve) => { + release = resolve; + }); + const read = prototype.read; + let blocked = false; + const spy = vi.spyOn(prototype, "read").mockImplementation(async function ( + this: FileHandle, + ...args: Parameters + ) { + if (!blocked) { + blocked = true; + entered(); + await barrier; + } + return read.apply(this, args); + }); + let active: ReturnType | undefined; + let closing: Promise | undefined; + try { + active = provider.inspect({ path, analysis: "kernels" }); + await admitted; + let closed = false; + closing = provider.close().then(() => { + closed = true; + }); + const overlap = await provider.inspect({ path, analysis: "inventory" }); + expect(overlap).toMatchObject({ + ok: false, + error: { _tag: "AnalysisResourceConstraintError" }, + }); + expect(closed).toBe(false); + release(); + const result = await active; + expect(result.ok).toBe(true); + await closing; + expect(closed).toBe(true); + } finally { + release(); + await active; + await closing; + spy.mockRestore(); + await provider.close(); + } +}); + +it("classifies removal of an admitted named source as changed identity", async () => { + const { path, prototype, provider } = await fixture(); + const read = prototype.read; + let removed = false; + const spy = vi.spyOn(prototype, "read").mockImplementation(async function ( + this: FileHandle, + ...args: Parameters + ) { + const result = await read.apply(this, args); + if (!removed) { + removed = true; + await unlink(path); + } + return result; + }); + try { + const result = await provider.inspect({ path, analysis: "kernels" }); + expect(result).toMatchObject({ + ok: false, + error: { _tag: "AnalysisArtifactChangedError" }, + }); + } finally { + spy.mockRestore(); + await provider.close(); + } +}); +it("reports a source shortened during the streaming hash as changed identity", async () => { + const { path, prototype, provider } = await fixture(); + await writeFile(path, Buffer.alloc(2 * 1024 * 1024)); + const read = prototype.read; + let shortened = false; + const spy = vi.spyOn(prototype, "read").mockImplementation(async function ( + this: FileHandle, + ...args: Parameters + ) { + const result = await read.apply(this, args); + if (!shortened) { + shortened = true; + await truncate(path, 0); + } + return result; + }); + try { + const result = await provider.inspect({ path, analysis: "inventory" }); + expect(result).toMatchObject({ + ok: false, + error: { _tag: "AnalysisArtifactChangedError" }, + }); + } finally { + spy.mockRestore(); + await provider.close(); + } +});