blob: 9e34c5f481caa51a122488096644cc87974ad7dd [file] [log] [blame]
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//
// This file implements a wrapper to run the virtual me2me session within a
// proper PAM session. It will generally be run as root and drop privileges to
// the specified user before running the me2me session script.
// Usage: user-session start [--foreground] [--user user] [-- SCRIPT_ARGS...]
//
// Options:
// --foreground - Don't daemonize.
// --user - Create a session for the specified user. Required when
// running as root, not allowed when running as a normal user.
// SCRIPT_ARGS - Arguments following -- are passed to the script verbatim.
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <grp.h>
#include <limits.h>
#include <pwd.h>
#include <signal.h>
#include <unistd.h>
#include <security/pam_appl.h>
#include <cerrno>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <ctime>
#include <map>
#include <memory>
#include <string>
#include <tuple>
#include <utility>
#include <vector>
#include "base/environment.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/logging.h"
#include "base/optional.h"
#include "base/process/launch.h"
#include "base/stl_util.h"
#include "base/strings/string_piece.h"
#include "base/strings/string_util.h"
namespace {
// This is the file descriptor used for the Python session script to pass us
// messages during startup. It must be kept in sync with USER_SESSION_MESSAGE_FD
// in linux_me2me_host.py. It should be high enough that login scripts are
// unlikely to interfere with it, but is otherwise arbitrary.
const int kMessageFd = 202;
// This is the exit code the Python session script will use to signal that the
// user-session wrapper should restart instead of exiting. It must be kept in
// sync with RELAUNCH_EXIT_CODE in linux_me2me_host.py
const int kRelaunchExitCode = 41;
const char kPamName[] = "chrome-remote-desktop";
const char kScriptName[] = "chrome-remote-desktop";
const char kStartCommand[] = "start";
const char kForegroundFlag[] = "--foreground";
const char kUserFlag[] = "--user";
const char kExeSymlink[] = "/proc/self/exe";
// This template will be formatted by strftime and then used by mkstemp
const char kLogFileTemplate[] =
"/tmp/chrome_remote_desktop_%Y%m%d_%H%M%S_XXXXXX";
const char kUsageMessage[] =
"This program is not intended to be run by end users. To configure Chrome\n"
"Remote Desktop, please install the app from the Chrome Web Store:\n"
"https://chrome.google.com/remotedesktop\n";
// A list of variable to pass through to the child environment. Should be kept
// in sync with remoting_user_session_wrapper.sh for testing.
const char* const kPassthroughVariables[] = {
"GOOGLE_CLIENT_ID_REMOTING", "GOOGLE_CLIENT_ID_REMOTING_HOST",
"GOOGLE_CLIENT_SECRET_REMOTING", "GOOGLE_CLIENT_SECRET_REMOTING_HOST",
"CHROME_REMOTE_DESKTOP_HOST_EXTRA_PARAMS"};
// Holds the null-terminated path to this executable. This is obtained at
// startup, since it may be harder to obtain later. (E.g., Linux will append
// " (deleted)" if the file has been replaced by an update.)
char gExecutablePath[PATH_MAX] = {};
void PrintUsage() {
std::fputs(kUsageMessage, stderr);
}
// Shell-escapes a single argument in a way that is compatible with various
// different shells. Returns nullopt when argument contains a newline, which
// can't be represented in a cross-shell fashion.
base::Optional<std::string> ShellEscapeArgument(
const base::StringPiece argument) {
std::string result;
for (char character : argument) {
// csh in particular doesn't provide a good way to handle this
if (character == '\n') {
return base::nullopt;
}
// Some shells ascribe special meaning to some escape sequences such as \t,
// so don't escape any alphanumerics. (Also cuts down on verbosity.) This is
// similar to the approach sudo takes.
if (!((character >= '0' && character <= '9') ||
(character >= 'A' && character <= 'Z') ||
(character >= 'a' && character <= 'z') ||
(character == '-' || character == '_'))) {
result.push_back('\\');
}
result.push_back(character);
}
return result;
}
// PAM conversation function. Since the wrapper runs in a non-interactive
// context, log any messages, but return an error if asked to provide user
// input.
extern "C" int Converse(int num_messages,
const struct pam_message** messages,
struct pam_response** responses,
void* context) {
bool failed = false;
for (int i = 0; i < num_messages; ++i) {
// This is correct for the PAM included with Linux, OS X, and BSD. However,
// apparently Solaris and HP/UX require instead `&(*msg)[i]`. That is, they
// disagree as to which level of indirection contains the array.
const pam_message* message = messages[i];
switch (message->msg_style) {
case PAM_PROMPT_ECHO_OFF:
case PAM_PROMPT_ECHO_ON:
LOG(WARNING) << "PAM requested user input (unsupported): "
<< (message->msg ? message->msg : "");
failed = true;
break;
case PAM_TEXT_INFO:
LOG(INFO) << "[PAM] " << (message->msg ? message->msg : "");
break;
case PAM_ERROR_MSG:
// Error messages from PAM are not necessarily fatal to the operation,
// as the module may be optional.
LOG(WARNING) << "[PAM] " << (message->msg ? message->msg : "");
break;
default:
LOG(WARNING) << "Encountered unknown PAM message style";
failed = true;
break;
}
}
if (failed)
return PAM_CONV_ERR;
pam_response* response_list = static_cast<pam_response*>(
std::calloc(num_messages, sizeof(*response_list)));
if (response_list == nullptr)
return PAM_BUF_ERR;
*responses = response_list;
return PAM_SUCCESS;
}
const struct pam_conv kPamConversation = {Converse, nullptr};
// Wrapper class for working with PAM and cleaning up in an RAII fashion
class PamHandle {
public:
// Attempts to initialize PAM transaction. Check the result with IsInitialized
// before calling any other member functions.
PamHandle(const char* service_name,
const char* user,
const struct pam_conv* pam_conversation) {
last_return_code_ =
pam_start(service_name, user, pam_conversation, &pam_handle_);
if (last_return_code_ != PAM_SUCCESS) {
pam_handle_ = nullptr;
}
}
// Terminates PAM transaction
~PamHandle() {
if (pam_handle_ != nullptr) {
pam_end(pam_handle_, last_return_code_);
}
}
// Checks whether the PAM transaction was successfully initialized. Only call
// other member functions if this returns true.
bool IsInitialized() const { return pam_handle_ != nullptr; }
// Performs account validation
int AccountManagement(int flags) {
return last_return_code_ = pam_acct_mgmt(pam_handle_, flags);
}
// Establishes or deletes PAM user credentials
int SetCredentials(int flags) {
return last_return_code_ = pam_setcred(pam_handle_, flags);
}
// Starts user session
int OpenSession(int flags) {
return last_return_code_ = pam_open_session(pam_handle_, flags);
}
// Ends user session
int CloseSession(int flags) {
return last_return_code_ = pam_close_session(pam_handle_, flags);
}
// Returns the current username according to PAM. It is possible for PAM
// modules to change this from the initial value passed to the constructor.
base::Optional<std::string> GetUser() {
const char* user;
last_return_code_ = pam_get_item(pam_handle_, PAM_USER,
reinterpret_cast<const void**>(&user));
if (last_return_code_ != PAM_SUCCESS || user == nullptr)
return base::nullopt;
return std::string(user);
}
// Obtains the list of environment variables provided by PAM modules.
base::Optional<base::EnvironmentMap> GetEnvironment() {
char** environment = pam_getenvlist(pam_handle_);
if (environment == nullptr)
return base::nullopt;
base::EnvironmentMap environment_map;
for (char** variable = environment; *variable != nullptr; ++variable) {
char* delimiter = std::strchr(*variable, '=');
if (delimiter != nullptr) {
environment_map[std::string(*variable, delimiter)] =
std::string(delimiter + 1);
}
std::free(*variable);
}
std::free(environment);
return environment_map;
}
// Returns a description of the given return code
const char* ErrorString(int return_code) {
return pam_strerror(pam_handle_, return_code);
}
// Logs a fatal error if return_code isn't PAM_SUCCESS
void CheckReturnCode(int return_code, base::StringPiece what) {
if (return_code != PAM_SUCCESS) {
LOG(FATAL) << "[PAM] " << what << ": " << ErrorString(return_code);
}
}
private:
pam_handle_t* pam_handle_ = nullptr;
int last_return_code_ = PAM_SUCCESS;
DISALLOW_COPY_AND_ASSIGN(PamHandle);
};
// Initializes the gExecutablePath global to the location of the running
// executable. Should be called at program start.
void DetermineExecutablePath() {
ssize_t path_size =
readlink(kExeSymlink, gExecutablePath, base::size(gExecutablePath));
PCHECK(path_size >= 0) << "Failed to determine executable location";
CHECK(path_size < PATH_MAX) << "Executable path too long";
gExecutablePath[path_size] = '\0';
CHECK(gExecutablePath[0] == '/') << "Executable path not absolute";
}
// Returns the expected location of the session script based on the path to
// this executable.
std::string FindScriptPath() {
return base::FilePath(gExecutablePath).DirName().Append(kScriptName).value();
}
// Execs the me2me script.
// This function is called after forking and dropping privileges. It never
// returns.
void ExecMe2MeScript(base::EnvironmentMap environment,
const struct passwd* pwinfo,
const std::vector<std::string>& script_args) {
// By convention, a login shell is signified by preceeding the shell name in
// argv[0] with a '-'.
std::string shell_name =
'-' + base::FilePath(pwinfo->pw_shell).BaseName().value();
base::Optional<std::string> escaped_script_path =
ShellEscapeArgument(FindScriptPath());
CHECK(escaped_script_path) << "Could not escape script path";
std::string shell_arg = *escaped_script_path + " --start --child-process";
for (const std::string& arg : script_args) {
base::Optional<std::string> escaped_arg = ShellEscapeArgument(arg);
CHECK(escaped_arg) << "Could not escape script argument";
shell_arg += " ";
shell_arg += *escaped_arg;
}
environment["USER"] = pwinfo->pw_name;
environment["LOGNAME"] = pwinfo->pw_name;
environment["HOME"] = pwinfo->pw_dir;
environment["SHELL"] = pwinfo->pw_shell;
if (!environment.count("PATH")) {
environment["PATH"] = "/bin:/usr/bin";
}
for (const char* variable : kPassthroughVariables) {
char* value = std::getenv(variable);
if (value != nullptr) {
environment[variable] = value;
}
}
std::vector<std::string> env_strings;
for (const auto& env_var : environment) {
env_strings.emplace_back(env_var.first + "=" + env_var.second);
}
std::vector<const char*> arg_ptrs = {shell_name.c_str(), "-c",
shell_arg.c_str(), nullptr};
std::vector<const char*> env_ptrs;
env_ptrs.reserve(env_strings.size() + 1);
for (const auto& env_string : env_strings) {
env_ptrs.push_back(env_string.c_str());
}
env_ptrs.push_back(nullptr);
execve(pwinfo->pw_shell, const_cast<char* const*>(arg_ptrs.data()),
const_cast<char* const*>(env_ptrs.data()));
PLOG(FATAL) << "Failed to exec login shell " << pwinfo->pw_shell;
}
// Relaunch the user session. When calling this function, the real UID must be
// set to the target user, while the effective UID must be root. The provided
// user must correspond with the current real UID.
void Relaunch(const std::string& user,
const std::vector<std::string>& script_args) {
CHECK(getuid() != 0);
// Real user ID has already been set to the target user, but the corresponding
// environment variables may not have been if the session was started by root
// (e.g., at boot).
PCHECK(setenv("USER", user.c_str(), true) == 0) << "setenv failed";
PCHECK(setenv("LOGNAME", user.c_str(), true) == 0) << "setenv failed";
// Pass --foreground to continue using the same log file.
std::vector<const char*> arg_ptrs = {gExecutablePath, kStartCommand,
kForegroundFlag, "--"};
for (const std::string& arg : script_args) {
arg_ptrs.push_back(arg.c_str());
}
arg_ptrs.push_back(nullptr);
execv(gExecutablePath, const_cast<char* const*>(arg_ptrs.data()));
PCHECK(false) << "Failed to exec self";
}
// Runs the me2me script in a PAM session. Exits the program on failure.
// If chown_log is true, the owner and group of the file associated with stdout
// will be changed to the target user. If match_uid is specified, this function
// will fail if the final user id does not match the one provided. If
// script_args is not empty, the contained arguments will be passed on to the
// me2me script.
void ExecuteSession(std::string user,
bool chown_log,
base::Optional<uid_t> match_uid,
const std::vector<std::string>& script_args) {
PamHandle pam_handle(kPamName, user.c_str(), &kPamConversation);
CHECK(pam_handle.IsInitialized()) << "Failed to initialize PAM";
// Make sure the account is valid and enabled.
pam_handle.CheckReturnCode(pam_handle.AccountManagement(0), "Account check");
// PAM may remap the user at any stage.
user = pam_handle.GetUser().value_or(std::move(user));
// setcred explicitly does not handle user id or group membership, and
// specifies that they should be established before calling setcred. Only the
// real user id is set here, as we still require root privileges. PAM modules
// may use getpwnam, so pwinfo can only be assumed valid until the next PAM
// call.
errno = 0;
struct passwd* pwinfo = getpwnam(user.c_str());
PCHECK(pwinfo != nullptr) << "getpwnam failed";
PCHECK(setreuid(pwinfo->pw_uid, -1) == 0) << "setreuid failed";
PCHECK(setgid(pwinfo->pw_gid) == 0) << "setgid failed";
PCHECK(initgroups(pwinfo->pw_name, pwinfo->pw_gid) == 0)
<< "initgroups failed";
// The documentation states that setcred should be called before open_session,
// as done here, but it may be worth noting that `login` calls open_session
// first.
pam_handle.CheckReturnCode(pam_handle.SetCredentials(PAM_ESTABLISH_CRED),
"Set credentials");
pam_handle.CheckReturnCode(pam_handle.OpenSession(0), "Open session");
// The above may have remapped the user.
user = pam_handle.GetUser().value_or(std::move(user));
// Fetch pwinfo again, as it may have been invalidated or the user name might
// have been remapped.
pwinfo = getpwnam(user.c_str());
PCHECK(pwinfo != nullptr) << "getpwnam failed";
if (match_uid && pwinfo->pw_uid != *match_uid) {
LOG(FATAL) << "PAM remapped username to one with a different user ID.";
}
if (chown_log) {
int result = fchown(STDOUT_FILENO, pwinfo->pw_uid, pwinfo->pw_gid);
PLOG_IF(WARNING, result != 0) << "Failed to change log file owner";
}
pid_t child_pid = fork();
PCHECK(child_pid >= 0) << "fork failed";
if (child_pid == 0) {
PCHECK(setuid(pwinfo->pw_uid) == 0) << "setuid failed";
PCHECK(chdir(pwinfo->pw_dir) == 0) << "chdir to $HOME failed";
base::Optional<base::EnvironmentMap> pam_environment =
pam_handle.GetEnvironment();
CHECK(pam_environment) << "Failed to get environment from PAM";
// Never returns.
ExecMe2MeScript(std::move(*pam_environment), pwinfo, script_args);
} else {
// Close pipe write fd if it is open.
close(kMessageFd);
// waitpid will return if the child is ptraced, so loop until the process
// actually exits.
int status;
do {
pid_t wait_result = waitpid(child_pid, &status, 0);
// Die if wait fails so we don't close the PAM session while the child is
// still running.
PCHECK(wait_result >= 0) << "wait failed";
} while (!WIFEXITED(status) && !WIFSIGNALED(status));
bool relaunch = false;
if (WIFEXITED(status)) {
if (WEXITSTATUS(status) == EXIT_SUCCESS) {
LOG(INFO) << "Child exited successfully";
} else if (WEXITSTATUS(status) == kRelaunchExitCode) {
LOG(INFO) << "Restarting session";
relaunch = true;
} else {
LOG(WARNING) << "Child exited with status " << WEXITSTATUS(status);
}
} else if (WIFSIGNALED(status)) {
LOG(WARNING) << "Child terminated by signal " << WTERMSIG(status);
}
// Best effort PAM cleanup
if (pam_handle.CloseSession(0) != PAM_SUCCESS) {
LOG(WARNING) << "Failed to close PAM session";
}
ignore_result(pam_handle.SetCredentials(PAM_DELETE_CRED));
if (relaunch) {
Relaunch(user, script_args);
}
}
}
struct LogFile {
int fd;
std::string path;
};
// Opens a temp file for logging. Exits the program on failure.
// Returns open file descriptor and path to log file.
LogFile OpenLogFile() {
char logfile[265];
std::time_t time = std::time(nullptr);
CHECK_NE(time, (std::time_t)(-1));
// Safe because we're single threaded
std::tm* localtime = std::localtime(&time);
CHECK_NE(std::strftime(logfile, sizeof(logfile), kLogFileTemplate, localtime),
static_cast<std::size_t>(0))
<< "Failed to format log file name";
mode_t mode = umask(0177);
int fd = mkstemp(logfile);
PCHECK(fd != -1) << "Failed to open log file";
umask(mode);
return {fd, logfile};
}
// Find the username for the current user. If either USER or LOGNAME is set to
// a user matching our real user id, we return that. Otherwise, we use getpwuid
// to attempt a reverse lookup. Note: It's possible for multiple usernames to
// share the same user id (e.g., to allow a user to have logins with different
// home directories or group membership, but be considered the same user as far
// as file permissions are concerned). Consulting USER/LOGNAME allows us to pick
// the correct entry in these circumstances.
std::string FindCurrentUsername() {
uid_t real_uid = getuid();
struct passwd* pwinfo;
for (const char* var : {"USER", "LOGNAME"}) {
const char* value = getenv(var);
if (value) {
pwinfo = getpwnam(value);
// USER and LOGNAME can be overridden, so make sure the value is valid
// and matches the UID of the invoking user.
if (pwinfo && pwinfo->pw_uid == real_uid) {
return pwinfo->pw_name;
}
}
}
errno = 0;
pwinfo = getpwuid(real_uid);
PCHECK(pwinfo) << "getpwuid failed";
return pwinfo->pw_name;
}
// Handle SIGINT and SIGTERM by printing a message and reraising the signal.
// This handler expects to be registered with the SA_RESETHAND and SA_NODEFER
// options to sigaction. (Don't register using signal.)
void HandleInterrupt(int signal) {
static const char kInterruptedMessage[] =
"Interrupted. The daemon is still running in the background.\n";
// Use write since fputs isn't async-signal-handler safe.
ignore_result(write(STDERR_FILENO, kInterruptedMessage,
base::size(kInterruptedMessage) - 1));
raise(signal);
}
// Handle SIGALRM timeout
void HandleAlarm(int) {
static const char kTimeoutMessage[] =
"Timeout waiting for session to start. It may have crashed, or may still "
"be running in the background.\n";
// Use write since fputs isn't async-signal-handler safe.
ignore_result(
write(STDERR_FILENO, kTimeoutMessage, base::size(kTimeoutMessage) - 1));
// A slow system or directory replication delay may cause the host to take
// longer than expected to start. Since it may still succeed, optimistically
// return success to prevent the host from being automatically unregistered.
std::_Exit(EXIT_SUCCESS);
}
// Relay messages from the host session and then exit.
void WaitForMessagesAndExit(int read_fd, const std::string& log_name) {
// Use initializer-list syntax to avoid trailing null
static const base::StringPiece kMessagePrefix = "MSG:";
static const base::StringPiece kReady = "READY\n";
struct sigaction action = {};
sigemptyset(&action.sa_mask);
action.sa_flags = SA_RESETHAND | SA_NODEFER;
// If Ctrl-C is pressed or TERM is received, inform the user that the daemon
// is still running before exiting.
action.sa_handler = HandleInterrupt;
sigaction(SIGINT, &action, nullptr);
sigaction(SIGTERM, &action, nullptr);
// Install a fallback timeout to end the parent process, in case the daemon
// never responds (e.g. host crash-looping, daemon killed).
//
// The value of 120s is chosen to match the heartbeat retry timeout in
// hearbeat_sender.cc.
action.sa_handler = HandleAlarm;
sigaction(SIGALRM, &action, nullptr);
alarm(120);
std::FILE* stream = fdopen(read_fd, "r");
char* buffer = nullptr;
std::size_t buffer_size = 0;
ssize_t line_size;
bool message_received = false;
bool host_ready = false;
while ((line_size = getline(&buffer, &buffer_size, stream)) >= 0) {
message_received = true;
base::StringPiece line(buffer, line_size);
if (base::StartsWith(line, kMessagePrefix, base::CompareCase::SENSITIVE)) {
line.remove_prefix(kMessagePrefix.size());
std::fwrite(line.data(), sizeof(char), line.size(), stderr);
} else if (line == kReady) {
host_ready = true;
} else {
std::fputs("Unrecognized command: ", stderr);
std::fwrite(line.data(), sizeof(char), line.size(), stderr);
}
}
// If we're not at EOF, it means a read error occured and we don't know if the
// host is still running or not. Similarly, if we received an EOF before any
// messages were received, it probably means the user's log-in shell closed
// the pipe before execing the python script, so again we don't know the state
// of the host. This latter behavior has only been observed in csh and tcsh.
// All other shells tested allowed the python script to inherit the pipe file
// descriptor without trouble.
if (!std::feof(stream) || !message_received) {
LOG(WARNING) << "Failed to read from message pipe. Please check log to "
"determine host status.\n";
// Assume host started so native messaging host allows flow to complete.
host_ready = true;
}
std::fprintf(stderr, "Log file: %s\n", log_name.c_str());
std::exit(host_ready ? EXIT_SUCCESS : EXIT_FAILURE);
}
// Daemonizes the process. Output is redirected to a log file. Exits the program
// on failure. Only returns in the child process.
//
// When executed by root (almost certainly via the init script), or if a pipe
// cannot be created, the parent will immediately exit. When executed by a
// user, the parent process will drop privileges and wait for the host to
// start, relaying any start-up messages to stdout.
//
// TODO(lambroslambrou): Having stdout/stderr redirected to a log file is not
// ideal - it could create a filesystem DoS if the daemon or a child process
// were to write excessive amounts to stdout/stderr. Ideally, stdout/stderr
// should be redirected to a pipe or socket, and a process at the other end
// should consume the data and write it to a logging facility which can do
// data-capping or log-rotation. The 'logger' command-line utility could be
// used for this, but it might cause too much syslog spam.
void Daemonize() {
// Open file descriptors before forking so errors can be reported.
LogFile log_file = OpenLogFile();
int devnull_fd = open("/dev/null", O_RDONLY);
PCHECK(devnull_fd != -1) << "Failed to open /dev/null";
uid_t real_uid = getuid();
// Set up message pipe
bool pipe_created = false;
int read_fd;
if (real_uid != 0) {
int pipe_fd[2];
int pipe_result = ::pipe(pipe_fd);
if (pipe_result != 0 || dup2(pipe_fd[1], kMessageFd) != kMessageFd) {
PLOG(WARNING) << "Failed to create message pipe. Please check log to "
"determine host status.\n";
} else {
pipe_created = true;
read_fd = pipe_fd[0];
close(pipe_fd[1]);
}
}
// Allow parent to exit, and ensure we're not a session leader so setsid can
// succeed
pid_t pid = fork();
PCHECK(pid != -1) << "fork failed";
if (pid != 0) {
if (!pipe_created) {
std::exit(EXIT_SUCCESS);
} else {
PCHECK(setuid(real_uid) == 0) << "setuid failed";
close(kMessageFd);
WaitForMessagesAndExit(read_fd, log_file.path);
CHECK(false);
}
}
// Start a new process group and session with no controlling terminal.
PCHECK(setsid() != -1) << "setsid failed";
// Fork again so we're no longer a session leader and can't get a controlling
// terminal.
pid = fork();
PCHECK(pid != -1) << "fork failed";
if (pid != 0) {
std::exit(EXIT_SUCCESS);
}
LOG(INFO) << "Daemon process started in the background, logging to '"
<< log_file.path << "'";
// We don't want to change to the target user's home directory until we've
// dropped privileges, so change to / to make sure we're not keeping any other
// directory in use.
PCHECK(chdir("/") == 0) << "chdir / failed";
PCHECK(dup2(devnull_fd, STDIN_FILENO) != -1) << "dup2 failed";
PCHECK(dup2(log_file.fd, STDOUT_FILENO) != -1) << "dup2 failed";
PCHECK(dup2(log_file.fd, STDERR_FILENO) != -1) << "dup2 failed";
// Close all file descriptors except stdio and kMessageFd, including any we
// may have inherited.
if (pipe_created) {
base::CloseSuperfluousFds(
{base::InjectionArc(kMessageFd, kMessageFd, false)});
} else {
base::CloseSuperfluousFds(base::InjectiveMultimap());
}
}
} // namespace
int main(int argc, char** argv) {
// Initialize gExecutablePath
DetermineExecutablePath();
// This binary requires elevated privileges.
if (geteuid() != 0) {
std::fprintf(stderr,
"%s not installed setuid root. Host must be started by "
"administrator.\n",
gExecutablePath);
std::exit(EXIT_FAILURE);
}
if (argc < 2 || std::strcmp(argv[1], kStartCommand) != 0) {
PrintUsage();
std::exit(EXIT_FAILURE);
}
// Skip initial args
argc -= 2;
argv += 2;
bool foreground = false;
base::Optional<std::string> user;
std::vector<std::string> script_args;
while (argc > 0) {
if (std::strcmp(argv[0], kForegroundFlag) == 0) {
foreground = true;
argc -= 1;
argv += 1;
} else if (std::strcmp(argv[0], kUserFlag) == 0 && argc >= 2) {
user = std::string(argv[1]);
argc -= 2;
argv += 2;
} else if (std::strcmp(argv[0], "--") == 0) {
argc -= 1;
argv += 1;
// Remaining args get forwarded to python script.
while (argc > 0) {
script_args.emplace_back(argv[0]);
argc -= 1;
argv += 1;
}
} else {
PrintUsage();
std::exit(EXIT_FAILURE);
}
}
uid_t real_uid = getuid();
// Note: This logic is security sensitive. It is imperative that a non-root
// user is not allowed to specify an arbitrary target user.
if (real_uid != 0) {
if (user) {
std::fputs("Target user may not be specified by non-root users.\n",
stderr);
std::exit(EXIT_FAILURE);
}
user = FindCurrentUsername();
} else {
if (!user) {
std::fputs("Target user must be specified when run as root.\n", stderr);
std::exit(EXIT_FAILURE);
}
}
if (!foreground) {
Daemonize();
}
// Daemonizing redirects stdout to a log file, which we want to be owned by
// the target user.
bool chown_stdout = !foreground;
base::Optional<uid_t> match_uid =
real_uid != 0 ? base::make_optional(real_uid) : base::nullopt;
ExecuteSession(std::move(*user), chown_stdout, match_uid,
std::move(script_args));
}