fix(targets): project invalid raw-binary loads as caller input (#2004)
* fix(targets): project invalid raw-binary loads as caller input * fix(targets): name real load options and cover DOS COM kind pairing
This commit is contained in:
parent
5dfb9d050c
commit
edf46248f2
|
|
@ -26,7 +26,10 @@ import {
|
|||
validateDosComLength,
|
||||
type ExecutableFormatHint,
|
||||
} from "../domain/dosCom.js";
|
||||
import { admitRawBinary } from "../domain/rawBinary.js";
|
||||
import {
|
||||
admitRawBinary,
|
||||
rawBinarySelectionError,
|
||||
} from "../domain/rawBinary.js";
|
||||
import type { BinaryArchitecture } from "../domain/binaryTargetTypes.js";
|
||||
import { mzWindowsHeaderOffset } from "../domain/dosMz.js";
|
||||
import {
|
||||
|
|
@ -93,7 +96,7 @@ export const parseBinaryTarget = async (
|
|||
new BinaryTargetError(
|
||||
path,
|
||||
"raw-binary format requires an executable target kind",
|
||||
{ constraint: "file_format_rejected" },
|
||||
{ constraint: "load_selection_rejected" },
|
||||
),
|
||||
);
|
||||
const admitted = admitRawBinary({
|
||||
|
|
@ -103,8 +106,8 @@ export const parseBinaryTarget = async (
|
|||
});
|
||||
if (!admitted.ok)
|
||||
return err(
|
||||
new BinaryTargetError(path, admitted.error, {
|
||||
constraint: "file_format_rejected",
|
||||
new BinaryTargetError(path, admitted.error.reason, {
|
||||
constraint: admitted.error.constraint,
|
||||
}),
|
||||
);
|
||||
return ok({
|
||||
|
|
@ -119,12 +122,16 @@ export const parseBinaryTarget = async (
|
|||
entryPointSource: admitted.value.entryPointSource,
|
||||
});
|
||||
}
|
||||
if (architecture !== undefined || entryPoint !== undefined)
|
||||
const pairing = rawBinarySelectionError({
|
||||
...(formatHint === undefined ? {} : { format: formatHint }),
|
||||
architecture,
|
||||
entryPoint,
|
||||
});
|
||||
if (pairing !== undefined)
|
||||
return err(
|
||||
new BinaryTargetError(
|
||||
path,
|
||||
"architecture and entry_point apply only with format=raw-binary",
|
||||
),
|
||||
new BinaryTargetError(path, pairing, {
|
||||
constraint: "load_selection_rejected",
|
||||
}),
|
||||
);
|
||||
if (formatHint === "dos-com") {
|
||||
if (targetKind !== undefined && targetKind !== "executable")
|
||||
|
|
@ -132,7 +139,7 @@ export const parseBinaryTarget = async (
|
|||
new BinaryTargetError(
|
||||
path,
|
||||
"DOS COM format requires an executable target kind",
|
||||
{ constraint: "file_format_rejected" },
|
||||
{ constraint: "load_selection_rejected" },
|
||||
),
|
||||
);
|
||||
const length = validateDosComLength(metadata.size);
|
||||
|
|
|
|||
|
|
@ -76,6 +76,11 @@ export const analysisErrorRemediationAction = (
|
|||
error.constraint === "directory_requires_file"
|
||||
)
|
||||
return "For a JavaScript/Electron application directory, call analyze_javascript_application with input_path or run `rea analyze <directory>`. For binary analysis, select its executable file.";
|
||||
if (
|
||||
error instanceof BinaryTargetError &&
|
||||
error.constraint === "load_selection_rejected"
|
||||
)
|
||||
return "Correct the reported load argument (--target-format, --architecture, or --entry-point on the CLI; format, architecture, or entry_point through MCP) and retry.";
|
||||
if (
|
||||
error instanceof BinaryTargetError &&
|
||||
error.constraint === "file_format_rejected"
|
||||
|
|
@ -105,6 +110,11 @@ export const analysisErrorRemediationAction = (
|
|||
export const analysisErrorCategory = (
|
||||
error: AnalysisError,
|
||||
): AnalysisErrorProjection["category"] => {
|
||||
if (
|
||||
error instanceof BinaryTargetError &&
|
||||
error.constraint === "load_selection_rejected"
|
||||
)
|
||||
return "invalid_input";
|
||||
if (
|
||||
error instanceof BinaryTargetError &&
|
||||
(error.constraint === "wasm_requires_static_inspection" ||
|
||||
|
|
@ -237,7 +247,8 @@ export const analysisErrorUserMessage = (error: AnalysisError): string => {
|
|||
if (error instanceof BinaryTargetError)
|
||||
return error.constraint === "directory_requires_file"
|
||||
? "open_binary accepts files and macOS app bundles. This target is a directory; JavaScript/Electron application directories can be analyzed directly with analyze_javascript_application or `rea analyze <directory>`."
|
||||
: error.constraint === "file_format_rejected"
|
||||
: error.constraint === "file_format_rejected" ||
|
||||
error.constraint === "load_selection_rejected"
|
||||
? `${error.message}.`
|
||||
: `${error.message}. Check that the path exists, is readable, and points to a supported file.`;
|
||||
if (error._tag === "ProcessCaptureError")
|
||||
|
|
|
|||
|
|
@ -105,9 +105,11 @@ const underlyingErrorCode = (
|
|||
if (error instanceof ArtifactOperationError)
|
||||
return artifactOperationCode(error);
|
||||
if (error instanceof BinaryTargetError)
|
||||
return error.systemCode === undefined
|
||||
? "target_unavailable"
|
||||
: "access_denied";
|
||||
return error.constraint === "load_selection_rejected"
|
||||
? "invalid_request"
|
||||
: error.systemCode === undefined
|
||||
? "target_unavailable"
|
||||
: "access_denied";
|
||||
if (error instanceof EvidenceFileError) return evidenceFileCode(error.reason);
|
||||
if (error instanceof UnknownRegistryError)
|
||||
return unknownRegistryCode(error.reason);
|
||||
|
|
|
|||
|
|
@ -41,7 +41,8 @@ export interface BinaryTargetErrorOptions extends AnalysisErrorOptions {
|
|||
readonly constraint?:
|
||||
| "directory_requires_file"
|
||||
| "wasm_requires_static_inspection"
|
||||
| "file_format_rejected";
|
||||
| "file_format_rejected"
|
||||
| "load_selection_rejected";
|
||||
}
|
||||
|
||||
/** A supplied target path could not be safely opened as a supported app or binary. */
|
||||
|
|
|
|||
|
|
@ -55,15 +55,20 @@ describe("raw-binary admission", () => {
|
|||
},
|
||||
});
|
||||
});
|
||||
it("rejects an empty file", () => {
|
||||
it("rejects an empty file as a target refusal while naming the extent", () => {
|
||||
expect(validateRawBinaryLength(0).ok).toBe(false);
|
||||
expect(
|
||||
admitRawBinary({
|
||||
architecture: "x86",
|
||||
entryPoint: "0x1000",
|
||||
fileBytes: 0,
|
||||
}).ok,
|
||||
).toBe(false);
|
||||
const result = admitRawBinary({
|
||||
architecture: "x86",
|
||||
entryPoint: "0x1000",
|
||||
fileBytes: 0,
|
||||
});
|
||||
expect(result).toEqual({
|
||||
ok: false,
|
||||
error: {
|
||||
reason: "raw-binary requires at least one file byte",
|
||||
constraint: "file_format_rejected",
|
||||
},
|
||||
});
|
||||
});
|
||||
it("rejects a 32-bit mapping that exceeds the architecture width", () => {
|
||||
const result = admitRawBinary({
|
||||
|
|
@ -73,7 +78,8 @@ describe("raw-binary admission", () => {
|
|||
});
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("expected overflow rejection");
|
||||
expect(result.error).toContain("arm address width");
|
||||
expect(result.error.reason).toContain("arm address width");
|
||||
expect(result.error.constraint).toBe("load_selection_rejected");
|
||||
});
|
||||
it("allows a 64-bit mapping that would overflow 32-bit ARM", () => {
|
||||
expect(
|
||||
|
|
@ -92,11 +98,27 @@ describe("raw-binary admission", () => {
|
|||
admitRawBinary({ architecture, entryPoint, fileBytes: 4 }),
|
||||
).toEqual({
|
||||
ok: false,
|
||||
error: `raw-binary ${architecture} entry_point ${entryPoint} must be 4-byte aligned`,
|
||||
error: {
|
||||
reason: `raw-binary ${architecture} entry_point ${entryPoint} must be 4-byte aligned`,
|
||||
constraint: "load_selection_rejected",
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
it.each([
|
||||
["malformed entry", { architecture: "arm", entryPoint: "0xgg" }],
|
||||
["missing entry", { architecture: "arm", entryPoint: undefined }],
|
||||
[
|
||||
"missing architecture",
|
||||
{ architecture: undefined, entryPoint: "0x8000000" },
|
||||
],
|
||||
])("classifies a %s as caller input", (_name, selection) => {
|
||||
const result = admitRawBinary({ ...selection, fileBytes: 4 });
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("expected caller-input rejection");
|
||||
expect(result.error.constraint).toBe("load_selection_rejected");
|
||||
});
|
||||
it.each(["x86", "x86_64"])(
|
||||
"admits byte-aligned %s entries",
|
||||
(architecture) => {
|
||||
|
|
@ -105,7 +127,7 @@ describe("raw-binary admission", () => {
|
|||
entryPoint: "0x1001",
|
||||
fileBytes: 1,
|
||||
});
|
||||
if (!admitted.ok) throw new Error(admitted.error);
|
||||
if (!admitted.ok) throw new Error(admitted.error.reason);
|
||||
expect(admitted.value.entryPoint).toBe("0x1001");
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -60,6 +60,16 @@ export interface RawBinaryAdmission {
|
|||
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 === "x86" || architecture === "arm"
|
||||
? 0xffffffffn
|
||||
|
|
@ -70,33 +80,47 @@ export const admitRawBinary = (input: {
|
|||
readonly architecture: unknown;
|
||||
readonly entryPoint: unknown;
|
||||
readonly fileBytes: number;
|
||||
}): Result<RawBinaryAdmission, string> => {
|
||||
}): Result<RawBinaryAdmission, RawBinaryAdmissionRejection> => {
|
||||
const architecture = binaryArchitectureSchema.safeParse(input.architecture);
|
||||
if (!architecture.success)
|
||||
return err(
|
||||
"raw-binary format requires architecture (x86, x86_64, arm, or arm64)",
|
||||
);
|
||||
return err({
|
||||
reason:
|
||||
"raw-binary format requires architecture (x86, x86_64, arm, or arm64)",
|
||||
constraint: "load_selection_rejected",
|
||||
});
|
||||
if (typeof input.entryPoint !== "string")
|
||||
return err(
|
||||
"raw-binary format requires entry_point as a hexadecimal address of the form 0x…",
|
||||
);
|
||||
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 entry;
|
||||
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(
|
||||
`raw-binary ${architecture.data} entry_point ${entry.value.canonical} must be 4-byte aligned`,
|
||||
);
|
||||
return err({
|
||||
reason: `raw-binary ${architecture.data} entry_point ${entry.value.canonical} must be 4-byte aligned`,
|
||||
constraint: "load_selection_rejected",
|
||||
});
|
||||
const length = validateRawBinaryLength(input.fileBytes);
|
||||
if (!length.ok) return length;
|
||||
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(
|
||||
`raw-binary mapping from ${entry.value.canonical} must fit in the ${architecture.data} address width`,
|
||||
);
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ cliTest(
|
|||
});
|
||||
expect(missing.exitCode).toBe(1);
|
||||
expect(missing.json).toMatchObject({
|
||||
code: "target_unavailable",
|
||||
code: "invalid_request",
|
||||
details: { reason: expect.stringContaining("architecture") },
|
||||
});
|
||||
},
|
||||
|
|
@ -62,7 +62,7 @@ cliTest(
|
|||
});
|
||||
expect(result.exitCode).toBe(1);
|
||||
expect(result.json).toMatchObject({
|
||||
code: "target_unavailable",
|
||||
code: "invalid_request",
|
||||
details: { reason: expect.stringContaining("must be 4-byte aligned") },
|
||||
});
|
||||
},
|
||||
|
|
|
|||
|
|
@ -130,7 +130,6 @@ describe("target route diagnostics", () => {
|
|||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("identifies the directory opening constraint and gives the direct analysis route", async () => {
|
||||
const directory = await createTestTempDirectory("rea-js-target-");
|
||||
await writeFile(join(directory, "index.js"), "export const answer = 42;\n");
|
||||
|
|
@ -201,3 +200,123 @@ describe("target route diagnostics", () => {
|
|||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("raw-binary load rejection diagnostics", () => {
|
||||
const rawBinaryRejection = async (
|
||||
options: Parameters<typeof parseBinaryTarget>[1],
|
||||
bytes = Buffer.alloc(64),
|
||||
) => {
|
||||
const directory = await createTestTempDirectory("rea-raw-load-");
|
||||
const path = join(directory, "image.bin");
|
||||
await writeFile(path, bytes);
|
||||
const result = await parseBinaryTarget(path, options);
|
||||
if (result.ok) throw new Error("invalid raw-binary load was admitted");
|
||||
return projectAnalysisError(result.error);
|
||||
};
|
||||
|
||||
it("projects a misaligned arm entry as caller input without path advice", async () => {
|
||||
const projected = await rawBinaryRejection({
|
||||
formatHint: "raw-binary",
|
||||
architecture: "arm",
|
||||
entryPoint: "0x1",
|
||||
});
|
||||
expect(projected).toMatchObject({
|
||||
code: "invalid_request",
|
||||
category: "invalid_input",
|
||||
// invalid_request is retryable: correcting the load argument succeeds.
|
||||
retryable: true,
|
||||
message:
|
||||
"Cannot open artifact: raw-binary arm entry_point 0x1 must be 4-byte aligned.",
|
||||
details: {
|
||||
reason: "raw-binary arm entry_point 0x1 must be 4-byte aligned",
|
||||
constraint: "load_selection_rejected",
|
||||
},
|
||||
remediation: {
|
||||
action:
|
||||
"Correct the reported load argument (--target-format, --architecture, or --entry-point on the CLI; format, architecture, or entry_point through MCP) and retry.",
|
||||
},
|
||||
});
|
||||
expect(projected.message).not.toContain("Check that the path");
|
||||
});
|
||||
|
||||
it("projects an x86 entry beyond the address width as caller input", async () => {
|
||||
const projected = await rawBinaryRejection({
|
||||
formatHint: "raw-binary",
|
||||
architecture: "x86",
|
||||
entryPoint: "0xffffffffff",
|
||||
});
|
||||
expect(projected).toMatchObject({
|
||||
code: "invalid_request",
|
||||
category: "invalid_input",
|
||||
details: { constraint: "load_selection_rejected" },
|
||||
});
|
||||
expect(projected.message).toContain("x86 address width");
|
||||
expect(projected.message).not.toContain("Check that the path");
|
||||
});
|
||||
|
||||
it("projects architecture and entry without raw-binary format as caller input", async () => {
|
||||
const projected = await rawBinaryRejection({
|
||||
architecture: "arm",
|
||||
entryPoint: "0x0",
|
||||
});
|
||||
expect(projected).toMatchObject({
|
||||
code: "invalid_request",
|
||||
category: "invalid_input",
|
||||
message:
|
||||
"Cannot open artifact: architecture and entry_point apply only with format=raw-binary.",
|
||||
details: { constraint: "load_selection_rejected" },
|
||||
});
|
||||
expect(projected.message).not.toContain("Check that the path");
|
||||
});
|
||||
|
||||
it("keeps an empty raw-binary file a target refusal", async () => {
|
||||
const projected = await rawBinaryRejection(
|
||||
{
|
||||
formatHint: "raw-binary",
|
||||
architecture: "arm",
|
||||
entryPoint: "0x8000000",
|
||||
},
|
||||
Buffer.alloc(0),
|
||||
);
|
||||
expect(projected).toMatchObject({
|
||||
code: "target_unavailable",
|
||||
category: "unsupported_target",
|
||||
details: {
|
||||
reason: "raw-binary requires at least one file byte",
|
||||
constraint: "file_format_rejected",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("projects the raw-binary target-kind pairing as caller input", async () => {
|
||||
const projected = await rawBinaryRejection({
|
||||
formatHint: "raw-binary",
|
||||
targetKind: "database",
|
||||
architecture: "x86",
|
||||
entryPoint: "0x1000",
|
||||
});
|
||||
expect(projected).toMatchObject({
|
||||
code: "invalid_request",
|
||||
category: "invalid_input",
|
||||
details: {
|
||||
reason: "raw-binary format requires an executable target kind",
|
||||
constraint: "load_selection_rejected",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("projects the DOS COM target-kind pairing as caller input too", async () => {
|
||||
const projected = await rawBinaryRejection({
|
||||
formatHint: "dos-com",
|
||||
targetKind: "database",
|
||||
});
|
||||
expect(projected).toMatchObject({
|
||||
code: "invalid_request",
|
||||
category: "invalid_input",
|
||||
details: {
|
||||
reason: "DOS COM format requires an executable target kind",
|
||||
constraint: "load_selection_rejected",
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue