Skip to content

Commit d355743

Browse files
jasnellnpaun
andauthored
Limit VFS symlink depth (#6485)
Co-authored-by: Nicholas Paun <npaun@cloudflare.com>
1 parent 862a428 commit d355743

2 files changed

Lines changed: 50 additions & 0 deletions

File tree

src/workerd/io/bundle-fs-test.c++

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,5 +151,49 @@ KJ_TEST("Guarding against circular symlinks works") {
151151
KJ_EXPECT(vfs->resolve(env.js, "file:///c"_url) == kj::none);
152152
});
153153
}
154+
155+
KJ_TEST("Guarding against deep non-circular symlink chains works") {
156+
// Regression test: a deep chain of distinct symlinks (no cycle) must not
157+
// cause a stack overflow. The recursion guard should reject the chain once
158+
// the depth limit is exceeded.
159+
TestFixture fixture;
160+
161+
fixture.runInIoContext([&](const TestFixture::Environment& env) {
162+
auto vfs = newVirtualFileSystem(kj::heap<FsMap>(), getTmpDirectoryImpl());
163+
164+
auto maybeTemp = KJ_ASSERT_NONNULL(vfs->resolve(env.js, "file:///"_url));
165+
auto& tempDir = maybeTemp.get<kj::Rc<Directory>>();
166+
167+
// Create a target file at the end of the chain.
168+
auto targetResult = KJ_ASSERT_NONNULL(tempDir->tryOpen(
169+
env.js, kj::Path({"target"}), Directory::OpenOptions{.createAs = FsType::FILE}));
170+
auto& targetFile = targetResult.get<kj::Rc<File>>();
171+
KJ_EXPECT(targetFile->write(env.js, 0, "hello"_kjb).get<uint32_t>() == 5);
172+
173+
// Build a chain of 300 distinct symlinks: link_0 -> link_1 -> ... -> link_299 -> target.
174+
// This exceeds the depth limit (256) without forming a cycle.
175+
constexpr int chainLen = 300;
176+
for (int i = chainLen - 1; i >= 0; i--) {
177+
kj::String target =
178+
i == chainLen - 1 ? kj::str("file:///target") : kj::str("file:///link_", i + 1);
179+
auto targetUrl = KJ_ASSERT_NONNULL(jsg::Url::tryParse(target.asPtr()));
180+
KJ_EXPECT(tempDir->add(env.js, kj::str("link_", i),
181+
vfs->newSymbolicLink(env.js, targetUrl)) == kj::none);
182+
}
183+
184+
// Resolving a link near the end of the chain should succeed (under the limit).
185+
KJ_ASSERT_NONNULL(vfs->resolve(env.js, "file:///link_299"_url));
186+
187+
// Resolving from the start of the chain must fail with SYMLINK_DEPTH_EXCEEDED,
188+
// not crash with a stack overflow.
189+
auto resolved = KJ_ASSERT_NONNULL(vfs->resolve(env.js, "file:///link_0"_url));
190+
KJ_EXPECT(resolved.get<workerd::FsError>() == workerd::FsError::SYMLINK_DEPTH_EXCEEDED);
191+
192+
// stat should also fail gracefully.
193+
auto resolvedStat = KJ_ASSERT_NONNULL(vfs->resolveStat(env.js, "file:///link_0"_url));
194+
KJ_EXPECT(resolvedStat.get<workerd::FsError>() == workerd::FsError::SYMLINK_DEPTH_EXCEEDED);
195+
});
196+
}
197+
154198
} // namespace
155199
} // namespace workerd

src/workerd/io/worker-fs.c++

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ bool setCurrentWorkingDirectory(kj::Path newCwd) {
4040
}
4141

4242
namespace {
43+
// Maximum distinct symlinks that can be traversed during a single resolution.
44+
static constexpr size_t kMaxSymlinkDepth = 256;
45+
4346
// The SymbolicLinkRecursionGuardScope is used on-stack to guard against
4447
// circular symbolic links. As soon as a cycle is detected, it throws.
4548
// Since resolution is always synchronous, we can use thread-local to
@@ -1439,6 +1442,9 @@ kj::Maybe<FsError> SymbolicLinkRecursionGuardScope::checkSeen(SymbolicLink* link
14391442
return FsError::SYMLINK_DEPTH_EXCEEDED;
14401443
}
14411444
guard.linksSeen.insert(link);
1445+
if (guard.linksSeen.size() > kMaxSymlinkDepth) {
1446+
return FsError::SYMLINK_DEPTH_EXCEEDED;
1447+
}
14421448
return kj::none;
14431449
}
14441450

0 commit comments

Comments
 (0)