Add a subprocess API to use output callbacks. This is to be used by the Centipede adaptor. Also fix a minor issue of counting the unavailablity of the same file descriptor redundantly, which is incorrect. PiperOrigin-RevId: 763130600
diff --git a/fuzztest/BUILD b/fuzztest/BUILD index df77213..d7a6eeb 100644 --- a/fuzztest/BUILD +++ b/fuzztest/BUILD
@@ -784,8 +784,10 @@ deps = [ ":logging", "@abseil-cpp//absl/container:flat_hash_map", + "@abseil-cpp//absl/functional:function_ref", "@abseil-cpp//absl/strings", "@abseil-cpp//absl/time", + "@abseil-cpp//absl/types:span", ], )
diff --git a/fuzztest/CMakeLists.txt b/fuzztest/CMakeLists.txt index 64655c6..5de10c0 100644 --- a/fuzztest/CMakeLists.txt +++ b/fuzztest/CMakeLists.txt
@@ -756,7 +756,10 @@ DEPS fuzztest::logging absl::flat_hash_map + absl::function_ref + absl::span absl::strings + absl::string_view absl::time )
diff --git a/fuzztest/internal/centipede_adaptor.cc b/fuzztest/internal/centipede_adaptor.cc index a06abaf..273afa5 100644 --- a/fuzztest/internal/centipede_adaptor.cc +++ b/fuzztest/internal/centipede_adaptor.cc
@@ -332,12 +332,12 @@ int RunCentipede(const Environment& env, const std::optional<std::string>& centipede_command) { if (centipede_command.has_value()) { - std::string cmdline = *centipede_command; + std::string cmdline = "exec 2>&1 "; + absl::StrAppend(&cmdline, *centipede_command); for (const auto& flag : env.CreateFlags()) { absl::StrAppend(&cmdline, " "); absl::StrAppend(&cmdline, ShellEscape(flag)); } - absl::StrAppend(&cmdline, " 2>&1"); absl::FPrintF(GetStderr(), "[.] Running Centipede command %s\n", cmdline); FILE* pipe = popen(cmdline.c_str(), "r"); FUZZTEST_INTERNAL_CHECK(pipe != nullptr, "popen failed with errno %d",
diff --git a/fuzztest/internal/subprocess.cc b/fuzztest/internal/subprocess.cc index 959bcef..a6951a8 100644 --- a/fuzztest/internal/subprocess.cc +++ b/fuzztest/internal/subprocess.cc
@@ -30,12 +30,16 @@ #include <future> #include <string> +#include <utility> #include <vector> #include "absl/container/flat_hash_map.h" +#include "absl/functional/function_ref.h" #include "absl/strings/str_cat.h" +#include "absl/strings/string_view.h" #include "absl/time/clock.h" #include "absl/time/time.h" +#include "absl/types/span.h" #include "./fuzztest/internal/logging.h" namespace fuzztest::internal { @@ -58,8 +62,10 @@ // Helper class for running commands in a subprocess. class SubProcess { public: - RunResults Run( - const std::vector<std::string>& command_line, + TerminationStatus Run( + absl::Span<const std::string> command_line, + absl::FunctionRef<void(absl::string_view)> on_stdout_output, + absl::FunctionRef<void(absl::string_view)> on_stderr_output, const absl::flat_hash_map<std::string, std::string>& environment, absl::Duration timeout); @@ -70,11 +76,14 @@ posix_spawn_file_actions_t CreateChildFileActions(); void StartWatchdog(absl::Duration timeout); pid_t StartChild( - const std::vector<std::string>& command_line, + absl::Span<const std::string> command_line, const absl::flat_hash_map<std::string, std::string>& environment); - void ReadChildOutput(std::string* stdout_output, std::string* stderr_output); + void ReadChildOutput( + absl::FunctionRef<void(absl::string_view)> on_stdout_output, + absl::FunctionRef<void(absl::string_view)> on_stderr_output); - // Pipe file descriptors pairs. Index 0 is for stdout, index 1 is for stderr. + // Pipe file descriptors pairs. Index 0 is for stdout, index 1 is for + // stderr. static constexpr int kStdOutIdx = 0; static constexpr int kStdErrIdx = 1; int parent_pipe_[2]; @@ -147,7 +156,7 @@ // Do fork() and exec() in one step, using posix_spawnp(). pid_t SubProcess::StartChild( - const std::vector<std::string>& command_line, + absl::Span<const std::string> command_line, const absl::flat_hash_map<std::string, std::string>& environment) { posix_spawn_file_actions_t actions = CreateChildFileActions(); @@ -155,7 +164,7 @@ size_t argc = command_line.size(); std::vector<char*> argv(argc + 1); for (int i = 0; i < argc; i++) { - argv[i] = strdup(command_line[i].c_str()); + argv[i] = strndup(command_line[i].data(), command_line[i].size()); } argv[argc] = nullptr; @@ -189,17 +198,16 @@ return ((e == EINTR) || (e == EAGAIN) || (e == EWOULDBLOCK)); } -void SubProcess::ReadChildOutput(std::string* stdout_output, - std::string* stderr_output) { +void SubProcess::ReadChildOutput( + absl::FunctionRef<void(absl::string_view)> on_stdout_output, + absl::FunctionRef<void(absl::string_view)> on_stderr_output) { // Set up poll()-ing the pipes. constexpr int fd_count = 2; struct pollfd pfd[fd_count]; - std::string* out_str[fd_count]; for (int channel : {kStdOutIdx, kStdErrIdx}) { pfd[channel].fd = parent_pipe_[channel]; pfd[channel].events = POLLIN; pfd[channel].revents = 0; - out_str[channel] = channel == kStdOutIdx ? stdout_output : stderr_output; } // Loop reading stdout and stderr from the child process. @@ -213,14 +221,22 @@ FUZZTEST_INTERNAL_CHECK(false, "Impossible timeout: ", strerror(errno)); } else if (ret > 0) { for (int channel : {kStdOutIdx, kStdErrIdx}) { + // According to the poll() spec, use -1 for ignored entries. + if (pfd[channel].fd == -1) { + continue; + } if ((pfd[channel].revents & (POLLIN | POLLHUP)) != 0) { ssize_t n = read(pfd[channel].fd, buf, sizeof(buf)); if (n > 0) { - out_str[channel]->append(buf, n); + auto on_output = + channel == kStdOutIdx ? on_stdout_output : on_stderr_output; + on_output({buf, static_cast<size_t>(n)}); } else if ((n == 0) || !ShouldRetry(errno)) { + pfd[channel].fd = -1; fd_remain--; } } else if ((pfd[channel].revents & (POLLERR | POLLNVAL)) != 0) { + pfd[channel].fd = -1; fd_remain--; } } @@ -274,8 +290,10 @@ } // anonymous namespace -RunResults SubProcess::Run( - const std::vector<std::string>& command_line, +TerminationStatus SubProcess::Run( + absl::Span<const std::string> command_line, + absl::FunctionRef<void(absl::string_view)> on_stdout_output, + absl::FunctionRef<void(absl::string_view)> on_stderr_output, const absl::flat_hash_map<std::string, std::string>& environment, absl::Duration timeout) { CreatePipes(); @@ -283,17 +301,18 @@ CloseChildPipes(); std::future<int> status = std::async(std::launch::async, &WaitWithTimeout, child_pid, timeout); - std::string stdout_output, stderr_output; - ReadChildOutput(&stdout_output, &stderr_output); + ReadChildOutput(on_stdout_output, on_stderr_output); CloseParentPipes(); - return {TerminationStatus(status.get()), stdout_output, stderr_output}; + return TerminationStatus(status.get()); } #endif // !defined(_MSC_VER) && !(defined(__ANDROID_MIN_SDK_VERSION__) && // __ANDROID_MIN_SDK_VERSION__ < 28) -RunResults RunCommand( - const std::vector<std::string>& command_line, +TerminationStatus RunCommandWithOutputCallbacks( + absl::Span<const std::string> command_line, + absl::FunctionRef<void(absl::string_view)> on_stdout_output, + absl::FunctionRef<void(absl::string_view)> on_stderr_output, const absl::flat_hash_map<std::string, std::string>& environment, absl::Duration timeout) { #if defined(_MSC_VER) @@ -305,8 +324,23 @@ "Subprocess library not implemented on older Android NDK versions yet"); #else SubProcess proc; - return proc.Run(command_line, environment, timeout); + return proc.Run(command_line, on_stdout_output, on_stderr_output, environment, + timeout); #endif } +RunResults RunCommand( + absl::Span<const std::string> command_line, + const absl::flat_hash_map<std::string, std::string>& environment, + absl::Duration timeout) { + std::string stdout; + std::string stderr; + auto status = RunCommandWithOutputCallbacks( + command_line, + [&stdout](absl::string_view output) { stdout.append(output); }, + [&stderr](absl::string_view output) { stderr.append(output); }, + environment, timeout); + return {std::move(status), std::move(stdout), std::move(stderr)}; +} + } // namespace fuzztest::internal
diff --git a/fuzztest/internal/subprocess.h b/fuzztest/internal/subprocess.h index e3f080d..8d70020 100644 --- a/fuzztest/internal/subprocess.h +++ b/fuzztest/internal/subprocess.h
@@ -21,7 +21,10 @@ #include <vector> #include "absl/container/flat_hash_map.h" +#include "absl/functional/function_ref.h" +#include "absl/strings/string_view.h" #include "absl/time/time.h" +#include "absl/types/span.h" namespace fuzztest::internal { @@ -93,11 +96,24 @@ std::string stderr_output; }; -// Runs `command_line` in a subprocess. Environment variables can be set via +// Runs `command_line` in a subprocess and passes through its stdout/stderr to +// `on_stdout_output` and `on_stderr_output` callbacks. Environment variables +// can be set via `environment`. If optional `timeout` is provided, the process +// is terminated after the given timeout. The timeout will be rounded up to +// seconds. +TerminationStatus RunCommandWithOutputCallbacks( + absl::Span<const std::string> command_line, + absl::FunctionRef<void(absl::string_view)> on_stdout_output, + absl::FunctionRef<void(absl::string_view)> on_stderr_output, + const absl::flat_hash_map<std::string, std::string>& environment = {}, + absl::Duration timeout = absl::InfiniteDuration()); + +// Runs `command_line` in a subprocess and returns the run results that captures +// the stdout/stderr as strings. Environment variables can be set via // `environment`. If optional `timeout` is provided, the process is terminated // after the given timeout. The timeout will be rounded up to seconds. RunResults RunCommand( - const std::vector<std::string>& command_line, + absl::Span<const std::string> command_line, const absl::flat_hash_map<std::string, std::string>& environment = {}, absl::Duration timeout = absl::InfiniteDuration());
diff --git a/fuzztest/internal/subprocess_test.cc b/fuzztest/internal/subprocess_test.cc index bfb3c3a..844c0bc 100644 --- a/fuzztest/internal/subprocess_test.cc +++ b/fuzztest/internal/subprocess_test.cc
@@ -57,6 +57,16 @@ EXPECT_THAT(std_err, HasSubstr("command not found")); } +TEST(SubProcessTest, StdErrIsCapturedIfStdOutIsClosedEarly) { + auto [status, std_out, std_err] = RunCommand( + {"bash", "-c", + "exec >&- bash -c 'sleep 1; echo some stderr output >&2; exit 0'"}); + EXPECT_TRUE(status.Exited()); + EXPECT_EQ(status, ExitCode(0)); + EXPECT_EQ(std_out, ""); + EXPECT_THAT(std_err, HasSubstr("some stderr output")); +} + TEST(SubProcessTest, CrashesWithWrongArguments) { EXPECT_DEATH(RunCommand({"not-a-binary"}), "Cannot spawn child process"); }