blob: a582626a9616060813d2ede6ba66af191bcd9e8d [file] [log] [blame]
// Copyright 2023 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "src/developer/debug/debug_agent/linux_binary_launcher.h"
#include <fcntl.h>
#include <sys/ptrace.h>
#include <sys/resource.h>
#include <sys/wait.h>
#include <unistd.h>
#include <zircon/errors.h>
#include "src/developer/debug/debug_agent/linux_process_handle.h"
#include "src/developer/debug/debug_agent/linux_task.h"
#include "src/developer/debug/debug_agent/linux_utils.h"
#include "src/developer/debug/debug_agent/posix/dir_reader_linux.h"
#include "src/developer/debug/debug_agent/posix/eintr_wrapper.h"
#include "src/developer/debug/debug_agent/posix/file_descriptor_shuffle.h"
#include "src/lib/fxl/strings/string_printf.h"
namespace debug_agent {
namespace {
static const rlim_t kSystemDefaultMaxFds = 8192;
static const char kFDDir[] = "/proc/self/fd";
size_t GetMaxFds() {
rlim_t max_fds;
struct rlimit nofile;
if (getrlimit(RLIMIT_NOFILE, &nofile)) {
// getrlimit failed. Take a best guess.
max_fds = kSystemDefaultMaxFds;
} else {
max_fds = nofile.rlim_cur;
}
if (max_fds > INT_MAX)
max_fds = INT_MAX;
return static_cast<size_t>(max_fds);
}
void CloseSuperfluousFds(const posix::InjectiveMultimap& saved_mapping) {
// DANGER: no calls to malloc or locks are allowed from now on:
// http://crbug.com/36678
// Get the maximum number of FDs possible.
size_t max_fds = GetMaxFds();
::linux::DirReaderLinux fd_dir(kFDDir);
if (!fd_dir.IsValid()) {
// Fallback case: Try every possible fd.
for (size_t i = 0; i < max_fds; ++i) {
const int fd = static_cast<int>(i);
if (fd == STDIN_FILENO || fd == STDOUT_FILENO || fd == STDERR_FILENO)
continue;
// Cannot use STL iterators here, since debug iterators use locks.
size_t j;
for (j = 0; j < saved_mapping.size(); j++) {
if (fd == saved_mapping[j].dest)
break;
}
if (j < saved_mapping.size())
continue;
// Since we're just trying to close anything we can find, ignore any error return values of
// close().
close(fd);
}
return;
}
const int dir_fd = fd_dir.fd();
for (; fd_dir.Next();) {
// Skip . and .. entries.
if (fd_dir.name()[0] == '.')
continue;
char* endptr;
errno = 0;
const long int fd = strtol(fd_dir.name(), &endptr, 10);
if (fd_dir.name()[0] == 0 || *endptr || fd < 0 || errno ||
fd > static_cast<decltype(fd)>(std::numeric_limits<int>::max())) {
continue;
}
if (fd == STDIN_FILENO || fd == STDOUT_FILENO || fd == STDERR_FILENO)
continue;
// Cannot use STL iterators here, since debug iterators use locks.
size_t i;
for (i = 0; i < saved_mapping.size(); i++) {
if (fd == saved_mapping[i].dest)
break;
}
if (i < saved_mapping.size())
continue;
if (fd == dir_fd)
continue;
int ret = IGNORE_EINTR(close(static_cast<int>(fd)));
FX_DCHECK(ret == 0);
}
}
} // namespace
debug::Status LinuxBinaryLauncher::Setup(const std::vector<std::string>& argv) {
if (argv.empty())
return debug::Status("No program specified");
// Create the pipes to redirect stdout and stderr. Note: [0] = read, [1] = write
int stdout_pipe[2] = {0, 0};
if (pipe2(stdout_pipe, 0) != 0) {
return debug::Status("Could not create pipe for stdout.");
}
int stderr_pipe[2] = {0, 0};
if (pipe2(stderr_pipe, 0) != 0) {
return debug::Status("Could not create pipe for stdout.");
}
// For redirecting stdin, stdout, and stderr. This needs to be allocated before forking to avoid
// allocating in the child. We need two copies because one will be destructively modified.
posix::InjectiveMultimap fd_shuffle1;
posix::InjectiveMultimap fd_shuffle2;
fd_shuffle1.reserve(3);
fd_shuffle2.reserve(3);
// Construct the parameter array.
std::vector<char*> ptrs;
for (const auto& arg : argv)
ptrs.push_back(const_cast<char*>(arg.c_str()));
ptrs.push_back(nullptr);
pid_ = fork();
if (pid_ == 0) {
// DANGER: fork() rule: in the child, if you don't end up doing exec*(), you call _exit()
// instead of exit(). This is because _exit() does not call any previously-registered (in the
// parent) exit handlers, which might do things like block waiting for threads that don't even
// exist in the child.
// Map /dev/null to stdin so programs waiting for input (as for readline) don't hang.
{
int null_fd = HANDLE_EINTR(open("/dev/null", O_RDONLY));
if (null_fd == -1) {
_exit(127);
}
int new_null_fd = HANDLE_EINTR(dup2(null_fd, STDIN_FILENO));
if (new_null_fd != STDIN_FILENO) {
_exit(127);
}
IGNORE_EINTR(close(null_fd));
}
// Redirect output to our pipes (for both copies of the shuffle structure).
fd_shuffle1.push_back(posix::InjectionArc(stdout_pipe[1], STDOUT_FILENO, false));
fd_shuffle2.push_back(posix::InjectionArc(stdout_pipe[1], STDOUT_FILENO, false));
fd_shuffle1.push_back(posix::InjectionArc(stderr_pipe[1], STDERR_FILENO, false));
fd_shuffle2.push_back(posix::InjectionArc(stderr_pipe[1], STDERR_FILENO, false));
// NOTE: fd_shuffle1 is destructively mutated by this call: it can not be used after this.
if (!posix::ShuffleFileDescriptors(&fd_shuffle1)) {
_exit(127);
}
CloseSuperfluousFds(fd_shuffle2);
ptrace(PTRACE_TRACEME, 0, nullptr, nullptr);
// Child process. Execve returns only on failure.
execv(argv[0].c_str(), &ptrs[0]);
_exit(1);
} else {
// Close our copy of the write end of the stdio handles.
IGNORE_EINTR(close(stdout_pipe[1]));
IGNORE_EINTR(close(stderr_pipe[1]));
// Make our read end of the pipes non-blocking (expected by the handle watching system).
int flags = fcntl(stdout_pipe[0], F_GETFL);
if (flags != -1 && (flags & O_NONBLOCK) == 0) {
fcntl(stdout_pipe[0], F_SETFL, flags | O_NONBLOCK);
}
flags = fcntl(stderr_pipe[0], F_GETFL);
if (flags != -1 && (flags & O_NONBLOCK) == 0) {
fcntl(stderr_pipe[0], F_SETFL, flags | O_NONBLOCK);
}
stdio_handles_.out = OwnedStdioHandle(stdout_pipe[0]);
stdio_handles_.err = OwnedStdioHandle(stderr_pipe[0]);
}
// The execv will issue a trap just after setting up the new process. To ensure the new process is
// set up by the time we return, block on that signal.
int status = 0;
if (waitpid(pid_, &status, 0) < 0)
return debug::ErrnoStatus(errno, "Can't create process.");
if (WIFEXITED(status)) {
// If it exits before we got a stop signal, that means exec failed and the process exited.
return debug::Status(fxl::StringPrintf("Can't launch '%s'.", argv[0].c_str()));
}
if (!WIFSTOPPED(status) || WSTOPSIG(status) != SIGTRAP) {
DEBUG_LOG(Process) << "Unexpected first signal from the child.";
return debug::Status("Unexpected signal " + std::to_string(status));
}
task_ = fxl::MakeRefCounted<LinuxTask>(pid_, true);
// The process is now stopped. We need to track this suspension such that later parts of the
// initialization can use SuspendHandles without resuming the process when those go out-of-scope.
//
// As a result, we manually take a suspend reference count to account for the current stop.
task_->suspend_count_++;
holding_initial_suspend_ = true;
return debug::Status();
}
StdioHandles LinuxBinaryLauncher::ReleaseStdioHandles() { return std::move(stdio_handles_); }
std::unique_ptr<ProcessHandle> LinuxBinaryLauncher::GetProcess() const {
return std::make_unique<LinuxProcessHandle>(task_);
}
debug::Status LinuxBinaryLauncher::Start() {
// Continue from the first trap caught in Setup(). Decrement the suspend count using the official
// Task::DecrementSuspendCount() function because we want this to actually resume the task
// (assuming that the suspend count is going to 0).
FX_DCHECK(holding_initial_suspend_);
holding_initial_suspend_ = false;
task_->DecrementSuspendCount();
return debug::Status();
}
} // namespace debug_agent