Files
reverse-engineering-anything/src/domain/javascript/javascriptSemanticAmbientWrites.ts
T
396b89c5e2 fix(javascript): attribute service workers to intact platform paths
Check the complete navigator.serviceWorker.register identity through the shared
ambient-write owner, including replaced methods and aliased receiver mutations.
Reuse the module URL reader for intact registrations while preserving unknown
document-relative strings. Cover the unresolved review case and the CLI graph.

Refs #3841

Co-authored-by: Chandan Chaudhary <chandankkumar156@gmail.com>
Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
2026-10-16 06:06:06 +08:00

324 lines
12 KiB
TypeScript

import * as t from "@babel/types";
import {
semanticStaticPropertyKey,
unwrapJavaScriptExpression,
} from "./javascriptAstValues.js";
import {
collectJavaScriptIdentityWritesSteps,
electronMemberPath,
} from "./javascriptElectronMemberWrites.js";
import { createSemanticModuleInterop } from "./javascriptModuleInterop.js";
import {
isUnshadowedGlobal,
resolveSemanticBindingState,
semanticStableInitializer,
type JavaScriptSemanticAnalysisState,
} from "./javascriptSemanticState.js";
import { isImportMetaUrl } from "./javascriptStaticAnalysisHelpers.js";
import { semanticMutationInitializers } from "./javascriptSemanticMutationInitializers.js";
const GLOBAL_OBJECT_NAMES = ["globalThis", "global", "window", "self"];
const globalObjectName = (
node: t.Node,
state: JavaScriptSemanticAnalysisState,
): string | undefined =>
GLOBAL_OBJECT_NAMES.find((name) => isUnshadowedGlobal(node, state, name));
/** Global self references retain object identity, rather than adding a property. */
const canonicalAmbientPath = (
path: readonly (string | null)[],
): readonly (string | null)[] => {
if (path[0] !== "global") return path;
let firstProperty = 1;
while (GLOBAL_OBJECT_NAMES.includes(path[firstProperty] ?? ""))
firstProperty += 1;
return ["global", ...path.slice(firstProperty)];
};
type AmbientPath = readonly (string | null)[];
/** Undefined means a cyclic or conflicting projection can select any depth. */
type AmbientProjection = AmbientPath | undefined;
/**
* Follow local object identities without equating shallow copies with their
* sources. Write lookup retains every visible initializer: reassigning an alias
* later cannot erase a possible earlier mutation. Intrinsic reads stay strict.
*/
const createAmbientPathResolver = (
state: JavaScriptSemanticAnalysisState,
possibleWrites: boolean,
) => {
const cached = new WeakMap<t.Node, readonly AmbientPath[]>();
return (node: t.Node, capture?: t.Node): readonly AmbientPath[] => {
const prior = possibleWrites ? undefined : cached.get(node);
if (prior !== undefined) return prior;
const result = new Map<string, AmbientPath>();
// 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.
const expanded = new WeakMap<t.Node, AmbientProjection>();
const retain = (path: AmbientPath) => {
let canonical = canonicalAmbientPath(path);
for (;;) {
result.set(JSON.stringify(canonical), canonical);
// An unknown key may select a global self reference as well as a
// distinct property. Retain both possible object identities for writes.
if (
!possibleWrites ||
canonical[0] !== "global" ||
canonical[1] !== null
)
break;
canonical = canonicalAmbientPath(["global", ...canonical.slice(2)]);
}
};
while (pending.length > 0) {
const current = pending.pop();
if (current === undefined) break;
const { root, members } = electronMemberPath(current.node);
let suffix: AmbientProjection =
current.suffix === undefined
? undefined
: [...members, ...current.suffix];
if (expanded.has(root)) {
const previous = expanded.get(root);
if (previous === undefined) continue;
if (suffix !== undefined && previous.length === suffix.length) {
const joined: AmbientPath = previous.map((key, index) =>
key === suffix?.[index] ? key : null,
);
if (joined.every((key, index) => key === previous[index])) continue;
suffix = joined;
} else suffix = undefined;
if (!possibleWrites) {
cached.set(node, []);
return [];
}
}
expanded.set(root, suffix);
// 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,
...(current.readsAtSelf && { readsAtSelf: true }),
});
if (
t.isMetaProperty(root) &&
root.meta.name === "import" &&
root.property.name === "meta"
) {
retain(["import-meta", ...(suffix ?? [])]);
continue;
}
if (t.isIdentifier(root)) {
// The wrapper module object is distinct from globalThis.module. Keep
// its identity through the same alias/projection owner used for writes.
if (isUnshadowedGlobal(root, state, "module")) {
retain(["commonjs-module", ...(suffix ?? [])]);
continue;
}
if (globalObjectName(root, state) !== undefined) {
retain(["global", ...(suffix ?? [])]);
continue;
}
if (isUnshadowedGlobal(root, state, root.name)) {
retain(["global", root.name, ...(suffix ?? [])]);
continue;
}
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
? readAt === undefined
? binding.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
: [
...initializer.projection.map((key) =>
key === null ? null : String(key),
),
...suffix,
],
});
continue;
}
if (possibleWrites && t.isConditionalExpression(root)) {
continueWith(root.consequent);
continueWith(root.alternate);
continue;
}
if (possibleWrites && t.isLogicalExpression(root)) {
continueWith(root.left);
continueWith(root.right);
continue;
}
const key = suffix?.[0];
if (suffix !== undefined && key === undefined) continue;
if (t.isObjectExpression(root)) {
if (possibleWrites) {
for (const property of root.properties) {
if (t.isSpreadElement(property)) continueWith(property.argument);
else if (
t.isObjectProperty(property) &&
t.isExpression(property.value)
) {
const ownKey = semanticStaticPropertyKey(
property.key,
property.computed,
);
if (
suffix === undefined ||
key === null ||
ownKey === null ||
ownKey === key
)
continueWith(property.value, suffix?.slice(1));
}
}
} else if (
suffix !== undefined &&
key !== null &&
root.properties.every(
(property) =>
t.isObjectProperty(property) &&
semanticStaticPropertyKey(property.key, property.computed) !==
null,
)
) {
const property = root.properties.findLast(
(property) =>
t.isObjectProperty(property) &&
semanticStaticPropertyKey(property.key, property.computed) ===
key,
);
if (t.isObjectProperty(property) && t.isExpression(property.value))
continueWith(property.value, suffix.slice(1));
}
} else if (t.isArrayExpression(root)) {
if (suffix === undefined) {
for (const element of root.elements)
if (t.isSpreadElement(element)) continueWith(element.argument);
else if (t.isExpression(element)) continueWith(element);
} else if (!root.elements.some(t.isSpreadElement)) {
const index = Number(key);
if (key === null && possibleWrites) {
for (const element of root.elements)
if (t.isExpression(element))
continueWith(element, suffix.slice(1));
} else if (
key !== null &&
Number.isInteger(index) &&
index >= 0 &&
String(index) === key
) {
const element = root.elements[index];
if (t.isExpression(element)) continueWith(element, suffix.slice(1));
}
}
}
}
const paths = [...result.values()];
if (!possibleWrites) cached.set(node, paths);
return paths;
};
};
/**
* Index source-local writes through ambient identities and stable aliases.
* The shared invocation owner resolves intrinsic aliases, bind/call/apply and
* literal argument spreads. Runtime reachability and arbitrary call effects
* remain unevaluated; a visible possible write is sufficient.
*/
export function* indexJavaScriptAmbientWritesSteps(
program: t.Program,
state: JavaScriptSemanticAnalysisState,
): Generator<void> {
const ambientReads = createAmbientPathResolver(state, false);
const ambientWrites = createAmbientPathResolver(state, true);
const interop = createSemanticModuleInterop(state, {
intrinsicPath: (node) => {
const [path] = ambientReads(node);
return path?.[0] === "global" && path.every((key) => key !== null)
? ["intrinsic", ...(path.slice(1) as readonly string[])]
: undefined;
},
});
yield* collectJavaScriptIdentityWritesSteps(
program,
(root, kind, writeSite) => {
if (kind === "opaque-call") return [];
// Reassigning a local alias changes the binding, not the referenced object.
if (kind === "binding")
return t.isIdentifier(root) &&
isUnshadowedGlobal(root, state, root.name)
? [["global", root.name]]
: [];
return ambientWrites(root, writeSite);
},
interop.intrinsicInvocation,
interop.staticString,
interop.isFreshObject,
(node, path) => {
state.ambientWrites.retain(path);
state.ambientWriteEffects.push({ path, node });
},
);
}
/**
* Recognize an ambient property path, optionally through a global object, without a visible
* source-local overwrite. This is a syntax-local identity fact, not proof that
* the runtime's globals have never been patched by other code.
*/
export const isUnmodifiedGlobalReference = (
callee: t.Node,
state: JavaScriptSemanticAnalysisState,
name: string | readonly [string, ...string[]],
): boolean => {
const path: readonly [string, ...string[]] =
typeof name === "string" ? [name] : name;
if (state.ambientWrites.covers(["global", ...path])) return false;
const { root, members } = electronMemberPath(callee);
const matches = (expected: readonly string[]) =>
members.length === expected.length &&
members.every((key, index) => key === expected[index]);
if (isUnshadowedGlobal(root, state, path[0]) && matches(path.slice(1)))
return true;
const namespace = globalObjectName(root, state);
return (
namespace !== undefined &&
matches(path) &&
!state.ambientWrites.covers(["global", namespace])
);
};
/** A module URL base is known only while its property has no visible write. */
export const isUnmodifiedImportMetaUrl = (
node: t.Node | undefined,
state: JavaScriptSemanticAnalysisState,
): boolean =>
isImportMetaUrl(node) && !state.ambientWrites.covers(["import-meta", "url"]);