diff --git a/src/application/binary/AnalysisSnapshotCache.test.ts b/src/application/binary/AnalysisSnapshotCache.test.ts index 93e73104ad..17fab89578 100644 --- a/src/application/binary/AnalysisSnapshotCache.test.ts +++ b/src/application/binary/AnalysisSnapshotCache.test.ts @@ -716,6 +716,26 @@ describe("analysis snapshot cache policy", () => { effects: { changesPermissions: true }, cacheable: false, }, + { + policy: "snapshot", + operation: "decode_interface_builder", + cacheable: false, + }, + { + policy: "snapshot", + operation: "inspect_asset_catalog", + cacheable: false, + }, + { + policy: "snapshot", + operation: "inspect_keyed_archive", + cacheable: false, + }, + { + policy: "snapshot", + operation: "trace_dylib_resolution", + cacheable: false, + }, { policy: "snapshot", operation: "list_documents", cacheable: false }, { policy: "snapshot", operation: "list_strings", cacheable: false }, { diff --git a/src/application/binary/AnalysisSnapshotCache.ts b/src/application/binary/AnalysisSnapshotCache.ts index 4e90c3d8e6..159139d72e 100644 --- a/src/application/binary/AnalysisSnapshotCache.ts +++ b/src/application/binary/AnalysisSnapshotCache.ts @@ -51,6 +51,15 @@ const RELATIONSHIP_OPERATIONS: ReadonlySet = new Set([ "procedure_callees", ]); +// These reports depend on selected bundle resources or loader context beyond +// the immutable executable identity carried by a binary analysis snapshot. +const RESOURCE_SCOPED_OPERATIONS: ReadonlySet = new Set([ + "decode_interface_builder", + "inspect_asset_catalog", + "inspect_keyed_archive", + "trace_dylib_resolution", +]); + const STATEFUL_OPERATIONS: ReadonlySet = new Set([ "health", "current_address", @@ -85,6 +94,7 @@ export const isSnapshotCacheable = ( parameters: Readonly>, ): descriptor is CapabilityDescriptor => !STATEFUL_OPERATIONS.has(operation) && + !RESOURCE_SCOPED_OPERATIONS.has(operation) && (!DOCUMENT_SCOPED_OPERATIONS.has(operation) || typeof parameters.document === "string") && (!CURSOR_DEFAULT_OPERATIONS.has(operation) || diff --git a/src/application/binary/BinarySession.ts b/src/application/binary/BinarySession.ts index 6627353b5a..7053d3efce 100644 --- a/src/application/binary/BinarySession.ts +++ b/src/application/binary/BinarySession.ts @@ -1224,12 +1224,7 @@ export class BinarySession unboundOperationError: (operation, route) => this.#providerRouter.unboundOperationError(operation, route), lookupSnapshot: (target, profile, operation, parameters) => - operation === "decode_interface_builder" || - operation === "inspect_asset_catalog" || - operation === "inspect_keyed_archive" || - operation === "trace_dylib_resolution" - ? undefined - : this.lookupSnapshot(target, profile, operation, parameters), + this.lookupSnapshot(target, profile, operation, parameters), }); if (!prepared.ok) return prepared; const { active, capability, profile, cacheable, cached } = prepared.value; @@ -1275,15 +1270,7 @@ export class BinarySession ); if (!sourcesRecorded.ok) return sourcesRecorded; } - if ( - execution.ok && - cacheable && - profile !== undefined && - name !== "decode_interface_builder" && - name !== "inspect_asset_catalog" && - name !== "inspect_keyed_archive" && - name !== "trace_dylib_resolution" - ) { + if (execution.ok && cacheable && profile !== undefined) { const evidence = createAnalysisExecutionEvidence( name, arguments_, diff --git a/tests/composition/analysis-sessions/binarySession.test.ts b/tests/composition/analysis-sessions/binarySession.test.ts index b3442bd31e..64b9acac88 100644 --- a/tests/composition/analysis-sessions/binarySession.test.ts +++ b/tests/composition/analysis-sessions/binarySession.test.ts @@ -968,6 +968,55 @@ describe("snapshot switch rollback", () => { }); describe("snapshot cache eligibility", () => { + it.each([ + "decode_interface_builder", + "inspect_asset_catalog", + "inspect_keyed_archive", + "trace_dylib_resolution", + ] as const)( + "reobserves resource-scoped %s without executable snapshot Evidence", + async (operation) => { + const [target] = await createBinarySessionTargets(); + const provider = createCacheProvider([]); + const [baseCapability] = provider.capabilities(); + if (baseCapability === undefined) + throw new Error("Expected fixture capability"); + provider.capabilities = () => [ + { ...baseCapability, operation, cachePolicy: "snapshot" }, + ]; + let observations = 0; + provider.createClient = () => ({ + execute: async (name) => + resultOk( + createAnalysisExecution( + name === "health" ? null : ++observations, + provider.identity(), + ), + ), + close: async () => resultOk(null), + }); + const session = createTestBinarySession(provider); + try { + expect((await session.open(target)).ok).toBe(true); + for (const observation of [1, 2]) { + const result = await session.execute(operation, {}); + expect(result).toMatchObject({ + ok: true, + value: { result: observation }, + }); + if (result.ok) expect(result.value.evidenceId).toBeUndefined(); + } + expect(session.exportEvidenceBundle().records).toEqual([]); + expect(session.exportAnalysisSnapshot()).toMatchObject({ + ok: true, + value: { entries: [] }, + }); + } finally { + await session.close(); + } + }, + ); + it.each([false, true])( "retains source Evidence independently of snapshot eligibility (live=%s)", async (live) => {