fix(javascript): preserve runtime path bytes and Electron ownership (#4941)

* fix(javascript): preserve literal percent runtime paths

* fix(javascript): ignore resolve prefixes before the final anchor

* fix(electron): retain application roots for loadFile declarations

* fix(electron): resolve loadFile from enclosing application package

* fix(electron): retain outer application root across package scopes

* fix(electron): derive application ownership from inventoried packages

* fix(electron): retain resolved package entrypoint ownership

* fix(electron): preserve competing application ownership

* fix(electron): retain invalid and omitted runtime ownership

* fix(electron): preserve omitted module ownership and runtime paths

* fix(electron): preserve proven roots before confining page paths

* fix(electron): retain possible owners behind missing imports

* fix(electron): distinguish absent imports and owned page candidates

* fix(electron): keep unavailable JSON out of owner gaps

* fix(electron): retain exact loader formats and default entry status

* fix(electron): retain unavailable entrypoint ownership candidates

* test(ghidra): preserve Windows Node probe prerequisites

* test(ghidra): assert cancellation during profile shutdown

* fix(electron): separate examined imports from frontend ownership

* fix(electron): retain unavailable directory entrypoint owners

* test(jeb): retain Linux ownership prerequisite in isolation

* fix(javascript): retain unchecked loader ownership and literal main paths

* fix(javascript): preserve runtime paths and call-site module aliases

* fix(javascript): bound module alias traversal and preserve projections

* fix(javascript): retain host module and nested artifact path boundaries

* fix(javascript): preserve ESM and archive module loader provenance

* test(javascript): distinguish unmapped containers from missing exports

* fix: retain default module aliases and Electron builtin imports

* fix: preserve host file URL and inward ASAR ownership

* fix: preserve CommonJS file URL classification in CI fixtures

* fix: retain bound module loaders and Windows entrypoint uncertainty

* fix: retain ASAR uncertainty and invalidate changed Electron app roots

* fix(javascript): retain ownership uncertainty across loader boundaries

* fix(javascript): preserve loader sources and bound alias queries

* fix(javascript): retain destructured loader sources

* fix(javascript): preserve loader uncertainty at resolution boundaries

* fix(javascript): preserve callable and package launch ownership

* fix(javascript): preserve captured values across member reads

* test(process): assert tagged filesystem cancellation failures

* fix(javascript): preserve inventory bases and invocation read timing

* fix(javascript): preserve ownership across indirect and unread loads

* test(javascript): order fixture directory creation on Windows

* test(javascript): share ordered filesystem fixture writer

* fix(javascript): unify loader provenance and package validation

* fix(javascript): preserve captured Node require loader values

* fix(javascript): preserve loader invocation authority

* fix(javascript): verify generic loader invocation authority

* fix(javascript): preserve conditional loader method alternatives

* fix(javascript): require proven factories for source association

* fix(javascript): honor replaced Electron app path setters

* fix(javascript): prove path and assignment source ownership

* fix(javascript): retain native delegate eligibility through shared writes

* fix(process): settle stale baseline errors after confirmed absence

* fix(javascript): preserve assignment and path boundary evidence

* fix(javascript): prove source-directory resolve anchors

* fix: preserve application path provenance and portable schemas

* fix(javascript): retain CommonJS exported setter effects

* fix: isolate Linux identity observations and retain native module exports

* fix(javascript): preserve published app and helper provenance

* fix(javascript): normalize typed stores and native path fixtures
This commit is contained in:
ShuaixinHuang
2026-10-20 17:25:19 +08:00
committed by GitHub
parent d532b18123
commit e9fc8fb30c
149 changed files with 16881 additions and 988 deletions
@@ -8,6 +8,7 @@ import {
semanticMutableBuiltinOrigin,
semanticRequireExportsIdentity,
semanticRequireOrigin,
hasSemanticCreateRequireFactoryOrigin,
} from "./javascriptSemanticRequireOrigin.js";
import {
@@ -23,16 +24,22 @@ import {
} from "./javascriptSemanticValues.js";
import { semanticContainer } from "./javascriptSemanticSlots.js";
import {
semanticArrayPropertyReferences,
semanticClassCandidates,
semanticClassPropertyReferences,
semanticObjectPropertyReferences,
type JavaScriptSemanticObjectReference,
} from "./javascriptSemanticObjectReferences.js";
import { semanticIterationSources } from "./javascriptSemanticIterationSources.js";
import {
semanticIterationSources,
type SemanticIterationSource,
} from "./javascriptSemanticIterationSources.js";
import { semanticCallableResultExpressions } from "./javascriptSemanticCallableResults.js";
import { createSemanticReturnedReferences } from "./javascriptSemanticReturnedReferences.js";
import { semanticMutationInitializers } from "./javascriptSemanticMutationInitializers.js";
import {
semanticCopiedReferencePath,
semanticKnownArrayIteration,
semanticReferenceSourcesInactive,
semanticSelectedReferenceSlot,
} from "./javascriptSemanticReferenceSources.js";
@@ -54,6 +61,23 @@ import {
type PropertyPath = JavaScriptSemanticPropertyPath;
type ValueEffect = "write" | "escape";
/** Only replacing or possibly selecting an iterator-bearing prototype slot changes iteration. */
const arrayPrototypeIterationMayChange = (path: PropertyPath): boolean => {
// Array.prototype and Object.prototype are on native arrays' prototype
// chain. Their default constructor.prototype pairs return to the same owner.
let offset = 0;
while (path[offset] === "constructor" && path[offset + 1] === "prototype")
offset += 2;
const key = path[offset];
return (
key === undefined ||
key === null ||
typeof key === "object" ||
semanticArrayIndex(key) !== null ||
key === "__proto__"
);
};
/** Preserve uncertainty from property writes and references exposed to calls, yielding during traversal and fixpoint iteration. */
/** Setter keys of one object literal; a computed setter key may match any key. */
@@ -198,7 +222,7 @@ export function* collectSemanticMemberMutationsSteps(
recordRequiredEffect("node:process", path.slice(2), "write", node);
const selections = new Map<string, readonly string[]>();
const coveringPaths = new Map<string, SemanticPropertyPathCoverage>();
let arrayIterationUnknown = false;
state.arrayIterationUnknown = false;
// Definition collection finishes before this phase, so a scope's bindings no
// longer change and one node resolves to one binding. Repeated visits of the
// same node otherwise repeat the same scope walk.
@@ -278,11 +302,21 @@ export function* collectSemanticMemberMutationsSteps(
source.node.start,
source.node.end,
source.projection,
source.iterationSources?.map((iterable) => [
iterable.node.start,
iterable.node.end,
iterable.projection,
]),
]),
reference.fallbackSources?.map((source) => [
source.node.start,
source.node.end,
source.projection,
source.iterationSources?.map((iterable) => [
iterable.node.start,
iterable.node.end,
iterable.projection,
]),
]),
[...reference.bindings].sort(compareUnicodeCodePoints),
]);
@@ -300,6 +334,7 @@ export function* collectSemanticMemberMutationsSteps(
originAt: t.Node | undefined = mutation,
ownerInitializer?: t.Node,
widenedGetterResult = false,
assignedSources?: readonly SemanticIterationSource[],
): void => {
const pending: (ReferencedValue & {
readonly bindings: ReadonlySet<string>;
@@ -351,15 +386,18 @@ export function* collectSemanticMemberMutationsSteps(
mutation,
);
if (
(expression.name === "Array" &&
(current.path[0] === "prototype" || current.path[0] === null)) ||
(["Array", "Object"].includes(expression.name) &&
(current.path[0] === "prototype" || current.path[0] === null) &&
arrayPrototypeIterationMayChange(current.path.slice(1))) ||
(["globalThis", "global", "window", "self"].includes(
expression.name,
) &&
(current.path[0] === "Array" || current.path[0] === null) &&
(current.path[1] === "prototype" || current.path[1] === null))
(["Array", "Object"].includes(String(current.path[0])) ||
current.path[0] === null) &&
(current.path[1] === "prototype" || current.path[1] === null) &&
arrayPrototypeIterationMayChange(current.path.slice(2)))
)
arrayIterationUnknown = true;
state.arrayIterationUnknown = true;
continue;
}
// An alias restored from a saved reference can revisit the same binding
@@ -487,7 +525,7 @@ export function* collectSemanticMemberMutationsSteps(
current.originAt?.start ?? -1
}\u0001${current.originAt?.end ?? -1}\u0001${
current.ownerInitializer?.start ?? -1
}\u0001${current.ownerInitializer?.end ?? -1}\u0001${widenedGetterResult}`;
}\u0001${current.ownerInitializer?.end ?? -1}\u0001${widenedGetterResult}\u0001${assignedSources?.[0]?.target?.start ?? -1}`;
let covering = coveringPaths.get(pathState);
if (covering === undefined) {
covering = new SemanticPropertyPathCoverage();
@@ -521,9 +559,29 @@ export function* collectSemanticMemberMutationsSteps(
mutation,
);
}
const receiverInitializers = [
...origins.initializers,
...origins.referenceInitializers,
];
if (effect === "escape")
binding.escapedPaths.push({ path, node: mutation });
else binding.memberWrites.push({ path, node: mutation });
binding.escapedPaths.push({
path,
node: mutation,
receiverInitializers,
...(current.originAt === undefined
? {}
: { receiverAt: current.originAt }),
});
else
binding.memberWrites.push({
path,
node: mutation,
receiverInitializers,
...(current.originAt === undefined
? {}
: { receiverAt: current.originAt }),
...(assignedSources === undefined ? {} : { assignedSources }),
});
clearSemanticValueCaches(state);
for (const initializer of origins.initializers)
pending.push({
@@ -534,7 +592,11 @@ export function* collectSemanticMemberMutationsSteps(
});
}
for (const initializer of origins.referenceInitializers) {
const copiedPath = copiedReferencePath(initializer, path, effect);
const copiedPath = semanticCopiedReferencePath(
initializer,
path,
effect,
);
if (copiedPath === null) continue;
pendingReferences.push({
initializer,
@@ -617,10 +679,14 @@ export function* collectSemanticMemberMutationsSteps(
): readonly ReferencedValue[] {
if (
t.isArrayExpression(expression) &&
((path[0] === "__proto__" && (effect === "escape" || path.length > 1)) ||
(path[0] === "constructor" && path[1] === "prototype"))
((path[0] === "__proto__" &&
(effect === "escape" || path.length > 1) &&
arrayPrototypeIterationMayChange(path.slice(1))) ||
(path[0] === "constructor" &&
path[1] === "prototype" &&
arrayPrototypeIterationMayChange(path.slice(2))))
)
arrayIterationUnknown = true;
state.arrayIterationUnknown = true;
const references = referencedValues(
expression,
path,
@@ -732,6 +798,21 @@ export function* collectSemanticMemberMutationsSteps(
markEscaped(expression.object, mutation);
};
const markTarget = (node: t.Node, mutation: t.Node): void => {
const sources = semanticIterationSources(
mutation,
state.parentsByNode.get(mutation) ?? null,
);
const sourcesByTarget = new Map<
t.MemberExpression | t.OptionalMemberExpression,
SemanticIterationSource[]
>();
for (const source of sources) {
if (source.kind !== "reference-store" || source.target === undefined)
continue;
const selected = sourcesByTarget.get(source.target) ?? [];
selected.push(source);
sourcesByTarget.set(source.target, selected);
}
for (const target of semanticWriteTargets(node))
if (!t.isIdentifier(target))
markValue(
@@ -740,6 +821,10 @@ export function* collectSemanticMemberMutationsSteps(
new Set(),
"write",
mutation,
mutation,
undefined,
false,
sourcesByTarget.get(target) ?? [],
);
};
yield* traverseJavaScriptAstSteps(program, {
@@ -798,7 +883,7 @@ export function* collectSemanticMemberMutationsSteps(
) {
const effectsBefore = recordedEffectCount;
const iterablesBefore = iterableReferences.length;
const iterationBefore: boolean = arrayIterationUnknown;
const iterationBefore: boolean = state.arrayIterationUnknown;
for (const [index, reference] of pendingReferences.splice(0).entries()) {
if (index % 64 === 0) yield;
const { initializer } = reference;
@@ -866,12 +951,7 @@ export function* collectSemanticMemberMutationsSteps(
const slot = semanticSelectedReferenceSlot(reference, state);
// Custom iteration can yield the iterable itself as well as any child.
// Treat its root as escaped, including writes through yielded references.
const knownArray =
!arrayIterationUnknown &&
slot?.value.status === "array" &&
slot.value.items.every(
(item) => semanticArrayIndex(item.name) !== null,
);
const knownArray = semanticKnownArrayIteration(slot?.value, state);
if (knownArray && reference.iterationOnly) continue;
markValue(
reference.node,
@@ -901,7 +981,7 @@ export function* collectSemanticMemberMutationsSteps(
if (
recordedEffectCount === effectsBefore &&
iterableReferences.length === iterablesBefore &&
arrayIterationUnknown === iterationBefore
state.arrayIterationUnknown === iterationBefore
)
break;
}
@@ -922,14 +1002,12 @@ interface IterableReference extends ReferencedValue {
readonly mutation?: t.Node;
readonly originAt: t.Node | undefined;
readonly iterationOnly?: boolean;
readonly requiredSources?: readonly {
readonly node: t.Node;
readonly projection: PropertyPath;
}[];
readonly fallbackSources?: readonly {
readonly node: t.Node;
readonly projection: PropertyPath;
}[];
readonly requiredSources?: NonNullable<
SemanticIterationSource["requiredSources"]
>;
readonly fallbackSources?: NonNullable<
SemanticIterationSource["fallbackSources"]
>;
}
const expandedIterationReferences = (
@@ -1004,7 +1082,7 @@ const referencedValues = (
if (effect === "write" && path.length < 2) return [];
return t.isObjectExpression(node)
? semanticObjectPropertyReferences(node, path, state)
: arrayReferencedValues(node, path);
: semanticArrayPropertyReferences(node, path);
}
if (t.isClass(node) || t.isNewExpression(node))
return constructedReferenceValues(
@@ -1081,6 +1159,7 @@ type MutationObservableOrigin =
| "ambient"
| "array"
| "array-prototype"
| "function"
| "opaque";
interface MutationObserverNode {
@@ -1172,7 +1251,9 @@ const observableMutationPaths = (
// CommonJS namespace from `require("node:fs")`). Keep those writes at their
// exact member paths instead of widening the first path to a wildcard.
const hasParsedRequireOrigin = binding.initializers.some(
({ node }) => semanticRequireOrigin(node, state) !== undefined,
({ node }) =>
semanticRequireOrigin(node, state) !== undefined ||
hasSemanticCreateRequireFactoryOrigin(node, state),
);
if (
binding.directOrigins.length > 0 ||
@@ -1205,6 +1286,10 @@ const observableMutationPaths = (
const selected = pendingExpressions.pop();
if (selected === undefined) break;
const { expression, observer } = selected;
if (t.isFunction(expression))
// Keep own-member writes precise without replacing the existing
// links to references handed to a function's eventual caller.
(observer.origins ??= new Set()).add("function");
// Keep class/getter projections distinct so cycle pruning does not
// discard observable escapes through a shared getter result.
if (
@@ -1386,98 +1471,3 @@ const observableMutationPaths = (
};
return { normalize, repeatsUnpruned };
};
const arrayReferencedValues = (
node: t.ArrayExpression,
path: PropertyPath,
): readonly ReferencedValue[] => {
const [key, ...remaining] = path;
const selected = key ?? null;
const selectedIndex =
typeof selected === "object" ? null : semanticArrayIndex(selected);
if (typeof selected !== "object" && selectedIndex === null) return [];
const references: ReferencedValue[] = [];
let minimumIndex = 0;
let uncertainIndex = false;
for (const element of node.elements) {
if (t.isSpreadElement(element)) {
if (selectedIndex === null || selectedIndex >= minimumIndex) {
let sourceKey: PropertyPath[number] = null;
if (!uncertainIndex) {
if (selectedIndex !== null) sourceKey = selectedIndex - minimumIndex;
else if (selected !== null && typeof selected === "object")
sourceKey = {
excludedKeys: selected.excludedKeys.flatMap((name) => {
const index = semanticArrayIndex(name);
return index === null || index < minimumIndex
? []
: [String(index - minimumIndex)];
}),
startIndex: Math.max(
0,
(selected.startIndex ?? 0) - minimumIndex,
),
};
}
references.push({
node: element.argument,
path: [sourceKey, ...remaining],
iterableFallbackPath: [],
});
}
uncertainIndex = true;
continue;
}
if (
element !== null &&
(uncertainIndex
? selectedIndex === null || selectedIndex >= minimumIndex
: semanticPropertyPathKeyMatches(selected, String(minimumIndex)))
)
references.push({ node: element, path: remaining });
minimumIndex++;
}
return references;
};
const copiedReferencePath = (
initializer: JavaScriptSemanticBindingState["referenceInitializers"][number],
path: PropertyPath,
effect: ValueEffect,
): PropertyPath | null => {
const offset = initializer.copyProjectionOffset;
if (offset === undefined) return [...initializer.projection, ...path];
const copiedPath = [...initializer.projection.slice(offset + 1), ...path];
if (effect === "write" && copiedPath.length < 2) return null;
let [key, ...remaining] = copiedPath;
if (initializer.copyKind === "array-rest") {
const startIndex = initializer.copyStartIndex ?? 0;
if (key === undefined || key === null)
key = { excludedKeys: [], startIndex };
else if (typeof key === "object")
key = {
excludedKeys: key.excludedKeys.flatMap((name) => {
const index = semanticArrayIndex(name);
return index === null ? [] : [String(index + startIndex)];
}),
startIndex: (key.startIndex ?? 0) + startIndex,
};
else {
const index = semanticArrayIndex(key);
if (index === null) return null;
key = index + startIndex;
}
} else {
const excludedKeys = initializer.copyExcludedKeys ?? [];
if (key === undefined || key === null) key = { excludedKeys };
else if (typeof key === "object")
key = {
...key,
excludedKeys: [...new Set([...key.excludedKeys, ...excludedKeys])].sort(
compareUnicodeCodePoints,
),
};
else if (excludedKeys.includes(String(key))) return null;
}
return [...initializer.projection.slice(0, offset), key, ...remaining];
};