Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 32 additions & 11 deletions fuzzing/replay/file_util.cc
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@

#include <cerrno>
#include <cstdio>
#include <set>
#include <string>
#include <utility>

#include "absl/functional/function_ref.h"
#include "absl/status/status.h"
Expand All @@ -36,7 +38,33 @@ namespace {

absl::Status TraverseDirectory(
absl::string_view path,
absl::FunctionRef<void(absl::string_view, const struct stat&)> callback) {
absl::FunctionRef<void(absl::string_view, const struct stat&)> callback,
std::set<std::pair<dev_t, ino_t>>& visited);

absl::Status YieldFilesInternal(
absl::string_view path,
absl::FunctionRef<void(absl::string_view, const struct stat&)> callback,
std::set<std::pair<dev_t, ino_t>>& 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<void(absl::string_view, const struct stat&)> callback,
std::set<std::pair<dev_t, ino_t>>& visited) {
DIR* dir = opendir(std::string(path).c_str());
if (!dir) {
return ErrnoStatus(absl::StrCat("could not open directory ", path), errno);
Expand All @@ -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;
Expand All @@ -69,15 +97,8 @@ absl::Status TraverseDirectory(
absl::Status YieldFiles(
absl::string_view path,
absl::FunctionRef<void(absl::string_view, const struct stat&)> 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<std::pair<dev_t, ino_t>> visited;
return YieldFilesInternal(path, callback, visited);
}

absl::Status SetFileContents(absl::string_view path,
Expand Down
40 changes: 40 additions & 0 deletions fuzzing/replay/file_util_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,11 @@

#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

#include <cerrno>
#include <cstdlib>
#include <cstring>
#include <functional>
#include <string>
#include <vector>
Expand Down Expand Up @@ -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<std::string> 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