fix(ida): supervise regex inventory matching (#4862)
This commit is contained in:
1 parent
e3a00bb5f9
commit
db8556e724
3 files changed
+170
-11
No files matched your search
@@ -9,7 +9,12 @@ import {
|
||||
import {
|
||||
analysisSearchPredicate,
|
||||
analysisSearchQuerySchema,
|
||||
type AnalysisSearchQuery,
|
||||
} from "../domain/analysisSearch.js";
|
||||
import {
|
||||
matchRegexValues,
|
||||
REGEX_MATCHING_LIMITATION,
|
||||
} from "../process/RegexMatcher.js";
|
||||
import {
|
||||
AnalysisInputError,
|
||||
AnalysisCancelledError,
|
||||
@@ -584,6 +589,29 @@ export class IdaOperationRunner {
|
||||
};
|
||||
}
|
||||
|
||||
private async searchItems<T extends { readonly value: string }>(
|
||||
operation: IdaOperation,
|
||||
items: T[],
|
||||
query: AnalysisSearchQuery | undefined,
|
||||
): Promise<T[]> {
|
||||
if (query === undefined) return items;
|
||||
if (query.mode === "literal") {
|
||||
const matches = analysisSearchPredicate(query);
|
||||
return items.filter(({ value }) => matches(value));
|
||||
}
|
||||
this.limitations.push(REGEX_MATCHING_LIMITATION);
|
||||
const matched = await matchRegexValues({
|
||||
operation,
|
||||
pattern: query.pattern,
|
||||
caseSensitive: query.case_sensitive,
|
||||
values: items.map(({ value }) => value),
|
||||
signal: this.options.signal,
|
||||
});
|
||||
if (!matched.ok) throw matched.error;
|
||||
const indices = new Set(matched.value);
|
||||
return items.filter((_, index) => indices.has(index));
|
||||
}
|
||||
|
||||
async run(
|
||||
operation: IdaOperation,
|
||||
parameters: Readonly<Record<string, JsonValue>>,
|
||||
@@ -592,16 +620,12 @@ export class IdaOperationRunner {
|
||||
operation === "search_procedures" || operation === "search_strings"
|
||||
? analysisSearchQuerySchema.parse(parameters)
|
||||
: undefined;
|
||||
const matches =
|
||||
query === undefined ? undefined : analysisSearchPredicate(query);
|
||||
if (operation === "list_procedures" || operation === "search_procedures") {
|
||||
const items = (await this.functions()).map((fn) => ({
|
||||
address: fn.address,
|
||||
value: fn.name,
|
||||
}));
|
||||
return matches === undefined
|
||||
? items
|
||||
: items.filter(({ value }) => matches(value));
|
||||
return this.searchItems(operation, items, query);
|
||||
}
|
||||
if (operation === "list_strings" || operation === "search_strings") {
|
||||
const nativePattern =
|
||||
@@ -609,8 +633,8 @@ export class IdaOperationRunner {
|
||||
? nativeLiteralSearchPattern(query.pattern, query.case_sensitive)
|
||||
: undefined;
|
||||
const items = await this.strings(nativePattern);
|
||||
return matches !== undefined
|
||||
? items.filter(({ value }) => matches(value))
|
||||
return query !== undefined
|
||||
? this.searchItems(operation, items, query)
|
||||
: parameters.address === undefined
|
||||
? items
|
||||
: items.filter(
|
||||
|
||||
@@ -2,6 +2,7 @@ import type {
|
||||
AnalysisOperation,
|
||||
CapabilityDescriptor,
|
||||
} from "../application/AnalysisProvider.js";
|
||||
import { REGEX_MATCHING_LIMITATION } from "../process/RegexMatcher.js";
|
||||
|
||||
/** This version identifies REA's adapter, not IDA or the upstream MCP server. */
|
||||
export const IDA_PROVIDER_IDENTITY = Object.freeze({
|
||||
@@ -98,10 +99,15 @@ export const idaCapabilities = (
|
||||
changesPermissions: false,
|
||||
requiresRoot: false,
|
||||
},
|
||||
limitations:
|
||||
native &&
|
||||
limitations: [
|
||||
...IDA_LIMITATIONS,
|
||||
...(native &&
|
||||
(operation === "list_strings" || operation === "search_strings")
|
||||
? [...IDA_LIMITATIONS, IDA_NATIVE_STRING_LIMITATION]
|
||||
: IDA_LIMITATIONS,
|
||||
? [IDA_NATIVE_STRING_LIMITATION]
|
||||
: []),
|
||||
...(operation === "search_procedures" || operation === "search_strings"
|
||||
? [REGEX_MATCHING_LIMITATION]
|
||||
: []),
|
||||
],
|
||||
}),
|
||||
);
|
||||
@@ -461,6 +461,135 @@ describe("attached IDA function observations", () => {
|
||||
).toBe(false);
|
||||
await client.close();
|
||||
});
|
||||
});
|
||||
|
||||
describe("attached IDA regex supervision", () => {
|
||||
it.each(["search_procedures", "search_strings"] as const)(
|
||||
"preserves Unicode case semantics and inventory order in %s",
|
||||
async (operation) => {
|
||||
const { producer, client } = await fixture();
|
||||
const values = ["skip", "ÄPFEL", "äpfel"];
|
||||
producer.overrideCall = (name) =>
|
||||
name === "list_functions"
|
||||
? {
|
||||
data: values.map((name, index) => ({
|
||||
address: `0x${String(1000 + index)}`,
|
||||
name,
|
||||
size: "0x10",
|
||||
})),
|
||||
next_offset: null,
|
||||
}
|
||||
: name === "list_strings"
|
||||
? {
|
||||
data: values.map((string, index) => ({
|
||||
address: `0x${String(3000 + index)}`,
|
||||
length: string.length,
|
||||
string,
|
||||
})),
|
||||
next_offset: null,
|
||||
}
|
||||
: undefined;
|
||||
try {
|
||||
for (const case_sensitive of [false, true]) {
|
||||
const found = await client.execute(operation, {
|
||||
pattern: "^ä",
|
||||
mode: "regex",
|
||||
case_sensitive,
|
||||
});
|
||||
expect(found.ok && found.value.result).toEqual(
|
||||
values.flatMap((value, index) =>
|
||||
index === 0 || (case_sensitive && index === 1)
|
||||
? []
|
||||
: [
|
||||
{
|
||||
address: `0x${String((operation === "search_strings" ? 3000 : 1000) + index)}`,
|
||||
value,
|
||||
},
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
await client.close();
|
||||
}
|
||||
},
|
||||
);
|
||||
it.each(["search_procedures", "search_strings"] as const)(
|
||||
"cancels CPU-bound %s matching and keeps the session usable",
|
||||
async (operation) => {
|
||||
const { producer, client } = await fixture();
|
||||
const value = "a".repeat(26) + "!";
|
||||
producer.overrideCall = (name) =>
|
||||
name === "list_functions"
|
||||
? {
|
||||
data: [{ address: "0x1000", name: value, size: "0x10" }],
|
||||
next_offset: null,
|
||||
}
|
||||
: name === "list_strings"
|
||||
? {
|
||||
data: [
|
||||
{ address: "0x3000", length: value.length, string: value },
|
||||
],
|
||||
next_offset: null,
|
||||
}
|
||||
: undefined;
|
||||
await client.execute("health", {});
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), 50);
|
||||
try {
|
||||
const result = await client.execute(
|
||||
operation,
|
||||
{ pattern: "^(a+)+$", mode: "regex" },
|
||||
{ signal: controller.signal },
|
||||
);
|
||||
expect(result.ok).toBe(false);
|
||||
if (!result.ok)
|
||||
expect(result.error._tag).toBe("AnalysisCancelledError");
|
||||
const recovered = await client.execute(operation, {
|
||||
pattern: "!",
|
||||
mode: "literal",
|
||||
});
|
||||
expect(recovered.ok && recovered.value.result).toHaveLength(1);
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
await client.close();
|
||||
}
|
||||
},
|
||||
15_000,
|
||||
);
|
||||
it("bounds regex matching without caller cancellation and preserves subsequent searches", async () => {
|
||||
const { producer, client } = await fixture();
|
||||
const value = "a".repeat(40) + "!";
|
||||
producer.overrideCall = (name) =>
|
||||
name === "list_strings"
|
||||
? {
|
||||
data: [{ address: "0x3000", length: value.length, string: value }],
|
||||
next_offset: null,
|
||||
}
|
||||
: undefined;
|
||||
try {
|
||||
const result = await client.execute("search_strings", {
|
||||
pattern: "^(a+)+$",
|
||||
mode: "regex",
|
||||
});
|
||||
expect(result.ok).toBe(false);
|
||||
if (!result.ok)
|
||||
expect(result.error._tag).toBe("AnalysisResourceConstraintError");
|
||||
const recovered = await client.execute("search_strings", {
|
||||
pattern: "!$",
|
||||
mode: "regex",
|
||||
});
|
||||
expect(recovered.ok && recovered.value.result).toHaveLength(1);
|
||||
expect(recovered.ok && recovered.value.limitations).toContain(
|
||||
"Regex mode uses ECMAScript Unicode semantics in a cancellable worker with a 5-second matching deadline; matching failure leaves the service available.",
|
||||
);
|
||||
} finally {
|
||||
await client.close();
|
||||
}
|
||||
}, 15_000);
|
||||
});
|
||||
|
||||
describe("attached IDA function failure handling", () => {
|
||||
it("rejects mismatched identities and target switches rather than returning a successful observation", async () => {
|
||||
const { producer, client, target } = await fixture();
|
||||
producer.inputSha256 = "a".repeat(64);
|
||||
|
||||
Reference in new issue
Block a user