From 57f75bb723d43d3541bc0a3e68dbefb9c50a7a86 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Sun, 15 Nov 2026 18:07:27 +0800 Subject: [PATCH] refactor(native): build debugger and native cancellations with a required reason (#10599) * refactor(errors): require a reason for every analysis cancellation Add cancelledBy, the one constructor for AnalysisCancelledError. Its reason argument is required: an aborted signal, an earlier cancellation, a reason carried from an inner failure, or an OwnerCancellation when REA closed the work's owner itself. Closed adb, apktool, firmware, sqlite, gdb, web-bundle analyzer and binary-session record owners now report OwnerCancellation.ownerClosed instead of an absent reason. Firmware's predecessor wait returns its cancellation as a Result, removing an unreachable reason-less fallback. An architecture guard rejects new direct constructions, with a counted per-file baseline that can only shrink. * refactor(errors): share one aborted-signal predicate with cancelledBy * refactor(process): return a departing caller's reason from a shared startup wait * refactor(providers): build decompiler-provider cancellations with a required reason (#10587) Migrate every AnalysisCancelledError construction under src/ghidra, src/ida, src/cutter, src/jeb and src/kuna to cancelledBy and lower the guard baseline from 671 to 572. Closed Kuna target commands report OwnerCancellation.ownerClosed instead of an absent reason, and Kuna and JEB close abort their lifetimes with that typed reason. * fix(errors): read the reason of any signal-shaped cancellation source * refactor(native): build debugger and native cancellations with a required reason Migrate every AnalysisCancelledError construction under src/native, src/gdb and src/rizin to cancelledBy and delete Rizin's local rizinCancellation constructor; its reason choice becomes a CancellationReason. GDB close aborts active startups with OwnerCancellation.ownerClosed. * test(ci): cover the counted-baseline comparison behind the architecture guard * refactor(native): drop an unused cancellation import from native UI observation --- .../verify/architecture-guard-baselines.json | 18 +--- src/gdb/GdbSessionManager.ts | 64 ++++++------- src/native/AppleDispatchMetadata.ts | 23 ++--- src/native/LldbCallTracer.ts | 32 +++---- src/native/NativeCallObservation.ts | 10 +- src/native/NativeCallTarget.ts | 16 ++-- src/native/NativeCommandAnalysisError.ts | 24 +++-- src/native/NativeMacOSProvider.ts | 67 ++++++-------- src/native/NativePlistSource.ts | 17 ++-- src/native/NativeUiInterruptionControl.ts | 5 +- src/native/NativeUiObservation.ts | 5 +- src/native/pe/PeBinaryFailure.ts | 10 +- src/native/pwndbg/PwndbgCoreContext.ts | 33 ++++--- src/native/pwntools/PwntoolsDecoder.ts | 61 ++++++------- .../pwntools/PwntoolsRecordedCrashProvider.ts | 5 +- src/rizin/RizinDebugSessionManager.ts | 91 ++++++++++--------- 16 files changed, 229 insertions(+), 252 deletions(-) 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);