mirror of
https://github.com/morluto/rea
synced 2026-10-27 14:42:18 -04:00
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:
@@ -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];
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user