From 81f9f71f6d7d564456381bf7c7d278f0335ea31b Mon Sep 17 00:00:00 2001 From: Entrpi <133157614+Entrpi@users.noreply.github.com> Date: Fri, 6 Nov 2026 23:48:59 +1100 Subject: [PATCH] feat(rizin): select the decoder and map address for one-shot commands (#8991) * feat(rizin): select the decoder and map address for one-shot commands execute_rizin_command and rea execute-rizin-command accept optional asm_arch, asm_bits, asm_cpu and base_address (CLI --asm-arch, --asm-bits, --asm-cpu and --base-address). REA passes them to Rizin before the artifact path as -a, -b, -e asm.cpu= and -m. Rizin 0.9.1 refuses -e asm.arch= and -e asm.bits= at startup, while -a and -b work in 0.7.4 and 0.9.1. Rizin applies -e settings after -a and -b. base_address is the address of file offset 0, as in REA's raw-binary inputs. REA checks only the spelling, because Rizin owns the arch and CPU vocabulary and its builds differ. The Homebrew build of Rizin 0.9.1 has no xtensa or riscv plugin; the official static Linux build of 0.7.4 has both. Evidence records each input and the complete argument list. When any input is set, Evidence also states that REA does not confirm the decoder or map that Rizin used: Rizin uses another value for one it cannot apply and exits 0. Rizin 0.9.1 keeps its default and reports the value on stderr, and Rizin 0.7.4 turned -b 8 for arm into 16 without a message. Calls without these inputs keep their argument list and Evidence. The real-provider gate tests/boundary/providers/RizinDecoderInputs.test.ts checks the effect of each flag through MCP with Rizin's own e asm.*, omlj and byte reads. It skips when no Rizin is on PATH. It passes with Rizin 0.9.1 (Homebrew, macOS arm64) and 0.7.4 (official static build, Linux x86_64). * docs(rizin): map multi-segment ESP32 images with om An ESP32-family app or bootloader image holds several segments, each loaded at its own address, so one base_address cannot map it. The recipe removes the default map with om-* and adds one om map per segment, using the segment offsets from the image format or from esptool image-info. It does not depend on an ESP image reader in REA. The offsets were checked against esptool 4.12.0 image_info on a synthetic two-segment ESP32 image. Rizin 0.9.1 and 0.7.4 mapped its segments at their load addresses. Rizin 0.7.4, whose static Linux build has the xtensa plugin, decoded the IRAM segment as nop.n, nop.n, ret.n and nop.n. * fix(rizin): read the file at base_address when a loader would claim it On Rizin 0.9.1, base_address could read 0xff at the mapped address with exit 0. -m maps the file, but Rizin still runs its format loaders, and a loose one can claim a raw image. Rizin 0.9.1's AVR loader matches when bytes 1 and 3 look like rjmp and turns io.va off, so reads use file offsets. The BIOS loader, in 0.7.4 and 0.9.1, matches a 64 KiB file with a jmp 16 bytes from its end and maps it at another address. An acceptance run on a raw ESP32-S3 segment hit the first case. With base_address, REA now passes -n together with -m. Rizin then skips the loaders and maps the whole file at the address: rizin.c calls rz_io_map_new at the -m address and leaves io.va at its default. The input therefore means a raw image, and Rizin loads no sections, symbols or entry point, even from an ELF file. Evidence states this in its own limitation, and the decoder limitation no longer covers the map. The real-provider gate adds an AVR-like and a BIOS-like image and reads below the base, at it, at the end of the image and past it. Without -n it fails on Rizin 0.9.1. It passes with Rizin 0.9.1 (Homebrew, macOS arm64) and 0.7.4 (official static build, Linux x86_64). The ESP om recipe now starts with e io.va=true, because reads use the maps only while io.va is on. * fix(rizin): advertise name patterns that compile in v mode The asm_arch and asm_cpu pattern ended its character class with an unescaped '-', which JavaScript's v (unicodeSets) mode rejects, so clients compiling advertised patterns in that mode could not use execute_rizin_command's schema. Escaping it keeps the same names valid in every mode, as main's own patterns write it. * fix(rizin): state what the decoder and map inputs leave unconfirmed The base_address limitation reported a map REA never observes and left out that -n keeps Rizin's default arch, bits and byte order. The asm_* limitation implied Rizin replaces a bad CPU, but e asm.cpu echoes any value. Compact schemas drop property descriptions, so the tool description now carries the raw-loading warning. The real-provider gate checks that asm_cpu changes a decode, and a base_address-only call. * docs(rizin): record the gate's full run on Rizin 0.7.4 * test(rizin): follow the test guide in the decoder input tests Focused tests resolve execute_rizin_command from its contract family, the harness owns the temporary directory before setup can fail, and guidance is compared with its owner, not with sentences from it. --- docs/reverse-engineering-tools.md | 114 ++++++++ docs/testing-lanes.md | 15 + scripts/ci/scopes.mjs | 1 + .../reverse/ReverseEngineeringCommandPort.ts | 2 + .../reverse/ReverseEngineeringService.ts | 1 + src/cli/reverseEngineeringCommands.ts | 17 ++ src/composition/reverseEngineering.ts | 6 +- .../reverseEngineeringToolContracts.ts | 2 +- src/domain/rawBinary.ts | 3 +- src/domain/reverseEngineering.ts | 39 +++ src/rizin/RizinCommand.ts | 27 ++ .../cli/reverseEngineeringTargetInput.test.ts | 69 +++++ .../reverseEngineeringEnvironmentMcp.test.ts | 93 +++++++ .../providers/RizinDecoderInputs.test.ts | 259 ++++++++++++++++++ tests/composition/reverseEngineering.test.ts | 148 ++++++++++ tsconfig.reverse-tools-mutation.json | 1 + vitest.reverse-tools.config.ts | 1 + 17 files changed, 795 insertions(+), 3 deletions(-) create mode 100644 tests/boundary/providers/RizinDecoderInputs.test.ts diff --git a/docs/reverse-engineering-tools.md b/docs/reverse-engineering-tools.md index 6136a5f9f3..3fb605acc9 100644 --- a/docs/reverse-engineering-tools.md +++ b/docs/reverse-engineering-tools.md @@ -124,6 +124,120 @@ rea execute-rizin-command ./program iI Captured stdout and stderr are retained up to a 16 MiB combined limit; `output_truncated` marks output beyond that limit. One-shot commands use the `command_timeout_ms` budget described above. That 16 MiB cap has no runtime override. +### Decoder and map inputs + +Four optional inputs select how Rizin decodes the artifact and where it maps +it. REA passes each one to Rizin before the artifact path: + +| Input | CLI option | Rizin argument | Example | +| -------------- | ---------------- | -------------- | -------------------- | +| `asm_arch` | `--asm-arch` | `-a` | `arm`, `x86`, `6502` | +| `asm_bits` | `--asm-bits` | `-b` | `16` for Thumb | +| `asm_cpu` | `--asm-cpu` | `-e asm.cpu=` | `cortexm` | +| `base_address` | `--base-address` | `-n -m` | `0x08000000` | + +Rizin 0.9.1 refuses `-e asm.arch=` and `-e asm.bits=` at startup, and Rizin +0.7.4 accepts them, so REA uses `-a` and `-b`, which both accept. Rizin applies +`-e` settings after `-a` and `-b`, so `asm_cpu` applies to the selected arch. + +`base_address` is the address of file offset 0, as in REA's raw-binary inputs. +REA passes it as `-m` together with `-n`, so Rizin maps the whole file there as +a raw image and loads no format information, such as sections, symbols or an +entry point, even from an ELF file. Without `-n`, a Rizin format loader can +claim a raw image and give wrong reads with no error. In Rizin 0.9.1 the AVR +loader turns `io.va` off, so every read at the mapped address returns `0xff`. +In Rizin 0.7.4 and 0.9.1 the BIOS loader maps the file at another address. +A loader can also claim a raw image that you open with only `asm_*` inputs, so +pass `base_address`, `0x0` if the image has no known address. Evidence records +each input and the complete argument list. + +With `-n`, Rizin has no headers to read, so arch, bits and byte order stay at +its defaults unless the call selects them. A Thumb image opened with only +`base_address` decodes as plausible AArch64 on an arm64 host. Pass `asm_arch` +and `asm_bits` with `base_address`, and for a big-endian image start the command +with `e cfg.bigendian=true`. + +REA checks only the spelling. Arch and CPU names contain letters, digits, `_`, +`.`, `+` and `-`, and start with a letter or a digit. `asm_bits` is an integer +from 1 to 64. `base_address` is `0x` followed by 1 to 16 hexadecimal digits. +Rizin owns the names, and its builds differ: for example, the Homebrew build of +Rizin 0.9.1 has no `xtensa` or `riscv` plugin, while the official static Linux +build of Rizin 0.7.4 has both. The commands `e asm.arch=?` and `e asm.cpu=?` +list what the installed Rizin supports. To use another build, set +`REA_RIZIN_COMMAND`. When Rizin cannot apply an arch or bits value, it uses +another value and exits with status 0. Rizin 0.9.1 keeps its default and +reports the value on stderr. Rizin 0.7.4 reports an unknown arch on stderr, but +for `arm` it used 16 bits instead of 8 without a message. Both accept any +`asm.cpu` value, and `e asm.cpu` then echoes it, so compare it with +`e asm.cpu=?`, which lists the CPUs of the selected arch. Evidence therefore +states that REA does not confirm the decoder that Rizin used. To check the +decoder and the map, start the command with +`e asm.arch; e asm.bits; e asm.cpu; e cfg.bigendian; oml`. + +This call reads a Cortex-M flash dump at its address, in Thumb state: + +```sh +rea execute-rizin-command ./flash.bin "e asm.arch; e asm.bits; oml; pxw 8 @ 0x08000000" --asm-arch arm --asm-bits 16 --asm-cpu cortexm --base-address 0x08000000 +``` + +Through MCP, the same call is: + +```json +{ + "path": "/firmware/flash.bin", + "command": "e asm.arch; e asm.bits; oml; pxw 8 @ 0x08000000", + "asm_arch": "arm", + "asm_bits": 16, + "asm_cpu": "cortexm", + "base_address": "0x08000000" +} +``` + +`pxw 8` prints the initial stack pointer and the Reset vector. The Reset vector +is a Thumb address with its low bit set. Disassemble the handler at the even +address, for example `pd 16 @ 0x08000108` for a Reset vector of `0x08000109`. + +### Multi-segment ESP images + +An app or bootloader image of the ESP32 family (the ESP32, ESP32-S, ESP32-C, +ESP32-H and ESP32-P series) starts with the magic byte `0xE9` and holds several +segments. ESP8266 images use a different header, and this recipe does not +cover them. Each segment loads at its own address, so one `base_address` cannot +map the image. Instead, remove the default map and add one `om` map for each +segment, in the same command: + +1. Find the load address and the length of each segment, and the file offset + of its data. The image header is 24 bytes. Each segment starts with an + 8-byte header that holds the load address and the data length, both 32-bit + little-endian, and its data follows. So the data of the first segment + starts at file offset `0x20`, and each later segment header follows the + data of the segment before it. `esptool image-info` lists the same + segments; esptool v4 spells it `image_info --version 2`. Its `File offs` + column is the offset of the 8-byte segment header, so the data starts 8 + bytes later. A padding segment carries no code or data, and you can skip + it. +2. Start the command with `e io.va=true`. Reads use the maps only while + `io.va` is on, and a Rizin format loader can turn it off. +3. Rizin opens the image as file descriptor 3, which `ol` shows. For each + segment, add this map, with `r-x` for code and `r--` for data: + `om 3 `. A map + cannot have more permissions than the opened file, which is read-only. +4. Select the decoder with `asm_arch` and set `asm_bits` to 32. ESP32, ESP32-S2 + and ESP32-S3 use Xtensa. The ESP32-C and ESP32-H series and ESP32-P4 use + RISC-V. Use the plugin name that `e asm.arch=?` lists for the installed + Rizin. + +For example, for an image with an IRAM segment of 8 bytes at `0x40080000` and +a DRAM segment of 16 bytes at `0x3ffb0000`: + +```sh +rea execute-rizin-command ./app.bin "e io.va=true; om-*; om 3 0x40080000 0x8 0x20 r-x iram0; om 3 0x3ffb0000 0x10 0x30 r-- dram0; oml; pd 4 @ 0x40080000" --asm-arch xtensa --asm-bits 32 +``` + +`om-*` removes the maps that Rizin made when it opened the file, such as the +default map of the whole file at address 0. The entry address is the 32-bit +little-endian value at file offset 4. + ## Cutter bridge Cutter commands use the local REA Python bridge and authenticated loopback messages. diff --git a/docs/testing-lanes.md b/docs/testing-lanes.md index 17985ebddc..63b600c2a4 100644 --- a/docs/testing-lanes.md +++ b/docs/testing-lanes.md @@ -15,6 +15,21 @@ typecheck selections; its real-provider assertions skip when Rizin 0.7.4 is unavailable. Run the focused command above on a host with that provider for the explicit real-provider verification gate. +The decoder and map inputs of `execute_rizin_command` have their own +real-provider gate. Run +`npm run test:focused -- tests/boundary/providers/RizinDecoderInputs.test.ts`. +It calls the tool through MCP with the Rizin on `PATH` and checks the effect of +`-a`, `-b`, `-e asm.cpu=` and `-n -m` on raw images with Rizin's own `e asm.*` +and `omlj` output, and that `asm_cpu` changes how an M-profile instruction +decodes. It also reads bytes below the base address, at it, at the end of the +image and past it, and checks a call with only `base_address`. Two of the images are ones that Rizin's AVR and +BIOS loaders claim without `-n`. It also selects an arch that no host uses by +default, an arch plugin that Rizin does not have, and a bits value that the +arch lacks. The test names the Rizin version it ran. It skips when no Rizin is +on `PATH`; to check another build, put its directory first on `PATH`. It has +passed with Rizin 0.9.1 (Homebrew, macOS arm64) and Rizin 0.7.4 (official static +build, Linux x86_64). + Each real-toolchain command must require only the host tools needed to prove its stated claim. Use a host-native fixture for host/provider acceptance, and place optional cross-target formats or platform-specific runners in separate diff --git a/scripts/ci/scopes.mjs b/scripts/ci/scopes.mjs index 03bcf518d3..c50f0c19d2 100644 --- a/scripts/ci/scopes.mjs +++ b/scripts/ci/scopes.mjs @@ -102,6 +102,7 @@ const PATH_SCOPES = { "tests/boundary/providers/RizinTimeoutEvidence.test.ts", "tests/boundary/providers/RizinDebugSession*.test.ts", "tests/boundary/providers/RizinInferiorTty.test.ts", + "tests/boundary/providers/RizinDecoderInputs.test.ts", "tests/boundary/providers/RizinTestInferiorTty.ts", "tests/boundary/process/ownedDebuggerSession.test.ts", "tests/boundary/cli/debuggerOneShotExit.test.ts", diff --git a/src/application/reverse/ReverseEngineeringCommandPort.ts b/src/application/reverse/ReverseEngineeringCommandPort.ts index 92a014e83f..7228526c86 100644 --- a/src/application/reverse/ReverseEngineeringCommandPort.ts +++ b/src/application/reverse/ReverseEngineeringCommandPort.ts @@ -9,6 +9,8 @@ export interface ReverseEngineeringProviderCommand { readonly executable: string; readonly arguments: readonly string[]; readonly identity: EvidenceProvider; + /** What the provider cannot attest about the arguments it built. */ + readonly limitations?: readonly string[]; readonly environmentOverrides?: Readonly; } diff --git a/src/application/reverse/ReverseEngineeringService.ts b/src/application/reverse/ReverseEngineeringService.ts index f56c362e15..1185f028cc 100644 --- a/src/application/reverse/ReverseEngineeringService.ts +++ b/src/application/reverse/ReverseEngineeringService.ts @@ -736,6 +736,7 @@ export class ReverseEngineeringService { "Rizin commands and retained plugins may read files or access network resources outside the selected artifact; those external inputs are not bound to Evidence.", ] : []), + ...(profile.limitations ?? []), ...(stdoutInvalidUtf8 || stderrInvalidUtf8 ? [ `Captured ${[ diff --git a/src/cli/reverseEngineeringCommands.ts b/src/cli/reverseEngineeringCommands.ts index 43a2fe81b6..eaa2470586 100644 --- a/src/cli/reverseEngineeringCommands.ts +++ b/src/cli/reverseEngineeringCommands.ts @@ -17,6 +17,7 @@ import { projectAnalysisError } from "../domain/analysisErrorProjection.js"; import { OBJDUMP_ARTIFACT_OPERATIONS, reverseCommandTimeoutSchema, + rizinInputSchema, } from "../domain/reverseEngineering.js"; import { logCliCommand } from "../cliLogging.js"; import type { CliInstance } from "./types.js"; @@ -309,6 +310,10 @@ export const registerReverseEngineeringCommands = ( command: z.string().min(1).describe("One caller-selected Rizin command."), }), options: z.object({ + asmArch: rizinInputSchema.shape.asm_arch, + asmBits: rizinInputSchema.shape.asm_bits, + asmCpu: rizinInputSchema.shape.asm_cpu, + baseAddress: rizinInputSchema.shape.base_address, commandTimeoutMs: reverseCommandTimeoutSchema, }), run: ({ args, options }) => @@ -318,6 +323,18 @@ export const registerReverseEngineeringCommands = ( service.executeRizinCommand( { ...args, + ...(options.asmArch === undefined + ? {} + : { asm_arch: options.asmArch }), + ...(options.asmBits === undefined + ? {} + : { asm_bits: options.asmBits }), + ...(options.asmCpu === undefined + ? {} + : { asm_cpu: options.asmCpu }), + ...(options.baseAddress === undefined + ? {} + : { base_address: options.baseAddress }), ...(options.commandTimeoutMs === undefined ? {} : { command_timeout_ms: options.commandTimeoutMs }), diff --git a/src/composition/reverseEngineering.ts b/src/composition/reverseEngineering.ts index e41561c8cb..6394dc1550 100644 --- a/src/composition/reverseEngineering.ts +++ b/src/composition/reverseEngineering.ts @@ -3,7 +3,10 @@ import type { ReverseEngineeringCommandPort } from "../application/reverse/Rever import { ReverseEngineeringService } from "../application/reverse/ReverseEngineeringService.js"; import { objdumpCommand } from "../objdump/ObjdumpCommand.js"; import { OBJDUMP_PROVIDER_IDENTITY } from "../objdump/ObjdumpProviderIdentity.js"; -import { rizinCommand } from "../rizin/RizinCommand.js"; +import { + rizinCommand, + rizinCommandLimitations, +} from "../rizin/RizinCommand.js"; import { RIZIN_PROVIDER_IDENTITY } from "../rizin/RizinProviderIdentity.js"; import { resolveWindowsProviderExecutable } from "./resolveWindowsProviderExecutable.js"; @@ -28,6 +31,7 @@ export const createReverseEngineeringCommandPort = ( platform, ), arguments: rizinCommand(input), + limitations: rizinCommandLimitations(input), identity: RIZIN_PROVIDER_IDENTITY, }), }); diff --git a/src/contracts/reverseEngineeringToolContracts.ts b/src/contracts/reverseEngineeringToolContracts.ts index d2aacf238b..b0098803e1 100644 --- a/src/contracts/reverseEngineeringToolContracts.ts +++ b/src/contracts/reverseEngineeringToolContracts.ts @@ -39,7 +39,7 @@ export const REVERSE_ENGINEERING_TOOL_CONTRACTS = [ ...toolContractMetadata("execute_rizin_command"), kind: "native-provider", description: - "Execute one caller-selected Rizin command against a local artifact using the Rizin CLI profile that suppresses user settings and scripts while retaining plugins. Preserves decoded stdout and stderr inline with SHA-256 Evidence, up to a shared 16 MiB combined limit; invalid UTF-8 bytes are represented by replacement characters and flagged in Evidence limitations. output_truncated reports output beyond that limit and completion_status is unknown if buffer enforcement terminated the process. Rizin commands and retained plugins can execute foreign code, mutate files, access the network, and read files outside the selected artifact; identities and contents of those external inputs are not bound to Evidence. Command effects depend on the Rizin command and loaded plugins; JSON is command-specific and raw output remains authoritative. In a shared MCP server, this admitted command closes any active binary-analysis session; after its effects have settled, call open_binary before further analysis. Close an active Ghidra session explicitly first. Optional command_timeout_ms is the per-phase wall clock in milliseconds (default 120000, range 1..2147483647); omitted means 120000 and the value applies on the next call. It does not raise the 16 MiB output cap or a Rizin debugger session deadline.", + "Execute one caller-selected Rizin command against a local artifact using the Rizin CLI profile that suppresses user settings and scripts while retaining plugins. Preserves decoded stdout and stderr inline with SHA-256 Evidence, up to a shared 16 MiB combined limit; invalid UTF-8 bytes are represented by replacement characters and flagged in Evidence limitations. output_truncated reports output beyond that limit and completion_status is unknown if buffer enforcement terminated the process. Rizin commands and retained plugins can execute foreign code, mutate files, access the network, and read files outside the selected artifact; identities and contents of those external inputs are not bound to Evidence. Command effects depend on the Rizin command and loaded plugins; JSON is command-specific and raw output remains authoritative. For a raw image, asm_arch, asm_bits and asm_cpu select the decoder and base_address maps file offset 0 there; base_address also passes -n, which skips format loaders even for an ELF, and REA confirms neither. In a shared MCP server, this admitted command closes any active binary-analysis session; after its effects have settled, call open_binary before further analysis. Close an active Ghidra session explicitly first. Optional command_timeout_ms is the per-phase wall clock in milliseconds (default 120000, range 1..2147483647); omitted means 120000 and the value applies on the next call. It does not raise the 16 MiB output cap or a Rizin debugger session deadline.", inputSchema: rizinInputSchema, outputSchema: evidenceResultOf(commandObservationSchema), examples: [ diff --git a/src/domain/rawBinary.ts b/src/domain/rawBinary.ts index deb9f81069..8a23541747 100644 --- a/src/domain/rawBinary.ts +++ b/src/domain/rawBinary.ts @@ -21,7 +21,8 @@ export const BINARY_ARCHITECTURES = [ /** Provider-neutral CPU family for a caller-supplied raw-binary load. */ export const binaryArchitectureSchema = z.enum(BINARY_ARCHITECTURES); -const RAW_BINARY_ENTRY_POINT = /^0x[0-9A-Fa-f]{1,16}$/u; +/** Caller-supplied hexadecimal address of at most 64 bits, such as 0x08000000. */ +export const RAW_BINARY_ENTRY_POINT = /^0x[0-9A-Fa-f]{1,16}$/u; /** Hexadecimal load and entry address; file offset 0 is mapped here. */ export const rawBinaryEntryPointSchema = z diff --git a/src/domain/reverseEngineering.ts b/src/domain/reverseEngineering.ts index 03601ae2c9..55926af60b 100644 --- a/src/domain/reverseEngineering.ts +++ b/src/domain/reverseEngineering.ts @@ -1,6 +1,7 @@ import { z } from "zod"; import { commandEvidenceDetailSchema } from "./commandObservation.js"; import { localPathStringSchema } from "./localPath.js"; +import { RAW_BINARY_ENTRY_POINT } from "./rawBinary.js"; /** Omitted one-shot objdump/rizin wall clock. Preflight shares it; the command gets it again. */ export const DEFAULT_REVERSE_COMMAND_TIMEOUT_MS = 120_000; @@ -72,6 +73,15 @@ export const objdumpInputSchema = z.discriminatedUnion("operation", [ }), ]); +// Rizin owns its arch and CPU vocabulary, and the plugins differ between +// builds, so REA checks only the spelling. Rizin sets no length limit. +const rizinNameSchema = z + .string() + .regex( + /^[A-Za-z0-9][A-Za-z0-9_.+\-]*$/u, + "must be a Rizin name: letters, digits, '_', '.', '+' or '-', starting with a letter or digit", + ); + /** Caller-selected Rizin command against one local artifact. */ export const rizinInputSchema = z .object({ @@ -83,6 +93,35 @@ export const rizinInputSchema = z (value) => !/[\r\n\u0000]/u.test(value), "A single command line is required", ), + asm_arch: rizinNameSchema + .exactOptional() + .describe( + "Rizin arch plugin, passed as -a, such as arm. Plugins differ between builds; e asm.arch=? lists them.", + ), + // Rizin's widest decoders are 64-bit. + asm_bits: z + .number() + .int() + .min(1) + .max(64) + .exactOptional() + .describe( + "Rizin asm.bits, passed as -b, such as 16 for Thumb. Omitted, Rizin's default applies.", + ), + asm_cpu: rizinNameSchema + .exactOptional() + .describe( + "Rizin asm.cpu, passed as -e asm.cpu=, such as cortexm; e asm.cpu=? lists those of the selected arch.", + ), + base_address: z + .string() + .regex(RAW_BINARY_ENTRY_POINT, { + message: "base_address must be a hexadecimal address of the form 0x…", + }) + .exactOptional() + .describe( + "Address of file offset 0. REA passes -n and -m, so Rizin maps the file there as a raw image, reads no format headers, and keeps its default decoder unless asm_arch and asm_bits select one.", + ), ...commandTimeoutMsField, }) .strict(); diff --git a/src/rizin/RizinCommand.ts b/src/rizin/RizinCommand.ts index c571a29c2f..7ed8566054 100644 --- a/src/rizin/RizinCommand.ts +++ b/src/rizin/RizinCommand.ts @@ -7,7 +7,34 @@ export const rizinCommand = (input: RizinInput): readonly string[] => [ "-q", "-e", "scr.color=0", + // Rizin 0.9.1 refuses -e asm.arch= and -e asm.bits= at startup. Once the + // file is open it applies -a and -b, then the -e settings. + ...(input.asm_arch === undefined ? [] : ["-a", input.asm_arch]), + ...(input.asm_bits === undefined ? [] : ["-b", String(input.asm_bits)]), + ...(input.asm_cpu === undefined ? [] : ["-e", `asm.cpu=${input.asm_cpu}`]), + // A format loader can claim a raw image. Rizin 0.9.1's AVR loader turns + // io.va off, so reads at the -m map return 0xff, and the BIOS loader moves + // the map. -n skips the loaders, and Rizin maps file offset 0 at the base. + ...(input.base_address === undefined ? [] : ["-n", "-m", input.base_address]), "-c", input.command, absoluteCallerPath(input.path), ]; + +/** What REA cannot attest about the decoder and map inputs it passed to Rizin. */ +export const rizinCommandLimitations = ( + input: RizinInput, +): readonly string[] => [ + ...(input.asm_arch === undefined && + input.asm_bits === undefined && + input.asm_cpu === undefined + ? [] + : [ + "REA does not confirm that Rizin applied the selected asm_* values. Rizin uses another arch or bits value when it cannot apply one, at most reporting it on stderr, and e asm.cpu echoes any value, so compare it with e asm.cpu=?.", + ]), + ...(input.base_address === undefined + ? [] + : [ + "With base_address, REA passed -n and -m so that Rizin maps file offset 0 at that address as a raw image; REA does not confirm the map, which oml shows. -n skips Rizin's format loaders, so expect no sections, symbols or entry point, and arch, bits and byte order stay at Rizin's defaults unless the call selects them.", + ]), +]; diff --git a/tests/boundary/cli/reverseEngineeringTargetInput.test.ts b/tests/boundary/cli/reverseEngineeringTargetInput.test.ts index 5a65b2247a..edb2c5abb8 100644 --- a/tests/boundary/cli/reverseEngineeringTargetInput.test.ts +++ b/tests/boundary/cli/reverseEngineeringTargetInput.test.ts @@ -1,8 +1,10 @@ +import { chmod, writeFile } from "node:fs/promises"; import { join } from "node:path"; import { expect } from "vitest"; import { analysisErrorProjectionSchema } from "../../../src/contracts/errorSchemas.js"; +import { REVERSE_ENGINEERING_TOOL_CONTRACTS } from "../../../src/contracts/reverseEngineeringToolContracts.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; import { cliTest } from "../../support/cli/cliFixture.js"; @@ -51,6 +53,73 @@ cliTest( }, ); +cliTest( + "passes Rizin decoder and map flags before the artifact", + async ({ cli }) => { + const root = await createTestTempDirectory("rea-rizin-load-flags-"); + const provider = join(root, "argv-rizin"); + const artifact = join(root, "flash.bin"); + await writeFile(artifact, "flash"); + await writeFile( + provider, + `#!${process.execPath}\nprocess.stdout.write(JSON.stringify(process.argv.slice(2)));\n`, + ); + await chmod(provider, 0o755); + const result = await cli.run({ + arguments: [ + "execute-rizin-command", + artifact, + "oml", + "--asm-arch", + "riscv", + "--asm-bits", + "32", + "--asm-cpu", + "rv32imc", + "--base-address", + "0x42000000", + "--format", + "json", + ], + cwd: root, + environment: { REA_RIZIN_COMMAND: provider }, + }); + expect(result.exitCode, result.stderr).toBe(0); + const rizinContract = REVERSE_ENGINEERING_TOOL_CONTRACTS.find( + (contract) => contract.name === "execute_rizin_command", + ); + if (rizinContract === undefined) + throw new Error("execute_rizin_command contract is missing"); + const evidence = rizinContract.outputSchema.parse(result.json); + const argv = [ + "-N", + "-q", + "-e", + "scr.color=0", + "-a", + "riscv", + "-b", + "32", + "-e", + "asm.cpu=rv32imc", + "-n", + "-m", + "0x42000000", + "-c", + "oml", + artifact, + ]; + expect(JSON.parse(evidence.normalized_result.stdout)).toEqual(argv); + expect(evidence.parameters).toMatchObject({ + asm_arch: "riscv", + asm_bits: 32, + asm_cpu: "rv32imc", + base_address: "0x42000000", + command_arguments: argv, + }); + }, +); + cliTest( "reports the rejected Rizin command before inspecting the target", async ({ cli }) => { diff --git a/tests/boundary/mcp/reverseEngineeringEnvironmentMcp.test.ts b/tests/boundary/mcp/reverseEngineeringEnvironmentMcp.test.ts index ff47db600f..1742f56562 100644 --- a/tests/boundary/mcp/reverseEngineeringEnvironmentMcp.test.ts +++ b/tests/boundary/mcp/reverseEngineeringEnvironmentMcp.test.ts @@ -3,8 +3,12 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import type { Client } from "@modelcontextprotocol/client"; +import type { CallToolResult } from "@modelcontextprotocol/server"; import { expect, it } from "vitest"; +import { REVERSE_ENGINEERING_TOOL_CONTRACTS } from "../../../src/contracts/reverseEngineeringToolContracts.js"; +import { rizinCommandLimitations } from "../../../src/rizin/RizinCommand.js"; +import { parseMcpToolError } from "../../fixtures/mcpToolError.js"; import { createProviderEnvironmentMcpHarness } from "../../support/reverseDebuggerMcpTestHelpers.js"; const writeProvider = async (path: string, output: string): Promise => { @@ -119,3 +123,92 @@ it("snapshots an explicit provider environment when the server is created", asyn '"captured"', ); }); + +it("passes Rizin decoder and map inputs as flags before the artifact", async () => { + const directory = await mkdtemp(join(tmpdir(), "rea-rizin-load-mcp-")); + const provider = join(directory, "argv-rizin"); + const artifact = join(directory, "flash.bin"); + await writeFile(artifact, "flash"); + await writeFile( + provider, + `#!${process.execPath}\nprocess.stdout.write(JSON.stringify(process.argv.slice(2)));\n`, + ); + await chmod(provider, 0o755); + const { client } = await connectProviderClient({ + directory, + clientName: "reverse-rizin-load-inputs", + configuredProvider: provider, + }); + const input = { + path: artifact, + command: "e asm.arch; e asm.bits; e asm.cpu; oml", + asm_arch: "arm", + asm_bits: 16, + asm_cpu: "cortexm", + base_address: "0x08000000", + }; + + const response = await client.callTool({ + name: "execute_rizin_command", + arguments: input, + }); + expect(response.isError, JSON.stringify(response.content)).not.toBe(true); + const rizinContract = REVERSE_ENGINEERING_TOOL_CONTRACTS.find( + (contract) => contract.name === "execute_rizin_command", + ); + if (rizinContract === undefined) + throw new Error("execute_rizin_command contract is missing"); + const evidence = rizinContract.outputSchema.parse(response.structuredContent); + const argv = [ + "-N", + "-q", + "-e", + "scr.color=0", + "-a", + "arm", + "-b", + "16", + "-e", + "asm.cpu=cortexm", + "-n", + "-m", + "0x08000000", + "-c", + input.command, + artifact, + ]; + expect(JSON.parse(evidence.normalized_result.stdout)).toEqual(argv); + expect(evidence.parameters).toMatchObject({ + ...input, + command_arguments: argv, + }); + expect(evidence.limitations).toEqual( + expect.arrayContaining([...rizinCommandLimitations(input)]), + ); + + // Compact schemas drop property descriptions, so the tool description names + // the input that changes how Rizin loads the file. + const { tools } = await client.listTools(); + const listed = tools.find((tool) => tool.name === rizinContract.name); + expect(listed?.description).toBe(rizinContract.description); + expect(listed?.description).toContain("base_address"); + + const refused = await client.callTool({ + name: "execute_rizin_command", + arguments: { ...input, asm_cpu: "cortexm; q" }, + }); + expect(refused.isError).toBe(true); + expect(parseMcpToolError(refused as CallToolResult).error).toMatchObject({ + code: "invalid_request", + details: { + operation: "execute_rizin_command", + issues: [ + { + path: ["asm_cpu"], + reason: "invalid_format", + message: expect.stringMatching(/^must be a Rizin name: /u), + }, + ], + }, + }); +}); diff --git a/tests/boundary/providers/RizinDecoderInputs.test.ts b/tests/boundary/providers/RizinDecoderInputs.test.ts new file mode 100644 index 0000000000..468e42039b --- /dev/null +++ b/tests/boundary/providers/RizinDecoderInputs.test.ts @@ -0,0 +1,259 @@ +import { execFileSync } from "node:child_process"; +import { mkdtemp, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +import { expect, it } from "vitest"; + +import { REVERSE_ENGINEERING_TOOL_CONTRACTS } from "../../../src/contracts/reverseEngineeringToolContracts.js"; +import { rizinCommandLimitations } from "../../../src/rizin/RizinCommand.js"; +import { createProviderEnvironmentMcpHarness } from "../../support/reverseDebuggerMcpTestHelpers.js"; + +// Real-provider gate for execute_rizin_command's decoder and map inputs. It +// runs the Rizin found on PATH and skips without one; put another Rizin's +// bin directory first on PATH to check that version. +const rizin = findRizin(); +const banner = rizin === undefined ? "no Rizin on PATH" : versionBanner(rizin); + +// Cortex-M vector table head (SP 0x20002000, Reset 0x08000109), then the +// Thumb instructions `bx lr` and `nop`. +const CORTEX_M_IMAGE = Buffer.from([ + 0x00, 0x20, 0x00, 0x20, 0x09, 0x01, 0x00, 0x08, 0x70, 0x47, 0x00, 0xbf, +]); +// Thumb `msr primask, r0`, which only an M-profile decoder knows. +const MSR_PRIMASK = Buffer.from([0x80, 0xf3, 0x10, 0x88]); +// 6502 `nop; rts`; no host decodes 6502 by default. +const MOS_6502_IMAGE = Buffer.from([0xea, 0x60]); +// Raw images that Rizin's loaders claim unless REA passes -n. Rizin 0.9.1's +// AVR loader (bytes 1 and 3 look like rjmp) turns io.va off, so reads at the +// -m map return 0xff. The BIOS loader (a jmp 16 bytes from the end of a 64 KiB +// file) maps the file at another address in 0.7.4 and 0.9.1. +const AVR_LIKE_IMAGE = Buffer.from( + Array.from({ length: 64 }, (_, index) => + index === 1 || index === 3 ? 0xc0 : index, + ), +); +const BIOS_LIKE_IMAGE = Buffer.alloc(0x10000); +BIOS_LIKE_IMAGE.set([0x11, 0x22, 0x33, 0x44], 0); +BIOS_LIKE_IMAGE[0xfff0] = 0xea; +BIOS_LIKE_IMAGE[0xffff] = 0x5a; + +const rizinContract = REVERSE_ENGINEERING_TOOL_CONTRACTS.find( + (contract) => contract.name === "execute_rizin_command", +); +if (rizinContract === undefined) + throw new Error("execute_rizin_command contract is missing"); + +const hex = (value: number): string => `0x${value.toString(16)}`; + +const runRizin = async ( + input: Readonly>, + image: Buffer, +) => { + const directory = await mkdtemp(join(tmpdir(), "rea-rizin-decoder-")); + // The harness owns the directory, so it must exist before setup can fail. + const { client } = await createProviderEnvironmentMcpHarness({ + directory, + clientName: "real-rizin-decoder-inputs", + serverOptions: { environment: { REA_RIZIN_COMMAND: rizin! } }, + }); + const path = join(directory, "image.bin"); + await writeFile(path, image); + const toolArguments = { path, ...input }; + const response = await client.callTool({ + name: "execute_rizin_command", + arguments: toolArguments, + }); + expect(response.isError, JSON.stringify(response.content)).not.toBe(true); + const evidence = rizinContract.outputSchema.parse(response.structuredContent); + expect(evidence.normalized_result.exit_code).toBe(0); + expect(evidence.limitations).toEqual( + expect.arrayContaining([ + ...rizinCommandLimitations({ + ...toolArguments, + command: String(input.command), + }), + ]), + ); + return { + lines: evidence.normalized_result.stdout.trimEnd().split("\n"), + stderr: evidence.normalized_result.stderr, + }; +}; + +const mapsOf = (line: string | undefined) => + JSON.parse(line ?? "null") as readonly { + readonly fd: number; + readonly delta: number; + readonly from: number; + }[]; + +it.skipIf(rizin === undefined)( + `applies -a, -b, -e asm.cpu= and -m to a raw Cortex-M image (${banner})`, + async () => { + const { lines } = await runRizin( + { + command: + "e asm.arch; e asm.bits; e asm.cpu; omlj; p8 1 @ 0x07ffffff; p8 12 @ 0x08000000; p8 1 @ 0x0800000c; pi 1 @ 0x08000008", + asm_arch: "arm", + asm_bits: 16, + asm_cpu: "cortexm", + base_address: "0x08000000", + }, + CORTEX_M_IMAGE, + ); + expect(lines.slice(0, 3)).toEqual(["arm", "16", "cortexm"]); + // One map: file offset 0 at the base address, and nothing mapped below + // it or past its end. + expect(mapsOf(lines[3])).toEqual([ + expect.objectContaining({ delta: 0, from: 0x08000000 }), + ]); + expect(lines.slice(4)).toEqual([ + "ff", + CORTEX_M_IMAGE.toString("hex"), + "ff", + "bx lr", + ]); + }, +); + +it.skipIf(rizin === undefined)( + `applies asm_cpu to the decoder, and echoes one Rizin lacks (${banner})`, + async () => { + const thumb = { asm_arch: "arm", asm_bits: 16, base_address: "0x0" }; + const plain = await runRizin({ command: "pi 1", ...thumb }, MSR_PRIMASK); + expect(plain.lines).toEqual(["invalid"]); + + const selected = await runRizin( + { command: "pi 1", ...thumb, asm_cpu: "cortexm" }, + MSR_PRIMASK, + ); + expect(selected.lines).toEqual(["msr primask, r0"]); + + const unknown = await runRizin( + { command: "e asm.cpu; pi 1", ...thumb, asm_cpu: "rea-no-such-cpu" }, + MSR_PRIMASK, + ); + expect(unknown.lines).toEqual(["rea-no-such-cpu", "invalid"]); + }, +); + +it.skipIf(rizin === undefined)( + `states what a call with only base_address leaves unconfirmed (${banner})`, + async () => { + const { lines } = await runRizin( + { command: "omlj; om-*; omlj", base_address: "0x8000" }, + CORTEX_M_IMAGE, + ); + expect(mapsOf(lines[0])).toEqual([ + expect.objectContaining({ delta: 0, from: 0x8000 }), + ]); + // The command removed the map, so the map is not a fact of the result. + expect(mapsOf(lines[1])).toEqual([]); + }, +); + +it.skipIf(rizin === undefined)( + `reads the file at base_address when a Rizin loader would claim it (${banner})`, + async () => { + const base = 0x08000000; + for (const image of [AVR_LIKE_IMAGE, BIOS_LIKE_IMAGE]) { + const last = base + image.length - 1; + const { lines } = await runRizin( + { + command: `e io.va; omlj; p8 1 @ ${hex(base - 1)}; p8 4 @ ${hex(base)}; p8 1 @ ${hex(last)}; p8 1 @ ${hex(last + 1)}`, + asm_arch: "arm", + asm_bits: 16, + base_address: hex(base), + }, + image, + ); + expect(lines[0]).toBe("true"); + expect(mapsOf(lines[1])).toEqual([ + expect.objectContaining({ delta: 0, from: base }), + ]); + expect(lines.slice(2)).toEqual([ + "ff", + image.subarray(0, 4).toString("hex"), + image.subarray(-1).toString("hex"), + "ff", + ]); + } + }, +); + +it.skipIf(rizin === undefined)( + `selects an arch that differs from the host default (${banner})`, + async () => { + const plain = await runRizin( + { command: "e asm.arch; e asm.bits; omlj" }, + MOS_6502_IMAGE, + ); + expect(plain.lines[0]).not.toBe("6502"); + expect(mapsOf(plain.lines[2])).toEqual([ + expect.objectContaining({ delta: 0, from: 0 }), + ]); + + const selected = await runRizin( + { + command: "e asm.arch; e asm.bits; omlj; pi 2 @ 0x8000", + asm_arch: "6502", + asm_bits: 8, + base_address: "0x8000", + }, + MOS_6502_IMAGE, + ); + expect(selected.lines.slice(0, 2)).toEqual(["6502", "8"]); + expect(mapsOf(selected.lines[2])).toEqual([ + expect.objectContaining({ delta: 0, from: 0x8000 }), + ]); + expect(selected.lines.slice(3)).toEqual(["nop", "rts"]); + }, +); + +it.skipIf(rizin === undefined)( + `does not apply an arch or bits value that Rizin lacks (${banner})`, + async () => { + const plain = await runRizin({ command: "e asm.arch" }, MOS_6502_IMAGE); + const unknown = await runRizin( + { + command: "e asm.arch; e asm.bits", + asm_arch: "rea-no-such-arch", + asm_bits: 16, + }, + MOS_6502_IMAGE, + ); + // Rizin exits 0, decodes with its default arch and names the value only + // on stderr. Evidence carries the limitation for this case. + expect(unknown.lines[0]).toBe(plain.lines[0]); + expect(unknown.stderr).toContain("rea-no-such-arch"); + + // ARM has no 8-bit mode. Rizin 0.9.1 keeps its default and reports the + // value on stderr; Rizin 0.7.4 uses 16 without a message. + const unsupported = await runRizin( + { command: "e asm.bits", asm_arch: "arm", asm_bits: 8 }, + CORTEX_M_IMAGE, + ); + expect(unsupported.lines[0]).not.toBe("8"); + }, +); + +function findRizin(): string | undefined { + try { + const path = execFileSync("which", ["rizin"], { encoding: "utf8" }).trim(); + return path === "" ? undefined : path; + } catch { + return undefined; + } +} + +function versionBanner(binary: string): string { + try { + return ( + execFileSync(binary, ["-v"], { encoding: "utf8" }).split("\n")[0] ?? + "unknown Rizin" + ).trim(); + } catch { + return "unknown Rizin"; + } +} diff --git a/tests/composition/reverseEngineering.test.ts b/tests/composition/reverseEngineering.test.ts index 6242584ace..101fc3d3e5 100644 --- a/tests/composition/reverseEngineering.test.ts +++ b/tests/composition/reverseEngineering.test.ts @@ -17,6 +17,7 @@ import { createReverseEngineeringService } from "../../src/composition/reverseEn import { projectAnalysisError } from "../../src/domain/analysisErrorProjection.js"; import { AnalysisCancelledError } from "../../src/domain/analysisErrorCore.js"; import { REVERSE_ENGINEERING_TOOL_CONTRACTS } from "../../src/contracts/reverseEngineeringToolContracts.js"; +import { rizinCommandLimitations } from "../../src/rizin/RizinCommand.js"; it("documents the shared combined capture limit for both command tools", () => { expect(REVERSE_ENGINEERING_TOOL_CONTRACTS.map(({ name }) => name)).toEqual( @@ -494,6 +495,153 @@ describe("Rizin command service", () => { }); }); +it("passes decoder and map inputs to Rizin before the path and records them", async () => { + const directory = await mkdtemp(join(tmpdir(), "rea-rizin-load-")); + const path = join(directory, "flash.bin"); + await writeFile(path, Buffer.from([0x70, 0x47])); + const calls: (readonly string[])[] = []; + const service = new ReverseEngineeringService({ + environment: { REA_RIZIN_COMMAND: "/tools/rizin" }, + run: async (_command, args) => { + calls.push(args); + return { stdout: "bx lr\n", stderr: "" }; + }, + }); + const input = { + path, + command: "pi 1", + asm_arch: "arm", + asm_bits: 16, + asm_cpu: "cortexm", + base_address: "0x08000000", + }; + + try { + const result = await service.executeRizinCommand(input); + if (!result.ok) throw result.error; + const argv = [ + "-N", + "-q", + "-e", + "scr.color=0", + "-a", + "arm", + "-b", + "16", + "-e", + "asm.cpu=cortexm", + "-n", + "-m", + "0x08000000", + "-c", + "pi 1", + path, + ]; + // The command is the last launch; a provider probe may precede it. + expect(calls.at(-1)).toEqual(argv); + expect(result.value.parameters).toMatchObject({ + ...input, + command_arguments: argv, + }); + expect(result.value.limitations).toEqual( + expect.arrayContaining([...rizinCommandLimitations(input)]), + ); + // One statement for the decoder inputs and one for the raw map. + expect(rizinCommandLimitations(input)).toHaveLength(2); + expect( + rizinCommandLimitations({ path, command: "pi 1", asm_bits: 16 }), + ).toHaveLength(1); + expect( + rizinCommandLimitations({ path, command: "pi 1", base_address: "0x0" }), + ).toHaveLength(1); + expect(rizinCommandLimitations({ path, command: "pi 1" })).toEqual([]); + } finally { + await rm(directory, { recursive: true, force: true }); + } +}); + +it("checks only the spelling and range of decoder and map inputs", async () => { + const directory = await mkdtemp(join(tmpdir(), "rea-rizin-load-input-")); + const path = join(directory, "image.bin"); + await writeFile(path, "image"); + const calls: (readonly string[])[] = []; + const service = new ReverseEngineeringService({ + run: async (_command, args) => { + calls.push(args); + return { stdout: "", stderr: "" }; + }, + }); + + try { + // Rizin's own names: digits first, dotted plugins, '+', '-' and case. + for (const [selection, flags] of [ + [{ asm_arch: "6502", asm_bits: 8 }, ["-a", "6502", "-b", "8"]], + [{ asm_arch: "x86.nasm", asm_bits: 64 }, ["-a", "x86.nasm", "-b", "64"]], + [{ asm_cpu: "c55x+" }, ["-e", "asm.cpu=c55x+"]], + [{ asm_cpu: "c166-generic" }, ["-e", "asm.cpu=c166-generic"]], + [{ asm_cpu: "ATxmega128a4u" }, ["-e", "asm.cpu=ATxmega128a4u"]], + [{ asm_bits: 1 }, ["-b", "1"]], + [{ base_address: "0x0" }, ["-n", "-m", "0x0"]], + [ + { base_address: "0xFFFFFFFFFFFFFFFF" }, + ["-n", "-m", "0xFFFFFFFFFFFFFFFF"], + ], + ] as const) { + const result = await service.executeRizinCommand({ + path, + command: "e asm.arch", + ...selection, + }); + if (!result.ok) throw result.error; + expect(result.value.parameters.command_arguments).toEqual([ + "-N", + "-q", + "-e", + "scr.color=0", + ...flags, + "-c", + "e asm.arch", + path, + ]); + } + + calls.length = 0; + for (const [field, value] of [ + ["asm_arch", ""], + ["asm_arch", "arm;q"], + ["asm_arch", "arm x"], + ["asm_arch", "-q"], + ["asm_cpu", "?"], + ["asm_cpu", "cortex=m"], + ["asm_bits", 0], + ["asm_bits", 65], + ["asm_bits", 16.5], + ["asm_bits", "16"], + ["base_address", "0x"], + ["base_address", "08000000"], + ["base_address", "0X08000000"], + ["base_address", "0x10000000000000000"], + ] as const) { + const result = await service.executeRizinCommand({ + path, + command: "e asm.arch", + [field]: value, + }); + if (result.ok) throw new Error(`${field}=${String(value)} was accepted`); + expect(projectAnalysisError(result.error)).toMatchObject({ + code: "invalid_request", + details: { + operation: "execute_rizin_command", + issues: [expect.objectContaining({ path: [field] })], + }, + }); + } + expect(calls).toEqual([]); + } finally { + await rm(directory, { recursive: true, force: true }); + } +}); + it("retains partial Evidence when a provider command times out", async () => { const directory = await mkdtemp(join(tmpdir(), "rea-rizin-timeout-partial-")); const path = join(directory, "fixture.bin"); diff --git a/tsconfig.reverse-tools-mutation.json b/tsconfig.reverse-tools-mutation.json index 4fd7937fad..9942cb5fc7 100644 --- a/tsconfig.reverse-tools-mutation.json +++ b/tsconfig.reverse-tools-mutation.json @@ -18,6 +18,7 @@ "tests/boundary/providers/RizinTimeoutEvidence.test.ts", "tests/boundary/mcp/rizinPendingOutput.test.ts", "tests/boundary/providers/RizinInferiorTty.test.ts", + "tests/boundary/providers/RizinDecoderInputs.test.ts", "tests/boundary/providers/RizinTestInferiorTty.ts", "tests/boundary/providers/RizinCommandTimeout.test.ts", "tests/boundary/providers/CutterBridgeClient.test.ts", diff --git a/vitest.reverse-tools.config.ts b/vitest.reverse-tools.config.ts index 82468f43df..3fab3790d0 100644 --- a/vitest.reverse-tools.config.ts +++ b/vitest.reverse-tools.config.ts @@ -24,6 +24,7 @@ export default defineConfig({ "tests/boundary/providers/GdbSessionManagers.test.ts", "tests/boundary/providers/RizinDebugSessionManagers.test.ts", "tests/boundary/providers/RizinInferiorTty.test.ts", + "tests/boundary/providers/RizinDecoderInputs.test.ts", "tests/boundary/providers/GdbSessionManagerCoverage.test.ts", "tests/boundary/providers/GdbCommandQueue.test.ts", "tests/boundary/providers/GdbSessionReviewRegressions.test.ts",