@@ -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
0 commit comments