fix(javascript): restore read-site ambient aliases and string config reads

Main fails five source tests after 04672f2b9 and d4bd65c53.

- Ambient write resolution used the caller's write site for every alias
  read it reached, so `alias = alias.globalThis` resolved its own self-read
  only to the reassignment and lost the original `globalThis` identity. Reads
  reached through an initializer now select initializers at their own
  position; the written target keeps the caller's write site.
- Process escape tracking recorded `Number(process.env.PORT)` and
  `Number(process.argv[2])` as escapes of the read slot, which dropped the
  configuration read inside functions. Node stores env values and argv
  entries as strings, so passing one exposes no mutable object; replacing an
  entry still records its own write or receiver escape.
- The dynamic isolated-world test now expects the direct observation that the
  proven Webpack bootstrap resolution produces.
- The native IDA admission test now matches d4bd65c53: attached profiles
  report the bound document and still refuse address resolution.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kaoru0822 2026-10-14 01:50:50 +00:00
parent c7105d9723
commit 4450463019
4 changed files with 48 additions and 23 deletions

View File

@ -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");
});

View File

@ -56,9 +56,13 @@ const createAmbientPathResolver = (
const prior = possibleWrites ? undefined : cached.get(node);
if (prior !== undefined) return prior;
const result = new Map<string, AmbientPath>();
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

View File

@ -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,

View File

@ -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",