fix(native): retain completed debugger evidence through reply cleanup

This commit is contained in:
morluto committed 2026-10-26 18:12:01 +08:00
1 parent f8161ad7b3
commit c06dc2d356
3 files changed
+387 -48

No files matched your search

+124 -35
View File
@@ -2,6 +2,7 @@ import {
bindArtifactFileCleanup,
artifactFileCancellationCause,
} from "../../artifacts/ArtifactFileOperation.js";
import { safeParseUtf8Json } from "../../domain/safeJson.js";
import { access, mkdir, stat, writeFile } from "node:fs/promises";
import { constants } from "node:fs";
import { randomUUID } from "node:crypto";
@@ -20,10 +21,15 @@ import {
recordedCrashDebuggerSchema,
type RecordedCrash,
} from "../../domain/native/recordedCrash.js";
import { readStableArtifact } from "../../artifacts/readStableArtifact.js";
import {
completedStableArtifact,
readStableArtifact,
} from "../../artifacts/readStableArtifact.js";
import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js";
import type { ArtifactResourceScope } from "../../artifacts/ArtifactResourceScope.js";
import { AnalysisError } from "../../domain/analysisErrorBase.js";
import { analysisErrorWithCleanupFailure } from "../../domain/analysisErrorCleanup.js";
import { ProviderCleanupError } from "../../domain/providerCleanupError.js";
import {
capturedPwntoolsOutput,
pwntoolsDecoderFailure,
@@ -48,6 +54,12 @@ const replySchema = z.discriminatedUnion("ok", [
}),
]);
/** Completed debugger observations stay separate from their reply descriptor's close. */
export interface PwndbgCoreInspection {
readonly context: Extract<RecordedCrash["debugger"], { status: "available" }>;
readonly finalization: AnalysisError | undefined;
}
/** Add mapping candidates from an owned core-only debugger, using the existing snapshot. */
export const inspectPwndbgCore = async ({
environment,
@@ -74,7 +86,7 @@ export const inspectPwndbgCore = async ({
kind: "file" | "directory",
mode: number,
) => Promise<void>;
}): Promise<RecordedCrash["debugger"]> => {
}): Promise<PwndbgCoreInspection> => {
const gdb = environment.REA_PWNDBG_GDB ?? "/usr/bin/gdb";
const entry = environment.REA_PWNDBG_GDBINIT ?? "";
const venv = environment.REA_PWNDBG_VENV_PATH ?? "";
@@ -189,15 +201,33 @@ export const inspectPwndbgCore = async ({
);
output = capturedPwntoolsOutput(execution);
let reply: z.output<typeof replySchema>;
let finalization: AnalysisError | undefined;
try {
const raw = await readStableArtifact(
replyPath,
PWNTOOLS_LIMITS.outputBytes,
options?.signal,
undefined,
resources,
);
reply = replySchema.parse(JSON.parse(raw.bytes.toString("utf8")));
let raw;
try {
raw = await readStableArtifact(
replyPath,
PWNTOOLS_LIMITS.outputBytes,
options?.signal,
undefined,
resources,
);
} catch (cause: unknown) {
const completed = completedStableArtifact(cause);
if (
!(cause instanceof ArtifactReaderFailure) ||
completed === undefined
)
throw cause;
finalization = completedReplyFailure(
cause,
recordedCrashStageOutput(diagnostics, output),
);
raw = completed;
}
const decoded = safeParseUtf8Json(raw.bytes);
if (!decoded.ok) throw decoded.cause;
reply = replySchema.parse(decoded.value);
} catch (cause: unknown) {
if (
cause instanceof ArtifactReaderFailure &&
@@ -212,7 +242,7 @@ export const inspectPwndbgCore = async ({
: {}),
});
bindArtifactFileCleanup(cause, error);
throw error;
throw pwndbgContextFinalizationFailure(error, finalization);
}
const error = new AnalysisOutputError(
OPERATION,
@@ -226,38 +256,52 @@ export const inspectPwndbgCore = async ({
},
);
bindArtifactFileCleanup(cause, error);
throw error;
throw pwndbgContextFinalizationFailure(error, finalization);
}
if (!reply.ok) {
if (reply.reason === "unavailable" || reply.reason === "unsupported")
throw new AnalysisCapabilityUnavailableError(
"pwndbg-core",
OPERATION,
reply.message,
{
userMessage: reply.message,
capturedOutput: recordedCrashStageOutput(diagnostics, output),
},
throw pwndbgContextFinalizationFailure(
new AnalysisCapabilityUnavailableError(
"pwndbg-core",
OPERATION,
reply.message,
{
userMessage: reply.message,
capturedOutput: recordedCrashStageOutput(diagnostics, output),
},
),
finalization,
);
if (reply.reason === "format" || reply.reason === "output-limit")
throw new AnalysisOutputError(OPERATION, reply.message, {
capturedOutput: recordedCrashStageOutput(diagnostics, output),
});
throw new ProviderAdapterError("pwndbg-core", OPERATION, {
diagnostics: {
stage: "debugger",
reason: reply.message,
captured_output: { ...recordedCrashStageOutput(diagnostics, output) },
},
});
throw pwndbgContextFinalizationFailure(
new AnalysisOutputError(OPERATION, reply.message, {
capturedOutput: recordedCrashStageOutput(diagnostics, output),
}),
finalization,
);
throw pwndbgContextFinalizationFailure(
new ProviderAdapterError("pwndbg-core", OPERATION, {
diagnostics: {
stage: "debugger",
reason: reply.message,
captured_output: {
...recordedCrashStageOutput(diagnostics, output),
},
},
}),
finalization,
);
}
if (reply.value.status !== "available")
throw new AnalysisOutputError(
OPERATION,
"Requested debugger context returned no core evidence.",
{ capturedOutput: recordedCrashStageOutput(diagnostics, output) },
throw pwndbgContextFinalizationFailure(
new AnalysisOutputError(
OPERATION,
"Requested debugger context returned no core evidence.",
{ capturedOutput: recordedCrashStageOutput(diagnostics, output) },
),
finalization,
);
return { ...reply.value, diagnostics: output };
return { context: { ...reply.value, diagnostics: output }, finalization };
} catch (cause: unknown) {
if (cause instanceof AnalysisError && cause.cleanupIncomplete) throw cause;
const primary = pwntoolsDecoderFailure(
@@ -272,6 +316,51 @@ export const inspectPwndbgCore = async ({
}
};
const completedReplyFailure = (
cause: ArtifactReaderFailure,
capturedOutput: AnalysisCapturedOutput,
): AnalysisError => {
const cancellation =
cause.reason === "cancelled"
? new AnalysisCancelledError(OPERATION, {
cause: artifactFileCancellationCause(cause),
capturedOutput,
})
: undefined;
if (cause.cleanup === undefined) {
if (cancellation !== undefined) return cancellation;
return new AnalysisOutputError(OPERATION, cause.message, {
cause,
capturedOutput,
});
}
const cleanup = new ProviderCleanupError(
"pwndbg-core",
cause.cleanup.resources,
{ stage: "debugger_reply_descriptor_close", reason: cause.cleanup.reason },
{ operation: OPERATION, cause, capturedOutput },
);
bindArtifactFileCleanup(cause, cleanup);
return cancellation === undefined
? cleanup
: pwndbgContextFinalizationFailure(cancellation, cleanup);
};
/** A rejected/malformed context stays primary over its descriptor finalization. */
export const pwndbgContextFinalizationFailure = (
primary: AnalysisError,
finalization: AnalysisError | undefined,
): AnalysisError =>
finalization === undefined
? primary
: finalization.cleanupIncomplete
? analysisErrorWithCleanupFailure(
primary,
finalization,
OPERATION,
).bindCleanupDependencies([primary, finalization])
: primary.retainSecondaryFailure(finalization);
/** Preserve both stages' exact text and their independently observed truncation state. */
export const recordedCrashStageOutput = (
basic: AnalysisCapturedOutput,
@@ -18,6 +18,7 @@ import { validateRecordedCrashSources } from "./RecordedCrashSourceBindings.js";
import { validateRecordedCrashStructure } from "./RecordedCrashStructureBindings.js";
import {
inspectPwndbgCore,
pwndbgContextFinalizationFailure,
recordedCrashStageOutput,
} from "../pwndbg/PwndbgCoreContext.js";
import type { z } from "zod";
@@ -133,7 +134,7 @@ export class PwntoolsRecordedCrashProvider
snapshot.bytes,
);
if (!authority.ok) throw authority.error;
const context = await inspectPwndbgCore({
const completed = await inspectPwndbgCore({
environment,
architecture: value.architecture,
rootPath,
@@ -143,20 +144,25 @@ export class PwntoolsRecordedCrashProvider
runCommand,
timeoutMs,
});
const context = completed.context;
const enriched = validate(context);
if (!enriched.success)
throw new AnalysisOutputError(
"inspect_recorded_crash",
`Recorded debugger context is invalid: ${enriched.error.issues[0]?.message ?? "schema mismatch"}`,
{
capturedOutput:
context.status === "available"
? recordedCrashStageOutput(
diagnostics,
context.diagnostics,
)
: diagnostics,
},
throw pwndbgContextFinalizationFailure(
new AnalysisOutputError(
"inspect_recorded_crash",
`Recorded debugger context is invalid: ${enriched.error.issues[0]?.message ?? "schema mismatch"}`,
{
capturedOutput: recordedCrashStageOutput(
diagnostics,
context.diagnostics,
),
},
),
completed.finalization,
);
if (completed.finalization !== undefined)
throw completed.finalization.retainPartialObservation(
enriched.data,
);
return enriched.data;
} catch (cause: unknown) {