Files
2026-07-16 17:48:25 -07:00

1363 lines
44 KiB
C++

// Copyright 2026 The gVisor Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include <errno.h>
#include <fcntl.h>
#include <linux/prctl.h>
#include <linux/sched.h>
#include <poll.h>
#include <sched.h>
#include <signal.h>
#include <sys/epoll.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/prctl.h>
#include <sys/ptrace.h>
#include <sys/socket.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <atomic>
#include <cerrno>
#include <csignal>
#include <cstdint>
#include <cstring>
#include <memory>
#include <string>
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include "absl/flags/flag.h"
#include "absl/strings/str_cat.h"
#include "absl/time/clock.h"
#include "absl/time/time.h"
#include "test/util/cleanup.h"
#include "test/util/file_descriptor.h"
#include "test/util/linux_capability_util.h"
#include "test/util/logging.h"
#include "test/util/memory_util.h"
#include "test/util/multiprocess_util.h"
#include "test/util/posix_error.h"
#include "test/util/save_util.h"
#include "test/util/test_util.h"
#include "test/util/thread_util.h"
#if defined(__x86_64__)
#ifndef SYS_pidfd_send_signal
#define SYS_pidfd_send_signal 424
#endif
#ifndef SYS_pidfd_open
#define SYS_pidfd_open 434
#endif
#ifndef SYS_clone3
#define SYS_clone3 435
#endif
#ifndef SYS_pidfd_getfd
#define SYS_pidfd_getfd 438
#endif
#elif defined(__aarch64__)
#ifndef SYS_pidfd_send_signal
#define SYS_pidfd_send_signal 424
#endif
#ifndef SYS_pidfd_open
#define SYS_pidfd_open 434
#endif
#ifndef SYS_clone3
#define SYS_clone3 435
#endif
#ifndef SYS_pidfd_getfd
#define SYS_pidfd_getfd 438
#endif
#else
#error "Unknown architecture"
#endif
#ifndef SYS_pidfd_open
#define SYS_pidfd_open 434
#endif
#ifndef SYS_clone3
#define SYS_clone3 435
#endif
#ifndef SYS_pidfd_send_signal
#define SYS_pidfd_send_signal 424
#endif
#ifndef SYS_pidfd_getfd
#define SYS_pidfd_getfd 438
#endif
// A flag for clone3().
#ifndef CLONE_PIDFD
#define CLONE_PIDFD 0x00001000
#endif
// A flag for waitid().
#ifndef P_PIDFD
#define P_PIDFD static_cast<idtype_t>(3)
#endif
// A flag for pidfd_open().
#ifndef PIDFD_THREAD
#define PIDFD_THREAD O_EXCL
#endif
// A flag for pidfd_send_signal().
#ifndef PIDFD_SIGNAL_THREAD
#define PIDFD_SIGNAL_THREAD (1UL << 0)
#endif
ABSL_FLAG(std::string, pidfd_helper, "",
"The name of the helper logic to run.");
ABSL_FLAG(int, pidfd_pipe_fd, -1, "The FD of a pipe for coordination.");
ABSL_FLAG(int, pidfd_tid_pipe_fd, -1, "Another pipe FD for TID reporting.");
namespace gvisor {
namespace testing {
// RunLeaderDelayedHelper implements the helper logic for the test
// PidfdThreadLeaderDelayedNotification. Threadgroup leader creates a subthread
// that waits on a pipe, and then exits immediately. The subthread sticks around
// until signalled.
void RunLeaderDelayedHelper() {
int rfd = absl::GetFlag(FLAGS_pidfd_pipe_fd);
// Use raw pthread_create instead of ScopedThread to avoid dependency on the
// (shortly exiting) leader.
pthread_t pt;
TEST_PCHECK(pthread_create(
&pt, nullptr,
[](void* arg) -> void* {
int fd = static_cast<int>(reinterpret_cast<intptr_t>(arg));
char buf;
(void)read(fd, &buf, 1); // Wait to be signaled.
close(fd);
_exit(0);
return nullptr;
},
reinterpret_cast<void*>(static_cast<intptr_t>(rfd))) == 0);
syscall(SYS_exit, 0); // Leader (just the thread) exits.
}
// RunSubthreadNotificationHelper implements the helper logic for
// PidfdThreadSubthreadNotification. The thread-group leader creates a subthread
// that reports its TID on a pipe and then waits on another pipe. The leader
// then pauses.
void RunSubthreadNotificationHelper() {
int rfd = absl::GetFlag(FLAGS_pidfd_pipe_fd);
int wfd = absl::GetFlag(FLAGS_pidfd_tid_pipe_fd);
ScopedThread thread([rfd, wfd] {
pid_t tid = gettid();
(void)write(wfd, &tid, sizeof(tid)); // Report TID.
char buf;
(void)read(rfd, &buf, 1); // Wait to be signaled.
close(rfd);
close(wfd);
});
pause(); // Leader pauses.
}
// RunExecRevivalHelper implements the child process for the
// PidfdThreadExecRevival test. The leader spawns a subthread and then pauses.
// The subthread reports its TID, and then execve()s to a program that pauses.
void RunExecRevivalHelper() {
int rfd = absl::GetFlag(FLAGS_pidfd_pipe_fd);
int wfd = absl::GetFlag(FLAGS_pidfd_tid_pipe_fd);
ScopedThread t([rfd, wfd] {
pid_t tid = gettid();
(void)write(wfd, &tid, sizeof(tid)); // Report TID.
char buf;
(void)read(rfd, &buf, 1); // Wait to be signaled.
close(rfd);
close(wfd);
char* const argv[] = {const_cast<char*>("/proc/self/exe"),
const_cast<char*>("--pidfd_helper=RunPauseHelper"),
nullptr};
execve(argv[0], argv, nullptr); // execve() to become leader.
_exit(errno);
});
pause(); // Leader pauses.
}
// RunPauseHelper implements a simple helper that pauses indefinitely.
void RunPauseHelper() { pause(); }
void RunSubthreadTracedHelper() {
int rfd = absl::GetFlag(FLAGS_pidfd_pipe_fd);
int tid_wfd = absl::GetFlag(FLAGS_pidfd_tid_pipe_fd);
ScopedThread t([rfd, tid_wfd] {
pid_t tid = gettid();
TEST_PCHECK(write(tid_wfd, &tid, sizeof(tid)) == sizeof(tid));
char buf;
(void)read(rfd, &buf, 1); // Wait to exit.
});
}
namespace {
class ScopedChildReaper {
public:
explicit ScopedChildReaper(pid_t child) : child_(child) {}
~ScopedChildReaper() {
if (child_ <= 0) return;
kill(child_, SIGKILL);
kill(child_, SIGCONT);
int status;
RetryEINTR(waitpid)(child_, &status, 0);
}
void Release() { child_ = -1; }
private:
pid_t child_;
};
PosixErrorOr<FileDescriptor> PidfdOpen(pid_t pid, unsigned int flags) {
int fd = syscall(SYS_pidfd_open, pid, flags);
if (fd < 0) {
return PosixError(errno, "pidfd_open failed");
}
return FileDescriptor(fd);
}
int PidfdSendSignal(int pidfd, int sig, siginfo_t* info, unsigned int flags) {
return syscall(SYS_pidfd_send_signal, pidfd, sig, info, flags);
}
PosixErrorOr<FileDescriptor> PidfdGetfd(int pidfd, int targetfd,
unsigned int flags) {
int fd = syscall(SYS_pidfd_getfd, pidfd, targetfd, flags);
if (fd < 0) {
return PosixError(errno, "pidfd_getfd failed");
}
return FileDescriptor(fd);
}
template <typename ChildFunc>
PosixErrorOr<FileDescriptor> Clone3Pidfd(pid_t& child, ChildFunc&& child_func) {
clone_args ca = {};
ca.flags = CLONE_PIDFD;
ca.exit_signal = SIGCHLD;
int fd = -1;
ca.pidfd = static_cast<uint64_t>(reinterpret_cast<uintptr_t>(&fd));
child = syscall(SYS_clone3, &ca, sizeof(ca));
if (child == 0) {
child_func();
_exit(0);
}
if (child < 0) {
return PosixError(errno, "clone3 failed");
}
if (fd < 0) {
return PosixError(errno, "clone3 succeeded but no pidfd returned");
}
return FileDescriptor(fd);
}
TEST(PidfdTest, PidfdOpenInvalid) {
EXPECT_THAT(PidfdOpen(-1, 0), PosixErrorIs(EINVAL));
EXPECT_THAT(PidfdOpen(getpid(), 1), PosixErrorIs(EINVAL));
}
TEST(PidfdTest, OpenPidfdSelfSucceeds) {
auto pidfd = ASSERT_NO_ERRNO_AND_VALUE(PidfdOpen(getpid(), 0));
int flags = fcntl(pidfd.get(), F_GETFD, 0);
EXPECT_TRUE(flags & FD_CLOEXEC);
}
static std::atomic<int> signal_received{0};
static void sigusr1_handler(int sig) {
if (sig == SIGUSR1) signal_received.store(1, std::memory_order_relaxed);
}
// "PidfdOpenPidfd" refers to a pidfd obtained via pidfd_open().
TEST(PidfdTest, SendSignalViaPidfdOpenPidfdToSelfSucceeds) {
auto pidfd = ASSERT_NO_ERRNO_AND_VALUE(PidfdOpen(getpid(), 0));
auto sighandler = signal(SIGUSR1, sigusr1_handler);
// Restore old handler after the test.
auto cleanup = Cleanup([sighandler] { signal(SIGUSR1, sighandler); });
signal_received.store(0, std::memory_order_relaxed);
EXPECT_THAT(PidfdSendSignal(pidfd.get(), SIGUSR1, nullptr, 0),
SyscallSucceeds());
absl::Time start = absl::Now();
while (signal_received.load(std::memory_order_relaxed) == 0) {
if (absl::Now() - start > absl::Seconds(2)) break;
sched_yield();
}
EXPECT_EQ(signal_received.load(std::memory_order_relaxed), 1);
}
TEST(PidfdTest, SendSignalViaPidfdOpenPidfdToDeadChildFails) {
pid_t child = fork();
if (child == 0) {
_exit(0);
}
ASSERT_THAT(child, SyscallSucceeds());
ScopedChildReaper cleanup(child);
auto pidfd = ASSERT_NO_ERRNO_AND_VALUE(PidfdOpen(child, 0));
int status;
ASSERT_THAT(RetryEINTR(waitpid)(child, &status, 0),
SyscallSucceedsWithValue(child));
cleanup.Release();
EXPECT_THAT(PidfdSendSignal(pidfd.get(), 0, nullptr, 0),
SyscallFailsWithErrno(ESRCH));
}
TEST(PidfdTest, Clone3InvalidFdAddrFails) {
// Unmapped or PROT_NONE pointer EFAULTs.
{
auto mapping =
ASSERT_NO_ERRNO_AND_VALUE(MmapAnon(kPageSize, PROT_NONE, MAP_PRIVATE));
clone_args ca = {};
ca.flags = CLONE_PIDFD;
ca.exit_signal = SIGCHLD;
ca.pidfd = static_cast<uint64_t>(mapping.addr());
pid_t child = syscall(SYS_clone3, &ca, sizeof(ca));
ASSERT_LT(child, 0);
EXPECT_EQ(errno, EFAULT);
}
// Read-only pointer EFAULTs.
{
auto mapping =
ASSERT_NO_ERRNO_AND_VALUE(MmapAnon(kPageSize, PROT_READ, MAP_PRIVATE));
clone_args ca = {};
ca.flags = CLONE_PIDFD;
ca.exit_signal = SIGCHLD;
ca.pidfd = static_cast<uint64_t>(mapping.addr());
pid_t child = syscall(SYS_clone3, &ca, sizeof(ca));
ASSERT_LT(child, 0);
EXPECT_EQ(errno, EFAULT);
}
}
TEST(PidfdTest, SendSignalToDeadChildFails) {
pid_t child = -1;
auto pidfd =
ASSERT_NO_ERRNO_AND_VALUE(Clone3Pidfd(child, []() { _exit(0); }));
ScopedChildReaper cleanup(child);
int status;
ASSERT_THAT(RetryEINTR(waitpid)(child, &status, 0),
SyscallSucceedsWithValue(child));
cleanup.Release();
EXPECT_THAT(PidfdSendSignal(pidfd.get(), 0, nullptr, 0),
SyscallFailsWithErrno(ESRCH));
}
TEST(PidfdTest, SendSignalToZombieSucceeds) {
pid_t child = -1;
auto pidfd =
ASSERT_NO_ERRNO_AND_VALUE(Clone3Pidfd(child, []() { _exit(0); }));
ScopedChildReaper cleanup(child);
// Wait for child to become a zombie.
siginfo_t info = {};
ASSERT_THAT(RetryEINTR(waitid)(P_PID, child, &info, WEXITED | WNOWAIT),
SyscallSucceeds());
// Target process is a zombie. pidfd_send_signal should fail with ESRCH.
EXPECT_THAT(PidfdSendSignal(pidfd.get(), 0, nullptr, 0), SyscallSucceeds());
}
TEST(PidfdTest, SendSignalToStoppedChildSucceeds) {
pid_t child = -1;
auto pidfd = ASSERT_NO_ERRNO_AND_VALUE(Clone3Pidfd(child, []() {
raise(SIGSTOP);
_exit(0);
}));
ScopedChildReaper cleanup(child);
int status;
ASSERT_THAT(RetryEINTR(waitpid)(child, &status, WSTOPPED),
SyscallSucceedsWithValue(child));
ASSERT_TRUE(WIFSTOPPED(status));
EXPECT_EQ(WSTOPSIG(status), SIGSTOP);
EXPECT_THAT(PidfdSendSignal(pidfd.get(), SIGCONT, nullptr, 0),
SyscallSucceeds());
ASSERT_THAT(RetryEINTR(waitpid)(child, &status, 0),
SyscallSucceedsWithValue(child));
EXPECT_TRUE(WIFEXITED(status));
EXPECT_EQ(WEXITSTATUS(status), 0);
cleanup.Release();
}
TEST(PidfdTest, SendSignalToPausedChildFailsWhenUnprivileged) {
SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SETUID)));
pid_t child = -1;
auto pidfd = ASSERT_NO_ERRNO_AND_VALUE(Clone3Pidfd(child, []() {
pause();
_exit(0);
}));
ScopedChildReaper cleanup(child);
int pidfd_raw = pidfd.get();
constexpr int kFeebleUid = 65534;
EXPECT_THAT(InForkedProcess([pidfd_raw] {
TEST_PCHECK(setresuid(kFeebleUid, kFeebleUid, kFeebleUid) == 0);
TEST_PCHECK_MSG(!HaveCapability(CAP_KILL).ValueOrDie(),
"Still have CAP_KILL");
TEST_CHECK_ERRNO(
syscall(SYS_pidfd_send_signal, pidfd_raw, 0, nullptr, 0),
EPERM);
}),
IsPosixErrorOkAndHolds(0));
}
TEST(PidfdTest, SendSignalRejection) {
pid_t child = -1;
auto pidfd = ASSERT_NO_ERRNO_AND_VALUE(Clone3Pidfd(child, []() {
pause();
_exit(0);
}));
ScopedChildReaper cleanup(child);
// Signo mismatch.
siginfo_t info = {};
info.si_signo = SIGUSR1;
EXPECT_THAT(PidfdSendSignal(pidfd.get(), SIGUSR2, &info, 0),
SyscallFailsWithErrno(EINVAL));
// Spoofing protection.
info = {};
info.si_signo = SIGUSR1;
info.si_code = SI_TKILL;
EXPECT_THAT(PidfdSendSignal(pidfd.get(), SIGUSR1, &info, 0),
SyscallFailsWithErrno(EPERM));
info = {};
info.si_signo = SIGUSR1;
info.si_code = 0;
EXPECT_THAT(PidfdSendSignal(pidfd.get(), SIGUSR1, &info, 0),
SyscallFailsWithErrno(EPERM));
// But can spoof self.
auto self_pidfd = ASSERT_NO_ERRNO_AND_VALUE(PidfdOpen(getpid(), 0));
info = {};
info.si_signo = 0;
info.si_code = SI_TKILL;
EXPECT_THAT(PidfdSendSignal(self_pidfd.get(), 0, &info, 0),
SyscallSucceeds());
// Valid signal delivery.
EXPECT_THAT(PidfdSendSignal(pidfd.get(), SIGUSR1, nullptr, 0),
SyscallSucceeds());
info = {};
info.si_signo = SIGUSR2;
info.si_code = SI_QUEUE;
EXPECT_THAT(PidfdSendSignal(pidfd.get(), SIGUSR2, &info, 0),
SyscallSucceeds());
}
TEST(PidfdTest, GetfdUnknown) {
pid_t child = -1;
auto pidfd = ASSERT_NO_ERRNO_AND_VALUE(Clone3Pidfd(child, []() {
pause();
_exit(0);
}));
ScopedChildReaper cleanup(child);
// The child is very unlikely to have an fd at 666.
EXPECT_THAT(PidfdGetfd(pidfd.get(), 666, 0), PosixErrorIs(EBADF));
}
TEST(PidfdTest, GetfdInvalidFlags) {
pid_t child = -1;
auto pidfd = ASSERT_NO_ERRNO_AND_VALUE(Clone3Pidfd(child, []() {
pause();
_exit(0);
}));
ScopedChildReaper cleanup(child);
EXPECT_THAT(PidfdGetfd(pidfd.get(), 0, 1), PosixErrorIs(EINVAL));
}
TEST(PidfdTest, GetfdWorks) {
int pfd[2];
ASSERT_THAT(pipe(pfd), SyscallSucceeds());
FileDescriptor read_pipe(pfd[0]);
FileDescriptor write_pipe(pfd[1]);
pid_t child = -1;
auto pidfd = ASSERT_NO_ERRNO_AND_VALUE(Clone3Pidfd(child, []() {
pause();
_exit(0);
}));
ScopedChildReaper cleanup(child);
// Close our read end of the pipe.
int childs_read_fd = read_pipe.get();
read_pipe.reset();
// And get the child's copy instead.
auto duped_readfd =
ASSERT_NO_ERRNO_AND_VALUE(PidfdGetfd(pidfd.get(), childs_read_fd, 0));
int flags = fcntl(duped_readfd.get(), F_GETFD, 0);
EXPECT_TRUE(flags & FD_CLOEXEC);
// Verify that the stolen read fd works.
const char write_buf[] = "hello pidfd";
EXPECT_THAT(write(write_pipe.get(), write_buf, sizeof(write_buf)),
SyscallSucceedsWithValue(sizeof(write_buf)));
char read_buf[sizeof(write_buf)] = {};
EXPECT_THAT(read(duped_readfd.get(), read_buf, sizeof(read_buf)),
SyscallSucceedsWithValue(sizeof(read_buf)));
EXPECT_STREQ(read_buf, write_buf);
}
TEST(PidfdTest, GetfdFailsWhenUnprivileged) {
pid_t child = -1;
auto pidfd = ASSERT_NO_ERRNO_AND_VALUE(Clone3Pidfd(child, []() {
pause();
_exit(0);
}));
ScopedChildReaper cleanup(child);
int stdin_fd = 0;
// We can get at the child's stdin.
auto duped_fd =
ASSERT_NO_ERRNO_AND_VALUE(PidfdGetfd(pidfd.get(), stdin_fd, 0));
// But our unprivileged other child cannot.
EXPECT_THAT(InForkedProcess([&pidfd, stdin_fd] {
TEST_CHECK_SUCCESS(syscall(SYS_unshare, CLONE_NEWUSER));
TEST_CHECK_ERRNO(
syscall(SYS_pidfd_getfd, pidfd.get(), stdin_fd, 0), EPERM);
}),
IsPosixErrorOkAndHolds(0));
}
TEST(PidfdTest, InvalidPidfd) {
constexpr int kNotAPidfd = 0;
EXPECT_THAT(PidfdSendSignal(kNotAPidfd, SIGKILL, nullptr, 0),
SyscallFailsWithErrno(EBADF));
EXPECT_THAT(PidfdGetfd(kNotAPidfd, 0, 0), PosixErrorIs(EBADF));
}
TEST(PidfdTest, WaitSingleState) {
pid_t child = -1;
auto pidfd =
ASSERT_NO_ERRNO_AND_VALUE(Clone3Pidfd(child, []() { _exit(66); }));
ScopedChildReaper cleanup(child);
siginfo_t info = {};
int ret = RetryEINTR(waitid)(P_PIDFD, pidfd.get(), &info, WEXITED);
ASSERT_THAT(ret, SyscallSucceeds());
cleanup.Release();
EXPECT_EQ(info.si_signo, SIGCHLD);
EXPECT_EQ(info.si_code, CLD_EXITED);
EXPECT_EQ(info.si_status, 66);
EXPECT_EQ(info.si_pid, child);
}
TEST(PidfdTest, WaitMultipleStates) {
int pfd[2];
ASSERT_THAT(pipe(pfd), SyscallSucceeds());
FileDescriptor read_pipe(pfd[0]);
FileDescriptor write_pipe(pfd[1]);
pid_t child = -1;
auto pidfd =
ASSERT_NO_ERRNO_AND_VALUE(Clone3Pidfd(child, [&write_pipe, &read_pipe]() {
write_pipe.reset();
TEST_CHECK_SUCCESS(kill(getpid(), SIGSTOP));
char buf;
TEST_PCHECK(read(read_pipe.get(), &buf, 1) == 1);
read_pipe.reset();
TEST_CHECK_SUCCESS(kill(getpid(), SIGSTOP));
_exit(0);
}));
ScopedChildReaper cleanup(child);
read_pipe.reset();
siginfo_t info = {};
ASSERT_THAT(RetryEINTR(waitid)(P_PIDFD, pidfd.get(), &info, WSTOPPED),
SyscallSucceeds());
EXPECT_EQ(info.si_code, CLD_STOPPED);
EXPECT_THAT(PidfdSendSignal(pidfd.get(), SIGCONT, nullptr, 0),
SyscallSucceeds());
info = {};
ASSERT_THAT(RetryEINTR(waitid)(P_PIDFD, pidfd.get(), &info, WCONTINUED),
SyscallSucceeds());
EXPECT_EQ(info.si_code, CLD_CONTINUED);
info = {};
ASSERT_THAT(write(write_pipe.get(), "C", 1), SyscallSucceedsWithValue(1));
write_pipe.reset();
ASSERT_THAT(RetryEINTR(waitid)(P_PIDFD, pidfd.get(), &info, WSTOPPED),
SyscallSucceeds());
EXPECT_EQ(info.si_code, CLD_STOPPED);
info = {};
EXPECT_THAT(PidfdSendSignal(pidfd.get(), SIGKILL, nullptr, 0),
SyscallSucceeds());
ASSERT_THAT(RetryEINTR(waitid)(P_PIDFD, pidfd.get(), &info, WEXITED),
SyscallSucceeds());
EXPECT_EQ(info.si_code, CLD_KILLED);
cleanup.Release();
}
TEST(PidfdTest, WaitNonblock) {
pid_t child = -1;
auto pidfd = ASSERT_NO_ERRNO_AND_VALUE(Clone3Pidfd(child, []() {
pause();
_exit(0);
}));
ScopedChildReaper cleanup(child);
int flags = fcntl(pidfd.get(), F_GETFL, 0);
ASSERT_THAT(fcntl(pidfd.get(), F_SETFL, flags | O_NONBLOCK),
SyscallSucceeds());
siginfo_t info = {};
EXPECT_THAT(RetryEINTR(waitid)(P_PIDFD, pidfd.get(), &info, WEXITED),
SyscallFailsWithErrno(EAGAIN));
EXPECT_THAT(
RetryEINTR(waitid)(P_PIDFD, pidfd.get(), &info, WEXITED | WNOHANG),
SyscallSucceedsWithValue(0));
kill(child, SIGKILL);
ASSERT_THAT(fcntl(pidfd.get(), F_SETFL, flags), SyscallSucceeds());
ASSERT_THAT(RetryEINTR(waitid)(P_PIDFD, pidfd.get(), &info, WEXITED),
SyscallSucceeds());
EXPECT_EQ(info.si_code, CLD_KILLED);
cleanup.Release();
}
TEST(PidfdTest, Poll) {
pid_t child = -1;
auto pidfd =
ASSERT_NO_ERRNO_AND_VALUE(Clone3Pidfd(child, []() { _exit(0); }));
ScopedChildReaper cleanup(child);
// Wait for the child to become a zombie.
siginfo_t info = {};
ASSERT_THAT(RetryEINTR(waitid)(P_PID, child, &info, WEXITED | WNOWAIT),
SyscallSucceeds());
// The child is already a zombie, POLLIN should be immediately available.
struct pollfd fds = {
.fd = pidfd.get(),
.events = POLLIN,
};
ASSERT_THAT(poll(&fds, 1, 0), SyscallSucceedsWithValue(1));
EXPECT_TRUE(fds.revents & POLLIN);
// Reap the child to observe POLLHUP.
int status;
ASSERT_THAT(RetryEINTR(waitpid)(child, &status, 0),
SyscallSucceedsWithValue(child));
cleanup.Release();
fds.revents = 0;
ASSERT_THAT(poll(&fds, 1, 0), SyscallSucceedsWithValue(1));
EXPECT_TRUE(fds.revents & POLLIN);
KernelVersion version = ASSERT_NO_ERRNO_AND_VALUE(GetKernelVersion());
if (IsRunningOnGvisor() ||
(version.major > 6 || (version.major == 6 && version.minor >= 10))) {
EXPECT_TRUE(fds.revents & POLLHUP);
}
// Create a second child that only exits when told to do so.
int pfd[2];
ASSERT_THAT(pipe(pfd), SyscallSucceeds());
FileDescriptor read_pipe(pfd[0]);
FileDescriptor write_pipe(pfd[1]);
pid_t child2 = -1;
int rfd = read_pipe.get();
auto pidfd2 = ASSERT_NO_ERRNO_AND_VALUE(Clone3Pidfd(child2, [rfd]() {
char buf;
read(rfd, &buf, 1);
_exit(0);
}));
ScopedChildReaper cleanup2(child2);
read_pipe.reset();
// Not pollable yet.
struct pollfd fds2 = {
.fd = pidfd2.get(),
.events = POLLIN,
};
ASSERT_THAT(poll(&fds2, 1, 100), SyscallSucceedsWithValue(0));
// Instruct child2 to exit.
ASSERT_THAT(write(write_pipe.get(), "a", 1), SyscallSucceedsWithValue(1));
write_pipe.reset();
// Now pollable.
ASSERT_THAT(poll(&fds2, 1, 5000), SyscallSucceedsWithValue(1));
EXPECT_TRUE(fds2.revents & POLLIN);
}
TEST(PidfdTest, Epoll) {
pid_t child = -1;
auto pidfd =
ASSERT_NO_ERRNO_AND_VALUE(Clone3Pidfd(child, []() { _exit(0); }));
ScopedChildReaper cleanup(child);
// Wait for the child to become a zombie.
siginfo_t info = {};
ASSERT_THAT(RetryEINTR(waitid)(P_PID, child, &info, WEXITED | WNOWAIT),
SyscallSucceeds());
int epfd = epoll_create1(0);
ASSERT_THAT(epfd, SyscallSucceeds());
FileDescriptor epollfd(epfd);
struct epoll_event ev = {};
ev.events = EPOLLIN;
ev.data.fd = pidfd.get();
ASSERT_THAT(epoll_ctl(epollfd.get(), EPOLL_CTL_ADD, pidfd.get(), &ev),
SyscallSucceeds());
struct epoll_event events[1];
// The child is already a zombie, EPOLLIN should be immediately available.
ASSERT_THAT(epoll_wait(epollfd.get(), events, 1, 0),
SyscallSucceedsWithValue(1));
EXPECT_TRUE(events[0].events & EPOLLIN);
// Reap the child to observe EPOLLHUP.
int status;
ASSERT_THAT(RetryEINTR(waitpid)(child, &status, 0),
SyscallSucceedsWithValue(child));
cleanup.Release();
events[0].events = 0;
ASSERT_THAT(epoll_wait(epollfd.get(), events, 1, 0),
SyscallSucceedsWithValue(1));
EXPECT_TRUE(events[0].events & EPOLLIN);
KernelVersion version = ASSERT_NO_ERRNO_AND_VALUE(GetKernelVersion());
if (IsRunningOnGvisor() ||
(version.major > 6 || (version.major == 6 && version.minor >= 10))) {
EXPECT_TRUE(events[0].events & EPOLLHUP);
}
}
TEST(PidfdTest, SetnsInvalidFlags) {
SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_ADMIN)));
pid_t child = -1;
auto pidfd = ASSERT_NO_ERRNO_AND_VALUE(Clone3Pidfd(child, []() {
pause();
_exit(0);
}));
ScopedChildReaper cleanup(child);
EXPECT_THAT(setns(pidfd.get(), 0), SyscallFailsWithErrno(EINVAL));
EXPECT_THAT(setns(pidfd.get(), -1), SyscallFailsWithErrno(EINVAL));
EXPECT_THAT(setns(pidfd.get(), CLONE_VM), SyscallFailsWithErrno(EINVAL));
}
TEST(PidfdTest, SetnsPtraceEPERM) {
SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_ADMIN)));
AutoCapability cap(CAP_SYS_PTRACE, false);
int before;
ASSERT_THAT(before = prctl(PR_GET_DUMPABLE), SyscallSucceeds());
auto cleanup = Cleanup([before] {
ASSERT_THAT(prctl(PR_SET_DUMPABLE, before), SyscallSucceeds());
});
// Mark ourselves as not dumpable.
constexpr int kSuidDumpDisable = 0;
TEST_PCHECK(prctl(PR_SET_DUMPABLE, kSuidDumpDisable, 0, 0, 0) == 0);
auto pidfd = ASSERT_NO_ERRNO_AND_VALUE(PidfdOpen(getpid(), 0));
EXPECT_THAT(InForkedProcess([&pidfd] {
TEST_CHECK_SUCCESS(syscall(SYS_unshare, CLONE_NEWNET));
TEST_CHECK_ERRNO(syscall(SYS_setns, pidfd.get(), CLONE_NEWNET),
EPERM);
}),
IsPosixErrorOkAndHolds(0));
}
TEST(PidfdTest, SetnsExitedChild) {
SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_ADMIN)));
pid_t child = -1;
auto pidfd =
ASSERT_NO_ERRNO_AND_VALUE(Clone3Pidfd(child, []() { _exit(0); }));
ScopedChildReaper cleanup(child);
int std_status;
ASSERT_THAT(RetryEINTR(waitpid)(child, &std_status, 0),
SyscallSucceedsWithValue(child));
cleanup.Release();
EXPECT_THAT(setns(pidfd.get(), CLONE_NEWNET), SyscallFailsWithErrno(ESRCH));
}
TEST(PidfdTest, SetnsZombieChild) {
SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_ADMIN)));
pid_t child = -1;
auto pidfd =
ASSERT_NO_ERRNO_AND_VALUE(Clone3Pidfd(child, []() { _exit(0); }));
ScopedChildReaper cleanup(child);
// Wait for the child to become a zombie.
siginfo_t info = {};
ASSERT_THAT(RetryEINTR(waitid)(P_PID, child, &info, WEXITED | WNOWAIT),
SyscallSucceeds());
EXPECT_THAT(setns(pidfd.get(), CLONE_NEWNET), SyscallFailsWithErrno(ESRCH));
}
TEST(PidfdTest, SetnsSingleNamespaceWorks) {
SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_SYS_ADMIN)));
int pfd[2];
ASSERT_THAT(pipe(pfd), SyscallSucceeds());
FileDescriptor pipe_read(pfd[0]);
FileDescriptor pipe_write(pfd[1]);
pid_t child = -1;
constexpr char kNewHostname[] = "pidfd-uts-test";
auto pidfd = ASSERT_NO_ERRNO_AND_VALUE(
Clone3Pidfd(child, [&pipe_write, &kNewHostname]() {
TEST_CHECK_SUCCESS(syscall(SYS_unshare, CLONE_NEWUTS));
TEST_CHECK_SUCCESS(sethostname(kNewHostname, sizeof(kNewHostname) - 1));
TEST_CHECK_SUCCESS(write(pipe_write.get(), "R", 1));
pause();
_exit(0);
}));
ScopedChildReaper cleanup(child);
char buf;
ASSERT_THAT(read(pipe_read.get(), &buf, 1), SyscallSucceedsWithValue(1));
EXPECT_THAT(
InForkedProcess([&pidfd, &kNewHostname] {
TEST_CHECK_SUCCESS(syscall(SYS_setns, pidfd.get(), CLONE_NEWUTS));
char current_hostname[256] = {};
TEST_CHECK_SUCCESS(
gethostname(current_hostname, sizeof(current_hostname)));
TEST_PCHECK_MSG(strcmp(current_hostname, kNewHostname) == 0,
"hostname mismatch");
}),
IsPosixErrorOkAndHolds(0));
}
TEST(PidfdTest, PidfdThreadTracksSingleThread) {
std::atomic<bool> stop_sibling{false};
std::atomic<pid_t> tid{-1};
auto t = std::make_unique<ScopedThread>([&]() {
tid.store(syscall(SYS_gettid));
while (!stop_sibling.load()) {
sched_yield();
}
});
auto cleanup = Cleanup([&t, &stop_sibling] {
stop_sibling.store(true);
t.reset();
});
// Wait till the sibling tells us its tid.
while (tid.load() == -1) {
sched_yield();
}
int pidfd_raw = syscall(SYS_pidfd_open, tid.load(), PIDFD_THREAD);
if (pidfd_raw == -1 && errno == EINVAL) {
GTEST_SKIP() << "PIDFD_THREAD not supported on this kernel";
}
ASSERT_THAT(pidfd_raw, SyscallSucceeds());
FileDescriptor pidfd(pidfd_raw);
// In the absence of PIDFD_THREAD, we should see ENOENT (EINVAL on older
// kernels).
int want_errno = ENOENT;
if (!IsRunningOnGvisor()) {
KernelVersion version = ASSERT_NO_ERRNO_AND_VALUE(GetKernelVersion());
if (version.major < 6 || (version.major == 6 && version.minor < 15)) {
want_errno = EINVAL;
}
}
ASSERT_THAT(syscall(SYS_pidfd_open, tid.load(), 0),
SyscallFailsWithErrno(want_errno));
struct pollfd pfd = {
.fd = pidfd.get(),
.events = POLLIN,
};
// The sibling thread is alive, so the pidfd should NOT be readable.
ASSERT_THAT(RetryEINTR(poll)(&pfd, 1, 0), SyscallSucceedsWithValue(0));
// And we should be able to send it signals.
EXPECT_THAT(PidfdSendSignal(pidfd_raw, 0, nullptr, PIDFD_SIGNAL_THREAD),
SyscallSucceedsWithValue(0));
// Stop the sibling thread to make the poll report POLLIN.
stop_sibling.store(true);
t.reset();
cleanup.Release();
pfd.revents = 0;
ASSERT_THAT(RetryEINTR(poll)(&pfd, 1, 1000), SyscallSucceedsWithValue(1));
EXPECT_TRUE(pfd.revents & POLLIN);
}
// PidfdThreadLeaderDelayedNotification verifies that a PIDFD_THREAD for a
// thread group leader only notifies when the entire thread group exits, even
// if the leader thread itself has already exited.
TEST(PidfdTest, PidfdThreadLeaderDelayedNotification) {
if (!IsRunningOnGvisor()) {
// Test only on kernels bearing this patch:
// https://github.com/torvalds/linux/commit/0fb482728ba1
KernelVersion version = ASSERT_NO_ERRNO_AND_VALUE(GetKernelVersion());
if (version.major < 6 || (version.major == 6 && version.minor < 15)) {
GTEST_SKIP() << "PIDFD_THREAD behavior requires kernel 6.15+";
}
}
int pipe_fds[2];
ASSERT_THAT(pipe(pipe_fds), SyscallSucceeds());
FileDescriptor read_pipe(pipe_fds[0]);
FileDescriptor write_pipe(pipe_fds[1]);
pid_t child;
int exec_errno;
auto cleanup = ASSERT_NO_ERRNO_AND_VALUE(
ForkAndExec("/proc/self/exe",
{"/proc/self/exe", "--pidfd_helper=RunLeaderDelayedHelper",
absl::StrCat("--pidfd_pipe_fd=", read_pipe.get())},
{}, &child, &exec_errno));
ASSERT_EQ(exec_errno, 0);
ASSERT_THAT(child, SyscallSucceeds());
read_pipe.reset();
auto leader_pidfd = ASSERT_NO_ERRNO_AND_VALUE(PidfdOpen(child, PIDFD_THREAD));
// Poll should timeout on the leader thread pidfd as long as the subthread is
// alive.
struct pollfd pfd = {
.fd = leader_pidfd.get(),
.events = POLLIN,
};
EXPECT_THAT(poll(&pfd, 1, 1000), SyscallSucceedsWithValue(0));
// Let the subthread exit.
ASSERT_THAT(write(write_pipe.get(), "a", 1), SyscallSucceedsWithValue(1));
write_pipe.reset();
// Now poll should succeed.
EXPECT_THAT(poll(&pfd, 1, 5000), SyscallSucceedsWithValue(1));
EXPECT_TRUE(pfd.revents & POLLIN);
}
// PidfdThreadSubthreadNotification verifies that a PIDFD_THREAD for a
// non-leader thread notifies as soon as that thread exits, even if other
// threads (including the leader) in the same group are still alive.
TEST(PidfdTest, PidfdThreadSubthreadNotification) {
if (!IsRunningOnGvisor()) {
// Test only on kernels bearing this patch:
// https://github.com/torvalds/linux/commit/0fb482728ba1
KernelVersion version = ASSERT_NO_ERRNO_AND_VALUE(GetKernelVersion());
if (version.major < 6 || (version.major == 6 && version.minor < 15)) {
GTEST_SKIP() << "PIDFD_THREAD behavior requires kernel 6.15+";
}
}
int pipe_fds[2];
ASSERT_THAT(pipe(pipe_fds), SyscallSucceeds());
FileDescriptor read_pipe(pipe_fds[0]);
FileDescriptor write_pipe(pipe_fds[1]);
int tid_pipe_fds[2];
ASSERT_THAT(pipe(tid_pipe_fds), SyscallSucceeds());
FileDescriptor tid_read_pipe(tid_pipe_fds[0]);
FileDescriptor tid_write_pipe(tid_pipe_fds[1]);
pid_t child;
int exec_errno;
auto cleanup = ASSERT_NO_ERRNO_AND_VALUE(ForkAndExec(
"/proc/self/exe",
{"/proc/self/exe", "--pidfd_helper=RunSubthreadNotificationHelper",
absl::StrCat("--pidfd_pipe_fd=", read_pipe.get()),
absl::StrCat("--pidfd_tid_pipe_fd=", tid_write_pipe.get())},
{}, &child, &exec_errno));
ASSERT_EQ(exec_errno, 0);
ASSERT_THAT(child, SyscallSucceeds());
tid_write_pipe.reset();
read_pipe.reset();
pid_t subthread_tid;
ASSERT_THAT(read(tid_read_pipe.get(), &subthread_tid, sizeof(subthread_tid)),
SyscallSucceedsWithValue(sizeof(subthread_tid)));
auto subthread_pidfd =
ASSERT_NO_ERRNO_AND_VALUE(PidfdOpen(subthread_tid, PIDFD_THREAD));
// Poll should timeout on the subthread pidfd as long as the subthread is
// alive.
struct pollfd pfd = {
.fd = subthread_pidfd.get(),
.events = POLLIN,
};
EXPECT_THAT(poll(&pfd, 1, 1000), SyscallSucceedsWithValue(0));
// Let subthread exit.
ASSERT_THAT(write(write_pipe.get(), "a", 1), SyscallSucceedsWithValue(1));
write_pipe.reset();
// Poll on subthread pidfd should succeed even while leader is alive.
EXPECT_THAT(poll(&pfd, 1, 5000), SyscallSucceedsWithValue(1));
EXPECT_TRUE(pfd.revents & POLLIN);
}
// PidfdThreadExecRevival verifies that an execve() "revives" a thread group
// and prevents a PIDFD_THREAD for the leader PID from notifying, even if the
// original leader has exited.
TEST(PidfdTest, PidfdThreadExecRevival) {
if (!IsRunningOnGvisor()) {
// Test only on kernels bearing this patch:
// https://github.com/torvalds/linux/commit/0fb482728ba1
KernelVersion version = ASSERT_NO_ERRNO_AND_VALUE(GetKernelVersion());
if (version.major < 6 || (version.major == 6 && version.minor < 15)) {
GTEST_SKIP() << "PIDFD_THREAD behavior requires kernel 6.15+";
}
}
int pipe_fds[2];
ASSERT_THAT(pipe(pipe_fds), SyscallSucceeds());
FileDescriptor read_pipe(pipe_fds[0]);
FileDescriptor write_pipe(pipe_fds[1]);
int tid_pipe_fds[2];
ASSERT_THAT(pipe(tid_pipe_fds), SyscallSucceeds());
FileDescriptor tid_read_pipe(tid_pipe_fds[0]);
FileDescriptor tid_write_pipe(tid_pipe_fds[1]);
pid_t child;
int exec_errno;
auto cleanup = ASSERT_NO_ERRNO_AND_VALUE(
ForkAndExec("/proc/self/exe",
{"/proc/self/exe", "--pidfd_helper=RunExecRevivalHelper",
absl::StrCat("--pidfd_pipe_fd=", read_pipe.get()),
absl::StrCat("--pidfd_tid_pipe_fd=", tid_write_pipe.get())},
{}, &child, &exec_errno));
ASSERT_EQ(exec_errno, 0);
ASSERT_THAT(child, SyscallSucceeds());
tid_write_pipe.reset();
read_pipe.reset();
pid_t subthread_tid;
ASSERT_THAT(read(tid_read_pipe.get(), &subthread_tid, sizeof(subthread_tid)),
SyscallSucceedsWithValue(sizeof(subthread_tid)));
auto leader_pidfd = ASSERT_NO_ERRNO_AND_VALUE(PidfdOpen(child, PIDFD_THREAD));
auto subthread_pidfd =
ASSERT_NO_ERRNO_AND_VALUE(PidfdOpen(subthread_tid, PIDFD_THREAD));
// Signal subthread to exec.
ASSERT_THAT(write(write_pipe.get(), "a", 1), SyscallSucceedsWithValue(1));
write_pipe.reset();
// The subthread should notify because it is killed by the above exec.
struct pollfd sub_pfd = {
.fd = subthread_pidfd.get(),
.events = POLLIN,
};
EXPECT_THAT(poll(&sub_pfd, 1, 5000), SyscallSucceedsWithValue(1));
EXPECT_TRUE(sub_pfd.revents & POLLIN);
// The leader pidfd should NOT notify because it is "revived" by the exec.
struct pollfd leader_pfd = {
.fd = leader_pidfd.get(),
.events = POLLIN,
};
EXPECT_THAT(poll(&leader_pfd, 1, 1000), SyscallSucceedsWithValue(0));
// Now kill the revived process and verify the leader pidfd finally notifies.
EXPECT_THAT(kill(child, SIGKILL), SyscallSucceeds());
EXPECT_THAT(poll(&leader_pfd, 1, 5000), SyscallSucceedsWithValue(1));
EXPECT_TRUE(leader_pfd.revents & POLLIN);
}
TEST(PidfdTest, PidfdThreadTracedSubthreadNotification) {
if (!IsRunningOnGvisor()) {
// Test only on kernels bearing this patch:
// https://github.com/torvalds/linux/commit/0fb482728ba1
KernelVersion version = ASSERT_NO_ERRNO_AND_VALUE(GetKernelVersion());
if (version.major < 6 || (version.major == 6 && version.minor < 15)) {
GTEST_SKIP() << "PIDFD_THREAD behavior requires kernel 6.15+";
}
}
int pipe_fds[2];
ASSERT_THAT(pipe(pipe_fds), SyscallSucceeds());
FileDescriptor read_pipe(pipe_fds[0]);
FileDescriptor write_pipe(pipe_fds[1]);
int tid_pipe_fds[2];
ASSERT_THAT(pipe(tid_pipe_fds), SyscallSucceeds());
FileDescriptor tid_read_pipe(tid_pipe_fds[0]);
FileDescriptor tid_write_pipe(tid_pipe_fds[1]);
pid_t child;
int exec_errno;
auto cleanup = ASSERT_NO_ERRNO_AND_VALUE(
ForkAndExec("/proc/self/exe",
{"/proc/self/exe", "--pidfd_helper=RunSubthreadTracedHelper",
absl::StrCat("--pidfd_pipe_fd=", read_pipe.get()),
absl::StrCat("--pidfd_tid_pipe_fd=", tid_write_pipe.get())},
{}, &child, &exec_errno));
ASSERT_EQ(exec_errno, 0);
ASSERT_THAT(child, SyscallSucceeds());
tid_write_pipe.reset();
read_pipe.reset();
// Wait for the TID of the subthread and open a pidfd.
pid_t subthread_tid;
ASSERT_THAT(read(tid_read_pipe.get(), &subthread_tid, sizeof(subthread_tid)),
SyscallSucceedsWithValue(sizeof(subthread_tid)));
auto subthread_pidfd =
ASSERT_NO_ERRNO_AND_VALUE(PidfdOpen(subthread_tid, PIDFD_THREAD));
// Start tracing the subthread.
ASSERT_THAT(ptrace(PTRACE_ATTACH, subthread_tid, nullptr, nullptr),
SyscallSucceeds());
int status;
ASSERT_THAT(waitpid(subthread_tid, &status, 0),
SyscallSucceedsWithValue(subthread_tid));
ASSERT_TRUE(WIFSTOPPED(status) && WSTOPSIG(status) == SIGSTOP);
ASSERT_THAT(ptrace(PTRACE_CONT, subthread_tid, nullptr, nullptr),
SyscallSucceeds());
// Tell the thread to exit.
ASSERT_THAT(write(write_pipe.get(), "a", 1), SyscallSucceedsWithValue(1));
// The pidfd should notify even though the tracer has not yet acked the exit.
struct pollfd pfd = {
.fd = subthread_pidfd.get(),
.events = POLLIN,
};
EXPECT_THAT(poll(&pfd, 1, 1000), SyscallSucceedsWithValue(1));
EXPECT_TRUE(pfd.revents & POLLIN);
// Tracer's ack of the exit.
ASSERT_THAT(waitpid(subthread_tid, &status, 0),
SyscallSucceedsWithValue(subthread_tid));
ASSERT_TRUE(WIFEXITED(status));
}
// PidfdThreadTracedLeaderNotification verifies that a PIDFD_THREAD for a
// traced thread group leader is delayed until the entire thread group exits,
// but once triggered, the notification occurs before the tracer reaps the
// leader.
TEST(PidfdTest, PidfdThreadTracedLeaderNotification) {
if (!IsRunningOnGvisor()) {
// Test behavior on kernels with PIDFD_THREAD.
KernelVersion version = ASSERT_NO_ERRNO_AND_VALUE(GetKernelVersion());
if (version.major < 6 || (version.major == 6 && version.minor < 15)) {
GTEST_SKIP() << "PIDFD_THREAD behavior requires kernel 6.15+";
}
}
int pipe_fds[2];
ASSERT_THAT(pipe(pipe_fds), SyscallSucceeds());
FileDescriptor read_pipe(pipe_fds[0]);
FileDescriptor write_pipe(pipe_fds[1]);
pid_t child;
int exec_errno;
auto cleanup = ASSERT_NO_ERRNO_AND_VALUE(
ForkAndExec("/proc/self/exe",
{"/proc/self/exe", "--pidfd_helper=RunLeaderDelayedHelper",
absl::StrCat("--pidfd_pipe_fd=", read_pipe.get())},
{}, &child, &exec_errno));
ASSERT_EQ(exec_errno, 0);
ASSERT_THAT(child, SyscallSucceeds());
read_pipe.reset();
auto leader_pidfd = ASSERT_NO_ERRNO_AND_VALUE(PidfdOpen(child, PIDFD_THREAD));
// Start tracing the leader.
ASSERT_THAT(ptrace(PTRACE_ATTACH, child, nullptr, nullptr),
SyscallSucceeds());
int status;
ASSERT_THAT(waitpid(child, &status, 0), SyscallSucceedsWithValue(child));
ASSERT_TRUE(WIFSTOPPED(status) && WSTOPSIG(status) == SIGSTOP);
ASSERT_THAT(ptrace(PTRACE_CONT, child, nullptr, nullptr), SyscallSucceeds());
// The leader should exit quickly as it calls syscall(SYS_exit).
// Poll should timeout as long as the subthread is alive, confirming the
// delay is preserved even for traced leaders.
struct pollfd pfd = {
.fd = leader_pidfd.get(),
.events = POLLIN,
};
EXPECT_THAT(poll(&pfd, 1, 1000), SyscallSucceedsWithValue(0));
// Now let the subthread exit.
ASSERT_THAT(write(write_pipe.get(), "a", 1), SyscallSucceedsWithValue(1));
write_pipe.reset();
// Now the leader's pidfd should notify, even though the tracer has not yet
// acked the exit.
EXPECT_THAT(poll(&pfd, 1, 5000), SyscallSucceedsWithValue(1));
EXPECT_TRUE(pfd.revents & POLLIN);
// Tracer's ack of the exit.
ASSERT_THAT(waitpid(child, &status, 0), SyscallSucceedsWithValue(child));
ASSERT_TRUE(WIFEXITED(status));
}
TEST(PidfdTest, FasyncIsUnsupported) {
pid_t child;
auto pidfd = ASSERT_NO_ERRNO_AND_VALUE(Clone3Pidfd(child, []() {
pause();
_exit(0);
}));
ScopedChildReaper cleanup(child);
int flags = fcntl(pidfd.get(), F_GETFL);
ASSERT_THAT(flags, SyscallSucceeds());
ASSERT_THAT(fcntl(pidfd.get(), F_SETFL, flags | FASYNC), SyscallSucceeds());
int who = getpid();
int want_errno = EINVAL;
if (!IsRunningOnGvisor()) {
KernelVersion version = ASSERT_NO_ERRNO_AND_VALUE(GetKernelVersion());
if (version.major < 6 || (version.major == 6 && version.minor < 9)) {
want_errno = ENOTTY;
}
}
EXPECT_THAT(ioctl(pidfd.get(), FIOSETOWN, &who),
SyscallFailsWithErrno(want_errno));
}
// This reproduces the race condition where pidfd_send_signal can target
// a cloned task whose struct task is initialized but has not
// yet completed `updateInfoLocked()` (initializing `logPrefix`), triggering
// a nil pointer dereference on the target task's Debugf/Infof logging paths.
TEST(PidfdTest, SendSignalToStartingTaskRace) {
DisableSave ds; // Violent test that hopes to trigger a crash.
std::atomic<int> shared_pidfd{-1};
std::atomic<bool> stop_signal_thread{false};
// These threads will try to race with clone(CLONE_PIDFD)
// to get at the pidfd FD before the corresponding task is
// fully initialized.
std::unique_ptr<ScopedThread> signallers[4];
for (int i = 0; i < 4; ++i) {
signallers[i] =
std::make_unique<ScopedThread>([&shared_pidfd, &stop_signal_thread]() {
while (!stop_signal_thread.load(std::memory_order_relaxed)) {
int fd = shared_pidfd.load(std::memory_order_relaxed);
if (fd != -1) {
syscall(SYS_pidfd_send_signal, fd, SIGUSR1, nullptr, 0);
}
}
});
}
for (int iter = 0; iter < 20; ++iter) {
shared_pidfd.store(-1, std::memory_order_relaxed);
clone_args ca = {};
ca.flags = CLONE_PIDFD;
ca.exit_signal = SIGCHLD;
ca.pidfd = reinterpret_cast<uint64_t>(&shared_pidfd);
pid_t child = syscall(SYS_clone3, &ca, sizeof(ca));
if (child == 0) {
// Child immediately exits.
_exit(0);
}
ASSERT_GT(child, 0);
int status;
EXPECT_THAT(RetryEINTR(waitpid)(child, &status, 0),
SyscallSucceedsWithValue(child));
int fd = shared_pidfd.load(std::memory_order_relaxed);
if (fd != -1) {
close(fd);
}
}
stop_signal_thread.store(true, std::memory_order_relaxed);
}
TEST(PidfdTest, PidfdGetfdNonDumpableExitingRace) {
// Skip when running natively because the host kernel may be vulnerable to
// CVE-2026-46333, which would cause the test to fail. We only want to verify
// the fix inside the gVisor sandbox.
SKIP_IF(!IsRunningOnGvisor());
AutoCapability cap(CAP_SYS_PTRACE, false);
for (int iter = 0; iter < 100; ++iter) {
int pfd[2];
ASSERT_THAT(pipe(pfd), SyscallSucceeds());
FileDescriptor read_pipe(pfd[0]);
FileDescriptor write_pipe(pfd[1]);
pid_t child = -1;
auto pidfd = ASSERT_NO_ERRNO_AND_VALUE(
Clone3Pidfd(child, [&write_pipe, &read_pipe]() {
read_pipe.reset();
TEST_PCHECK(prctl(PR_SET_DUMPABLE, 0, 0, 0, 0) == 0);
char c = 'r';
TEST_PCHECK(write(write_pipe.get(), &c, 1) == 1);
write_pipe.reset();
_exit(0);
}));
ScopedChildReaper reaper(child);
write_pipe.reset();
char c;
ASSERT_THAT(read(read_pipe.get(), &c, 1), SyscallSucceedsWithValue(1));
read_pipe.reset();
bool succeeded = false;
for (;;) {
auto res = PidfdGetfd(pidfd.get(), 0, 0);
if (res.ok()) {
succeeded = true;
break;
} else {
if (res.error().errno_value() == ESRCH) {
break;
}
}
}
EXPECT_FALSE(succeeded) << "Unexpectedly succeeded calling pidfd_getfd on "
"non-dumpable exiting process at iteration "
<< iter;
if (succeeded) {
break;
}
}
}
} // namespace
} // namespace testing
} // namespace gvisor
int main(int argc, char** argv) {
gvisor::testing::TestInit(&argc, &argv);
std::string helper = absl::GetFlag(FLAGS_pidfd_helper);
if (!helper.empty()) {
if (helper == "RunLeaderDelayedHelper") {
gvisor::testing::RunLeaderDelayedHelper();
} else if (helper == "RunSubthreadNotificationHelper") {
gvisor::testing::RunSubthreadNotificationHelper();
} else if (helper == "RunExecRevivalHelper") {
gvisor::testing::RunExecRevivalHelper();
} else if (helper == "RunSubthreadTracedHelper") {
gvisor::testing::RunSubthreadTracedHelper();
} else if (helper == "RunPauseHelper") {
gvisor::testing::RunPauseHelper();
}
return 0;
}
return gvisor::testing::RunAllTests();
}