fix(cli): validate byte queries before opening targets (#7723)

This commit is contained in:
gensokyo-dev
2026-10-28 15:00:01 +08:00
committed by GitHub
parent b0fb51cb4c
commit a0a1962e06
3 changed files with 196 additions and 9 deletions
+36 -9
View File
@@ -1,6 +1,13 @@
import { projectCliAnalysisResult } from "./projectCliAnalysisResult.js";
import { nativeDispatchMetadataInputSchema } from "../domain/native/objcSwiftMetadata.js";
import { z } from "incur";
import {
bytePatternFields,
validateBytePattern,
MAX_BYTE_SEARCH_MATCHES,
} from "../domain/native/bytePattern.js";
import { analysisInputErrorFromIssues } from "../domain/inputIssueProjection.js";
import {
addressOrProcedureGuidance,
equalSelectorGuidance,
@@ -25,6 +32,10 @@ import {
} from "./analysisEvidenceDelivery.js";
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
const bytePatternInputSchema = z
.object(bytePatternFields)
.superRefine(validateBytePattern);
/** Register native load-image, memory, and annotation CLI commands. */
export const registerCoreNativeCommands = (
cli: CliInstance,
@@ -36,10 +47,10 @@ export const registerCoreNativeCommands = (
description: "Find masked byte patterns in initialized provider memory",
args: z.object({
path: z.string().describe("Local executable path"),
pattern: z.string().describe("Hexadecimal byte pairs with ?? wildcards"),
pattern: bytePatternFields.pattern,
}),
options: z.object({
mask: z.string().optional().describe("Equal-length hexadecimal bit mask"),
mask: bytePatternFields.mask,
start: z.string().optional().describe("Inclusive start address"),
end: z.string().optional().describe("Inclusive end address"),
cursor: z
@@ -50,27 +61,43 @@ export const registerCoreNativeCommands = (
.number()
.int()
.min(1)
.max(1000)
.max(MAX_BYTE_SEARCH_MATCHES)
.default(100)
.describe("Maximum matches on this page"),
...nativeOpenOptionFields,
}),
run: ({ args, options }) =>
logCliCommand(logger, CLI_COMMANDS.searchBytes, () =>
runDirectAnalysis(
logCliCommand(logger, CLI_COMMANDS.searchBytes, async () => {
const selection = {
pattern: args.pattern,
...(options.mask === undefined ? {} : { mask: options.mask }),
};
const parsed = bytePatternInputSchema.safeParse(selection);
if (!parsed.success)
return projectAnalysisError(
analysisInputErrorFromIssues(
"search_bytes",
parsed.error.issues,
selection,
{ cause: parsed.error },
),
);
return runDirectAnalysis(
args.path,
"search_bytes",
{
pattern: args.pattern,
pattern: parsed.data.pattern,
...(parsed.data.mask === undefined
? {}
: { mask: parsed.data.mask }),
limit: options.limit,
...(options.mask === undefined ? {} : { mask: options.mask }),
...(options.start === undefined ? {} : { start: options.start }),
...(options.end === undefined ? {} : { end: options.end }),
...(options.cursor === undefined ? {} : { cursor: options.cursor }),
},
directAnalysisOptionsFromCli(logger, options),
).then(projectCliAnalysisResult),
),
).then(projectCliAnalysisResult);
}),
});
cli.command(CLI_COMMANDS.inspectNativeLoadImage, {
description:
@@ -0,0 +1,54 @@
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { expect } from "vitest";
import { cliTest } from "../support/cli/cliFixture.js";
import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js";
cliTest.for([
{ pattern: "??", mask: undefined, field: "pattern" },
{ pattern: "ZZ", mask: undefined, field: "pattern" },
{ pattern: "", mask: undefined, field: "pattern" },
{ pattern: "AB", mask: "FFFF", field: "mask" },
])(
"reports query correction before target refusal ($pattern/$mask)",
async ({ pattern, mask, field }, { cli }) => {
const root = await createTestTempDirectory("rea-byte-admission-");
const text = join(root, "text.bin");
await writeFile(text, "hello world");
for (const path of [text, join(root, "missing")]) {
const result = await cli.run({
arguments: [
"search-bytes",
path,
pattern,
...(mask === undefined ? [] : ["--mask", mask]),
"--json",
],
});
expect(result.exitCode).toBe(1);
expect(result.json).toMatchObject({
code: "invalid_request",
category: "invalid_input",
details: {
issues: [
expect.objectContaining({ path: [field], reason: "invalid_value" }),
],
},
});
}
},
);
cliTest("keeps target admission for a valid byte query", async ({ cli }) => {
const root = await createTestTempDirectory("rea-byte-valid-");
const path = join(root, "text.bin");
await writeFile(path, "hello world");
const result = await cli.run({
arguments: ["search-bytes", path, "AB", "--json"],
});
expect(result.exitCode).toBe(1);
expect(result.json).toMatchObject({
code: "target_unavailable",
category: "unsupported_target",
});
});
@@ -0,0 +1,106 @@
import { Cli } from "incur";
import pino from "pino";
import { expect, it, vi } from "vitest";
import { registerCoreNativeCommands } from "../../../src/cli/coreNativeCommands.js";
const registry = () => {
const run = vi.fn(async () => ({
ok: false as const,
error: { code: "invalid_request", message: "Admitted query control" },
}));
const cli = Cli.create("byte-admission", { sync: false });
registerCoreNativeCommands(cli, pino({ enabled: false }), run);
return { cli, run };
};
it.each([
{ pattern: "??", mask: undefined, field: "pattern" },
{ pattern: "ZZ", mask: undefined, field: "pattern" },
{ pattern: "", mask: undefined, field: "pattern" },
{ pattern: "AB", mask: "FFFF", field: "mask" },
{ pattern: "AB", mask: "00", field: "pattern" },
])(
"rejects $pattern/$mask before any target access",
async ({ pattern, mask, field }) => {
const { cli, run } = registry();
let stdout = "";
await cli.serve(
[
"search-bytes",
"/missing-target",
pattern,
...(mask === undefined ? [] : ["--mask", mask]),
"--json",
],
{
stdout: (text) => {
stdout += text;
},
exit: () => undefined,
},
);
expect(run).not.toHaveBeenCalled();
expect(JSON.parse(stdout)).toMatchObject({
code: "invalid_request",
category: "invalid_input",
details: {
operation: "search_bytes",
issues: [
expect.objectContaining({ path: [field], reason: "invalid_value" }),
],
},
});
},
);
it("forwards admitted original strings and caller-selected search/provider controls", async () => {
const { cli, run } = registry();
await cli.serve(
[
"search-bytes",
"/selected",
"ab ?? CD",
"--mask",
"FF 00 ff",
"--start",
"0x10",
"--end",
"0x20",
"--cursor",
"next",
"--limit",
"7",
"--provider",
"ghidra",
"--json",
],
{ stdout: () => undefined, exit: () => undefined },
);
expect(run).toHaveBeenCalledExactlyOnceWith(
"/selected",
"search_bytes",
{
pattern: "ab ?? CD",
mask: "FF 00 ff",
start: "0x10",
end: "0x20",
cursor: "next",
limit: 7,
},
expect.objectContaining({ providerId: "ghidra" }),
);
});
it("keeps an omitted mask omitted", async () => {
const { cli, run } = registry();
await cli.serve(["search-bytes", "/selected", "AB", "--json"], {
stdout: () => undefined,
exit: () => undefined,
});
expect(run).toHaveBeenCalledExactlyOnceWith(
"/selected",
"search_bytes",
{ pattern: "AB", limit: 100 },
expect.any(Object),
);
});