Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

module: disallow CJS <-> ESM edges in a cycle from require(esm) #52264

Merged
merged 2 commits into from
Apr 8, 2024

Conversation

joyeecheung
Copy link
Member

@joyeecheung joyeecheung commented Mar 29, 2024

This patch disallows CJS <-> ESM cycle edges when they come from require(esm) requested in ESM evalaution.

Drive-by: don't reuse the cache for imported CJS modules to stash source code of required ESM because the former is also used for cycle detection.

Fixes: #52145

@nodejs-github-bot
Copy link
Collaborator

Review requested:

  • @nodejs/loaders
  • @nodejs/vm

@joyeecheung joyeecheung added the request-ci Add this label to start a Jenkins CI on a PR. label Mar 29, 2024
@nodejs-github-bot nodejs-github-bot added lib / src Issues and PRs related to general changes in the lib or src directory. needs-ci PRs that need a full CI run. labels Mar 29, 2024
@github-actions github-actions bot removed the request-ci Add this label to start a Jenkins CI on a PR. label Mar 29, 2024
@nodejs-github-bot
Copy link
Collaborator

doc/api/errors.md Outdated Show resolved Hide resolved
doc/api/errors.md Outdated Show resolved Hide resolved
@joyeecheung joyeecheung added the request-ci Add this label to start a Jenkins CI on a PR. label Mar 31, 2024
@github-actions github-actions bot removed the request-ci Add this label to start a Jenkins CI on a PR. label Mar 31, 2024
@nodejs-github-bot
Copy link
Collaborator

@guybedford
Copy link
Contributor

Just to confirm - can this deal with the case where the require(esm) module is to a parent of a cycle that hasn't been executed yet? That is, the invariant should specifically be that none of the require(esm) module, nor any of its transitive dependencies are currently part of an ESM execution operation.

Consider the graph:

A (esm) -> B (esm)
B (esm) -> C (cjs)
C (cjs) -> Z (esm)
Z (esm) -> A (esm)

Where we import A first, and when the require(esm) gets Z it will be linked but not previously evaluated.

That is, my intuition here would be that a full upfront graph analysis must be done at require(esm) time to ensure acyclic behaviour.

This patch disallows CJS <-> ESM edges when they come from
require(esm) requested in ESM evalaution.

Drive-by: don't reuse the cache for imported CJS modules to stash
source code of required ESM because the former is also used for
cycle detection.
@joyeecheung
Copy link
Member Author

Where we import A first, and when the require(esm) gets Z it will be linked but not previously evaluated.

Yes this can be detected. Added a test.

@joyeecheung joyeecheung added the request-ci Add this label to start a Jenkins CI on a PR. label Apr 6, 2024
@github-actions github-actions bot removed the request-ci Add this label to start a Jenkins CI on a PR. label Apr 6, 2024
@nodejs-github-bot
Copy link
Collaborator

Copy link
Contributor

@guybedford guybedford left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for verifying the test case.

@nodejs-github-bot
Copy link
Collaborator

Comment on lines +359 to +364
function urlToFilename(url) {
if (url && StringPrototypeStartsWith(url, 'file://')) {
return fileURLToPath(url);
}
return url;
}
Copy link
Contributor

@aduh95 aduh95 Apr 7, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could use that util in more places:

return StringPrototypeStartsWith(resolvedURL, 'file://') ? fileURLToPath(resolvedURL) : resolvedURL;
const filename = StringPrototypeStartsWith(url, 'file://') ? fileURLToPath(url) : url;

Can happen in a follow-up PR.

@joyeecheung joyeecheung added commit-queue Add this label to land a pull request using GitHub Actions. commit-queue-squash Add this label to instruct the Commit Queue to squash all the PR commits into the first one. labels Apr 8, 2024
@nodejs-github-bot nodejs-github-bot removed the commit-queue Add this label to land a pull request using GitHub Actions. label Apr 8, 2024
@nodejs-github-bot nodejs-github-bot merged commit db17461 into nodejs:main Apr 8, 2024
59 checks passed
@nodejs-github-bot
Copy link
Collaborator

Landed in db17461

Copy link
Contributor

@JakobJingleheimer JakobJingleheimer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this, and especially thanks for the code docs!

Sorry, just realised this review was pending / never got submitted 😞

already being evaluated.

To avoid the cycle, the `require()` call involved in a cycle should not happen
at the top-level of either a ES Module (via `createRequire()`) or a CommonJS
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
at the top-level of either a ES Module (via `createRequire()`) or a CommonJS
at the top-level of either an ES Module (via `createRequire()`) or a CommonJS

parseCachedModule.loaded = true;
// If it's cached by the ESM loader as a way to indirectly pass
// the module in to avoid creating it twice, the loading request
// come from imported CJS. In that case use the importedCJSCache
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// come from imported CJS. In that case use the importedCJSCache
// came from imported CJS. In that case use the importedCJSCache

Comment on lines +260 to +264
* @param {import('../cjs/loader.js').Module} mod CJS module wrapper of the ESM.
* @param {string} filename Resolved filename of the module being require()'d
* @param {string} source Source code. TODO(joyeecheung): pass the raw buffer.
* @param {string} isMain Whether this module is a main module.
* @returns {ModuleNamespaceObject}
* @param {import('../cjs/loader.js').Module|undefined} parent Parent module, if any.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: we could DRY this up with a @typedef

Comment on lines +270 to +279
// This module job is already created:
// 1. If it was loaded by `require()` before, at this point the instantiation
// is already completed and we can check the whether it is in a cycle
// (in that case the module status is kEvaluaing), and whether the
// required graph is synchronous.
// 2. If it was loaded by `import` before, only allow it if it's already evaluated
// to forbid cycles.
// TODO(joyeecheung): ensure that imported synchronous graphs are evaluated
// synchronously so that any previously imported synchronous graph is already
// evaluated at this point.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

w00t! thanks for this!

}
throw new ERR_REQUIRE_CYCLE_MODULE(message);
}
// Othersie the module could be imported before but the evaluation may be already
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Othersie the module could be imported before but the evaluation may be already
// Otherwise the module could be imported before but the evaluation may be already

Comment on lines 355 to +356
const { responseURL, source } = loadResult;
let { format: finalFormat } = loadResult;
const { format: finalFormat } = loadResult;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: collapsable

Suggested change
const { responseURL, source } = loadResult;
let { format: finalFormat } = loadResult;
const { format: finalFormat } = loadResult;
const {
format: finalFormat,
responseURL,
source,
} = loadResult;

@marco-ippolito marco-ippolito added the backport-blocked-v20.x PRs that should land on the v20.x-staging branch but are blocked by another PR's pending backport. label May 23, 2024
joyeecheung added a commit to joyeecheung/node that referenced this pull request Jun 17, 2024
This patch disallows CJS <-> ESM edges when they come from
require(esm) requested in ESM evalaution.

Drive-by: don't reuse the cache for imported CJS modules to stash
source code of required ESM because the former is also used for
cycle detection.

PR-URL: nodejs#52264
Fixes: nodejs#52145
Reviewed-By: Geoffrey Booth <webadmin@geoffreybooth.com>
Reviewed-By: Guy Bedford <guybedford@gmail.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
backport-blocked-v20.x PRs that should land on the v20.x-staging branch but are blocked by another PR's pending backport. commit-queue-squash Add this label to instruct the Commit Queue to squash all the PR commits into the first one. lib / src Issues and PRs related to general changes in the lib or src directory. needs-ci PRs that need a full CI run.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Disallow ESM-CJS-ESM-ESM cycles when using require(esm)
7 participants