Implement PR_{S,G}ET_CHILD_SUBREAPER.

Closes #2323

PiperOrigin-RevId: 548205854
This commit is contained in:
Nicolas Lacasse
2023-07-14 13:19:25 -07:00
committed by gVisor bot
parent f5049f6885
commit e7bd1b4c9c
12 changed files with 425 additions and 28 deletions
+30 -5
View File
@@ -397,6 +397,8 @@ func (t *Task) exitChildren() {
// findReparentTargetLocked returns the task to which t's children should be
// reparented. If no such task exists, findNewParentLocked returns nil.
//
// This corresponds to Linux's find_new_reaper().
//
// Preconditions: The TaskSet mutex must be locked.
func (t *Task) findReparentTargetLocked() *Task {
// Reparent to any sibling in the same thread group that hasn't begun
@@ -404,12 +406,35 @@ func (t *Task) findReparentTargetLocked() *Task {
if t2 := t.tg.anyNonExitingTaskLocked(); t2 != nil {
return t2
}
// "A child process that is orphaned within the namespace will be
// reparented to [the init process for the namespace] ..." -
// pid_namespaces(7)
if init := t.tg.pidns.tasks[InitTID]; init != nil {
return init.tg.anyNonExitingTaskLocked()
if !t.tg.hasChildSubreaper {
// No child subreaper exists. We can immediately return the
// init process in this PID namespace if it exists.
if init := t.tg.pidns.tasks[initTID]; init != nil {
return init.tg.anyNonExitingTaskLocked()
}
return nil
}
// Walk up the process tree until we either find a subreaper, or we hit
// the init process in the PID namespace.
for parent := t.parent; parent != nil; parent = parent.parent {
if parent.tg.isInitInLocked(parent.PIDNamespace()) {
// We found the init process for this pid namespace,
// return a task from it. If the init process is
// exiting, this might return nil.
return parent.tg.anyNonExitingTaskLocked()
}
if parent.tg.isChildSubreaper {
// We found a subreaper process. Return a non-exiting
// task if there is one, otherwise keep walking up the
// process tree.
if target := parent.tg.anyNonExitingTaskLocked(); target != nil {
return target
}
}
}
return nil
}
+7 -1
View File
@@ -258,6 +258,12 @@ func (ts *TaskSet) newTask(ctx context.Context, cfg *TaskConfig) (*Task, error)
tg.processGroup = parentPG
tg.tty = t.parent.tg.tty
}
// If our parent is a child subreaper, or if it has a child
// subreaper, then this new thread group does as well.
if t.parent != nil {
tg.hasChildSubreaper = t.parent.tg.isChildSubreaper || t.parent.tg.hasChildSubreaper
}
}
tg.tasks.PushBack(t)
tg.tasksCount++
@@ -330,7 +336,7 @@ func (ns *PIDNamespace) allocateTID() (ThreadID, error) {
// Next.
tid++
if tid > TasksLimit {
tid = InitTID + 1
tid = initTID + 1
}
// Is it available?
+83 -1
View File
@@ -250,6 +250,21 @@ type ThreadGroup struct {
// currently not used but is maintained for consistency.
// TODO(gvisor.dev/issue/1967)
oomScoreAdj atomicbitops.Int32
// isChildSubreaper and hasChildSubreaper correspond to Linux's
// signal_struct::is_child_subreaper and has_child_subreaper.
//
// Both fields are protected by the TaskSet mutex.
//
// Quoting from signal.h:
// "PR_SET_CHILD_SUBREAPER marks a process, like a service manager, to
// re-parent orphan (double-forking) child processes to this process
// instead of 'init'. The service manager is able to receive SIGCHLD
// signals and is able to investigate the process until it calls
// wait(). All children of this process will inherit a flag if they
// should look for a child_subreaper process at exit"
isChildSubreaper bool
hasChildSubreaper bool
}
// NewThreadGroup returns a new, empty thread group in PID namespace pidns. The
@@ -318,10 +333,30 @@ func (tg *ThreadGroup) Release(ctx context.Context) {
//
// Precondition: TaskSet.mu must be held.
func (tg *ThreadGroup) forEachChildThreadGroupLocked(fn func(*ThreadGroup)) {
tg.walkDescendantThreadGroupsLocked(func(child *ThreadGroup) bool {
fn(child)
// Don't recurse below the immediate children.
return false
})
}
// walkDescendantThreadGroupsLocked recursively walks all descendent
// ThreadGroups and executes the visitor function. If visitor returns false for
// a given ThreadGroup, then that ThreadGroups descendants are excluded from
// further iteration.
//
// This corresponds to Linux's walk_process_tree.
//
// Precondition: TaskSet.mu must be held.
func (tg *ThreadGroup) walkDescendantThreadGroupsLocked(visitor func(*ThreadGroup) bool) {
for t := tg.tasks.Front(); t != nil; t = t.Next() {
for child := range t.children {
if child == child.tg.leader {
fn(child.tg)
if !visitor(child.tg) {
// Don't recurse below child.
continue
}
child.tg.walkDescendantThreadGroupsLocked(visitor)
}
}
}
@@ -512,6 +547,53 @@ func (tg *ThreadGroup) SetForegroundProcessGroupID(tty *TTY, pgid ProcessGroupID
return nil
}
// SetChildSubreaper marks this ThreadGroup sets the isChildSubreaper field on
// this ThreadGroup, and marks all child ThreadGroups as having a subreaper.
// Recursion stops if we find another subreaper process, which is either a
// ThreadGroup with isChildSubreaper bit set, or a ThreadGroup with PID=1
// inside a PID namespace.
func (tg *ThreadGroup) SetChildSubreaper(isSubreaper bool) {
ts := tg.TaskSet()
ts.mu.Lock()
defer ts.mu.Unlock()
tg.isChildSubreaper = isSubreaper
tg.walkDescendantThreadGroupsLocked(func(child *ThreadGroup) bool {
// Is this child PID 1 in its PID namespace, or already a
// subreaper?
if child.isInitInLocked(child.PIDNamespace()) || child.isChildSubreaper {
// Don't set hasChildSubreaper, and don't recurse.
return false
}
child.hasChildSubreaper = isSubreaper
return true // Recurse.
})
}
// IsChildSubreaper returns whether this ThreadGroup is a child subreaper.
func (tg *ThreadGroup) IsChildSubreaper() bool {
ts := tg.TaskSet()
ts.mu.RLock()
defer ts.mu.RUnlock()
return tg.isChildSubreaper
}
// IsInitIn returns whether this ThreadGroup has TID 1 int the given
// PIDNamespace.
func (tg *ThreadGroup) IsInitIn(pidns *PIDNamespace) bool {
ts := tg.TaskSet()
ts.mu.RLock()
defer ts.mu.RUnlock()
return tg.isInitInLocked(pidns)
}
// isInitInLocked returns whether this ThreadGroup has TID 1 in the given
// PIDNamespace.
//
// Preconditions: TaskSet.mu must be locked.
func (tg *ThreadGroup) isInitInLocked(pidns *PIDNamespace) bool {
return pidns.tgids[tg] == initTID
}
// itimerRealListener implements ktime.Listener for ITIMER_REAL expirations.
//
// +stateify savable
+3 -3
View File
@@ -46,11 +46,11 @@ func (tid ThreadID) String() string {
return fmt.Sprintf("%d", tid)
}
// InitTID is the TID given to the first task added to each PID namespace. The
// thread group led by InitTID is called the namespace's init process. The
// initTID is the TID given to the first task added to each PID namespace. The
// thread group led by initTID is called the namespace's init process. The
// death of a PID namespace's init process causes all tasks visible in that
// namespace to be killed.
const InitTID ThreadID = 1
const initTID ThreadID = 1
// A TaskSet comprises all tasks in a system.
//
+10 -11
View File
@@ -225,17 +225,17 @@ func Prctl(t *kernel.Task, sysno uintptr, args arch.SyscallArguments) (uintptr,
case linux.PR_SET_CHILD_SUBREAPER:
// "If arg2 is nonzero, set the "child subreaper" attribute of
// the calling process; if arg2 is zero, unset the attribute."
//
// TODO(gvisor.dev/issues/2323): We only support setting, and
// only if the task is already TID 1 in the PID namespace,
// because it already acts as a subreaper in that case.
isPid1 := t.PIDNamespace().IDOfTask(t) == kernel.InitTID
if args[1].Int() != 0 && isPid1 {
return 0, nil, nil
}
isSubreaper := args[1].Int() != 0
t.ThreadGroup().SetChildSubreaper(isSubreaper)
return 0, nil, nil
t.Kernel().EmitUnimplementedEvent(t, sysno)
return 0, nil, linuxerr.EINVAL
case linux.PR_GET_CHILD_SUBREAPER:
var isSubreaper int32
if t.ThreadGroup().IsChildSubreaper() {
isSubreaper = 1
}
_, err := primitive.CopyInt32Out(t, args[1].Pointer(), isSubreaper)
return 0, nil, err
case linux.PR_GET_TIMING,
linux.PR_SET_TIMING,
@@ -248,7 +248,6 @@ func Prctl(t *kernel.Task, sysno uintptr, args arch.SyscallArguments) (uintptr,
linux.PR_MCE_KILL,
linux.PR_MCE_KILL_GET,
linux.PR_GET_TID_ADDRESS,
linux.PR_GET_CHILD_SUBREAPER,
linux.PR_GET_THP_DISABLE,
linux.PR_SET_THP_DISABLE,
linux.PR_MPX_ENABLE_MANAGEMENT,
+2 -1
View File
@@ -110,7 +110,8 @@ func Kill(t *kernel.Task, sysno uintptr, args arch.SyscallArguments) (uintptr, *
if tg == t.ThreadGroup() {
continue
}
if t.PIDNamespace().IDOfThreadGroup(tg) == kernel.InitTID {
// Don't send the signal to the init process in t's PID namespace.
if tg.IsInitIn(t.PIDNamespace()) {
continue
}
+6 -1
View File
@@ -11,14 +11,19 @@ go_binary(
srcs = [
"fds.go",
"main.go",
"zombies.go",
],
static = True,
visibility = ["//runsc/container:__pkg__"],
visibility = [
"//runsc/container:__pkg__",
"//test/syscalls/linux:__pkg__",
],
deps = [
"//pkg/test/testutil",
"//pkg/unet",
"//runsc/flag",
"@com_github_google_subcommands//:go_default_library",
"@com_github_kr_pty//:go_default_library",
"@org_golang_x_sys//unix:go_default_library",
],
)
+2 -1
View File
@@ -46,12 +46,13 @@ func main() {
subcommands.Register(new(fdReceiver), "")
subcommands.Register(new(fdSender), "")
subcommands.Register(new(forkBomb), "")
subcommands.Register(new(fsTreeCreator), "")
subcommands.Register(new(ptyRunner), "")
subcommands.Register(new(reaper), "")
subcommands.Register(new(syscall), "")
subcommands.Register(new(taskTree), "")
subcommands.Register(new(uds), "")
subcommands.Register(new(fsTreeCreator), "")
subcommands.Register(new(zombieTest), "")
flag.Parse()
+168
View File
@@ -0,0 +1,168 @@
// Copyright 2021 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.
package main
import (
"context"
"fmt"
"log"
"os"
"os/exec"
"strconv"
"strings"
sys "syscall"
"time"
"github.com/google/subcommands"
"golang.org/x/sys/unix"
"gvisor.dev/gvisor/runsc/flag"
)
func fatalf(s string, args ...any) {
fmt.Fprintf(os.Stderr, s+"\n", args...)
os.Exit(1)
}
// zombieTest creates an orphaned process that will be reparented to PID 1
// (or the nearest subreaper) and expect that it is reaped.
//
// The setup involves three different processes:
//
// 1. The zombiemonitor process starts the zombieparent process and reads the
// zombiechild process pid from zombieparent's stdout. It waits on
// zombieparent, and after that dies, zombiechild will be reparented to PID 1
// (or nearest subreaper). The zombiemonitor kills zombiechild and expects
// that it will be reaped.
//
// 2. The zombieparent process starts the zombiechild process, writes the
// zombiechild process pid to stdout, and exits, causing zombiechild to be
// reparented to PID 1 (or nearest subreaper).
//
// 3. zombiechild just waits until it is killed.
type zombieTest struct{}
// Name implements subcommands.Command.Name.
func (*zombieTest) Name() string {
return "zombie_test"
}
// Synopsis implements subcommands.Command.Synopsys.
func (*zombieTest) Synopsis() string {
return "creates an orphaned grandchild and expects to be reparented and reaped."
}
// Usage implements subcommands.Command.Usage.
func (*zombieTest) Usage() string {
return "Usage: zombie_test [zombieparent|zombiechild]"
}
// SetFlags implements subcommands.Command.SetFlags.
func (*zombieTest) SetFlags(f *flag.FlagSet) {}
// Execute implements subcommands.Command.Execute.
func (zt *zombieTest) Execute(ctx context.Context, f *flag.FlagSet, args ...any) subcommands.ExitStatus {
n := f.NArg()
if n > 1 {
log.Fatal(zt.Usage())
}
if n == 0 {
// Run the monitor, which is the main entrypoint of this program.
runOrphanMonitor()
return subcommands.ExitSuccess
}
// One argument passed
switch f.Arg(0) {
case "zombieparent":
runZombieParent()
case "zombiechild":
runZombieChild()
default:
log.Fatal(zt.Usage())
}
return subcommands.ExitSuccess
}
func runOrphanMonitor() {
// Start the zombieparent and read its output. The call to
// CombinedOutput() will wait() on zombieparent, so when it returns we
// know that zombiechild has been orphaned and reparented to PID 1.
cmd := exec.Command("/proc/self/exe", "zombie_test", "zombieparent")
out, err := cmd.CombinedOutput()
if err != nil {
log.Fatalf("failed to exec zombieparent: %v\noutput: %s\n", err, string(out))
}
// Parse zombiechild pid from zombieparent output.
zombieChildPid, err := strconv.Atoi(strings.TrimSpace(string(out)))
if err != nil {
log.Fatalf("failed to parse zombieparent output: %q", string(out))
}
fmt.Printf("started zombiechild with pid %d\n", zombieChildPid)
// Kill the zombiechild.
fmt.Printf("killing zombiechild\n")
if err := unix.Kill(zombieChildPid, unix.SIGTERM); err != nil {
log.Fatalf("error killing for zombiechild: %v", err)
}
// Wait for zombiechild to be reaped by PID 1.
if err := waitForZombieReaped(zombieChildPid, 10*time.Second); err != nil {
log.Fatalf("error waiting for zombiechild to be reaped: %v", err)
}
fmt.Printf("zombiechild has been reaped\n")
// Success.
}
func runZombieParent() {
// Start the zombiechild, and write the pid to stdout.
cmd := exec.Command("/proc/self/exe", "zombie_test", "zombiechild")
if err := cmd.Start(); err != nil {
log.Fatalf("failed to exec zombiechild: %v", err)
}
fmt.Fprint(os.Stdout, strconv.Itoa(cmd.Process.Pid))
// Die. This will cause zombiechild to be reparented.
}
func runZombieChild() {
// Sleep for a long time. We will be killed before this exits.
time.Sleep(1 * time.Minute)
}
// waitForZombieReaped sends a harmless signal to the given pid until it gets
// ESRCH, indicating that the process has been reaped, or until the timeout is
// reached.
func waitForZombieReaped(pid int, timeout time.Duration) error {
deadline := time.Now().Add(timeout)
for {
if time.Now().After(deadline) {
return fmt.Errorf("pid %d was not reaped after %v", pid, timeout)
}
err := unix.Kill(pid, 0)
if err == nil {
fmt.Printf("pid %d still exists\n", pid)
time.Sleep(1 * time.Second)
continue
}
if errno := err.(sys.Errno); errno != unix.ESRCH {
return fmt.Errorf("unexpected error signalling pid %d: %v", pid, err)
}
fmt.Printf("pid %d has been reaped\n", pid)
return nil
}
}
+2
View File
@@ -1714,6 +1714,7 @@ cc_binary(
name = "prctl_test",
testonly = 1,
srcs = ["prctl.cc"],
data = ["//test/cmd/test_app"],
linkstatic = 1,
deps = [
"//test/util:capability_util",
@@ -1722,6 +1723,7 @@ cc_binary(
gtest,
"//test/util:multiprocess_util",
"//test/util:posix_error",
"//test/util:signal_util",
"//test/util:test_util",
"//test/util:thread_util",
],
+90 -3
View File
@@ -26,6 +26,7 @@
#include "test/util/cleanup.h"
#include "test/util/multiprocess_util.h"
#include "test/util/posix_error.h"
#include "test/util/signal_util.h"
#include "test/util/test_util.h"
#include "test/util/thread_util.h"
@@ -214,10 +215,96 @@ TEST(PrctlTest, RootDumpability) {
SyscallFailsWithErrno(EINVAL));
}
TEST(PrctlTest, SetGetSubreaper) {
// Setting subreaper on PID 1 works vacuously because PID 1 is always a
// subreaper.
TEST(PrctlTest, SimpleSetGetChildSubreaper) {
// Tasks start off not subreaper.
int is_subreaper = 0;
EXPECT_THAT(prctl(PR_GET_CHILD_SUBREAPER, &is_subreaper), SyscallSucceeds());
EXPECT_EQ(is_subreaper, 0);
// Set to 1.
EXPECT_THAT(prctl(PR_SET_CHILD_SUBREAPER, 1), SyscallSucceeds());
EXPECT_THAT(prctl(PR_GET_CHILD_SUBREAPER, &is_subreaper), SyscallSucceeds());
EXPECT_EQ(is_subreaper, 1);
// Set to something positive but not 1.
EXPECT_THAT(prctl(PR_SET_CHILD_SUBREAPER, 42), SyscallSucceeds());
// Get still returns 1.
EXPECT_THAT(prctl(PR_GET_CHILD_SUBREAPER, &is_subreaper), SyscallSucceeds());
EXPECT_EQ(is_subreaper, 1);
// Set to something negative.
EXPECT_THAT(prctl(PR_SET_CHILD_SUBREAPER, -42), SyscallSucceeds());
// Get still returns 1.
EXPECT_THAT(prctl(PR_GET_CHILD_SUBREAPER, &is_subreaper), SyscallSucceeds());
EXPECT_EQ(is_subreaper, 1);
}
TEST(PrctlTest, ThreadsInheritChildSubreaperBit) {
// Set child subreaper bit.
ASSERT_THAT(prctl(PR_SET_CHILD_SUBREAPER, 1), SyscallSucceeds());
ScopedThread thread([&] {
int is_subreaper = 0;
ASSERT_THAT(prctl(PR_GET_CHILD_SUBREAPER, &is_subreaper),
SyscallSucceeds());
EXPECT_EQ(is_subreaper, 1);
});
}
TEST(PrctlTest, ProcessesDoNotInheritChildSubreaperBit) {
// Set child subreaper bit.
ASSERT_THAT(prctl(PR_SET_CHILD_SUBREAPER, 1), SyscallSucceeds());
const auto rest = [&] {
int is_subreaper = 0;
TEST_CHECK_SUCCESS(prctl(PR_GET_CHILD_SUBREAPER, &is_subreaper));
TEST_CHECK(is_subreaper == 0);
};
EXPECT_THAT(InForkedProcess(rest), IsPosixErrorOkAndHolds(0));
}
static std::atomic<bool> got_sigchild;
void sigchild_handler(int sig, siginfo_t* siginfo, void* arg) {
got_sigchild = true;
}
TEST(PrctlTest, OrphansReparentedToSubreaper) {
// Set the subreaper bit.
ASSERT_THAT(prctl(PR_SET_CHILD_SUBREAPER, 1), SyscallSucceeds());
// Set up a signal handler to listen for reparented children.
struct sigaction sa = {};
sa.sa_sigaction = sigchild_handler;
sigfillset(&sa.sa_mask);
auto const sig_cleanup =
ASSERT_NO_ERRNO_AND_VALUE(ScopedSigaction(SIGCHLD, sa));
// Execute the test_app zombie_test, which will create an orphaned process
// and expect that it is reaped.
constexpr char kTestApp[] = "test/cmd/test_app/test_app";
const std::string path = RunfilePath(kTestApp);
int execve_errno;
pid_t pid;
auto exec_cleanup = ASSERT_NO_ERRNO_AND_VALUE(
ForkAndExec(path, {path, "zombie_test"}, {}, &pid, &execve_errno));
ASSERT_EQ(execve_errno, 0);
// Wait for 2 children: the process we just started, and the orphan that will
// be reparented to us.
for (int i = 0; i < 2; i++) {
int status;
int wait_pid;
ASSERT_THAT(wait_pid = RetryEINTR(waitpid)(-1, &status, 0),
SyscallSucceeds());
if (wait_pid == pid) {
// Test app should have exited cleanly.
EXPECT_EQ(status, 0);
}
}
// We should have gotten a SIGCHILD for the reparented orphan.
EXPECT_TRUE(got_sigchild);
}
} // namespace
+22 -1
View File
@@ -39,7 +39,28 @@ std::string RunfilePath(std::string path) {
return JoinPath("__main__", path);
}
return runfiles->Rlocation(JoinPath("__main__", path));
// Try to resolve the path as it was passed to us, and check that it exists
// before returning.
std::string runfile_path = runfiles->Rlocation(JoinPath("__main__", path));
struct stat st = {};
if (!runfile_path.empty() && stat(runfile_path.c_str(), &st) == 0) {
// Found it.
return runfile_path;
}
// You are not gonna like this, but go_binary data dependencies have an extra
// directory name with a "_" suffix, so we must check for that path too.
//
// For example, a go_binary with name"//foo/bar:baz" will be placed in
// "<runfiles_dir>/foo/bar/baz_/baz".
//
// See
// https://github.com/bazelbuild/rules_go/blob/d2a3cf2d6b18f5be19adccc6a6806e0c3b8c410b/go/private/context.bzl#L137.
absl::string_view dirname = Dirname(path);
absl::string_view basename = Basename(path);
std::string go_binary_path =
JoinPath(dirname, absl::StrCat(basename, "_"), basename);
return runfiles->Rlocation(JoinPath("__main__", go_binary_path));
}
} // namespace testing