Skip to content

Commit d579020

Browse files
Avoid infinite loop through loop detection in link resolution
1 parent 3f4517c commit d579020

2 files changed

Lines changed: 29 additions & 3 deletions

File tree

src/internal/executables.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,13 +54,16 @@ export function resolveExecutable(
5454
}
5555

5656
try {
57-
while (true) {
57+
const seen = {};
58+
while (!hasOwn(seen, resolved)) {
59+
seen[resolved] = null;
5860
resolved = readlink(resolved);
5961
}
6062
} catch {
6163
// An error will be thrown if the executable is not a (sym)link, this is not
6264
// a problem so the error is ignored
65+
return resolved;
6366
}
6467

65-
return resolved;
68+
throw new Error(`${executable} points to a link loop, cannot resolve shell`);
6669
}

test/unit/executables/resolve.test.js

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ test("the executable exists and is a (sym)link", (t) => {
190190
t.context.deps.which.returns(resolvedExecutable);
191191

192192
t.context.deps.readlink.onCall(0).returns(linkedExecutable);
193-
t.context.deps.readlink.onCall(1).throws(linkedExecutable);
193+
t.context.deps.readlink.onCall(1).throws();
194194

195195
const result = resolveExecutable(args, t.context.deps);
196196
t.is(result, linkedExecutable);
@@ -224,3 +224,26 @@ test("the executable exists and is a (sym)link to a (sym)link", (t) => {
224224
t.is(t.context.deps.which.callCount, 1);
225225
t.is(t.context.deps.exists.callCount, 1);
226226
});
227+
228+
testProp(
229+
"the executable exists but there is a link cycle",
230+
[fc.array(fc.string({ minLength: 3 }), { minLength: 3, maxLength: 64 })],
231+
(t, links) => {
232+
const { env, executable, resolvedExecutable } = t.context;
233+
const args = { env, executable };
234+
235+
t.context.deps.exists.returns(true);
236+
t.context.deps.which.returns(resolvedExecutable);
237+
238+
t.context.deps.readlink = sinon.stub();
239+
for (const index in links) {
240+
t.context.deps.readlink.onCall(index).returns(links[index]);
241+
}
242+
t.context.deps.readlink.onCall(links.length).returns(links[0]);
243+
244+
t.throws(() => resolveExecutable(args, t.context.deps), {
245+
instanceOf: Error,
246+
message: `${executable} points to a link loop, cannot resolve shell`,
247+
});
248+
},
249+
);

0 commit comments

Comments
 (0)