blob: b1abc344475e4c2d234cc25e268f4e621cbe0a59 [file] [log] [blame]
// Copyright 2018 The Chromium OS 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 "vm_tools/garcon/service_impl.h"
#include <arpa/inet.h>
#include <errno.h>
#include <fcntl.h>
#include <linux/sockios.h>
#include <stdint.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <algorithm>
#include <cstdlib>
#include <limits>
#include <map>
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include <base/bind.h>
#include <base/files/file_path.h>
#include <base/files/file_util.h>
#include <base/logging.h>
#include <base/process/launch.h>
#include <base/strings/string_split.h>
#include <base/strings/string_util.h>
#include <base/strings/stringprintf.h>
#include "vm_tools/garcon/desktop_file.h"
#include "vm_tools/garcon/host_notifier.h"
#include "vm_tools/garcon/icon_finder.h"
namespace vm_tools {
namespace garcon {
namespace {
constexpr char kForkedProcessConsole[] = "/dev/null";
constexpr char kStartupIDEnv[] = "DESKTOP_STARTUP_ID";
constexpr char kXDisplayEnv[] = "DISPLAY";
constexpr char kXLowDensityDisplayEnv[] = "DISPLAY_LOW_DENSITY";
constexpr char kWaylandDisplayEnv[] = "WAYLAND_DISPLAY";
constexpr char kWaylandLowDensityDisplayEnv[] = "WAYLAND_DISPLAY_LOW_DENSITY";
constexpr char kXCursorSizeEnv[] = "XCURSOR_SIZE";
constexpr char kLowDensityXCursorSizeEnv[] = "XCURSOR_SIZE_LOW_DENSITY";
constexpr size_t kMaxIconSize = 1048576; // 1MB, very large for an icon
// Information about any errors that happen in the child process before the exec
// call. This is sent back to the parent process via a socket.
struct __attribute__((packed)) ChildErrorInfo {
enum class Reason : uint8_t {
// Failed to set session id.
SESSION_ID = 0,
// Unable to open console.
CONSOLE = 1,
// Unable to set stdio fds.
STDIO_FD = 2,
// Unable to set environment variable.
SETENV = 3,
// Unable to reset signal handlers.
SIGNAL_RESET = 4,
// Failed to exec the requested program.
EXEC = 5,
// Failed to set the working directory.
WORKING_DIR = 6,
};
union {
// If |reason| is STDIO_FD, the fd that we failed to dup.
int32_t fd;
// If |reason| is SETENV, then the child process will append the key and
// value of the environment variable pair that failed to this struct. This
// value tells the parent process the length of the 2 strings, including the
// '\0' byte for each string.
uint16_t env_length;
// If |reason| is SIGNAL_RESET, the signal number for which we failed to set
// the default disposition.
int32_t signo;
} details;
// The errno value after the failed action.
int32_t err;
// Error reason.
Reason reason;
};
// Number of defined signals that the process could receive (not including
// real time signals).
constexpr int kNumSignals = 32;
// Resets all signal handlers to the default. This is called in child processes
// immediately before exec-ing so that signals are not unexpectedly blocked.
// Returns 0 if all signal handlers were successfully set to their default
// dispositions. Returns the signal number of the signal for which resetting
// the signal handler failed, if any. Callers should inspect errno for the
// error.
int ResetSignalHandlers() {
for (int signo = 1; signo < kNumSignals; ++signo) {
if (signo == SIGKILL || signo == SIGSTOP) {
// sigaction returns an error if we try to set the disposition of these
// signals to SIG_DFL.
continue;
}
struct sigaction act = {
.sa_handler = SIG_DFL, .sa_flags = 0,
};
sigemptyset(&act.sa_mask);
if (sigaction(signo, &act, nullptr) != 0) {
return signo;
}
}
return 0;
}
// Performs various setup steps in the child process after calling fork() but
// before calling exec(). |error_fd| should be a valid file descriptor for a
// socket and will be used to send error information back to the parent process
// if any of the setup steps fail.
void DoChildSetup(const std::map<std::string, std::string>& env,
std::string working_dir,
int error_fd) {
// Create a new session and process group.
if (setsid() == -1) {
struct ChildErrorInfo info = {
.reason = ChildErrorInfo::Reason::SESSION_ID, .err = errno,
};
send(error_fd, &info, sizeof(info), MSG_NOSIGNAL);
_exit(errno);
}
// File descriptor for the child's stdio.
int fd = open(kForkedProcessConsole, O_RDWR | O_NOCTTY);
if (fd < 0) {
struct ChildErrorInfo info = {
.reason = ChildErrorInfo::Reason::CONSOLE, .err = errno,
};
send(error_fd, &info, sizeof(info), MSG_NOSIGNAL);
_exit(errno);
}
// Override the parent's stdio fds with the console fd.
for (int newfd = 0; newfd < 3; ++newfd) {
if (dup2(fd, newfd) < 0) {
struct ChildErrorInfo info = {
.reason = ChildErrorInfo::Reason::STDIO_FD,
.err = errno,
.details = {.fd = newfd},
};
send(error_fd, &info, sizeof(info), MSG_NOSIGNAL);
_exit(errno);
}
}
// Close the console fd, if necessary.
if (fd >= 3) {
close(fd);
}
// Set the umask back to a reasonable default.
umask(0022);
// Set the environment variables.
for (const auto& pair : env) {
if (setenv(pair.first.c_str(), pair.second.c_str(), 1) == 0) {
continue;
}
// Failed to set an environment variable. Send the error back to the
// parent process.
uint16_t env_length = 0;
if (pair.first.size() + pair.second.size() + 2 <
std::numeric_limits<uint16_t>::max()) {
env_length =
static_cast<uint16_t>(pair.first.size() + pair.second.size() + 2);
}
struct ChildErrorInfo info = {
.reason = ChildErrorInfo::Reason::SETENV,
.err = errno,
.details = {.env_length = env_length},
};
send(error_fd, &info, sizeof(info), MSG_NOSIGNAL);
// Also send back the offending (key, value) pair if it's not too long.
// The pair is sent back in the format: <key>\0<value>\0.
if (env_length != 0) {
struct iovec iovs[] = {
{
.iov_base =
static_cast<void*>(const_cast<char*>(pair.first.data())),
.iov_len = pair.first.size() + 1,
},
{
.iov_base =
static_cast<void*>(const_cast<char*>(pair.second.data())),
.iov_len = pair.second.size() + 1,
},
};
struct msghdr hdr = {
.msg_name = nullptr,
.msg_namelen = 0,
.msg_iov = iovs,
.msg_iovlen = sizeof(iovs) / sizeof(iovs[0]),
.msg_control = nullptr,
.msg_controllen = 0,
.msg_flags = 0,
};
sendmsg(error_fd, &hdr, MSG_NOSIGNAL);
}
_exit(errno);
}
// Set the working directory if requested.
if (!working_dir.empty()) {
if (chdir(working_dir.c_str())) {
// If we failed, then send a failure message back to the parent process
// and terminate the child process.
struct ChildErrorInfo info = {
.reason = ChildErrorInfo::Reason::WORKING_DIR, .err = errno,
};
send(error_fd, &info, sizeof(info), MSG_NOSIGNAL);
_exit(errno);
}
}
// Restore signal handlers and unblock all signals.
int signo = ResetSignalHandlers();
if (signo != 0) {
struct ChildErrorInfo info = {
.reason = ChildErrorInfo::Reason::SIGNAL_RESET,
.err = errno,
.details = {.signo = signo},
};
send(error_fd, &info, sizeof(info), MSG_NOSIGNAL);
_exit(errno);
}
// Unblock all signals.
sigset_t mask;
sigfillset(&mask);
sigprocmask(SIG_UNBLOCK, &mask, nullptr);
}
// Logs information about the error that occurred in the child process.
void LogChildError(const struct ChildErrorInfo& child_info,
int fd,
const std::string& working_dir) {
const char* msg = nullptr;
switch (child_info.reason) {
case ChildErrorInfo::Reason::SESSION_ID:
msg = "Failed to set session id in child process: ";
break;
case ChildErrorInfo::Reason::CONSOLE:
msg = "Failed to open console in child process: ";
break;
case ChildErrorInfo::Reason::STDIO_FD:
msg = "Failed to setup stdio file descriptors in child process: ";
break;
case ChildErrorInfo::Reason::SETENV:
msg = "Failed to set environment variable in child process: ";
break;
case ChildErrorInfo::Reason::SIGNAL_RESET:
msg = "Failed to reset signal handler disposition in child process: ";
break;
case ChildErrorInfo::Reason::EXEC:
msg = "Failed to execute requested program in child process: ";
break;
case ChildErrorInfo::Reason::WORKING_DIR:
msg = "Failed to set working directory in child process: ";
break;
}
LOG(ERROR) << msg << strerror(child_info.err);
if (child_info.reason == ChildErrorInfo::Reason::STDIO_FD) {
LOG(ERROR) << "Unable to dup console fd to " << child_info.details.fd;
return;
}
if (child_info.reason == ChildErrorInfo::Reason::SIGNAL_RESET) {
LOG(ERROR) << "Unable to set signal disposition for signal "
<< child_info.details.signo << " to SIG_DFL";
return;
}
if (child_info.reason == ChildErrorInfo::Reason::SETENV &&
child_info.details.env_length > 0) {
auto buf = std::make_unique<char[]>(child_info.details.env_length + 1);
if (recv(fd, buf.get(), child_info.details.env_length, 0) !=
child_info.details.env_length) {
PLOG(ERROR) << "Unable to fetch error details from child process";
return;
}
buf[child_info.details.env_length] = '\0';
char* key = buf.get();
char* value = strchr(buf.get(), '\0');
if (value - key == child_info.details.env_length) {
LOG(ERROR) << "Missing value in SETENV error details";
return;
}
// Step over the nullptr at the end of |key|.
++value;
LOG(ERROR) << "Unable to set " << key << " to " << value;
}
if (child_info.reason == ChildErrorInfo::Reason::WORKING_DIR) {
LOG(ERROR) << "Unable to change to dir " << working_dir;
}
}
// Executed a process with the specified |argv| arguments using the |env|
// environment. If |working_dir| is not empty, it is executed inside of that
// working directory. Returns true on successful execution, false otherwise.
bool Spawn(std::vector<std::string> argv,
std::map<std::string, std::string> env,
const std::string& working_dir) {
CHECK(!argv.empty());
// Build the argv.
std::vector<const char*> argv_c(argv.size());
std::transform(
argv.begin(), argv.end(), argv_c.begin(),
[](const std::string& arg) -> const char* { return arg.c_str(); });
argv_c.emplace_back(nullptr);
// Create a pair of sockets for communicating information about the child
// process setup. If there was an error in any of the steps performed before
// running execvp, then the child process will send back a ChildErrorInfo
// struct with the error details over the socket. If the execvp runs
// successful then the socket will automatically be closed (because of
// the SOCK_CLOEXEC flag) and the parent will read 0 bytes from its end
// of the socketpair.
int info_fds[2];
if (socketpair(AF_UNIX, SOCK_SEQPACKET | SOCK_CLOEXEC, 0, info_fds) != 0) {
PLOG(ERROR) << "Failed to create socketpair for child process";
return false;
}
// Block all signals before forking to prevent signals from arriving in the
// child.
sigset_t mask, omask;
sigfillset(&mask);
sigprocmask(SIG_BLOCK, &mask, &omask);
pid_t pid = fork();
if (pid < 0) {
PLOG(ERROR) << "Failed to fork";
return false;
}
if (pid == 0) {
// Child process.
close(info_fds[0]);
DoChildSetup(env, working_dir, info_fds[1]);
// Launch the process.
execvp(argv_c[0], const_cast<char* const*>(argv_c.data()));
// execvp never returns except in case of an error.
struct ChildErrorInfo info = {
.reason = ChildErrorInfo::Reason::EXEC, .err = errno,
};
send(info_fds[1], &info, sizeof(info), MSG_NOSIGNAL);
_exit(errno);
}
// Parent process.
close(info_fds[1]);
struct ChildErrorInfo child_info = {};
ssize_t ret = recv(info_fds[0], &child_info, sizeof(child_info), 0);
bool retval = false;
// There are 3 possibilities here:
// - The process setup completed successfully and the program was launched.
// In this case the socket fd in the child process will be closed on
// exec and ret will be 0.
// - An error occurred during setup. ret will be sizeof(child_info).
// - An error occurred during the recv. In this case we assume the child
// setup was successful. If it wasn't, we'll find out about it through
// the normal child reaping mechanism.
if (ret == sizeof(child_info)) {
// Error occurred in the child.
LogChildError(child_info, info_fds[0], working_dir);
// Reap the child process here since we know it already failed.
int status = 0;
pid_t child = waitpid(pid, &status, 0);
DCHECK_EQ(child, pid);
} else if (ret < 0) {
PLOG(ERROR) << "Failed to receive information about child process setup";
} else {
CHECK_EQ(ret, 0);
retval = true;
}
close(info_fds[0]);
// Restore the signal mask.
sigprocmask(SIG_SETMASK, &omask, nullptr);
return retval;
}
} // namespace
ServiceImpl::ServiceImpl(PackageKitProxy* package_kit_proxy)
: package_kit_proxy_(package_kit_proxy) {
CHECK(package_kit_proxy_);
}
grpc::Status ServiceImpl::LaunchApplication(
grpc::ServerContext* ctx,
const vm_tools::container::LaunchApplicationRequest* request,
vm_tools::container::LaunchApplicationResponse* response) {
LOG(INFO) << "Received request to launch application in container";
if (request->desktop_file_id().empty()) {
return grpc::Status(grpc::INVALID_ARGUMENT, "missing desktop_file_id");
}
// Find the actual file path that corresponds to this desktop file id.
base::FilePath file_path =
DesktopFile::FindFileForDesktopId(request->desktop_file_id());
if (file_path.empty()) {
response->set_success(false);
response->set_failure_reason("Desktop file does not exist");
return grpc::Status::OK;
}
// Now parse the actual desktop file.
std::unique_ptr<DesktopFile> desktop_file =
DesktopFile::ParseDesktopFile(file_path);
if (!desktop_file) {
response->set_success(false);
response->set_failure_reason("Desktop file contents are invalid");
return grpc::Status::OK;
}
// Make sure this desktop file is for an application.
if (!desktop_file->IsApplication()) {
response->set_success(false);
response->set_failure_reason("Desktop file is not for an application");
return grpc::Status::OK;
}
std::vector<std::string> files(request->files().begin(),
request->files().end());
// Get the argv string from the desktop file we need for execution.
// TODO(timloh): Desktop files using %u/%f should execute multiple copies of
// the program for multiple files.
std::vector<std::string> argv = desktop_file->GenerateArgvWithFiles(files);
if (argv.empty()) {
response->set_success(false);
response->set_failure_reason(
"Failure in generating argv list for application");
return grpc::Status::OK;
}
std::map<std::string, std::string> env;
if (desktop_file->startup_notify()) {
env[kStartupIDEnv] = request->desktop_file_id();
}
if (request->display_scaling() ==
vm_tools::container::LaunchApplicationRequest::SCALED) {
env[kXDisplayEnv] = std::getenv(kXLowDensityDisplayEnv);
env[kWaylandDisplayEnv] = std::getenv(kWaylandLowDensityDisplayEnv);
env[kXCursorSizeEnv] = std::getenv(kLowDensityXCursorSizeEnv);
}
if (!Spawn(std::move(argv), std::move(env), desktop_file->path())) {
response->set_success(false);
response->set_failure_reason("Failure in execution of application");
} else {
response->set_success(true);
}
// Return OK no matter what because the RPC itself succeeded even if there
// was an issue with launching the process.
return grpc::Status::OK;
}
grpc::Status ServiceImpl::GetIcon(
grpc::ServerContext* ctx,
const vm_tools::container::IconRequest* request,
vm_tools::container::IconResponse* response) {
LOG(INFO) << "Received request to get application icons in container";
for (const std::string& desktop_file_id : request->desktop_file_ids()) {
std::string icon_data;
base::FilePath icon_filepath =
LocateIconFile(desktop_file_id, request->icon_size(), request->scale());
if (icon_filepath.empty()) {
continue;
}
if (!base::ReadFileToStringWithMaxSize(icon_filepath, &icon_data,
kMaxIconSize)) {
LOG(ERROR) << "Failed to read icon data file " << icon_filepath.value();
continue;
}
container::DesktopIcon* desktop_icon = response->add_desktop_icons();
desktop_icon->set_desktop_file_id(desktop_file_id);
desktop_icon->set_icon(icon_data);
}
return grpc::Status::OK;
}
grpc::Status ServiceImpl::LaunchVshd(
grpc::ServerContext* ctx,
const vm_tools::container::LaunchVshdRequest* request,
vm_tools::container::LaunchVshdResponse* response) {
LOG(INFO) << "Received request to launch vshd in container";
if (request->port() == 0) {
return grpc::Status(grpc::INVALID_ARGUMENT, "vshd port cannot be 0");
}
std::vector<std::string> argv{
"/opt/google/cros-containers/bin/vshd", "--inherit_env",
base::StringPrintf("--forward_to_host_port=%u", request->port())};
if (!Spawn(std::move(argv), {}, "")) {
response->set_success(false);
response->set_failure_reason("Failed to spawn vshd");
} else {
response->set_success(true);
}
// Return OK no matter what because the RPC itself succeeded even if there
// was an issue with launching the process.
return grpc::Status::OK;
}
grpc::Status ServiceImpl::GetLinuxPackageInfo(
grpc::ServerContext* ctx,
const vm_tools::container::LinuxPackageInfoRequest* request,
vm_tools::container::LinuxPackageInfoResponse* response) {
LOG(INFO) << "Received request to get Linux package info";
if (request->file_path().empty()) {
return grpc::Status(grpc::INVALID_ARGUMENT, "file_path cannot be empty");
}
base::FilePath file_path(request->file_path());
if (!base::PathExists(file_path)) {
return grpc::Status(grpc::INVALID_ARGUMENT, "file_path does not exist");
}
std::string error_msg;
std::shared_ptr<PackageKitProxy::LinuxPackageInfo> pkg_info =
std::make_shared<PackageKitProxy::LinuxPackageInfo>();
response->set_success(
package_kit_proxy_->GetLinuxPackageInfo(file_path, pkg_info, &error_msg));
if (response->success()) {
response->set_package_id(std::move(pkg_info->package_id));
response->set_license(std::move(pkg_info->license));
response->set_description(std::move(pkg_info->description));
response->set_project_url(std::move(pkg_info->project_url));
response->set_size(pkg_info->size);
response->set_summary(std::move(pkg_info->summary));
} else {
response->set_failure_reason(error_msg);
}
return grpc::Status::OK;
}
grpc::Status ServiceImpl::InstallLinuxPackage(
grpc::ServerContext* ctx,
const vm_tools::container::InstallLinuxPackageRequest* request,
vm_tools::container::InstallLinuxPackageResponse* response) {
LOG(INFO) << "Received request to install Linux package";
if (request->file_path().empty()) {
return grpc::Status(grpc::INVALID_ARGUMENT, "file_path cannot be empty");
}
base::FilePath file_path(request->file_path());
if (!base::PathExists(file_path)) {
return grpc::Status(grpc::INVALID_ARGUMENT, "file_path does not exist");
}
std::string error_msg;
response->set_status(
package_kit_proxy_->InstallLinuxPackage(file_path, &error_msg));
response->set_failure_reason(error_msg);
return grpc::Status::OK;
}
grpc::Status ServiceImpl::UninstallPackageOwningFile(
grpc::ServerContext* ctx,
const vm_tools::container::UninstallPackageOwningFileRequest* request,
vm_tools::container::UninstallPackageOwningFileResponse* response) {
LOG(INFO) << "Received request to uninstall package owning a file";
if (request->desktop_file_id().empty()) {
return grpc::Status(grpc::INVALID_ARGUMENT, "missing desktop_file_id");
}
// Find the actual file path that corresponds to this desktop file id.
base::FilePath file_path =
DesktopFile::FindFileForDesktopId(request->desktop_file_id());
if (file_path.empty()) {
return grpc::Status(grpc::INVALID_ARGUMENT,
"desktop_file_id does not exist");
}
std::string error;
response->set_status(
package_kit_proxy_->UninstallPackageOwningFile(file_path, &error));
response->set_failure_reason(error);
return grpc::Status::OK;
}
grpc::Status ServiceImpl::GetDebugInformation(
grpc::ServerContext* ctx,
const vm_tools::container::GetDebugInformationRequest* request,
vm_tools::container::GetDebugInformationResponse* response) {
LOG(INFO) << "Received request to get container debug information";
std::string* debug_information = response->mutable_debug_information();
*debug_information += "Installed Crostini Packages:\n";
std::string dpkg_out;
base::GetAppOutput({"dpkg", "-l", "cros-*"}, &dpkg_out);
std::vector<base::StringPiece> dpkg_lines = base::SplitStringPiece(
dpkg_out, "\n", base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY);
for (const auto& pkg_line : dpkg_lines) {
std::vector<base::StringPiece> pkg_info = base::SplitStringPiece(
pkg_line, base::kWhitespaceASCII, base::TRIM_WHITESPACE,
base::SPLIT_WANT_NONEMPTY);
// Filter out unrelated lines.
if (pkg_info.size() < 3)
continue;
// Only collect installed packages.
if (pkg_info[0] != "ii")
continue;
base::StringPiece pkg_name = pkg_info[1];
base::StringPiece pkg_version = pkg_info[2];
*debug_information += "\t";
pkg_name.AppendToString(debug_information);
*debug_information += "-";
pkg_version.AppendToString(debug_information);
*debug_information += "\n";
}
*debug_information += "systemctl status:\n";
std::string systemctl_out;
base::GetAppOutput({"systemctl", "--no-legend"}, &systemctl_out);
std::vector<base::StringPiece> systemctl_out_lines = base::SplitStringPiece(
systemctl_out, "\n", base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY);
for (const auto& line : systemctl_out_lines) {
*debug_information += "\t";
line.AppendToString(debug_information);
*debug_information += "\n";
}
*debug_information += "systemctl user status:\n";
std::string systemctl_user_out;
base::GetAppOutput({"systemctl", "--user", "--no-legend"},
&systemctl_user_out);
std::vector<base::StringPiece> systemctl_user_out_lines =
base::SplitStringPiece(systemctl_user_out, "\n", base::TRIM_WHITESPACE,
base::SPLIT_WANT_NONEMPTY);
for (const auto& line : systemctl_user_out_lines) {
*debug_information += "\t";
line.AppendToString(debug_information);
*debug_information += "\n";
}
return grpc::Status::OK;
}
} // namespace garcon
} // namespace vm_tools