mirror of
https://github.com/netbirdio/gvisor.git
synced 2026-05-22 17:12:49 -07:00
024d75d263
Setting t.tg.exiting during PrepareGroupExit prevents a SIGKILL that arrives while the task is zombied from changing the exit status (see ThreadGroup.applySignalSideEffectsLocked). However, PrepareExit does not set t.tg.exiting, meaning that if the last task exits via exit(2), then a SIGKILL _can_ change the exit status. This does not match Linux. Fix this by detecting that we are the last task in PrepareExit and, if so, going through the PrepareGroupExit path. My initial version of this CL had Task.exitThreadGroup set t.tg.exiting if it wasn't already set. This leaves a small window between return from exit(2) and the start of the runExit state where a SIGKILL could still change the exit status. While this window likely doesn't matter much in practice, I suspect it is observable via ptrace, which could see exit(2) return and assume that the exit status is fixed. I didn't test this case, but figured it would be cleaner to just close the race window. Fixes #7930.
179 lines
4.7 KiB
C++
179 lines
4.7 KiB
C++
// Copyright 2018 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 <sys/wait.h>
|
|
#include <unistd.h>
|
|
|
|
#include "gtest/gtest.h"
|
|
#include "absl/flags/flag.h"
|
|
#include "absl/time/time.h"
|
|
#include "test/util/file_descriptor.h"
|
|
#include "test/util/multiprocess_util.h"
|
|
#include "test/util/test_util.h"
|
|
#include "test/util/thread_util.h"
|
|
#include "test/util/time_util.h"
|
|
|
|
ABSL_FLAG(bool, test_child, false,
|
|
"If true, run the ExitAllThreads child workload.");
|
|
|
|
namespace gvisor {
|
|
namespace testing {
|
|
|
|
namespace {
|
|
|
|
void TestExit(int code) {
|
|
pid_t pid = fork();
|
|
if (pid == 0) {
|
|
_exit(code);
|
|
}
|
|
|
|
ASSERT_THAT(pid, SyscallSucceeds());
|
|
|
|
int status;
|
|
EXPECT_THAT(RetryEINTR(waitpid)(pid, &status, 0), SyscallSucceeds());
|
|
EXPECT_TRUE(WIFEXITED(status) && WEXITSTATUS(status) == code) << status;
|
|
}
|
|
|
|
TEST(ExitTest, Success) { TestExit(0); }
|
|
|
|
TEST(ExitTest, Failure) { TestExit(1); }
|
|
|
|
// This test ensures that a process's file descriptors are closed when it calls
|
|
// exit(). In order to test this, the parent tries to read from a pipe whose
|
|
// write end is held by the child. While the read is blocking, the child exits,
|
|
// which should cause the parent to read 0 bytes due to EOF.
|
|
TEST(ExitTest, CloseFds) {
|
|
int pipe_fds[2];
|
|
ASSERT_THAT(pipe(pipe_fds), SyscallSucceeds());
|
|
|
|
FileDescriptor read_fd(pipe_fds[0]);
|
|
FileDescriptor write_fd(pipe_fds[1]);
|
|
|
|
pid_t pid = fork();
|
|
if (pid == 0) {
|
|
read_fd.reset();
|
|
|
|
SleepSafe(absl::Seconds(10));
|
|
|
|
_exit(0);
|
|
}
|
|
|
|
EXPECT_THAT(pid, SyscallSucceeds());
|
|
|
|
write_fd.reset();
|
|
|
|
char buf[10];
|
|
EXPECT_THAT(ReadFd(read_fd.get(), buf, sizeof(buf)),
|
|
SyscallSucceedsWithValue(0));
|
|
}
|
|
|
|
TEST(ExitTest, ExitAllThreads) {
|
|
pid_t child_pid = -1;
|
|
int execve_errno = 0;
|
|
auto cleanup = ASSERT_NO_ERRNO_AND_VALUE(
|
|
ForkAndExec("/proc/self/exe", {"/proc/self/exe", "--test_child"}, {},
|
|
nullptr, &child_pid, &execve_errno));
|
|
ASSERT_GT(child_pid, 0);
|
|
ASSERT_EQ(execve_errno, 0);
|
|
|
|
int status;
|
|
EXPECT_THAT(RetryEINTR(waitpid)(child_pid, &status, 0), SyscallSucceeds());
|
|
EXPECT_TRUE(WIFEXITED(status) && WEXITSTATUS(status) == 0) << status;
|
|
}
|
|
|
|
void RunChild() {
|
|
ScopedThread t([] { _exit(0); });
|
|
t.Join();
|
|
// Should not be reached
|
|
abort();
|
|
}
|
|
|
|
// SIGKILL of zombied thread group does not change exit status.
|
|
TEST(ExitTest, SigkillZombieGroup) {
|
|
int pipe_fds[2];
|
|
ASSERT_THAT(pipe(pipe_fds), SyscallSucceeds());
|
|
|
|
FileDescriptor read_fd(pipe_fds[0]);
|
|
FileDescriptor write_fd(pipe_fds[1]);
|
|
|
|
pid_t pid = fork();
|
|
if (pid == 0) {
|
|
read_fd.reset();
|
|
|
|
_exit(0);
|
|
}
|
|
|
|
EXPECT_THAT(pid, SyscallSucceeds());
|
|
write_fd.reset();
|
|
|
|
// Wait for pipe to automatically close to indicate that the child is zombied.
|
|
char buf[10];
|
|
EXPECT_THAT(ReadFd(read_fd.get(), buf, sizeof(buf)),
|
|
SyscallSucceedsWithValue(0));
|
|
|
|
EXPECT_THAT(kill(pid, SIGKILL), SyscallSucceeds());
|
|
|
|
// SIGKILL did not change exit status.
|
|
int status;
|
|
EXPECT_THAT(RetryEINTR(waitpid)(pid, &status, 0), SyscallSucceeds());
|
|
EXPECT_TRUE(WIFEXITED(status) && WEXITSTATUS(status) == 0) << status;
|
|
}
|
|
|
|
// Variant of SigkillZombieGroup using exit(2) instead of exit_group(2).
|
|
TEST(ExitTest, SigkillZombieThread) {
|
|
int pipe_fds[2];
|
|
ASSERT_THAT(pipe(pipe_fds), SyscallSucceeds());
|
|
|
|
FileDescriptor read_fd(pipe_fds[0]);
|
|
FileDescriptor write_fd(pipe_fds[1]);
|
|
|
|
pid_t pid = fork();
|
|
if (pid == 0) {
|
|
read_fd.reset();
|
|
|
|
syscall(SYS_exit, 0);
|
|
}
|
|
|
|
EXPECT_THAT(pid, SyscallSucceeds());
|
|
write_fd.reset();
|
|
|
|
// Wait for pipe to automatically close to indicate that the child is zombied.
|
|
char buf[10];
|
|
EXPECT_THAT(ReadFd(read_fd.get(), buf, sizeof(buf)),
|
|
SyscallSucceedsWithValue(0));
|
|
|
|
EXPECT_THAT(kill(pid, SIGKILL), SyscallSucceeds());
|
|
|
|
// SIGKILL did not change exit status.
|
|
int status;
|
|
EXPECT_THAT(RetryEINTR(waitpid)(pid, &status, 0), SyscallSucceeds());
|
|
EXPECT_TRUE(WIFEXITED(status) && WEXITSTATUS(status) == 0) << status;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
} // namespace testing
|
|
} // namespace gvisor
|
|
|
|
int main(int argc, char** argv) {
|
|
gvisor::testing::TestInit(&argc, &argv);
|
|
|
|
if (absl::GetFlag(FLAGS_test_child)) {
|
|
gvisor::testing::RunChild();
|
|
return 1;
|
|
}
|
|
|
|
return gvisor::testing::RunAllTests();
|
|
}
|