Fix base::Subprocess move ctor and operator=
base::Subprocess move ctor/operator= had a classic std::move bug.
The Subprocess dtor kills the process if pid_ is non-zero.
std::move() doesn't clear pid_ because it's an int.
As a matter of facts, in some cases (when the compiler doesn't elide
a move because non trivial) the moved-from object would kill the
process being moved.
Bug: 162505491
Test: perfetto_unittests --gtest_filter=SubprocessTest
Change-Id: Ia08222a8a43959ecb65a8f166c1b0db39d3073b6
diff --git a/include/perfetto/ext/base/subprocess.h b/include/perfetto/ext/base/subprocess.h
index b61e02f..d97ba5b 100644
--- a/include/perfetto/ext/base/subprocess.h
+++ b/include/perfetto/ext/base/subprocess.h
@@ -186,13 +186,18 @@
// Sends a SIGKILL and wait to see the process termination.
void KillAndWaitForTermination();
- PlatformProcessId pid() const { return pid_; }
- Status status() const { return status_; }
- int returncode() const { return returncode_; }
+ PlatformProcessId pid() const { return s_.pid; }
+
+ // The accessors below are updated only after a call to Poll(), Wait() or
+ // KillAndWaitForTermination().
+ // In most cases you want to call Poll() rather than these accessors.
+
+ Status status() const { return s_.status; }
+ int returncode() const { return s_.returncode; }
// This contains both stdout and stderr (if the corresponding _mode ==
// kBuffer). It's non-const so the caller can std::move() it.
- std::string& output() { return output_; }
+ std::string& output() { return s_.output; }
Args args;
@@ -205,15 +210,21 @@
void KillAtMostOnce();
bool PollInternal(int poll_timeout_ms);
- base::Pipe stdin_pipe_;
- base::Pipe stdouterr_pipe_;
- base::Pipe exit_status_pipe_;
- PlatformProcessId pid_;
- size_t input_written_ = 0;
- Status status_ = kNotStarted;
- int returncode_ = -1;
- std::string output_; // Stdin+stderr. Only when kBuffer.
- std::thread waitpid_thread_;
+ // This is to deal robustly with the move operators, without having to
+ // manually maintain member-wise move instructions.
+ struct MovableState {
+ base::Pipe stdin_pipe;
+ base::Pipe stdouterr_pipe;
+ base::Pipe exit_status_pipe;
+ PlatformProcessId pid;
+ size_t input_written = 0;
+ Status status = kNotStarted;
+ int returncode = -1;
+ std::string output; // Stdin+stderr. Only when kBuffer.
+ std::thread waitpid_thread;
+ };
+
+ MovableState s_;
};
} // namespace base
diff --git a/src/base/subprocess.cc b/src/base/subprocess.cc
index 6be24bd..0e1fbbd 100644
--- a/src/base/subprocess.cc
+++ b/src/base/subprocess.cc
@@ -27,6 +27,7 @@
#include <algorithm>
#include <thread>
+#include <tuple>
#include "perfetto/base/build_config.h"
#include "perfetto/base/logging.h"
@@ -172,16 +173,30 @@
Subprocess::Args::Args(Args&&) noexcept = default;
Subprocess::Args& Subprocess::Args::operator=(Args&&) = default;
-Subprocess::Subprocess(std::initializer_list<std::string> _args)
- : args(_args) {}
+Subprocess::Subprocess(std::initializer_list<std::string> a) : args(a) {}
-Subprocess::Subprocess(Subprocess&&) noexcept = default;
-Subprocess& Subprocess::operator=(Subprocess&&) = default;
+Subprocess::Subprocess(Subprocess&& other) noexcept {
+ static_assert(sizeof(Subprocess) == sizeof(std::tuple<MovableState, Args>),
+ "base::Subprocess' move ctor needs updating");
+ s_ = std::move(other.s_);
+ args = std::move(other.args);
+
+ // Reset the state of the moved-from object.
+ other.s_.status = kNotStarted; // So the dtor doesn't try to kill().
+ other.~Subprocess();
+ new (&other) Subprocess();
+}
+
+Subprocess& Subprocess::operator=(Subprocess&& other) {
+ this->~Subprocess();
+ new (this) Subprocess(std::move(other));
+ return *this;
+}
Subprocess::~Subprocess() {
- if (status_ == kRunning)
+ if (s_.status == kRunning)
KillAndWaitForTermination();
- PERFETTO_CHECK(!waitpid_thread_.joinable());
+ PERFETTO_CHECK(!s_.waitpid_thread.joinable());
}
void Subprocess::Start() {
@@ -207,42 +222,42 @@
}
// Setup the pipes for stdin/err redirection.
- stdin_pipe_ = base::Pipe::Create(base::Pipe::kWrNonBlock);
- proc_args.stdin_pipe_rd = *stdin_pipe_.rd;
- stdouterr_pipe_ = base::Pipe::Create(base::Pipe::kRdNonBlock);
- proc_args.stdouterr_pipe_wr = *stdouterr_pipe_.wr;
+ s_.stdin_pipe = base::Pipe::Create(base::Pipe::kWrNonBlock);
+ proc_args.stdin_pipe_rd = *s_.stdin_pipe.rd;
+ s_.stdouterr_pipe = base::Pipe::Create(base::Pipe::kRdNonBlock);
+ proc_args.stdouterr_pipe_wr = *s_.stdouterr_pipe.wr;
// Spawn the child process that will exec().
- pid_ = fork();
- PERFETTO_CHECK(pid_ >= 0);
- if (pid_ == 0) {
+ s_.pid = fork();
+ PERFETTO_CHECK(s_.pid >= 0);
+ if (s_.pid == 0) {
// Close the parent-ends of the pipes.
- stdin_pipe_.wr.reset();
- stdouterr_pipe_.rd.reset();
+ s_.stdin_pipe.wr.reset();
+ s_.stdouterr_pipe.rd.reset();
ChildProcess(&proc_args);
// ChildProcess() doesn't return, not even in case of failures.
PERFETTO_FATAL("not reached");
}
- status_ = kRunning;
+ s_.status = kRunning;
// Close the child-end of the pipes.
- // Deliberately NOT closing the stdin_pipe_.rd. This is to avoid crashing
+ // Deliberately NOT closing the s_.stdin_pipe.rd. This is to avoid crashing
// with a SIGPIPE if the process exits without consuming its stdin, while
// the parent tries to write() on the other end of the stdin pipe.
- stdouterr_pipe_.wr.reset();
+ s_.stdouterr_pipe.wr.reset();
// Spawn a thread that is blocked on waitpid() and writes the termination
// status onto a pipe. The problem here is that waipid() doesn't have a
// timeout option and can't be passed to poll(). The alternative would be
// using a SIGCHLD handler, but anecdotally signal handlers introduce more
// problems than what they solve.
- exit_status_pipe_ = base::Pipe::Create(base::Pipe::kRdNonBlock);
+ s_.exit_status_pipe = base::Pipe::Create(base::Pipe::kRdNonBlock);
// Both ends of the pipe are closed after the thread.join().
- int pid = pid_;
- int exit_status_pipe_wr = exit_status_pipe_.wr.release();
- waitpid_thread_ = std::thread([pid, exit_status_pipe_wr] {
+ int pid = s_.pid;
+ int exit_status_pipe_wr = s_.exit_status_pipe.wr.release();
+ s_.waitpid_thread = std::thread([pid, exit_status_pipe_wr] {
int pid_stat = -1;
int wait_res = PERFETTO_EINTR(waitpid(pid, &pid_stat, 0));
PERFETTO_CHECK(wait_res == pid);
@@ -253,11 +268,11 @@
}
Subprocess::Status Subprocess::Poll() {
- if (status_ != kRunning)
- return status_; // Nothing to poll.
+ if (s_.status != kRunning)
+ return s_.status; // Nothing to poll.
while (PollInternal(0 /* don't block*/)) {
}
- return status_;
+ return s_.status;
}
// |timeout_ms| semantic:
@@ -270,18 +285,18 @@
bool Subprocess::PollInternal(int poll_timeout_ms) {
struct pollfd fds[3]{};
size_t num_fds = 0;
- if (exit_status_pipe_.rd) {
- fds[num_fds].fd = *exit_status_pipe_.rd;
+ if (s_.exit_status_pipe.rd) {
+ fds[num_fds].fd = *s_.exit_status_pipe.rd;
fds[num_fds].events = POLLIN;
num_fds++;
}
- if (stdouterr_pipe_.rd) {
- fds[num_fds].fd = *stdouterr_pipe_.rd;
+ if (s_.stdouterr_pipe.rd) {
+ fds[num_fds].fd = *s_.stdouterr_pipe.rd;
fds[num_fds].events = POLLIN;
num_fds++;
}
- if (stdin_pipe_.wr) {
- fds[num_fds].fd = *stdin_pipe_.wr;
+ if (s_.stdin_pipe.wr) {
+ fds[num_fds].fd = *s_.stdin_pipe.wr;
fds[num_fds].events = POLLOUT;
num_fds++;
}
@@ -301,7 +316,7 @@
}
bool Subprocess::Wait(int timeout_ms) {
- PERFETTO_CHECK(status_ != kNotStarted);
+ PERFETTO_CHECK(s_.status != kNotStarted);
// Break out of the loop only after both conditions are satisfied:
// - All stdout/stderr data has been read (if kBuffer).
@@ -315,7 +330,7 @@
// state where the write(stdin_pipe_.wr) will never unblock.
const int64_t t_start = base::GetWallTimeMs().count();
- while (exit_status_pipe_.rd || stdouterr_pipe_.rd) {
+ while (s_.exit_status_pipe.rd || s_.stdouterr_pipe.rd) {
int poll_timeout_ms = -1; // Block until a FD is ready.
if (timeout_ms > 0) {
const int64_t now = GetWallTimeMs().count();
@@ -329,42 +344,42 @@
}
bool Subprocess::Call(int timeout_ms) {
- PERFETTO_CHECK(status_ == kNotStarted);
+ PERFETTO_CHECK(s_.status == kNotStarted);
Start();
if (!Wait(timeout_ms)) {
KillAndWaitForTermination();
// TryReadExitStatus must have joined the thread.
- PERFETTO_DCHECK(!waitpid_thread_.joinable());
+ PERFETTO_DCHECK(!s_.waitpid_thread.joinable());
}
- PERFETTO_DCHECK(status_ != kRunning);
- return status_ == kExited && returncode_ == 0;
+ PERFETTO_DCHECK(s_.status != kRunning);
+ return s_.status == kExited && s_.returncode == 0;
}
void Subprocess::TryReadExitStatus() {
- if (!exit_status_pipe_.rd)
+ if (!s_.exit_status_pipe.rd)
return;
int pid_stat = -1;
- int64_t rsize =
- PERFETTO_EINTR(read(*exit_status_pipe_.rd, &pid_stat, sizeof(pid_stat)));
+ int64_t rsize = PERFETTO_EINTR(
+ read(*s_.exit_status_pipe.rd, &pid_stat, sizeof(pid_stat)));
if (rsize < 0 && errno == EAGAIN)
return;
if (rsize > 0) {
PERFETTO_CHECK(rsize == sizeof(pid_stat));
} else if (rsize < 0) {
- PERFETTO_PLOG("Subprocess read(exit_status_pipe_) failed");
+ PERFETTO_PLOG("Subprocess read(s_.exit_status_pipe) failed");
}
- waitpid_thread_.join();
- exit_status_pipe_.rd.reset();
+ s_.waitpid_thread.join();
+ s_.exit_status_pipe.rd.reset();
if (WIFEXITED(pid_stat)) {
- returncode_ = WEXITSTATUS(pid_stat);
- status_ = kExited;
+ s_.returncode = WEXITSTATUS(pid_stat);
+ s_.status = kExited;
} else if (WIFSIGNALED(pid_stat)) {
- returncode_ = 128 + WTERMSIG(pid_stat); // Follow bash convention.
- status_ = kKilledBySignal;
+ s_.returncode = 128 + WTERMSIG(pid_stat); // Follow bash convention.
+ s_.status = kKilledBySignal;
} else {
PERFETTO_FATAL("waitpid() returned an unexpected value (0x%x)", pid_stat);
}
@@ -372,51 +387,51 @@
// If the stidn pipe is still open, push input data and close it at the end.
void Subprocess::TryPushStdin() {
- if (!stdin_pipe_.wr)
+ if (!s_.stdin_pipe.wr)
return;
- PERFETTO_DCHECK(args.input.empty() || input_written_ < args.input.size());
+ PERFETTO_DCHECK(args.input.empty() || s_.input_written < args.input.size());
if (args.input.size()) {
int64_t wsize =
- PERFETTO_EINTR(write(*stdin_pipe_.wr, &args.input[input_written_],
- args.input.size() - input_written_));
+ PERFETTO_EINTR(write(*s_.stdin_pipe.wr, &args.input[s_.input_written],
+ args.input.size() - s_.input_written));
if (wsize < 0 && errno == EAGAIN)
return;
if (wsize >= 0) {
// Whether write() can return 0 is one of the greatest mysteries of UNIX.
// Just ignore it.
- input_written_ += static_cast<size_t>(wsize);
+ s_.input_written += static_cast<size_t>(wsize);
} else {
PERFETTO_PLOG("Subprocess write(stdin) failed");
- stdin_pipe_.wr.reset();
+ s_.stdin_pipe.wr.reset();
}
}
- PERFETTO_DCHECK(input_written_ <= args.input.size());
- if (input_written_ == args.input.size())
- stdin_pipe_.wr.reset(); // Close stdin.
+ PERFETTO_DCHECK(s_.input_written <= args.input.size());
+ if (s_.input_written == args.input.size())
+ s_.stdin_pipe.wr.reset(); // Close stdin.
}
void Subprocess::TryReadStdoutAndErr() {
- if (!stdouterr_pipe_.rd)
+ if (!s_.stdouterr_pipe.rd)
return;
char buf[4096];
- int64_t rsize = PERFETTO_EINTR(read(*stdouterr_pipe_.rd, buf, sizeof(buf)));
+ int64_t rsize = PERFETTO_EINTR(read(*s_.stdouterr_pipe.rd, buf, sizeof(buf)));
if (rsize < 0 && errno == EAGAIN)
return;
if (rsize > 0) {
- output_.append(buf, static_cast<size_t>(rsize));
+ s_.output.append(buf, static_cast<size_t>(rsize));
} else if (rsize == 0 /* EOF */) {
- stdouterr_pipe_.rd.reset();
+ s_.stdouterr_pipe.rd.reset();
} else {
PERFETTO_PLOG("Subprocess read(stdout/err) failed");
- stdouterr_pipe_.rd.reset();
+ s_.stdouterr_pipe.rd.reset();
}
}
void Subprocess::KillAndWaitForTermination() {
- kill(pid_, SIGKILL);
+ kill(s_.pid, SIGKILL);
Wait();
}
diff --git a/src/base/subprocess_unittest.cc b/src/base/subprocess_unittest.cc
index 6bcdc99..13bed15 100644
--- a/src/base/subprocess_unittest.cc
+++ b/src/base/subprocess_unittest.cc
@@ -269,6 +269,34 @@
EXPECT_EQ(kill(pid, SIGWINCH), -1);
}
+// Regression test for b/162505491.
+TEST(SubprocessTest, MoveOperators) {
+ {
+ Subprocess initial = Subprocess({"sleep", "10000"});
+ initial.Start();
+ Subprocess moved(std::move(initial));
+ EXPECT_EQ(moved.Poll(), Subprocess::kRunning);
+ EXPECT_EQ(initial.Poll(), Subprocess::kNotStarted);
+
+ // Check that reuse works
+ initial = Subprocess({"echo", "-n", "hello"});
+ initial.args.stdout_mode = Subprocess::OutputMode::kBuffer;
+ initial.Start();
+ initial.Wait(/*timeout=*/5000);
+ EXPECT_EQ(initial.status(), Subprocess::kExited);
+ EXPECT_EQ(initial.returncode(), 0);
+ EXPECT_EQ(initial.output(), "hello");
+ }
+
+ std::vector<Subprocess> v;
+ for (int i = 0; i < 10; i++) {
+ v.emplace_back(Subprocess({"sleep", "10"}));
+ v.back().Start();
+ }
+ for (auto& p : v)
+ EXPECT_EQ(p.Poll(), Subprocess::kRunning);
+}
+
} // namespace
} // namespace base
} // namespace perfetto