fix(errors): preserve caller abort reasons at admission (#5671)

* fix(errors): preserve caller abort reasons at admission

Keep the opaque caller reason on reverse, pcap, and SQLite cancellation,
and leave a recognized physical digest or command failure in place when a
late abort is also observed.

Fixes #5670

Co-authored-by: morluto <morluto@users.noreply.github.com>

* fix(reverse): keep a foreign digest ABORT_ERR as an adapter failure

Treat ABORT_ERR as cancellation on the command boundary, where that code
already meant an aborted provider process. A digest or stat rejection with
the same code stays a physical adapter failure unless the caller signal
is aborted. Cover aborted command EACCES, ETIMEDOUT, and ABORT_ERR with
and without captured output.

Co-authored-by: morluto <morluto@users.noreply.github.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: morluto <morluto@users.noreply.github.com>
This commit is contained in:
authored and GitHub committed 2026-10-21 04:16:44 +08:00
1 parent 6389e50337
commit 6b101ff23b
6 files changed
+324 -10

No files matched your search

@@ -0,0 +1,28 @@
import { expect, it } from "vitest";
import { PcapAnalysisService } from "./PcapAnalysisService.js";
it("preserves the exact caller reason when pcap admission is already aborted", async () => {
const reason = Object.freeze({ requested: "stop" });
const controller = new AbortController();
controller.abort(reason);
let calls = 0;
const service = new PcapAnalysisService({
inspectAvailability: () => Promise.reject(new Error("unused")),
close: () => Promise.resolve(),
execute: () => {
calls += 1;
return Promise.reject(new Error("provider must not run"));
},
});
const result = await service.execute(
"inspect_pcap_client",
{},
{ signal: controller.signal },
);
expect(result.ok).toBe(false);
if (result.ok) return;
expect(result.error._tag).toBe("AnalysisCancelledError");
expect(result.error.cause).toBe(reason);
expect(calls).toBe(0);
});
+5 -1
View File
@@ -24,7 +24,11 @@ export class PcapAnalysisService {
options?: ExecutionOptions,
): Promise<Result<Evidence, AnalysisError>> {
if (options?.signal?.aborted === true)
return err(new AnalysisCancelledError(operation));
return err(
new AnalysisCancelledError(operation, {
cause: options.signal.reason,
}),
);
const request = pcapRequestSchema.safeParse({ operation, input });
if (!request.success)
return err(new AnalysisInputError(operation, { cause: request.error }));
@@ -250,7 +250,9 @@ export class ReverseEngineeringService {
signal?: AbortSignal,
): Promise<Result<Evidence, AnalysisError>> {
if (signal?.aborted)
return Promise.resolve(err(new AnalysisCancelledError(operation)));
return Promise.resolve(
err(new AnalysisCancelledError(operation, { cause: signal.reason })),
);
if (this.#closing)
return Promise.resolve(
err(
@@ -311,7 +313,10 @@ export class ReverseEngineeringService {
signal?: AbortSignal,
): Promise<Result<Evidence, AnalysisError>> {
const { executable: command, arguments: args } = profile;
if (signal?.aborted) return err(new AnalysisCancelledError(operation));
if (signal?.aborted)
return err(
new AnalysisCancelledError(operation, { cause: signal.reason }),
);
const cleanup = await this.retryCleanup(operation);
if (!cleanup.ok) return cleanup;
const absolutePath =
@@ -322,8 +327,12 @@ export class ReverseEngineeringService {
digest = await this.#digest(absolutePath, signal, this.#stat);
} catch (cause) {
const code = commandFailureCode(cause);
if (signal?.aborted)
return err(new AnalysisCancelledError(operation, { cause }));
if (preflightCancelled(signal, cause, code))
return err(
new AnalysisCancelledError(operation, {
cause: callerCancellationCause(signal, cause),
}),
);
if (code === "EACCES" || code === "EPERM") {
const deniedPath = absolutePath === undefined ? command : absolutePath;
return err(
@@ -464,9 +473,9 @@ export class ReverseEngineeringService {
code === "ENOENT" ||
code === "ERR_PROVIDER_LAUNCH")
) {
if (signal?.aborted || code === "ABORT_ERR")
if (commandCancelled(signal, cause, code))
earlyFailure = new AnalysisCancelledError(operation, {
cause,
cause: callerCancellationCause(signal, cause),
...ownedCleanupOptions(cause),
});
else if (code === "ETIMEDOUT")
@@ -520,9 +529,9 @@ export class ReverseEngineeringService {
: null,
signal: capture.signal,
};
if (signal?.aborted || code === "ABORT_ERR")
if (commandCancelled(signal, cause, code))
failure = new AnalysisCancelledError(operation, {
cause,
cause: callerCancellationCause(signal, cause),
capturedOutput,
...ownedCleanupOptions(cause),
});
@@ -605,6 +614,7 @@ export class ReverseEngineeringService {
}
if (signal?.aborted && failure === undefined) {
failure = new AnalysisCancelledError(operation, {
cause: signal.reason,
capturedOutput: {
stdout,
stderr,
@@ -972,6 +982,48 @@ const digestFile = async (
});
};
const PHYSICAL_FAILURE_CODES = new Set([
"EACCES",
"EPERM",
"ETIMEDOUT",
"ENOENT",
"ENOTDIR",
"EINVAL",
"ELOOP",
"ENAMETOOLONG",
"E2BIG",
"EBUSY",
"ERR_PROVIDER_LAUNCH",
]);
/**
* A late abort does not replace a recognized physical failure. Other rejections
* observed while the caller signal is aborted remain cancellation. A bare
* `ABORT_ERR` is not cancellation until the command boundary, which already
* used that code for an aborted provider process.
*/
const preflightCancelled = (
signal: AbortSignal | undefined,
cause: unknown,
code: string | number | undefined,
): boolean => {
if (cause instanceof AnalysisCancelledError) return true;
if (signal?.aborted !== true) return false;
if (cause === signal.reason) return true;
return typeof code !== "string" || !PHYSICAL_FAILURE_CODES.has(code);
};
const commandCancelled = (
signal: AbortSignal | undefined,
cause: unknown,
code: string | number | undefined,
): boolean => code === "ABORT_ERR" || preflightCancelled(signal, cause, code);
const callerCancellationCause = (
signal: AbortSignal | undefined,
cause: unknown,
): unknown => (signal?.aborted === true ? signal.reason : cause);
const commandFailureCode = (cause: unknown): string | number | undefined => {
if (typeof cause !== "object" || cause === null) return undefined;
const code: unknown = Reflect.get(cause, "code");
@@ -145,3 +145,25 @@ it("preserves completed observed evidence when cancellation arrives after provid
value: { normalized_result: value },
});
});
it("preserves the exact caller reason when inspection is already aborted", async () => {
const reason = Object.freeze({ requested: "stop" });
const controller = new AbortController();
controller.abort(reason);
let calls = 0;
const service = new SqliteDatabaseService({
inspect: () => {
calls += 1;
return Promise.reject(new Error("provider must not run"));
},
});
const result = await service.inspect(
{ path: "/selected.db" },
{ signal: controller.signal },
);
expect(result.ok).toBe(false);
if (result.ok) return;
expect(result.error._tag).toBe("AnalysisCancelledError");
expect(result.error.cause).toBe(reason);
expect(calls).toBe(0);
});
@@ -71,7 +71,11 @@ export class SqliteDatabaseService {
options?: ExecutionOptions,
): Promise<Result<Evidence, AnalysisError>> {
if (options?.signal?.aborted)
return err(new AnalysisCancelledError(OPERATION));
return err(
new AnalysisCancelledError(OPERATION, {
cause: options.signal.reason,
}),
);
const input = inspectSqliteDatabaseInputSchema.safeParse(rawInput);
if (!input.success)
return err(
@@ -101,3 +101,207 @@ it("bounds concurrent reverse-engineering provider calls", async () => {
await rm(directory, { recursive: true, force: true });
}
});
it("preserves the exact caller reason when objdump admission is already aborted", async () => {
const directory = await mkdtemp(join(tmpdir(), "rea-reverse-preabort-"));
const path = join(directory, "fixture.bin");
await writeFile(path, "fixture");
const reason = Object.freeze({ requested: "stop" });
const controller = new AbortController();
controller.abort(reason);
let runs = 0;
const service = new TestReverseEngineeringService({
run: async () => {
runs += 1;
return { stdout: "", stderr: "" };
},
});
try {
const result = await service.inspectWithObjdump(
{ path, operation: "symbols" },
{ signal: controller.signal },
);
expect(result.ok).toBe(false);
if (result.ok) return;
expect(result.error._tag).toBe("AnalysisCancelledError");
expect(result.error.cause).toBe(reason);
expect(runs).toBe(0);
} finally {
await service.close();
await rm(directory, { recursive: true, force: true });
}
});
it("preserves the caller reason when cancellation arrives after provider output", async () => {
const directory = await mkdtemp(join(tmpdir(), "rea-reverse-postabort-"));
const path = join(directory, "fixture.bin");
await writeFile(path, "fixture");
const reason = Object.freeze({ requested: "stop-after-output" });
const controller = new AbortController();
const service = new TestReverseEngineeringService({
run: async () => {
controller.abort(reason);
return { stdout: "symbols", stderr: "" };
},
});
try {
const result = await service.inspectWithObjdump(
{ path, operation: "symbols" },
{ signal: controller.signal },
);
expect(result.ok).toBe(false);
if (result.ok) return;
expect(result.error._tag).toBe("AnalysisCancelledError");
expect(result.error.cause).toBe(reason);
expect(result.error.partialObservation).toMatchObject({
raw_result: { stdout: "symbols" },
});
} finally {
await service.close();
await rm(directory, { recursive: true, force: true });
}
});
it("keeps a physical digest denial primary when the caller aborts during that failure", async () => {
const directory = await mkdtemp(join(tmpdir(), "rea-reverse-digest-deny-"));
const path = join(directory, "fixture.bin");
await writeFile(path, "fixture");
const reason = Object.freeze({ requested: "stop-during-denial" });
const controller = new AbortController();
const denied = Object.assign(new Error("permission denied"), {
code: "EACCES",
});
let runs = 0;
const service = new TestReverseEngineeringService({
digest: async () => {
controller.abort(reason);
throw denied;
},
run: async () => {
runs += 1;
return { stdout: "", stderr: "" };
},
});
try {
const result = await service.inspectWithObjdump(
{ path, operation: "symbols" },
{ signal: controller.signal },
);
expect(result).toMatchObject({
ok: false,
error: { _tag: "AnalysisAccessDeniedError" },
});
if (result.ok) return;
expect(result.error.cause).toBe(denied);
expect(runs).toBe(0);
} finally {
await service.close();
await rm(directory, { recursive: true, force: true });
}
});
it("keeps an unrelated digest ABORT_ERR as an adapter failure", async () => {
const directory = await mkdtemp(join(tmpdir(), "rea-reverse-digest-abort-"));
const path = join(directory, "fixture.bin");
await writeFile(path, "fixture");
const denied = Object.assign(new Error("digest aborted elsewhere"), {
code: "ABORT_ERR",
});
const service = new TestReverseEngineeringService({
digest: async () => {
throw denied;
},
});
try {
const result = await service.inspectWithObjdump({
path,
operation: "symbols",
});
expect(result).toMatchObject({
ok: false,
error: { _tag: "ProviderAdapterError" },
});
if (result.ok) return;
expect(result.error.cause).toBe(denied);
} finally {
await service.close();
await rm(directory, { recursive: true, force: true });
}
});
it.each([
{
captured: true,
code: "EACCES",
tag: "AnalysisAccessDeniedError",
},
{
captured: true,
code: "ETIMEDOUT",
tag: "AnalysisTimeoutError",
},
{
captured: true,
code: "ABORT_ERR",
tag: "AnalysisCancelledError",
},
{
captured: false,
code: "EACCES",
tag: "AnalysisAccessDeniedError",
},
{
captured: false,
code: "ETIMEDOUT",
tag: "AnalysisTimeoutError",
},
{
captured: false,
code: "ABORT_ERR",
tag: "AnalysisCancelledError",
},
] as const)(
"classifies a $code command failure with captured output $captured while the caller is aborted",
async ({ captured, code, tag }) => {
const directory = await mkdtemp(join(tmpdir(), "rea-reverse-command-abort-"));
const path = join(directory, "fixture.bin");
await writeFile(path, "fixture");
const reason = Object.freeze({ requested: `stop-${code}` });
const controller = new AbortController();
const failure = Object.assign(new Error("provider failure"), {
code,
...(captured
? { stdout: "partial stdout", stderr: "partial stderr" }
: {}),
});
const service = new TestReverseEngineeringService({
run: async () => {
controller.abort(reason);
throw failure;
},
});
try {
const result = await service.inspectWithObjdump(
{ path, operation: "symbols" },
{ signal: controller.signal },
);
expect(result).toMatchObject({ ok: false, error: { _tag: tag } });
if (result.ok) return;
expect(result.error.cause).toBe(
code === "ABORT_ERR" ? reason : failure,
);
if (captured && code === "ABORT_ERR") {
expect(result.error.capturedOutput).toMatchObject({
stdout: "partial stdout",
stderr: "partial stderr",
});
expect(result.error.partialObservation).toMatchObject({
raw_result: { stdout: "partial stdout" },
});
}
} finally {
await service.close();
await rm(directory, { recursive: true, force: true });
}
},
);