diff --git a/scripts/verify/architecture-guard-baselines.json b/scripts/verify/architecture-guard-baselines.json index 85a9ae47a3..ba3a89ed4d 100644 --- a/scripts/verify/architecture-guard-baselines.json +++ b/scripts/verify/architecture-guard-baselines.json @@ -79,22 +79,8 @@ "src/application/releaseExposure/ReleaseExposureService.ts": 3, "src/application/reverse/ReverseEngineeringService.ts": 12, "src/application/wasm/WasmInterfaceComparisonService.ts": 2, - "src/gdb/GdbSessionManager.ts": 13, - "src/native/AppleDispatchMetadata.ts": 3, - "src/native/LldbCallTracer.ts": 2, - "src/native/NativeCallObservation.ts": 2, - "src/native/NativeCallTarget.ts": 2, - "src/native/NativeCommandAnalysisError.ts": 1, - "src/native/NativeMacOSProvider.ts": 8, - "src/native/NativePlistSource.ts": 2, - "src/native/NativeUiInterruptionControl.ts": 1, - "src/native/NativeUiObservation.ts": 1, - "src/native/pe/PeBinaryFailure.ts": 1, - "src/native/pwndbg/PwndbgCoreContext.ts": 3, - "src/native/pwntools/PwntoolsDecoder.ts": 8, - "src/native/pwntools/PwntoolsRecordedCrashProvider.ts": 1, - "src/process/ownedDebuggerSession.ts": 1, - "src/rizin/RizinDebugSessionManager.ts": 1 + "src/gdb/GdbSessionManager.ts": 1, + "src/process/ownedDebuggerSession.ts": 1 }, "parser-format-failure-class": { "src/browser/assets/WebArtifactReads.ts": 1, diff --git a/src/gdb/GdbSessionManager.ts b/src/gdb/GdbSessionManager.ts index c54e9e78c7..6aebd1e045 100644 --- a/src/gdb/GdbSessionManager.ts +++ b/src/gdb/GdbSessionManager.ts @@ -27,6 +27,7 @@ import { AnalysisTimeoutError, cancelledBy, OwnerCancellation, + carriedReason, } from "../domain/analysisErrorCore.js"; import { GDB_SESSION_TOOLS, @@ -137,8 +138,7 @@ const cancelledWithCallerReason = ( signal: AbortSignal, previous?: AnalysisCancelledError, ): AnalysisCancelledError => { - const cancellation = new AnalysisCancelledError(operation, { - cause: signal.reason, + const cancellation = cancelledBy(operation, signal, { cleanupPrimary: previous, ...(previous?.diagnostics === undefined ? {} @@ -550,12 +550,7 @@ export class GdbSessionManager { AnalysisError > > { - if (signal?.aborted) - return err( - new AnalysisCancelledError("start_gdb_session", { - cause: signal.reason, - }), - ); + if (signal?.aborted) return err(cancelledBy("start_gdb_session", signal)); const occupied = this.#sessions.occupied(); if (occupied >= this.#maxSessions.value) return err( @@ -639,7 +634,7 @@ export class GdbSessionManager { (signal?.aborted && cause === signal.reason) || cause instanceof AnalysisCancelledError ) - return err(new AnalysisCancelledError("start_gdb_session", { cause })); + return err(cancelledBy("start_gdb_session", carriedReason(cause))); const refusal = classifyLaunchFailure(cause); if (refusal !== undefined && cause instanceof Error) return err( @@ -743,11 +738,7 @@ export class GdbSessionManager { session.startupResolve = resolve; session.startupReject = reject; startupAbortListener = () => - reject( - new AnalysisCancelledError("start_gdb_session", { - cause: signal?.reason, - }), - ); + reject(cancelledBy("start_gdb_session", signal)); signal?.addEventListener("abort", startupAbortListener, { once: true, }); @@ -773,10 +764,7 @@ export class GdbSessionManager { ); startupTimer.unref(); }); - if (signal?.aborted) - throw new AnalysisCancelledError("start_gdb_session", { - cause: signal.reason, - }); + if (signal?.aborted) throw cancelledBy("start_gdb_session", signal); // -gdb-version loads nothing, so it may precede the auto-load setting. const reported = await this.#request( session, @@ -847,16 +835,10 @@ export class GdbSessionManager { ); session.inferiorTtyVerified = true; } - if (signal?.aborted) - throw new AnalysisCancelledError("start_gdb_session", { - cause: signal.reason, - }); + if (signal?.aborted) throw cancelledBy("start_gdb_session", signal); // Give queued exit notifications a turn before publishing startup. await new Promise((resolve) => setImmediate(resolve)); - if (signal?.aborted) - throw new AnalysisCancelledError("start_gdb_session", { - cause: signal.reason, - }); + if (signal?.aborted) throw cancelledBy("start_gdb_session", signal); if (session.exited || session.protocolError !== undefined) throw new Error( session.protocolError ?? "GDB exited during startup readiness.", @@ -965,14 +947,14 @@ export class GdbSessionManager { cleanupResources: [session.id], }); const cleanup = { reason: stopped.reason, resources: [session.id] }; - const details = { - cause, + const detailsWithoutCause = { capturedOutput, cleanup, ...(startupEvidence === undefined ? {} : { partialObservation: startupEvidence }), }; + const details = { cause, ...detailsWithoutCause }; if (cause instanceof AnalysisTimeoutError) return err( retainCleanup( @@ -984,7 +966,13 @@ export class GdbSessionManager { ); if (cause instanceof AnalysisCancelledError) return err( - retainCleanup(new AnalysisCancelledError(cause.operation, details)), + retainCleanup( + cancelledBy( + cause.operation, + carriedReason(cause), + detailsWithoutCause, + ), + ), ); const startupFailureMessage = gdbErrorMessage(cause); return err( @@ -998,16 +986,20 @@ export class GdbSessionManager { ), ); } - const errorDetails = { - cause, + const errorDetailsWithoutCause = { capturedOutput, ...(startupEvidence === undefined ? {} : { partialObservation: startupEvidence }), }; + const errorDetails = { cause, ...errorDetailsWithoutCause }; const error = cause instanceof AnalysisCancelledError - ? new AnalysisCancelledError("start_gdb_session", errorDetails) + ? cancelledBy( + "start_gdb_session", + carriedReason(cause), + errorDetailsWithoutCause, + ) : cause instanceof AnalysisTimeoutError ? new AnalysisTimeoutError( cause instanceof GdbCommandTimeoutError @@ -1224,7 +1216,7 @@ export class GdbSessionManager { const error = cause instanceof AnalysisError ? cause - : new AnalysisCancelledError("gdb_console", { cause }); + : cancelledBy("gdb_console", carriedReason(cause)); if (error.cleanupIncomplete) this.#sessions.retain(session, error); return err(error); } @@ -2060,8 +2052,7 @@ export class GdbSessionManager { void this.#stopDebuggerOnly(session).then((stopped) => { if (stopped.status !== "incomplete") { pending.reject( - new AnalysisCancelledError("gdb_console", { - cause: signal?.reason, + cancelledBy("gdb_console", carriedReason(signal?.reason), { partialObservation: evidence, }), ); @@ -2078,8 +2069,7 @@ export class GdbSessionManager { { cause: cancellation }, ); pending.reject( - new AnalysisCancelledError("gdb_console", { - cause: signal?.reason, + cancelledBy("gdb_console", carriedReason(signal?.reason), { cleanup: { reason: stopped.reason, resources: [session.id], diff --git a/src/native/AppleDispatchMetadata.ts b/src/native/AppleDispatchMetadata.ts index 08d48a1965..ec7b29520a 100644 --- a/src/native/AppleDispatchMetadata.ts +++ b/src/native/AppleDispatchMetadata.ts @@ -18,12 +18,13 @@ import { type BinaryTarget, } from "../domain/binaryTargetTypes.js"; import { - AnalysisCancelledError, AnalysisArtifactChangedError, AnalysisAccessDeniedError, AnalysisCapabilityUnavailableError, AnalysisResourceConstraintError, AnalysisUnsupportedTargetError, + cancelledBy, + carriedReason, } from "../domain/analysisErrorCore.js"; import { ArtifactFileCleanupFailure, @@ -369,10 +370,8 @@ export const inspectAppleDispatchMetadata = async ( } : { maximum_bytes: options.maxInputBytes, source: "caller" as const }; const maximumBytes = admission.maximum_bytes; - const cancelled = () => - new AnalysisCancelledError(OPERATION, { - cause: signal?.reason, - }); + // Called only after the caller's signal aborted. + const cancelled = () => cancelledBy(OPERATION, carriedReason(signal?.reason)); const { bytes, digest } = await withArtifactRegularFile( target.path, resources, @@ -438,9 +437,10 @@ export const appleDispatchMetadataFailure = ( ) : physical?.completionCancellation === undefined ? undefined - : new AnalysisCancelledError(OPERATION, { - cause: physical.completionCancellation.reason, - }); + : cancelledBy( + OPERATION, + carriedReason(physical.completionCancellation.reason), + ); const failure = new ProviderCleanupError( IDENTITY.id, cause.cleanup.resources, @@ -487,9 +487,10 @@ const appleDispatchMetadataPrimaryFailure = ( { cause }, ); if (cause instanceof ArtifactReaderFailure && cause.reason === "cancelled") - return new AnalysisCancelledError(OPERATION, { - cause: artifactFileCancellationCause(cause), - }); + return cancelledBy( + OPERATION, + carriedReason(artifactFileCancellationCause(cause)), + ); if ( cause instanceof ArtifactReaderFailure && cause.reason === "unavailable" diff --git a/src/native/LldbCallTracer.ts b/src/native/LldbCallTracer.ts index 308635c17c..e0f41e2e58 100644 --- a/src/native/LldbCallTracer.ts +++ b/src/native/LldbCallTracer.ts @@ -15,10 +15,11 @@ import { } from "../domain/native/nativeCallCaptureLimits.js"; import { - AnalysisCancelledError, AnalysisCapabilityUnavailableError, AnalysisResourceConstraintError, AnalysisTimeoutError, + cancelledBy, + carriedReason, } from "../domain/analysisErrorCore.js"; import { EvidenceIntegrityError } from "../domain/evidenceErrors.js"; import type { @@ -788,15 +789,13 @@ export class LldbCallTracer implements NativeCallTracer { ); }; if (exited.kind === "cancelled") - return failureWithPartial( - (partialObservation) => - new AnalysisCancelledError(OPERATION, { - cause: signal?.reason, - ...(lifecycleCleanup === undefined - ? {} - : { cleanup: lifecycleCleanup }), - partialObservation, - }), + return failureWithPartial((partialObservation) => + cancelledBy(OPERATION, carriedReason(signal?.reason), { + ...(lifecycleCleanup === undefined + ? {} + : { cleanup: lifecycleCleanup }), + partialObservation, + }), ); if (exited.kind === "timeout") return failureWithPartial( @@ -1287,8 +1286,7 @@ const lldbPreparationFailure = ( ): AnalysisError => { const marker = cause.cleanupFailure; const compiler = cause.compilerOutcome; - const options = { - cause, + const optionsWithoutCause = { ...(cause.capturedOutput === undefined ? {} : { capturedOutput: cause.capturedOutput }), @@ -1318,13 +1316,15 @@ const lldbPreparationFailure = ( }), }, }; + const options = { cause, ...optionsWithoutCause }; const error = cause.cancellation === undefined ? new ProviderAdapterError(PROVIDER, OPERATION, options) - : new AnalysisCancelledError(OPERATION, { - ...options, - cause: cause.cancellation.reason, - }); + : cancelledBy( + OPERATION, + carriedReason(cause.cancellation.reason), + optionsWithoutCause, + ); return marker === undefined ? error : error.bindCleanupDependencies([marker]); }; diff --git a/src/native/NativeCallObservation.ts b/src/native/NativeCallObservation.ts index f10c5dd7af..1eb1081cab 100644 --- a/src/native/NativeCallObservation.ts +++ b/src/native/NativeCallObservation.ts @@ -1,8 +1,8 @@ import { nativeCallCaptureLimits } from "../domain/native/nativeCallCaptureLimits.js"; import { AnalysisCapabilityUnavailableError, - AnalysisCancelledError, AnalysisInputError, + cancelledBy, } from "../domain/analysisErrorCore.js"; import type { AnalysisError } from "../domain/analysisErrorBase.js"; import { @@ -70,10 +70,7 @@ export const observeNativeCalls = async ( return await resources.run(async () => { const selected = await verifyNativeCallTarget(target, resources, signal); if (!selected.ok) return selected; - if (signal?.aborted) - return err( - new AnalysisCancelledError(OPERATION, { cause: signal.reason }), - ); + if (signal?.aborted) return err(cancelledBy(OPERATION, signal)); try { const traced = await tracer.trace( { @@ -87,8 +84,7 @@ export const observeNativeCalls = async ( if (!traced.ok) return traced; return signal?.aborted ? err( - new AnalysisCancelledError(OPERATION, { - cause: signal.reason, + cancelledBy(OPERATION, signal, { partialObservation: projectPartialNativeCalls( target, parsed.data, diff --git a/src/native/NativeCallTarget.ts b/src/native/NativeCallTarget.ts index 9f85c0731f..70078a0f25 100644 --- a/src/native/NativeCallTarget.ts +++ b/src/native/NativeCallTarget.ts @@ -1,7 +1,7 @@ import { NATIVE_MACOS_PROVIDER_IDENTITY as IDENTITY } from "./NativeMacosProviderIdentity.js"; import type { BinaryTarget } from "../domain/binaryTargetTypes.js"; import type { AnalysisError } from "../domain/analysisErrorBase.js"; -import { AnalysisCancelledError } from "../domain/analysisErrorCore.js"; +import { cancelledBy, carriedReason } from "../domain/analysisErrorCore.js"; import { EvidenceIntegrityError } from "../domain/evidenceErrors.js"; import { ProviderAdapterError } from "../domain/providerAdapterError.js"; import { ProviderCleanupError } from "../domain/providerCleanupError.js"; @@ -54,16 +54,17 @@ export const nativeCallTargetFailure = ( cause instanceof RegularFileOperationCancelled || cause instanceof RegularFileCancelled ) - return new AnalysisCancelledError(OPERATION, { - cause: cause.reason, - ...(cause instanceof RegularFileCancelled + return cancelledBy( + OPERATION, + carriedReason(cause.reason), + cause instanceof RegularFileCancelled ? { diagnostics: { completed_hash: jsonValueSchema.parse(cause.outcome.value), }, } - : {}), - }); + : {}, + ); const physical = stableFileHashCause(cause); if (physical instanceof RegularFileChangedError) return new EvidenceIntegrityError( @@ -125,8 +126,7 @@ export const verifyNativeCallTarget = async ( ? nativeCallTargetFailure(cause.outcome.cause, target.path) : cancellation === undefined ? undefined - : new AnalysisCancelledError(OPERATION, { - cause: cancellation.reason, + : cancelledBy(OPERATION, carriedReason(cancellation.reason), { diagnostics: completed, }); const framed = new ArtifactFileCleanupFailure(cause, cleanupLease); diff --git a/src/native/NativeCommandAnalysisError.ts b/src/native/NativeCommandAnalysisError.ts index e6cd374b39..76ba696993 100644 --- a/src/native/NativeCommandAnalysisError.ts +++ b/src/native/NativeCommandAnalysisError.ts @@ -10,10 +10,11 @@ import { } from "../providers/OwnedCommandCleanup.js"; import type { AnalysisError } from "../domain/analysisErrorBase.js"; import { - AnalysisCancelledError, AnalysisCapabilityUnavailableError, AnalysisResourceConstraintError, AnalysisTimeoutError, + cancelledBy, + carriedReason, } from "../domain/analysisErrorCore.js"; import { ProviderAdapterError } from "../domain/providerAdapterError.js"; import { NATIVE_MACOS_PROVIDER_IDENTITY as IDENTITY } from "./NativeMacosProviderIdentity.js"; @@ -163,11 +164,11 @@ export const translateNativeCommandFailure = ( reason: failure.cleanupFailure, resources: [...failure.cleanupResources], }; - const errorOptions = { - cause: failure, + const errorOptionsWithoutCause = { ...(capturedOutput === undefined ? {} : { capturedOutput }), ...(cleanup === undefined ? {} : { cleanup }), }; + const errorOptions = { cause: failure, ...errorOptionsWithoutCause }; if (failure.reason === "unavailable") { const reason = presentation.unavailableMessage ?? @@ -181,12 +182,17 @@ export const translateNativeCommandFailure = ( } if (failure.reason === "cancelled") return bindCleanup( - new AnalysisCancelledError(operation, { - ...errorOptions, - ...(failure.cancellation === undefined - ? {} - : { cause: failure.cancellation.reason }), - }), + cancelledBy( + operation, + carriedReason( + failure.cancellation === undefined + ? failure + : failure.cancellation.reason, + ), + { + ...errorOptionsWithoutCause, + }, + ), ); if (failure.reason === "timeout") return bindCleanup( diff --git a/src/native/NativeMacOSProvider.ts b/src/native/NativeMacOSProvider.ts index ba59d4c80a..e3032b0c43 100644 --- a/src/native/NativeMacOSProvider.ts +++ b/src/native/NativeMacOSProvider.ts @@ -76,6 +76,8 @@ import { AnalysisOutputError, AnalysisUnsupportedTargetError, AnalysisResourceConstraintError, + cancelledBy, + carriedReason, } from "../domain/analysisErrorCore.js"; import { DEFAULT_NATIVE_COMMAND_TIMEOUT_MS, @@ -287,9 +289,7 @@ class NativeMacOSClient implements AnalysisClient { ), ); if (options?.signal?.aborted === true) - return err( - new AnalysisCancelledError(operation, { cause: options.signal.reason }), - ); + return err(cancelledBy(operation, options.signal)); if (operation === "health") return ok(createAnalysisExecution(null, IDENTITY)); if (this.activeTarget === undefined && operation !== "inspect_plist") @@ -1487,12 +1487,7 @@ class NativeMacOSClient implements AnalysisClient { ): Promise> { try { return await this.#machoOperandResources.run(async () => { - if (signal?.aborted) - return err( - new AnalysisCancelledError("inspect_macho", { - cause: signal.reason, - }), - ); + if (signal?.aborted) return err(cancelledBy("inspect_macho", signal)); let root: Pick; try { root = await (this.privateRuntimeRootFactory?.() ?? @@ -1524,11 +1519,7 @@ class NativeMacOSClient implements AnalysisClient { const operand = join(root.path, "artifact.macho"); await symlink(this.target.path, operand); result = signal?.aborted - ? err( - new AnalysisCancelledError("inspect_macho", { - cause: signal.reason, - }), - ) + ? err(cancelledBy("inspect_macho", signal)) : await inspect(operand); } catch (cause: unknown) { result = err( @@ -1558,8 +1549,7 @@ class NativeMacOSClient implements AnalysisClient { new AnalysisCleanupLeaseGroup(cleanup, [released.lease]); const primary = result.ok ? signal?.aborted - ? new AnalysisCancelledError("inspect_macho", { - cause: signal.reason, + ? cancelledBy("inspect_macho", signal, { partialObservation: inspectMachoSchema.parse( result.value.result, ), @@ -1578,8 +1568,7 @@ class NativeMacOSClient implements AnalysisClient { } if (signal?.aborted && result.ok) return err( - new AnalysisCancelledError("inspect_macho", { - cause: signal.reason, + cancelledBy("inspect_macho", signal, { partialObservation: inspectMachoSchema.parse(result.value.result), }), ); @@ -1765,9 +1754,8 @@ const nativeMachoSourcePrimary = ( : undefined; const completedFailure = nativeMachoCompletedFailure(completed); const previous = failed ?? completedFailure; - const options = { - cause, - ...(previous === undefined + const optionsWithoutCause = + previous === undefined ? {} : { diagnostics: { @@ -1775,22 +1763,24 @@ const nativeMachoSourcePrimary = ( projectAnalysisError(previous), ), }, - }), - }; + }; + const options = { cause, ...optionsWithoutCause }; if (cause instanceof ArtifactReaderFailure && cause.reason === "cancelled") { // A returned failure is already the producer's physical outcome. A later // cancelled identity check must not replace its output or cleanup owner. if (completedFailure !== undefined) return analysisErrorWithCancellation( completedFailure, - new AnalysisCancelledError(operation, { - cause: artifactFileCancellationCause(cause), - }), + cancelledBy( + operation, + carriedReason(artifactFileCancellationCause(cause)), + ), ); - return new AnalysisCancelledError(operation, { - ...options, - cause: artifactFileCancellationCause(cause), - }); + return cancelledBy( + operation, + carriedReason(artifactFileCancellationCause(cause)), + optionsWithoutCause, + ); } if (cause instanceof ArtifactReaderFailure && cause.reason === "format") return new AnalysisOutputError(operation, cause.message, options); @@ -1841,12 +1831,13 @@ const nativeMachoCompletion = ( const completionCancellation = completion?.cancellation === undefined ? undefined - : new AnalysisCancelledError(operation, { - cause: completion.cancellation.reason, - ...(completedFailure === undefined || - completedFailure.cleanupIncomplete || - (cause instanceof ArtifactReaderFailure && - cause.cleanup !== undefined) + : cancelledBy( + operation, + carriedReason(completion.cancellation.reason), + completedFailure === undefined || + completedFailure.cleanupIncomplete || + (cause instanceof ArtifactReaderFailure && + cause.cleanup !== undefined) ? {} : { diagnostics: { @@ -1854,8 +1845,8 @@ const nativeMachoCompletion = ( projectAnalysisError(completedFailure), ), }, - }), - }); + }, + ); return { observation, completedFailure, completionCancellation }; }; diff --git a/src/native/NativePlistSource.ts b/src/native/NativePlistSource.ts index 83f0a1ab7a..d76efc68a7 100644 --- a/src/native/NativePlistSource.ts +++ b/src/native/NativePlistSource.ts @@ -13,10 +13,11 @@ import type { } from "../domain/analysisErrorBase.js"; import { AnalysisArtifactChangedError, - AnalysisCancelledError, AnalysisCapabilityUnavailableError, AnalysisInputError, AnalysisResourceConstraintError, + cancelledBy, + carriedReason, } from "../domain/analysisErrorCore.js"; import { BinaryTargetError } from "../domain/configurationErrors.js"; import { ProviderAdapterError } from "../domain/providerAdapterError.js"; @@ -155,9 +156,10 @@ export const nativePlistFailure = ( ? nativePlistPrimaryFailure(artifactFilePrimaryFacts(cause), selection) : physical?.completionCancellation === undefined ? undefined - : new AnalysisCancelledError("inspect_plist", { - cause: physical.completionCancellation.reason, - }); + : cancelledBy( + "inspect_plist", + carriedReason(physical.completionCancellation.reason), + ); const previousProjection = previous === undefined ? null : projectAnalysisError(previous); const failure = new ProviderCleanupError( @@ -185,9 +187,10 @@ const nativePlistPrimaryFailure = ( ): AnalysisError => { if (origin === "owned-or-classified") { if (cause instanceof ArtifactReaderFailure && cause.reason === "cancelled") - return new AnalysisCancelledError("inspect_plist", { - cause: artifactFileCancellationCause(cause), - }); + return cancelledBy( + "inspect_plist", + carriedReason(artifactFileCancellationCause(cause)), + ); if ( cause instanceof ArtifactReaderFailure && cause.reason === "unavailable" diff --git a/src/native/NativeUiInterruptionControl.ts b/src/native/NativeUiInterruptionControl.ts index 9d94fbec00..113413c935 100644 --- a/src/native/NativeUiInterruptionControl.ts +++ b/src/native/NativeUiInterruptionControl.ts @@ -2,6 +2,8 @@ import type { AnalysisPartialObservation } from "../domain/analysisErrorBase.js" import { AnalysisCancelledError, AnalysisTimeoutError, + cancelledBy, + carriedReason, } from "../domain/analysisErrorCore.js"; import { DEFAULT_NATIVE_UI_DEADLINE_MS, @@ -90,8 +92,7 @@ export class NativeUiInterruptionControl { "The 67108864-byte scenario output allowance and the 180000 ms cap on one wait step have no runtime override.", }), }) - : new AnalysisCancelledError(operation, { - cause: interruption.reason, + : cancelledBy(operation, carriedReason(interruption.reason), { partialObservation, }); } diff --git a/src/native/NativeUiObservation.ts b/src/native/NativeUiObservation.ts index 0457c100f2..0924e63c07 100644 --- a/src/native/NativeUiObservation.ts +++ b/src/native/NativeUiObservation.ts @@ -16,9 +16,10 @@ import { nativeUiResultSchema, } from "../domain/native/nativeUiObservation.js"; import { - AnalysisCancelledError, AnalysisCapabilityUnavailableError, AnalysisInputError, + cancelledBy, + carriedReason, } from "../domain/analysisErrorCore.js"; import { ProviderAdapterError } from "../domain/providerAdapterError.js"; import { AnalysisError } from "../domain/analysisErrorBase.js"; @@ -278,7 +279,7 @@ const observeWithHelper = async ( ? ((signal.aborted && cause.cause === signal.reason ? interruption.error(operation) : undefined) ?? - new AnalysisCancelledError(operation, { cause: cause.cause })) + cancelledBy(operation, carriedReason(cause.cause))) : new ProviderAdapterError("native-macos", operation, { cause, diagnostics: { diff --git a/src/native/pe/PeBinaryFailure.ts b/src/native/pe/PeBinaryFailure.ts index 1d6706e304..58e2268de1 100644 --- a/src/native/pe/PeBinaryFailure.ts +++ b/src/native/pe/PeBinaryFailure.ts @@ -14,9 +14,10 @@ import { import { AnalysisError } from "../../domain/analysisErrorBase.js"; import { AnalysisArtifactChangedError, - AnalysisCancelledError, AnalysisResourceConstraintError, type ResourceConstraintRecovery, + cancelledBy, + carriedReason, } from "../../domain/analysisErrorCore.js"; import { ProviderAdapterError } from "../../domain/providerAdapterError.js"; import { BinaryResourceFailure } from "../metadata/BinaryContainer.js"; @@ -58,9 +59,10 @@ const peBinaryFailurePrimary = ( cause instanceof ArtifactReaderFailure && cause.reason === "cancelled" ) - return new AnalysisCancelledError(operation, { - cause: artifactFileCancellationCause(cause), - }); + return cancelledBy( + operation, + carriedReason(artifactFileCancellationCause(cause)), + ); if (cause instanceof BinaryResourceFailure) { const limit = options.limit(cause); return new AnalysisResourceConstraintError( diff --git a/src/native/pwndbg/PwndbgCoreContext.ts b/src/native/pwndbg/PwndbgCoreContext.ts index 9843657f4d..ec0bfd91c7 100644 --- a/src/native/pwndbg/PwndbgCoreContext.ts +++ b/src/native/pwndbg/PwndbgCoreContext.ts @@ -16,6 +16,8 @@ import { AnalysisCancelledError, AnalysisCapabilityUnavailableError, AnalysisOutputError, + cancelledBy, + carriedReason, } from "../../domain/analysisErrorCore.js"; import { ProviderAdapterError } from "../../domain/providerAdapterError.js"; import { @@ -99,8 +101,7 @@ export const inspectPwndbgCore = async ({ }); const checkCancelled = () => { if (options?.signal?.aborted) - throw new AnalysisCancelledError(OPERATION, { - cause: options.signal.reason, + throw cancelledBy(OPERATION, options.signal, { capturedOutput: diagnostics, }); }; @@ -232,14 +233,17 @@ export const inspectPwndbgCore = async ({ cause instanceof ArtifactReaderFailure && cause.reason === "cancelled" ) { - const error = new AnalysisCancelledError(OPERATION, { - cause: artifactFileCancellationCause(cause), - capturedOutput: recordedCrashStageOutput(diagnostics, output), - ...(cause instanceof ArtifactReaderFailure && - cause.cleanup !== undefined - ? { cleanup: cause.cleanup } - : {}), - }); + const error = cancelledBy( + OPERATION, + carriedReason(artifactFileCancellationCause(cause)), + { + capturedOutput: recordedCrashStageOutput(diagnostics, output), + ...(cause instanceof ArtifactReaderFailure && + cause.cleanup !== undefined + ? { cleanup: cause.cleanup } + : {}), + }, + ); bindArtifactFileCleanup(cause, error); throw pwndbgContextFinalizationFailure(error, finalization); } @@ -334,10 +338,11 @@ const completedReplyFailure = ( ): AnalysisError => { const cancellation = cause.reason === "cancelled" - ? new AnalysisCancelledError(OPERATION, { - cause: artifactFileCancellationCause(cause), - capturedOutput, - }) + ? cancelledBy( + OPERATION, + carriedReason(artifactFileCancellationCause(cause)), + { capturedOutput }, + ) : undefined; if (cause.cleanup === undefined) { if (cancellation !== undefined) return cancellation; diff --git a/src/native/pwntools/PwntoolsDecoder.ts b/src/native/pwntools/PwntoolsDecoder.ts index bf0cf2762b..7435a3a4b4 100644 --- a/src/native/pwntools/PwntoolsDecoder.ts +++ b/src/native/pwntools/PwntoolsDecoder.ts @@ -37,12 +37,13 @@ import { ArtifactResourceScope } from "../../artifacts/ArtifactResourceScope.js" import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js"; import { AnalysisError } from "../../domain/analysisErrorBase.js"; import { - AnalysisCancelledError, AnalysisCapabilityUnavailableError, AnalysisUnsupportedTargetError, AnalysisInputError, AnalysisOutputError, AnalysisResourceConstraintError, + cancelledBy, + carriedReason, } from "../../domain/analysisErrorCore.js"; import { projectAnalysisError, @@ -214,11 +215,7 @@ export class PwntoolsDecoder< ): Promise> { if (options?.signal?.aborted) return Promise.resolve( - err( - new AnalysisCancelledError(this.profile.operation, { - cause: options?.signal?.reason, - }), - ), + err(cancelledBy(this.profile.operation, options?.signal)), ); if (this.#closing) return Promise.resolve(err(this.#closedError())); const key = Symbol(); @@ -229,11 +226,7 @@ export class PwntoolsDecoder< const operation = this.#tail.then(() => { this.#admissions.settle(key, "admitted"); if (options?.signal?.aborted) - return err( - new AnalysisCancelledError(this.profile.operation, { - cause: options?.signal?.reason, - }), - ); + return err(cancelledBy(this.profile.operation, options?.signal)); return this.#closing ? err(this.#closedError()) : inspect(); }); this.#tail = operation.then( @@ -243,9 +236,10 @@ export class PwntoolsDecoder< return admission.then((state) => state === "cancelled" ? err( - new AnalysisCancelledError(this.profile.operation, { - cause: options?.signal?.reason, - }), + cancelledBy( + this.profile.operation, + carriedReason(options?.signal?.reason), + ), ) : operation, ); @@ -337,9 +331,7 @@ export class PwntoolsDecoder< ); try { if (options?.signal?.aborted) - throw new AnalysisCancelledError(OPERATION, { - cause: options.signal.reason, - }); + throw cancelledBy(OPERATION, options.signal); if (!pwntoolsHostSupported(process.platform, process.arch)) { const requirement = pwntoolsHostRequirement( process.platform, @@ -430,9 +422,7 @@ export class PwntoolsDecoder< cause.code === "ABORT_ERR" && cause.cause === options.signal.reason ) - throw new AnalysisCancelledError(OPERATION, { - cause: options.signal.reason, - }); + throw cancelledBy(OPERATION, options.signal); throw cause; } await writeFile( @@ -501,10 +491,11 @@ export class PwntoolsDecoder< if (cleanup !== undefined) bindArtifactFileCleanup(cause, cleanup); const primary = cause.reason === "cancelled" - ? new AnalysisCancelledError(OPERATION, { - cause: artifactFileCancellationCause(cause), - capturedOutput, - }) + ? cancelledBy( + OPERATION, + carriedReason(artifactFileCancellationCause(cause)), + { capturedOutput }, + ) : (cleanup ?? new AnalysisOutputError(OPERATION, cause.message, { cause, @@ -527,14 +518,17 @@ export class PwntoolsDecoder< cause instanceof ArtifactReaderFailure && cause.reason === "cancelled" ) { - const error = new AnalysisCancelledError(OPERATION, { - cause: artifactFileCancellationCause(cause), - capturedOutput, - ...(cause instanceof ArtifactReaderFailure && - cause.cleanup !== undefined - ? { cleanup: cause.cleanup } - : {}), - }); + const error = cancelledBy( + OPERATION, + carriedReason(artifactFileCancellationCause(cause)), + { + capturedOutput, + ...(cause instanceof ArtifactReaderFailure && + cause.cleanup !== undefined + ? { cleanup: cause.cleanup } + : {}), + }, + ); bindArtifactFileCleanup(cause, error); throw error; } @@ -706,8 +700,7 @@ export class PwntoolsDecoder< const cleanup = await this.#completeCleanup(() => this.#retire()); if (result.ok && options?.signal?.aborted) result = err( - new AnalysisCancelledError(OPERATION, { - cause: options.signal.reason, + cancelledBy(OPERATION, options.signal, { partialObservation: result.value, capturedOutput: result.value.diagnostics, }), diff --git a/src/native/pwntools/PwntoolsRecordedCrashProvider.ts b/src/native/pwntools/PwntoolsRecordedCrashProvider.ts index c11eaea4b3..fc6b3ca835 100644 --- a/src/native/pwntools/PwntoolsRecordedCrashProvider.ts +++ b/src/native/pwntools/PwntoolsRecordedCrashProvider.ts @@ -1,6 +1,6 @@ import { - AnalysisCancelledError, AnalysisOutputError, + cancelledBy, } from "../../domain/analysisErrorCore.js"; import { AnalysisError } from "../../domain/analysisErrorBase.js"; import { @@ -145,8 +145,7 @@ export class PwntoolsRecordedCrashProvider }); try { if (options?.signal?.aborted) - throw new AnalysisCancelledError("inspect_recorded_crash", { - cause: options.signal.reason, + throw cancelledBy("inspect_recorded_crash", options.signal, { capturedOutput: diagnostics, }); const authority = validateCoreFileMappingAuthority( diff --git a/src/rizin/RizinDebugSessionManager.ts b/src/rizin/RizinDebugSessionManager.ts index dd630f1755..b6f4dd60b4 100644 --- a/src/rizin/RizinDebugSessionManager.ts +++ b/src/rizin/RizinDebugSessionManager.ts @@ -22,6 +22,10 @@ import { AnalysisCapabilityUnavailableError, AnalysisInputError, AnalysisTimeoutError, + cancelledBy, + carriedReason, + isAborted, + type CancellationReason, } from "../domain/analysisErrorCore.js"; import { RIZIN_DEBUG_SESSION_TOOLS, @@ -422,7 +426,7 @@ export class RizinDebugSessionManager { > > { if (signal?.aborted) - return err(rizinCancellation("start_rizin_debug_session", signal)); + return err(cancelledBy("start_rizin_debug_session", signal)); if (!persistentDebuggerHostSupported(this.#platform)) return err( new AnalysisCapabilityUnavailableError( @@ -506,10 +510,9 @@ export class RizinDebugSessionManager { }, ], ) - : rizinCancellation( + : cancelledBy( "start_rizin_debug_session", - undefined, - cancellation.reason, + carriedReason(cancellation.reason), ); if (!(cause instanceof RegularFileCleanupFailure)) return err(primary); const cleanup = new ProviderCleanupError( @@ -533,7 +536,7 @@ export class RizinDebugSessionManager { // TTY identity validation is asynchronous. Cancellation may arrive while // realpath/open/stat are pending, and must prevent the debugger spawn. if (signal?.aborted) - return err(rizinCancellation("start_rizin_debug_session", signal)); + return err(cancelledBy("start_rizin_debug_session", signal)); const id = randomUUID(); const resolvedPath = absoluteCallerPath(input.path); const launchProvenance: RizinLaunchProvenance = { @@ -570,7 +573,10 @@ export class RizinDebugSessionManager { } catch (cause) { if (signal?.aborted) return err( - rizinCancellation("start_rizin_debug_session", signal, cause), + cancelledBy( + "start_rizin_debug_session", + rizinCancellationReason(signal, cause), + ), ); return err( new AnalysisCapabilityUnavailableError( @@ -663,7 +669,7 @@ export class RizinDebugSessionManager { // initial frame run before publishing the session as ready. await new Promise((resolve) => setImmediate(resolve)); if (signal?.aborted) - throw rizinCancellation("start_rizin_debug_session", signal); + throw cancelledBy("start_rizin_debug_session", signal); if (session.exited) throw new RizinProcessExitError("Rizin exited during startup."); if (session.protocolError !== undefined) @@ -675,19 +681,19 @@ export class RizinDebugSessionManager { const protocolFailure = session.protocolError; const cancellation = signal?.aborted || caught instanceof AnalysisCancelledError - ? rizinCancellation("start_rizin_debug_session", signal, caught) + ? cancelledBy( + "start_rizin_debug_session", + rizinCancellationReason(signal, caught), + ) : undefined; const stopped = await this.#stopDebuggerOnly(session); const capturedOutput = this.#startupCapturedOutput(session); if (stopped.status !== "incomplete") this.#sessions.remove(session); let primary: AnalysisError; if (cancellation !== undefined) - primary = rizinCancellation( - "start_rizin_debug_session", - undefined, - cancellation, - { capturedOutput }, - ); + primary = cancelledBy("start_rizin_debug_session", cancellation, { + capturedOutput, + }); else if (protocolFailure !== undefined) primary = new AnalysisCapabilityUnavailableError( "rizin", @@ -754,8 +760,7 @@ export class RizinDebugSessionManager { stopped, ), ); - if (signal?.aborted) - return err(rizinCancellation("rizin_debug_command", signal)); + if (signal?.aborted) return err(cancelledBy("rizin_debug_command", signal)); if (session.pendingCommandCount >= this.#maxPendingCommands.value) return err( debuggerResourceConstraintError( @@ -807,7 +812,7 @@ export class RizinDebugSessionManager { await waitForTurn(previous, signal); acquired = true; if (signal?.aborted) - return err(rizinCancellation("rizin_debug_command", signal)); + return err(cancelledBy("rizin_debug_command", signal)); const stoppedWhileQueued = this.#stopReason(session); if (stoppedWhileQueued !== undefined) return err( @@ -877,8 +882,7 @@ export class RizinDebugSessionManager { throw interruption.cause; } const captured = received.value; - if (signal?.aborted) - throw rizinCancellation("rizin_debug_command", signal); + if (signal?.aborted) throw cancelledBy("rizin_debug_command", signal); let stopped: Awaited> = { status: "stopped", }; @@ -948,7 +952,12 @@ export class RizinDebugSessionManager { }); if (signal?.aborted || cause instanceof AnalysisCancelledError) { if (!commandSent) - return err(rizinCancellation("rizin_debug_command", signal, cause)); + return err( + cancelledBy( + "rizin_debug_command", + rizinCancellationReason(signal, cause), + ), + ); const evidence = partialEvidence([ "The Rizin command was cancelled; collected frame output is partial and command completion is unknown.", "The owned debugger session was stopped because command output can no longer be correlated safely.", @@ -956,10 +965,9 @@ export class RizinDebugSessionManager { ]); session.protocolError = "A Rizin command was cancelled; the owned session was stopped because command output can no longer be correlated safely."; - const primary = rizinCancellation( + const primary = cancelledBy( "rizin_debug_command", - signal, - cause, + rizinCancellationReason(signal, cause), { ...(capturedOutput === undefined ? {} : { capturedOutput }), ...(evidence === undefined ? {} : { partialObservation: evidence }), @@ -1600,10 +1608,9 @@ export class RizinDebugSessionManager { signal?: AbortSignal, ): Result { if (signal?.aborted) { - const primary = rizinCancellation( + const primary = cancelledBy( "rizin_debug_command", - signal, - undefined, + rizinCancellationReason(signal, undefined), { capturedOutput: { stdout: observation.result.output, @@ -1643,7 +1650,7 @@ export class RizinDebugSessionManager { signal?: AbortSignal, ): AnalysisError { if (signal?.aborted && !(primary instanceof AnalysisCancelledError)) - primary.retainSecondaryFailure(rizinCancellation(operation, signal), { + primary.retainSecondaryFailure(cancelledBy(operation, signal), { finalization_cancelled: true, }); if (stopped.status !== "incomplete") return primary; @@ -1956,7 +1963,7 @@ const waitForFrame = ( signal?: AbortSignal, ): Promise => { if (signal?.aborted) - return Promise.reject(rizinCancellation("rizin_debug_command", signal)); + return Promise.reject(cancelledBy("rizin_debug_command", signal)); const queued = session.frames.shift(); if (queued !== undefined) { session.queuedFrameBytes -= queued.queueChargeBytes; @@ -1973,7 +1980,7 @@ const waitForFrame = ( }; const onAbort = (): void => { cleanup(); - reject(rizinCancellation("rizin_debug_command", signal)); + reject(cancelledBy("rizin_debug_command", carriedReason(signal?.reason))); }; const timer = setTimeout(() => { cleanup(); @@ -2002,21 +2009,17 @@ const waitForTurn = ( if (signal === undefined) return previous; return waitForAbortable(previous, signal).then((outcome) => { if (outcome instanceof AbortedWait) - throw rizinCancellation("rizin_debug_command", signal); + throw cancelledBy("rizin_debug_command", outcome); }); }; /** Translate only this transport's typed cancellation, preserving opaque/null caller reasons. */ -const rizinCancellation = ( - operation: string, - signal?: AbortSignal, - cause?: unknown, - options: AnalysisErrorOptions = {}, -): AnalysisCancelledError => - new AnalysisCancelledError(operation, { - ...options, - cause: signal?.aborted - ? signal.reason - : cause instanceof AnalysisCancelledError - ? cause.cause - : cause, - }); +/** The caller's reason when it aborted, else the reason an earlier failure carried. */ +const rizinCancellationReason = ( + signal: AbortSignal | undefined, + cause: unknown, +): CancellationReason => + isAborted(signal) + ? signal + : cause instanceof AnalysisCancelledError + ? cause + : carriedReason(cause);