fix(javascript): keep strict module checks unproven when alias walks exhaust their budget (#9387)

* fix(javascript): keep strict module checks unproven when alias walks exhaust their budget

Fixes #8201.

* fix(javascript): examine queued module sources after a strict walk exhausts its budget

A strict check that exhausts the alias budget stopped the whole walk, so a module branch queued beside the growing alias was never examined. Stop expanding instead, and drain the queued sources.

* fix(javascript): drain exhausted strict walks by selected keys and finite expressions

After exhaustion the limiter widens every key, so draining by normalized paths could match process.foo as mainModule. Keep each queued source's selected keys, expand finite expressions such as sequences and conditionals, and stop only at bindings and calls.

* fix(javascript): follow bindings and calls breadth first in exhausted strict walks

An exhausted strict walk skipped every binding and call, so (flag ? m : r).require with const m = module lost the module branch when r was explored first. Drain the queue breadth first after exhaustion and follow bindings and calls under a second alias selection budget, so a growing alias cannot starve a sibling.

* fix(javascript): bound every state a strict walk creates after exhaustion

After the alias budget is exhausted, paths are no longer normalized, so a
cycle through containers or getters can keep growing its selected path
without crossing a binding or call. Count each new state against the
drain budget instead of only binding and call follow-ups.

* fix(javascript): retain uncertainty at alias cutoff

---------

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
This commit is contained in:
authored and GitHub committed 2026-11-09 07:59:15 -05:00
1 parent 3c3d405c00
commit 9108d9a627
2 files changed
+233 -51

No files matched your search

@@ -156,3 +156,157 @@ it("revokes the host delegate after a destructured module receiver write", () =>
expect.objectContaining({ kind: "require", specifier: null }),
]);
});
it("revokes the host delegate after a dynamic defineProperty on module", () => {
const analysis = analyzeJavaScriptStaticSource(
`Object.defineProperty(module,key,{value:x});require('./shared.cjs');`,
);
expect(analysis.parse_error_count).toBe(0);
expect(analysis.references).toEqual([
expect.objectContaining({ kind: "require", specifier: null }),
]);
});
// Zod's issue formatter (`r = s` after `Object.defineProperty(r, k, …)`)
// exhausts the alias-selection budget. A strict module check stays unproven.
const loopAliasedPropertyWalk = (defineProperty: boolean): string => {
const step = defineProperty
? "Object.prototype.hasOwnProperty.call(r,k)||Object.defineProperty(r,k,{value:{_errors:[]}});"
: "Object.prototype.hasOwnProperty.call(r,k);";
return `let h=require("electron");function f(n,e){let r=n,i=0;for(;i<e.length;){let k=e[i];${step}let s=r[k];r=s,i++}return r}new h.BrowserWindow({webPreferences:{sandbox:!0}});h.ipcMain.handle(\`x:y\`,()=>1);`;
};
const expectElectronProbe = (
source: string,
): ReturnType<typeof analyzeJavaScriptStaticSource> => {
const analysis = analyzeJavaScriptStaticSource(source);
expect(analysis.parse_error_count).toBe(0);
expect(analysis.electron.browser_windows).toHaveLength(1);
expect(analysis.electron.ipc).toEqual(
expect.arrayContaining([
expect.objectContaining({ channel: "x:y", side: "main" }),
]),
);
expect(analysis.references).toEqual(
expect.arrayContaining([
expect.objectContaining({ kind: "require", specifier: "electron" }),
]),
);
return analysis;
};
it("keeps a possible defineProperty write unknown when alias selections are exhausted", () => {
const analysis = analyzeJavaScriptStaticSource(loopAliasedPropertyWalk(true));
expect(analysis.electron.browser_windows).toHaveLength(0);
expect(analysis.references).toEqual([
expect.objectContaining({ kind: "require", specifier: null }),
]);
expect(analysis.analysis_coverage).toMatchObject({
examination: {
semantic_resource_limits: expect.arrayContaining(["alias-selections"]),
},
});
});
it("keeps electron facts for the same path walk without defineProperty", () => {
expectElectronProbe(loopAliasedPropertyWalk(false));
});
// A strict check that exhausts the budget still examines queued sources by
// their selected keys, so the result does not depend on branch order.
const exhaustedWalkWrite = (receiver: string): string =>
`const m=module,g=()=>module;function f(n,e,flag){let r=n,i=0;for(;i<e.length;){let k=e[i];Object.prototype.hasOwnProperty.call(r,k)||Object.defineProperty(r,k,{value:{_errors:[]}});let s=r[k];r=s,i++}${receiver}.require=replacement;return r}let h=require("electron");new h.BrowserWindow({});`;
it.each([
"(flag?module:r)",
"(flag?r:module)",
"(flag?(0,module):r)",
"(flag?r:(0,module))",
"(flag?process.mainModule:r)",
"(flag?r:process.mainModule)",
"(flag?m:r)",
"(flag?r:m)",
"(flag?g():r)",
"(flag?r:g())",
])(
"revokes the host delegate through %s after alias selections are exhausted",
(receiver) => {
const analysis = analyzeJavaScriptStaticSource(
exhaustedWalkWrite(receiver),
);
expect(analysis.parse_error_count).toBe(0);
expect(analysis.electron.browser_windows).toHaveLength(0);
expect(analysis.references).toEqual([
expect.objectContaining({ kind: "require", specifier: null }),
]);
},
);
it.each([true, false])(
"does not certify a loader when the final drain leaves a module source pending (reversed: %s)",
(reversed) => {
const array = `[${Array.from({ length: 520 }, () => "0").join(",")},module][key]`;
const receiver = reversed ? `(flag?r:${array})` : `(flag?${array}:r)`;
const analysis = analyzeJavaScriptStaticSource(
exhaustedWalkWrite(receiver),
);
expect(analysis.electron.browser_windows).toHaveLength(0);
expect(analysis.references).toEqual([
expect.objectContaining({ kind: "require", specifier: null }),
]);
},
);
it.each([true, false])(
"keeps the loader unknown when identifier alternatives exhaust the final drain (reversed: %s)",
(reversed) => {
const array = `[${Array.from({ length: 520 }, () => "zero").join(",")},module][key]`;
const receiver = reversed ? `(flag?r:${array})` : `(flag?${array}:r)`;
const source = `const zero=0;${exhaustedWalkWrite(receiver)}`;
const analysis = analyzeJavaScriptStaticSource(source);
expect(analysis.references).toEqual([
expect.objectContaining({ kind: "require", specifier: null }),
]);
expect(
analysis.analysis_coverage?.examination.semantic_resource_limits,
).toContain("alias-selections");
},
);
it.each(["defineProperty", "assign"])(
"keeps a possible Object.%s write unknown when receiver analysis is incomplete",
(operation) => {
const array = `[${Array.from({ length: 520 }, () => "zero").join(",")},module][key]`;
const receiver = `(flag?${array}:r)`;
const write =
operation === "defineProperty"
? `Object.defineProperty(${receiver},"require",{value:replacement})`
: `Object.assign(${receiver},{require:replacement})`;
const source = `const zero=0;${exhaustedWalkWrite(receiver).replace(`${receiver}.require=replacement`, write)}`;
const analysis = analyzeJavaScriptStaticSource(source);
expect(analysis.parse_error_count).toBe(0);
expect(analysis.references).toEqual([
expect.objectContaining({ kind: "require", specifier: null }),
]);
expect(
analysis.analysis_coverage?.examination.semantic_resource_limits,
).toContain("alias-selections");
},
);
it.each(["(flag?process.foo:r)", "(flag?r:process.foo)"])(
"keeps the delegate unknown through %s after alias selections are exhausted",
(receiver) => {
const analysis = analyzeJavaScriptStaticSource(
exhaustedWalkWrite(receiver),
);
expect(analysis.parse_error_count).toBe(0);
expect(analysis.electron.browser_windows).toHaveLength(0);
expect(analysis.references).toEqual([
expect.objectContaining({ kind: "require", specifier: null }),
]);
expect(
analysis.analysis_coverage?.examination.semantic_resource_limits,
).toContain("alias-selections");
},
);