diff --git a/fuzzing/replay/file_util.cc b/fuzzing/replay/file_util.cc index 477468d..f96eb39 100644 --- a/fuzzing/replay/file_util.cc +++ b/fuzzing/replay/file_util.cc @@ -21,7 +21,9 @@ #include #include +#include #include +#include #include "absl/functional/function_ref.h" #include "absl/status/status.h" @@ -36,7 +38,33 @@ namespace { absl::Status TraverseDirectory( absl::string_view path, - absl::FunctionRef callback) { + absl::FunctionRef callback, + std::set>& visited); + +absl::Status YieldFilesInternal( + absl::string_view path, + absl::FunctionRef callback, + std::set>& visited) { + struct stat path_stat; + if (stat(std::string(path).c_str(), &path_stat) < 0) { + return ErrnoStatus(absl::StrCat("could not stat ", path), errno); + } + if (S_ISDIR(path_stat.st_mode)) { + auto dir_id = std::make_pair(path_stat.st_dev, path_stat.st_ino); + // Prevent infinite recursion by tracking visited directories (dev,inode). + if (!visited.insert(dir_id).second) { + return absl::OkStatus(); + } + return TraverseDirectory(path, callback, visited); + } + callback(path, path_stat); + return absl::OkStatus(); +} + +absl::Status TraverseDirectory( + absl::string_view path, + absl::FunctionRef callback, + std::set>& visited) { DIR* dir = opendir(std::string(path).c_str()); if (!dir) { return ErrnoStatus(absl::StrCat("could not open directory ", path), errno); @@ -58,7 +86,7 @@ absl::Status TraverseDirectory( continue; } const std::string entry_path = absl::StrCat(path, "/", entry_name); - status.Update(YieldFiles(entry_path, callback)); + status.Update(YieldFilesInternal(entry_path, callback, visited)); } closedir(dir); return status; @@ -69,15 +97,8 @@ absl::Status TraverseDirectory( absl::Status YieldFiles( absl::string_view path, absl::FunctionRef callback) { - struct stat path_stat; - if (stat(std::string(path).c_str(), &path_stat) < 0) { - return ErrnoStatus(absl::StrCat("could not stat ", path), errno); - } - if (S_ISDIR(path_stat.st_mode)) { - return TraverseDirectory(path, callback); - } - callback(path, path_stat); - return absl::OkStatus(); + std::set> visited; + return YieldFilesInternal(path, callback, visited); } absl::Status SetFileContents(absl::string_view path, diff --git a/fuzzing/replay/file_util_test.cc b/fuzzing/replay/file_util_test.cc index 34ae35b..20d7a7e 100644 --- a/fuzzing/replay/file_util_test.cc +++ b/fuzzing/replay/file_util_test.cc @@ -16,8 +16,11 @@ #include #include +#include +#include #include +#include #include #include #include @@ -113,6 +116,43 @@ TEST(YieldFilesTest, YieldsHiddenFilesAndDirs) { EXPECT_THAT(collected_paths, testing::SizeIs(2)); } +TEST(YieldFilesTest, DoesNotRecurseThroughSymlinkLoop) { + const std::string root_dir = + absl::StrCat(getenv("TEST_TMPDIR"), "/symlink-loop-root"); + ASSERT_EQ(mkdir(root_dir.c_str(), 0755), 0); + const std::string dir_a = absl::StrCat(root_dir, "/dirA"); + ASSERT_EQ(mkdir(dir_a.c_str(), 0755), 0); + const std::string dir_b = absl::StrCat(root_dir, "/dirB"); + ASSERT_EQ(mkdir(dir_b.c_str(), 0755), 0); + + // Normal files that must each be yielded exactly once. + const std::string file_a = absl::StrCat(root_dir, "/a"); + const std::string file_b = absl::StrCat(root_dir, "/b"); + ASSERT_TRUE(SetFileContents(file_a, "foo").ok()); + ASSERT_TRUE(SetFileContents(file_b, "bar").ok()); + + // Build a symlink cycle: dirA/toB -> dirB and dirB/toA -> dirA. Without cycle + // detection, traversal would recurse forever (dirA -> dirB -> dirA -> ...). + const std::string link_to_b = absl::StrCat(dir_a, "/toB"); + if (symlink(dir_b.c_str(), link_to_b.c_str()) != 0) { + GTEST_SKIP() << "symlink unsupported in this environment: " + << std::strerror(errno); + } + const std::string link_to_a = absl::StrCat(dir_b, "/toA"); + ASSERT_EQ(symlink(dir_a.c_str(), link_to_a.c_str()), 0); + + std::vector collected_paths; + const absl::Status status = + YieldFiles(root_dir, CollectPathsCallback(&collected_paths)); + EXPECT_TRUE(status.ok()); + // Only the two regular files are yielded; directories (including those reached + // through the symlinks) never invoke the callback, and the cycle is visited + // at most once, so the result is fully deterministic regardless of readdir + // ordering. + EXPECT_THAT(collected_paths, + testing::UnorderedElementsAre(file_a, file_b)); +} + } // namespace } // namespace fuzzing