Skip to content
Permalink
Browse files

Don't loop forever when faced with circular symlinks

We faced this issue when running Webpack on a Windows Concourse CI
(https://concourse-ci.org) worker.

In Windows, Concourse builds happen in directories that look like this:

```
C:\concourse\work\volumes\live\e553aae5-ad96-4632-4970-959e0c11924b\volumes
```

On top of that, from within a build, Concourse symlinks C:\ to that same
volume directory that lives inside it. This means that when the
`SymlinkPlugin` kicks in, it will do something like this:

```
READLINK C:\concourse\work\volumes\live\e553aae5-ad96-4632-4970-959e0c11924b\volumes -> undefined
READLINK C:\concourse\work\volumes\live\e553aae5-ad96-4632-4970-959e0c11924b -> undefined
READLINK C:\concourse\work\volumes\live -> undefined
READLINK C:\concourse\work\volumes -> undefined
READLINK C:\concourse\work -> undefined
READLINK C:\concourse -> undefined
READLINK C: -> C:\concourse\work\volumes\live\e553aae5-ad96-4632-4970-959e0c11924b\volumes
```

Therefore, the directory will be appended to itself over and over again,
leading to something like this:

```
C:\concourse\work\volumes\live\e553aae5-ad96-4632-4970-959e0c11924b\volumes\concourse\work\volumes\live\e553aae5-ad96-4632-4970-959e0c11924b\volumes\concourse\work\volumes\live\e553aae5-ad96-4632-4970-959e0c11924b\volumes\concourse\work\volumes\live\e553aae5-ad96-4632-4970-959e0c11924b\volumes
```

The program will loop forever, until the host eventually runs out of
memory and crashes.

The fix is to include a set that tracks the visited directories, and
break the loop if we're going back to one of those.

Signed-off-by: Juan Cruz Viotti <jv@jviotti.com>
  • Loading branch information...
jviotti committed Mar 27, 2018
1 parent e42b8ad commit b4a6a32e7b28b8105e1d47df449e7d1bfb25e528
Showing with 69 additions and 0 deletions.
  1. +12 −0 lib/SymlinkPlugin.js
  2. +57 −0 test/SymlinkPlugin.js
@@ -22,15 +22,27 @@ module.exports = class SymlinkPlugin {
const paths = pathsResult.paths;

let containsSymlink = false;
const visitedPaths = new Set();

forEachBail.withIndex(paths, (path, idx, callback) => {
fs.readlink(path, (err, result) => {
if(!err && result) {
// Avoid circular links
if(visitedPaths.has(result)) {
return callback();
}

pathSeqments[idx] = result;
containsSymlink = true;
// Shortcut when absolute symlink found
if(/^(\/|[a-zA-z]:($|\\))/.test(result))
return callback(null, idx);
}

if(!err) {
visitedPaths.add(path);
}

callback();
});
}, (err, idx) => {
@@ -0,0 +1,57 @@
var fs = require("graceful-fs");
var pathModule = require("path");
var SymlinkPlugin = require("../lib/SymlinkPlugin");
var ResolverFactory = require("../lib/ResolverFactory");

describe("SymlinkPlugin: circular links", function() {
var resolver = ResolverFactory.createResolver({
extensions: [".js"],
fileSystem: {
stat: function(path, callback) {
if(path === "C:\\foo\\bar\\baz\\qux\\node_modules") {
return fs.stat(pathModule.join(__dirname, "..", "node_modules"), callback);
}

if(path === "C:\\foo\\bar\\baz\\qux\\lib\\app.js") {
return fs.stat(pathModule.join(__dirname, "..", "lib", "node.js"), callback);
}

fs.stat(path, callback);
},
readFile: function(path, options, callback) {
if(typeof options === "function") {
callback = options;
options = {};
}

if(path === "C:\\foo\\bar\\baz\\qux\\package.json") {
return callback(null, JSON.stringify(require("../package.json")));
}

if(path === "C:\\foo\\bar\\baz\\qux\\lib\\app.js") {
return callback(null, "console.log('foo')");
}

fs.readFile(path, options, callback);
},
readlink: function(path, options, callback) {
if(typeof options === "function") {
callback = options;
options = {};
}

if(path === "C:") {
return callback(null, "C:\\foo\\bar\\baz\\qux");
}

return callback();
}
}
});

it("should not loop forever", function(done) {
const plugin = new SymlinkPlugin("file", "relative");
plugin.apply(resolver);
resolver.resolve({}, "C:\\foo\\bar\\baz\\qux", ".\\lib\\app.js", {}, done);
});
});

0 comments on commit b4a6a32

Please sign in to comment.
You can’t perform that action at this time.