From e7bd1b4c9cb35726bc5266e5410048dcdc9ea1aa Mon Sep 17 00:00:00 2001 From: Nicolas Lacasse Date: Fri, 14 Jul 2023 13:16:52 -0700 Subject: [PATCH] Implement PR_{S,G}ET_CHILD_SUBREAPER. Closes #2323 PiperOrigin-RevId: 548205854 --- pkg/sentry/kernel/task_exit.go | 35 ++++- pkg/sentry/kernel/task_start.go | 8 +- pkg/sentry/kernel/thread_group.go | 84 +++++++++++- pkg/sentry/kernel/threads.go | 6 +- pkg/sentry/syscalls/linux/sys_prctl.go | 21 ++- pkg/sentry/syscalls/linux/sys_signal.go | 3 +- test/cmd/test_app/BUILD | 7 +- test/cmd/test_app/main.go | 3 +- test/cmd/test_app/zombies.go | 168 ++++++++++++++++++++++++ test/syscalls/linux/BUILD | 2 + test/syscalls/linux/prctl.cc | 93 ++++++++++++- test/util/test_util_runfiles.cc | 23 +++- 12 files changed, 425 insertions(+), 28 deletions(-) create mode 100644 test/cmd/test_app/zombies.go diff --git a/pkg/sentry/kernel/task_exit.go b/pkg/sentry/kernel/task_exit.go index ef4d9556c..30d299bd8 100644 --- a/pkg/sentry/kernel/task_exit.go +++ b/pkg/sentry/kernel/task_exit.go @@ -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 } diff --git a/pkg/sentry/kernel/task_start.go b/pkg/sentry/kernel/task_start.go index 5745afc31..0a8c8c04a 100644 --- a/pkg/sentry/kernel/task_start.go +++ b/pkg/sentry/kernel/task_start.go @@ -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? diff --git a/pkg/sentry/kernel/thread_group.go b/pkg/sentry/kernel/thread_group.go index 8285503c7..4fce5e916 100644 --- a/pkg/sentry/kernel/thread_group.go +++ b/pkg/sentry/kernel/thread_group.go @@ -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 diff --git a/pkg/sentry/kernel/threads.go b/pkg/sentry/kernel/threads.go index 791c4d1bd..e0a1596ea 100644 --- a/pkg/sentry/kernel/threads.go +++ b/pkg/sentry/kernel/threads.go @@ -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. // diff --git a/pkg/sentry/syscalls/linux/sys_prctl.go b/pkg/sentry/syscalls/linux/sys_prctl.go index cd4b56a75..949f6ad13 100644 --- a/pkg/sentry/syscalls/linux/sys_prctl.go +++ b/pkg/sentry/syscalls/linux/sys_prctl.go @@ -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, diff --git a/pkg/sentry/syscalls/linux/sys_signal.go b/pkg/sentry/syscalls/linux/sys_signal.go index 733c33be7..b2ca06f94 100644 --- a/pkg/sentry/syscalls/linux/sys_signal.go +++ b/pkg/sentry/syscalls/linux/sys_signal.go @@ -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 } diff --git a/test/cmd/test_app/BUILD b/test/cmd/test_app/BUILD index c02fc3d20..6035980c4 100644 --- a/test/cmd/test_app/BUILD +++ b/test/cmd/test_app/BUILD @@ -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", ], ) diff --git a/test/cmd/test_app/main.go b/test/cmd/test_app/main.go index 179578b29..fc91aebee 100644 --- a/test/cmd/test_app/main.go +++ b/test/cmd/test_app/main.go @@ -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() diff --git a/test/cmd/test_app/zombies.go b/test/cmd/test_app/zombies.go new file mode 100644 index 000000000..ee37306ce --- /dev/null +++ b/test/cmd/test_app/zombies.go @@ -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 + } +} diff --git a/test/syscalls/linux/BUILD b/test/syscalls/linux/BUILD index b596aa73b..597c403b2 100644 --- a/test/syscalls/linux/BUILD +++ b/test/syscalls/linux/BUILD @@ -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", ], diff --git a/test/syscalls/linux/prctl.cc b/test/syscalls/linux/prctl.cc index 286b3d168..f76a34fb3 100644 --- a/test/syscalls/linux/prctl.cc +++ b/test/syscalls/linux/prctl.cc @@ -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 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 diff --git a/test/util/test_util_runfiles.cc b/test/util/test_util_runfiles.cc index 7210094eb..d70dffa86 100644 --- a/test/util/test_util_runfiles.cc +++ b/test/util/test_util_runfiles.cc @@ -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 + // "/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