diff --git a/src/domain/javascript/electronWrappedCallPaths.test.ts b/src/domain/javascript/electronWrappedCallPaths.test.ts index 6d30a10f5..b92aa990f 100644 --- a/src/domain/javascript/electronWrappedCallPaths.test.ts +++ b/src/domain/javascript/electronWrappedCallPaths.test.ts @@ -147,7 +147,9 @@ describe("wrapped contextBridge exposure methods", () => { `const worldId = selectWorld(); electron.contextBridge.exposeInIsolatedWorld(worldId, "isolatedApi", {ping: true});`, ), ); - expect(result.electron.context_bridge_apis).toContainEqual( + // The proven Webpack bootstrap resolves `load(2)` directly, so the complete + // direct observation is retained instead of a wrapper substitution. + expect(result.electron.context_bridge_apis).toEqual([ expect.objectContaining({ world: "isolated", world_id: { @@ -156,9 +158,15 @@ describe("wrapped contextBridge exposure methods", () => { expression: "worldId", }, api_key: "isolatedApi", + api_status: "object-literal", members: ["ping"], - resolution_basis: "wrapper-substitution", + unknown_members: 0, + module_key: "9", + module_runtime: "webpack-bootstrap@0", }), + ]); + expect(result.electron.context_bridge_apis[0]).not.toHaveProperty( + "resolution_basis", ); expect(result.parse_status).toBe("partial"); }); diff --git a/src/domain/javascript/javascriptSemanticAmbientWrites.ts b/src/domain/javascript/javascriptSemanticAmbientWrites.ts index f7c4a8e7a..e582f07a8 100644 --- a/src/domain/javascript/javascriptSemanticAmbientWrites.ts +++ b/src/domain/javascript/javascriptSemanticAmbientWrites.ts @@ -56,9 +56,13 @@ const createAmbientPathResolver = ( const prior = possibleWrites ? undefined : cached.get(node); if (prior !== undefined) return prior; const result = new Map(); - const pending: { node: t.Node; suffix: AmbientProjection }[] = [ - { node, suffix: [] }, - ]; + // The caller's write site selects initializers for the written target. + // Reads reached through an initializer occur at their own position. + const pending: { + node: t.Node; + suffix: AmbientProjection; + readsAtSelf?: true; + }[] = [{ node, suffix: [] }]; // Join projections at their producing expression, rather than enumerating // alias histories. Each key can only widen to a wildcard, and conflicting // depths widen to unknown, so diamonds and cycles have finite state. @@ -105,7 +109,11 @@ const createAmbientPathResolver = ( // A widened projection is a possible write, never proof of an intrinsic. if (suffix === undefined && !possibleWrites) continue; const continueWith = (node: t.Node, path: AmbientProjection = suffix) => - pending.push({ node, suffix: path }); + pending.push({ + node, + suffix: path, + ...(current.readsAtSelf && { readsAtSelf: true }), + }); if ( t.isMetaProperty(root) && root.meta.name === "import" && @@ -126,20 +134,19 @@ const createAmbientPathResolver = ( const binding = resolveSemanticBindingState(state, root, root.name); if (binding === undefined) continue; const stable = semanticStableInitializer(binding, state); + const readAt = current.readsAtSelf ? root : capture; const initializers = possibleWrites - ? capture === undefined + ? readAt === undefined ? binding.initializers - : semanticMutationInitializers( - binding, - capture, - state.parentsByNode, - ).initializers + : semanticMutationInitializers(binding, readAt, state.parentsByNode) + .initializers : stable === undefined ? [] : [stable]; for (const initializer of initializers) pending.push({ node: initializer.node, + readsAtSelf: true, suffix: suffix === undefined ? undefined diff --git a/src/domain/javascript/javascriptSemanticMemberMutations.ts b/src/domain/javascript/javascriptSemanticMemberMutations.ts index f3d7063b7..34666e65c 100644 --- a/src/domain/javascript/javascriptSemanticMemberMutations.ts +++ b/src/domain/javascript/javascriptSemanticMemberMutations.ts @@ -69,6 +69,16 @@ export function* collectSemanticMemberMutationsSteps( node: t.Node | undefined, ): void => { const identity = semanticRequireExportsIdentity(specifier, path); + // Node stores process.env values and process.argv entries as strings, so + // passing one to a call exposes no object that the callee could mutate. + // Replacing an entry records its own write or receiver escape. + if ( + effect === "escape" && + identity.specifier === "process" && + identity.path.length === 2 && + (identity.path[0] === "env" || identity.path[0] === "argv") + ) + return; const key = JSON.stringify([ "required", identity.specifier, diff --git a/src/ida/IdaNativeProtocol.test.ts b/src/ida/IdaNativeProtocol.test.ts index 466a09ad5..a1ba9fc0c 100644 --- a/src/ida/IdaNativeProtocol.test.ts +++ b/src/ida/IdaNativeProtocol.test.ts @@ -779,18 +779,18 @@ describe("native IDA attached operation admission", () => { await client.close(); }); - it("does not admit the headless-only containment and document operations", async () => { - const { client, producer } = await fixture(); - for (const [operation, parameters] of [ - ["resolve_containing_procedure", { address: "0x1000" }], - ["list_documents", {}], - ] as const) { - const result = await client.execute(operation, parameters); + it("reports the bound document but not headless-only address resolution", async () => { + const { client, producer, target } = await fixture(); + const containing = await client.execute("resolve_containing_procedure", { + address: "0x1000", + }); + expect(containing.ok).toBe(false); + if (!containing.ok) + expect(containing.error._tag).toBe("AnalysisCapabilityUnavailableError"); - expect(result.ok).toBe(false); - if (!result.ok) - expect(result.error._tag).toBe("AnalysisCapabilityUnavailableError"); - } + const documents = await client.execute("list_documents", {}); + if (!documents.ok) throw documents.error; + expect(documents.value.result).toEqual([target.path]); expect( producer.calls.some(({ name }) => name === "lookup_funcs"), "nothing is dispatched upstream",