diff options
Diffstat (limited to 'test')
-rw-r--r-- | test/syscalls/linux/BUILD | 1 | ||||
-rw-r--r-- | test/syscalls/linux/ptrace.cc | 458 | ||||
-rw-r--r-- | test/util/posix_error.h | 10 |
3 files changed, 191 insertions, 278 deletions
diff --git a/test/syscalls/linux/BUILD b/test/syscalls/linux/BUILD index 112390569..c7991cfaa 100644 --- a/test/syscalls/linux/BUILD +++ b/test/syscalls/linux/BUILD @@ -1885,6 +1885,7 @@ cc_binary( linkstatic = 1, deps = [ "@com_google_absl//absl/flags:flag", + "@com_google_absl//absl/strings", "@com_google_absl//absl/time", gtest, "//test/util:capability_util", diff --git a/test/syscalls/linux/ptrace.cc b/test/syscalls/linux/ptrace.cc index d519b65e6..f64c23ac0 100644 --- a/test/syscalls/linux/ptrace.cc +++ b/test/syscalls/linux/ptrace.cc @@ -30,6 +30,7 @@ #include "gmock/gmock.h" #include "gtest/gtest.h" #include "absl/flags/flag.h" +#include "absl/strings/string_view.h" #include "absl/time/clock.h" #include "absl/time/time.h" #include "test/util/capability_util.h" @@ -51,17 +52,10 @@ ABSL_FLAG(bool, ptrace_test_execve_child, false, ABSL_FLAG(bool, ptrace_test_trace_descendants_allowed, false, "If set, run the child workload for " "PtraceTest_TraceDescendantsAllowed."); -ABSL_FLAG(bool, ptrace_test_prctl_set_ptracer_pid, false, - "If set, run the child workload for PtraceTest_PrctlSetPtracerPID."); -ABSL_FLAG(bool, ptrace_test_prctl_set_ptracer_any, false, - "If set, run the child workload for PtraceTest_PrctlSetPtracerAny."); -ABSL_FLAG(bool, ptrace_test_prctl_clear_ptracer, false, - "If set, run the child workload for PtraceTest_PrctlClearPtracer."); -ABSL_FLAG(bool, ptrace_test_prctl_replace_ptracer, false, - "If set, run the child workload for PtraceTest_PrctlReplacePtracer."); -ABSL_FLAG(int, ptrace_test_prctl_replace_ptracer_tid, -1, - "Specifies the replacement tracer tid in the child workload for " - "PtraceTest_PrctlReplacePtracer."); +ABSL_FLAG(bool, ptrace_test_ptrace_attacher, false, + "If set, run the child workload for PtraceAttacherSubprocess."); +ABSL_FLAG(bool, ptrace_test_prctl_set_ptracer, false, + "If set, run the child workload for PrctlSetPtracerSubprocess."); ABSL_FLAG(bool, ptrace_test_prctl_set_ptracer_and_exit_tracee_thread, false, "If set, run the child workload for " "PtraceTest_PrctlSetPtracerPersistsPastTraceeThreadExit."); @@ -161,6 +155,86 @@ int CheckPtraceAttach(pid_t pid) { return 0; } +class SimpleSubprocess { + public: + explicit SimpleSubprocess(absl::string_view child_flag) { + int sockets[2]; + TEST_PCHECK(socketpair(AF_UNIX, SOCK_STREAM, 0, sockets) == 0); + + // Allocate vector before forking (not async-signal-safe). + ExecveArray const owned_child_argv = {"/proc/self/exe", child_flag, + "--ptrace_test_fd", + std::to_string(sockets[0])}; + char* const* const child_argv = owned_child_argv.get(); + + pid_ = fork(); + if (pid_ == 0) { + TEST_PCHECK(close(sockets[1]) == 0); + execve(child_argv[0], child_argv, /* envp = */ nullptr); + TEST_PCHECK_MSG(false, "Survived execve to test child"); + } + TEST_PCHECK(pid_ > 0); + TEST_PCHECK(close(sockets[0]) == 0); + sockfd_ = sockets[1]; + } + + SimpleSubprocess(SimpleSubprocess&& orig) + : pid_(orig.pid_), sockfd_(orig.sockfd_) { + orig.pid_ = -1; + orig.sockfd_ = -1; + } + + SimpleSubprocess& operator=(SimpleSubprocess&& orig) { + if (this != &orig) { + this->~SimpleSubprocess(); + pid_ = orig.pid_; + sockfd_ = orig.sockfd_; + orig.pid_ = -1; + orig.sockfd_ = -1; + } + return *this; + } + + SimpleSubprocess(SimpleSubprocess const&) = delete; + SimpleSubprocess& operator=(SimpleSubprocess const&) = delete; + + ~SimpleSubprocess() { + if (pid_ < 0) { + return; + } + EXPECT_THAT(shutdown(sockfd_, SHUT_RDWR), SyscallSucceeds()); + EXPECT_THAT(close(sockfd_), SyscallSucceeds()); + int status; + EXPECT_THAT(waitpid(pid_, &status, 0), SyscallSucceedsWithValue(pid_)); + EXPECT_TRUE(WIFEXITED(status) && WEXITSTATUS(status) == 0) + << " status " << status; + } + + pid_t pid() const { return pid_; } + + // Sends the child process the given value, receives an errno in response, and + // returns a PosixError corresponding to the received errno. + template <typename T> + PosixError Cmd(T val) { + if (WriteFd(sockfd_, &val, sizeof(val)) < 0) { + return PosixError(errno, "write failed"); + } + return RecvErrno(); + } + + private: + PosixError RecvErrno() { + int resp_errno; + if (ReadFd(sockfd_, &resp_errno, sizeof(resp_errno)) < 0) { + return PosixError(errno, "read failed"); + } + return PosixError(resp_errno); + } + + pid_t pid_ = -1; + int sockfd_ = -1; +}; + TEST(PtraceTest, AttachSelf) { EXPECT_THAT(ptrace(PTRACE_ATTACH, gettid(), 0, 0), SyscallFailsWithErrno(EPERM)); @@ -343,289 +417,128 @@ TEST(PtraceTest, PrctlSetPtracerInvalidPID) { EXPECT_THAT(prctl(PR_SET_PTRACER, 123456789), SyscallFailsWithErrno(EINVAL)); } -TEST(PtraceTest, PrctlSetPtracerPID) { - SKIP_IF(ASSERT_NO_ERRNO_AND_VALUE(YamaPtraceScope()) != 1); - - AutoCapability cap(CAP_SYS_PTRACE, false); - - // Use sockets to synchronize between tracer and tracee. - int sockets[2]; - ASSERT_THAT(socketpair(AF_UNIX, SOCK_STREAM, 0, sockets), SyscallSucceeds()); - - // Allocate vector before forking (not async-signal-safe). - ExecveArray const owned_child_argv = { - "/proc/self/exe", "--ptrace_test_prctl_set_ptracer_pid", - "--ptrace_test_fd", std::to_string(sockets[0])}; - char* const* const child_argv = owned_child_argv.get(); - - pid_t const tracee_pid = fork(); - if (tracee_pid == 0) { - TEST_PCHECK(close(sockets[1]) == 0); - // This test will create a new thread in the child process. - // pthread_create(2) isn't async-signal-safe, so we execve() first. - execve(child_argv[0], child_argv, /* envp = */ nullptr); - TEST_PCHECK_MSG(false, "Survived execve to test child"); - } - ASSERT_THAT(tracee_pid, SyscallSucceeds()); - ASSERT_THAT(close(sockets[0]), SyscallSucceeds()); - - pid_t const tracer_pid = fork(); - if (tracer_pid == 0) { - // Wait until tracee has called prctl. - char done; - TEST_PCHECK(read(sockets[1], &done, 1) == 1); - MaybeSave(); - - TEST_PCHECK(CheckPtraceAttach(tracee_pid) == 0); - _exit(0); - } - ASSERT_THAT(tracer_pid, SyscallSucceeds()); - - // Clean up tracer. - int status; - ASSERT_THAT(waitpid(tracer_pid, &status, 0), SyscallSucceeds()); - EXPECT_TRUE(WIFEXITED(status) && WEXITSTATUS(status) == 0); - - // Clean up tracee. - ASSERT_THAT(kill(tracee_pid, SIGKILL), SyscallSucceeds()); - ASSERT_THAT(waitpid(tracee_pid, &status, 0), - SyscallSucceedsWithValue(tracee_pid)); - EXPECT_TRUE(WIFSIGNALED(status) && WTERMSIG(status) == SIGKILL) - << " status " << status; +SimpleSubprocess CreatePtraceAttacherSubprocess() { + return SimpleSubprocess("--ptrace_test_ptrace_attacher"); } -[[noreturn]] void RunPrctlSetPtracerPID(int fd) { - ScopedThread t([fd] { - // Perform prctl in a separate thread to verify that it is process-wide. - TEST_PCHECK(prctl(PR_SET_PTRACER, getppid()) == 0); - MaybeSave(); - // Indicate that the prctl has been set. - TEST_PCHECK(write(fd, "x", 1) == 1); - MaybeSave(); +[[noreturn]] static void RunPtraceAttacher(int sockfd) { + // execve() may have restored CAP_SYS_PTRACE if we had real UID 0. + TEST_CHECK(SetCapability(CAP_SYS_PTRACE, false).ok()); + // Perform PTRACE_ATTACH in a separate thread to verify that permissions + // apply process-wide. + ScopedThread t([&] { + while (true) { + pid_t pid; + int rv = read(sockfd, &pid, sizeof(pid)); + if (rv == 0) { + _exit(0); + } + if (rv < 0) { + _exit(1); + } + int resp_errno = 0; + if (CheckPtraceAttach(pid) < 0) { + resp_errno = errno; + } + TEST_PCHECK(write(sockfd, &resp_errno, sizeof(resp_errno)) == + sizeof(resp_errno)); + } }); while (true) { SleepSafe(absl::Seconds(1)); } } -TEST(PtraceTest, PrctlSetPtracerAny) { - SKIP_IF(ASSERT_NO_ERRNO_AND_VALUE(YamaPtraceScope()) != 1); - AutoCapability cap(CAP_SYS_PTRACE, false); - - // Use sockets to synchronize between tracer and tracee. - int sockets[2]; - ASSERT_THAT(socketpair(AF_UNIX, SOCK_STREAM, 0, sockets), SyscallSucceeds()); - - // Allocate vector before forking (not async-signal-safe). - ExecveArray const owned_child_argv = { - "/proc/self/exe", "--ptrace_test_prctl_set_ptracer_any", - "--ptrace_test_fd", std::to_string(sockets[0])}; - char* const* const child_argv = owned_child_argv.get(); - - pid_t const tracee_pid = fork(); - if (tracee_pid == 0) { - // This test will create a new thread in the child process. - // pthread_create(2) isn't async-signal-safe, so we execve() first. - TEST_PCHECK(close(sockets[1]) == 0); - execve(child_argv[0], child_argv, /* envp = */ nullptr); - TEST_PCHECK_MSG(false, "Survived execve to test child"); - } - ASSERT_THAT(tracee_pid, SyscallSucceeds()); - ASSERT_THAT(close(sockets[0]), SyscallSucceeds()); - - pid_t const tracer_pid = fork(); - if (tracer_pid == 0) { - // Wait until tracee has called prctl. - char done; - TEST_PCHECK(read(sockets[1], &done, 1) == 1); - MaybeSave(); - - TEST_PCHECK(CheckPtraceAttach(tracee_pid) == 0); - _exit(0); - } - ASSERT_THAT(tracer_pid, SyscallSucceeds()); - - // Clean up tracer. - int status; - ASSERT_THAT(waitpid(tracer_pid, &status, 0), SyscallSucceeds()); - EXPECT_TRUE(WIFEXITED(status) && WEXITSTATUS(status) == 0) - << " status " << status; - - // Clean up tracee. - ASSERT_THAT(kill(tracee_pid, SIGKILL), SyscallSucceeds()); - ASSERT_THAT(waitpid(tracee_pid, &status, 0), - SyscallSucceedsWithValue(tracee_pid)); - EXPECT_TRUE(WIFSIGNALED(status) && WTERMSIG(status) == SIGKILL) - << " status " << status; +SimpleSubprocess CreatePrctlSetPtracerSubprocess() { + return SimpleSubprocess("--ptrace_test_prctl_set_ptracer"); } -[[noreturn]] void RunPrctlSetPtracerAny(int fd) { - ScopedThread t([fd] { - // Perform prctl in a separate thread to verify that it is process-wide. - TEST_PCHECK(prctl(PR_SET_PTRACER, PR_SET_PTRACER_ANY) == 0); - MaybeSave(); - // Indicate that the prctl has been set. - TEST_PCHECK(write(fd, "x", 1) == 1); - MaybeSave(); +[[noreturn]] static void RunPrctlSetPtracer(int sockfd) { + // Perform prctl in a separate thread to verify that it applies + // process-wide. + ScopedThread t([&] { + while (true) { + pid_t pid; + int rv = read(sockfd, &pid, sizeof(pid)); + if (rv == 0) { + _exit(0); + } + if (rv < 0) { + _exit(1); + } + int resp_errno = 0; + if (prctl(PR_SET_PTRACER, pid) < 0) { + resp_errno = errno; + } + TEST_PCHECK(write(sockfd, &resp_errno, sizeof(resp_errno)) == + sizeof(resp_errno)); + } }); while (true) { SleepSafe(absl::Seconds(1)); } } -TEST(PtraceTest, PrctlClearPtracer) { +TEST(PtraceTest, PrctlSetPtracer) { SKIP_IF(ASSERT_NO_ERRNO_AND_VALUE(YamaPtraceScope()) != 1); - AutoCapability cap(CAP_SYS_PTRACE, false); - - // Use sockets to synchronize between tracer and tracee. - int sockets[2]; - ASSERT_THAT(socketpair(AF_UNIX, SOCK_STREAM, 0, sockets), SyscallSucceeds()); - - // Allocate vector before forking (not async-signal-safe). - ExecveArray const owned_child_argv = { - "/proc/self/exe", "--ptrace_test_prctl_clear_ptracer", "--ptrace_test_fd", - std::to_string(sockets[0])}; - char* const* const child_argv = owned_child_argv.get(); - - pid_t const tracee_pid = fork(); - if (tracee_pid == 0) { - // This test will create a new thread in the child process. - // pthread_create(2) isn't async-signal-safe, so we execve() first. - TEST_PCHECK(close(sockets[1]) == 0); - execve(child_argv[0], child_argv, /* envp = */ nullptr); - TEST_PCHECK_MSG(false, "Survived execve to test child"); - } - ASSERT_THAT(tracee_pid, SyscallSucceeds()); - ASSERT_THAT(close(sockets[0]), SyscallSucceeds()); - - pid_t const tracer_pid = fork(); - if (tracer_pid == 0) { - // Wait until tracee has called prctl. - char done; - TEST_PCHECK(read(sockets[1], &done, 1) == 1); - MaybeSave(); - - TEST_CHECK(CheckPtraceAttach(tracee_pid) == -1); - TEST_PCHECK(errno == EPERM); - _exit(0); - } - ASSERT_THAT(tracer_pid, SyscallSucceeds()); - - // Clean up tracer. - int status; - ASSERT_THAT(waitpid(tracer_pid, &status, 0), SyscallSucceeds()); - EXPECT_TRUE(WIFEXITED(status) && WEXITSTATUS(status) == 0) - << " status " << status; - - // Clean up tracee. - ASSERT_THAT(kill(tracee_pid, SIGKILL), SyscallSucceeds()); - ASSERT_THAT(waitpid(tracee_pid, &status, 0), - SyscallSucceedsWithValue(tracee_pid)); - EXPECT_TRUE(WIFSIGNALED(status) && WTERMSIG(status) == SIGKILL) - << " status " << status; -} - -[[noreturn]] void RunPrctlClearPtracer(int fd) { - ScopedThread t([fd] { - // Perform prctl in a separate thread to verify that it is process-wide. - TEST_PCHECK(prctl(PR_SET_PTRACER, PR_SET_PTRACER_ANY) == 0); - MaybeSave(); - TEST_PCHECK(prctl(PR_SET_PTRACER, 0) == 0); - MaybeSave(); - // Indicate that the prctl has been set/cleared. - TEST_PCHECK(write(fd, "x", 1) == 1); - MaybeSave(); - }); - while (true) { - SleepSafe(absl::Seconds(1)); - } -} -TEST(PtraceTest, PrctlReplacePtracer) { - SKIP_IF(ASSERT_NO_ERRNO_AND_VALUE(YamaPtraceScope()) != 1); AutoCapability cap(CAP_SYS_PTRACE, false); - pid_t const unused_pid = fork(); - if (unused_pid == 0) { - while (true) { - SleepSafe(absl::Seconds(1)); - } - } - ASSERT_THAT(unused_pid, SyscallSucceeds()); + // Ensure that initially, no tracer exception is set. + ASSERT_THAT(prctl(PR_SET_PTRACER, 0), SyscallSucceeds()); - // Use sockets to synchronize between tracer and tracee. - int sockets[2]; - ASSERT_THAT(socketpair(AF_UNIX, SOCK_STREAM, 0, sockets), SyscallSucceeds()); + SimpleSubprocess tracee = CreatePrctlSetPtracerSubprocess(); + SimpleSubprocess tracer = CreatePtraceAttacherSubprocess(); - // Allocate vector before forking (not async-signal-safe). - ExecveArray const owned_child_argv = { - "/proc/self/exe", - "--ptrace_test_prctl_replace_ptracer", - "--ptrace_test_prctl_replace_ptracer_tid", - std::to_string(unused_pid), - "--ptrace_test_fd", - std::to_string(sockets[0])}; - char* const* const child_argv = owned_child_argv.get(); + // By default, Yama should prevent tracer from tracing its parent (this + // process) or siblings (tracee). + EXPECT_THAT(tracer.Cmd(gettid()), PosixErrorIs(EPERM)); + EXPECT_THAT(tracer.Cmd(tracee.pid()), PosixErrorIs(EPERM)); - pid_t const tracee_pid = fork(); - if (tracee_pid == 0) { - TEST_PCHECK(close(sockets[1]) == 0); - // This test will create a new thread in the child process. - // pthread_create(2) isn't async-signal-safe, so we execve() first. - execve(child_argv[0], child_argv, /* envp = */ nullptr); - TEST_PCHECK_MSG(false, "Survived execve to test child"); - } - ASSERT_THAT(tracee_pid, SyscallSucceeds()); - ASSERT_THAT(close(sockets[0]), SyscallSucceeds()); + // If tracee invokes PR_SET_PTRACER on either tracer's pid, the pid of any of + // its ancestors (i.e. us), or PR_SET_PTRACER_ANY, then tracer can trace it + // (but not us). - pid_t const tracer_pid = fork(); - if (tracer_pid == 0) { - // Wait until tracee has called prctl. - char done; - TEST_PCHECK(read(sockets[1], &done, 1) == 1); - MaybeSave(); + ASSERT_THAT(tracee.Cmd(tracer.pid()), PosixErrorIs(0)); + EXPECT_THAT(tracer.Cmd(tracee.pid()), PosixErrorIs(0)); + EXPECT_THAT(tracer.Cmd(gettid()), PosixErrorIs(EPERM)); - TEST_CHECK(CheckPtraceAttach(tracee_pid) == -1); - TEST_PCHECK(errno == EPERM); - _exit(0); - } - ASSERT_THAT(tracer_pid, SyscallSucceeds()); + ASSERT_THAT(tracee.Cmd(gettid()), PosixErrorIs(0)); + EXPECT_THAT(tracer.Cmd(tracee.pid()), PosixErrorIs(0)); + EXPECT_THAT(tracer.Cmd(gettid()), PosixErrorIs(EPERM)); - // Clean up tracer. - int status; - ASSERT_THAT(waitpid(tracer_pid, &status, 0), SyscallSucceeds()); - EXPECT_TRUE(WIFEXITED(status) && WEXITSTATUS(status) == 0) - << " status " << status; + ASSERT_THAT(tracee.Cmd(static_cast<pid_t>(PR_SET_PTRACER_ANY)), + PosixErrorIs(0)); + EXPECT_THAT(tracer.Cmd(tracee.pid()), PosixErrorIs(0)); + EXPECT_THAT(tracer.Cmd(gettid()), PosixErrorIs(EPERM)); - // Clean up tracee. - ASSERT_THAT(kill(tracee_pid, SIGKILL), SyscallSucceeds()); - ASSERT_THAT(waitpid(tracee_pid, &status, 0), - SyscallSucceedsWithValue(tracee_pid)); - EXPECT_TRUE(WIFSIGNALED(status) && WTERMSIG(status) == SIGKILL) - << " status " << status; + // If tracee invokes PR_SET_PTRACER with pid 0, then tracer can no longer + // trace it. + ASSERT_THAT(tracee.Cmd(0), PosixErrorIs(0)); + EXPECT_THAT(tracer.Cmd(tracee.pid()), PosixErrorIs(EPERM)); - // Clean up unused. - ASSERT_THAT(kill(unused_pid, SIGKILL), SyscallSucceeds()); - ASSERT_THAT(waitpid(unused_pid, &status, 0), - SyscallSucceedsWithValue(unused_pid)); - EXPECT_TRUE(WIFSIGNALED(status) && WTERMSIG(status) == SIGKILL) - << " status " << status; -} + // If we invoke PR_SET_PTRACER with tracer's pid, then it can trace us (but + // not our descendants). + ASSERT_THAT(prctl(PR_SET_PTRACER, tracer.pid()), SyscallSucceeds()); + EXPECT_THAT(tracer.Cmd(gettid()), PosixErrorIs(0)); + EXPECT_THAT(tracer.Cmd(tracee.pid()), PosixErrorIs(EPERM)); -[[noreturn]] void RunPrctlReplacePtracer(int new_tracer_pid, int fd) { - TEST_PCHECK(prctl(PR_SET_PTRACER, getppid()) == 0); - MaybeSave(); + // If we invoke PR_SET_PTRACER with pid 0, then tracer can no longer trace us. + ASSERT_THAT(prctl(PR_SET_PTRACER, 0), SyscallSucceeds()); + EXPECT_THAT(tracer.Cmd(gettid()), PosixErrorIs(EPERM)); - ScopedThread t([new_tracer_pid, fd] { - TEST_PCHECK(prctl(PR_SET_PTRACER, new_tracer_pid) == 0); - MaybeSave(); - // Indicate that the prctl has been set. - TEST_PCHECK(write(fd, "x", 1) == 1); - MaybeSave(); - }); - while (true) { - SleepSafe(absl::Seconds(1)); - } + // Another thread in our thread group can invoke PR_SET_PTRACER instead; its + // effect applies to the whole thread group. + pid_t const our_tid = gettid(); + ScopedThread([&] { + ASSERT_THAT(prctl(PR_SET_PTRACER, tracer.pid()), SyscallSucceeds()); + EXPECT_THAT(tracer.Cmd(gettid()), PosixErrorIs(0)); + EXPECT_THAT(tracer.Cmd(our_tid), PosixErrorIs(0)); + + ASSERT_THAT(prctl(PR_SET_PTRACER, 0), SyscallSucceeds()); + EXPECT_THAT(tracer.Cmd(gettid()), PosixErrorIs(EPERM)); + EXPECT_THAT(tracer.Cmd(our_tid), PosixErrorIs(EPERM)); + }).Join(); } // Tests that YAMA exceptions store tracees by thread group leader. Exceptions @@ -2342,21 +2255,12 @@ int main(int argc, char** argv) { gvisor::testing::RunTraceDescendantsAllowed(fd); } - if (absl::GetFlag(FLAGS_ptrace_test_prctl_set_ptracer_pid)) { - gvisor::testing::RunPrctlSetPtracerPID(fd); - } - - if (absl::GetFlag(FLAGS_ptrace_test_prctl_set_ptracer_any)) { - gvisor::testing::RunPrctlSetPtracerAny(fd); - } - - if (absl::GetFlag(FLAGS_ptrace_test_prctl_clear_ptracer)) { - gvisor::testing::RunPrctlClearPtracer(fd); + if (absl::GetFlag(FLAGS_ptrace_test_ptrace_attacher)) { + gvisor::testing::RunPtraceAttacher(fd); } - if (absl::GetFlag(FLAGS_ptrace_test_prctl_replace_ptracer)) { - gvisor::testing::RunPrctlReplacePtracer( - absl::GetFlag(FLAGS_ptrace_test_prctl_replace_ptracer_tid), fd); + if (absl::GetFlag(FLAGS_ptrace_test_prctl_set_ptracer)) { + gvisor::testing::RunPrctlSetPtracer(fd); } if (absl::GetFlag( diff --git a/test/util/posix_error.h b/test/util/posix_error.h index 9ca09b77c..40853cb21 100644 --- a/test/util/posix_error.h +++ b/test/util/posix_error.h @@ -385,7 +385,7 @@ class PosixErrorIsMatcher { }; // Returns a gMock matcher that matches a PosixError or PosixErrorOr<> whose -// whose error code matches code_matcher, and whose error message matches +// error code matches code_matcher, and whose error message matches // message_matcher. template <typename ErrorCodeMatcher> PosixErrorIsMatcher PosixErrorIs( @@ -395,6 +395,14 @@ PosixErrorIsMatcher PosixErrorIs( std::move(message_matcher)); } +// Returns a gMock matcher that matches a PosixError or PosixErrorOr<> whose +// error code matches code_matcher. +template <typename ErrorCodeMatcher> +PosixErrorIsMatcher PosixErrorIs(ErrorCodeMatcher&& code_matcher) { + return PosixErrorIsMatcher(std::forward<ErrorCodeMatcher>(code_matcher), + ::testing::_); +} + // Returns a gMock matcher that matches a PosixErrorOr<> which is ok() and // value matches the inner matcher. template <typename InnerMatcher> |