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

fix(pnp): Avoid infinite loop in findApiPathFor #1298

Merged
merged 5 commits into from
May 6, 2020
Merged

fix(pnp): Avoid infinite loop in findApiPathFor #1298

merged 5 commits into from
May 6, 2020

Conversation

keithlayne
Copy link
Contributor

@keithlayne keithlayne commented May 5, 2020

What's the problem this PR addresses?

This is definitely a corner case. The error only manifests when
findApiPathFor is called with a paths option that includes the empty
string when the CWD does not contain a .pnp.js. The way that the
directory tree is iterated is to use path.dirname, with the root (/) as
the terminating case. path.dirname('') returns '.' as does
path.dirname('.'), so the loop never ends.

How did you fix it?

The fix is to resolve the path before trying to walk the tree.

@arcanis
Copy link
Member

arcanis commented May 5, 2020

Oh interesting! I think a better fix might be to call ppath.resolve on the path, otherwise the search will stop early. Can you do that and add a small test in pnp.test.js?

@keithlayne
Copy link
Contributor Author

yeah, just saw your message, took a while to clone :) Will do a test.

@keithlayne
Copy link
Contributor Author

BTW when done what should I pick for yarn version check -i? Or do you manage that more directly?

@keithlayne keithlayne marked this pull request as draft May 5, 2020 21:18
* Add failing test

* Fix infinite loop

  This is definitely a corner case.  The error only manifests when
  `findApiPathFor` is called with a `paths` option that includes the empty
  string when the CWD does not contain a `.pnp.js`.  The way that the
  directory tree is to use `path.dirname`, with the root (`/`) as the
  terminating case.  `path.dirname('')` returns `'.'` as does
  `path.dirname('.')`, so the loop never ends.

  The fix is to resolve the path before trying to walk the tree.

* Bump dependent versions
@keithlayne keithlayne changed the title Fix infinite loop in findApiPathFor fix(pnp): Avoid infinite loop in findApiPathFor May 5, 2020
@keithlayne
Copy link
Contributor Author

@arcanis Apparently this fix doesn't work on windows. Plus, the integration tests seem to be timing out correctly - when you run them locally (before the fix) they time out and jest exits. In CI it seems like (from the cancelled windows jobs) that everything runs fine, fails with a timeout, and then never terminates. I thought it was fairly safe to trigger the infinite loop in Jest because of it's timeout stuff.

Anyway, this fix doesn't seem to work on windows, and I don't know of an easy way for me to debug that. Logically though, toPortablePath('') just returns an empty string on both platforms, and then ppath.resolve('') should resolve to the working dir. Does path.posix.resolve somehow have different behavior on windows?

@keithlayne
Copy link
Contributor Author

@arcanis I think maybe the right thing to do here is:

let next = npath.toPortablePath(npath.resolve(modulePath));

i.e. resolve it natively before converting to a posix path. I can't imagine what's going on in windows (can't test) but can only assume things are acting differently there with the empty string.

@arcanis
Copy link
Member

arcanis commented May 6, 2020

I'm like 99% sure this is because path.posix.relative doesn't take into account that process.cwd() isn't a posix path when called on Windows. I've pushed a fix into the fslib, which I think is the right layer to normalize this kind of behavior.

BTW when done what should I pick for yarn version check -i? Or do you manage that more directly?

It's faster when contributors bump the versions, this way I often don't have to checkout the branch locally 😄 Note that in this case we don't need to bump aaaall the plugins, just the affected ones + the CLI (so that a new CLI bundle gets generated). Cf my fix.

@keithlayne
Copy link
Contributor Author

booyah. Sorry I didn't save you any time on this. But yeah, I was starting to think that the solution should have been at a higher level. Thanks for figuring that out!

@keithlayne keithlayne marked this pull request as ready for review May 6, 2020 22:59
@arcanis arcanis marked this pull request as ready for review May 6, 2020 22:59
@arcanis arcanis merged commit d3dd0e4 into yarnpkg:master May 6, 2020
@arcanis
Copy link
Member

arcanis commented May 6, 2020

Well, the bug was a quite devilish one! Thanks for digging in 🛠

@keithlayne keithlayne deleted the patch-1 branch May 6, 2020 23:01
mergify bot pushed a commit to aws/aws-cdk that referenced this pull request May 13, 2020
The `findNpmPackage` function in `runtime-info.ts` has a subtle and
mostly innocuous bug.  Its parent, `collectRuntimeInformation`, iterates
over the modules in `require.cache` and resolves the library version for
each.

It uses the `paths` property of each entry to locate the package
description for that module.  An example:

For `/foo/bar/aws-cdk/packages/aws-cdk/lib/index.js`:

```js
[ '/foo/aws-cdk/packages/aws-cdk/lib/node_modules',
'/foo/aws-cdk/packages/aws-cdk/node_modules',
'/foo/aws-cdk/packages/node_modules',
'/foo/aws-cdk/node_modules',
'/foo/node_modules',
'/node_modules' ]
```

`findNpmPackage` maps over these, and if the string ends with
`/node_modules` or `\\node_modules` it strips that off, ending with:

```js
[ '/foo/aws-cdk/packages/aws-cdk/lib',
'/foo/aws-cdk/packages/aws-cdk',
'/foo/aws-cdk/packages',
'/foo/aws-cdk',
'/foo',
'' ]
```

...and passes this array as the `paths` option to `require.resolve`.

The problem is that `''` is interpreted as the current working
directory, and instead of walking the file tree to the root (as was
probably intended) it appends whatever node's current working directory
is to the search.

It's hard to imagine this ever really having a negative impact on
package resolution, but unfortunately this triggered a likewise obscure
bug in yarn 2 when configured with PnP.  (See yarnpkg/berry#1298).  This
bug is fixed in yarn 2 master but hasn't been released yet.  The effect
of these two bugs when triggered is for the CDK CLI to hang.

This change seems to meet the intent of the code without having the
above problem.  It seems canonically correct and uses the `fs` lib more
idiomatically.  It's also less code :)

This passes all existing tests.  It's not clear how this would be tested
without exporting `findNpmPackage` and adding tests for it.

**CAVEAT:** I can't test this on windows, but it's worth noting the
following:

```js
// Node 12.16.3
path.win32.resolve('C:')
// => 'C:\\foo\\aws-cdk' (current working directory)
```

```js
// Node 10.13.0
path.win32.resolve('C:')
// => 'C:\\'
```

Both of these results come on OSX.  But it appears that the yarn bug
would not trigger on either version, while node may or may not have the
same wrong behavior for `findNpmVersion` on windows.
karupanerura pushed a commit to karupanerura/aws-cdk that referenced this pull request May 22, 2020
The `findNpmPackage` function in `runtime-info.ts` has a subtle and
mostly innocuous bug.  Its parent, `collectRuntimeInformation`, iterates
over the modules in `require.cache` and resolves the library version for
each.

It uses the `paths` property of each entry to locate the package
description for that module.  An example:

For `/foo/bar/aws-cdk/packages/aws-cdk/lib/index.js`:

```js
[ '/foo/aws-cdk/packages/aws-cdk/lib/node_modules',
'/foo/aws-cdk/packages/aws-cdk/node_modules',
'/foo/aws-cdk/packages/node_modules',
'/foo/aws-cdk/node_modules',
'/foo/node_modules',
'/node_modules' ]
```

`findNpmPackage` maps over these, and if the string ends with
`/node_modules` or `\\node_modules` it strips that off, ending with:

```js
[ '/foo/aws-cdk/packages/aws-cdk/lib',
'/foo/aws-cdk/packages/aws-cdk',
'/foo/aws-cdk/packages',
'/foo/aws-cdk',
'/foo',
'' ]
```

...and passes this array as the `paths` option to `require.resolve`.

The problem is that `''` is interpreted as the current working
directory, and instead of walking the file tree to the root (as was
probably intended) it appends whatever node's current working directory
is to the search.

It's hard to imagine this ever really having a negative impact on
package resolution, but unfortunately this triggered a likewise obscure
bug in yarn 2 when configured with PnP.  (See yarnpkg/berry#1298).  This
bug is fixed in yarn 2 master but hasn't been released yet.  The effect
of these two bugs when triggered is for the CDK CLI to hang.

This change seems to meet the intent of the code without having the
above problem.  It seems canonically correct and uses the `fs` lib more
idiomatically.  It's also less code :)

This passes all existing tests.  It's not clear how this would be tested
without exporting `findNpmPackage` and adding tests for it.

**CAVEAT:** I can't test this on windows, but it's worth noting the
following:

```js
// Node 12.16.3
path.win32.resolve('C:')
// => 'C:\\foo\\aws-cdk' (current working directory)
```

```js
// Node 10.13.0
path.win32.resolve('C:')
// => 'C:\\'
```

Both of these results come on OSX.  But it appears that the yarn bug
would not trigger on either version, while node may or may not have the
same wrong behavior for `findNpmVersion` on windows.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants