fix(javascript): refuse Webpack loaders that can fall off the end (#6007)

* fix(javascript): refuse Webpack loaders that can fall off the end

The bootstrap integrity proof accepted any loader whose explicit returns
all yielded the module's exports. A loader with no returns, or with only
the cache-hit return, passed vacuously even though its miss path returns
undefined, so a bundled `n(45840).BrowserWindow` became an exact Electron
window.

Require that the loader body cannot complete normally before the proof
checks its returns, so the bundled-call identity stays unresolved while
the literal require("electron") reference is preserved.

Fixes #5958

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(javascript): cover finally completion in Webpack loader proofs

Group the loader completion-path cases in their own suite and add
focused finally cases: a completing finally keeps a falling-through
catch unproven, a finally returning another value stays unresolved, and
a finally returning the exports proves the loader.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(javascript): keep IPC checks for unresolved Webpack loader cases

Share the unresolved-identity assertion so the completion-path cases
also reject directly attributed IPC observations.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kaoru0822
2026-10-21 07:53:42 -07:00
committed by GitHub
parent c8d3f8f8ec
commit 63b43b69bb
3 changed files with 173 additions and 34 deletions
@@ -30,11 +30,12 @@ interface LoaderShape {
/**
* Prove that `loader(key)` returns the exports of the factory declared at
* `table[key]`: the loader returns only the dispatched module's exports or a
* cache entry it created for that module, the table has only literal keys,
* and the table and cache are referenced only by the loader or through the
* runtime's `loader.m`/`loader.c` exposure. Literal-key writes through that
* exposure replace their slot. Returns undefined when any of this is unproven.
* `table[key]`: every normal completion of the loader returns the dispatched
* module's exports or a cache entry it created for that module, the table has
* only literal keys, and the table and cache are referenced only by the loader
* or through the runtime's `loader.m`/`loader.c` exposure. Literal-key writes
* through that exposure replace their slot. Returns undefined when any of this
* is unproven.
*/
export const proveJavaScriptBootstrapIntegrity = (
call: t.CallExpression,
@@ -74,12 +75,14 @@ const loaderShape = (
const parameterId = t.isIdentifier(parameter)
? bindingOf(state, parameter)
: undefined;
// A rewritten key would dispatch a different slot than the caller named.
// A rewritten key would dispatch a different slot than the caller named,
// and falling off the end returns undefined instead of the exports.
if (
parameterId === undefined ||
state.bindingsById.get(parameterId)?.definitions.length !== 1 ||
loader.async ||
loader.generator
loader.generator ||
completesNormally(loader.body)
)
return undefined;
const dispatches = new Set<t.Node>();
@@ -129,6 +132,35 @@ const loaderShape = (
: undefined;
};
/**
* Whether control may reach the end of `statement`. Loops, switches and labels
* can absorb a `break`, so they are assumed to complete.
*/
const completesNormally = (statement: t.Statement): boolean => {
if (t.isReturnStatement(statement) || t.isThrowStatement(statement))
return false;
if (t.isBlockStatement(statement))
return statement.body.every(completesNormally);
if (t.isIfStatement(statement))
return (
statement.alternate === null ||
statement.alternate === undefined ||
completesNormally(statement.consequent) ||
completesNormally(statement.alternate)
);
if (t.isTryStatement(statement))
return (
(statement.finalizer === null ||
statement.finalizer === undefined ||
completesNormally(statement.finalizer)) &&
(completesNormally(statement.block) ||
(statement.handler !== null &&
statement.handler !== undefined &&
completesNormally(statement.handler.body)))
);
return true;
};
/** `table[key](module, ...)` or `table[key].call(thisArg, module, ...)`. */
const tableDispatch = (
call: t.CallExpression,