Files
reverse-engineering-anything/src/domain/rawBinary.ts
T
Entrpi 81f9f71f6d 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.
2026-11-06 20:48:59 +08:00

171 lines
5.9 KiB
TypeScript

import { z } from "zod";
import type {
BinaryArchitecture,
HeaderArchitecture,
RawOnlyArchitecture,
} from "./binaryTargetTypes.js";
import type { ExecutableFormatHint } from "./dosCom.js";
import { err, ok, type Result } from "./result.js";
/** Caller-visible CPU families admitted for headerless raw binaries. */
export const BINARY_ARCHITECTURES = [
"x86",
"x86_64",
"arm",
"arm64",
"6502",
"m68k",
] as const satisfies readonly BinaryArchitecture[];
/** Provider-neutral CPU family for a caller-supplied raw-binary load. */
export const binaryArchitectureSchema = z.enum(BINARY_ARCHITECTURES);
/** 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
.string()
.regex(RAW_BINARY_ENTRY_POINT, {
message: "entry_point must be a hexadecimal address of the form 0x…",
});
/** Canonicalized caller-supplied raw-binary entry with the original spelling. */
export interface CanonicalRawBinaryEntry {
readonly source: string;
readonly canonical: string;
readonly value: bigint;
}
/** Parse a caller-supplied hex entry while preserving the source spelling. */
export const parseRawBinaryEntryPoint = (
value: string,
): Result<CanonicalRawBinaryEntry, string> => {
if (!RAW_BINARY_ENTRY_POINT.test(value))
return err("entry_point must be a hexadecimal address of the form 0x…");
const parsed = BigInt(value);
return ok({
source: value,
canonical: `0x${parsed.toString(16)}`,
value: parsed,
});
};
/** Reject empty or non-integral file extents for a headerless image. */
export const validateRawBinaryLength = (
length: number,
): Result<null, string> =>
Number.isSafeInteger(length) && length >= 1
? ok(null)
: err("raw-binary requires at least one file byte");
/** Admitted headerless load identity used by target resolution and profiles. */
export interface RawBinaryAdmission {
readonly architecture: HeaderArchitecture | RawOnlyArchitecture;
readonly entryPoint: string;
readonly entryPointSource: string;
}
/** Why a headerless load was refused, with its owning classification. */
export interface RawBinaryAdmissionRejection {
readonly reason: string;
/**
* `load_selection_rejected` names a caller-input selection problem;
* `file_format_rejected` names a refusal caused by the target file itself.
*/
readonly constraint: "load_selection_rejected" | "file_format_rejected";
}
const addressWidth = (architecture: BinaryArchitecture): bigint =>
architecture === "6502"
? 0xffffn
: architecture === "x86" ||
architecture === "arm" ||
architecture === "m68k"
? 0xffffffffn
: 0xffffffffffffffffn;
/** Validate architecture, entry, and that the mapped image fits the CPU width. */
export const admitRawBinary = (input: {
readonly architecture: unknown;
readonly entryPoint: unknown;
readonly fileBytes: number;
}): Result<RawBinaryAdmission, RawBinaryAdmissionRejection> => {
const architecture = binaryArchitectureSchema.safeParse(input.architecture);
if (!architecture.success)
return err({
reason:
"raw-binary format requires architecture (x86, x86_64, arm, arm64, 6502, or m68k)",
constraint: "load_selection_rejected",
});
if (typeof input.entryPoint !== "string")
return err({
reason:
"raw-binary format requires entry_point as a hexadecimal address of the form 0x…",
constraint: "load_selection_rejected",
});
const entry = parseRawBinaryEntryPoint(input.entryPoint);
if (!entry.ok)
return err({
reason: entry.error,
constraint: "load_selection_rejected",
});
if (
(architecture.data === "arm" || architecture.data === "arm64") &&
entry.value.value % 4n !== 0n
)
return err({
reason: `raw-binary ${architecture.data} entry_point ${entry.value.canonical} must be 4-byte aligned`,
constraint: "load_selection_rejected",
});
// 68000-family instruction words are fetched from even addresses.
if (architecture.data === "m68k" && entry.value.value % 2n !== 0n)
return err({
reason: `raw-binary m68k entry_point ${entry.value.canonical} must be 2-byte aligned`,
constraint: "load_selection_rejected",
});
const length = validateRawBinaryLength(input.fileBytes);
if (!length.ok)
return err({
reason: length.error,
constraint: "file_format_rejected",
});
const last = entry.value.value + BigInt(input.fileBytes) - 1n;
const max = addressWidth(architecture.data);
if (entry.value.value > max || last > max)
return err({
reason: `raw-binary mapping from ${entry.value.canonical} must fit in the ${architecture.data} address width`,
constraint: "load_selection_rejected",
});
return ok({
architecture: architecture.data,
entryPoint: entry.value.canonical,
entryPointSource: entry.value.source,
});
};
/** Describe the format/architecture/entry pairing without performing I/O. */
export const rawBinarySelectionError = (input: {
readonly format?: ExecutableFormatHint;
readonly architecture?: unknown;
readonly entryPoint?: unknown;
}): string | undefined => {
const hasArchitecture = input.architecture !== undefined;
const hasEntry = input.entryPoint !== undefined;
if (input.format === "gamecube-dol") {
// The DOL header fixes architecture; entry_point is checked against it at admission.
return hasArchitecture
? "format=gamecube-dol takes its architecture from the DOL header; omit architecture"
: undefined;
}
if (input.format === "raw-binary") {
if (!hasArchitecture || !hasEntry)
return "format=raw-binary requires architecture and entry_point";
return undefined;
}
if (hasArchitecture || hasEntry)
return "architecture and entry_point apply only with format=raw-binary";
return undefined;
};